diff options
| -rw-r--r-- | core/java/android/app/Application.java | 49 | ||||
| -rw-r--r-- | core/java/android/app/WindowContext.java | 21 | ||||
| -rw-r--r-- | core/java/android/app/WindowTokenClient.java | 4 | ||||
| -rw-r--r-- | core/java/android/content/ComponentCallbacksController.java | 127 | ||||
| -rw-r--r-- | core/java/android/content/Context.java | 21 | ||||
| -rw-r--r-- | core/java/android/content/TEST_MAPPING | 5 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/content/ComponentCallbacksControllerTest.java | 140 |
7 files changed, 324 insertions, 43 deletions
diff --git a/core/java/android/app/Application.java b/core/java/android/app/Application.java index 146d648fe65a..618eda8c84e8 100644 --- a/core/java/android/app/Application.java +++ b/core/java/android/app/Application.java @@ -22,6 +22,7 @@ import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentCallbacks; import android.content.ComponentCallbacks2; +import android.content.ComponentCallbacksController; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; @@ -53,14 +54,14 @@ import java.util.ArrayList; public class Application extends ContextWrapper implements ComponentCallbacks2 { private static final String TAG = "Application"; @UnsupportedAppUsage - private ArrayList<ComponentCallbacks> mComponentCallbacks = - new ArrayList<ComponentCallbacks>(); - @UnsupportedAppUsage private ArrayList<ActivityLifecycleCallbacks> mActivityLifecycleCallbacks = new ArrayList<ActivityLifecycleCallbacks>(); @UnsupportedAppUsage private ArrayList<OnProvideAssistDataListener> mAssistCallbacks = null; + private final ComponentCallbacksController mCallbacksController = + new ComponentCallbacksController(); + /** @hide */ @UnsupportedAppUsage public LoadedApk mLoadedApk; @@ -260,47 +261,25 @@ public class Application extends ContextWrapper implements ComponentCallbacks2 { @CallSuper public void onConfigurationChanged(@NonNull Configuration newConfig) { - Object[] callbacks = collectComponentCallbacks(); - if (callbacks != null) { - for (int i=0; i<callbacks.length; i++) { - ((ComponentCallbacks)callbacks[i]).onConfigurationChanged(newConfig); - } - } + mCallbacksController.dispatchConfigurationChanged(newConfig); } @CallSuper public void onLowMemory() { - Object[] callbacks = collectComponentCallbacks(); - if (callbacks != null) { - for (int i=0; i<callbacks.length; i++) { - ((ComponentCallbacks)callbacks[i]).onLowMemory(); - } - } + mCallbacksController.dispatchLowMemory(); } @CallSuper public void onTrimMemory(int level) { - Object[] callbacks = collectComponentCallbacks(); - if (callbacks != null) { - for (int i=0; i<callbacks.length; i++) { - Object c = callbacks[i]; - if (c instanceof ComponentCallbacks2) { - ((ComponentCallbacks2)c).onTrimMemory(level); - } - } - } + mCallbacksController.dispatchTrimMemory(level); } public void registerComponentCallbacks(ComponentCallbacks callback) { - synchronized (mComponentCallbacks) { - mComponentCallbacks.add(callback); - } + mCallbacksController.registerCallbacks(callback); } public void unregisterComponentCallbacks(ComponentCallbacks callback) { - synchronized (mComponentCallbacks) { - mComponentCallbacks.remove(callback); - } + mCallbacksController.unregisterCallbacks(callback); } public void registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks callback) { @@ -575,16 +554,6 @@ public class Application extends ContextWrapper implements ComponentCallbacks2 { } } - private Object[] collectComponentCallbacks() { - Object[] callbacks = null; - synchronized (mComponentCallbacks) { - if (mComponentCallbacks.size() > 0) { - callbacks = mComponentCallbacks.toArray(); - } - } - return callbacks; - } - @UnsupportedAppUsage private Object[] collectActivityLifecycleCallbacks() { Object[] callbacks = null; diff --git a/core/java/android/app/WindowContext.java b/core/java/android/app/WindowContext.java index cbe2995f2467..d44918cf0379 100644 --- a/core/java/android/app/WindowContext.java +++ b/core/java/android/app/WindowContext.java @@ -20,8 +20,11 @@ import static android.view.WindowManagerImpl.createWindowContextWindowManager; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UiContext; +import android.content.ComponentCallbacks; +import android.content.ComponentCallbacksController; import android.content.Context; import android.content.ContextWrapper; +import android.content.res.Configuration; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; @@ -49,6 +52,8 @@ public class WindowContext extends ContextWrapper { private final IWindowManager mWms; private final WindowTokenClient mToken; private boolean mListenerRegistered; + private final ComponentCallbacksController mCallbacksController = + new ComponentCallbacksController(); /** * Default constructor. Will generate a {@link WindowTokenClient} and attach this context to @@ -131,8 +136,24 @@ public class WindowContext extends ContextWrapper { } void destroy() { + mCallbacksController.clearCallbacks(); final ContextImpl impl = (ContextImpl) getBaseContext(); impl.scheduleFinalCleanup(getClass().getName(), "WindowContext"); Reference.reachabilityFence(this); } + + @Override + public void registerComponentCallbacks(@NonNull ComponentCallbacks callback) { + mCallbacksController.registerCallbacks(callback); + } + + @Override + public void unregisterComponentCallbacks(@NonNull ComponentCallbacks callback) { + mCallbacksController.unregisterCallbacks(callback); + } + + /** Dispatch {@link Configuration} to each {@link ComponentCallbacks}. */ + void dispatchConfigurationChanged(@NonNull Configuration newConfig) { + mCallbacksController.dispatchConfigurationChanged(newConfig); + } } diff --git a/core/java/android/app/WindowTokenClient.java b/core/java/android/app/WindowTokenClient.java index 2298e84d755e..82cef072ad0f 100644 --- a/core/java/android/app/WindowTokenClient.java +++ b/core/java/android/app/WindowTokenClient.java @@ -61,7 +61,7 @@ public class WindowTokenClient extends IWindowToken.Stub { @Override public void onConfigurationChanged(Configuration newConfig, int newDisplayId) { - final Context context = mContextRef.get(); + final WindowContext context = mContextRef.get(); if (context == null) { return; } @@ -72,6 +72,8 @@ public class WindowTokenClient extends IWindowToken.Stub { if (displayChanged || configChanged) { // TODO(ag/9789103): update resource manager logic to track non-activity tokens mResourcesManager.updateResourcesForActivity(this, newConfig, newDisplayId); + ActivityThread.currentActivityThread().getHandler().post( + () -> context.dispatchConfigurationChanged(newConfig)); } if (displayChanged) { context.updateDisplay(newDisplayId); diff --git a/core/java/android/content/ComponentCallbacksController.java b/core/java/android/content/ComponentCallbacksController.java new file mode 100644 index 000000000000..a81aaf78bdb9 --- /dev/null +++ b/core/java/android/content/ComponentCallbacksController.java @@ -0,0 +1,127 @@ +/* + * 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.content; + +import android.annotation.NonNull; +import android.content.res.Configuration; + +import com.android.internal.annotations.GuardedBy; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * A helper class to manage {@link ComponentCallbacks} and {@link ComponentCallbacks2}, such as + * registering ,unregistering {@link ComponentCallbacks} and sending callbacks to all registered + * {@link ComponentCallbacks}. + * + * @see Context#registerComponentCallbacks(ComponentCallbacks) + * @see Context#unregisterComponentCallbacks(ComponentCallbacks) + * @see ComponentCallbacks + * @see ComponentCallbacks2 + * + * @hide + */ +public class ComponentCallbacksController { + @GuardedBy("mLock") + private List<ComponentCallbacks> mComponentCallbacks; + + private final Object mLock = new Object(); + + /** + * Register the {@link ComponentCallbacks}. + * + * @see Context#registerComponentCallbacks(ComponentCallbacks) + */ + public void registerCallbacks(@NonNull ComponentCallbacks callbacks) { + synchronized (mLock) { + if (mComponentCallbacks == null) { + mComponentCallbacks = new ArrayList<>(); + } + mComponentCallbacks.add(callbacks); + } + } + + /** + * Unregister the {@link ComponentCallbacks}. + * + * @see Context#unregisterComponentCallbacks(ComponentCallbacks) + */ + public void unregisterCallbacks(@NonNull ComponentCallbacks callbacks) { + synchronized (mLock) { + if (mComponentCallbacks == null || mComponentCallbacks.isEmpty()) { + return; + } + mComponentCallbacks.remove(callbacks); + } + } + + /** + * Clear all registered {@link ComponentCallbacks}. + * It is useful when the associated {@link Context} is going to be released. + */ + public void clearCallbacks() { + synchronized (mLock) { + if (mComponentCallbacks != null) { + mComponentCallbacks.clear(); + } + } + } + + /** + * Sending {@link ComponentCallbacks#onConfigurationChanged(Configuration)} to all registered + * {@link ComponentCallbacks}. + */ + public void dispatchConfigurationChanged(@NonNull Configuration newConfig) { + forAllComponentCallbacks(callbacks -> callbacks.onConfigurationChanged(newConfig)); + } + + /** + * Sending {@link ComponentCallbacks#onLowMemory()} to all registered + * {@link ComponentCallbacks}. + */ + public void dispatchLowMemory() { + forAllComponentCallbacks(ComponentCallbacks::onLowMemory); + } + + /** + * Sending {@link ComponentCallbacks2#onTrimMemory(int)} to all registered + * {@link ComponentCallbacks2}. + */ + public void dispatchTrimMemory(int level) { + forAllComponentCallbacks(callbacks -> { + if (callbacks instanceof ComponentCallbacks2) { + ((ComponentCallbacks2) callbacks).onTrimMemory(level); + } + }); + } + + private void forAllComponentCallbacks(Consumer<ComponentCallbacks> callbacksConsumer) { + final ComponentCallbacks[] callbacksArray; + synchronized (mLock) { + if (mComponentCallbacks == null || mComponentCallbacks.isEmpty()) { + return; + } + callbacksArray = new ComponentCallbacks[mComponentCallbacks.size()]; + mComponentCallbacks.toArray(callbacksArray); + } + for (ComponentCallbacks callbacks : callbacksArray) { + callbacksConsumer.accept(callbacks); + } + } +} diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 64ca92fa8132..92ff640e33b0 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -655,12 +655,21 @@ public abstract class Context { /** * 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 + * 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#S}, Registering the ComponentCallbacks to Context created + * via {@link #createWindowContext(int, Bundle)} or + * {@link #createWindowContext(Display, int, Bundle)} will receive + * {@link ComponentCallbacks#onConfigurationChanged(Configuration)} from Window Context rather + * than its base application. It is helpful if you want to handle UI components that + * associated with the Window Context when the Window Context has configuration changes.</p> * * @param callback The interface to call. This can be either a * {@link ComponentCallbacks} or {@link ComponentCallbacks2} interface. + * + * @see Context#createWindowContext(int, Bundle) */ public void registerComponentCallbacks(ComponentCallbacks callback) { getApplicationContext().registerComponentCallbacks(callback); @@ -6358,6 +6367,16 @@ public abstract class Context { * windowContext.getSystemService(WindowManager.class).addView(overlayView, mParams); * </pre> * <p> + * After {@link Build.VERSION_CODES#S}, window context provides the capability to listen to its + * {@link Configuration} changes by calling + * {@link #registerComponentCallbacks(ComponentCallbacks)}, while other kinds of {@link Context} + * will register the {@link ComponentCallbacks} to {@link #getApplicationContext() its + * Application context}. Note that window context only propagate + * {@link ComponentCallbacks#onConfigurationChanged(Configuration)} callback. + * {@link ComponentCallbacks#onLowMemory()} or other callbacks in {@link ComponentCallbacks2} + * won't be invoked. + * </p> + * <p> * Note that using {@link android.app.Application} or {@link android.app.Service} context for * UI-related queries may result in layout or continuity issues on devices with variable screen * sizes (e.g. foldables) or in multi-window modes, since these non-UI contexts may not reflect diff --git a/core/java/android/content/TEST_MAPPING b/core/java/android/content/TEST_MAPPING index a2880dfdfd17..614143e7c04d 100644 --- a/core/java/android/content/TEST_MAPPING +++ b/core/java/android/content/TEST_MAPPING @@ -44,9 +44,12 @@ }, { "include-filter": "android.content.ContextTest" + }, + { + "include-filter": "android.content.ComponentCallbacksControllerTest" } ], - "file_patterns": ["(/|^)Context.java", "(/|^)ContextWrapper.java"] + "file_patterns": ["(/|^)Context.java", "(/|^)ContextWrapper.java", "(/|^)ComponentCallbacksController.java"] } ] }
\ No newline at end of file diff --git a/core/tests/coretests/src/android/content/ComponentCallbacksControllerTest.java b/core/tests/coretests/src/android/content/ComponentCallbacksControllerTest.java new file mode 100644 index 000000000000..09985a8bee4b --- /dev/null +++ b/core/tests/coretests/src/android/content/ComponentCallbacksControllerTest.java @@ -0,0 +1,140 @@ +/* + * 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.content; + +import static android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.WindowConfiguration; +import android.content.res.Configuration; +import android.graphics.Rect; + +import androidx.annotation.NonNull; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Build/Install/Run: + * atest FrameworksCoreTests:ComponentCallbacksControllerTest + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ComponentCallbacksControllerTest { + private ComponentCallbacksController mController; + + @Before + public void setUp() { + mController = new ComponentCallbacksController(); + } + + @Test + public void testUnregisterCallbackWithoutRegistrationNoCrash() { + mController.unregisterCallbacks(new FakeComponentCallbacks()); + } + + @Test + public void testDispatchWithEmptyCallbacksNoCrash() { + mController.dispatchConfigurationChanged(new Configuration()); + mController.dispatchLowMemory(); + mController.dispatchTrimMemory(TRIM_MEMORY_BACKGROUND); + } + + @Test + public void testClearCallbacksNoCrash() { + mController.clearCallbacks(); + } + + @Test + public void testDispatchTrimMemoryWithoutComponentCallbacks2NoCrash() { + // Register a ComponentCallbacks instead of ComponentCallbacks2 + mController.registerCallbacks(new FakeComponentCallbacks()); + + mController.dispatchTrimMemory(TRIM_MEMORY_BACKGROUND); + } + + @Test + public void testDispatchConfigurationChanged() throws Exception { + final TestComponentCallbacks2 callback = new TestComponentCallbacks2(); + mController.registerCallbacks(callback); + + final Configuration config = new Configuration(); + config.windowConfiguration.setWindowingMode(WindowConfiguration.WINDOWING_MODE_FREEFORM); + config.windowConfiguration.setBounds(new Rect(0, 0, 100, 100)); + + mController.dispatchConfigurationChanged(config); + + assertThat(callback.mConfiguration).isEqualTo(config); + + mController.dispatchConfigurationChanged(Configuration.EMPTY); + + assertThat(callback.mConfiguration).isEqualTo(Configuration.EMPTY); + } + + @Test + public void testDispatchLowMemory() { + final TestComponentCallbacks2 callback = new TestComponentCallbacks2(); + mController.registerCallbacks(callback); + + mController.dispatchLowMemory(); + + assertThat(callback.mLowMemoryCalled).isTrue(); + } + + @Test + public void testDispatchTrimMemory() { + final TestComponentCallbacks2 callback = new TestComponentCallbacks2(); + mController.registerCallbacks(callback); + + mController.dispatchTrimMemory(TRIM_MEMORY_BACKGROUND); + + assertThat(callback.mLevel).isEqualTo(TRIM_MEMORY_BACKGROUND); + } + + private static class FakeComponentCallbacks implements ComponentCallbacks { + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) {} + + @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; + } + } +} |