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

Commit b8a6fa9f authored by Chris Li's avatar Chris Li
Browse files

Introduce ActivityWindowInfoChangedListener

Bug: 287582673
Test: atest FrameworksCoreTests:ActivityThreadTest
Test: atest FrameworksCoreTests:ClientTransactionListenerControllerTest
Change-Id: I9ec55a425cc1207354cd199944bc5cc606754d78
parent 2434731b
Loading
Loading
Loading
Loading
+24 −1
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@ import static android.window.ConfigurationHelper.shouldUpdateResources;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
import static com.android.internal.os.SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL;
import static com.android.sdksandbox.flags.Flags.sandboxActivitySdkBasedContext;
import static com.android.window.flags.Flags.activityWindowInfoFlag;

import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -63,6 +64,7 @@ import android.app.servertransaction.ActivityLifecycleItem.LifecycleState;
import android.app.servertransaction.ActivityRelaunchItem;
import android.app.servertransaction.ActivityResultItem;
import android.app.servertransaction.ClientTransaction;
import android.app.servertransaction.ClientTransactionListenerController;
import android.app.servertransaction.DestroyActivityItem;
import android.app.servertransaction.PauseActivityItem;
import android.app.servertransaction.PendingTransactionActions;
@@ -606,6 +608,8 @@ public final class ActivityThread extends ClientTransactionHandler
        Configuration overrideConfig;
        @NonNull
        private ActivityWindowInfo mActivityWindowInfo;
        @Nullable
        private ActivityWindowInfo mLastReportedActivityWindowInfo;

        // Used for consolidating configs before sending on to Activity.
        private final Configuration tmpConfig = new Configuration();
