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 */);