Skip to content

Instantly share code, notes, and snippets.

@xrigau
Last active October 22, 2022 02:36
Show Gist options
  • Save xrigau/11284124 to your computer and use it in GitHub Desktop.
Save xrigau/11284124 to your computer and use it in GitHub Desktop.
Disable animations for Espresso tests - run with `gradle cATDD`
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.novoda.espresso">
<!-- For espresso testing purposes, this is removed in live builds, but not in dev builds -->
<uses-permission android:name="android.permission.SET_ANIMATION_SCALE" />
<!-- ... -->
</manifest>
apply plugin: 'android'
android {
// ...
productFlavors {
dev {
// The one for development/testing
}
live {
// The flavour for releasing
}
}
}
// ...
task grantAnimationPermission(type: Exec, dependsOn: 'installDevDebug') { // or install{productFlavour}{buildType}
commandLine "adb shell pm grant $android.defaultConfig.packageName android.permission.SET_ANIMATION_SCALE".split(' ')
}
tasks.whenTaskAdded { task ->
if (task.name.startsWith('connectedAndroidTest')) {
task.dependsOn grantAnimationPermission
}
}
def copyAndReplaceText(source, dest, Closure replaceText) {
dest.write(replaceText(source.text))
}
// Override Data in Manifest - This can be done using different Manifest files for each flavor, this way there's no need to modify the manifest
android.applicationVariants.all { variant ->
if (variant.name.startsWith('dev')) { // Where dev is the one you'll use to run Espresso tests
System.out.println("Not removing the SET_ANIMATION_SCALE permission for $variant.name")
return
}
System.out.println("Removing the SET_ANIMATION_SCALE permission for $variant.name")
variant.processManifest.doLast {
copyAndReplaceText(manifestOutputFile, manifestOutputFile) {
def replaced = it.replace('<uses-permission android:name="android.permission.SET_ANIMATION_SCALE"/>', '');
if (replaced.contains('SET_ANIMATION_SCALE')) {
// For security, imagine an extra space is added before closing tag, then the replace would fail - TODO use regex
throw new RuntimeException("Don't ship with this permission! android.permission.SET_ANIMATION_SCALE")
}
replaced
}
}
}
public class MyInstrumentationTestCase extends ActivityInstrumentationTestCase2<MyActivity> {
private SystemAnimations systemAnimations;
public MyInstrumentationTestCase() {
super(MyActivity.class);
}
@Override
protected void setUp() throws Exception {
super.setUp();
systemAnimations = new SystemAnimations(getInstrumentation().getContext());
systemAnimations.disableAll();
getActivity();
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
systemAnimations.enableAll();
}
}
class SystemAnimations {
private static final String ANIMATION_PERMISSION = "android.permission.SET_ANIMATION_SCALE";
private static final float DISABLED = 0.0f;
private static final float DEFAULT = 1.0f;
private final Context context;
SystemAnimations(Context context) {
this.context = context;
}
void disableAll() {
int permStatus = context.checkCallingOrSelfPermission(ANIMATION_PERMISSION);
if (permStatus == PackageManager.PERMISSION_GRANTED) {
setSystemAnimationsScale(DISABLED);
}
}
void enableAll() {
int permStatus = context.checkCallingOrSelfPermission(ANIMATION_PERMISSION);
if (permStatus == PackageManager.PERMISSION_GRANTED) {
setSystemAnimationsScale(DEFAULT);
}
}
private void setSystemAnimationsScale(float animationScale) {
try {
Class<?> windowManagerStubClazz = Class.forName("android.view.IWindowManager$Stub");
Method asInterface = windowManagerStubClazz.getDeclaredMethod("asInterface", IBinder.class);
Class<?> serviceManagerClazz = Class.forName("android.os.ServiceManager");
Method getService = serviceManagerClazz.getDeclaredMethod("getService", String.class);
Class<?> windowManagerClazz = Class.forName("android.view.IWindowManager");
Method setAnimationScales = windowManagerClazz.getDeclaredMethod("setAnimationScales", float[].class);
Method getAnimationScales = windowManagerClazz.getDeclaredMethod("getAnimationScales");
IBinder windowManagerBinder = (IBinder) getService.invoke(null, "window");
Object windowManagerObj = asInterface.invoke(null, windowManagerBinder);
float[] currentScales = (float[]) getAnimationScales.invoke(windowManagerObj);
for (int i = 0; i < currentScales.length; i++) {
currentScales[i] = animationScale;
}
setAnimationScales.invoke(windowManagerObj, new Object[]{currentScales});
} catch (Exception e) {
Log.e("SystemAnimations", "Could not change animation scale to " + animationScale + " :'(");
}
}
}
@xrigau
Copy link
Author

xrigau commented Apr 25, 2014

