diff options
4 files changed, 241 insertions, 2 deletions
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 41151c0dc647..a80d70e56c69 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -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) { diff --git a/core/java/android/app/servertransaction/ClientTransactionListenerController.java b/core/java/android/app/servertransaction/ClientTransactionListenerController.java index 1a8136e06c28..7383d07c82e9 100644 --- a/core/java/android/app/servertransaction/ClientTransactionListenerController.java +++ b/core/java/android/app/servertransaction/ClientTransactionListenerController.java @@ -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() { @@ -62,6 +76,57 @@ public class ClientTransactionListenerController { } /** + * 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. */ diff --git a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java index d115bf306b45..2963acf17b9e 100644 --- a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java +++ b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java @@ -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 diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java index 213fd7bd494d..77d31a5f27e7 100644 --- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java +++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java @@ -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()); + } } |