Merge "Add setting/experiment for small battery devices to have all forced app standby enabled except for when the device is charging."
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index b2cc18b..60ce42b 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -10375,6 +10375,15 @@
         public static final String FORCED_APP_STANDBY_ENABLED = "forced_app_standby_enabled";
 
         /**
+         * Whether or not to enable Forced App Standby on small battery devices.
+         * Type: int (0 for false, 1 for true)
+         * Default: 0
+         * @hide
+         */
+        public static final String FORCED_APP_STANDBY_FOR_SMALL_BATTERY_ENABLED
+                = "forced_app_standby_for_small_battery_enabled";
+
+        /**
          * Whether or not Network Watchlist feature is enabled.
          * Type: int (0 for false, 1 for true)
          * Default: 0
diff --git a/core/proto/android/server/forceappstandbytracker.proto b/core/proto/android/server/forceappstandbytracker.proto
index 8753bf7..c9f7d52 100644
--- a/core/proto/android/server/forceappstandbytracker.proto
+++ b/core/proto/android/server/forceappstandbytracker.proto
@@ -41,4 +41,13 @@
 
   // Packages that are disallowed OP_RUN_ANY_IN_BACKGROUND.
   repeated RunAnyInBackgroundRestrictedPackages run_any_in_background_restricted_packages = 5;
+
+  // Whether device is a small battery device
+  optional bool is_small_battery_device = 6;
+
+  // Whether force app standby for small battery device setting is enabled
+  optional bool force_all_apps_standby_for_small_battery = 7;
+
+  // Whether device is charging
+  optional bool is_charging = 8;
 }
diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
index ec3a6ce..fa0ea5c 100644
--- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
@@ -213,6 +213,7 @@
                     Settings.Global.FANCY_IME_ANIMATIONS,
                     Settings.Global.FORCE_ALLOW_ON_EXTERNAL,
                     Settings.Global.FORCED_APP_STANDBY_ENABLED,
+                    Settings.Global.FORCED_APP_STANDBY_FOR_SMALL_BATTERY_ENABLED,
                     Settings.Global.FSTRIM_MANDATORY_INTERVAL,
                     Settings.Global.GLOBAL_HTTP_PROXY_EXCLUSION_LIST,
                     Settings.Global.GLOBAL_HTTP_PROXY_HOST,
diff --git a/services/core/java/com/android/server/ForceAppStandbyTracker.java b/services/core/java/com/android/server/ForceAppStandbyTracker.java
index a538bde..de113a6 100644
--- a/services/core/java/com/android/server/ForceAppStandbyTracker.java
+++ b/services/core/java/com/android/server/ForceAppStandbyTracker.java
@@ -26,6 +26,8 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.BatteryManager;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
@@ -89,6 +91,9 @@
 
     private final MyHandler mHandler;
 
+    @VisibleForTesting
+    FeatureFlagsObserver mFlagsObserver;
+
     /**
      * Pair of (uid (not user-id), packageName) with OP_RUN_ANY_IN_BACKGROUND *not* allowed.
      */
@@ -113,14 +118,36 @@
     @GuardedBy("mLock")
     boolean mStarted;
 
+    /**
+     * Only used for small battery use-case.
+     */
     @GuardedBy("mLock")
-    boolean mForceAllAppsStandby;   // True if device is in extreme battery saver mode
+    boolean mIsPluggedIn;
 
     @GuardedBy("mLock")
-    boolean mForcedAppStandbyEnabled;   // True if the forced app standby feature is enabled
+    boolean mBatterySaverEnabled;
 
