Merge "Don't finish activity on duplicate resume request" into pi-dev
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 1df724e..50a4398 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -3736,54 +3736,64 @@
//Slog.i(TAG, "Running services: " + mServices);
}
- ActivityClientRecord performResumeActivity(IBinder token, boolean finalStateRequest,
+ /**
+ * Resume the activity.
+ * @param token Target activity token.
+ * @param finalStateRequest Flag indicating if this is part of final state resolution for a
+ * transaction.
+ * @param reason Reason for performing the action.
+ *
+ * @return The {@link ActivityClientRecord} that was resumed, {@code null} otherwise.
+ */
+ @VisibleForTesting
+ public ActivityClientRecord performResumeActivity(IBinder token, boolean finalStateRequest,
String reason) {
- ActivityClientRecord r = mActivities.get(token);
- if (localLOGV) Slog.v(TAG, "Performing resume of " + r
- + " finished=" + r.activity.mFinished);
- if (r != null && !r.activity.mFinished) {
- if (r.getLifecycleState() == ON_RESUME) {
- if (!finalStateRequest) {
- final RuntimeException e = new IllegalStateException(
- "Trying to resume activity which is already resumed");
- Slog.e(TAG, e.getMessage(), e);
- Slog.e(TAG, r.getStateString());
- // TODO(lifecycler): A double resume request is possible when an activity
- // receives two consequent transactions with relaunch requests and "resumed"
- // final state requests and the second relaunch is omitted. We still try to
- // handle two resume requests for the final state. For cases other than this
- // one, we don't expect it to happen.
- }
- return null;
+ final ActivityClientRecord r = mActivities.get(token);
+ if (localLOGV) {
+ Slog.v(TAG, "Performing resume of " + r + " finished=" + r.activity.mFinished);
+ }
+ if (r == null || r.activity.mFinished) {
+ return null;
+ }
+ if (r.getLifecycleState() == ON_RESUME) {
+ if (!finalStateRequest) {
+ final RuntimeException e = new IllegalStateException(
+ "Trying to resume activity which is already resumed");
+ Slog.e(TAG, e.getMessage(), e);
+ Slog.e(TAG, r.getStateString());
+ // TODO(lifecycler): A double resume request is possible when an activity
+ // receives two consequent transactions with relaunch requests and "resumed"
+ // final state requests and the second relaunch is omitted. We still try to
+ // handle two resume requests for the final state. For cases other than this
+ // one, we don't expect it to happen.
}
- if (finalStateRequest) {
- r.hideForNow = false;
- r.activity.mStartedActivity = false;
+ return null;
+ }
+ if (finalStateRequest) {
+ r.hideForNow = false;
+ r.activity.mStartedActivity = false;
+ }
+ try {
+ r.activity.onStateNotSaved();
+ r.activity.mFragments.noteStateNotSaved();
+ checkAndBlockForNetworkAccess();
+ if (r.pendingIntents != null) {
+ deliverNewIntents(r, r.pendingIntents);
+ r.pendingIntents = null;
}
- try {
- r.activity.onStateNotSaved();
- r.activity.mFragments.noteStateNotSaved();
- checkAndBlockForNetworkAccess();
- if (r.pendingIntents != null) {
- deliverNewIntents(r, r.pendingIntents);
- r.pendingIntents = null;
- }
- if (r.pendingResults != null) {
- deliverResults(r, r.pendingResults);
- r.pendingResults = null;
- }
- r.activity.performResume(r.startsNotResumed, reason);
+ if (r.pendingResults != null) {
+ deliverResults(r, r.pendingResults);
+ r.pendingResults = null;
+ }
+ r.activity.performResume(r.startsNotResumed, reason);
- r.state = null;
- r.persistentState = null;
- r.setState(ON_RESUME);
- } catch (Exception e) {
- if (!mInstrumentation.onException(r.activity, e)) {
- throw new RuntimeException(
- "Unable to resume activity "
- + r.intent.getComponent().toShortString()
- + ": " + e.toString(), e);
- }
+ r.state = null;
+ r.persistentState = null;
+ r.setState(ON_RESUME);
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException("Unable to resume activity "
+ + r.intent.getComponent().toShortString() + ": " + e.toString(), e);
}
}
return r;
@@ -3816,126 +3826,115 @@
// TODO Push resumeArgs into the activity for consideration
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
+ if (r == null) {
+ // We didn't actually resume the activity, so skipping any follow-up actions.
+ return;
+ }
- if (r != null) {
- final Activity a = r.activity;
+ final Activity a = r.activity;
- if (localLOGV) Slog.v(
- TAG, "Resume " + r + " started activity: " +
- a.mStartedActivity + ", hideForNow: " + r.hideForNow
- + ", finished: " + a.mFinished);
+ if (localLOGV) {
+ Slog.v(TAG, "Resume " + r + " started activity: " + a.mStartedActivity
+ + ", hideForNow: " + r.hideForNow + ", finished: " + a.mFinished);
+ }
- final int forwardBit = isForward ?
- WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;
+ final int forwardBit = isForward
+ ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;
- // If the window hasn't yet been added to the window manager,
- // and this guy didn't finish itself or start another activity,
- // then go ahead and add the window.
- boolean willBeVisible = !a.mStartedActivity;
- if (!willBeVisible) {
- try {
- willBeVisible = ActivityManager.getService().willActivityBeVisible(
- a.getActivityToken());
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ // If the window hasn't yet been added to the window manager,
+ // and this guy didn't finish itself or start another activity,
+ // then go ahead and add the window.
+ boolean willBeVisible = !a.mStartedActivity;
+ if (!willBeVisible) {
+ try {
+ willBeVisible = ActivityManager.getService().willActivityBeVisible(
+ a.getActivityToken());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ if (r.window == null && !a.mFinished && willBeVisible) {
+ r.window = r.activity.getWindow();
+ View decor = r.window.getDecorView();
+ decor.setVisibility(View.INVISIBLE);
+ ViewManager wm = a.getWindowManager();
+ WindowManager.LayoutParams l = r.window.getAttributes();
+ a.mDecor = decor;
+ l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+ l.softInputMode |= forwardBit;
+ if (r.mPreserveWindow) {
+ a.mWindowAdded = true;
+ r.mPreserveWindow = false;
+ // Normally the ViewRoot sets up callbacks with the Activity
+ // in addView->ViewRootImpl#setView. If we are instead reusing
+ // the decor view we have to notify the view root that the
+ // callbacks may have changed.
+ ViewRootImpl impl = decor.getViewRootImpl();
+ if (impl != null) {
+ impl.notifyChildRebuilt();
}
}
- if (r.window == null && !a.mFinished && willBeVisible) {
- r.window = r.activity.getWindow();
- View decor = r.window.getDecorView();
- decor.setVisibility(View.INVISIBLE);
- ViewManager wm = a.getWindowManager();
- WindowManager.LayoutParams l = r.window.getAttributes();
- a.mDecor = decor;
- l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
- l.softInputMode |= forwardBit;
- if (r.mPreserveWindow) {
+ if (a.mVisibleFromClient) {
+ if (!a.mWindowAdded) {
a.mWindowAdded = true;
- r.mPreserveWindow = false;
- // Normally the ViewRoot sets up callbacks with the Activity
- // in addView->ViewRootImpl#setView. If we are instead reusing
- // the decor view we have to notify the view root that the
- // callbacks may have changed.
- ViewRootImpl impl = decor.getViewRootImpl();
- if (impl != null) {
- impl.notifyChildRebuilt();
- }
+ wm.addView(decor, l);
+ } else {
+ // The activity will get a callback for this {@link LayoutParams} change
+ // earlier. However, at that time the decor will not be set (this is set
+ // in this method), so no action will be taken. This call ensures the
+ // callback occurs with the decor set.
+ a.onWindowAttributesChanged(l);
}
- if (a.mVisibleFromClient) {
- if (!a.mWindowAdded) {
- a.mWindowAdded = true;
- wm.addView(decor, l);
- } else {
- // The activity will get a callback for this {@link LayoutParams} change
- // earlier. However, at that time the decor will not be set (this is set
- // in this method), so no action will be taken. This call ensures the
- // callback occurs with the decor set.
- a.onWindowAttributesChanged(l);
- }
- }
+ }
// If the window has already been added, but during resume
// we started another activity, then don't yet make the
// window visible.
- } else if (!willBeVisible) {
- if (localLOGV) Slog.v(
- TAG, "Launch " + r + " mStartedActivity set");
- r.hideForNow = true;
+ } else if (!willBeVisible) {
+ if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
+ r.hideForNow = true;
+ }
+
+ // Get rid of anything left hanging around.
+ cleanUpPendingRemoveWindows(r, false /* force */);
+
+ // The window is now visible if it has been added, we are not
+ // simply finishing, and we are not starting another activity.
+ if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {
+ if (r.newConfig != null) {
+ performConfigurationChangedForActivity(r, r.newConfig);
+ if (DEBUG_CONFIGURATION) {
+ Slog.v(TAG, "Resuming activity " + r.activityInfo.name + " with newConfig "
+ + r.activity.mCurrentConfig);
+ }
+ r.newConfig = null;
}
-
- // Get rid of anything left hanging around.
- cleanUpPendingRemoveWindows(r, false /* force */);
-
- // The window is now visible if it has been added, we are not
- // simply finishing, and we are not starting another activity.
- if (!r.activity.mFinished && willBeVisible
- && r.activity.mDecor != null && !r.hideForNow) {
- if (r.newConfig != null) {
- performConfigurationChangedForActivity(r, r.newConfig);
- if (DEBUG_CONFIGURATION) Slog.v(TAG, "Resuming activity "
- + r.activityInfo.name + " with newConfig " + r.activity.mCurrentConfig);
- r.newConfig = null;
- }
- if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward="
- + isForward);
- WindowManager.LayoutParams l = r.window.getAttributes();
- if ((l.softInputMode
- & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
- != forwardBit) {
- l.softInputMode = (l.softInputMode
- & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))
- | forwardBit;
- if (r.activity.mVisibleFromClient) {
- ViewManager wm = a.getWindowManager();
- View decor = r.window.getDecorView();
- wm.updateViewLayout(decor, l);
- }
- }
-
- r.activity.mVisibleFromServer = true;
- mNumVisibleActivities++;
+ if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward=" + isForward);
+ WindowManager.LayoutParams l = r.window.getAttributes();
+ if ((l.softInputMode
+ & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
+ != forwardBit) {
+ l.softInputMode = (l.softInputMode
+ & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))
+ | forwardBit;
if (r.activity.mVisibleFromClient) {
- r.activity.makeVisible();
+ ViewManager wm = a.getWindowManager();
+ View decor = r.window.getDecorView();
+ wm.updateViewLayout(decor, l);
}
}
- r.nextIdle = mNewActivities;
- mNewActivities = r;
- if (localLOGV) {
- Slog.v(TAG, "Scheduling idle handler for " + r);
- }
- Looper.myQueue().addIdleHandler(new Idler());
- } else {
- // If an exception was thrown when trying to resume, then
- // just end this activity.
- try {
- ActivityManager.getService()
- .finishActivity(token, Activity.RESULT_CANCELED, null,
- Activity.DONT_FINISH_TASK_WITH_ACTIVITY);
- } catch (RemoteException ex) {
- throw ex.rethrowFromSystemServer();
+ r.activity.mVisibleFromServer = true;
+ mNumVisibleActivities++;
+ if (r.activity.mVisibleFromClient) {
+ r.activity.makeVisible();
}
}
+
+ r.nextIdle = mNewActivities;
+ mNewActivities = r;
+ if (localLOGV) Slog.v(TAG, "Scheduling idle handler for " + r);
+ Looper.myQueue().addIdleHandler(new Idler());
}
@Override
diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java
index 925080e..0639b00 100644
--- a/core/java/android/app/ClientTransactionHandler.java
+++ b/core/java/android/app/ClientTransactionHandler.java
@@ -24,6 +24,7 @@
import android.os.IBinder;
import android.util.MergedConfiguration;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.ReferrerIntent;
import java.io.PrintWriter;
@@ -48,7 +49,8 @@
* Execute transaction immediately without scheduling it. This is used for local requests, so
* it will also recycle the transaction.
*/
- void executeTransaction(ClientTransaction transaction) {
+ @VisibleForTesting
+ public void executeTransaction(ClientTransaction transaction) {
transaction.preExecute(this);
getTransactionExecutor().execute(transaction);
transaction.recycle();
diff --git a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
index a99e139..9ef58ab 100644
--- a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
+++ b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
@@ -16,7 +16,10 @@
package android.app.activity;
+import static org.junit.Assert.assertNull;
+
import android.app.Activity;
+import android.app.ActivityThread;
import android.app.IApplicationThread;
import android.app.servertransaction.ActivityRelaunchItem;
import android.app.servertransaction.ClientTransaction;
@@ -76,6 +79,21 @@
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
+ /** Verify that repeated resume requests to activity will be ignored. */
+ @Test
+ public void testRepeatedResume() throws Exception {
+ final Activity activity = mActivityTestRule.launchActivity(new Intent());
+ final ActivityThread activityThread = activity.getActivityThread();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ activityThread.executeTransaction(newResumeTransaction(activity));
+ assertNull(activityThread.performResumeActivity(activity.getActivityToken(),
+ true /* finalStateRequest */, "test"));
+
+ assertNull(activityThread.performResumeActivity(activity.getActivityToken(),
+ false /* finalStateRequest */, "test"));
+ });
+ }
+
private static ClientTransaction newRelaunchResumeTransaction(Activity activity) {
final ClientTransactionItem callbackItem = ActivityRelaunchItem.obtain(null,
null, 0, new MergedConfiguration(), false /* preserveWindow */);