From 5fdd2f3ecfbb537d3c0421c0f1371a22796b1840 Mon Sep 17 00:00:00 2001 From: William Xiao Date: Tue, 21 Mar 2023 17:34:21 -0700 Subject: Implement flag to not relaunch on dock config change for apps without -desk resources Activities are relaunched when they encounter an unhandled configuration change. When a device is docked, this is surfaced to activities as a uiMode change to UI_MODE_TYPE_DESK. Some apps don't handle this configuration change by default, leading to a poor user experience, such as video playback stopping when docking. This change adds a flag that modifies this behavior and does not relaunch activities if the device is docked or undocked and the app does not have desk resources. Apps that have desk resources likely already either handle the uiMode configuration update or expect to be relaunched, so we want to preserve the default behavior for them. This flag is turned off by default. Bug: 266924897 Test: atest WmTests:ActivityRecordTests Change-Id: Ib9eeb251f803e41ff8fd0ed56f1ea8d52e67ba81 Merged-In: Ib9eeb251f803e41ff8fd0ed56f1ea8d52e67ba81 --- core/java/android/app/ActivityThread.java | 38 ++++++++++++- core/java/android/content/res/AssetManager.java | 11 ++++ core/java/android/content/res/Resources.java | 5 ++ core/java/android/content/res/ResourcesImpl.java | 4 ++ core/jni/android_util_AssetManager.cpp | 15 ++++- core/res/res/values/config.xml | 6 ++ core/res/res/values/symbols.xml | 1 + .../java/com/android/server/wm/ActivityRecord.java | 65 +++++++++++++++++++++- .../android/server/wm/WindowManagerService.java | 12 ++++ .../com/android/server/wm/ActivityRecordTests.java | 57 +++++++++++++++++++ 10 files changed, 210 insertions(+), 4 deletions(-) diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index b26504dd1c49..5985d6e93107 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -29,6 +29,8 @@ import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP; import static android.app.servertransaction.ActivityLifecycleItem.PRE_ON_CREATE; import static android.content.ContentResolver.DEPRECATE_DATA_COLUMNS; import static android.content.ContentResolver.DEPRECATE_DATA_PREFIX; +import static android.content.res.Configuration.UI_MODE_TYPE_DESK; +import static android.content.res.Configuration.UI_MODE_TYPE_MASK; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static android.window.ConfigurationHelper.freeTextLayoutCachesIfNeeded; @@ -194,6 +196,7 @@ import android.window.SplashScreen; import android.window.SplashScreenView; import android.window.WindowProviderService; +import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.IVoiceInteractor; @@ -5890,9 +5893,21 @@ public final class ActivityThread extends ClientTransactionHandler final boolean shouldUpdateResources = hasPublicResConfigChange || shouldUpdateResources(activityToken, currentResConfig, newConfig, amOverrideConfig, movedToDifferentDisplay, hasPublicResConfigChange); + + // TODO(b/266924897): temporary workaround, remove for U. + boolean skipActivityRelaunchWhenDocking = activity.getResources().getBoolean( + R.bool.config_skipActivityRelaunchWhenDocking); + int handledConfigChanges = activity.mActivityInfo.getRealConfigChanged(); + if (skipActivityRelaunchWhenDocking && onlyDeskInUiModeChanged(activity.mCurrentConfig, + newConfig)) { + // If we're not relaunching this activity when docking, we should send the configuration + // changed event. Pretend as if the activity is handling uiMode config changes in its + // manifest so that we'll report any dock changes. + handledConfigChanges |= ActivityInfo.CONFIG_UI_MODE; + } + final boolean shouldReportChange = shouldReportChange(activity.mCurrentConfig, newConfig, - r != null ? r.mSizeConfigurations : null, - activity.mActivityInfo.getRealConfigChanged()); + r != null ? r.mSizeConfigurations : null, handledConfigChanges); // Nothing significant, don't proceed with updating and reporting. if (!shouldUpdateResources && !shouldReportChange) { return null; @@ -5938,6 +5953,25 @@ public final class ActivityThread extends ClientTransactionHandler return configToReport; } + /** + * Returns true if the uiMode configuration changed, and desk mode + * ({@link android.content.res.Configuration#UI_MODE_TYPE_DESK}) was the only change to uiMode. + */ + private boolean onlyDeskInUiModeChanged(Configuration oldConfig, Configuration newConfig) { + boolean deskModeChanged = isInDeskUiMode(oldConfig) != isInDeskUiMode(newConfig); + + // UI mode contains fields other than the UI mode type, so determine if any other fields + // changed. + boolean uiModeOtherFieldsChanged = + (oldConfig.uiMode & ~UI_MODE_TYPE_MASK) != (newConfig.uiMode & ~UI_MODE_TYPE_MASK); + + return deskModeChanged && !uiModeOtherFieldsChanged; + } + + private static boolean isInDeskUiMode(Configuration config) { + return (config.uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_DESK; + } + /** * Returns {@code true} if {@link Activity#onConfigurationChanged(Configuration)} should be * dispatched. diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java index c8bbb0c1994d..6f09e79bdba9 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/android/content/res/AssetManager.java @@ -1452,6 +1452,16 @@ public final class AssetManager implements AutoCloseable { } } + /** + * @hide + */ + Configuration[] getSizeAndUiModeConfigurations() { + synchronized (this) { + ensureValidLocked(); + return nativeGetSizeAndUiModeConfigurations(mObject); + } + } + /** * Change the configuration used when retrieving resources. Not for use by * applications. @@ -1603,6 +1613,7 @@ public final class AssetManager implements AutoCloseable { private static native @Nullable String nativeGetResourceEntryName(long ptr, @AnyRes int resid); private static native @Nullable String[] nativeGetLocales(long ptr, boolean excludeSystem); private static native @Nullable Configuration[] nativeGetSizeConfigurations(long ptr); + private static native @Nullable Configuration[] nativeGetSizeAndUiModeConfigurations(long ptr); private static native void nativeSetResourceResolutionLoggingEnabled(long ptr, boolean enabled); private static native @Nullable String nativeGetLastResourceResolution(long ptr); diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index a03286d3ec6f..9b169499b41f 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -2207,6 +2207,11 @@ public class Resources { return mResourcesImpl.getSizeConfigurations(); } + /** @hide */ + public Configuration[] getSizeAndUiModeConfigurations() { + return mResourcesImpl.getSizeAndUiModeConfigurations(); + } + /** * Return the compatibility mode information for the application. * The returned object should be treated as read-only. diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java index ff072916292b..3bb237ac1228 100644 --- a/core/java/android/content/res/ResourcesImpl.java +++ b/core/java/android/content/res/ResourcesImpl.java @@ -203,6 +203,10 @@ public class ResourcesImpl { return mAssets.getSizeConfigurations(); } + Configuration[] getSizeAndUiModeConfigurations() { + return mAssets.getSizeAndUiModeConfigurations(); + } + CompatibilityInfo getCompatibilityInfo() { return mDisplayAdjustments.getCompatibilityInfo(); } diff --git a/core/jni/android_util_AssetManager.cpp b/core/jni/android_util_AssetManager.cpp index 8c23b214b8fe..206ad17e3c4b 100644 --- a/core/jni/android_util_AssetManager.cpp +++ b/core/jni/android_util_AssetManager.cpp @@ -93,6 +93,7 @@ static struct configuration_offsets_t { jfieldID mScreenWidthDpOffset; jfieldID mScreenHeightDpOffset; jfieldID mScreenLayoutOffset; + jfieldID mUiMode; } gConfigurationOffsets; static struct arraymap_offsets_t { @@ -1027,10 +1028,11 @@ static jobject ConstructConfigurationObject(JNIEnv* env, const ResTable_config& env->SetIntField(result, gConfigurationOffsets.mScreenWidthDpOffset, config.screenWidthDp); env->SetIntField(result, gConfigurationOffsets.mScreenHeightDpOffset, config.screenHeightDp); env->SetIntField(result, gConfigurationOffsets.mScreenLayoutOffset, config.screenLayout); + env->SetIntField(result, gConfigurationOffsets.mUiMode, config.uiMode); return result; } -static jobjectArray NativeGetSizeConfigurations(JNIEnv* env, jclass /*clazz*/, jlong ptr) { +static jobjectArray GetSizeAndUiModeConfigurations(JNIEnv* env, jlong ptr) { ScopedLock assetmanager(AssetManagerFromLong(ptr)); auto configurations = assetmanager->GetResourceConfigurations(true /*exclude_system*/, false /*exclude_mipmap*/); @@ -1057,6 +1059,14 @@ static jobjectArray NativeGetSizeConfigurations(JNIEnv* env, jclass /*clazz*/, j return array; } +static jobjectArray NativeGetSizeConfigurations(JNIEnv* env, jclass /*clazz*/, jlong ptr) { + return GetSizeAndUiModeConfigurations(env, ptr); +} + +static jobjectArray NativeGetSizeAndUiModeConfigurations(JNIEnv* env, jclass /*clazz*/, jlong ptr) { + return GetSizeAndUiModeConfigurations(env, ptr); +} + static jintArray NativeAttributeResolutionStack( JNIEnv* env, jclass /*clazz*/, jlong ptr, jlong theme_ptr, jint xml_style_res, @@ -1487,6 +1497,8 @@ static const JNINativeMethod gAssetManagerMethods[] = { {"nativeGetLocales", "(JZ)[Ljava/lang/String;", (void*)NativeGetLocales}, {"nativeGetSizeConfigurations", "(J)[Landroid/content/res/Configuration;", (void*)NativeGetSizeConfigurations}, + {"nativeGetSizeAndUiModeConfigurations", "(J)[Landroid/content/res/Configuration;", + (void*)NativeGetSizeAndUiModeConfigurations}, // Style attribute related methods. {"nativeAttributeResolutionStack", "(JJIII)[I", (void*)NativeAttributeResolutionStack}, @@ -1565,6 +1577,7 @@ int register_android_content_AssetManager(JNIEnv* env) { GetFieldIDOrDie(env, configurationClass, "screenHeightDp", "I"); gConfigurationOffsets.mScreenLayoutOffset = GetFieldIDOrDie(env, configurationClass, "screenLayout", "I"); + gConfigurationOffsets.mUiMode = GetFieldIDOrDie(env, configurationClass, "uiMode", "I"); jclass arrayMapClass = FindClassOrDie(env, "android/util/ArrayMap"); gArrayMapOffsets.classObject = MakeGlobalRefOrDie(env, arrayMapClass); diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index dafa0ad7989f..6ab6557d6c89 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -5410,6 +5410,12 @@ treatment for stretched issues in camera viewfinder. --> false + + false + false diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 591ba5feeee9..c6c20f9430c8 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4485,6 +4485,7 @@ + diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index de5defa1bcd4..aad078b89e24 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -100,6 +100,7 @@ import static android.content.res.Configuration.EMPTY; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.content.res.Configuration.ORIENTATION_UNDEFINED; +import static android.content.res.Configuration.UI_MODE_TYPE_DESK; import static android.content.res.Configuration.UI_MODE_TYPE_MASK; import static android.content.res.Configuration.UI_MODE_TYPE_VR_HEADSET; import static android.os.Build.VERSION_CODES.HONEYCOMB; @@ -827,6 +828,13 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A /** Whether the input to this activity will be dropped during the current playing animation. */ private boolean mIsInputDroppedForAnimation; + /** + * Whether the application has desk mode resources. Calculated and cached when + * {@link #hasDeskResources()} is called. + */ + @Nullable + private Boolean mHasDeskResources; + boolean mHandleExitSplashScreen; @TransferSplashScreenState int mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_IDLE; @@ -9419,7 +9427,14 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A configChanged |= CONFIG_UI_MODE; } - return (changes&(~configChanged)) != 0; + // TODO(b/274944389): remove workaround after long-term solution is implemented + // Don't restart due to desk mode change if the app does not have desk resources. + if (mWmService.mSkipActivityRelaunchWhenDocking && onlyDeskInUiModeChanged(changesConfig) + && !hasDeskResources()) { + configChanged |= CONFIG_UI_MODE; + } + + return (changes & (~configChanged)) != 0; } /** @@ -9432,6 +9447,50 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A != isInVrUiMode(lastReportedConfig)); } + /** + * Returns true if the uiMode configuration changed, and desk mode + * ({@link android.content.res.Configuration#UI_MODE_TYPE_DESK}) was the only change to uiMode. + */ + private boolean onlyDeskInUiModeChanged(Configuration lastReportedConfig) { + final Configuration currentConfig = getConfiguration(); + + boolean deskModeChanged = isInDeskUiMode(currentConfig) != isInDeskUiMode( + lastReportedConfig); + // UI mode contains fields other than the UI mode type, so determine if any other fields + // changed. + boolean uiModeOtherFieldsChanged = + (currentConfig.uiMode & ~UI_MODE_TYPE_MASK) != (lastReportedConfig.uiMode + & ~UI_MODE_TYPE_MASK); + + return deskModeChanged && !uiModeOtherFieldsChanged; + } + + /** + * Determines whether or not the application has desk mode resources. + */ + boolean hasDeskResources() { + if (mHasDeskResources != null) { + // We already determined this, return the cached value. + return mHasDeskResources; + } + + mHasDeskResources = false; + try { + Resources packageResources = mAtmService.mContext.createPackageContextAsUser( + packageName, 0, UserHandle.of(mUserId)).getResources(); + for (Configuration sizeConfiguration : + packageResources.getSizeAndUiModeConfigurations()) { + if (isInDeskUiMode(sizeConfiguration)) { + mHasDeskResources = true; + break; + } + } + } catch (PackageManager.NameNotFoundException e) { + Slog.w(TAG, "Exception thrown during checking for desk resources " + this, e); + } + return mHasDeskResources; + } + private int getConfigurationChanges(Configuration lastReportedConfig) { // Determine what has changed. May be nothing, if this is a config that has come back from // the app after going idle. In that case we just want to leave the official config object @@ -9727,6 +9786,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return (config.uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_VR_HEADSET; } + private static boolean isInDeskUiMode(Configuration config) { + return (config.uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_DESK; + } + String getProcessName() { return info.applicationInfo.processName; } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index e6c1e75da581..02f3f9b96b74 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -551,6 +551,16 @@ public class WindowManagerService extends IWindowManager.Stub // everything else on screen). Otherwise, it will be put under always-on-top stacks. final boolean mAssistantOnTopOfDream; + /** + * If true, don't relaunch the activity upon receiving a configuration change to transition to + * or from the {@link UI_MODE_TYPE_DESK} uiMode, which is sent when docking. The configuration + * change will still be sent regardless, only the relaunch is skipped. Apps with desk resources + * are exempt from this and will behave like normal, since they may expect the relaunch upon the + * desk uiMode change. + */ + @VisibleForTesting + boolean mSkipActivityRelaunchWhenDocking; + final boolean mLimitedAlphaCompositing; final int mMaxUiWidth; @@ -1218,6 +1228,8 @@ public class WindowManagerService extends IWindowManager.Stub com.android.internal.R.bool.config_perDisplayFocusEnabled); mAssistantOnTopOfDream = context.getResources().getBoolean( com.android.internal.R.bool.config_assistantOnTopOfDream); + mSkipActivityRelaunchWhenDocking = context.getResources() + .getBoolean(R.bool.config_skipActivityRelaunchWhenDocking); mLetterboxConfiguration = new LetterboxConfiguration( // Using SysUI context to have access to Material colors extracted from Wallpaper. diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index ea50179746d8..302c284c4915 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -45,6 +45,7 @@ import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.content.res.Configuration.UI_MODE_TYPE_DESK; import static android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS; import static android.os.Process.NOBODY_UID; import static android.view.Display.DEFAULT_DISPLAY; @@ -489,6 +490,62 @@ public class ActivityRecordTests extends WindowTestsBase { .scheduleTransaction(any(), any(), isA(ActivityConfigurationChangeItem.class)); } + @Test + public void testDeskModeChange_doesNotRelaunch() throws RemoteException { + mWm.mSkipActivityRelaunchWhenDocking = true; + + final ActivityRecord activity = createActivityWithTask(); + // The activity will already be relaunching out of the gate, finish the relaunch so we can + // test properly. + activity.finishRelaunching(); + // Clear out any calls to scheduleTransaction from launching the activity. + reset(mAtm.getLifecycleManager()); + + final Task task = activity.getTask(); + activity.setState(RESUMED, "Testing"); + + // Send a desk UI mode config update. + final Configuration newConfig = new Configuration(task.getConfiguration()); + newConfig.uiMode |= UI_MODE_TYPE_DESK; + task.onRequestedOverrideConfigurationChanged(newConfig); + ensureActivityConfiguration(activity); + + // The activity shouldn't start relaunching since it doesn't have any desk resources. + assertFalse(activity.isRelaunching()); + + // The configuration change is still sent to the activity, even if it doesn't relaunch. + final ActivityConfigurationChangeItem expected = + ActivityConfigurationChangeItem.obtain(newConfig); + verify(mAtm.getLifecycleManager()).scheduleTransaction( + eq(activity.app.getThread()), eq(activity.token), eq(expected)); + } + + @Test + public void testDeskModeChange_relaunchesWithDeskResources() { + mWm.mSkipActivityRelaunchWhenDocking = true; + + final ActivityRecord activity = createActivityWithTask(); + // The activity will already be relaunching out of the gate, finish the relaunch so we can + // test properly. + activity.finishRelaunching(); + + // Activities with desk resources should get relaunched when a UI_MODE_TYPE_DESK change + // comes in. + doReturn(true).when(activity).hasDeskResources(); + + final Task task = activity.getTask(); + activity.setState(RESUMED, "Testing"); + + // Send a desk UI mode config update. + final Configuration newConfig = new Configuration(task.getConfiguration()); + newConfig.uiMode |= UI_MODE_TYPE_DESK; + task.onRequestedOverrideConfigurationChanged(newConfig); + ensureActivityConfiguration(activity); + + // The activity will relaunch since it has desk resources. + assertTrue(activity.isRelaunching()); + } + @Test public void testSetRequestedOrientationUpdatesConfiguration() throws Exception { final ActivityRecord activity = new ActivityBuilder(mAtm) -- cgit v1.2.3-59-g8ed1b