Restrict background activity start by task

We now also allow apps to start activity in its own background task, while
that task won't move to front.

Background activity is not allowed to be started in new task or app
is not in that task stack.
Also, task moving / reparent task is not allowed.

Bug: 128772406
Test: atest android.server.wm.BackgroundActivityLaunchTest
Change-Id: I20f501e755f30f9ef581f7f8c39a2875f959e5b8
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 4ef8753..fe937c2 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -160,6 +160,10 @@
     private int mCallingUid;
     private ActivityOptions mOptions;
 
+    // If it is true, background activity can only be started in an existing task that contains
+    // an activity with same uid.
+    private boolean mRestrictedBgActivity;
+
     private int mLaunchMode;
     private boolean mLaunchTaskBehind;
     private int mLaunchFlags;
@@ -455,6 +459,7 @@
         mIntent = starter.mIntent;
         mCallingUid = starter.mCallingUid;
         mOptions = starter.mOptions;
+        mRestrictedBgActivity = starter.mRestrictedBgActivity;
 
         mLaunchTaskBehind = starter.mLaunchTaskBehind;
         mLaunchFlags = starter.mLaunchFlags;
@@ -551,7 +556,8 @@
             mLastStartActivityTimeMs = System.currentTimeMillis();
             mLastStartActivityRecord[0] = r;
             mLastStartActivityResult = startActivity(r, sourceRecord, voiceSession, voiceInteractor,
-                    startFlags, doResume, options, inTask, mLastStartActivityRecord);
+                    startFlags, doResume, options, inTask, mLastStartActivityRecord,
+                    false /* restrictedBgActivity */);
             mSupervisor.getActivityMetricsLogger().notifyActivityLaunched(mLastStartActivityResult,
                     mLastStartActivityRecord[0]);
             return mLastStartActivityResult;
@@ -760,22 +766,17 @@
         abort |= !mService.mIntentFirewall.checkStartActivity(intent, callingUid,
                 callingPid, resolvedType, aInfo.applicationInfo);
 
-        boolean abortBackgroundStart = false;
+        boolean restrictedBgActivity = false;
         if (!abort) {
             try {
                 Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
                         "shouldAbortBackgroundActivityStart");
-                abortBackgroundStart = shouldAbortBackgroundActivityStart(callingUid,
+                restrictedBgActivity = shouldAbortBackgroundActivityStart(callingUid,
                         callingPid, callingPackage, realCallingUid, realCallingPid, callerApp,
                         originatingPendingIntent, allowBackgroundActivityStart, intent);
             } finally {
                 Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
             }
-            abort |= (abortBackgroundStart && !mService.isBackgroundActivityStartsEnabled());
-            // TODO: remove this toast after feature development is done
-            if (abortBackgroundStart) {
-                showBackgroundActivityBlockedToast(abort, callingPackage);
-            }
         }
 
         // Merge the two options bundles, while realCallerOptions takes precedence.
@@ -918,8 +919,10 @@
                 || stack.getResumedActivity().info.applicationInfo.uid != realCallingUid)) {
             if (!mService.checkAppSwitchAllowedLocked(callingPid, callingUid,
                     realCallingPid, realCallingUid, "Activity start")) {
-                mController.addPendingActivityLaunch(new PendingActivityLaunch(r,
-                        sourceRecord, startFlags, stack, callerApp));
+                if (!restrictedBgActivity) {
+                    mController.addPendingActivityLaunch(new PendingActivityLaunch(r,
+                            sourceRecord, startFlags, stack, callerApp));
+                }
                 ActivityOptions.abort(checkedOptions);
                 return ActivityManager.START_SWITCHES_CANCELED;
             }
@@ -929,7 +932,7 @@
         mController.doPendingActivityLaunches(false);
 
         final int res = startActivity(r, sourceRecord, voiceSession, voiceInteractor, startFlags,
-                true /* doResume */, checkedOptions, inTask, outActivity);
+                true /* doResume */, checkedOptions, inTask, outActivity, restrictedBgActivity);
         mSupervisor.getActivityMetricsLogger().notifyActivityLaunched(res, outActivity[0]);
         return res;
     }
