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?) {
super.onCreate(savedInstanceState)
// 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.