Loading core/java/android/content/ContextWrapper.java +105 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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<ComponentCallbacks> mCallbacksRegisteredToSuper; private final Object mLock = new Object(); public ContextWrapper(Context base) { mBase = base; } Loading Loading @@ -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 * <em>must</em> be sure to use {@link #unregisterComponentCallbacks} when * appropriate in the future; this will not be removed for you. * <p> * 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)}. * <p> * 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)} * </p> * * @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. } } core/java/android/content/TEST_MAPPING +3 −0 Original line number Diff line number Diff line Loading @@ -32,6 +32,9 @@ }, { "include-filter": "android.content.ComponentCallbacksControllerTest" }, { "include-filter": "android.content.ContextWrapperTest" } ], "file_patterns": ["(/|^)Context.java", "(/|^)ContextWrapper.java", "(/|^)ComponentCallbacksController.java"] Loading core/java/android/window/WindowContext.java +4 −1 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); } Loading core/tests/coretests/src/android/content/ComponentCallbacksControllerTest.java +0 −21 Original line number Diff line number Diff line Loading @@ -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; } } } core/tests/coretests/src/android/content/ContextWrapperTest.java 0 → 100644 +158 −0 Original line number Diff line number Diff line /* * 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(); } } } } Loading
core/java/android/content/ContextWrapper.java +105 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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<ComponentCallbacks> mCallbacksRegisteredToSuper; private final Object mLock = new Object(); public ContextWrapper(Context base) { mBase = base; } Loading Loading @@ -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 * <em>must</em> be sure to use {@link #unregisterComponentCallbacks} when * appropriate in the future; this will not be removed for you. * <p> * 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)}. * <p> * 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)} * </p> * * @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. } }
core/java/android/content/TEST_MAPPING +3 −0 Original line number Diff line number Diff line Loading @@ -32,6 +32,9 @@ }, { "include-filter": "android.content.ComponentCallbacksControllerTest" }, { "include-filter": "android.content.ContextWrapperTest" } ], "file_patterns": ["(/|^)Context.java", "(/|^)ContextWrapper.java", "(/|^)ComponentCallbacksController.java"] Loading
core/java/android/window/WindowContext.java +4 −1 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); } Loading
core/tests/coretests/src/android/content/ComponentCallbacksControllerTest.java +0 −21 Original line number Diff line number Diff line Loading @@ -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; } } }
core/tests/coretests/src/android/content/ContextWrapperTest.java 0 → 100644 +158 −0 Original line number Diff line number Diff line /* * 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(); } } } }