Show a scrim activity if task is not resizable

Add a callback to TaskStackChangeListener which gets fired when the system
might need to inform the user that a specific app might not work in
multi-window.

Use that callback in SysUI to show a translucent activity which scrims the
activity behind to inform that it might not be resizable.

Debounce the information to once per multi-window session, to not make it
annoying.

Introduce launchTaskId to start an activity in an existing task, and protect
that with START_TASKS_FROM_RECENTS permission.

Bug: 27327287
Bug: 27431869
Change-Id: I89e8d653872ab01ba3c1e252b426e5481da0e6ca
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 1abb5ff..df983b1 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -1471,6 +1471,7 @@
     static final int NOTIFY_ACTIVITY_PINNED_LISTENERS_MSG = 64;
     static final int NOTIFY_PINNED_ACTIVITY_RESTART_ATTEMPT_LISTENERS_MSG = 65;
     static final int NOTIFY_PINNED_STACK_ANIMATION_ENDED_LISTENERS_MSG = 66;
+    static final int NOTIFY_FORCED_RESIZABLE_MSG = 67;
 
     static final int FIRST_ACTIVITY_STACK_MSG = 100;
     static final int FIRST_BROADCAST_QUEUE_MSG = 200;
@@ -2019,6 +2020,21 @@
                 }
                 break;
             }
+            case NOTIFY_FORCED_RESIZABLE_MSG: {
+                synchronized (ActivityManagerService.this) {
+                    for (int i = mTaskStackListeners.beginBroadcast() - 1; i >= 0; i--) {
+                        try {
+                            // Make a one-way callback to the listener
+                            mTaskStackListeners.getBroadcastItem(i).onActivityForcedResizable(
+                                    (String) msg.obj, msg.arg1);
+                        } catch (RemoteException e){
+                            // Handled by the RemoteCallbackList
+                        }
+                    }
+                    mTaskStackListeners.finishBroadcast();
+                }
+                break;
+            }
             case NOTIFY_CLEARTEXT_NETWORK_MSG: {
                 final int uid = msg.arg1;
                 final byte[] firstPacket = (byte[]) msg.obj;
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index 7def1bd..fc003b8 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -101,7 +101,9 @@
 import java.util.Objects;
 import java.util.Set;
 
+import static android.Manifest.permission.MANAGE_ACTIVITY_STACKS;
 import static android.Manifest.permission.START_ANY_ACTIVITY;
+import static android.Manifest.permission.START_TASKS_FROM_RECENTS;
 import static android.app.ActivityManager.LOCK_TASK_MODE_LOCKED;
 import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
 import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED;
@@ -151,6 +153,7 @@
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.am.ActivityManagerService.ANIMATE;
 import static com.android.server.am.ActivityManagerService.FIRST_SUPERVISOR_STACK_MSG;
+import static com.android.server.am.ActivityManagerService.NOTIFY_FORCED_RESIZABLE_MSG;
 import static com.android.server.am.ActivityRecord.APPLICATION_ACTIVITY_TYPE;
 import static com.android.server.am.ActivityRecord.HOME_ACTIVITY_TYPE;
 import static com.android.server.am.ActivityRecord.RECENTS_ACTIVITY_TYPE;
@@ -1304,7 +1307,7 @@
     boolean checkStartAnyActivityPermission(Intent intent, ActivityInfo aInfo,
             String resultWho, int requestCode, int callingPid, int callingUid,
             String callingPackage, boolean ignoreTargetSecurity, ProcessRecord callerApp,
-            ActivityRecord resultRecord, ActivityStack resultStack) {
+            ActivityRecord resultRecord, ActivityStack resultStack, ActivityOptions options) {
         final int startAnyPerm = mService.checkPermission(START_ANY_ACTIVITY, callingPid,
                 callingUid);
         if (startAnyPerm ==  PERMISSION_GRANTED) {
@@ -1358,6 +1361,19 @@
             Slog.w(TAG, message);
             return false;
         }
+        if (options != null && options.getLaunchTaskId() != -1) {
+            final int startInTaskPerm = mService.checkPermission(START_TASKS_FROM_RECENTS,
+                    callingPid, callingUid);
+            if (startInTaskPerm != PERMISSION_GRANTED) {
+                final String msg = "Permission Denial: starting " + intent.toString()
+                        + " from " + callerApp + " (pid=" + callingPid
+                        + ", uid=" + callingUid + ") with launchTaskId="
+                        + options.getLaunchTaskId();
+                Slog.w(TAG, msg);
+                throw new SecurityException(msg);
+            }
+        }
+
         return true;
     }
 
@@ -1788,6 +1804,8 @@
 
         if (DEBUG_STACK) Slog.d(TAG_STACK,
                 "findTaskToMoveToFront: moved to front of stack=" + task.stack);
+
+        showNonResizeableDockToastIfNeeded(task, INVALID_STACK_ID, task.stack.mStackId);
     }
 
     boolean canUseActivityOptionsLaunchBounds(ActivityOptions options, int launchStackId) {
@@ -3309,10 +3327,14 @@
             return;
         }
 
