Merge "Enable to listen to WindowContext's config changes" into sc-dev

This commit is contained in:
Charles Chen
2021-03-22 12:28:02 +00:00
committed by Android (Google) Code Review
7 changed files with 324 additions and 43 deletions

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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);
}
}
}

View File

@@ -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

View File

@@ -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"]
}
]
}

View File

@@ -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;
}
}
}