Merge "Override (un)registerComponentCallbacks for ContextWrapper"

This commit is contained in:
Charles Chen
2022-01-25 14:18:58 +00:00
committed by Android (Google) Code Review
7 changed files with 397 additions and 24 deletions

View File

@@ -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<ComponentCallbacks> 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
* <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.
}
}

View File

@@ -32,6 +32,9 @@
},
{
"include-filter": "android.content.ComponentCallbacksControllerTest"
},
{
"include-filter": "android.content.ContextWrapperTest"
}
],
"file_patterns": ["(/|^)Context.java", "(/|^)ContextWrapper.java", "(/|^)ComponentCallbacksController.java"]

View File

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

View File

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

View File

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

View File

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

View File

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