@@ -4180,6 +4184,9 @@ public final class ActivityThread extends ClientTransactionHandler
                pendingActions.setRestoreInstanceState(true);
                pendingActions.setCallOnPostCreate(true);
            }

            // Trigger ActivityWindowInfo callback if first launch or change from relaunch.
            handleActivityWindowInfoChanged(r);
        } else {
            // If there was an error, for any reason, tell the activity manager to stop us.
            ActivityClient.getInstance().finishActivity(r.token, Activity.RESULT_CANCELED,
@@ -6740,7 +6747,7 @@ public final class ActivityThread extends ClientTransactionHandler
        // Perform updates.
        r.overrideConfig = overrideConfig;
        r.mActivityWindowInfo = activityWindowInfo;
        // TODO(b/287582673): notify on ActivityWindowInfo change

        final ViewRootImpl viewRoot = r.activity.mDecor != null
            ? r.activity.mDecor.getViewRootImpl() : null;

@@ -6763,6 +6770,22 @@ public final class ActivityThread extends ClientTransactionHandler
            viewRoot.updateConfiguration(displayId);
        }
        mSomeActivitiesChanged = true;

        // Trigger ActivityWindowInfo callback if changed.
        handleActivityWindowInfoChanged(r);
    }

    private void handleActivityWindowInfoChanged(@NonNull ActivityClientRecord r) {
        if (!activityWindowInfoFlag()) {
            return;
        }
        if (r.mActivityWindowInfo == null
                || r.mActivityWindowInfo.equals(r.mLastReportedActivityWindowInfo)) {
            return;
        }
        r.mLastReportedActivityWindowInfo = r.mActivityWindowInfo;
        ClientTransactionListenerController.getInstance().onActivityWindowInfoChanged(r.token,
                r.mActivityWindowInfo);
    }

    final void handleProfilerControl(boolean start, ProfilerInfo profilerInfo, int profileType) {
+65 −0
Original line number Diff line number Diff line
@@ -16,16 +16,24 @@

package android.app.servertransaction;

import static com.android.window.flags.Flags.activityWindowInfoFlag;
import static com.android.window.flags.Flags.bundleClientTransactionFlag;

import static java.util.Objects.requireNonNull;

import android.annotation.NonNull;
import android.app.Activity;
import android.app.ActivityThread;
import android.hardware.display.DisplayManagerGlobal;
import android.os.IBinder;
import android.util.ArraySet;
import android.window.ActivityWindowInfo;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;

import java.util.function.BiConsumer;

/**
 * Singleton controller to manage listeners to individual {@link ClientTransaction}.
 *
@@ -35,8 +43,14 @@ public class ClientTransactionListenerController {

    private static ClientTransactionListenerController sController;

    private final Object mLock = new Object();
    private final DisplayManagerGlobal mDisplayManager;

    /** Listeners registered via {@link #registerActivityWindowInfoChangedListener(BiConsumer)}. */
    @GuardedBy("mLock")
    private final ArraySet<BiConsumer<IBinder, ActivityWindowInfo>>
            mActivityWindowInfoChangedListeners = new ArraySet<>();

    /** Gets the singleton controller. */
    @NonNull
    public static ClientTransactionListenerController getInstance() {
@@ -61,6 +75,57 @@ public class ClientTransactionListenerController {
        mDisplayManager = requireNonNull(displayManager);
    }

    /**
     * Registers to listen on activity {@link ActivityWindowInfo} change.
     * The listener will be invoked with two parameters: {@link Activity#getActivityToken()} and
     * {@link ActivityWindowInfo}.
     */
    public void registerActivityWindowInfoChangedListener(
            @NonNull BiConsumer<IBinder, ActivityWindowInfo> listener) {
        if (!activityWindowInfoFlag()) {
            return;
        }
        synchronized (mLock) {
            mActivityWindowInfoChangedListeners.add(listener);
        }
    }

    /**
     * Unregisters the listener that was previously registered via
     * {@link #registerActivityWindowInfoChangedListener(BiConsumer)}
     */
    public void unregisterActivityWindowInfoChangedListener(
            @NonNull BiConsumer<IBinder, ActivityWindowInfo> listener) {
        if (!activityWindowInfoFlag()) {
            return;
        }
        synchronized (mLock) {
            mActivityWindowInfoChangedListeners.remove(listener);
        }
    }

    /**
     * Called when receives a {@link ClientTransaction} that is updating an activity's
     * {@link ActivityWindowInfo}.
     */
    public void onActivityWindowInfoChanged(@NonNull IBinder activityToken,
            @NonNull ActivityWindowInfo activityWindowInfo) {
        if (!activityWindowInfoFlag()) {
            return;
        }
        final Object[] activityWindowInfoChangedListeners;
        synchronized (mLock) {
            if (mActivityWindowInfoChangedListeners.isEmpty()) {
                return;
            }
            activityWindowInfoChangedListeners = mActivityWindowInfoChangedListeners.toArray();
        }
        for (Object activityWindowInfoChangedListener : activityWindowInfoChangedListeners) {
            ((BiConsumer<IBinder, ActivityWindowInfo>) activityWindowInfoChangedListener)
                    .accept(activityToken, activityWindowInfo);
        }
    }

    /**
     * Called when receives a {@link ClientTransaction} that is updating display-related
     * window configuration.
+118 −1
Original line number Diff line number Diff line
@@ -21,16 +21,22 @@ import static android.content.Intent.ACTION_EDIT;
import static android.content.Intent.ACTION_VIEW;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;

import static com.android.window.flags.Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG;

import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;

import android.annotation.NonNull;
@@ -46,6 +52,7 @@ import android.app.servertransaction.ActivityConfigurationChangeItem;
import android.app.servertransaction.ActivityRelaunchItem;
import android.app.servertransaction.ClientTransaction;
import android.app.servertransaction.ClientTransactionItem;
import android.app.servertransaction.ClientTransactionListenerController;
import android.app.servertransaction.ConfigurationChangeItem;
import android.app.servertransaction.NewIntentItem;
import android.app.servertransaction.ResumeActivityItem;
@@ -60,7 +67,9 @@ import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.SetFlagsRule;
import android.util.DisplayMetrics;
import android.util.MergedConfiguration;
import android.view.Display;
@@ -81,11 +90,14 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

/**
@@ -103,11 +115,17 @@ public class ActivityThreadTest {
    // few sequence numbers the framework used to launch the test activity.
    private static final int BASE_SEQ = 10000000;

    @Rule
    @Rule(order = 0)
    public final ActivityTestRule<TestActivity> mActivityTestRule =
            new ActivityTestRule<>(TestActivity.class, true /* initialTouchMode */,
                    false /* launchActivity */);

    @Rule(order = 1)
    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);

    @Mock
    private BiConsumer<IBinder, ActivityWindowInfo> mActivityWindowInfoListener;

    private WindowTokenClientController mOriginalWindowTokenClientController;
    private Configuration mOriginalAppConfig;