-        if (!task.canGoInDockedStack() || task.mResizeMode == RESIZE_MODE_FORCE_RESIZEABLE) {
-            // Display warning toast if we tried to put a non-dockable task in the docked stack or
-            // the task was forced to be resizable by the system.
+        if (!task.canGoInDockedStack()) {
+            // Display a warning toast that we tried to put a non-dockable task in the docked stack.
             mWindowManager.scheduleShowNonResizeableDockToast(task.taskId);
+        } else if (task.mResizeMode == RESIZE_MODE_FORCE_RESIZEABLE) {
+            String packageName = task.getTopActivity() != null
+                    ? task.getTopActivity().appInfo.packageName : null;
+            mService.mHandler.obtainMessage(NOTIFY_FORCED_RESIZABLE_MSG, task.taskId, 0,
+                    packageName).sendToTarget();
         }
     }
 
diff --git a/services/core/java/com/android/server/am/ActivityStarter.java b/services/core/java/com/android/server/am/ActivityStarter.java
index af69c93..f244c93 100644
--- a/services/core/java/com/android/server/am/ActivityStarter.java
+++ b/services/core/java/com/android/server/am/ActivityStarter.java
@@ -362,7 +362,7 @@
 
         boolean abort = !mSupervisor.checkStartAnyActivityPermission(intent, aInfo, resultWho,
                 requestCode, callingPid, callingUid, callingPackage, ignoreTargetSecurity, callerApp,
-                resultRecord, resultStack);
+                resultRecord, resultStack, options);
         abort |= !mService.mIntentFirewall.checkStartActivity(intent, callingUid,
                 callingPid, resolvedType, aInfo.applicationInfo);
 
@@ -879,6 +879,9 @@
 
         ActivityRecord intentActivity = getReusableIntentActivity();
 
+        final int preferredLaunchStackId =
+                (mOptions != null) ? mOptions.getLaunchStackId() : INVALID_STACK_ID;
+
         if (intentActivity != null) {
             // When the flags NEW_TASK and CLEAR_TASK are set, then the task gets reused but
             // still needs to be a lock task mode violation since the task gets cleared out and
@@ -938,6 +941,8 @@
                 // We didn't do anything...  but it was needed (a.k.a., client don't use that
                 // intent!)  And for paranoia, make sure we have correctly resumed the top activity.
                 resumeTargetStackIfNeeded();
+                mSupervisor.showNonResizeableDockToastIfNeeded(mStartActivity.task,
+                        preferredLaunchStackId, mTargetStack.mStackId);
                 return START_TASK_TO_FRONT;
             }
         }
@@ -977,6 +982,8 @@
             }
             top.deliverNewIntentLocked(
                     mCallingUid, mStartActivity.intent, mStartActivity.launchedFromPackage);
+            mSupervisor.showNonResizeableDockToastIfNeeded(mStartActivity.task,
+                    preferredLaunchStackId, mTargetStack.mStackId);
             return START_DELIVERED_TO_TOP;
         }
 
@@ -1063,8 +1070,6 @@
         }
         mSupervisor.updateUserStackLocked(mStartActivity.userId, mTargetStack);
 
-        final int preferredLaunchStackId =
-                (mOptions != null) ? mOptions.getLaunchStackId() : INVALID_STACK_ID;
         mSupervisor.showNonResizeableDockToastIfNeeded(
                 mStartActivity.task, preferredLaunchStackId, mTargetStack.mStackId);
 
@@ -1297,7 +1302,10 @@
         // same component, then instead of launching bring that one to the front.
         putIntoExistingTask &= mInTask == null && mStartActivity.resultTo == null;
         ActivityRecord intentActivity = null;
