Introduce Low Power Standby API and wakelock restrictions

In Low Power Standby, additional restrictions are placed on apps that
are in a process state of FOREGROUND_SERVICE or less important
during standby (while the device is non-interactive):
- Wakelocks are disabled
- Network access is blocked
During doze maintenance windows the restrictions are lifted temporarily.

This change introduces the APIs for Low Power Standby, as well as
the wakelock restrictions.

This feature is targeting TVs. To prevent Low Power Standby from being
enabled on other devices, the feature is guarded by the config flag
config_lowPowerStandbySupported.

Bug: 190822356
Test: atest LowPowerStandbyControllerTest PowerManagerServiceTest
Ignore-AOSP-First: New permission only added internally for now.
Change-Id: Ia40f8a0fc4b366860af58ad76c988f93a5d41936
diff --git a/core/api/current.txt b/core/api/current.txt
index ee5b2e1..687fb25 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -31664,6 +31664,7 @@
     method public boolean isDeviceLightIdleMode();
     method public boolean isIgnoringBatteryOptimizations(String);
     method public boolean isInteractive();
+    method public boolean isLowPowerStandbyEnabled();
     method public boolean isPowerSaveMode();
     method public boolean isRebootingUserspaceSupported();
     method @Deprecated public boolean isScreenOn();
@@ -31675,6 +31676,7 @@
     field public static final int ACQUIRE_CAUSES_WAKEUP = 268435456; // 0x10000000
     field public static final String ACTION_DEVICE_IDLE_MODE_CHANGED = "android.os.action.DEVICE_IDLE_MODE_CHANGED";
     field public static final String ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED = "android.os.action.LIGHT_DEVICE_IDLE_MODE_CHANGED";
+    field public static final String ACTION_LOW_POWER_STANDBY_ENABLED_CHANGED = "android.os.action.LOW_POWER_STANDBY_ENABLED_CHANGED";
     field public static final String ACTION_POWER_SAVE_MODE_CHANGED = "android.os.action.POWER_SAVE_MODE_CHANGED";
     field @Deprecated public static final int FULL_WAKE_LOCK = 26; // 0x1a
     field public static final int LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF = 2; // 0x2
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 18a3f86..c5bc165 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -166,6 +166,7 @@
     field public static final String MANAGE_GAME_MODE = "android.permission.MANAGE_GAME_MODE";
     field public static final String MANAGE_HOTWORD_DETECTION = "android.permission.MANAGE_HOTWORD_DETECTION";
     field public static final String MANAGE_IPSEC_TUNNELS = "android.permission.MANAGE_IPSEC_TUNNELS";
+    field public static final String MANAGE_LOW_POWER_STANDBY = "android.permission.MANAGE_LOW_POWER_STANDBY";
     field public static final String MANAGE_MUSIC_RECOGNITION = "android.permission.MANAGE_MUSIC_RECOGNITION";
     field public static final String MANAGE_NOTIFICATION_LISTENERS = "android.permission.MANAGE_NOTIFICATION_LISTENERS";
     field public static final String MANAGE_ONE_TIME_PERMISSION_SESSIONS = "android.permission.MANAGE_ONE_TIME_PERMISSION_SESSIONS";
@@ -9127,11 +9128,13 @@
     method @RequiresPermission(android.Manifest.permission.READ_DREAM_STATE) public boolean isAmbientDisplayAvailable();
     method @RequiresPermission(android.Manifest.permission.READ_DREAM_STATE) public boolean isAmbientDisplaySuppressed();
     method @RequiresPermission(android.Manifest.permission.READ_DREAM_STATE) public boolean isAmbientDisplaySuppressedForToken(@NonNull String);
+    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_LOW_POWER_STANDBY, android.Manifest.permission.DEVICE_POWER}) public boolean isLowPowerStandbySupported();
     method @RequiresPermission(anyOf={android.Manifest.permission.DEVICE_POWER, android.Manifest.permission.POWER_SAVER}) public boolean setAdaptivePowerSaveEnabled(boolean);
     method @RequiresPermission(anyOf={android.Manifest.permission.DEVICE_POWER, android.Manifest.permission.POWER_SAVER}) public boolean setAdaptivePowerSavePolicy(@NonNull android.os.BatterySaverPolicyConfig);
     method @RequiresPermission(anyOf={android.Manifest.permission.BATTERY_PREDICTION, android.Manifest.permission.DEVICE_POWER}) public void setBatteryDischargePrediction(@NonNull java.time.Duration, boolean);
     method @RequiresPermission(android.Manifest.permission.POWER_SAVER) public boolean setDynamicPowerSaveHint(boolean, int);
     method @RequiresPermission(anyOf={android.Manifest.permission.DEVICE_POWER, android.Manifest.permission.POWER_SAVER}) public boolean setFullPowerSavePolicy(@NonNull android.os.BatterySaverPolicyConfig);
+    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_LOW_POWER_STANDBY, android.Manifest.permission.DEVICE_POWER}) public void setLowPowerStandbyEnabled(boolean);
     method @RequiresPermission(anyOf={android.Manifest.permission.DEVICE_POWER, android.Manifest.permission.POWER_SAVER}) public boolean setPowerSaveModeEnabled(boolean);
     method @RequiresPermission(android.Manifest.permission.WRITE_DREAM_STATE) public void suppressAmbientDisplay(@NonNull String, boolean);
     method @RequiresPermission(anyOf={android.Manifest.permission.DEVICE_POWER, android.Manifest.permission.USER_ACTIVITY}) public void userActivity(long, int, int);
diff --git a/core/java/android/os/IPowerManager.aidl b/core/java/android/os/IPowerManager.aidl
index 425e797..b59409e 100644
--- a/core/java/android/os/IPowerManager.aidl
+++ b/core/java/android/os/IPowerManager.aidl
@@ -65,6 +65,9 @@
     boolean isBatteryDischargePredictionPersonalized();
     boolean isDeviceIdleMode();
     boolean isLightDeviceIdleMode();