-    private class FeatureFlagObserver extends ContentObserver {
-        FeatureFlagObserver() {
+    /**
+     * True if the forced app standby is currently enabled
+     */
+    @GuardedBy("mLock")
+    boolean mForceAllAppsStandby;
+
+    /**
+     * True if the forced app standby for small battery devices feature is enabled in settings
+     */
+    @GuardedBy("mLock")
+    boolean mForceAllAppStandbyForSmallBattery;
+
+    /**
+     * True if the forced app standby feature is enabled in settings
+     */
+    @GuardedBy("mLock")
+    boolean mForcedAppStandbyEnabled;
+
+    @VisibleForTesting
+    class FeatureFlagsObserver extends ContentObserver {
+        FeatureFlagsObserver() {
             super(null);
         }
 
@@ -128,6 +155,9 @@
             mContext.getContentResolver().registerContentObserver(
                     Settings.Global.getUriFor(Settings.Global.FORCED_APP_STANDBY_ENABLED),
                     false, this);
+
+            mContext.getContentResolver().registerContentObserver(Settings.Global.getUriFor(
+                    Settings.Global.FORCED_APP_STANDBY_FOR_SMALL_BATTERY_ENABLED), false, this);
         }
 
         boolean isForcedAppStandbyEnabled() {
@@ -135,20 +165,43 @@
                     Settings.Global.FORCED_APP_STANDBY_ENABLED, 1) == 1;
         }
 
+        boolean isForcedAppStandbyForSmallBatteryEnabled() {
+            return Settings.Global.getInt(mContext.getContentResolver(),
+                    Settings.Global.FORCED_APP_STANDBY_FOR_SMALL_BATTERY_ENABLED, 0) == 1;
+        }
+
         @Override
-        public void onChange(boolean selfChange) {
-            final boolean enabled = isForcedAppStandbyEnabled();
-            synchronized (mLock) {
-                if (mForcedAppStandbyEnabled == enabled) {
-                    return;
+        public void onChange(boolean selfChange, Uri uri) {
+            if (Settings.Global.getUriFor(Settings.Global.FORCED_APP_STANDBY_ENABLED).equals(uri)) {
+                final boolean enabled = isForcedAppStandbyEnabled();
+                synchronized (mLock) {
+                    if (mForcedAppStandbyEnabled == enabled) {
+                        return;
+                    }
+                    mForcedAppStandbyEnabled = enabled;
+                    if (DEBUG) {
+                        Slog.d(TAG,"Forced app standby feature flag changed: "
+                                + mForcedAppStandbyEnabled);
+                    }
                 }
-                mForcedAppStandbyEnabled = enabled;
-                if (DEBUG) {
-                    Slog.d(TAG,
-                            "Forced app standby feature flag changed: " + mForcedAppStandbyEnabled);
+                mHandler.notifyForcedAppStandbyFeatureFlagChanged();
+            } else if (Settings.Global.getUriFor(
+                    Settings.Global.FORCED_APP_STANDBY_FOR_SMALL_BATTERY_ENABLED).equals(uri)) {
+                final boolean enabled = isForcedAppStandbyForSmallBatteryEnabled();
+                synchronized (mLock) {
+                    if (mForceAllAppStandbyForSmallBattery == enabled) {
+                        return;
+                    }
+                    mForceAllAppStandbyForSmallBattery = enabled;
+                    if (DEBUG) {
+                        Slog.d(TAG, "Forced app standby for small battery feature flag changed: "
+                                + mForceAllAppStandbyForSmallBattery);
+                    }
+                    updateForceAllAppStandbyState();
                 }
+            } else {
+                Slog.w(TAG, "Unexpected feature flag uri encountered: " + uri);
             }
-            mHandler.notifyFeatureFlagChanged();
         }
     }
 
@@ -289,9 +342,11 @@
             mAppOpsManager = Preconditions.checkNotNull(injectAppOpsManager());
             mAppOpsService = Preconditions.checkNotNull(injectIAppOpsService());
             mPowerManagerInternal = Preconditions.checkNotNull(injectPowerManagerInternal());
-            final FeatureFlagObserver flagObserver = new FeatureFlagObserver();
-            flagObserver.register();
-            mForcedAppStandbyEnabled = flagObserver.isForcedAppStandbyEnabled();
+            mFlagsObserver = new FeatureFlagsObserver();
+            mFlagsObserver.register();
+            mForcedAppStandbyEnabled = mFlagsObserver.isForcedAppStandbyEnabled();
+            mForceAllAppStandbyForSmallBattery =
+                    mFlagsObserver.isForcedAppStandbyForSmallBatteryEnabled();
 
             try {
                 mIActivityManager.registerUidObserver(new UidObserver(),
@@ -306,16 +361,24 @@
 
             IntentFilter filter = new IntentFilter();
             filter.addAction(Intent.ACTION_USER_REMOVED);
+            filter.addAction(Intent.ACTION_BATTERY_CHANGED);
             mContext.registerReceiver(new MyReceiver(), filter);
 
             refreshForcedAppStandbyUidPackagesLocked();
 
             mPowerManagerInternal.registerLowPowerModeObserver(
                     ServiceType.FORCE_ALL_APPS_STANDBY,
-                    (state) -> updateForceAllAppsStandby(state.batterySaverEnabled));
+                    (state) -> {
+                        synchronized (mLock) {
+                            mBatterySaverEnabled = state.batterySaverEnabled;
+                            updateForceAllAppStandbyState();
+                        }
+                    });
 
-            updateForceAllAppsStandby(mPowerManagerInternal.getLowPowerState(
-                    ServiceType.FORCE_ALL_APPS_STANDBY).batterySaverEnabled);
+            mBatterySaverEnabled = mPowerManagerInternal.getLowPowerState(
+                    ServiceType.FORCE_ALL_APPS_STANDBY).batterySaverEnabled;
+
+            updateForceAllAppStandbyState();
         }
     }
 
@@ -340,6 +403,11 @@
         return LocalServices.getService(PowerManagerInternal.class);
     }
 
+    @VisibleForTesting
+    boolean isSmallBatteryDevice() {
+        return ActivityManager.isSmallBatteryDevice();
+    }
+
     /**
      * Update {@link #mRunAnyRestrictedPackages} with the current app ops state.
      */
@@ -369,18 +437,26 @@
         }
     }
 