-        if (putIntoExistingTask) {
+        if (mOptions != null && mOptions.getLaunchTaskId() != -1) {
+            final TaskRecord task = mSupervisor.anyTaskForIdLocked(mOptions.getLaunchTaskId());
+            intentActivity = task != null ? task.getTopActivity() : null;
+        } else if (putIntoExistingTask) {
             // See if there is a task to bring to the front.  If this is a SINGLE_INSTANCE
             // activity, there can be one and only one instance of it in the history, and it is
             // always in its own unique task, so we do a special search.
diff --git a/services/core/java/com/android/server/policy/StatusBarController.java b/services/core/java/com/android/server/policy/StatusBarController.java
index 9d353c6..86d0468 100644
--- a/services/core/java/com/android/server/policy/StatusBarController.java
+++ b/services/core/java/com/android/server/policy/StatusBarController.java
@@ -29,6 +29,8 @@
 import android.view.animation.TranslateAnimation;
 
 import com.android.internal.statusbar.IStatusBarService;
+import com.android.server.LocalServices;
+import com.android.server.statusbar.StatusBarManagerInternal;
 
 import static android.view.WindowManagerInternal.*;
 
@@ -103,6 +105,20 @@
                 }
             });
         }
+
+        @Override
+        public void onAppTransitionFinishedLocked(IBinder token) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    StatusBarManagerInternal statusbar = LocalServices.getService(
+                            StatusBarManagerInternal.class);
+                    if (statusbar != null) {
+                        statusbar.appTransitionFinished();
+                    }
+                }
+            });
+        }
     };
 
     public StatusBarController() {
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index 6bda4ed..9614417 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -34,4 +34,5 @@
     void setSystemUiVisibility(int vis, int fullscreenStackVis, int dockedStackVis, int mask,
             Rect fullscreenBounds, Rect dockedBounds, String cause);
     void toggleSplitScreen();
+    void appTransitionFinished();
 }
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 403a4c6..4a00ebd 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -214,6 +214,15 @@
                 } catch (RemoteException ex) {}
             }
         }
+
+        public void appTransitionFinished() {
+            enforceStatusBarService();
+            if (mBar != null) {
+                try {
+                    mBar.appTransitionFinished();
+                } catch (RemoteException ex) {}
+            }
+        }
     };
 
     // ================================================================================
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index f097eb2..5c86155 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -140,41 +140,7 @@
             final String text =
                     mService.mContext.getString(R.string.dock_non_resizeble_failed_to_dock_text);
             mService.mH.obtainMessage(SHOW_NON_RESIZEABLE_DOCK_TOAST, 0, 0, text).sendToTarget();
-            return;
         }
-
-        final int dockSide = mStack.getDockSide();
-        if (mResizeMode != RESIZE_MODE_FORCE_RESIZEABLE || dockSide == DOCKED_INVALID) {
-            return;
-        }
-
-        int xOffset = 0;
-        int yOffset = 0;
-        mStack.getBounds(mTmpRect);
-
-        if (dockSide == DOCKED_LEFT || dockSide == DOCKED_RIGHT) {
-            // The toast was originally placed at the bottom and centered. To place it at the
-            // bottom-center of the stack, we offset it horizontally by the diff between the center
-            // of the stack bounds vs. the center of the screen.
-            displayContent.getLogicalDisplayRect(mTmpRect2);
-            xOffset = mTmpRect.centerX() - mTmpRect2.centerX();
-        } else if (dockSide == DOCKED_TOP) {
-            // The toast was originally placed at the bottom and centered. To place it at the bottom
-            // center of the top stack, we offset it vertically by the diff between the bottom of
-            // the stack bounds vs. the bottom of the content rect.
-            //
-            // Note here we use the content rect instead of the display rect, as we want the toast's
-            // distance to the dock divider (when it's placed at the top half) to be the same as
-            // it's distance to the top of the navigation bar (when it's placed at the bottom).
-
-            // We don't adjust for DOCKED_BOTTOM case since it's already at the bottom.
-            displayContent.getContentRect(mTmpRect2);
-            yOffset = mTmpRect2.bottom - mTmpRect.bottom;
-        }
-        final String text =
-                mService.mContext.getString(R.string.dock_forced_resizable);
-        mService.mH.obtainMessage(SHOW_NON_RESIZEABLE_DOCK_TOAST,
-                xOffset, yOffset, text).sendToTarget();
     }
 
     void addAppToken(int addPos, AppWindowToken wtoken, int resizeMode, boolean homeTask) {