diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index 805e499bba465..052f643127320 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -24,6 +24,9 @@ import android.annotation.TestApi; 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; @@ -47,12 +50,16 @@ import android.view.DisplayAdjustments; import android.view.WindowManager.LayoutParams.WindowType; import android.view.autofill.AutofillManager.AutofillClient; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; + import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.concurrent.Executor; @@ -66,6 +73,31 @@ 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 + * {@link #attachBaseContext(Context)}. + * It is to provide compatibility behavior for Application targeted prior to + * {@link Build.VERSION_CODES#TIRAMISU}. + * + * @hide + */ + @GuardedBy("mLock") + @VisibleForTesting + public List mCallbacksRegisteredToSuper; + + private final Object mLock = new Object(); + public ContextWrapper(Context base) { mBase = base; } @@ -1301,4 +1333,77 @@ public class ContextWrapper extends Context { } return mBase.isConfigurationContext(); } + + /** + * Add a new {@link ComponentCallbacks} to the base application of the + * Context, which will be called at the same times as the ComponentCallbacks + * methods of activities and other components are called. Note that you + * must be sure to use {@link #unregisterComponentCallbacks} when + * appropriate in the future; this will not be removed for you. + *

+ * After {@link Build.VERSION_CODES#TIRAMISU}, the {@link ComponentCallbacks} will be registered + * to {@link #getBaseContext() the base Context}, and can be only used after + * {@link #attachBaseContext(Context)}. Users can still call to + * {@code getApplicationContext().registerComponentCallbacks(ComponentCallbacks)} to add + * {@link ComponentCallbacks} to the base application. + * + * @param callback The interface to call. This can be either a + * {@link ComponentCallbacks} or {@link ComponentCallbacks2} interface. + * @throws IllegalStateException if this method calls before {@link #attachBaseContext(Context)} + */ + @Override + public void registerComponentCallbacks(ComponentCallbacks callback) { + if (mBase != null) { + mBase.registerComponentCallbacks(callback); + } else if (!CompatChanges.isChangeEnabled(COMPONENT_CALLBACK_ON_WRAPPER)) { + super.registerComponentCallbacks(callback); + synchronized (mLock) { + // Also register ComponentCallbacks to ContextWrapper, so we can find the correct + // Context to unregister it for compatibility. + if (mCallbacksRegisteredToSuper == null) { + mCallbacksRegisteredToSuper = new ArrayList<>(); + } + mCallbacksRegisteredToSuper.add(callback); + } + } else { + // Throw exception for Application targeting T+ + throw new IllegalStateException("ComponentCallbacks must be registered after " + + "this ContextWrapper is attached to a base Context."); + } + } + + /** + * Remove a {@link ComponentCallbacks} object that was previously registered + * with {@link #registerComponentCallbacks(ComponentCallbacks)}. + *

+ * After {@link Build.VERSION_CODES#TIRAMISU}, the {@link ComponentCallbacks} will be + * unregistered to {@link #getBaseContext() the base Context}, and can be only used after + * {@link #attachBaseContext(Context)} + *

+ * + * @param callback The interface to call. This can be either a + * {@link ComponentCallbacks} or {@link ComponentCallbacks2} interface. + * @throws IllegalStateException if this method calls before {@link #attachBaseContext(Context)} + */ + @Override + public void unregisterComponentCallbacks(ComponentCallbacks callback) { + // It usually means the ComponentCallbacks is registered before this ContextWrapper attaches + // to a base Context and Application is targeting prior to S-v2. We should unregister the + // ComponentCallbacks to the Application Context instead to prevent leak. + synchronized (mLock) { + if (mCallbacksRegisteredToSuper != null + && mCallbacksRegisteredToSuper.contains(callback)) { + super.unregisterComponentCallbacks(callback); + mCallbacksRegisteredToSuper.remove(callback); + } else if (mBase != null) { + mBase.unregisterComponentCallbacks(callback); + } else if (CompatChanges.isChangeEnabled(COMPONENT_CALLBACK_ON_WRAPPER)) { + // 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."); + } + } + // Do nothing if the callback hasn't been registered to Application Context by + // super.unregisterComponentCallbacks() for Application that is targeting prior to T. + } } diff --git a/core/java/android/content/TEST_MAPPING b/core/java/android/content/TEST_MAPPING index 7f1d0d1d05cfe..5bb845d5a1a1d 100644 --- a/core/java/android/content/TEST_MAPPING +++ b/core/java/android/content/TEST_MAPPING @@ -32,6 +32,9 @@ }, { "include-filter": "android.content.ComponentCallbacksControllerTest" + }, + { + "include-filter": "android.content.ContextWrapperTest" } ], "file_patterns": ["(/|^)Context.java", "(/|^)ContextWrapper.java", "(/|^)ComponentCallbacksController.java"] diff --git a/core/java/android/window/WindowContext.java b/core/java/android/window/WindowContext.java index cfccb712127e9..fdaab66f2de3c 100644 --- a/core/java/android/window/WindowContext.java +++ b/core/java/android/window/WindowContext.java @@ -17,6 +17,8 @@ package android.window; import static android.view.WindowManagerImpl.createWindowContextWindowManager; +import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; + import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UiContext; @@ -135,7 +137,8 @@ public class WindowContext extends ContextWrapper implements WindowProvider { } /** Dispatch {@link Configuration} to each {@link ComponentCallbacks}. */ - void dispatchConfigurationChanged(@NonNull Configuration newConfig) { + @VisibleForTesting(visibility = PACKAGE) + public void dispatchConfigurationChanged(@NonNull Configuration newConfig) { mCallbacksController.dispatchConfigurationChanged(newConfig); } diff --git a/core/tests/coretests/src/android/content/ComponentCallbacksControllerTest.java b/core/tests/coretests/src/android/content/ComponentCallbacksControllerTest.java index 09985a8bee4ba..aa188f2a30abc 100644 --- a/core/tests/coretests/src/android/content/ComponentCallbacksControllerTest.java +++ b/core/tests/coretests/src/android/content/ComponentCallbacksControllerTest.java @@ -116,25 +116,4 @@ public class ComponentCallbacksControllerTest { @Override public void onLowMemory() {} } - - private static class TestComponentCallbacks2 implements ComponentCallbacks2 { - private Configuration mConfiguration; - private boolean mLowMemoryCalled; - private int mLevel; - - @Override - public void onConfigurationChanged(@NonNull Configuration newConfig) { - mConfiguration = newConfig; - } - - @Override - public void onLowMemory() { - mLowMemoryCalled = true; - } - - @Override - public void onTrimMemory(int level) { - mLevel = level; - } - } } diff --git a/core/tests/coretests/src/android/content/ContextWrapperTest.java b/core/tests/coretests/src/android/content/ContextWrapperTest.java new file mode 100644 index 0000000000000..ecaf1f42fbcab --- /dev/null +++ b/core/tests/coretests/src/android/content/ContextWrapperTest.java @@ -0,0 +1,158 @@ +/* + * 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.content; + +import static android.content.ContextWrapper.COMPONENT_CALLBACK_ON_WRAPPER; +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.fail; + +import android.app.WindowConfiguration; +import android.compat.testing.PlatformCompatChangeRule; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.hardware.display.DisplayManager; +import android.platform.test.annotations.Presubmit; +import android.view.Display; +import android.window.WindowContext; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; + + +/** + * Build/Install/Run: + * atest FrameworksCoreTests:ContextWrapperTest + */ +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ContextWrapperTest { + @Rule + public TestRule compatChangeRule = new PlatformCompatChangeRule(); + + /** + * Before {@link android.os.Build.VERSION_CODES#TIRAMISU}, {@link ContextWrapper} must + * register {@link ComponentCallbacks} to {@link ContextWrapper#getApplicationContext} before + * {@link ContextWrapper#attachBaseContext(Context)}. + */ + @DisableCompatChanges(COMPONENT_CALLBACK_ON_WRAPPER) + @Test + public void testRegisterComponentCallbacksWithoutBaseContextBeforeT() { + final ContextWrapper wrapper = new TestContextWrapper(null /* base */); + final ComponentCallbacks callbacks = new TestComponentCallbacks2(); + + // It should be no-op if unregister a ComponentCallbacks without registration. + wrapper.unregisterComponentCallbacks(callbacks); + + wrapper.registerComponentCallbacks(callbacks); + + assertThat(wrapper.mCallbacksRegisteredToSuper.size()).isEqualTo(1); + assertThat(wrapper.mCallbacksRegisteredToSuper.get(0)).isEqualTo(callbacks); + + wrapper.unregisterComponentCallbacks(callbacks); + + assertThat(wrapper.mCallbacksRegisteredToSuper.isEmpty()).isTrue(); + } + + /** + * After {@link android.os.Build.VERSION_CODES#TIRAMISU}, {@link ContextWrapper} must + * throw {@link IllegalStateException} before {@link ContextWrapper#attachBaseContext(Context)}. + */ + @Test + public void testRegisterComponentCallbacksWithoutBaseContextAfterT() { + final ContextWrapper wrapper = new TestContextWrapper(null /* base */); + final ComponentCallbacks callbacks = new TestComponentCallbacks2(); + + try { + wrapper.unregisterComponentCallbacks(callbacks); + fail("ContextWrapper#unregisterComponentCallbacks must throw Exception before" + + " ContextWrapper#attachToBaseContext."); + } catch (IllegalStateException ignored) { + // It is expected to throw IllegalStateException. + } + + try { + wrapper.registerComponentCallbacks(callbacks); + fail("ContextWrapper#registerComponentCallbacks must throw Exception before" + + " ContextWrapper#attachToBaseContext."); + } catch (IllegalStateException ignored) { + // It is expected to throw IllegalStateException. + } + } + + /** + * {@link ContextWrapper#registerComponentCallbacks(ComponentCallbacks)} must delegate to its + * {@link ContextWrapper#getBaseContext()}, so does + * {@link ContextWrapper#unregisterComponentCallbacks(ComponentCallbacks)}. + */ + @Test + public void testRegisterComponentCallbacks() { + final Context appContext = ApplicationProvider.getApplicationContext(); + final Display display = appContext.getSystemService(DisplayManager.class) + .getDisplay(DEFAULT_DISPLAY); + final WindowContext windowContext = (WindowContext) appContext.createWindowContext(display, + TYPE_APPLICATION_OVERLAY, null /* options */); + final ContextWrapper wrapper = new ContextWrapper(windowContext); + final TestComponentCallbacks2 callbacks = new TestComponentCallbacks2(); + + wrapper.registerComponentCallbacks(callbacks); + + assertThat(wrapper.mCallbacksRegisteredToSuper).isNull(); + + final Configuration dispatchedConfig = new Configuration(); + dispatchedConfig.fontScale = 1.2f; + dispatchedConfig.windowConfiguration.setWindowingMode( + WindowConfiguration.WINDOWING_MODE_FREEFORM); + dispatchedConfig.windowConfiguration.setBounds(new Rect(0, 0, 100, 100)); + windowContext.dispatchConfigurationChanged(dispatchedConfig); + + assertThat(callbacks.mConfiguration).isEqualTo(dispatchedConfig); + } + + private static class TestContextWrapper extends ContextWrapper { + TestContextWrapper(Context base) { + super(base); + } + + @Override + public Context getApplicationContext() { + // The default implementation of ContextWrapper#getApplicationContext is to delegate + // to the base Context, and it leads to NPE if #registerComponentCallbacks is called + // directly before attach to base Context. + // We call to ApplicationProvider#getApplicationContext to prevent NPE because + // developers may have its implementation to prevent NPE without attaching base Context. + final Context baseContext = getBaseContext(); + if (baseContext == null) { + return ApplicationProvider.getApplicationContext(); + } else { + return super.getApplicationContext(); + } + } + } +} diff --git a/core/tests/coretests/src/android/content/TestComponentCallbacks2.java b/core/tests/coretests/src/android/content/TestComponentCallbacks2.java new file mode 100644 index 0000000000000..6ae7fc44c4ce8 --- /dev/null +++ b/core/tests/coretests/src/android/content/TestComponentCallbacks2.java @@ -0,0 +1,42 @@ +/* + * 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.content; + +import android.content.res.Configuration; + +import androidx.annotation.NonNull; + +class TestComponentCallbacks2 implements ComponentCallbacks2 { + android.content.res.Configuration mConfiguration; + boolean mLowMemoryCalled; + int mLevel; + + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + mConfiguration = newConfig; + } + + @Override + public void onLowMemory() { + mLowMemoryCalled = true; + } + + @Override + public void onTrimMemory(int level) { + mLevel = level; + } +} diff --git a/core/tests/coretests/src/android/window/WindowContextTest.java b/core/tests/coretests/src/android/window/WindowContextTest.java index 656e756416d0d..b2a404449de74 100644 --- a/core/tests/coretests/src/android/window/WindowContextTest.java +++ b/core/tests/coretests/src/android/window/WindowContextTest.java @@ -21,16 +21,25 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import android.app.Activity; import android.app.EmptyActivity; import android.app.Instrumentation; +import android.app.WindowConfiguration; +import android.content.ComponentCallbacks; import android.content.Context; +import android.content.ContextWrapper; import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.Rect; import android.hardware.display.DisplayManager; import android.os.Binder; import android.os.IBinder; @@ -43,6 +52,7 @@ import android.view.WindowManager.LayoutParams.WindowType; import android.view.WindowManagerGlobal; import android.view.WindowManagerImpl; +import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; @@ -79,6 +89,8 @@ public class WindowContextTest { private final WindowContext mWindowContext = createWindowContext(); private final IWindowManager mWms = WindowManagerGlobal.getWindowManagerService(); + private static final int TIMEOUT_IN_SECONDS = 4; + @Test public void testCreateWindowContextWindowManagerAttachClientToken() { final WindowManager windowContextWm = WindowManagerImpl @@ -131,7 +143,7 @@ public class WindowContextTest { }); - assertTrue(latch.await(4, TimeUnit.SECONDS)); + assertTrue(latch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS)); // Verify that the window token of the window context is created after first addView(). @@ -234,7 +246,7 @@ public class WindowContextTest { // Add the parent window mInstrumentation.runOnMainSync(() -> wm.addView(parentWindow, params)); - assertTrue(listener.mLatch.await(4, TimeUnit.SECONDS)); + assertTrue(listener.mLatch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS)); final WindowManager.LayoutParams subWindowAttrs = new WindowManager.LayoutParams(TYPE_APPLICATION_ATTACHED_DIALOG); @@ -251,6 +263,63 @@ public class WindowContextTest { null /* theme */)); } + @Test + public void testRegisterComponentCallbacks() { + final WindowContext windowContext = createWindowContext(); + final ConfigurationListener listener = new ConfigurationListener(); + + windowContext.registerComponentCallbacks(listener); + + try { + final Configuration config = new Configuration(); + config.windowConfiguration.setWindowingMode( + WindowConfiguration.WINDOWING_MODE_FREEFORM); + config.windowConfiguration.setBounds(new Rect(0, 0, 100, 100)); + + windowContext.dispatchConfigurationChanged(config); + + try { + assertWithMessage("Waiting for onConfigurationChanged timeout.") + .that(listener.mLatch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS)).isTrue(); + } catch (InterruptedException e) { + fail("Waiting for configuration changed failed because of " + e); + } + + assertThat(listener.mConfiguration).isEqualTo(config); + } finally { + windowContext.unregisterComponentCallbacks(listener); + } + } + + @Test + public void testRegisterComponentCallbacksOnWindowContextWrapper() { + final WindowContext windowContext = createWindowContext(); + final Context wrapper = new ContextWrapper(windowContext); + final ConfigurationListener listener = new ConfigurationListener(); + + wrapper.registerComponentCallbacks(listener); + + try { + final Configuration config = new Configuration(); + config.windowConfiguration.setWindowingMode( + WindowConfiguration.WINDOWING_MODE_FREEFORM); + config.windowConfiguration.setBounds(new Rect(0, 0, 100, 100)); + + windowContext.dispatchConfigurationChanged(config); + + try { + assertWithMessage("Waiting for onConfigurationChanged timeout.") + .that(listener.mLatch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS)).isTrue(); + } catch (InterruptedException e) { + fail("Waiting for configuration changed failed because of " + e); + } + + assertThat(listener.mConfiguration).isEqualTo(config); + } finally { + wrapper.unregisterComponentCallbacks(listener); + } + } + private WindowContext createWindowContext() { return createWindowContext(TYPE_APPLICATION_OVERLAY); } @@ -262,6 +331,20 @@ public class WindowContextTest { return (WindowContext) instContext.createWindowContext(display, type, null /* options */); } + private static class ConfigurationListener implements ComponentCallbacks { + private Configuration mConfiguration; + private CountDownLatch mLatch = new CountDownLatch(1); + + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + mConfiguration = newConfig; + mLatch.countDown(); + } + + @Override + public void onLowMemory() {} + } + private static class AttachStateListener implements View.OnAttachStateChangeListener { final CountDownLatch mLatch = new CountDownLatch(1);