Allow tile services to start foreground service when tile is clicked

Add the tile service app to temp allow list for 15 seconds to allow
it to start a foreground service during that short period.

Flag: NA
Bug: 329242921
Test: Manual test. CtsTileServiceTestCases, TileLifecycleManagerTest, CtsSystemUiHostTestCases
Change-Id: I26d4fb94162c88e3320d8b308c3cdec48b7e73b2
diff --git a/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java b/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java
index 24d815f..e734174 100644
--- a/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java
+++ b/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java
@@ -427,6 +427,12 @@
      */
     public static final int REASON_PACKAGE_UNARCHIVE = 328;
 
+    /**
+     * Tile onClick event
+     * @hide
+     */
+    public static final int REASON_TILE_ONCLICK = 329;
+
     /** @hide The app requests out-out. */
     public static final int REASON_OPT_OUT_REQUESTED = 1000;
 
@@ -504,13 +510,15 @@
             REASON_ROLE_EMERGENCY,
             REASON_SYSTEM_MODULE,
             REASON_CARRIER_PRIVILEGED_APP,
-            REASON_OPT_OUT_REQUESTED,
             REASON_DPO_PROTECTED_APP,
             REASON_DISALLOW_APPS_CONTROL,
             REASON_ACTIVE_DEVICE_ADMIN,
             REASON_MEDIA_NOTIFICATION_TRANSFER,
             REASON_PACKAGE_INSTALLER,
+            REASON_SYSTEM_EXEMPT_APP_OP,
             REASON_PACKAGE_UNARCHIVE,
+            REASON_TILE_ONCLICK,
+            REASON_OPT_OUT_REQUESTED,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ReasonCode {}
diff --git a/apex/jobscheduler/framework/java/android/os/PowerWhitelistManager.java b/apex/jobscheduler/framework/java/android/os/PowerWhitelistManager.java
index 20da171..18ffb7a 100644
--- a/apex/jobscheduler/framework/java/android/os/PowerWhitelistManager.java
+++ b/apex/jobscheduler/framework/java/android/os/PowerWhitelistManager.java
@@ -310,6 +310,11 @@
      * @hide
      */
     public static final int REASON_SHELL = PowerExemptionManager.REASON_SHELL;
+    /**
+     * Tile onClick event
+     * @hide
+     */
+    public static final int REASON_TILE_ONCLICK = PowerExemptionManager.REASON_TILE_ONCLICK;
 
     /**
      * The list of BG-FGS-Launch and temp-allowlist reason code.
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index 98ca09d..ef3f10f 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -76,6 +76,7 @@
 import android.net.NetworkScoreManager;
 import android.net.wifi.WifiManager;
 import android.os.BatteryStats;
+import android.os.IDeviceIdleController;
 import android.os.PowerExemptionManager;
 import android.os.PowerManager;
 import android.os.ServiceManager;
@@ -735,4 +736,11 @@
     static Optional<SatelliteManager> provideSatelliteManager(Context context) {
         return Optional.ofNullable(context.getSystemService(SatelliteManager.class));
     }
+
+    @Provides
+    @Singleton
+    static IDeviceIdleController provideDeviceIdleController() {
+        return IDeviceIdleController.Stub.asInterface(
+                ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
index 880289e..2a726c2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
@@ -15,6 +15,8 @@
  */
 package com.android.systemui.qs.external;
 
+import static android.os.PowerWhitelistManager.REASON_TILE_ONCLICK;
+import static android.provider.DeviceConfig.NAMESPACE_SYSTEMUI;
 import static android.service.quicksettings.TileService.START_ACTIVITY_NEEDS_PENDING_INTENT;
 
 import android.app.ActivityManager;
@@ -31,8 +33,10 @@
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.IDeviceIdleController;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.provider.DeviceConfig;
 import android.service.quicksettings.IQSService;
 import android.service.quicksettings.IQSTileService;
 import android.service.quicksettings.TileService;
