Merge "Prevent waiting if no new activity instance launched" into tm-qpr-dev

This commit is contained in:
Louis Chang
2022-07-20 08:14:27 +00:00
committed by Android (Google) Code Review
5 changed files with 124 additions and 13 deletions

View File

@@ -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) {
@@ -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) {
@@ -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);
@@ -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) {
@@ -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);
@@ -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);
@@ -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);
@@ -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);

View File

@@ -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;
@@ -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();
@@ -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. */
@@ -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.
@@ -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,
@@ -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;
}
}
/**

View File

@@ -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;
@@ -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);
};
mController.getHandler().postDelayed(mAppearEmptyTimeout, APPEAR_EMPTY_TIMEOUT_MS);
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;

View File

@@ -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;
@@ -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);

View File

@@ -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);
@@ -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();