@@ -1395,13 +1398,13 @@
     private int startActivity(final ActivityRecord r, ActivityRecord sourceRecord,
                 IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
                 int startFlags, boolean doResume, ActivityOptions options, TaskRecord inTask,
-                ActivityRecord[] outActivity) {
+                ActivityRecord[] outActivity, boolean restrictedBgActivity) {
         int result = START_CANCELED;
         final ActivityStack startedActivityStack;
         try {
             mService.mWindowManager.deferSurfaceLayout();
             result = startActivityUnchecked(r, sourceRecord, voiceSession, voiceInteractor,
-                    startFlags, doResume, options, inTask, outActivity);
+                    startFlags, doResume, options, inTask, outActivity, restrictedBgActivity);
         } finally {
             final ActivityStack currentStack = r.getActivityStack();
             startedActivityStack = currentStack != null ? currentStack : mTargetStack;
@@ -1437,14 +1440,40 @@
         return result;
     }
 
+    /**
+     * Return true if background activity is really aborted.
+     *
+     * TODO(b/131748165): Refactor the logic so we don't need to call this method everywhere.
+     */
+    private boolean handleBackgroundActivityAbort(ActivityRecord r) {
+        // TODO(b/131747138): Remove toast and refactor related code in Q release.
+        boolean abort = !mService.isBackgroundActivityStartsEnabled();
+        showBackgroundActivityBlockedToast(abort, r.launchedFromPackage);
+        if (!abort) {
+            return false;
+        }
+        ActivityRecord resultRecord = r.resultTo;
+        String resultWho = r.resultWho;
+        int requestCode = r.requestCode;
+        if (resultRecord != null) {
+            ActivityStack resultStack = resultRecord.getActivityStack();
+            resultStack.sendActivityResultLocked(-1, resultRecord, resultWho, requestCode,
+                    RESULT_CANCELED, null);
+        }
+        // We pretend to the caller that it was really started to make it backward compatible, but
+        // they will just get a cancel result.
+        ActivityOptions.abort(r.pendingOptions);
+        return true;
+    }
+
     // Note: This method should only be called from {@link startActivity}.
     private int startActivityUnchecked(final ActivityRecord r, ActivityRecord sourceRecord,
             IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
             int startFlags, boolean doResume, ActivityOptions options, TaskRecord inTask,
-            ActivityRecord[] outActivity) {
-
+            ActivityRecord[] outActivity, boolean restrictedBgActivity) {
         setInitialState(r, options, inTask, doResume, startFlags, sourceRecord, voiceSession,
-                voiceInteractor);
+                voiceInteractor, restrictedBgActivity);
+
         final int preferredWindowingMode = mLaunchParams.mWindowingMode;
 
         computeLaunchingTaskFlags();
