Add a setting for background activity starts enabled state
and basic rules for enforcement

The setting is on by default. It can be switched off with
a developer option (see accompanying commit) or
"adb shell settings put global background_activity_starts_enabled 0".

The setting is wired into ActivityStarter. When it's switched
off, all unsupported background activity starts are aborted and
it's currently manifested with a toast for quick feedback.

Only basic policy rules are added for now:
0) is it one of the most important UIDs?
1) does the calling app have any foreground activity?
2) is the calling process important enough to always be able
   to start an activity?
3) does the calling uid have any visible window?

The policy rules will be extended to allow for more use cases
in forthcoming CLs.

Most notable use cases not currently covered:
1) Notifications
2) Widgets
3) Shortcuts
4) Some of the most important system apps (e.g. com.android.vending)
5) Accessibility services and similar (IMEs covered thanks to
   visible window exemption)
6) Recents button double tap to open the previous app
See bluedoc for some more.

Bug: 110956953
Test: atest WmTests:ActivityStarterTests
Manual test:
  (on)  observe all activities are started as they used to be
  (off) starts not satisfying one of the above rules are aborted,
        a toast specifying callingPackage is shown instead
Change-Id:  I1a3e14828c96f005d975ef6998f3bda678ccab29
diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java
index 5c77f0a..8571ae6 100644
--- a/services/core/java/com/android/server/am/ActivityManagerConstants.java
+++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java
@@ -16,6 +16,8 @@
 
 package com.android.server.am;
 
+import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_POWER_QUICK;
+
 import android.content.ContentResolver;
 import android.database.ContentObserver;
 import android.net.Uri;
@@ -26,8 +28,6 @@
 
 import java.io.PrintWriter;
 
-import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_POWER_QUICK;
-
 /**
  * Settings constants that can modify the activity manager's behavior.
  */
@@ -222,6 +222,10 @@
     // Controlled by Settings.Global.ACTIVITY_STARTS_LOGGING_ENABLED
     volatile boolean mFlagActivityStartsLoggingEnabled;
 
+    // Indicates whether the background activity starts is enabled.
+    // Controlled by Settings.Global.BACKGROUND_ACTIVITY_STARTS_ENABLED
+    volatile boolean mFlagBackgroundActivityStartsEnabled;
+
     private final ActivityManagerService mService;
     private ContentResolver mResolver;
     private final KeyValueListParser mParser = new KeyValueListParser(',');
@@ -256,6 +260,10 @@
     private static final Uri ACTIVITY_STARTS_LOGGING_ENABLED_URI = Settings.Global.getUriFor(
                 Settings.Global.ACTIVITY_STARTS_LOGGING_ENABLED);
 
+    private static final Uri BACKGROUND_ACTIVITY_STARTS_ENABLED_URI =
+                Settings.Global.getUriFor(
+                        Settings.Global.BACKGROUND_ACTIVITY_STARTS_ENABLED);
+
     public ActivityManagerConstants(ActivityManagerService service, Handler handler) {
         super(handler);
         mService = service;
@@ -266,8 +274,10 @@
         mResolver = resolver;
         mResolver.registerContentObserver(ACTIVITY_MANAGER_CONSTANTS_URI, false, this);
         mResolver.registerContentObserver(ACTIVITY_STARTS_LOGGING_ENABLED_URI, false, this);
+        mResolver.registerContentObserver(BACKGROUND_ACTIVITY_STARTS_ENABLED_URI, false, this);
         updateConstants();
         updateActivityStartsLoggingEnabled();
+        updateBackgroundActivityStartsEnabled();
     }
 
     public void setOverrideMaxCachedProcesses(int value) {
@@ -290,6 +300,8 @@
             updateConstants();
         } else if (ACTIVITY_STARTS_LOGGING_ENABLED_URI.equals(uri)) {
             updateActivityStartsLoggingEnabled();
+        } else if (BACKGROUND_ACTIVITY_STARTS_ENABLED_URI.equals(uri)) {
+            updateBackgroundActivityStartsEnabled();
         }
     }
 
@@ -373,6 +385,11 @@
                 Settings.Global.ACTIVITY_STARTS_LOGGING_ENABLED, 0) == 1;
     }
 