+    private void updateForceAllAppStandbyState() {
+        synchronized (mLock) {
+            if (mForceAllAppStandbyForSmallBattery && isSmallBatteryDevice()) {
+                toggleForceAllAppsStandbyLocked(!mIsPluggedIn);
+            } else {
+                toggleForceAllAppsStandbyLocked(mBatterySaverEnabled);
+            }
+        }
+    }
+
     /**
      * Update {@link #mForceAllAppsStandby} and notifies the listeners.
      */
-    void updateForceAllAppsStandby(boolean enable) {
-        synchronized (mLock) {
-            if (enable == mForceAllAppsStandby) {
-                return;
-            }
-            mForceAllAppsStandby = enable;
-
-            mHandler.notifyForceAllAppsStandbyChanged();
+    private void toggleForceAllAppsStandbyLocked(boolean enable) {
+        if (enable == mForceAllAppsStandby) {
+            return;
         }
+        mForceAllAppsStandby = enable;
+
+        mHandler.notifyForceAllAppsStandbyChanged();
     }
 
     private int findForcedAppStandbyUidPackageIndexLocked(int uid, @NonNull String packageName) {
@@ -515,6 +591,11 @@
                 if (userId > 0) {
                     mHandler.doUserRemoved(userId);
                 }
+            } else if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) {
+                synchronized (mLock) {
+                    mIsPluggedIn = (intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0);
+                }
+                updateForceAllAppStandbyState();
             }
         }
     }
@@ -533,7 +614,7 @@
         private static final int MSG_TEMP_WHITELIST_CHANGED = 5;
         private static final int MSG_FORCE_ALL_CHANGED = 6;
         private static final int MSG_USER_REMOVED = 7;
-        private static final int MSG_FEATURE_FLAG_CHANGED = 8;
+        private static final int MSG_FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED = 8;
 
         public MyHandler(Looper looper) {
             super(looper);
@@ -563,8 +644,8 @@
             obtainMessage(MSG_FORCE_ALL_CHANGED).sendToTarget();
         }
 