@@ -89,7 +93,9 @@
     private static final int MAX_BIND_RETRIES = 5;
     private static final long DEFAULT_BIND_RETRY_DELAY = 5 * DateUtils.SECOND_IN_MILLIS;
     private static final long LOW_MEMORY_BIND_RETRY_DELAY = 20 * DateUtils.SECOND_IN_MILLIS;
-
+    private static final long TILE_SERVICE_ONCLICK_ALLOW_LIST_DEFAULT_DURATION_MS = 15_000;
+    private static final String PROPERTY_TILE_SERVICE_ONCLICK_ALLOW_LIST_DURATION =
+            "property_tile_service_onclick_allow_list_duration";
     // Shared prefs that hold tile lifecycle info.
     private static final String TILES = "tiles_prefs";
 
@@ -102,6 +108,7 @@
     private final PackageManagerAdapter mPackageManagerAdapter;
     private final BroadcastDispatcher mBroadcastDispatcher;
     private final ActivityManager mActivityManager;
+    private final IDeviceIdleController mDeviceIdleController;
 
     private Set<Integer> mQueuedMessages = new ArraySet<>();
     @NonNull
@@ -120,12 +127,15 @@
     private TileChangeListener mChangeListener;
     // Return value from bindServiceAsUser, determines whether safe to call unbind.
     private AtomicBoolean mIsBound = new AtomicBoolean(false);
+    private long mTempAllowFgsLaunchDuration = TILE_SERVICE_ONCLICK_ALLOW_LIST_DEFAULT_DURATION_MS;
+    private final DeviceConfig.OnPropertiesChangedListener mDeviceConfigChangedListener;
+    private AtomicBoolean mDeviceConfigChangedListenerRegistered = new AtomicBoolean(false);
 
     @AssistedInject
     TileLifecycleManager(@Main Handler handler, Context context, IQSService service,
             PackageManagerAdapter packageManagerAdapter, BroadcastDispatcher broadcastDispatcher,
             @Assisted Intent intent, @Assisted UserHandle user, ActivityManager activityManager,
-            @Background DelayableExecutor executor) {
+            IDeviceIdleController deviceIdleController, @Background DelayableExecutor executor) {
         mContext = context;
         mHandler = handler;
         mIntent = intent;
@@ -136,6 +146,16 @@
         mPackageManagerAdapter = packageManagerAdapter;
         mBroadcastDispatcher = broadcastDispatcher;
         mActivityManager = activityManager;
+        mDeviceIdleController = deviceIdleController;
+        mDeviceConfigChangedListener = properties -> {
+            if (!DeviceConfig.NAMESPACE_SYSTEMUI.equals(properties.getNamespace())) {
+                return;
+            }
+            mTempAllowFgsLaunchDuration = properties.getLong(
+                    PROPERTY_TILE_SERVICE_ONCLICK_ALLOW_LIST_DURATION,
+                    TILE_SERVICE_ONCLICK_ALLOW_LIST_DEFAULT_DURATION_MS);
+        };
+
         if (mDebug) Log.d(TAG, "Creating " + mIntent + " " + mUser);
     }
 
@@ -211,6 +231,13 @@
         }
         mBound.set(bind);
         if (bind) {
+            if (mDeviceConfigChangedListenerRegistered.compareAndSet(false, true)) {
+                DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_SYSTEMUI, mExecutor,
+                        mDeviceConfigChangedListener);
+                mTempAllowFgsLaunchDuration = DeviceConfig.getLong(NAMESPACE_SYSTEMUI,
+                        PROPERTY_TILE_SERVICE_ONCLICK_ALLOW_LIST_DURATION,
+                        TILE_SERVICE_ONCLICK_ALLOW_LIST_DEFAULT_DURATION_MS);
+            }
             if (mBindTryCount == MAX_BIND_RETRIES) {
                 // Too many failures, give up on this tile until an update.
                 startPackageListening();
@@ -363,6 +390,9 @@
             stopPackageListening();
         }
         mChangeListener = null;
