diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 02ab3143eed90..3de9a5b1252cb 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -18,7 +18,6 @@ package android.app; import static android.app.ActivityManager.PROCESS_STATE_UNKNOWN; import static android.app.ConfigurationController.createNewConfigAndUpdateIfNotNull; -import static android.app.ConfigurationController.freeTextLayoutCachesIfNeeded; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.servertransaction.ActivityLifecycleItem.ON_CREATE; @@ -31,6 +30,10 @@ 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.view.Display.INVALID_DISPLAY; +import static android.window.ConfigurationHelper.diffPublicWithSizeBuckets; +import static android.window.ConfigurationHelper.freeTextLayoutCachesIfNeeded; +import static android.window.ConfigurationHelper.isDifferentDisplay; +import static android.window.ConfigurationHelper.shouldUpdateResources; import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; @@ -91,7 +94,6 @@ import android.database.sqlite.SQLiteDebug.DbStats; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.HardwareRenderer; -import android.graphics.Rect; import android.graphics.Typeface; import android.hardware.display.DisplayManagerGlobal; import android.inputmethodservice.InputMethodService; @@ -186,6 +188,7 @@ import android.webkit.WebView; import android.window.SizeConfigurationBuckets; import android.window.SplashScreen; import android.window.SplashScreenView; +import android.window.WindowProviderService; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; @@ -5756,7 +5759,7 @@ public final class ActivityThread extends ClientTransactionHandler } @Override - public ArrayList collectComponentCallbacks(boolean includeActivities) { + public ArrayList collectComponentCallbacks(boolean includeUiContexts) { ArrayList callbacks = new ArrayList(); @@ -5765,7 +5768,7 @@ public final class ActivityThread extends ClientTransactionHandler for (int i=0; i= 0; i--) { final Activity a = mActivities.valueAt(i).activity; if (a != null && !a.mFinished) { @@ -5775,11 +5778,15 @@ public final class ActivityThread extends ClientTransactionHandler } final int NSVC = mServices.size(); for (int i=0; i callbacks = - collectComponentCallbacks(true /* includeActivities */); + collectComponentCallbacks(true /* includeUiContexts */); final int N = callbacks.size(); for (int i=0; i callbacks = - collectComponentCallbacks(true /* includeActivities */); + collectComponentCallbacks(true /* includeUiContexts */); final int N = callbacks.size(); for (int i = 0; i < N; i++) { diff --git a/core/java/android/app/ActivityThreadInternal.java b/core/java/android/app/ActivityThreadInternal.java index d91933c0f8173..bc471c4dca187 100644 --- a/core/java/android/app/ActivityThreadInternal.java +++ b/core/java/android/app/ActivityThreadInternal.java @@ -38,5 +38,5 @@ interface ActivityThreadInternal { Application getApplication(); - ArrayList collectComponentCallbacks(boolean includeActivities); + ArrayList collectComponentCallbacks(boolean includeUiContexts); } diff --git a/core/java/android/app/ConfigurationController.java b/core/java/android/app/ConfigurationController.java index f79e0780ecae2..c7702663ec9b9 100644 --- a/core/java/android/app/ConfigurationController.java +++ b/core/java/android/app/ConfigurationController.java @@ -17,17 +17,16 @@ package android.app; import static android.app.ActivityThread.DEBUG_CONFIGURATION; +import static android.window.ConfigurationHelper.freeTextLayoutCachesIfNeeded; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.ComponentCallbacks2; import android.content.Context; -import android.content.pm.ActivityInfo; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; -import android.graphics.Canvas; import android.graphics.HardwareRenderer; import android.inputmethodservice.InputMethodService; import android.os.Build; @@ -228,7 +227,7 @@ class ConfigurationController { } final ArrayList callbacks = - mActivityThread.collectComponentCallbacks(false /* includeActivities */); + mActivityThread.collectComponentCallbacks(false /* includeUiContexts */); freeTextLayoutCachesIfNeeded(configDiff); @@ -326,16 +325,4 @@ class ConfigurationController { return newConfig; } - /** Ask test layout engine to free its caches if there is a locale change. */ - static void freeTextLayoutCachesIfNeeded(int configDiff) { - if (configDiff != 0) { - boolean hasLocaleConfigChange = ((configDiff & ActivityInfo.CONFIG_LOCALE) != 0); - if (hasLocaleConfigChange) { - Canvas.freeTextLayoutCaches(); - if (DEBUG_CONFIGURATION) { - Slog.v(TAG, "Cleared TextLayout Caches"); - } - } - } - } } diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java index f28c760d54d95..9e28e07c64075 100644 --- a/core/java/android/app/ResourcesManager.java +++ b/core/java/android/app/ResourcesManager.java @@ -730,7 +730,7 @@ public class ResourcesManager { * @return true if activity resources override config matches the provided one or they are both * null, false otherwise. */ - boolean isSameResourcesOverrideConfig(@Nullable IBinder activityToken, + public boolean isSameResourcesOverrideConfig(@Nullable IBinder activityToken, @Nullable Configuration overrideConfig) { synchronized (mLock) { final ActivityResources activityResources diff --git a/core/java/android/window/ConfigurationHelper.java b/core/java/android/window/ConfigurationHelper.java new file mode 100644 index 0000000000000..9a079751553f1 --- /dev/null +++ b/core/java/android/window/ConfigurationHelper.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.window; + +import static android.view.Display.INVALID_DISPLAY; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ResourcesManager; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.IBinder; +import android.view.Display; +import android.view.WindowManager; + +/** + * A helper class to maintain {@link android.content.res.Configuration} related methods used both + * in {@link android.app.Activity} and {@link WindowContext}. + * + * @hide + */ +public class ConfigurationHelper { + private ConfigurationHelper() {} + + /** Ask text layout engine to free its caches if there is a locale change. */ + public static void freeTextLayoutCachesIfNeeded(int configDiff) { + if ((configDiff & ActivityInfo.CONFIG_LOCALE) != 0) { + Canvas.freeTextLayoutCaches(); + } + } + + /** + * A helper method to filter out {@link ActivityInfo#CONFIG_SCREEN_SIZE} if the + * {@link Configuration#diffPublicOnly(Configuration) diff} of two {@link Configuration} + * doesn't cross the boundary. + * + * @see SizeConfigurationBuckets#filterDiff(int, Configuration, Configuration, + * SizeConfigurationBuckets) + */ + public static int diffPublicWithSizeBuckets(@Nullable Configuration currentConfig, + @NonNull Configuration newConfig, @Nullable SizeConfigurationBuckets buckets) { + // If current configuration is null, it is definitely different from updated Configuration. + if (currentConfig == null) { + return 0xffffffff; + } + int publicDiff = currentConfig.diffPublicOnly(newConfig); + return SizeConfigurationBuckets.filterDiff(publicDiff, currentConfig, newConfig, buckets); + } + + /** + * Returns {@code true} if the {@link android.content.res.Resources} associated with + * a {@code token} needs to be updated. + * + * @param token A {@link Context#getActivityToken() activity token} or + * {@link Context#getWindowContextToken() window context token} + * @param config The original {@link Configuration} + * @param newConfig The updated Configuration + * @param displayChanged a flag to indicate there's a display change + * @param configChanged a flag to indicate there's a Configuration change. + * + * @see ResourcesManager#updateResourcesForActivity(IBinder, Configuration, int) + */ + public static boolean shouldUpdateResources(IBinder token, @Nullable Configuration config, + @NonNull Configuration newConfig, @NonNull Configuration overrideConfig, + boolean displayChanged, @Nullable Boolean configChanged) { + // The configuration has not yet been initialized. We should update it. + if (config == null) { + return true; + } + // If the token associated context is moved to another display, we should update the + // ResourcesKey. + if (displayChanged) { + return true; + } + // If the new config is the same as the config this Activity is already running with and + // the override config also didn't change, then don't update the Resources + if (!ResourcesManager.getInstance().isSameResourcesOverrideConfig(token, overrideConfig)) { + return true; + } + // If there's a update on WindowConfiguration#mBounds or maxBounds, we should update the + // Resources to make WindowMetrics API report the updated result. + if (shouldUpdateWindowMetricsBounds(config, newConfig)) { + return true; + } + return configChanged == null ? config.diff(newConfig) != 0 : configChanged; + } + + /** + * Returns {@code true} if {@code displayId} is different from {@code newDisplayId}. + * Note that {@link Display#INVALID_DISPLAY} means no difference. + */ + public static boolean isDifferentDisplay(int displayId, int newDisplayId) { + return newDisplayId != INVALID_DISPLAY && displayId != newDisplayId; + } + + // TODO(b/173090263): Remove this method after the improvement of AssetManager and ResourcesImpl + // constructions. + /** + * Returns {@code true} if the metrics reported by {@link android.view.WindowMetrics} APIs + * should be updated. + * + * @see WindowManager#getCurrentWindowMetrics() + * @see WindowManager#getMaximumWindowMetrics() + */ + private static boolean shouldUpdateWindowMetricsBounds(@NonNull Configuration currentConfig, + @NonNull Configuration newConfig) { + final Rect currentBounds = currentConfig.windowConfiguration.getBounds(); + final Rect newBounds = newConfig.windowConfiguration.getBounds(); + + final Rect currentMaxBounds = currentConfig.windowConfiguration.getMaxBounds(); + final Rect newMaxBounds = newConfig.windowConfiguration.getMaxBounds(); + + return !currentBounds.equals(newBounds) || !currentMaxBounds.equals(newMaxBounds); + } +} diff --git a/core/java/android/window/WindowTokenClient.java b/core/java/android/window/WindowTokenClient.java index 6abf5575d3538..04ac044aeff10 100644 --- a/core/java/android/window/WindowTokenClient.java +++ b/core/java/android/window/WindowTokenClient.java @@ -15,6 +15,11 @@ */ package android.window; +import static android.window.ConfigurationHelper.diffPublicWithSizeBuckets; +import static android.window.ConfigurationHelper.freeTextLayoutCachesIfNeeded; +import static android.window.ConfigurationHelper.isDifferentDisplay; +import static android.window.ConfigurationHelper.shouldUpdateResources; + import android.annotation.NonNull; import android.app.ActivityThread; import android.app.IWindowToken; @@ -33,7 +38,7 @@ import java.lang.ref.WeakReference; * {@link Context#getWindowContextToken() the token of non-Activity UI Contexts}. * * @see WindowContext - * @see android.view.IWindowManager#registerWindowContextListener(IBinder, int, int, Bundle) + * @see android.view.IWindowManager#attachWindowContextToDisplayArea(IBinder, int, int, Bundle) * * @hide */ @@ -46,11 +51,13 @@ public class WindowTokenClient extends IWindowToken.Stub { private final ResourcesManager mResourcesManager = ResourcesManager.getInstance(); + private final Configuration mConfiguration = new Configuration(); + /** * Attaches {@code context} to this {@link WindowTokenClient}. Each {@link WindowTokenClient} * can only attach one {@link Context}. *

This method must be called before invoking - * {@link android.view.IWindowManager#registerWindowContextListener(IBinder, int, int, + * {@link android.view.IWindowManager#attachWindowContextToDisplayArea(IBinder, int, int, * Bundle, boolean)}.

* * @param context context to be attached @@ -61,6 +68,7 @@ public class WindowTokenClient extends IWindowToken.Stub { throw new IllegalStateException("Context is already attached."); } mContextRef = new WeakReference<>(context); + mConfiguration.setTo(context.getResources().getConfiguration()); } @Override @@ -69,17 +77,34 @@ public class WindowTokenClient extends IWindowToken.Stub { if (context == null) { return; } - final int currentDisplayId = context.getDisplayId(); - final boolean displayChanged = newDisplayId != currentDisplayId; - final Configuration config = context.getResources().getConfiguration(); - final boolean configChanged = config.diff(newConfig) != 0; - if (displayChanged || configChanged) { + final boolean displayChanged = isDifferentDisplay(context.getDisplayId(), newDisplayId); + final boolean shouldUpdateResources = shouldUpdateResources(this, mConfiguration, + newConfig, newConfig /* overrideConfig */, displayChanged, + null /* configChanged */); + + if (shouldUpdateResources) { // TODO(ag/9789103): update resource manager logic to track non-activity tokens mResourcesManager.updateResourcesForActivity(this, newConfig, newDisplayId); + if (context instanceof WindowContext) { ActivityThread.currentActivityThread().getHandler().post( () -> ((WindowContext) context).dispatchConfigurationChanged(newConfig)); } + + // Dispatch onConfigurationChanged only if there's a significant public change to + // make it compatible with the original behavior. + final Configuration[] sizeConfigurations = context.getResources() + .getSizeConfigurations(); + final SizeConfigurationBuckets buckets = sizeConfigurations != null + ? new SizeConfigurationBuckets(sizeConfigurations) : null; + final int diff = diffPublicWithSizeBuckets(mConfiguration, newConfig, buckets); + + if (context instanceof WindowProviderService && diff != 0) { + ActivityThread.currentActivityThread().getHandler().post(() -> + ((WindowProviderService) context).onConfigurationChanged(newConfig)); + } + freeTextLayoutCachesIfNeeded(diff); + mConfiguration.setTo(newConfig); } if (displayChanged) { context.updateDisplay(newDisplayId); diff --git a/core/tests/mockingcoretests/src/android/window/ConfigurationHelperTest.java b/core/tests/mockingcoretests/src/android/window/ConfigurationHelperTest.java new file mode 100644 index 0000000000000..996d7b435e5aa --- /dev/null +++ b/core/tests/mockingcoretests/src/android/window/ConfigurationHelperTest.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.window; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; + +import android.app.ResourcesManager; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.os.Binder; +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +/** + * Tests for {@link ConfigurationHelper} + * + *

Build/Install/Run: + * atest FrameworksMockingCoreTests:ConfigurationHelperTest + * + *

This test class is a part of Window Manager Service tests and specified in + * {@link com.android.server.wm.test.filters.FrameworksTestsFilter}. + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +@Presubmit +public class ConfigurationHelperTest { + MockitoSession mMockitoSession; + ResourcesManager mResourcesManager; + + @Before + public void setUp() { + mMockitoSession = mockitoSession() + .strictness(Strictness.LENIENT) + .spyStatic(ResourcesManager.class) + .startMocking(); + doReturn(mock(ResourcesManager.class)).when(ResourcesManager::getInstance); + mResourcesManager = ResourcesManager.getInstance(); + } + + @After + public void tearDown() { + mMockitoSession.finishMocking(); + } + + @Test + public void testShouldUpdateResources_NullConfig_ReturnsTrue() { + assertThat(ConfigurationHelper.shouldUpdateResources(new Binder(), null /* config */, + new Configuration(), new Configuration(), false /* displayChanged */, + null /* configChanged */)).isTrue(); + } + + @Test + public void testShouldUpdateResources_DisplayChanged_ReturnsTrue() { + assertThat(ConfigurationHelper.shouldUpdateResources(new Binder(), new Configuration(), + new Configuration(), new Configuration(), true /* displayChanged */, + null /* configChanged */)).isTrue(); + } + + @Test + public void testShouldUpdateResources_DifferentResources_ReturnsTrue() { + doReturn(false).when(mResourcesManager).isSameResourcesOverrideConfig(any(), any()); + + assertThat(ConfigurationHelper.shouldUpdateResources(new Binder(), new Configuration(), + new Configuration(), new Configuration(), false /* displayChanged */, + null /* configChanged */)).isTrue(); + } + + @Test + public void testShouldUpdateResources_DifferentBounds_ReturnsTrue() { + doReturn(true).when(mResourcesManager).isSameResourcesOverrideConfig(any(), any()); + + final Configuration config = new Configuration(); + config.windowConfiguration.setBounds(new Rect(0, 0, 10, 10)); + config.windowConfiguration.setMaxBounds(new Rect(0, 0, 20, 20)); + + final Configuration newConfig = new Configuration(); + newConfig.windowConfiguration.setBounds(new Rect(0, 0, 20, 20)); + newConfig.windowConfiguration.setMaxBounds(new Rect(0, 0, 20, 20)); + + assertThat(ConfigurationHelper.shouldUpdateResources(new Binder(), config, newConfig, + new Configuration(), false /* displayChanged */, null /* configChanged */)) + .isTrue(); + } + + @Test + public void testShouldUpdateResources_SameConfig_ReturnsFalse() { + doReturn(true).when(mResourcesManager).isSameResourcesOverrideConfig(any(), any()); + + final Configuration config = new Configuration(); + final Configuration newConfig = new Configuration(); + + assertThat(ConfigurationHelper.shouldUpdateResources(new Binder(), config, newConfig, + new Configuration(), false /* displayChanged */, null /* configChanged */)) + .isFalse(); + } + + @Test + public void testShouldUpdateResources_DifferentConfig_ReturnsTrue() { + doReturn(true).when(mResourcesManager).isSameResourcesOverrideConfig(any(), any()); + + final Configuration config = new Configuration(); + final Configuration newConfig = new Configuration(); + newConfig.setToDefaults(); + + assertThat(ConfigurationHelper.shouldUpdateResources(new Binder(), config, newConfig, + new Configuration(), false /* displayChanged */, null /* configChanged */)) + .isTrue(); + } + + @Test + public void testShouldUpdateResources_DifferentNonPublicConfig_ReturnsTrue() { + doReturn(true).when(mResourcesManager).isSameResourcesOverrideConfig(any(), any()); + + final Configuration config = new Configuration(); + final Configuration newConfig = new Configuration(); + newConfig.windowConfiguration.setAppBounds(new Rect(0, 0, 10, 10)); + + assertThat(ConfigurationHelper.shouldUpdateResources(new Binder(), config, newConfig, + new Configuration(), false /* displayChanged */, null /* configChanged */)) + .isTrue(); + } + + @Test + public void testShouldUpdateResources_OverrideConfigChanged_ReturnsFalse() { + doReturn(true).when(mResourcesManager).isSameResourcesOverrideConfig(any(), any()); + + final Configuration config = new Configuration(); + final Configuration newConfig = new Configuration(); + final boolean configChanged = true; + + assertThat(ConfigurationHelper.shouldUpdateResources(new Binder(), config, newConfig, + new Configuration(), false /* displayChanged */, configChanged)) + .isEqualTo(configChanged); + } +} diff --git a/tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java b/tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java index bcd6ed73e1333..824f91e1e8267 100644 --- a/tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java +++ b/tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java @@ -45,6 +45,7 @@ public final class FrameworksTestsFilter extends SelectTest { // Test specifications for FrameworksMockingCoreTests. "android.app.activity.ActivityThreadClientTest", "android.view.DisplayTest", + "android.window.ConfigurationHelperTest", // Test specifications for FrameworksCoreTests. "android.app.servertransaction.", // all tests under the package. "android.view.CutoutSpecificationTest", @@ -59,10 +60,8 @@ public final class FrameworksTestsFilter extends SelectTest { "android.view.RoundedCornersTest", "android.view.WindowMetricsTest", "android.view.PendingInsetsControllerTest", - "android.window.WindowContextTest", - "android.window.WindowMetricsHelperTest", + "android.window.", // all tests under the package. "android.app.activity.ActivityThreadTest", - "android.window.WindowContextControllerTest" }; public FrameworksTestsFilter(Bundle testArgs) {