Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a4059c2f authored by Charles Chen's avatar Charles Chen
Browse files

Override (un)registerComponentCallbacks for ContextWrapper

Bug: 193247900
Test: WindowContextTest
Test: ContextWrapperTest

Change-Id: Ia070b12eea5631d70a96d6e41d03ab08f6b44caa
parent 7ade3e1c
Loading
Loading
Loading
Loading
+105 −0
Original line number Diff line number Diff line
@@ -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.
    }
}
+3 −0
Original line number Diff line number Diff line
@@ -32,6 +32,9 @@
        },
        {
          "include-filter": "android.content.ComponentCallbacksControllerTest"
        },
        {
          "include-filter": "android.content.ContextWrapperTest"
        }
      ],
      "file_patterns": ["(/|^)Context.java", "(/|^)ContextWrapper.java", "(/|^)ComponentCallbacksController.java"]
+4 −1
Original line number Diff line number Diff line
@@ -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);
    }

+0 −21
Original line number Diff line number Diff line
@@ -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;
        }
    }
}
+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