+    boolean isLowPowerStandbySupported();
+    boolean isLowPowerStandbyEnabled();
+    void setLowPowerStandbyEnabled(boolean enabled);
 
     @UnsupportedAppUsage
     void reboot(boolean confirm, String reason, boolean wait);
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index 881fced..87e488b 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -2146,6 +2146,64 @@
     }
 
     /**
+     * Returns true if Low Power Standby is supported on this device.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.MANAGE_LOW_POWER_STANDBY,
+            android.Manifest.permission.DEVICE_POWER
+    })
+    public boolean isLowPowerStandbySupported() {
+        try {
+            return mService.isLowPowerStandbySupported();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns true if Low Power Standby is enabled.
+     *
+     * <p>When Low Power Standby is enabled, apps (including apps running foreground services) are
+     * subject to additional restrictions while the device is non-interactive, outside of device
+     * idle maintenance windows: Their network access is disabled, and any wakelocks they hold are
+     * ignored.
+     *
+     * <p>When Low Power Standby is enabled or disabled, a Intent with action
+     * {@link #ACTION_LOW_POWER_STANDBY_ENABLED_CHANGED} is broadcast to registered receivers.
+     */
+    public boolean isLowPowerStandbyEnabled() {
+        try {
+            return mService.isLowPowerStandbyEnabled();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Set whether Low Power Standby is enabled.
+     * Does nothing if Low Power Standby is not supported.
+     *
+     * @see #isLowPowerStandbySupported()
+     * @see #isLowPowerStandbyEnabled()
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.MANAGE_LOW_POWER_STANDBY,
+            android.Manifest.permission.DEVICE_POWER
+    })
+    public void setLowPowerStandbyEnabled(boolean enabled) {
+        try {
+            mService.setLowPowerStandbyEnabled(enabled);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Return whether the given application package name is on the device's power allowlist.
      * Apps can be placed on the allowlist through the settings UI invoked by
      * {@link android.provider.Settings#ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS}.
@@ -2631,6 +2689,16 @@
             = "android.os.action.POWER_SAVE_TEMP_WHITELIST_CHANGED";
 
     /**
+     * Intent that is broadcast when Low Power Standby is enabled or disabled.
+     * This broadcast is only sent to registered receivers.
+     *
+     * @see #isLowPowerStandbyEnabled()
+     */
+    @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_LOW_POWER_STANDBY_ENABLED_CHANGED =
+            "android.os.action.LOW_POWER_STANDBY_ENABLED_CHANGED";
+
+    /**
      * Constant for PreIdleTimeout normal mode (default mode, not short nor extend timeout) .
      * @hide
      */
diff --git a/core/java/android/os/PowerManagerInternal.java b/core/java/android/os/PowerManagerInternal.java
index eb18b96..aee57fa 100644
--- a/core/java/android/os/PowerManagerInternal.java
+++ b/core/java/android/os/PowerManagerInternal.java
@@ -184,6 +184,14 @@
 
     public abstract void setDeviceIdleTempWhitelist(int[] appids);
 
+    /**
+     * Used by LowPowerStandbyController to notify the power manager that Low Power Standby's
+     * active state has changed.
+     *
+     * @param active {@code true} to activate Low Power Standby, {@code false} to turn it off.
+     */
+    public abstract void setLowPowerStandbyActive(boolean active);
+
     public abstract void startUidChanges();
 
     public abstract void finishUidChanges();
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 2a563ac..28d2a72 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -16706,6 +16706,17 @@
         public static final String RESTRICTED_NETWORKING_MODE = "restricted_networking_mode";
 
         /**
+         * Setting indicating whether Low Power Standby is enabled, if supported.
+         *
+         * Values are:
+         * 0: disabled
+         * 1: enabled
+         *
+         * @hide
+         */
+        public static final String LOW_POWER_STANDBY_ENABLED = "low_power_standby_enabled";
+
+        /**
          * Settings migrated from Wear OS settings provider.
          * @hide
          */
diff --git a/core/proto/android/server/powermanagerservice.proto b/core/proto/android/server/powermanagerservice.proto
index acb7429..0a8f9df 100644
--- a/core/proto/android/server/powermanagerservice.proto
+++ b/core/proto/android/server/powermanagerservice.proto
@@ -127,7 +127,7 @@
     optional bool is_light_device_idle_mode = 25;
     // True if we are currently in device idle mode.
     optional bool is_device_idle_mode = 26;
-    // Set of app ids that we will always respect the wake locks for.
+    // Set of app ids that we will respect the wake locks for while in device idle mode.
     repeated int32 device_idle_whitelist = 27;
     // Set of app ids that are temporarily allowed to acquire wakelocks due to
     // high-pri message
@@ -187,6 +187,8 @@
     // Whether or not the current enhanced discharge prediction is personalized based on device
     // usage or not.
     optional bool is_enhanced_discharge_prediction_personalized = 54;
+    optional bool is_low_power_standby_active = 55;
+    optional LowPowerStandbyControllerDumpProto low_power_standby_controller = 56;
 }
 
 // A com.android.server.power.PowerManagerService.SuspendBlockerImpl object.
@@ -428,3 +430,36 @@
 
     // Next tag: 23
 }
+
+message LowPowerStandbyControllerDumpProto {
+    option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
+    // True if Low Power Standby is active
+    optional bool is_active = 1;
+
+    // True if Low Power Standby is enabled
+    optional bool is_enabled = 2;
+
+    // True if Low Power Standby is supported
+    optional bool is_supported_config = 3;
+
+    // True if Low Power Standby is enabled by default
+    optional bool is_enabled_by_default_config = 4;
+
+    // True if the device is currently interactive
+    optional bool is_interactive = 5;
+
+    // Time (in elapsedRealtime) when the device was last interactive
+    optional bool last_interactive_time = 6;
+
+    // Time (in milliseconds) after becoming non-interactive that Low Power Standby can activate
+    optional int32 standby_timeout_config = 7;
+
+    // True if the device has entered idle mode since becoming non-interactive
+    optional int32 idle_since_non_interactive = 8;
+
+    // True if the device is currently in idle mode
+    optional int32 is_device_idle = 9;
+
+    // Next tag: 10
+}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 8cf5421..36cf4fe 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -107,6 +107,7 @@
     <protected-broadcast android:name="android.os.action.POWER_SAVE_WHITELIST_CHANGED" />
     <protected-broadcast android:name="android.os.action.POWER_SAVE_TEMP_WHITELIST_CHANGED" />
     <protected-broadcast android:name="android.os.action.POWER_SAVE_MODE_CHANGED_INTERNAL" />
+    <protected-broadcast android:name="android.os.action.LOW_POWER_STANDBY_ENABLED_CHANGED" />
     <protected-broadcast android:name="android.os.action.ENHANCED_DISCHARGE_PREDICTION_CHANGED" />
 
     <!-- @deprecated This is rarely used and will be phased out soon. -->
@@ -4955,6 +4956,11 @@
     <permission android:name="android.permission.USER_ACTIVITY"
         android:protectionLevel="signature|privileged" />
 
+    <!-- @hide @SystemApi Allows an application to manage Low Power Standby settings.
+         <p>Not for use by third-party applications. -->
+    <permission android:name="android.permission.MANAGE_LOW_POWER_STANDBY"
+                android:protectionLevel="signature|privileged" />
+
    <!-- @hide Allows low-level access to tun tap driver -->
     <permission android:name="android.permission.NET_TUNNELING"
         android:protectionLevel="signature" />
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 2dd17cf..e80f8e7 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -5621,4 +5621,14 @@
 
     <!-- Whether or not to enable the lock screen entry point for the QR code scanner. -->
     <bool name="config_enableQrCodeScannerOnLockScreen">false</bool>
+
+    <!-- Whether Low Power Standby is supported and can be enabled. -->
+    <bool name="config_lowPowerStandbySupported">false</bool>
+
+    <!-- If supported, whether Low Power Standby is enabled by default. -->
+    <bool name="config_lowPowerStandbyEnabledByDefault">false</bool>
+
+    <!-- The amount of time after becoming non-interactive (in ms) after which
+         Low Power Standby can activate. -->
+    <integer name="config_lowPowerStandbyNonInteractiveTimeout">5000</integer>
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 67369d2..a16d825 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4666,4 +4666,8 @@
   <java-symbol type="string" name="notification_content_abusive_bg_apps"/>
   <java-symbol type="string" name="notification_content_long_running_fgs"/>
   <java-symbol type="string" name="notification_action_check_bg_apps"/>
+
+  <java-symbol type="bool" name="config_lowPowerStandbySupported" />
+  <java-symbol type="bool" name="config_lowPowerStandbyEnabledByDefault" />
+  <java-symbol type="integer" name="config_lowPowerStandbyNonInteractiveTimeout" />
 </resources>
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index e68b1ac..b3dcc34 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -334,6 +334,7 @@
         <permission name="android.permission.MANAGE_ACCESSIBILITY"/>
         <permission name="android.permission.MANAGE_DEVICE_ADMINS"/>
         <permission name="android.permission.MANAGE_GAME_MODE"/>
+        <permission name="android.permission.MANAGE_LOW_POWER_STANDBY" />
         <permission name="android.permission.MANAGE_ROLLBACKS"/>
         <permission name="android.permission.MANAGE_USB"/>
         <permission name="android.permission.MODIFY_APPWIDGET_BIND_PERMISSIONS"/>
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 52a708d..259f503 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -326,6 +326,7 @@
                     Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL_MAX,
                     Settings.Global.LOW_POWER_MODE_STICKY,
                     Settings.Global.LOW_POWER_MODE_SUGGESTION_PARAMS,
+                    Settings.Global.LOW_POWER_STANDBY_ENABLED,
                     Settings.Global.LTE_SERVICE_FORCED,
                     Settings.Global.LID_BEHAVIOR,
                     Settings.Global.MAX_ERROR_BYTES_PREFIX,
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 4dddc8c..f80d116 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -249,6 +249,7 @@
     <uses-permission android:name="android.permission.MANAGE_CONTENT_CAPTURE" />
     <uses-permission android:name="android.permission.MANAGE_CONTENT_SUGGESTIONS" />
     <uses-permission android:name="android.permission.MANAGE_APP_PREDICTIONS" />
+    <uses-permission android:name="android.permission.MANAGE_LOW_POWER_STANDBY" />
     <uses-permission android:name="android.permission.MANAGE_SEARCH_UI" />
     <uses-permission android:name="android.permission.MANAGE_SMARTSPACE" />
     <uses-permission android:name="android.permission.MANAGE_WALLPAPER_EFFECTS_GENERATION" />
diff --git a/services/core/java/com/android/server/power/LowPowerStandbyController.java b/services/core/java/com/android/server/power/LowPowerStandbyController.java
new file mode 100644
index 0000000..b598493
--- /dev/null
+++ b/services/core/java/com/android/server/power/LowPowerStandbyController.java
@@ -0,0 +1,488 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+package com.android.server.power;
+
+import android.app.AlarmManager;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.PowerManagerInternal;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.IndentingPrintWriter;
+import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.LocalServices;
+
+import java.io.PrintWriter;
+
+/**
+ * Controls Low Power Standby state.
+ *
+ * Instantiated by {@link PowerManagerService} only if Low Power Standby is supported.
+ *
+ * <p>Low Power Standby is active when all of the following conditions are met:
+ * <ul>
+ *   <li>Low Power Standby is enabled
+ *   <li>The device is not interactive, and has been non-interactive for a given timeout
+ *   <li>The device is not in a doze maintenance window
+ * </ul>
+ *
+ * <p>When Low Power Standby is active, the following restrictions are applied to applications
+ * with procstate less important than {@link android.app.ActivityManager#PROCESS_STATE_BOUND_TOP}:
+ * <ul>
+ *   <li>Network access is blocked
+ *   <li>Wakelocks are disabled
+ * </ul>
+ *
+ * @hide
+ */
+public final class LowPowerStandbyController {
+    private static final String TAG = "LowPowerStandbyController";
+    private static final boolean DEBUG = false;
+
+    private static final int MSG_STANDBY_TIMEOUT = 0;
+    private static final int MSG_NOTIFY_ACTIVE_CHANGED = 1;
+
+    private final Handler mHandler;
+    private final SettingsObserver mSettingsObserver;
+    private final Object mLock = new Object();
+
+    private final Context mContext;
+    private final Clock mClock;
+    private final AlarmManager.OnAlarmListener mOnStandbyTimeoutExpired =
+            this::onStandbyTimeoutExpired;
+
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            switch (intent.getAction()) {
+                case Intent.ACTION_SCREEN_OFF:
+                    onNonInteractive();
+                    break;
+                case Intent.ACTION_SCREEN_ON:
+                    onInteractive();
+                    break;
+                case PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED:
+                    onDeviceIdleModeChanged();
+                    break;
+            }
+        }
+    };
+
+    @GuardedBy("mLock")
+    private AlarmManager mAlarmManager;
+    @GuardedBy("mLock")
+    private PowerManager mPowerManager;
+    @GuardedBy("mLock")
+    private boolean mSupportedConfig;
+    @GuardedBy("mLock")
+    private boolean mEnabledByDefaultConfig;
+    @GuardedBy("mLock")
+    private int mStandbyTimeoutConfig;
+
+    /** Whether Low Power Standby is enabled in Settings */
+    @GuardedBy("mLock")
+    private boolean mIsEnabled;
+
+    /**
+     * Whether Low Power Standby is currently active (enforcing restrictions).
+     */
+    @GuardedBy("mLock")
+    private boolean mIsActive;
+
+    /** Whether the device is currently interactive */
+    @GuardedBy("mLock")
+    private boolean mIsInteractive;
+
+    /** The time the device was last interactive, in {@link SystemClock#elapsedRealtime()}. */
+    @GuardedBy("mLock")
+    private long mLastInteractiveTimeElapsed;
+
+    /**
+     * Whether we are in device idle mode.
+     * During maintenance windows Low Power Standby is deactivated to allow
+     * apps to run maintenance tasks.
+     */
+    @GuardedBy("mLock")
+    private boolean mIsDeviceIdle;
+
+    /**
+     * Whether the device has entered idle mode since becoming non-interactive.
+     * In the initial non-idle period after turning the screen off, Low Power Standby is already
+     * allowed to become active. Later non-idle periods are treated as maintenance windows, during
+     * which Low Power Standby is deactivated to allow apps to run maintenance tasks.
+     */
+    @GuardedBy("mLock")
+    private boolean mIdleSinceNonInteractive;
+
+    /** Functional interface for providing time. */
+    @VisibleForTesting
+    interface Clock {
+        /** Returns milliseconds since boot, including time spent in sleep. */
+        long elapsedRealtime();
+    }
+
+    public LowPowerStandbyController(Context context, Looper looper, Clock clock) {
+        mContext = context;
+        mHandler = new LowPowerStandbyHandler(looper);
+        mClock = clock;
+        mSettingsObserver = new SettingsObserver(mHandler);
+    }
+
+    void systemReady() {
+        final Resources resources = mContext.getResources();
+        synchronized (mLock) {
+            mSupportedConfig = resources.getBoolean(
+                    com.android.internal.R.bool.config_lowPowerStandbySupported);
+
+            if (!mSupportedConfig) {
+                return;
+            }
+
+            mAlarmManager = mContext.getSystemService(AlarmManager.class);
+            mPowerManager = mContext.getSystemService(PowerManager.class);
+
+            mStandbyTimeoutConfig = resources.getInteger(
+                    R.integer.config_lowPowerStandbyNonInteractiveTimeout);
+            mEnabledByDefaultConfig = resources.getBoolean(
+                    R.bool.config_lowPowerStandbyEnabledByDefault);
+
+            mIsInteractive = mPowerManager.isInteractive();
+
+            mContext.getContentResolver().registerContentObserver(Settings.Global.getUriFor(
+                    Settings.Global.LOW_POWER_STANDBY_ENABLED),
+                    false, mSettingsObserver, UserHandle.USER_ALL);
+            updateSettingsLocked();
+
+            if (mIsEnabled) {
+                registerBroadcastReceiver();
+            }
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void updateSettingsLocked() {
+        final ContentResolver resolver = mContext.getContentResolver();
+        mIsEnabled = mSupportedConfig && Settings.Global.getInt(resolver,
+                Settings.Global.LOW_POWER_STANDBY_ENABLED,
+                mEnabledByDefaultConfig ? 1 : 0) != 0;
+    }
+
+    @GuardedBy("mLock")
+    private void updateActiveLocked() {
+        final long now = mClock.elapsedRealtime();
+        final boolean standbyTimeoutExpired =
+                (now - mLastInteractiveTimeElapsed) >= mStandbyTimeoutConfig;
+        final boolean maintenanceMode = mIdleSinceNonInteractive && !mIsDeviceIdle;
+        final boolean newActive =
+                mIsEnabled && !mIsInteractive && standbyTimeoutExpired && !maintenanceMode;
+        if (DEBUG) {
+            Slog.d(TAG, "updateActiveLocked: mIsEnabled=" + mIsEnabled + ", mIsInteractive="
+                    + mIsInteractive + ", standbyTimeoutExpired=" + standbyTimeoutExpired
+                    + ", mIdleSinceNonInteractive=" + mIdleSinceNonInteractive + ", mIsDeviceIdle="
+                    + mIsDeviceIdle + ", mIsActive=" + mIsActive + ", newActive=" + newActive);
+        }
+        if (mIsActive != newActive) {
+            mIsActive = newActive;
+            if (DEBUG) {
+                Slog.d(TAG, "mIsActive changed, mIsActive=" + mIsActive);
+            }
+            enqueueNotifyActiveChangedLocked();
+        }
+    }
+
+    private void onNonInteractive() {
+        if (DEBUG) {
+            Slog.d(TAG, "onNonInteractive");
+        }
+        final long now = mClock.elapsedRealtime();
+        synchronized (mLock) {
+            mIsInteractive = false;
+            mIsDeviceIdle = false;
+            mLastInteractiveTimeElapsed = now;
+
+            if (mStandbyTimeoutConfig > 0) {
+                scheduleStandbyTimeoutAlarmLocked();
+            }
+
+            updateActiveLocked();
+        }
+    }
+
+    private void onInteractive() {
+        if (DEBUG) {
+            Slog.d(TAG, "onInteractive");
+        }
+
+        synchronized (mLock) {
+            cancelStandbyTimeoutAlarmLocked();
+            mIsInteractive = true;
+            mIsDeviceIdle = false;
+            mIdleSinceNonInteractive = false;
+            updateActiveLocked();
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void scheduleStandbyTimeoutAlarmLocked() {
+        final long nextAlarmTime = SystemClock.elapsedRealtime() + mStandbyTimeoutConfig;
+        mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+                nextAlarmTime, "LowPowerStandbyController.StandbyTimeout",
+                mOnStandbyTimeoutExpired, mHandler);
+    }
+
+    @GuardedBy("mLock")
+    private void cancelStandbyTimeoutAlarmLocked() {
+        mAlarmManager.cancel(mOnStandbyTimeoutExpired);
+    }
+
+    private void onDeviceIdleModeChanged() {
+        synchronized (mLock) {
+            mIsDeviceIdle = mPowerManager.isDeviceIdleMode();
+            if (DEBUG) {
+                Slog.d(TAG, "onDeviceIdleModeChanged, mIsDeviceIdle=" + mIsDeviceIdle);
+            }
+
+            mIdleSinceNonInteractive = mIdleSinceNonInteractive || mIsDeviceIdle;
+            updateActiveLocked();
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void onEnabledLocked() {
+        if (DEBUG) {
+            Slog.d(TAG, "onEnabledLocked");
+        }
+
+        if (mPowerManager.isInteractive()) {
+            onInteractive();
+        } else {
+            onNonInteractive();
+        }
+
+        registerBroadcastReceiver();
+    }
+
+    @GuardedBy("mLock")
+    private void onDisabledLocked() {
+        if (DEBUG) {
+            Slog.d(TAG, "onDisabledLocked");
+        }
+
+        cancelStandbyTimeoutAlarmLocked();
+        unregisterBroadcastReceiver();
+        updateActiveLocked();
+    }
+
+    @VisibleForTesting
+    void onSettingsChanged() {
+        if (DEBUG) {
+            Slog.d(TAG, "onSettingsChanged");
+        }
+        synchronized (mLock) {
+            final boolean oldEnabled = mIsEnabled;
+            updateSettingsLocked();
+
+            if (mIsEnabled != oldEnabled) {
+                if (mIsEnabled) {
+                    onEnabledLocked();
+                } else {
+                    onDisabledLocked();
+                }
+
+                notifyEnabledChangedLocked();
+            }
+        }
+    }
+
+    private void registerBroadcastReceiver() {
+        IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);
+        intentFilter.addAction(Intent.ACTION_SCREEN_ON);
+        intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
+
+        mContext.registerReceiver(mBroadcastReceiver, intentFilter);
+    }
+
+    private void unregisterBroadcastReceiver() {
+        mContext.unregisterReceiver(mBroadcastReceiver);
+    }
+
+    @GuardedBy("mLock")
+    private void notifyEnabledChangedLocked() {
+        if (DEBUG) {
+            Slog.d(TAG, "notifyEnabledChangedLocked, mIsEnabled=" + mIsEnabled);
+        }
+
+        final Intent intent = new Intent(PowerManager.ACTION_LOW_POWER_STANDBY_ENABLED_CHANGED);
+        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY | Intent.FLAG_RECEIVER_FOREGROUND);
+        mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
+    }
+
+    private void onStandbyTimeoutExpired() {
+        if (DEBUG) {
+            Slog.d(TAG, "onStandbyTimeoutExpired");
+        }
+        synchronized (mLock) {
+            updateActiveLocked();
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void enqueueNotifyActiveChangedLocked() {
+        final long now = mClock.elapsedRealtime();
+        final Message msg = mHandler.obtainMessage(MSG_NOTIFY_ACTIVE_CHANGED, mIsActive);
+        mHandler.sendMessageAtTime(msg, now);
+    }
+
+    /** Notify other system components about the updated Low Power Standby active state */
+    private void notifyActiveChanged(boolean active) {
+        final PowerManagerInternal pmi = LocalServices.getService(PowerManagerInternal.class);
+        pmi.setLowPowerStandbyActive(active);
+    }
+
+    @VisibleForTesting
+    boolean isActive() {
+        synchronized (mLock) {
+            return mIsActive;
+        }
+    }
+
+    boolean isSupported() {
+        synchronized (mLock) {
+            return mSupportedConfig;
+        }
+    }
+
+    boolean isEnabled() {
+        synchronized (mLock) {
+            return mSupportedConfig && mIsEnabled;
+        }
+    }
+
+    void setEnabled(boolean enabled) {
+        synchronized (mLock) {
+            if (!mSupportedConfig) {
+                Slog.w(TAG, "Low Power Standby cannot be enabled "
+                        + "because it is not supported on this device");
+                return;
+            }
+
+            Settings.Global.putInt(mContext.getContentResolver(),
+                    Settings.Global.LOW_POWER_STANDBY_ENABLED, enabled ? 1 : 0);
+            onSettingsChanged();
+        }
+    }
+
+    void dump(PrintWriter pw) {
+        final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
+
+        ipw.println();
+        ipw.println("Low Power Standby Controller:");
+        ipw.increaseIndent();
+        synchronized (mLock) {
+            ipw.print("mIsActive=");
+            ipw.println(mIsActive);
+            ipw.print("mIsEnabled=");
+            ipw.println(mIsEnabled);
+            ipw.print("mSupportedConfig=");
+            ipw.println(mSupportedConfig);
+            ipw.print("mEnabledByDefaultConfig=");
+            ipw.println(mEnabledByDefaultConfig);
+            ipw.print("mStandbyTimeoutConfig=");
+            ipw.println(mStandbyTimeoutConfig);
+
+            if (mIsEnabled) {
+                ipw.print("mIsInteractive=");
+                ipw.println(mIsInteractive);
+                ipw.print("mLastInteractiveTime=");
+                ipw.println(mLastInteractiveTimeElapsed);
+                ipw.print("mIdleSinceNonInteractive=");
+                ipw.println(mIdleSinceNonInteractive);
+                ipw.print("mIsDeviceIdle=");
+                ipw.println(mIsDeviceIdle);
+            }
+        }
+        ipw.decreaseIndent();
+    }
+
+    void dumpProto(ProtoOutputStream proto, long tag) {
+        synchronized (mLock) {
+            final long token = proto.start(tag);
+            proto.write(LowPowerStandbyControllerDumpProto.IS_ACTIVE, mIsActive);
+            proto.write(LowPowerStandbyControllerDumpProto.IS_ENABLED, mIsEnabled);
+            proto.write(LowPowerStandbyControllerDumpProto.IS_SUPPORTED_CONFIG, mSupportedConfig);
+            proto.write(LowPowerStandbyControllerDumpProto.IS_ENABLED_BY_DEFAULT_CONFIG,
+                    mEnabledByDefaultConfig);
+            proto.write(LowPowerStandbyControllerDumpProto.IS_INTERACTIVE, mIsInteractive);
+            proto.write(LowPowerStandbyControllerDumpProto.LAST_INTERACTIVE_TIME,
+                    mLastInteractiveTimeElapsed);
+            proto.write(LowPowerStandbyControllerDumpProto.STANDBY_TIMEOUT_CONFIG,
+                    mStandbyTimeoutConfig);
+            proto.write(LowPowerStandbyControllerDumpProto.IDLE_SINCE_NON_INTERACTIVE,
+                    mIdleSinceNonInteractive);
+            proto.write(LowPowerStandbyControllerDumpProto.IS_DEVICE_IDLE, mIsDeviceIdle);
+
+            proto.end(token);
+        }
+    }
+
+    private class LowPowerStandbyHandler extends Handler {
+        LowPowerStandbyHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_STANDBY_TIMEOUT:
+                    onStandbyTimeoutExpired();
+                    break;
+                case MSG_NOTIFY_ACTIVE_CHANGED:
+                    boolean active = (boolean) msg.obj;
+                    notifyActiveChanged(active);
+                    break;
+            }
+        }
+    }
+
+    private final class SettingsObserver extends ContentObserver {
+        SettingsObserver(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            onSettingsChanged();
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 4185b2d9..0483c74 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -37,6 +37,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.app.SynchronousUserSwitchObserver;
@@ -276,6 +277,7 @@
     private final BatterySaverPolicy mBatterySaverPolicy;
     private final BatterySaverStateMachine mBatterySaverStateMachine;
     private final BatterySavingStats mBatterySavingStats;
+    private final LowPowerStandbyController mLowPowerStandbyController;
     private final AttentionDetector mAttentionDetector;
     private final FaceDownDetector mFaceDownDetector;
     private final ScreenUndimDetector mScreenUndimDetector;
@@ -609,12 +611,14 @@
     // True if we are currently in light device idle mode.
     private boolean mLightDeviceIdleMode;
 
-    // Set of app ids that we will always respect the wake locks for.
+    // Set of app ids that we will respect the wake locks for while in device idle mode.
     int[] mDeviceIdleWhitelist = new int[0];
 
     // Set of app ids that are temporarily allowed to acquire wakelocks due to high-pri message
     int[] mDeviceIdleTempWhitelist = new int[0];
 
+    private boolean mLowPowerStandbyActive;
+
     private final SparseArray<UidState> mUidState = new SparseArray<>();
 
     // A mapping from DisplayGroup Id to PowerGroup. There is a 1-1 mapping between DisplayGroups
@@ -966,6 +970,10 @@
         void invalidateIsInteractiveCaches() {
             PowerManager.invalidateIsInteractiveCaches();
         }
+
+        LowPowerStandbyController createLowPowerStandbyController(Context context, Looper looper) {
+            return new LowPowerStandbyController(context, looper, SystemClock::elapsedRealtime);
+        }
     }
 
     final Constants mConstants;
@@ -1015,6 +1023,8 @@
         mBatterySaverStateMachine = mInjector.createBatterySaverStateMachine(mLock, mContext,
                 mBatterySaverController);
 
+        mLowPowerStandbyController = mInjector.createLowPowerStandbyController(mContext,
+                Looper.getMainLooper());
         mInattentiveSleepWarningOverlayController =
                 mInjector.createInattentiveSleepWarningController();
 
@@ -1228,6 +1238,8 @@
                 // Shouldn't happen since in-process.
             }
 
+            mLowPowerStandbyController.systemReady();
+
             // Go.
             readConfigurationLocked();
             updateSettingsLocked();
@@ -1654,6 +1666,16 @@
     }
 
     @GuardedBy("mLock")
+    @VisibleForTesting
+    WakeLock findWakeLockLocked(IBinder lock) {
+        int index = findWakeLockIndexLocked(lock);
+        if (index == -1) {
+            return null;
+        }
+        return mWakeLocks.get(index);
+    }
+
+    @GuardedBy("mLock")
     private void notifyWakeLockAcquiredLocked(WakeLock wakeLock) {
         if (mSystemReady && !wakeLock.mDisabled) {
             wakeLock.mNotifiedAcquired = true;
@@ -3852,6 +3874,15 @@
         }
     }
 
+    void setLowPowerStandbyActiveInternal(boolean active) {
+        synchronized (mLock) {
+            if (mLowPowerStandbyActive != active) {
+                mLowPowerStandbyActive = active;
+                updateWakeLockDisabledStatesLocked();
+            }
+        }
+    }
+
     void startUidChangesInternal() {
         synchronized (mLock) {
             mUidsChanging = true;
@@ -3888,7 +3919,7 @@
                     <= ActivityManager.PROCESS_STATE_RECEIVER;
             state.mProcState = procState;
             if (state.mNumWakeLocks > 0) {
-                if (mDeviceIdleMode) {
+                if (mDeviceIdleMode || mLowPowerStandbyActive) {
                     handleUidStateChangeLocked();
                 } else if (!state.mActive && oldShouldAllow !=
                         (procState <= ActivityManager.PROCESS_STATE_RECEIVER)) {
@@ -3908,7 +3939,7 @@
                 state.mProcState = ActivityManager.PROCESS_STATE_NONEXISTENT;
                 state.mActive = false;
                 mUidState.removeAt(index);
-                if (mDeviceIdleMode && state.mNumWakeLocks > 0) {
+                if ((mDeviceIdleMode || mLowPowerStandbyActive) && state.mNumWakeLocks > 0) {
                     handleUidStateChangeLocked();
                 }
             }
@@ -3993,6 +4024,13 @@
                         disabled = true;
                     }
                 }
+                if (mLowPowerStandbyActive) {
+                    final UidState state = wakeLock.mUidState;
+                    if (state.mProcState != ActivityManager.PROCESS_STATE_NONEXISTENT
+                            && state.mProcState > ActivityManager.PROCESS_STATE_BOUND_TOP) {
+                        disabled = true;
+                    }
+                }
             }
             if (wakeLock.mDisabled != disabled) {
                 wakeLock.mDisabled = disabled;
@@ -4260,6 +4298,7 @@
         pw.println("POWER MANAGER (dumpsys power)\n");
 
         final WirelessChargerDetector wcd;
+        final LowPowerStandbyController lowPowerStandbyController;
         synchronized (mLock) {
             pw.println("Power Manager State:");
             mConstants.dump(pw);
@@ -4316,6 +4355,7 @@
             pw.println("  mDeviceIdleMode=" + mDeviceIdleMode);
             pw.println("  mDeviceIdleWhitelist=" + Arrays.toString(mDeviceIdleWhitelist));
             pw.println("  mDeviceIdleTempWhitelist=" + Arrays.toString(mDeviceIdleTempWhitelist));
+            pw.println("  mLowPowerStandbyActive=" + mLowPowerStandbyActive);
             pw.println("  mLastWakeTime=" + TimeUtils.formatUptime(mLastGlobalWakeTime));
             pw.println("  mLastSleepTime=" + TimeUtils.formatUptime(mLastGlobalSleepTime));
             pw.println("  mLastSleepReason=" + PowerManager.sleepReasonToString(
@@ -4491,10 +4531,13 @@
         mFaceDownDetector.dump(pw);
 
         mAmbientDisplaySuppressionController.dump(pw);
+
+        mLowPowerStandbyController.dump(pw);
     }
 
     private void dumpProto(FileDescriptor fd) {
         final WirelessChargerDetector wcd;
+        final LowPowerStandbyController lowPowerStandbyController;
         final ProtoOutputStream proto = new ProtoOutputStream(fd);
 
         synchronized (mLock) {
@@ -4599,6 +4642,9 @@
                 proto.write(PowerManagerServiceDumpProto.DEVICE_IDLE_TEMP_WHITELIST, id);
             }
 
+            proto.write(PowerManagerServiceDumpProto.IS_LOW_POWER_STANDBY_ACTIVE,
+                    mLowPowerStandbyActive);
+
             proto.write(PowerManagerServiceDumpProto.LAST_WAKE_TIME_MS, mLastGlobalWakeTime);
             proto.write(PowerManagerServiceDumpProto.LAST_SLEEP_TIME_MS, mLastGlobalSleepTime);
             proto.write(
@@ -4832,6 +4878,7 @@
             for (SuspendBlocker sb : mSuspendBlockers) {
                 sb.dumpDebug(proto, PowerManagerServiceDumpProto.SUSPEND_BLOCKERS);
             }
+
             wcd = mWirelessChargerDetector;
         }
 
@@ -4839,6 +4886,9 @@
             wcd.dumpDebug(proto, PowerManagerServiceDumpProto.WIRELESS_CHARGER_DETECTOR);
         }
 
+        mLowPowerStandbyController.dumpProto(proto,
+                PowerManagerServiceDumpProto.LOW_POWER_STANDBY_CONTROLLER);
+
         proto.flush();
     }
 
@@ -5768,6 +5818,58 @@
             }
         }
 
+        @Override // Binder call
+        @RequiresPermission(anyOf = {
+                android.Manifest.permission.MANAGE_LOW_POWER_STANDBY,
+                android.Manifest.permission.DEVICE_POWER
+        })
+        public boolean isLowPowerStandbySupported() {
+            if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER)
+                    != PackageManager.PERMISSION_GRANTED) {
+                mContext.enforceCallingOrSelfPermission(
+                        android.Manifest.permission.MANAGE_LOW_POWER_STANDBY,
+                        "isLowPowerStandbySupported");
+            }
+
+            final long ident = Binder.clearCallingIdentity();
+            try {
+                return mLowPowerStandbyController.isSupported();
+            } finally {
+                Binder.restoreCallingIdentity(ident);
+            }
+        }
+
+        @Override // Binder call
+        public boolean isLowPowerStandbyEnabled() {
+            final long ident = Binder.clearCallingIdentity();
+            try {
+                return mLowPowerStandbyController.isEnabled();
+            } finally {
+                Binder.restoreCallingIdentity(ident);
+            }
+        }
+
+        @Override // Binder call
+        @RequiresPermission(anyOf = {
+                android.Manifest.permission.MANAGE_LOW_POWER_STANDBY,
+                android.Manifest.permission.DEVICE_POWER
+        })
+        public void setLowPowerStandbyEnabled(boolean enabled) {
+            if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER)
+                    != PackageManager.PERMISSION_GRANTED) {
+                mContext.enforceCallingOrSelfPermission(
+                        android.Manifest.permission.MANAGE_LOW_POWER_STANDBY,
+                        "setLowPowerStandbyEnabled");
+            }
+
+            final long ident = Binder.clearCallingIdentity();
+            try {
+                mLowPowerStandbyController.setEnabled(enabled);
+            } finally {
+                Binder.restoreCallingIdentity(ident);
+            }
+        }
+
         /**
          * Gets the reason for the last time the phone had to reboot.
          *
@@ -6249,6 +6351,11 @@
         }
 
         @Override
+        public void setLowPowerStandbyActive(boolean enabled) {
+            setLowPowerStandbyActiveInternal(enabled);
+        }
+
+        @Override
         public void startUidChanges() {
             startUidChangesInternal();
         }
diff --git a/services/tests/servicestests/src/com/android/server/power/LowPowerStandbyControllerTest.java b/services/tests/servicestests/src/com/android/server/power/LowPowerStandbyControllerTest.java
new file mode 100644
index 0000000..cce6a73
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/power/LowPowerStandbyControllerTest.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+package com.android.server.power;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AlarmManager;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.IPowerManager;
+import android.os.PowerManager;
+import android.os.PowerManagerInternal;
+import android.os.test.TestLooper;
+import android.provider.Settings;
+import android.test.mock.MockContentResolver;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.internal.util.test.BroadcastInterceptingContext;
+import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.server.LocalServices;
+import com.android.server.testutils.OffsettableClock;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests for {@link com.android.server.power.LowPowerStandbyController}.
+ *
+ * Build/Install/Run:
+ * atest LowPowerStandbyControllerTest
+ */
+public class LowPowerStandbyControllerTest {
+    private static final int STANDBY_TIMEOUT = 5000;
+
+    private LowPowerStandbyController mController;
+    private BroadcastInterceptingContext mContextSpy;
+    private Resources mResourcesSpy;
+    private OffsettableClock mClock;
+    private TestLooper mTestLooper;
+
+    @Mock
+    private AlarmManager mAlarmManagerMock;
+    @Mock
+    private IPowerManager mIPowerManagerMock;
+    @Mock
+    private PowerManagerInternal mPowerManagerInternalMock;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mContextSpy = spy(new BroadcastInterceptingContext(InstrumentationRegistry.getContext()));
+        when(mContextSpy.getSystemService(AlarmManager.class)).thenReturn(mAlarmManagerMock);
+        PowerManager powerManager = new PowerManager(mContextSpy, mIPowerManagerMock, null, null);
+        when(mContextSpy.getSystemService(PowerManager.class)).thenReturn(powerManager);
+        addLocalServiceMock(PowerManagerInternal.class, mPowerManagerInternalMock);
+
+        when(mIPowerManagerMock.isInteractive()).thenReturn(true);
+
+        mResourcesSpy = spy(mContextSpy.getResources());
+        when(mContextSpy.getResources()).thenReturn(mResourcesSpy);
+        when(mResourcesSpy.getBoolean(
+                com.android.internal.R.bool.config_lowPowerStandbySupported))
+                .thenReturn(true);
+        when(mResourcesSpy.getInteger(
+                com.android.internal.R.integer.config_lowPowerStandbyNonInteractiveTimeout))
+                .thenReturn(STANDBY_TIMEOUT);
+        when(mResourcesSpy.getBoolean(
+                com.android.internal.R.bool.config_lowPowerStandbyEnabledByDefault))
+                .thenReturn(false);
+
+        FakeSettingsProvider.clearSettingsProvider();
+        MockContentResolver cr = new MockContentResolver(mContextSpy);
+        cr.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+        when(mContextSpy.getContentResolver()).thenReturn(cr);
+
+        mClock = new OffsettableClock.Stopped();
+        mTestLooper = new TestLooper(mClock::now);
+
+        mController = new LowPowerStandbyController(mContextSpy, mTestLooper.getLooper(),
+                () -> mClock.now());
+    }
+
+    @Test
+    public void testOnSystemReady_isInactivate() {
+        setLowPowerStandbySupportedConfig(true);
+        mController.systemReady();
+
+        assertThat(mController.isActive()).isFalse();
+        verify(mPowerManagerInternalMock, never()).setLowPowerStandbyActive(anyBoolean());
+    }
+
+    @Test
+    public void testActivate() throws Exception {
+        setLowPowerStandbySupportedConfig(true);
+        mController.systemReady();
+        mController.setEnabled(true);
+        setNonInteractive();
+        setDeviceIdleMode(true);
+        awaitStandbyTimeoutAlarm();
+        assertThat(mController.isActive()).isTrue();
+        verify(mPowerManagerInternalMock, times(1)).setLowPowerStandbyActive(true);
+    }
+
+    private void awaitStandbyTimeoutAlarm() {
+        ArgumentCaptor<Long> timeArg = ArgumentCaptor.forClass(Long.class);
+        ArgumentCaptor<AlarmManager.OnAlarmListener> listenerArg =
+                ArgumentCaptor.forClass(AlarmManager.OnAlarmListener.class);
+        verify(mAlarmManagerMock).setExact(
+                eq(AlarmManager.ELAPSED_REALTIME_WAKEUP),
+                timeArg.capture(), anyString(),
+                listenerArg.capture(), any());
+        mClock.reset();
+        mClock.fastForward(timeArg.getValue());
+        listenerArg.getValue().onAlarm();
+        mTestLooper.dispatchAll();
+    }
+
+    @Test
+    public void testOnNonInteractive_notImmediatelyActive() throws Exception {
+        setLowPowerStandbySupportedConfig(true);
+        mController.systemReady();
+        mController.setEnabled(true);
+
+        setNonInteractive();
+        mTestLooper.dispatchAll();
+
+        assertThat(mController.isActive()).isFalse();
+        verify(mPowerManagerInternalMock, never()).setLowPowerStandbyActive(anyBoolean());
+    }
+
+    @Test
+    public void testOnNonInteractive_activateAfterStandbyTimeout() throws Exception {
+        setLowPowerStandbySupportedConfig(true);
+        mController.systemReady();
+        mController.setEnabled(true);
+
+        setNonInteractive();
+        awaitStandbyTimeoutAlarm();
+
+        assertThat(mController.isActive()).isTrue();
+        verify(mPowerManagerInternalMock, times(1)).setLowPowerStandbyActive(true);
+    }
+
+    @Test
+    public void testOnNonInteractive_doesNotActivateWhenBecomingInteractive() throws Exception {
+        setLowPowerStandbySupportedConfig(true);
+        mController.systemReady();
+        mController.setEnabled(true);
+
+        setNonInteractive();
+        advanceTime(STANDBY_TIMEOUT / 2);
+        setInteractive();
+        verifyStandbyAlarmCancelled();
+
+        assertThat(mController.isActive()).isFalse();
+        verify(mPowerManagerInternalMock, never()).setLowPowerStandbyActive(anyBoolean());
+    }
+
+    private void verifyStandbyAlarmCancelled() {
+        InOrder inOrder = inOrder(mAlarmManagerMock);
+        inOrder.verify(mAlarmManagerMock, atLeast(0)).setExact(anyInt(), anyLong(), anyString(),
+                any(), any());
+        inOrder.verify(mAlarmManagerMock).cancel((AlarmManager.OnAlarmListener) any());
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
+    public void testOnInteractive_deactivate() throws Exception {
+        setLowPowerStandbySupportedConfig(true);
+        mController.systemReady();
+        mController.setEnabled(true);
+        setNonInteractive();
+        setDeviceIdleMode(true);
+        awaitStandbyTimeoutAlarm();
+
+        setInteractive();
+        mTestLooper.dispatchAll();
+
+        assertThat(mController.isActive()).isFalse();
+        verify(mPowerManagerInternalMock, times(1)).setLowPowerStandbyActive(false);
+    }
+
+    @Test
+    public void testOnDozeMaintenance_deactivate() throws Exception {
+        setLowPowerStandbySupportedConfig(true);
+        mController.systemReady();
+        mController.setEnabled(true);
+        setNonInteractive();
+        setDeviceIdleMode(true);
+        awaitStandbyTimeoutAlarm();
+
+        setDeviceIdleMode(false);
+        mTestLooper.dispatchAll();
+
+        assertThat(mController.isActive()).isFalse();
+        verify(mPowerManagerInternalMock, times(1)).setLowPowerStandbyActive(false);
+    }
+
+    @Test
+    public void testOnDozeMaintenanceEnds_activate() throws Exception {
+        setLowPowerStandbySupportedConfig(true);
+        mController.systemReady();
+        mController.setEnabled(true);
+        setNonInteractive();
+        setDeviceIdleMode(true);
+        awaitStandbyTimeoutAlarm();
+
+        setDeviceIdleMode(false);
+        advanceTime(1000);
+        setDeviceIdleMode(true);
+        mTestLooper.dispatchAll();
+
+        assertThat(mController.isActive()).isTrue();
+        verify(mPowerManagerInternalMock, times(2)).setLowPowerStandbyActive(true);
+    }
+
+    @Test
+    public void testLowPowerStandbyDisabled_doesNotActivate() throws Exception {
+        setLowPowerStandbySupportedConfig(true);
+        mController.systemReady();
+        mController.setEnabled(false);
+        setNonInteractive();
+
+        assertThat(mController.isActive()).isFalse();
+        verify(mAlarmManagerMock, never()).setExact(anyInt(), anyLong(), anyString(), any(), any());
+        verify(mPowerManagerInternalMock, never()).setLowPowerStandbyActive(anyBoolean());
+    }
+
+    @Test
+    public void testLowPowerStandbyEnabled_EnabledChangedBroadcastsAreSent() throws Exception {
+        setLowPowerStandbySupportedConfig(true);
+        mController.systemReady();
+
+        BroadcastInterceptingContext.FutureIntent futureIntent = mContextSpy.nextBroadcastIntent(
+                PowerManager.ACTION_LOW_POWER_STANDBY_ENABLED_CHANGED);
+        mController.setEnabled(false);
+        futureIntent.assertNotReceived();
+
+        futureIntent = mContextSpy.nextBroadcastIntent(
+                PowerManager.ACTION_LOW_POWER_STANDBY_ENABLED_CHANGED);
+        mController.setEnabled(true);
+        assertThat(futureIntent.get(1, TimeUnit.SECONDS)).isNotNull();
+
+        futureIntent = mContextSpy.nextBroadcastIntent(
+                PowerManager.ACTION_LOW_POWER_STANDBY_ENABLED_CHANGED);
+        mController.setEnabled(true);
+        futureIntent.assertNotReceived();
+
+        futureIntent = mContextSpy.nextBroadcastIntent(
+                PowerManager.ACTION_LOW_POWER_STANDBY_ENABLED_CHANGED);
+
+        mController.setEnabled(false);
+        assertThat(futureIntent.get(1, TimeUnit.SECONDS)).isNotNull();
+    }
+
+    @Test
+    public void testSetEnabled_WhenNotSupported_DoesNotEnable() throws Exception {
+        setLowPowerStandbySupportedConfig(false);
+        mController.systemReady();
+
+        mController.setEnabled(true);
+
+        assertThat(mController.isEnabled()).isFalse();
+    }
+
+    @Test
+    public void testIsSupported_WhenSupported() throws Exception {
+        setLowPowerStandbySupportedConfig(true);
+        mController.systemReady();
+
+        assertThat(mController.isSupported()).isTrue();
+    }
+
+    @Test
+    public void testIsSupported_WhenNotSupported() throws Exception {
+        setLowPowerStandbySupportedConfig(false);
+        mController.systemReady();
+
+        assertThat(mController.isSupported()).isFalse();
+    }
+
+    private void setLowPowerStandbySupportedConfig(boolean supported) {
+        when(mResourcesSpy.getBoolean(
+                com.android.internal.R.bool.config_lowPowerStandbySupported))
+                .thenReturn(supported);
+    }
+
+    private void setInteractive() throws Exception {
+        when(mIPowerManagerMock.isInteractive()).thenReturn(true);
+        mContextSpy.sendBroadcast(new Intent(Intent.ACTION_SCREEN_ON));
+    }
+
+    private void setNonInteractive() throws Exception {
+        when(mIPowerManagerMock.isInteractive()).thenReturn(false);
+        mContextSpy.sendBroadcast(new Intent(Intent.ACTION_SCREEN_OFF));
+    }
+
+    private void setDeviceIdleMode(boolean idle) throws Exception {
+        when(mIPowerManagerMock.isDeviceIdleMode()).thenReturn(idle);
+        mContextSpy.sendBroadcast(new Intent(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED));
+    }
+
+    private void advanceTime(long timeMs) {
+        mClock.fastForward(timeMs);
+        mTestLooper.dispatchAll();
+    }
+
+    /**
+     * Creates a mock and registers it to {@link LocalServices}.
+     */
+    private static <T> void addLocalServiceMock(Class<T> clazz, T mock) {
+        LocalServices.removeServiceForTest(clazz);
+        LocalServices.addService(clazz, mock);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
index d35c679..770796b 100644
--- a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.power;
 
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_TOP;
+import static android.app.ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
 import static android.os.PowerManagerInternal.WAKEFULNESS_ASLEEP;
 import static android.os.PowerManagerInternal.WAKEFULNESS_AWAKE;
 import static android.os.PowerManagerInternal.WAKEFULNESS_DOZING;
@@ -65,6 +67,7 @@
 import android.os.Looper;
 import android.os.PowerManager;
 import android.os.PowerSaveState;
+import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.test.TestLooper;
 import android.provider.Settings;
@@ -86,6 +89,7 @@
 import com.android.server.power.PowerManagerService.Injector;
 import com.android.server.power.PowerManagerService.NativeWrapper;
 import com.android.server.power.PowerManagerService.UserSwitchedReceiver;
+import com.android.server.power.PowerManagerService.WakeLock;
 import com.android.server.power.batterysaver.BatterySaverController;
 import com.android.server.power.batterysaver.BatterySaverPolicy;
 import com.android.server.power.batterysaver.BatterySaverStateMachine;
@@ -283,6 +287,13 @@
             void invalidateIsInteractiveCaches() {
                 // Avoids an SELinux failure.
             }
+
+            @Override
+            LowPowerStandbyController createLowPowerStandbyController(Context context,
+                    Looper looper) {
+                return new LowPowerStandbyController(context, mTestLooper.getLooper(),
+                        SystemClock::elapsedRealtime);
+            }
         });
         return mService;
     }