+    private void updateBackgroundActivityStartsEnabled() {
+        mFlagBackgroundActivityStartsEnabled = Settings.Global.getInt(mResolver,
+                Settings.Global.BACKGROUND_ACTIVITY_STARTS_ENABLED, 1) == 1;
+    }
+
     private void updateMaxCachedProcesses() {
         CUR_MAX_CACHED_PROCESSES = mOverrideMaxCachedProcesses < 0
                 ? MAX_CACHED_PROCESSES : mOverrideMaxCachedProcesses;
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 80e7313..b62f648 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -19204,6 +19204,10 @@
             return mConstants.mFlagActivityStartsLoggingEnabled;
         }
 
+        public boolean isBackgroundActivityStartsEnabled() {
+            return mConstants.mFlagBackgroundActivityStartsEnabled;
+        }
+
         public void reportCurKeyguardUsageEvent(boolean keyguardShowing) {
             synchronized(ActivityManagerService.this) {
                 ActivityManagerService.this.reportGlobalUsageEventLocked(keyguardShowing
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index 1c08d03..7c7553f 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -850,9 +850,10 @@
         builder.addTaggedData(FIELD_TARGET_UID_HAS_ANY_VISIBLE_WINDOW,
                 targetUidHasAnyVisibleWindow ? 1 : 0);
         builder.addTaggedData(FIELD_TARGET_WHITELIST_TAG, targetWhitelistTag);
-        builder.addTaggedData(FIELD_TARGET_SHORT_COMPONENT_NAME, r.shortComponentName);
         builder.addTaggedData(FIELD_COMING_FROM_PENDING_INTENT, comingFromPendingIntent ? 1 : 0);
-        builder.addTaggedData(FIELD_INTENT_ACTION, intent.getAction());
+        if (intent != null) {
+            builder.addTaggedData(FIELD_INTENT_ACTION, intent.getAction());
+        }
         if (callerApp != null) {
             builder.addTaggedData(FIELD_PROCESS_RECORD_PROCESS_NAME, callerApp.mName);
             builder.addTaggedData(FIELD_PROCESS_RECORD_CUR_PROC_STATE,
@@ -881,29 +882,34 @@
                         (nowUptime - callerApp.getWhenUnimportant()));
             }
         }
-        builder.addTaggedData(FIELD_ACTIVITY_RECORD_LAUNCH_MODE, r.info.launchMode);
-        builder.addTaggedData(FIELD_ACTIVITY_RECORD_TARGET_ACTIVITY, r.info.targetActivity);
-        builder.addTaggedData(FIELD_ACTIVITY_RECORD_FLAGS, r.info.flags);
-        builder.addTaggedData(FIELD_ACTIVITY_RECORD_REAL_ACTIVITY, r.realActivity.toShortString());
-        builder.addTaggedData(FIELD_ACTIVITY_RECORD_SHORT_COMPONENT_NAME, r.shortComponentName);
-        builder.addTaggedData(FIELD_ACTIVITY_RECORD_PROCESS_NAME, r.processName);
-        builder.addTaggedData(FIELD_ACTIVITY_RECORD_IS_FULLSCREEN, r.fullscreen ? 1 : 0);
-        builder.addTaggedData(FIELD_ACTIVITY_RECORD_IS_NO_DISPLAY, r.noDisplay ? 1 : 0);
-        if (r.lastVisibleTime != 0) {
-            builder.addTaggedData(FIELD_ACTIVITY_RECORD_MILLIS_SINCE_LAST_VISIBLE,
-                    (nowUptime - r.lastVisibleTime));
-        }
-        if (r.resultTo != null) {
-            builder.addTaggedData(FIELD_ACTIVITY_RECORD_RESULT_TO_PKG_NAME, r.resultTo.packageName);
-            builder.addTaggedData(FIELD_ACTIVITY_RECORD_RESULT_TO_SHORT_COMPONENT_NAME,
-                    r.resultTo.shortComponentName);
-        }
-        builder.addTaggedData(FIELD_ACTIVITY_RECORD_IS_VISIBLE, r.visible ? 1 : 0);
-        builder.addTaggedData(FIELD_ACTIVITY_RECORD_IS_VISIBLE_IGNORING_KEYGUARD,
-                r.visibleIgnoringKeyguard ? 1 : 0);
-        if (r.lastLaunchTime != 0) {
-            builder.addTaggedData(FIELD_ACTIVITY_RECORD_MILLIS_SINCE_LAST_LAUNCH,
-                    (nowUptime - r.lastLaunchTime));
+        if (r != null) {
+            builder.addTaggedData(FIELD_TARGET_SHORT_COMPONENT_NAME, r.shortComponentName);
+            builder.addTaggedData(FIELD_ACTIVITY_RECORD_LAUNCH_MODE, r.info.launchMode);
+            builder.addTaggedData(FIELD_ACTIVITY_RECORD_TARGET_ACTIVITY, r.info.targetActivity);
+            builder.addTaggedData(FIELD_ACTIVITY_RECORD_FLAGS, r.info.flags);
+            builder.addTaggedData(FIELD_ACTIVITY_RECORD_REAL_ACTIVITY,
+                    r.realActivity.toShortString());
+            builder.addTaggedData(FIELD_ACTIVITY_RECORD_SHORT_COMPONENT_NAME, r.shortComponentName);
+            builder.addTaggedData(FIELD_ACTIVITY_RECORD_PROCESS_NAME, r.processName);
+            builder.addTaggedData(FIELD_ACTIVITY_RECORD_IS_FULLSCREEN, r.fullscreen ? 1 : 0);
+            builder.addTaggedData(FIELD_ACTIVITY_RECORD_IS_NO_DISPLAY, r.noDisplay ? 1 : 0);
+            if (r.lastVisibleTime != 0) {
+                builder.addTaggedData(FIELD_ACTIVITY_RECORD_MILLIS_SINCE_LAST_VISIBLE,
+                        (nowUptime - r.lastVisibleTime));
+            }
+            if (r.resultTo != null) {
+                builder.addTaggedData(FIELD_ACTIVITY_RECORD_RESULT_TO_PKG_NAME,
+                        r.resultTo.packageName);
+                builder.addTaggedData(FIELD_ACTIVITY_RECORD_RESULT_TO_SHORT_COMPONENT_NAME,
+                        r.resultTo.shortComponentName);
+            }
+            builder.addTaggedData(FIELD_ACTIVITY_RECORD_IS_VISIBLE, r.visible ? 1 : 0);
+            builder.addTaggedData(FIELD_ACTIVITY_RECORD_IS_VISIBLE_IGNORING_KEYGUARD,
+                    r.visibleIgnoringKeyguard ? 1 : 0);
+            if (r.lastLaunchTime != 0) {
+                builder.addTaggedData(FIELD_ACTIVITY_RECORD_MILLIS_SINCE_LAST_LAUNCH,
+                        (nowUptime - r.lastLaunchTime));
+            }
         }
         mMetricsLogger.write(builder);
     }
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 90f3ff8..1735ef8 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -98,6 +98,7 @@
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.Trace;
@@ -108,6 +109,7 @@
 import android.util.EventLog;
 import android.util.Pools.SynchronizedPool;
 import android.util.Slog;
+import android.widget.Toast;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.HeavyWeightSwitcherActivity;
@@ -731,6 +733,12 @@
         abort |= !mService.mIntentFirewall.checkStartActivity(intent, callingUid,
                 callingPid, resolvedType, aInfo.applicationInfo);
 
+        // not sure if we need to create START_ABORTED_BACKGROUND so for now piggybacking
+        // on START_ABORTED
+        if (!abort) {
+            abort |= shouldAbortBackgroundActivityStart(callingUid, callingPackage, callerApp);
+        }
+
         // Merge the two options bundles, while realCallerOptions takes precedence.
         ActivityOptions checkedOptions = options != null
                 ? options.getOptions(intent, aInfo, callerApp, mSupervisor) : null;
@@ -774,6 +782,8 @@
             // We pretend to the caller that it was really started, but
             // they will just get a cancel result.
             ActivityOptions.abort(checkedOptions);
+            maybeLogActivityStart(callingUid, callingPackage, realCallingUid, intent, callerApp,
+                    /* r= */ null, originatingPendingIntent, /* abortedStart= */ true);
             return START_ABORTED;
         }
 
@@ -866,19 +876,50 @@
         mController.doPendingActivityLaunches(false);
 
         maybeLogActivityStart(callingUid, callingPackage, realCallingUid, intent, callerApp, r,
-                originatingPendingIntent);
+                originatingPendingIntent, /* abortedStart= */ false);
 
         return startActivity(r, sourceRecord, voiceSession, voiceInteractor, startFlags,
                 true /* doResume */, checkedOptions, inTask, outActivity);
     }
 
+    private boolean shouldAbortBackgroundActivityStart(int callingUid, final String callingPackage,
+            WindowProcessController callerApp) {
+        if (mService.isBackgroundActivityStartsEnabled()) {
+            return false;
+        }
+        // don't abort for the most important UIDs
+        if (callingUid == Process.ROOT_UID || callingUid == Process.SYSTEM_UID) {
+            return false;
+        }
+        // don't abort if the callerApp has any visible activity
+        if (callerApp != null && callerApp.hasForegroundActivities()) {
+            return false;
+        }
+        // don't abort if the callingUid's process is important enough
+        if (mService.getUidStateLocked(callingUid) <= ActivityManager.PROCESS_STATE_TOP) {
+            return false;
+        }
+        // don't abort if the callingUid has any visible window
+        if (mService.mWindowManager.isAnyWindowVisibleForUid(callingUid)) {
+            return false;
+        }
+        // anything that has fallen through will currently be aborted
+        // TODO: remove this toast after feature development is done
+        mService.mUiHandler.post(() -> {
+            Toast.makeText(mService.mContext,
+                    "Blocking background activity start for " + callingPackage,
+                    Toast.LENGTH_SHORT).show();
+        });
+        return true;
+    }
+
     private void maybeLogActivityStart(int callingUid, String callingPackage, int realCallingUid,
             Intent intent, WindowProcessController callerApp, ActivityRecord r,
-            PendingIntentRecord originatingPendingIntent) {
+            PendingIntentRecord originatingPendingIntent, boolean abortedStart) {
         boolean callerAppHasForegroundActivity =
                 callerApp != null && callerApp.hasForegroundActivities();
         if (!mService.isActivityStartsLoggingEnabled() || callerAppHasForegroundActivity
-                || r == null) {
+                || (!abortedStart && r == null)) {
             // skip logging in this case
             return;
         }
@@ -894,8 +935,8 @@
             final boolean realCallingUidHasAnyVisibleWindow = (callingUid == realCallingUid)
                     ? callingUidHasAnyVisibleWindow
                     : mService.mWindowManager.isAnyWindowVisibleForUid(realCallingUid);
-            final String targetPackage = r.packageName;
-            final int targetUid = (r.appInfo != null) ? r.appInfo.uid : -1;
+            final String targetPackage = (r != null) ? r.packageName : null;
+            final int targetUid = (r!= null) ? ((r.appInfo != null) ? r.appInfo.uid : -1) : -1;
             final int targetUidProcState = mService.getUidStateLocked(targetUid);
             final boolean targetUidHasAnyVisibleWindow = (targetUid != -1)
                     ? mService.mWindowManager.isAnyWindowVisibleForUid(targetUid)
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index d0e3fb4..054b105 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -5016,6 +5016,10 @@
         return mAmInternal.isActivityStartsLoggingEnabled();
     }
 