+        if (mDeviceConfigChangedListener != null) {
+            DeviceConfig.removeOnPropertiesChangedListener(mDeviceConfigChangedListener);
+        }
     }
 
     /**
@@ -566,7 +596,17 @@
     @Override
     public void onClick(IBinder iBinder) {
         if (mDebug) Log.d(TAG, "onClick " + iBinder + " " + getComponent() + " " + mUser);
-        if (isNullOrFailedAction(mOptionalWrapper, (wrapper) -> wrapper.onClick(iBinder))) {
+        if (isNullOrFailedAction(mOptionalWrapper, (wrapper) -> {
+            final String packageName = mIntent.getComponent().getPackageName();
+            try {
+                mDeviceIdleController.addPowerSaveTempWhitelistApp(packageName,
+                        mTempAllowFgsLaunchDuration, mUser.getIdentifier(), REASON_TILE_ONCLICK,
+                        "tile onclick");
+            } catch (RemoteException e) {
+                Log.d(TAG, "Caught exception trying to add client package to temp allow list", e);
+            }
+            return wrapper.onClick(iBinder);
+        })) {
             mClickBinder = iBinder;
             queueMessage(MSG_ON_CLICK);
             handleDeath();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
index 0a36ae6..f57f040 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.systemui.qs.external;
 
+import static android.os.PowerExemptionManager.REASON_TILE_ONCLICK;
 import static android.service.quicksettings.TileService.START_ACTIVITY_NEEDS_PENDING_INTENT;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -52,6 +53,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.IDeviceIdleController;
 import android.os.UserHandle;
 import android.service.quicksettings.IQSService;
 import android.service.quicksettings.IQSTileService;
@@ -83,6 +85,7 @@
             mock(BroadcastDispatcher.class);
     private final IQSTileService.Stub mMockTileService = mock(IQSTileService.Stub.class);
     private final ActivityManager mActivityManager = mock(ActivityManager.class);
+    private final IDeviceIdleController mDeviceIdleController = mock(IDeviceIdleController.class);
 
     private ComponentName mTileServiceComponentName;
     private Intent mTileServiceIntent;
@@ -126,6 +129,7 @@
                 mTileServiceIntent,
                 mUser,
                 mActivityManager,
+                mDeviceIdleController,
                 mExecutor);
     }
 
@@ -386,6 +390,20 @@
     }
 
     @Test
+    public void testClickCallsDeviceIdleManager() throws Exception {
+        mStateManager.onTileAdded();
+        mStateManager.onStartListening();
+        mStateManager.onClick(null);
+        mStateManager.executeSetBindService(true);
+        mExecutor.runAllReady();
+
+        verify(mMockTileService).onClick(null);
+        verify(mDeviceIdleController).addPowerSaveTempWhitelistApp(
+                mTileServiceComponentName.getPackageName(), 15000,
+                mUser.getIdentifier(), REASON_TILE_ONCLICK, "tile onclick");
+    }
+
+    @Test
     public void testFalseBindCallsUnbind() {
         Context falseContext = mock(Context.class);
         when(falseContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(false);
@@ -396,6 +414,7 @@
                 mTileServiceIntent,
                 mUser,
                 mActivityManager,
+                mDeviceIdleController,
                 mExecutor);
 
         manager.executeSetBindService(true);
@@ -418,6 +437,7 @@
                 mTileServiceIntent,
                 mUser,
                 mActivityManager,
+                mDeviceIdleController,
                 mExecutor);
 
         manager.executeSetBindService(true);
@@ -440,6 +460,7 @@
                 mTileServiceIntent,
                 mUser,
                 mActivityManager,
+                mDeviceIdleController,
                 mExecutor);
 
         manager.executeSetBindService(true);
@@ -464,6 +485,7 @@
                 mTileServiceIntent,
                 mUser,
                 mActivityManager,
+                mDeviceIdleController,
                 mExecutor);
 
         manager.executeSetBindService(true);