Android 12 Wallpaper Changes Recreate Your Activities

Following up on my previous post, and thanks to a crucial tip from cketti, it now appears that we know more about what is going on with wallpaper changes on Android 12: your activities will get recreated, akin to a normal configuration change, but without an opt-out mechanism.

The Original Change

cketti pointed me to this AOSP commit. It provides the engine for what I described above: it forces activities to be recreated:

Activities will be scheduled for restart via the regular life-cycle. This is similar to a configuration change but since ApplicationInfo changes are too low-level we don’t permit apps to opt out.

The catch is those last seven words: “we don’t permit apps to opt out”. This is not an actual configuration change that you can opt out of via android:configChanges.

The original rationale for this was for “runtime resource overlays and split APKs”. I am not 100% certain how split APKs come into play here, but runtime resource overlays (RROs) represent a mechanism for system-wide theme changes. So it is not surprising that Google hooked into that mechanism for Android 12 wallpaper changes.

Confirming the Wallpaper Effect

You can determine after the fact that this has occurred by comparing Configuration objects from before and after the activity recreation. We do not get an onConfigurationChanged() callback in the Activity, because we are not opting out of this recreation process. However, the Configuration object used by our previous activity instance will differ in one key way from the Configuration object used by the replacement activity instance.

For example, here is a trivial ViewModel that holds a Configuration? object:

class MainViewModel : ViewModel() {
    var originalConfiguration: Configuration? = null

In this activity, I save off the original Configuration object in the viewmodel if originalConfiguration is null. Otherwise, I use diff() on Configuration to examine the difference between the original Configuration and the now-current one:

class MainActivity : AppCompatActivity() {
    private val vm: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {

        // other good stuff goes here

        if (vm.originalConfiguration == null) {
            vm.originalConfiguration = resources.configuration
        } else {
            val diff = resources.configuration.diff(vm.originalConfiguration)

            Log.d("WallpaperCCTest", "matches CONFIG_ASSETS_PATHS? ${(diff.toLong() and 0x80000000) != 0L}")

diff() returns a bitmask of the elements of the configuration that differ between two Configuration objects. The magic 0x80000000 comes from the Android source code — it is ActivityInfo.CONFIG_ASSETS_PATHS, a constant that is marked with @hide:

     * Bit in {@link #configChanges} that indicates that the activity
     * can itself handle asset path changes.  Set from the {@link android.R.attr#configChanges}
     * attribute. This is not a core resource configuration, but a higher-level value, so its
     * constant starts at the high bits.
     * @hide We do not want apps handling this yet, but we do need some kind of bit for diffs.
    public static final int CONFIG_ASSETS_PATHS = 0x80000000;

On Android 12, after a wallpaper change, usually the else block is run and the bitmask test comes out true. I say “usually” because in my testing, it seems like there is an occasional hiccup where the wallpaper changes and the activity is left alone and not recreated — I have been getting this perhaps one time in ten, and I have not identified the pattern.

My code shown above compares the current Configuration with the original one — for production use, you probably should track the last-seen Configuration and compare it with the current one.

UPDATE: Based on this comment by Nguyễn Hoài Nam, you can detect this via onConfigurationChanged() in a custom Application subclass. However, that will get called for every configuration change; you would still need to use diff() or similar techniques to determine if the configuration change came from this sort of scenario.

Mitigation Approaches

You could use the above diff() approach to detect that your activity was recreated due to something like a wallpaper change. There are other things that will trigger the same effect, as the RRO system was contributed by Sony back in 2017. Android 12 wallpaper changes may increase the frequency of this occurrence somewhat.

However, there is no obvious way to prevent this destroy-and-recreate cycle from occurring. If anything, the various code comments suggest that Google + Sony specifically precluded the possibility of apps blocking it. That may be why Google elected to go this route rather than introduce a new configuration change type.

Even if your app tries to opt out of all configuration changes — something the Jetpack Compose team at Google has been espousing — your activity can still be destroyed and recreated without process termination. Ideally your app handles that case, even though not all apps do, apparently.