diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index a31aa28208dd7..7c90b717c495b 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -47,7 +47,9 @@ import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.compat.annotation.UnsupportedAppUsage; import android.content.ActivityNotFoundException; +import android.content.ComponentCallbacks; import android.content.ComponentCallbacks2; +import android.content.ComponentCallbacksController; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; @@ -982,6 +984,8 @@ public class Activity extends ContextThemeWrapper @Nullable private DumpableContainerImpl mDumpableContainer; + private ComponentCallbacksController mCallbacksController; + private final WindowControllerCallback mWindowControllerCallback = new WindowControllerCallback() { /** @@ -1323,6 +1327,28 @@ public class Activity extends ContextThemeWrapper } } + @Override + public void registerComponentCallbacks(ComponentCallbacks callback) { + if (CompatChanges.isChangeEnabled(OVERRIDABLE_COMPONENT_CALLBACKS) + && mCallbacksController == null) { + mCallbacksController = new ComponentCallbacksController(); + } + if (mCallbacksController != null) { + mCallbacksController.registerCallbacks(callback); + } else { + super.registerComponentCallbacks(callback); + } + } + + @Override + public void unregisterComponentCallbacks(ComponentCallbacks callback) { + if (mCallbacksController != null) { + mCallbacksController.unregisterCallbacks(callback); + } else { + super.unregisterComponentCallbacks(callback); + } + } + private void dispatchActivityPreCreated(@Nullable Bundle savedInstanceState) { getApplication().dispatchActivityPreCreated(this, savedInstanceState); Object[] callbacks = collectActivityLifecycleCallbacks(); @@ -2668,10 +2694,12 @@ public class Activity extends ContextThemeWrapper if (mUiTranslationController != null) { mUiTranslationController.onActivityDestroyed(); } - if (mDefaultBackCallback != null) { getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(mDefaultBackCallback); } + if (mCallbacksController != null) { + mCallbacksController.clearCallbacks(); + } } /** @@ -2991,6 +3019,9 @@ public class Activity extends ContextThemeWrapper } dispatchActivityConfigurationChanged(); + if (mCallbacksController != null) { + mCallbacksController.dispatchConfigurationChanged(newConfig); + } } /** @@ -3162,12 +3193,18 @@ public class Activity extends ContextThemeWrapper if (DEBUG_LIFECYCLE) Slog.v(TAG, "onLowMemory " + this); mCalled = true; mFragments.dispatchLowMemory(); + if (mCallbacksController != null) { + mCallbacksController.dispatchLowMemory(); + } } public void onTrimMemory(int level) { if (DEBUG_LIFECYCLE) Slog.v(TAG, "onTrimMemory " + this + ": " + level); mCalled = true; mFragments.dispatchTrimMemory(level); + if (mCallbacksController != null) { + mCallbacksController.dispatchTrimMemory(level); + } } /** diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 207412511198e..62dd627320a63 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -36,6 +36,7 @@ import android.annotation.SystemApi; import android.annotation.TestApi; import android.annotation.UiContext; import android.annotation.UserIdInt; +import android.app.Activity; import android.app.ActivityManager; import android.app.BroadcastOptions; import android.app.GameManager; @@ -45,6 +46,8 @@ import android.app.VrManager; import android.app.ambientcontext.AmbientContextManager; import android.app.people.PeopleManager; import android.app.time.TimeManager; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledSince; import android.compat.annotation.UnsupportedAppUsage; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; @@ -85,6 +88,7 @@ import android.view.contentcapture.ContentCaptureManager.ContentCaptureClient; import android.view.textclassifier.TextClassificationManager; import android.window.WindowContext; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.compat.IPlatformCompat; import com.android.internal.compat.IPlatformCompatNative; @@ -111,6 +115,19 @@ import java.util.function.Consumer; * broadcasting and receiving intents, etc. */ public abstract class Context { + /** + * After {@link Build.VERSION_CODES#TIRAMISU}, + * {@link #registerComponentCallbacks(ComponentCallbacks)} will add a {@link ComponentCallbacks} + * to {@link Activity} or {@link ContextWrapper#getBaseContext()} instead of always adding to + * {@link #getApplicationContext()}. + * + * @hide + */ + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) + @VisibleForTesting + public static final long OVERRIDABLE_COMPONENT_CALLBACKS = 193247900L; + /** @hide */ @IntDef(flag = true, prefix = { "MODE_" }, value = { MODE_PRIVATE, diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index 98ced6d7ed5ac..9adf173670391 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -25,8 +25,6 @@ import android.annotation.UiContext; import android.app.IApplicationThread; import android.app.IServiceConnection; import android.app.compat.CompatChanges; -import android.compat.annotation.ChangeId; -import android.compat.annotation.EnabledSince; import android.compat.annotation.UnsupportedAppUsage; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; @@ -73,16 +71,6 @@ public class ContextWrapper extends Context { @UnsupportedAppUsage Context mBase; - /** - * After {@link Build.VERSION_CODES#TIRAMISU}, - * {@link #registerComponentCallbacks(ComponentCallbacks)} will delegate to - * {@link #getBaseContext()} instead of {@link #getApplicationContext()}. - */ - @ChangeId - @EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) - @VisibleForTesting - static final long COMPONENT_CALLBACK_ON_WRAPPER = 193247900L; - /** * A list to store {@link ComponentCallbacks} which * passes to {@link #registerComponentCallbacks(ComponentCallbacks)} before @@ -1355,7 +1343,7 @@ public class ContextWrapper extends Context { public void registerComponentCallbacks(ComponentCallbacks callback) { if (mBase != null) { mBase.registerComponentCallbacks(callback); - } else if (!CompatChanges.isChangeEnabled(COMPONENT_CALLBACK_ON_WRAPPER)) { + } else if (!CompatChanges.isChangeEnabled(OVERRIDABLE_COMPONENT_CALLBACKS)) { super.registerComponentCallbacks(callback); synchronized (mLock) { // Also register ComponentCallbacks to ContextWrapper, so we can find the correct @@ -1397,7 +1385,7 @@ public class ContextWrapper extends Context { mCallbacksRegisteredToSuper.remove(callback); } else if (mBase != null) { mBase.unregisterComponentCallbacks(callback); - } else if (CompatChanges.isChangeEnabled(COMPONENT_CALLBACK_ON_WRAPPER)) { + } else if (CompatChanges.isChangeEnabled(OVERRIDABLE_COMPONENT_CALLBACKS)) { // Throw exception for Application that is targeting S-v2+ throw new IllegalStateException("ComponentCallbacks must be unregistered after " + "this ContextWrapper is attached to a base Context."); diff --git a/core/tests/coretests/src/android/app/activity/RegisterComponentCallbacksTest.java b/core/tests/coretests/src/android/app/activity/RegisterComponentCallbacksTest.java new file mode 100644 index 0000000000000..c1b6666a2d170 --- /dev/null +++ b/core/tests/coretests/src/android/app/activity/RegisterComponentCallbacksTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2022 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.app.activity; + +import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW; +import static android.content.Context.OVERRIDABLE_COMPONENT_CALLBACKS; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import static libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges; + +import android.app.Activity; +import android.app.WindowConfiguration; +import android.app.activity.ActivityThreadTest.TestActivity; +import android.compat.testing.PlatformCompatChangeRule; +import android.content.ComponentCallbacks; +import android.content.TestComponentCallbacks2; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.platform.test.annotations.Presubmit; + +import androidx.test.core.app.ActivityScenario; +import androidx.test.ext.junit.rules.ActivityScenarioRule; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; + +/** + * Test for verifying {@link Activity#registerComponentCallbacks(ComponentCallbacks)} behavior. + * Build/Install/Run: + * atest FrameworksCoreTests:android.app.activity.RegisterComponentCallbacksTest + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +@Presubmit +public class RegisterComponentCallbacksTest { + @Rule + public ActivityScenarioRule rule = new ActivityScenarioRule<>(TestActivity.class); + @Rule + public TestRule compatChangeRule = new PlatformCompatChangeRule(); + + @Test + public void testRegisterComponentCallbacks() { + final ActivityScenario scenario = rule.getScenario(); + final TestComponentCallbacks2 callbacks = new TestComponentCallbacks2(); + final Configuration config = new Configuration(); + config.fontScale = 1.2f; + config.windowConfiguration.setWindowingMode( + WindowConfiguration.WINDOWING_MODE_FREEFORM); + config.windowConfiguration.setBounds(new Rect(0, 0, 100, 100)); + final int trimMemoryLevel = TRIM_MEMORY_RUNNING_LOW; + + scenario.onActivity(activity -> { + // It should be no-op to unregister a ComponentCallbacks without registration. + activity.unregisterComponentCallbacks(callbacks); + + activity.registerComponentCallbacks(callbacks); + // Verify #onConfigurationChanged + activity.onConfigurationChanged(config); + assertThat(callbacks.mConfiguration).isEqualTo(config); + // Verify #onTrimMemory + activity.onTrimMemory(trimMemoryLevel); + assertThat(callbacks.mLevel).isEqualTo(trimMemoryLevel); + // verify #onLowMemory + activity.onLowMemory(); + assertThat(callbacks.mLowMemoryCalled).isTrue(); + + activity.unregisterComponentCallbacks(callbacks); + }); + } + + @DisableCompatChanges(OVERRIDABLE_COMPONENT_CALLBACKS) + @Test + public void testRegisterComponentCallbacksBeforeT() { + final ActivityScenario scenario = rule.getScenario(); + final TestComponentCallbacks2 callbacks = new TestComponentCallbacks2(); + final Configuration config = new Configuration(); + config.fontScale = 1.2f; + config.windowConfiguration.setWindowingMode( + WindowConfiguration.WINDOWING_MODE_FREEFORM); + config.windowConfiguration.setBounds(new Rect(0, 0, 100, 100)); + final int trimMemoryLevel = TRIM_MEMORY_RUNNING_LOW; + + scenario.onActivity(activity -> { + // It should be no-op to unregister a ComponentCallbacks without registration. + activity.unregisterComponentCallbacks(callbacks); + + activity.registerComponentCallbacks(callbacks); + // Verify #onConfigurationChanged + activity.onConfigurationChanged(config); + assertWithMessage("The ComponentCallbacks must be added to #getApplicationContext " + + "before T.").that(callbacks.mConfiguration).isNull(); + // Verify #onTrimMemory + activity.onTrimMemory(trimMemoryLevel); + assertWithMessage("The ComponentCallbacks must be added to #getApplicationContext " + + "before T.").that(callbacks.mLevel).isEqualTo(0); + // verify #onLowMemory + activity.onLowMemory(); + assertWithMessage("The ComponentCallbacks must be added to #getApplicationContext " + + "before T.").that(callbacks.mLowMemoryCalled).isFalse(); + + activity.unregisterComponentCallbacks(callbacks); + }); + } +} diff --git a/core/tests/coretests/src/android/content/ContextWrapperTest.java b/core/tests/coretests/src/android/content/ContextWrapperTest.java index ecaf1f42fbcab..495770245b49e 100644 --- a/core/tests/coretests/src/android/content/ContextWrapperTest.java +++ b/core/tests/coretests/src/android/content/ContextWrapperTest.java @@ -16,7 +16,7 @@ package android.content; -import static android.content.ContextWrapper.COMPONENT_CALLBACK_ON_WRAPPER; +import static android.content.Context.OVERRIDABLE_COMPONENT_CALLBACKS; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; @@ -61,7 +61,7 @@ public class ContextWrapperTest { * register {@link ComponentCallbacks} to {@link ContextWrapper#getApplicationContext} before * {@link ContextWrapper#attachBaseContext(Context)}. */ - @DisableCompatChanges(COMPONENT_CALLBACK_ON_WRAPPER) + @DisableCompatChanges(OVERRIDABLE_COMPONENT_CALLBACKS) @Test public void testRegisterComponentCallbacksWithoutBaseContextBeforeT() { final ContextWrapper wrapper = new TestContextWrapper(null /* base */); diff --git a/core/tests/coretests/src/android/content/TestComponentCallbacks2.java b/core/tests/coretests/src/android/content/TestComponentCallbacks2.java index 6ae7fc44c4ce8..5c8787aeda9c6 100644 --- a/core/tests/coretests/src/android/content/TestComponentCallbacks2.java +++ b/core/tests/coretests/src/android/content/TestComponentCallbacks2.java @@ -20,10 +20,10 @@ import android.content.res.Configuration; import androidx.annotation.NonNull; -class TestComponentCallbacks2 implements ComponentCallbacks2 { - android.content.res.Configuration mConfiguration; - boolean mLowMemoryCalled; - int mLevel; +public class TestComponentCallbacks2 implements ComponentCallbacks2 { + public Configuration mConfiguration = null; + public boolean mLowMemoryCalled = false; + public int mLevel = 0; @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { 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 824f91e1e8267..15a6afc5ff7c9 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 @@ -62,6 +62,7 @@ public final class FrameworksTestsFilter extends SelectTest { "android.view.PendingInsetsControllerTest", "android.window.", // all tests under the package. "android.app.activity.ActivityThreadTest", + "android.app.activity.RegisterComponentCallbacksTest" }; public FrameworksTestsFilter(Bundle testArgs) {