Loading core/java/android/app/Instrumentation.java +39 −0 Original line number Diff line number Diff line Loading @@ -783,6 +783,17 @@ public class Instrumentation { return null; } /** * This is called after starting an Activity and provides the result code that defined in * {@link ActivityManager}, like {@link ActivityManager#START_SUCCESS}. * * @param result the result code that returns after starting an Activity. * @param bOptions the bundle generated from {@link ActivityOptions} that originally * being used to start the Activity. * @hide */ public void onStartActivityResult(int result, @NonNull Bundle bOptions) {} final boolean match(Context who, Activity activity, Intent intent) { Loading Loading @@ -1344,6 +1355,28 @@ public class Instrumentation { return apk.getAppFactory(); } /** * This should be called before {@link #checkStartActivityResult(int, Object)}, because * exceptions might be thrown while checking the results. */ private void notifyStartActivityResult(int result, @Nullable Bundle options) { if (mActivityMonitors == null) { return; } synchronized (mSync) { final int size = mActivityMonitors.size(); for (int i = 0; i < size; i++) { final ActivityMonitor am = mActivityMonitors.get(i); if (am.ignoreMatchingSpecificIntents()) { if (options == null) { options = ActivityOptions.makeBasic().toBundle(); } am.onStartActivityResult(result, options); } } } } private void prePerformCreate(Activity activity) { if (mWaitingActivities != null) { synchronized (mSync) { Loading Loading @@ -1802,6 +1835,7 @@ public class Instrumentation { who.getOpPackageName(), who.getAttributionTag(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target != null ? target.mEmbeddedID : null, requestCode, 0, null, options); notifyStartActivityResult(result, options); checkStartActivityResult(result, intent); } catch (RemoteException e) { throw new RuntimeException("Failure from system", e); Loading Loading @@ -1876,6 +1910,7 @@ public class Instrumentation { int result = ActivityTaskManager.getService().startActivities(whoThread, who.getOpPackageName(), who.getAttributionTag(), intents, resolvedTypes, token, options, userId); notifyStartActivityResult(result, options); checkStartActivityResult(result, intents[0]); return result; } catch (RemoteException e) { Loading Loading @@ -1947,6 +1982,7 @@ public class Instrumentation { who.getOpPackageName(), who.getAttributionTag(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target, requestCode, 0, null, options); notifyStartActivityResult(result, options); checkStartActivityResult(result, intent); } catch (RemoteException e) { throw new RuntimeException("Failure from system", e); Loading Loading @@ -2017,6 +2053,7 @@ public class Instrumentation { who.getOpPackageName(), who.getAttributionTag(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, resultWho, requestCode, 0, null, options, user.getIdentifier()); notifyStartActivityResult(result, options); checkStartActivityResult(result, intent); } catch (RemoteException e) { throw new RuntimeException("Failure from system", e); Loading Loading @@ -2068,6 +2105,7 @@ public class Instrumentation { token, target != null ? target.mEmbeddedID : null, requestCode, 0, null, options, ignoreTargetSecurity, userId); notifyStartActivityResult(result, options); checkStartActivityResult(result, intent); } catch (RemoteException e) { throw new RuntimeException("Failure from system", e); Loading Loading @@ -2115,6 +2153,7 @@ public class Instrumentation { int result = appTask.startActivity(whoThread.asBinder(), who.getOpPackageName(), who.getAttributionTag(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), options); notifyStartActivityResult(result, options); checkStartActivityResult(result, intent); } catch (RemoteException e) { throw new RuntimeException("Failure from system", e); Loading libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +31 −2 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package androidx.window.extensions.embedding; import static android.app.ActivityManager.START_SUCCESS; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; Loading Loading @@ -97,6 +98,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen private final List<SplitInfo> mLastReportedSplitStates = new ArrayList<>(); private final Handler mHandler; private final Object mLock = new Object(); private final ActivityStartMonitor mActivityStartMonitor; public SplitController() { final MainThreadExecutor executor = new MainThreadExecutor(); Loading @@ -108,7 +110,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen new LifecycleCallbacks()); // Intercept activity starts to route activities to new containers if necessary. Instrumentation instrumentation = activityThread.getInstrumentation(); instrumentation.addMonitor(new ActivityStartMonitor()); mActivityStartMonitor = new ActivityStartMonitor(); instrumentation.addMonitor(mActivityStartMonitor); } /** Updates the embedding rules applied to future activity launches. */ Loading Loading @@ -1385,6 +1388,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return ActivityThread.currentActivityThread().getActivity(activityToken); } @VisibleForTesting ActivityStartMonitor getActivityStartMonitor() { return mActivityStartMonitor; } /** * Gets the token of the initial TaskFragment that embedded this activity. Do not rely on it * after creation because the activity could be reparented. Loading Loading @@ -1536,7 +1544,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * A monitor that intercepts all activity start requests originating in the client process and * can amend them to target a specific task fragment to form a split. */ private class ActivityStartMonitor extends Instrumentation.ActivityMonitor { @VisibleForTesting class ActivityStartMonitor extends Instrumentation.ActivityMonitor { @VisibleForTesting Intent mCurrentIntent; @Override public Instrumentation.ActivityResult onStartActivity(@NonNull Context who, Loading Loading @@ -1564,11 +1575,29 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // the dedicated container. options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, launchedInTaskFragment.getTaskFragmentToken()); mCurrentIntent = intent; } } return super.onStartActivity(who, intent, options); } @Override public void onStartActivityResult(int result, @NonNull Bundle bOptions) { super.onStartActivityResult(result, bOptions); if (mCurrentIntent != null && result != START_SUCCESS) { // Clear the pending appeared intent if the activity was not started successfully. final IBinder token = bOptions.getBinder( ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN); if (token != null) { final TaskFragmentContainer container = getContainer(token); if (container != null) { container.clearPendingAppearedIntentIfNeeded(mCurrentIntent); } } } mCurrentIntent = null; } } /** Loading libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +24 −3 Original line number Diff line number Diff line Loading @@ -198,6 +198,22 @@ class TaskFragmentContainer { return mPendingAppearedIntent; } void setPendingAppearedIntent(@Nullable Intent intent) { mPendingAppearedIntent = intent; } /** * Clears the pending appeared Intent if it is the same as given Intent. Otherwise, the * pending appeared Intent is cleared when TaskFragmentInfo is set and is not empty (has * running activities). */ void clearPendingAppearedIntentIfNeeded(@NonNull Intent intent) { if (mPendingAppearedIntent == null || mPendingAppearedIntent != intent) { return; } mPendingAppearedIntent = null; } boolean hasActivity(@NonNull IBinder token) { if (mInfo != null && mInfo.getActivities().contains(token)) { return true; Loading Loading @@ -230,13 +246,18 @@ class TaskFragmentContainer { void setInfo(@NonNull TaskFragmentInfo info) { if (!mIsFinished && mInfo == null && info.isEmpty()) { // onTaskFragmentAppeared with empty info. We will remove the TaskFragment if it is // still empty after timeout. // onTaskFragmentAppeared with empty info. We will remove the TaskFragment if no // pending appeared intent/activities. Otherwise, wait and removing the TaskFragment if // it is still empty after timeout. mAppearEmptyTimeout = () -> { mAppearEmptyTimeout = null; mController.onTaskFragmentAppearEmptyTimeout(this); }; if (mPendingAppearedIntent != null || !mPendingAppearedActivities.isEmpty()) { mController.getHandler().postDelayed(mAppearEmptyTimeout, APPEAR_EMPTY_TIMEOUT_MS); } else { mAppearEmptyTimeout.run(); } } else if (mAppearEmptyTimeout != null && !info.isEmpty()) { mController.getHandler().removeCallbacks(mAppearEmptyTimeout); mAppearEmptyTimeout = null; Loading libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +21 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package androidx.window.extensions.embedding; import static android.app.ActivityManager.START_CANCELED; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; Loading Loading @@ -292,6 +293,26 @@ public class SplitControllerTest { verify(mSplitPresenter).updateSplitContainer(splitContainer, tf, mTransaction); } @Test public void testOnStartActivityResultError() { final Intent intent = new Intent(); final TaskContainer taskContainer = new TaskContainer(TASK_ID); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, intent, taskContainer, mSplitController); final SplitController.ActivityStartMonitor monitor = mSplitController.getActivityStartMonitor(); container.setPendingAppearedIntent(intent); final Bundle bundle = new Bundle(); bundle.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, container.getTaskFragmentToken()); monitor.mCurrentIntent = intent; doReturn(container).when(mSplitController).getContainer(any()); monitor.onStartActivityResult(START_CANCELED, bundle); assertNull(container.getPendingAppearedIntent()); } @Test public void testOnActivityCreated() { mSplitController.onActivityCreated(mActivity); Loading libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java +9 −8 Original line number Diff line number Diff line Loading @@ -209,21 +209,21 @@ public class TaskFragmentContainerTest { assertNull(container.mAppearEmptyTimeout); // Not set if it is not appeared empty. final TaskFragmentInfo info = mock(TaskFragmentInfo.class); doReturn(new ArrayList<>()).when(info).getActivities(); doReturn(false).when(info).isEmpty(); container.setInfo(info); assertNull(container.mAppearEmptyTimeout); // Set timeout if the first info set is empty. final TaskFragmentInfo info = mock(TaskFragmentInfo.class); container.mInfo = null; doReturn(true).when(info).isEmpty(); container.setInfo(info); assertNotNull(container.mAppearEmptyTimeout); // Not set if it is not appeared empty. doReturn(new ArrayList<>()).when(info).getActivities(); doReturn(false).when(info).isEmpty(); container.setInfo(info); assertNull(container.mAppearEmptyTimeout); // Remove timeout after the container becomes non-empty. doReturn(false).when(info).isEmpty(); container.setInfo(info); Loading @@ -232,6 +232,7 @@ public class TaskFragmentContainerTest { // Running the timeout will call into SplitController.onTaskFragmentAppearEmptyTimeout. container.mInfo = null; container.setPendingAppearedIntent(mIntent); doReturn(true).when(info).isEmpty(); container.setInfo(info); container.mAppearEmptyTimeout.run(); Loading Loading
core/java/android/app/Instrumentation.java +39 −0 Original line number Diff line number Diff line Loading @@ -783,6 +783,17 @@ public class Instrumentation { return null; } /** * This is called after starting an Activity and provides the result code that defined in * {@link ActivityManager}, like {@link ActivityManager#START_SUCCESS}. * * @param result the result code that returns after starting an Activity. * @param bOptions the bundle generated from {@link ActivityOptions} that originally * being used to start the Activity. * @hide */ public void onStartActivityResult(int result, @NonNull Bundle bOptions) {} final boolean match(Context who, Activity activity, Intent intent) { Loading Loading @@ -1344,6 +1355,28 @@ public class Instrumentation { return apk.getAppFactory(); } /** * This should be called before {@link #checkStartActivityResult(int, Object)}, because * exceptions might be thrown while checking the results. */ private void notifyStartActivityResult(int result, @Nullable Bundle options) { if (mActivityMonitors == null) { return; } synchronized (mSync) { final int size = mActivityMonitors.size(); for (int i = 0; i < size; i++) { final ActivityMonitor am = mActivityMonitors.get(i); if (am.ignoreMatchingSpecificIntents()) { if (options == null) { options = ActivityOptions.makeBasic().toBundle(); } am.onStartActivityResult(result, options); } } } } private void prePerformCreate(Activity activity) { if (mWaitingActivities != null) { synchronized (mSync) { Loading Loading @@ -1802,6 +1835,7 @@ public class Instrumentation { who.getOpPackageName(), who.getAttributionTag(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target != null ? target.mEmbeddedID : null, requestCode, 0, null, options); notifyStartActivityResult(result, options); checkStartActivityResult(result, intent); } catch (RemoteException e) { throw new RuntimeException("Failure from system", e); Loading Loading @@ -1876,6 +1910,7 @@ public class Instrumentation { int result = ActivityTaskManager.getService().startActivities(whoThread, who.getOpPackageName(), who.getAttributionTag(), intents, resolvedTypes, token, options, userId); notifyStartActivityResult(result, options); checkStartActivityResult(result, intents[0]); return result; } catch (RemoteException e) { Loading Loading @@ -1947,6 +1982,7 @@ public class Instrumentation { who.getOpPackageName(), who.getAttributionTag(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target, requestCode, 0, null, options); notifyStartActivityResult(result, options); checkStartActivityResult(result, intent); } catch (RemoteException e) { throw new RuntimeException("Failure from system", e); Loading Loading @@ -2017,6 +2053,7 @@ public class Instrumentation { who.getOpPackageName(), who.getAttributionTag(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, resultWho, requestCode, 0, null, options, user.getIdentifier()); notifyStartActivityResult(result, options); checkStartActivityResult(result, intent); } catch (RemoteException e) { throw new RuntimeException("Failure from system", e); Loading Loading @@ -2068,6 +2105,7 @@ public class Instrumentation { token, target != null ? target.mEmbeddedID : null, requestCode, 0, null, options, ignoreTargetSecurity, userId); notifyStartActivityResult(result, options); checkStartActivityResult(result, intent); } catch (RemoteException e) { throw new RuntimeException("Failure from system", e); Loading Loading @@ -2115,6 +2153,7 @@ public class Instrumentation { int result = appTask.startActivity(whoThread.asBinder(), who.getOpPackageName(), who.getAttributionTag(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), options); notifyStartActivityResult(result, options); checkStartActivityResult(result, intent); } catch (RemoteException e) { throw new RuntimeException("Failure from system", e); Loading
libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +31 −2 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package androidx.window.extensions.embedding; import static android.app.ActivityManager.START_SUCCESS; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; Loading Loading @@ -97,6 +98,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen private final List<SplitInfo> mLastReportedSplitStates = new ArrayList<>(); private final Handler mHandler; private final Object mLock = new Object(); private final ActivityStartMonitor mActivityStartMonitor; public SplitController() { final MainThreadExecutor executor = new MainThreadExecutor(); Loading @@ -108,7 +110,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen new LifecycleCallbacks()); // Intercept activity starts to route activities to new containers if necessary. Instrumentation instrumentation = activityThread.getInstrumentation(); instrumentation.addMonitor(new ActivityStartMonitor()); mActivityStartMonitor = new ActivityStartMonitor(); instrumentation.addMonitor(mActivityStartMonitor); } /** Updates the embedding rules applied to future activity launches. */ Loading Loading @@ -1385,6 +1388,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return ActivityThread.currentActivityThread().getActivity(activityToken); } @VisibleForTesting ActivityStartMonitor getActivityStartMonitor() { return mActivityStartMonitor; } /** * Gets the token of the initial TaskFragment that embedded this activity. Do not rely on it * after creation because the activity could be reparented. Loading Loading @@ -1536,7 +1544,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * A monitor that intercepts all activity start requests originating in the client process and * can amend them to target a specific task fragment to form a split. */ private class ActivityStartMonitor extends Instrumentation.ActivityMonitor { @VisibleForTesting class ActivityStartMonitor extends Instrumentation.ActivityMonitor { @VisibleForTesting Intent mCurrentIntent; @Override public Instrumentation.ActivityResult onStartActivity(@NonNull Context who, Loading Loading @@ -1564,11 +1575,29 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // the dedicated container. options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, launchedInTaskFragment.getTaskFragmentToken()); mCurrentIntent = intent; } } return super.onStartActivity(who, intent, options); } @Override public void onStartActivityResult(int result, @NonNull Bundle bOptions) { super.onStartActivityResult(result, bOptions); if (mCurrentIntent != null && result != START_SUCCESS) { // Clear the pending appeared intent if the activity was not started successfully. final IBinder token = bOptions.getBinder( ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN); if (token != null) { final TaskFragmentContainer container = getContainer(token); if (container != null) { container.clearPendingAppearedIntentIfNeeded(mCurrentIntent); } } } mCurrentIntent = null; } } /** Loading
libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +24 −3 Original line number Diff line number Diff line Loading @@ -198,6 +198,22 @@ class TaskFragmentContainer { return mPendingAppearedIntent; } void setPendingAppearedIntent(@Nullable Intent intent) { mPendingAppearedIntent = intent; } /** * Clears the pending appeared Intent if it is the same as given Intent. Otherwise, the * pending appeared Intent is cleared when TaskFragmentInfo is set and is not empty (has * running activities). */ void clearPendingAppearedIntentIfNeeded(@NonNull Intent intent) { if (mPendingAppearedIntent == null || mPendingAppearedIntent != intent) { return; } mPendingAppearedIntent = null; } boolean hasActivity(@NonNull IBinder token) { if (mInfo != null && mInfo.getActivities().contains(token)) { return true; Loading Loading @@ -230,13 +246,18 @@ class TaskFragmentContainer { void setInfo(@NonNull TaskFragmentInfo info) { if (!mIsFinished && mInfo == null && info.isEmpty()) { // onTaskFragmentAppeared with empty info. We will remove the TaskFragment if it is // still empty after timeout. // onTaskFragmentAppeared with empty info. We will remove the TaskFragment if no // pending appeared intent/activities. Otherwise, wait and removing the TaskFragment if // it is still empty after timeout. mAppearEmptyTimeout = () -> { mAppearEmptyTimeout = null; mController.onTaskFragmentAppearEmptyTimeout(this); }; if (mPendingAppearedIntent != null || !mPendingAppearedActivities.isEmpty()) { mController.getHandler().postDelayed(mAppearEmptyTimeout, APPEAR_EMPTY_TIMEOUT_MS); } else { mAppearEmptyTimeout.run(); } } else if (mAppearEmptyTimeout != null && !info.isEmpty()) { mController.getHandler().removeCallbacks(mAppearEmptyTimeout); mAppearEmptyTimeout = null; Loading
libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +21 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package androidx.window.extensions.embedding; import static android.app.ActivityManager.START_CANCELED; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; Loading Loading @@ -292,6 +293,26 @@ public class SplitControllerTest { verify(mSplitPresenter).updateSplitContainer(splitContainer, tf, mTransaction); } @Test public void testOnStartActivityResultError() { final Intent intent = new Intent(); final TaskContainer taskContainer = new TaskContainer(TASK_ID); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, intent, taskContainer, mSplitController); final SplitController.ActivityStartMonitor monitor = mSplitController.getActivityStartMonitor(); container.setPendingAppearedIntent(intent); final Bundle bundle = new Bundle(); bundle.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, container.getTaskFragmentToken()); monitor.mCurrentIntent = intent; doReturn(container).when(mSplitController).getContainer(any()); monitor.onStartActivityResult(START_CANCELED, bundle); assertNull(container.getPendingAppearedIntent()); } @Test public void testOnActivityCreated() { mSplitController.onActivityCreated(mActivity); Loading
libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java +9 −8 Original line number Diff line number Diff line Loading @@ -209,21 +209,21 @@ public class TaskFragmentContainerTest { assertNull(container.mAppearEmptyTimeout); // Not set if it is not appeared empty. final TaskFragmentInfo info = mock(TaskFragmentInfo.class); doReturn(new ArrayList<>()).when(info).getActivities(); doReturn(false).when(info).isEmpty(); container.setInfo(info); assertNull(container.mAppearEmptyTimeout); // Set timeout if the first info set is empty. final TaskFragmentInfo info = mock(TaskFragmentInfo.class); container.mInfo = null; doReturn(true).when(info).isEmpty(); container.setInfo(info); assertNotNull(container.mAppearEmptyTimeout); // Not set if it is not appeared empty. doReturn(new ArrayList<>()).when(info).getActivities(); doReturn(false).when(info).isEmpty(); container.setInfo(info); assertNull(container.mAppearEmptyTimeout); // Remove timeout after the container becomes non-empty. doReturn(false).when(info).isEmpty(); container.setInfo(info); Loading @@ -232,6 +232,7 @@ public class TaskFragmentContainerTest { // Running the timeout will call into SplitController.onTaskFragmentAppearEmptyTimeout. container.mInfo = null; container.setPendingAppearedIntent(mIntent); doReturn(true).when(info).isEmpty(); container.setInfo(info); container.mAppearEmptyTimeout.run(); Loading