@@ -115,6 +133,8 @@ public class ActivityThreadTest {

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);

        // Keep track of the original controller, so that it can be used to restore in tearDown()
        // when there is override in some test cases.
        mOriginalWindowTokenClientController = WindowTokenClientController.getInstance();
@@ -129,6 +149,8 @@ public class ActivityThreadTest {
            mCreatedVirtualDisplays = null;
        }
        WindowTokenClientController.overrideForTesting(mOriginalWindowTokenClientController);
        ClientTransactionListenerController.getInstance()
                .unregisterActivityWindowInfoChangedListener(mActivityWindowInfoListener);
        InstrumentationRegistry.getInstrumentation().runOnMainSync(
                () -> restoreConfig(ActivityThread.currentActivityThread(), mOriginalAppConfig));
    }
@@ -783,6 +805,101 @@ public class ActivityThreadTest {
        verify(windowTokenClientController).onWindowContextWindowRemoved(clientToken);
    }

    @Test
    public void testActivityWindowInfoChanged_activityLaunch() {
        mSetFlagsRule.enableFlags(FLAG_ACTIVITY_WINDOW_INFO_FLAG);

        ClientTransactionListenerController.getInstance().registerActivityWindowInfoChangedListener(
                mActivityWindowInfoListener);

        final Activity activity = mActivityTestRule.launchActivity(new Intent());
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
        final ActivityClientRecord activityClientRecord = getActivityClientRecord(activity);

        verify(mActivityWindowInfoListener).accept(activityClientRecord.token,
                activityClientRecord.getActivityWindowInfo());
    }

    @Test
    public void testActivityWindowInfoChanged_activityRelaunch() throws RemoteException {
        mSetFlagsRule.enableFlags(FLAG_ACTIVITY_WINDOW_INFO_FLAG);

        ClientTransactionListenerController.getInstance().registerActivityWindowInfoChangedListener(
                mActivityWindowInfoListener);

        final Activity activity = mActivityTestRule.launchActivity(new Intent());
        final IApplicationThread appThread = activity.getActivityThread().getApplicationThread();
        appThread.scheduleTransaction(newRelaunchResumeTransaction(activity));
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
        final ActivityClientRecord activityClientRecord = getActivityClientRecord(activity);

        // The same ActivityWindowInfo won't trigger duplicated callback.
        verify(mActivityWindowInfoListener).accept(activityClientRecord.token,
                activityClientRecord.getActivityWindowInfo());

        final Configuration currentConfig = activity.getResources().getConfiguration();
        final ActivityWindowInfo activityWindowInfo = new ActivityWindowInfo();
        activityWindowInfo.set(true /* isEmbedded */, new Rect(0, 0, 1000, 2000),
                new Rect(0, 0, 1000, 1000));
        final ActivityRelaunchItem relaunchItem = ActivityRelaunchItem.obtain(
                activity.getActivityToken(), null, null, 0,
                new MergedConfiguration(currentConfig, currentConfig),
                false /* preserveWindow */, activityWindowInfo);
        final ClientTransaction transaction = newTransaction(activity);
        transaction.addTransactionItem(relaunchItem);
        appThread.scheduleTransaction(transaction);
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();

        verify(mActivityWindowInfoListener).accept(activityClientRecord.token,
                activityWindowInfo);
    }

    @Test
    public void testActivityWindowInfoChanged_activityConfigurationChanged()
            throws RemoteException {
        mSetFlagsRule.enableFlags(FLAG_ACTIVITY_WINDOW_INFO_FLAG);

        ClientTransactionListenerController.getInstance().registerActivityWindowInfoChangedListener(
                mActivityWindowInfoListener);

        final Activity activity = mActivityTestRule.launchActivity(new Intent());
        final IApplicationThread appThread = activity.getActivityThread().getApplicationThread();
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();

        clearInvocations(mActivityWindowInfoListener);
        final Configuration config = new Configuration(activity.getResources().getConfiguration());
        config.seq++;
        final Rect taskBounds = new Rect(0, 0, 1000, 2000);
        final Rect taskFragmentBounds = new Rect(0, 0, 1000, 1000);
        final ActivityWindowInfo activityWindowInfo = new ActivityWindowInfo();
        activityWindowInfo.set(true /* isEmbedded */, taskBounds, taskFragmentBounds);
        final ActivityConfigurationChangeItem activityConfigurationChangeItem =
                ActivityConfigurationChangeItem.obtain(
                        activity.getActivityToken(), config, activityWindowInfo);
        final ClientTransaction transaction = newTransaction(activity);
        transaction.addTransactionItem(activityConfigurationChangeItem);
        appThread.scheduleTransaction(transaction);
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();

        verify(mActivityWindowInfoListener).accept(activity.getActivityToken(),
                activityWindowInfo);

        clearInvocations(mActivityWindowInfoListener);
        final ActivityWindowInfo activityWindowInfo2 = new ActivityWindowInfo();
        activityWindowInfo2.set(true /* isEmbedded */, taskBounds, taskFragmentBounds);
        config.seq++;
        final ActivityConfigurationChangeItem activityConfigurationChangeItem2 =
                ActivityConfigurationChangeItem.obtain(
                        activity.getActivityToken(), config, activityWindowInfo2);
        final ClientTransaction transaction2 = newTransaction(activity);
        transaction2.addTransactionItem(activityConfigurationChangeItem2);
        appThread.scheduleTransaction(transaction);
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();

        // The same ActivityWindowInfo won't trigger duplicated callback.
        verify(mActivityWindowInfoListener, never()).accept(any(), any());
    }

    /**
     * Calls {@link ActivityThread#handleActivityConfigurationChanged(ActivityClientRecord,
     * Configuration, int, ActivityWindowInfo)} to try to push activity configuration to the
+34 −0
Original line number Diff line number Diff line
@@ -22,21 +22,29 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat

import static com.android.window.flags.Flags.FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;

import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManagerGlobal;
import android.hardware.display.IDisplayManager;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.SetFlagsRule;
import android.view.DisplayInfo;
import android.window.ActivityWindowInfo;

import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import com.android.window.flags.Flags;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -44,6 +52,8 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.function.BiConsumer;

/**
 * Tests for {@link ClientTransactionListenerController}.
 *
@@ -62,6 +72,10 @@ public class ClientTransactionListenerControllerTest {
    private IDisplayManager mIDisplayManager;
    @Mock
    private DisplayManager.DisplayListener mListener;
    @Mock
    private BiConsumer<IBinder, ActivityWindowInfo> mActivityWindowInfoListener;
    @Mock
    private IBinder mActivityToken;

    private DisplayManagerGlobal mDisplayManager;
    private Handler mHandler;
@@ -91,4 +105,24 @@ public class ClientTransactionListenerControllerTest {

        verify(mListener).onDisplayChanged(123);
    }

    @Test
    public void testActivityWindowInfoChangedListener() {
        mSetFlagsRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG);

        mController.registerActivityWindowInfoChangedListener(mActivityWindowInfoListener);
        final ActivityWindowInfo activityWindowInfo = new ActivityWindowInfo();
        activityWindowInfo.set(true /* isEmbedded */, new Rect(0, 0, 1000, 2000),
                new Rect(0, 0, 1000, 1000));
        mController.onActivityWindowInfoChanged(mActivityToken, activityWindowInfo);

        verify(mActivityWindowInfoListener).accept(mActivityToken, activityWindowInfo);

        clearInvocations(mActivityWindowInfoListener);
        mController.unregisterActivityWindowInfoChangedListener(mActivityWindowInfoListener);

        mController.onActivityWindowInfoChanged(mActivityToken, activityWindowInfo);

        verify(mActivityWindowInfoListener, never()).accept(any(), any());
    }
}