@@ -1575,4 +1586,47 @@
                 .setFullPowerSavePolicy(mockSetPolicyConfig)).isTrue();
         verify(mBatterySaverStateMachineMock).setFullBatterySaverPolicy(eq(mockSetPolicyConfig));
     }
+
+    @Test
+    public void testLowPowerStandby_whenInactive_FgsWakeLockEnabled() {
+        createService();
+        mService.systemReady();
+        WakeLock wakeLock = acquireWakeLock("fgsWakeLock", PowerManager.PARTIAL_WAKE_LOCK);
+        mService.updateUidProcStateInternal(wakeLock.mOwnerUid, PROCESS_STATE_FOREGROUND_SERVICE);
+        mService.setDeviceIdleModeInternal(true);
+
+        assertThat(wakeLock.mDisabled).isFalse();
+    }
+
+    @Test
+    public void testLowPowerStandby_whenActive_FgsWakeLockDisabled() {
+        createService();
+        mService.systemReady();
+        WakeLock wakeLock = acquireWakeLock("fgsWakeLock", PowerManager.PARTIAL_WAKE_LOCK);
+        mService.updateUidProcStateInternal(wakeLock.mOwnerUid, PROCESS_STATE_FOREGROUND_SERVICE);
+        mService.setDeviceIdleModeInternal(true);
+        mService.setLowPowerStandbyActiveInternal(true);
+
+        assertThat(wakeLock.mDisabled).isTrue();
+    }
+
+    @Test
+    public void testLowPowerStandby_whenActive_BoundTopWakeLockDisabled() {
+        createService();
+        mService.systemReady();
+        WakeLock wakeLock = acquireWakeLock("BoundTopWakeLock", PowerManager.PARTIAL_WAKE_LOCK);
+        mService.updateUidProcStateInternal(wakeLock.mOwnerUid, PROCESS_STATE_BOUND_TOP);
+        mService.setDeviceIdleModeInternal(true);
+        mService.setLowPowerStandbyActiveInternal(true);
+
+        assertThat(wakeLock.mDisabled).isFalse();
+    }
+
+    private WakeLock acquireWakeLock(String tag, int flags) {
+        IBinder token = new Binder();
+        String packageName = "pkg.name";
+        mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName,
+                null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY);
+        return mService.findWakeLockLocked(token);
+    }
 }