What this does:

  • Creates 2 build flavours, one will be used for Espresso tests and the other for releasing the app
  • Adds the SET_ANIMATION_SCALE permission to AndroidManifest
  • Remove the SET_ANIMATION_SCALE permission from the releasing flavour's generated Manifest.
  • Runs a task that installs the APK and runs the adb command to grant permission to the app to change animation scale.
  • Uses a custom InstrumentationTestRunner that disables before calling an Activity's onCreate and enables animations after calling an Activity's onDestroy using the code provided above.

There's a lot of room for improvements, so please feel free to help improving this.

@devisnik
Copy link

Great use case to enhance the gradle-android-command plugin.

Could you please explain why/how this needed for espresso?

@blundell
Copy link

👍 important to not add that permission to the live release lol!

@xrigau
Copy link
Author

xrigau commented Apr 25, 2014

@devisnik Yes that would make sense. I made this to disable system animations programmatically, since animations cause flaky tests (sometimes a test would fail just because an animation is running). The idea is that we don't need to do it manually on each device, instead we can disable before tests and enable afterwards - should this be made during setUp & tearDown instead?

Note that I took a snippet from the Espresso wiki and worked from there: https://code.google.com/p/android-test-kit/wiki/DisablingAnimations

@xrigau
Copy link
Author

xrigau commented Apr 29, 2014

The manifest parsing can be avoided using different AndroidManifest files for each flavour and using manifest merging

@jonathanstiansen
Copy link

In fact, flavours are unnecessary for just testing (unless you are testing your production app, which I guess makes a lot of sense), since it can be added to the 'debug' folder. It will use debug to run tests.

@solcott
Copy link

solcott commented Dec 22, 2014

in grantAnimationPermission packageName should be changed to applicationId

@denisk20
Copy link

No need to put SET_ANIMATION_SCALE permission into the main manifest and remove it afterwards. Just create a build flavor for integration tests and put the manifest with a single permission into it - it will be merged by the manifest merger when integration tests are be run. For example:
build.gradle

productFlavors {
    /**
     * This flavor is to be run only using connectedAndroidTestExtended
     */
    extended {}
}

src/extended/AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.mypackage">
    <uses-permission android:name="android.permission.SET_ANIMATION_SCALE"/>
</manifest>

Run integration tests with

./gradlew connectedAndroidTestExtended

Check this for more details:
http://stackoverflow.com/q/27826935/369317

@daj
Copy link

daj commented May 5, 2015

I had to make quite a lot of changes to get this Gist to work with Android Studio 1.1.0 and Espresso 2.0. Here's my updated Gist: https://gist.github.com/daj/7b48f1b8a92abf960e7b

@danielgomezrico
Copy link

@tasomaniac
Copy link

tasomaniac commented Jan 4, 2016

I know it is not the same thing but it can also be achieved by just the following adb commands. (Only for API 17 and above)

  • adb shell settings put global window_animation_scale 0
  • adb shell settings put global transition_animation_scale 0
  • adb shell settings put global animator_duration_scale 0

Here how it looks like when it is applied to Travis.
tasomaniac/android-topeka@e99e6f3#diff-354f30a63fb0907d4ad57269548329e3R19

@robertofrontado
Copy link

@timrijckaert
Copy link

timrijckaert commented Dec 3, 2016

Thanks for this I created a custom testrule with the help of this Gist

@tasomaniac
Copy link

tasomaniac commented Jan 2, 2017

Disabling system animations support has been added to Novoda Android Command Plugin as well.
https://github.com/novoda/gradle-android-command-plugin/

With the plugin, you can use ./gradlew disableSystemAnimations

You can also put this snippet into you build.gradle file to automatically disable/enable animations before/after connected tests.

afterEvaluate {
  if (tasks.findByPath("connectedAndroidTest") != null) {
    connectedAndroidTest.dependsOn disableSystemAnimations
    connectedAndroidTest.finalizedBy enableSystemAnimations
  }
}

Note: null check was necessary for us because of the latest changes in instant run. It doesn't create all the configurations in advance anymore.

@twyatt
Copy link

twyatt commented Jan 3, 2017

@tasomaniac Not sure what I'm doing wrong but issuing any of the adb commands you listed in your Jan 4, 2016 comment doesn't have any effect until I restart my emulator?

Tried with Nexus 5 API 21 AVD and with Genymotion running an API 21 image; neither would change animation scales using adb until I restarted the emulator.

@Kisty
Copy link

Kisty commented Jun 2, 2017

Have you seen https://github.com/linkedin/test-butler? Does the same thing but automatically, just need to install a tiny app on emulator.

Gradle support to auto install the test helper app works from 3.0.0-alpha1 or using a little gradle script

@eighthave
Copy link

Why not just make a manifest in the existing androidTest flavor? I.e. app/src/androidTest/AndroidManifest.xml. Then you don't need any special build flavor to include this.

@eighthave
Copy link

I see, because the disable animations thing is happening in the app, rather than in the instrumentation. If someone knows how to run it in the instrumentation, it would eliminate the need for build flavors and other gradle tricks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment