Merge "Apps can opt in to be stoppable in the task manager" into main
diff --git a/core/res/res/values/stoppable_fgs_system_apps.xml b/core/res/res/values/stoppable_fgs_system_apps.xml
new file mode 100644
index 0000000..165ff61
--- /dev/null
+++ b/core/res/res/values/stoppable_fgs_system_apps.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+<resources>
+    <!-- A list of system apps whose FGS can be stopped in the task manager. -->
+    <string-array translatable="false" name="stoppable_fgs_system_apps">
+    </string-array>
+    <!-- stoppable_fgs_system_apps which is supposed to be overridden by vendor -->
+    <string-array translatable="false" name="vendor_stoppable_fgs_system_apps">
+    </string-array>
+</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index aa08d5e..db81a3b 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1306,6 +1306,8 @@
   <java-symbol type="array" name="vendor_policy_exempt_apps" />
   <java-symbol type="array" name="cloneable_apps" />
   <java-symbol type="array" name="config_securityStatePackages" />
+  <java-symbol type="array" name="stoppable_fgs_system_apps" />
+  <java-symbol type="array" name="vendor_stoppable_fgs_system_apps" />
 
   <java-symbol type="drawable" name="default_wallpaper" />
   <java-symbol type="drawable" name="default_lock_wallpaper" />
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 3bf3e24..87ea2a7 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1743,3 +1743,13 @@
     description: "An implementation of shortcut customizations through shortcut helper."
     bug: "365064144"
 }
+
+flag {
+    name: "stoppable_fgs_system_app"
+    namespace: "systemui"
+    description: "System app with foreground service can opt in to be stoppable."
+    bug: "376564917"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/FgsManagerControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/FgsManagerControllerTest.java
index 16ae466..0356422 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/FgsManagerControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/FgsManagerControllerTest.java
@@ -42,6 +42,7 @@
 import android.os.Binder;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.platform.test.annotations.EnableFlags;
 import android.provider.DeviceConfig;
 import android.testing.TestableLooper;
 
@@ -49,6 +50,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
+import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.DialogTransitionAnimator;
 import com.android.systemui.broadcast.BroadcastDispatcher;
@@ -315,13 +317,36 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_STOPPABLE_FGS_SYSTEM_APP)
+    public void testButtonVisibilityOfStoppableApps() throws Exception {
+        setUserProfiles(0);
+        setBackgroundRestrictionExemptionReason("pkg", 12345, REASON_ALLOWLISTED_PACKAGE);
+        setBackgroundRestrictionExemptionReason("vendor_pkg", 67890, REASON_ALLOWLISTED_PACKAGE);
+
+        // Same as above, but apps are opt-in to be stoppable
+        setStoppableApps(new String[] {"pkg"}, /* vendor */ false);
+        setStoppableApps(new String[] {"vendor_pkg"}, /* vendor */ true);
+
+        final Binder binder = new Binder();
+        setShowStopButtonForUserAllowlistedApps(true);
+        // Both are foreground.
+        mIForegroundServiceObserver.onForegroundStateChanged(binder, "pkg", 0, true);
+        mIForegroundServiceObserver.onForegroundStateChanged(binder, "vendor_pkg", 0, true);
+        Assert.assertEquals(2, mFmc.visibleButtonsCount());
+
+        // The vendor package is no longer foreground. Only `pkg` remains.
+        mIForegroundServiceObserver.onForegroundStateChanged(binder, "vendor_pkg", 0, false);
+        Assert.assertEquals(1, mFmc.visibleButtonsCount());
+    }
+
+    @Test
     public void testShowUserVisibleJobsOnCreation() {
         // Test when the default is on.
         mDeviceConfigProxyFake.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
                 SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_USER_VISIBLE_JOBS,
                 "true", false);
         FgsManagerController fmc = new FgsManagerControllerImpl(
-                mContext,
+                mContext.getResources(),
                 mMainExecutor,
                 mBackgroundExecutor,
                 mSystemClock,
@@ -348,7 +373,7 @@
                 SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_USER_VISIBLE_JOBS,
                 "false", false);
         fmc = new FgsManagerControllerImpl(
-                mContext,
+                mContext.getResources(),
                 mMainExecutor,
                 mBackgroundExecutor,
                 mSystemClock,
@@ -446,6 +471,11 @@
                 .getBackgroundRestrictionExemptionReason(uid);
     }
 
+    private void setStoppableApps(String[] packageNames, boolean vendor) throws Exception {
+        overrideResource(vendor ? com.android.internal.R.array.vendor_stoppable_fgs_system_apps
+                    : com.android.internal.R.array.stoppable_fgs_system_apps, packageNames);
+    }
+
     FgsManagerController createFgsManagerController() throws RemoteException {
         ArgumentCaptor<IForegroundServiceObserver> iForegroundServiceObserverArgumentCaptor =
                 ArgumentCaptor.forClass(IForegroundServiceObserver.class);
@@ -455,7 +485,7 @@
                 ArgumentCaptor.forClass(BroadcastReceiver.class);
 
         FgsManagerController result = new FgsManagerControllerImpl(
-                mContext,
+                mContext.getResources(),
                 mMainExecutor,
                 mBackgroundExecutor,
                 mSystemClock,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
index a1071907..2a5ffc6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
@@ -27,6 +27,7 @@
 import android.content.IntentFilter
 import android.content.pm.PackageManager
 import android.content.pm.UserInfo
+import android.content.res.Resources
 import android.graphics.drawable.Drawable
 import android.os.IBinder
 import android.os.PowerExemptionManager
@@ -54,6 +55,7 @@
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_USER_VISIBLE_JOBS
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.systemui.Dumpable
+import com.android.systemui.Flags;
 import com.android.systemui.res.R
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogTransitionAnimator
@@ -137,7 +139,7 @@
 
 @SysUISingleton
 class FgsManagerControllerImpl @Inject constructor(
-    private val context: Context,
+    @Main private val resources: Resources,
     @Main private val mainExecutor: Executor,
     @Background private val backgroundExecutor: Executor,
     private val systemClock: SystemClock,
@@ -223,6 +225,14 @@
 
     private val userVisibleJobObserver = UserVisibleJobObserver()
 
+    private val stoppableApps by lazy { resources
+        .getStringArray(com.android.internal.R.array.stoppable_fgs_system_apps)
+    }
+
+    private val vendorStoppableApps by lazy { resources
+        .getStringArray(com.android.internal.R.array.vendor_stoppable_fgs_system_apps)
+    }
+
     override fun init() {
         synchronized(lock) {
             if (initialized) {
@@ -725,9 +735,22 @@
                     }
                 else -> UIControl.NORMAL
             }
+            // If the app wants to be a good citizen by being stoppable, even if the category it
+            // belongs to is exempted for background restriction, let it be stoppable by user.
+            if (Flags.stoppableFgsSystemApp()) {
+                if (isStoppableApp(packageName)) {
+                    uiControl = UIControl.NORMAL
+                }
+            }
+
             uiControlInitialized = true
         }
 
+        fun isStoppableApp(packageName: String): Boolean {
+            return stoppableApps.contains(packageName) ||
+                vendorStoppableApps.contains(packageName)
+        }
+
         override fun equals(other: Any?): Boolean {
             if (other !is UserPackage) {
                 return false