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

Commit 19e1e118 authored by Charles Chen's avatar Charles Chen Committed by Android (Google) Code Review
Browse files

Merge "Override (un)registerComponentCallbacks for ContextWrapper"

parents dc94a722 a4059c2f
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