+    boolean isBackgroundActivityStartsEnabled() {
+        return mAmInternal.isBackgroundActivityStartsEnabled();
+    }
+
     void enableScreenAfterBoot(boolean booted) {
         EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_ENABLE_SCREEN,
                 SystemClock.uptimeMillis());
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index 50aa541..f6ff05b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.app.ActivityManager.PROCESS_STATE_TOP;
 import static android.app.ActivityManager.START_ABORTED;
 import static android.app.ActivityManager.START_CLASS_NOT_FOUND;
 import static android.app.ActivityManager.START_DELIVERED_TO_TOP;
@@ -69,6 +70,7 @@
 import android.content.pm.PackageManagerInternal;
 import android.graphics.Rect;
 import android.os.IBinder;
+import android.os.Process;
 import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
 import android.service.voice.IVoiceInteractionSession;
@@ -110,6 +112,7 @@
     private static final int FAKE_CALLING_UID = 666;
     private static final int FAKE_REAL_CALLING_UID = 667;
     private static final String FAKE_CALLING_PACKAGE = "com.whatever.dude";
+    private static final int UNIMPORTANT_UID = 12345;
 
     @Before
     public void setUp() throws Exception {
@@ -551,6 +554,79 @@
     }
 
     /**
+     * This test ensures that unsupported usecases aren't aborted when background starts are
+     * allowed.
+     */
+    @Test
+    public void testBackgroundActivityStartsAllowed_noStartsAborted() {
+        doReturn(true).when(mService).isBackgroundActivityStartsEnabled();
+
+        runAndVerifyBackgroundActivityStartsSubtest("allowed_noStartsAborted",
+                false, UNIMPORTANT_UID, false, PROCESS_STATE_TOP + 1, false);
+    }
+
+    /**
+     * This test ensures that unsupported usecases are aborted when background starts are
+     * disallowed.
+     */
+    @Test
+    public void testBackgroundActivityStartsDisallowed_unsupportedStartsAborted() {
+        doReturn(false).when(mService).isBackgroundActivityStartsEnabled();
+
+        runAndVerifyBackgroundActivityStartsSubtest("disallowed_unsupportedUsecase_aborted",
+                true, UNIMPORTANT_UID, false, PROCESS_STATE_TOP + 1, false);
+    }
+
+    /**
+     * This test ensures that supported usecases aren't aborted when background starts are
+     * disallowed.
+     * The scenarios each have only one condidion that makes them supported.
+     */
+    @Test
+    public void testBackgroundActivityStartsDisallowed_supportedStartsNotAborted() {
+        doReturn(false).when(mService).isBackgroundActivityStartsEnabled();
+
+        runAndVerifyBackgroundActivityStartsSubtest("disallowed_rootUid_notAborted",
+                false, Process.ROOT_UID, false, PROCESS_STATE_TOP + 1, false);
+        runAndVerifyBackgroundActivityStartsSubtest("disallowed_systemUid_notAborted",
+                false, Process.SYSTEM_UID, false, PROCESS_STATE_TOP + 1, false);
+        runAndVerifyBackgroundActivityStartsSubtest("disallowed_hasVisibleWindow_notAborted",
+                false, UNIMPORTANT_UID, true, PROCESS_STATE_TOP + 1, false);
+        runAndVerifyBackgroundActivityStartsSubtest("disallowed_processStateTop_notAborted",
+                false, UNIMPORTANT_UID, false, PROCESS_STATE_TOP, false);
+        runAndVerifyBackgroundActivityStartsSubtest("disallowed_hasForegroundActivities_notAborted",
+                false, UNIMPORTANT_UID, false, PROCESS_STATE_TOP + 1, true);
+    }
+
+    private void runAndVerifyBackgroundActivityStartsSubtest(String name, boolean shouldHaveAborted,
+            int testCallingUid, boolean hasVisibleWindow, int procState,
+            boolean hasForegroundActivities) {
+        // window visibility
+        doReturn(hasVisibleWindow).when(mService.mWindowManager).isAnyWindowVisibleForUid(
+                testCallingUid);
+        // process importance
+        doReturn(procState).when(mService).getUidStateLocked(testCallingUid);
+        // foreground activities
+        final IApplicationThread caller = mock(IApplicationThread.class);
+        final ApplicationInfo ai = new ApplicationInfo();
+        ai.uid = testCallingUid;
+        final WindowProcessController callerApp =
+                new WindowProcessController(mService, ai, null, testCallingUid, -1, null, null);
+        callerApp.setHasForegroundActivities(hasForegroundActivities);
+        doReturn(callerApp).when(mService).getProcessController(caller);
+
+        final ActivityOptions options = spy(ActivityOptions.makeBasic());
+        ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_NEW_TASK).setCaller(caller)
+                .setCallingUid(testCallingUid).setActivityOptions(new SafeActivityOptions(options));
+
+        final int result = starter.setReason("testBackgroundActivityStarts_" + name).execute();
+
+        assertEquals(ActivityStarter.getExternalResult(
+                shouldHaveAborted ? START_ABORTED : START_SUCCESS), result);
+        verify(options, times(shouldHaveAborted ? 1 : 0)).abort();
+    }
+
+    /**
      * This test ensures that when starting an existing single task activity on secondary display
      * which is not the top focused display, it should deliver new intent to the activity and not
      * create a new stack.
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java
index c2ab3ac..cc0ae94 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java
@@ -402,6 +402,8 @@
             spyOn(getLifecycleManager());
             spyOn(getLockTaskController());
             doReturn(mock(IPackageManager.class)).when(this).getPackageManager();
+            // allow background activity starts by default
+            doReturn(true).when(this).isBackgroundActivityStartsEnabled();
         }
 
         void setActivityManagerService(IntentFirewall intentFirewall,