@@ -1652,7 +1681,7 @@
         } else {
             // This not being started from an existing activity, and not part of a new task...
             // just put it in the top task, though these days this case should never happen.
-            setTaskToCurrentTopOrCreateNewTask();
+            result = setTaskToCurrentTopOrCreateNewTask();
         }
         if (result != START_SUCCESS) {
             return result;
@@ -1725,6 +1754,7 @@
         mIntent = null;
         mCallingUid = -1;
         mOptions = null;
+        mRestrictedBgActivity = false;
 
         mLaunchTaskBehind = false;
         mLaunchFlags = 0;
@@ -1764,7 +1794,8 @@
 
     private void setInitialState(ActivityRecord r, ActivityOptions options, TaskRecord inTask,
             boolean doResume, int startFlags, ActivityRecord sourceRecord,
-            IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor) {
+            IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
+            boolean restrictedBgActivity) {
         reset(false /* clearRequest */);
 
         mStartActivity = r;
@@ -1774,6 +1805,7 @@
         mSourceRecord = sourceRecord;
         mVoiceSession = voiceSession;
         mVoiceInteractor = voiceInteractor;
+        mRestrictedBgActivity = restrictedBgActivity;
 
         mLaunchParams.reset();
 
@@ -1874,6 +1906,11 @@
         }
 
         mNoAnimation = (mLaunchFlags & FLAG_ACTIVITY_NO_ANIMATION) != 0;
+
+        if (restrictedBgActivity) {
+            mAvoidMoveToFront = true;
+            mDoResume = false;
+        }
     }
 
     private void sendNewTaskResultRequestIfNeeded() {
@@ -2271,6 +2308,9 @@
         // isLockTaskModeViolation fails below.
 
         if (mReuseTask == null) {
+            if (mRestrictedBgActivity && handleBackgroundActivityAbort(mStartActivity)) {
+                return START_ABORTED;
+            }
             final TaskRecord task = mTargetStack.createTaskRecord(
                     mSupervisor.getNextTaskIdForUserLocked(mStartActivity.mUserId),
                     mNewTaskInfo != null ? mNewTaskInfo : mStartActivity.info,
@@ -2283,6 +2323,11 @@
             if (DEBUG_TASKS) Slog.v(TAG_TASKS, "Starting new activity " + mStartActivity
                     + " in new task " + mStartActivity.getTaskRecord());
         } else {
+            if (mRestrictedBgActivity && !mReuseTask.containsAppUid(mCallingUid)) {
+                if (handleBackgroundActivityAbort(mStartActivity)) {
+                    return START_ABORTED;
+                }
+            }
             addOrReparentStartingActivity(mReuseTask, "setTaskFromReuseOrCreateNewTask");
         }
 
@@ -2322,6 +2367,12 @@
 
         final TaskRecord sourceTask = mSourceRecord.getTaskRecord();
         final ActivityStack sourceStack = mSourceRecord.getActivityStack();
+        if (mRestrictedBgActivity && !sourceTask.containsAppUid(mCallingUid)) {
+            if (handleBackgroundActivityAbort(mStartActivity)) {
+                return START_ABORTED;
+            }
+            return START_ABORTED;
+        }
         // We only want to allow changing stack in two cases:
         // 1. If the target task is not the top one. Otherwise we would move the launching task to
         //    the other side, rather than show two side by side.
@@ -2483,20 +2534,33 @@
         }
     }
 
-    private void setTaskToCurrentTopOrCreateNewTask() {
+    private int setTaskToCurrentTopOrCreateNewTask() {
         mTargetStack = computeStackFocus(mStartActivity, false, mLaunchFlags, mOptions);
         if (mDoResume) {
             mTargetStack.moveToFront("addingToTopTask");
         }
         final ActivityRecord prev = mTargetStack.getTopActivity();
+        if (mRestrictedBgActivity && prev == null) {
+            if (handleBackgroundActivityAbort(mStartActivity)) {
+                return START_ABORTED;
+            }
+            return START_ABORTED;
+        }
         final TaskRecord task = (prev != null)
                 ? prev.getTaskRecord() : mTargetStack.createTaskRecord(
                 mSupervisor.getNextTaskIdForUserLocked(mStartActivity.mUserId), mStartActivity.info,
                 mIntent, null, null, true, mStartActivity, mSourceRecord, mOptions);
+        if (mRestrictedBgActivity && !task.containsAppUid(mCallingUid)) {
+            if (handleBackgroundActivityAbort(mStartActivity)) {
+                return START_ABORTED;
+            }
+            return START_ABORTED;
+        }
         addOrReparentStartingActivity(task, "setTaskToCurrentTopOrCreateNewTask");
         mTargetStack.positionChildWindowContainerAtTop(task);
         if (DEBUG_TASKS) Slog.v(TAG_TASKS, "Starting new activity " + mStartActivity
                 + " in new guessed " + mStartActivity.getTaskRecord());
+        return START_SUCCESS;
     }
 
     private void addOrReparentStartingActivity(TaskRecord parent, String reason) {
diff --git a/services/core/java/com/android/server/wm/TaskRecord.java b/services/core/java/com/android/server/wm/TaskRecord.java
index 15060e1..c26e3fd 100644
--- a/services/core/java/com/android/server/wm/TaskRecord.java
+++ b/services/core/java/com/android/server/wm/TaskRecord.java
@@ -1161,6 +1161,19 @@
         return false;
     }
 
+    /**
+     * Return true if any activities in this task belongs to input uid.
+     */
+    boolean containsAppUid(int uid) {
+        for (int i = mActivities.size() - 1; i >= 0; --i) {
+            final ActivityRecord r = mActivities.get(i);
+            if (r.getUid() == uid) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     void getAllRunningVisibleActivitiesLocked(ArrayList<ActivityRecord> outActivities) {
         if (mStack != null) {
             for (int activityNdx = mActivities.size() - 1; activityNdx >= 0; --activityNdx) {