Background
In its default configuration, the Android OS actively optimizes resources to achieve fast activity loading times. The system makes an effort to retain previously displayed activities in memory so they can be loaded quickly if they are needed again in the future. All previous activities are kept in their last known state.
Sometimes though, the system does not have enough memory and has to unload formerly used activities from memory. When it does this, it calls the onSaveInstanceState event, allowing the activity to save its current state (namely the member variables). Later, when the activity is needed again, the system brings it back into memory and calls the onRestoreInstanceState event to allow the activity to restore its state.
The Problem
The save instance state / restore instance state flow is a critical part of the activity lifecycle. Having errors in this area can lead to random app crashes for seemingly “unknown” reasons. As Android programmers, we need to test our apps to make sure that our activities correctly save and restore the state.
Though important, this testing is not easy — it’s difficult. By default, the Android OS manages memory internally, without any input from us. The OS single-handedly decides whether and when to unload your activities from memory and call the instance state events. We can’t schedule it.
(UPDATE: Starting with Ice Cream Sandwich Google has made manual testing easier by providing a debug option for this. You can enable Settings > Developer Options > Apps > Don’t Keep Activities to get the same behavior as described here. This helps for manual testing only… if you need to perform automatic testing keep reading.)
Or can we?
The Solution
SetAlwaysFinish is an Android app that causes the Android OS to predictably trigger the onSaveInstanceState and onRestoreInstanceState events. SetAlwaysFinish instructs the Android OS to unload from memory any activity that moves to the background, immediately triggering the onSaveInstanceState event. Given that the activity is always removed from memory, when the activity is needed again, the Android OS has to load it back into memory and fire the onRestoreInstanceState event.
This change means that the instance state events will always trigger, and they will trigger in a predictable manner. We can now test whether our activities correctly process the instance state events.
Usage
Here is how to use SetAlwaysFinish to test an app for instance state problems:
- Download the SetAlwaysFinish APK from our Github repository and install it on the device. For the install to work, you must make sure that the ‘Unknown Sources’ checkbox is checked under Settings > Applications.
(UPDATE: In addition, if you have a device with JellyBean (Android 4.1) or above, the app will not work without being placed in the /system/app folder on the device. You will need to root your device, use adb shell to mount the system partition as read-write, copy the APK to /system/app, then set the system partition to read only again). - Start SetAlwaysFinish on the test device and make sure that the Always Finish checkbox is checked.
- Run your app and go through every activity. Every time you create a new activity, the Android OS will unload the previous activity and trigger its onSaveInstanceState event. Every time you close an activity (rather than open a new activity on top of it), the Android OS will reload the activity immediately below the current activity in the activity stack and trigger its onRestoreInstanceState event.
- To test the activities at the topmost level in your app’s activity stack, you will need to have another external activity pop on top of them, so their onSaveInstanceState will trigger. The best way to do this is by launching a new app through ADB, as shown below.
# Get shell access via ADB adb shell # Start the calculator application on the device am start -n com.android.calculator2/com.android.calculator2.Calculator
- Once you are done testing your app, you can return to the SetAlwaysFinish app and uncheck the Always Finish checkbox to allow Android to optimize for speed once again.
If you want to find out more about how the SetAlwaysFinish tool works, keep on reading. Otherwise, you have everything you need to test onSaveInstanceState and onRestoreInstanceState on a physical device.
How does it work?
The SetAlwaysFinish app uses hidden Android APIs. The exact setting is called (you guessed it!) AlwaysFinish and it is part of the hidden ActivityManagerNative class. To modify this setting, an app needs to have the ALWAYS_FINISH and WRITE_SETTINGS permissions.
(UPDATE: Starting with JellyBean, the ALWAYS_FINISH permission is granted to system apps only. An easy way for the SetAlwaysFinish app to become a system app is to place its APK in the /system/app directory as described in the usage section above, but keep in mind that this requires a rooted device.)
This setting applies system-wide for all apps running on the device. While you can use it for testing on your own devices, we do not recommend that you modify this setting on your customers’ devices.
The relevant code is located in the two functions below:
// Updates the system Always Finish setting private void writeFinishOptions() { try { // Due to restrictions related to hidden APIs, need to emulate the line below // using reflection: // ActivityManagerNative.getDefault().setAlwaysFinish(mAlwaysFinish); final Class classActivityManagerNative = Class.forName("android.app.ActivityManagerNative"); final Method methodGetDefault = classActivityManagerNative.getMethod("getDefault"); final Method methodSetAlwaysFinish = classActivityManagerNative.getMethod("setAlwaysFinish", new Class[] {boolean.class}); final Object objectInstance = methodGetDefault.invoke(null); methodSetAlwaysFinish.invoke(objectInstance, new Object[]{mAlwaysFinish}); } catch (Exception ex) { showAlert("Could not set always finish:\n\n" + ex, "Error"); } } // Gets the latest AlwaysFinish value from the system and // updates the checkbox private void updateFinishOptions() { mAlwaysFinish = Settings.System.getInt(getContentResolver(), Settings.System.ALWAYS_FINISH_ACTIVITIES, 0) != 0; mAlwaysFinishCB.setChecked(mAlwaysFinish); }
Notice that we use:
android.app.ActivityManagerNative.getDefault().setAlwaysFinish(booleanValue);
to set the setting, and we use:
android.provider.Settings.System.getInt(getContentResolver(), Settings.System.ALWAYS_FINISH_ACTIVITIES, 0)
to read it back.
Conclusion
Feel free to grab a copy of the SetAlwaysFinish source code from our Github repository at:
https://github.com/bricolsoftconsulting/SetAlwaysFinish
If you need the complete APK, you can find it here:
https://github.com/bricolsoftconsulting/SetAlwaysFinish/raw/master/release/SetAlwaysFinish.apk
Credits
This utility draws inspiration (and code) from Google’s DevTools app found in the emulator. The DevTools app contains similar functionality — an ‘Immediately destroy activities’ checkbox under ‘Development Settings’. Unfortunately, due to its use of restricted permissions, the DevTools app does not work outside of the emulator.