-        public void notifyFeatureFlagChanged() {
-            obtainMessage(MSG_FEATURE_FLAG_CHANGED).sendToTarget();
+        public void notifyForcedAppStandbyFeatureFlagChanged() {
+            obtainMessage(MSG_FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED).sendToTarget();
         }
 
         public void doUserRemoved(int userId) {
@@ -618,7 +699,7 @@
                         l.onForceAllAppsStandbyChanged(sender);
                     }
                     return;
-                case MSG_FEATURE_FLAG_CHANGED:
+                case MSG_FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED:
                     // Feature flag for forced app standby changed.
                     final boolean unblockAlarms;
                     synchronized (mLock) {
@@ -845,6 +926,18 @@
             pw.println(isForceAllAppsStandbyEnabled());
 
             pw.print(indent);
+            pw.print("Small Battery Device: ");
+            pw.println(isSmallBatteryDevice());
+
+            pw.print(indent);
+            pw.print("Force all apps standby for small battery device: ");
+            pw.println(mForceAllAppStandbyForSmallBattery);
+
+            pw.print(indent);
+            pw.print("Plugged In: ");
+            pw.println(mIsPluggedIn);
+
+            pw.print(indent);
             pw.print("Foreground uids: [");
 
             String sep = "";
@@ -883,6 +976,11 @@
             final long token = proto.start(fieldId);
 
             proto.write(ForceAppStandbyTrackerProto.FORCE_ALL_APPS_STANDBY, mForceAllAppsStandby);
+            proto.write(ForceAppStandbyTrackerProto.IS_SMALL_BATTERY_DEVICE,
+                    isSmallBatteryDevice());
+            proto.write(ForceAppStandbyTrackerProto.FORCE_ALL_APPS_STANDBY_FOR_SMALL_BATTERY,
+                    mForceAllAppStandbyForSmallBattery);
+            proto.write(ForceAppStandbyTrackerProto.IS_CHARGING, mIsPluggedIn);
 
             for (int i = 0; i < mForegroundUids.size(); i++) {
                 if (mForegroundUids.valueAt(i)) {
diff --git a/services/tests/servicestests/src/com/android/server/ForceAppStandbyTrackerTest.java b/services/tests/servicestests/src/com/android/server/ForceAppStandbyTrackerTest.java
index 429dd8f..de54e52 100644
--- a/services/tests/servicestests/src/com/android/server/ForceAppStandbyTrackerTest.java
+++ b/services/tests/servicestests/src/com/android/server/ForceAppStandbyTrackerTest.java
@@ -42,6 +42,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.os.BatteryManager;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.PowerManager.ServiceType;
@@ -51,6 +52,7 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.provider.Settings.Global;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 import android.test.mock.MockContentResolver;
@@ -105,6 +107,9 @@
         PowerManagerInternal injectPowerManagerInternal() {
             return mMockPowerManagerInternal;
         }
+
+        @Override
+        boolean isSmallBatteryDevice() { return mIsSmallBatteryDevice; };
     }
 
     private static final int UID_1 = Process.FIRST_APPLICATION_UID + 1;
@@ -144,6 +149,7 @@
     private FakeSettingsProvider mFakeSettingsProvider;
 
     private boolean mPowerSaveMode;
+    private boolean mIsSmallBatteryDevice;
 
     private final ArraySet<Pair<Integer, String>> mRestrictedPackages = new ArraySet();
 
@@ -232,6 +238,7 @@
         assertNotNull(mAppOpsCallback);
         assertNotNull(mPowerSaveObserver);
         assertNotNull(mReceiver);
+        assertNotNull(instance.mFlagsObserver);
     }
 
     private void setAppOps(int uid, String packageName, boolean restrict) throws RemoteException {
@@ -852,6 +859,56 @@
         assertTrue(instance.isRunAnyInBackgroundAppOpsAllowed(UID_10_2, PACKAGE_2));
     }
 
+    @Test
+    public void testSmallBatteryAndPluggedIn() throws Exception {
+        // This is a small battery device
+        mIsSmallBatteryDevice = true;
+
+        final ForceAppStandbyTrackerTestable instance = newInstance();
+        callStart(instance);
+        assertFalse(instance.isForceAllAppsStandbyEnabled());
+
+        // Setting/experiment for all app standby for small battery is enabled
+        Global.putInt(mMockContentResolver, Global.FORCED_APP_STANDBY_FOR_SMALL_BATTERY_ENABLED, 1);
+        instance.mFlagsObserver.onChange(true,
+                Global.getUriFor(Global.FORCED_APP_STANDBY_FOR_SMALL_BATTERY_ENABLED));
+        assertTrue(instance.isForceAllAppsStandbyEnabled());
+
+        // When battery is plugged in, force app standby is disabled
+        Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED);
+        intent.putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_USB);
+        mReceiver.onReceive(mMockContext, intent);
+        assertFalse(instance.isForceAllAppsStandbyEnabled());
+
+        // When battery stops plugged in, force app standby is enabled
+        mReceiver.onReceive(mMockContext, new Intent(Intent.ACTION_BATTERY_CHANGED));
+        assertTrue(instance.isForceAllAppsStandbyEnabled());
+    }
+
+    @Test
+    public void testNotSmallBatteryAndPluggedIn() throws Exception {
+        // Not a small battery device, so plugged in status should not affect forced app standby
+        mIsSmallBatteryDevice = false;
+
+        final ForceAppStandbyTrackerTestable instance = newInstance();
+        callStart(instance);
+        assertFalse(instance.isForceAllAppsStandbyEnabled());
+
+        mPowerSaveMode = true;
+        mPowerSaveObserver.accept(getPowerSaveState());
+        assertTrue(instance.isForceAllAppsStandbyEnabled());
+
+        // When battery is plugged in, force app standby is unaffected
+        Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED);
+        intent.putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_USB);
+        mReceiver.onReceive(mMockContext, intent);
+        assertTrue(instance.isForceAllAppsStandbyEnabled());
+
+        // When battery stops plugged in, force app standby is unaffected
+        mReceiver.onReceive(mMockContext, new Intent(Intent.ACTION_BATTERY_CHANGED));
+        assertTrue(instance.isForceAllAppsStandbyEnabled());
+    }
+
     static int[] array(int... appIds) {
         Arrays.sort(appIds);
         return appIds;