Refactor battery saver logic + add "per device" setting

- Extract the battery saver mode transition logic to BatterySaverController.

This now also supports running different code when screen turns on and off.

- BatterySaverPolicy now takes a "per-device configuration" from config.xml,
which can be overwritten via a global setting. We'll use this to set up
max CPU frequencies.

- The actual part to write max CPU frequencies is not finished yet.

Test: atest BatterySaverPolicyTest
Bug: 68769804
Change-Id: Ife38c2cd94ac9902911b005dbbca8b0d0a62e6d7
diff --git a/services/core/java/com/android/server/power/BatterySaverPolicy.java b/services/core/java/com/android/server/power/BatterySaverPolicy.java
index 0c73fe8..15121b8 100644
--- a/services/core/java/com/android/server/power/BatterySaverPolicy.java
+++ b/services/core/java/com/android/server/power/BatterySaverPolicy.java
@@ -15,25 +15,31 @@
  */
 package com.android.server.power;
 
-import android.annotation.IntDef;
 import android.content.ContentResolver;
+import android.content.Context;
 import android.database.ContentObserver;
 import android.net.Uri;
 import android.os.Handler;
-import android.os.PowerManager;
 import android.os.PowerManager.ServiceType;
+import android.os.PowerSaveState;
 import android.provider.Settings;
+import android.provider.Settings.Global;
+import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.KeyValueListParser;
 import android.util.Slog;
-import android.os.PowerSaveState;
+
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.R;
 
 import java.io.PrintWriter;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
 
 /**
  * Class to decide whether to turn on battery saver mode for specific service
+ *
+ * Test: atest BatterySaverPolicyTest
  */
 public class BatterySaverPolicy extends ContentObserver {
     private static final String TAG = "BatterySaverPolicy";
@@ -60,7 +66,12 @@
     private static final String KEY_FORCE_ALL_APPS_STANDBY_ALARMS = "force_all_apps_standby_alarms";
     private static final String KEY_OPTIONAL_SENSORS_DISABLED = "optional_sensors_disabled";
 
-    private final KeyValueListParser mParser = new KeyValueListParser(',');
+    private static final String KEY_SCREEN_ON_FILE_PREFIX = "file-on:";
+    private static final String KEY_SCREEN_OFF_FILE_PREFIX = "file-off:";
+
+    private static String mSettings;
+    private static String mDeviceSpecificSettings;
+    private static String mDeviceSpecificSettingsSource; // For dump() only.
 
     /**
      * {@code true} if vibration is disabled in battery saver mode.
@@ -159,55 +170,174 @@
      */
     private boolean mOptionalSensorsDisabled;
 
+    private final Object mLock = new Object();
+
+    @GuardedBy("mLock")
+    private Context mContext;
+
+    @GuardedBy("mLock")
     private ContentResolver mContentResolver;
 
+    @GuardedBy("mLock")
+    private final ArrayList<BatterySaverPolicyListener> mListeners = new ArrayList<>();
+
+    /**
+     * List of [Filename -> content] that should be written when battery saver is activated
+     * and the screen is on.
+     *
+     * We use this to change the max CPU frequencies.
+     */
+    @GuardedBy("mLock")
+    private ArrayMap<String, String> mScreenOnFiles;
+
+    /**
+     * List of [Filename -> content] that should be written when battery saver is activated
+     * and the screen is off.
+     *
+     * We use this to change the max CPU frequencies.
+     */
+    @GuardedBy("mLock")
+    private ArrayMap<String, String> mScreenOffFiles;
+
+    public interface BatterySaverPolicyListener {
+        void onBatterySaverPolicyChanged(BatterySaverPolicy policy);
+    }
+
     public BatterySaverPolicy(Handler handler) {
         super(handler);
     }
 
-    public void start(ContentResolver contentResolver) {
-        mContentResolver = contentResolver;
+    public void systemReady(Context context) {
+        synchronized (mLock) {
+            mContext = context;
+            mContentResolver = context.getContentResolver();
 
-        mContentResolver.registerContentObserver(Settings.Global.getUriFor(
-                Settings.Global.BATTERY_SAVER_CONSTANTS), false, this);
+            mContentResolver.registerContentObserver(Settings.Global.getUriFor(
+                    Settings.Global.BATTERY_SAVER_CONSTANTS), false, this);
+            mContentResolver.registerContentObserver(Settings.Global.getUriFor(
+                    Global.BATTERY_SAVER_DEVICE_SPECIFIC_CONSTANTS), false, this);
+        }
+        onChange(true, null);
+    }
+
+    public void addListener(BatterySaverPolicyListener listener) {
+        synchronized (mLock) {
+            mListeners.add(listener);
+        }
+    }
+
+    @VisibleForTesting
+    String getGlobalSetting(String key) {
+        return Settings.Global.getString(mContentResolver, key);
+    }
+
+    @VisibleForTesting
+    int getDeviceSpecificConfigResId() {
+        return R.string.config_batterySaverDeviceSpecificConfig;
+    }
+
+    @VisibleForTesting
+    void onChangeForTest() {
         onChange(true, null);
     }
 
     @Override
     public void onChange(boolean selfChange, Uri uri) {
-        final String value = Settings.Global.getString(mContentResolver,
-                Settings.Global.BATTERY_SAVER_CONSTANTS);
-        updateConstants(value);
+        final BatterySaverPolicyListener[] listeners;
+        synchronized (mLock) {
+            // Load the non-device-specific setting.
+            final String setting = getGlobalSetting(Settings.Global.BATTERY_SAVER_CONSTANTS);
+
+            // Load the device specific setting.
+            // We first check the global setting, and if it's empty or the string "null" is set,
+            // use the default value from config.xml.
+            String deviceSpecificSetting = getGlobalSetting(
+                    Settings.Global.BATTERY_SAVER_DEVICE_SPECIFIC_CONSTANTS);
+            mDeviceSpecificSettingsSource =
+                    Settings.Global.BATTERY_SAVER_DEVICE_SPECIFIC_CONSTANTS;
+
+            if (TextUtils.isEmpty(deviceSpecificSetting) || "null".equals(deviceSpecificSetting)) {
+                deviceSpecificSetting =
+                        mContext.getString(getDeviceSpecificConfigResId());
+                mDeviceSpecificSettingsSource = "(overlay)";
+            }
+
+            // Update.
+            updateConstantsLocked(setting, deviceSpecificSetting);
+
+            listeners = mListeners.toArray(new BatterySaverPolicyListener[mListeners.size()]);
+        }
+
+        // Notify the listeners.
+        for (BatterySaverPolicyListener listener : listeners) {
+            listener.onBatterySaverPolicyChanged(this);
+        }
     }
 
     @VisibleForTesting
-    void updateConstants(final String value) {
-        synchronized (BatterySaverPolicy.this) {
-            try {
-                mParser.setString(value);
-            } catch (IllegalArgumentException e) {
-                Slog.e(TAG, "Bad battery saver constants");
+    void updateConstantsLocked(final String setting, final String deviceSpecificSetting) {
+        mSettings = setting;
+        mDeviceSpecificSettings = deviceSpecificSetting;
+
+        final KeyValueListParser parser = new KeyValueListParser(',');
+
+        // Non-device-specific parameters.
+        try {
+            parser.setString(setting);
+        } catch (IllegalArgumentException e) {
+            Slog.wtf(TAG, "Bad battery saver constants: " + setting);
+        }
+
+        mVibrationDisabled = parser.getBoolean(KEY_VIBRATION_DISABLED, true);
+        mAnimationDisabled = parser.getBoolean(KEY_ANIMATION_DISABLED, true);
+        mSoundTriggerDisabled = parser.getBoolean(KEY_SOUNDTRIGGER_DISABLED, true);
+        mFullBackupDeferred = parser.getBoolean(KEY_FULLBACKUP_DEFERRED, true);
+        mKeyValueBackupDeferred = parser.getBoolean(KEY_KEYVALUE_DEFERRED, true);
+        mFireWallDisabled = parser.getBoolean(KEY_FIREWALL_DISABLED, false);
+        mAdjustBrightnessDisabled = parser.getBoolean(KEY_ADJUST_BRIGHTNESS_DISABLED, false);
+        mAdjustBrightnessFactor = parser.getFloat(KEY_ADJUST_BRIGHTNESS_FACTOR, 0.5f);
+        mDataSaverDisabled = parser.getBoolean(KEY_DATASAVER_DISABLED, true);
+        mForceAllAppsStandbyJobs = parser.getBoolean(KEY_FORCE_ALL_APPS_STANDBY_JOBS, true);
+        mForceAllAppsStandbyAlarms =
+                parser.getBoolean(KEY_FORCE_ALL_APPS_STANDBY_ALARMS, true);
+        mOptionalSensorsDisabled = parser.getBoolean(KEY_OPTIONAL_SENSORS_DISABLED, true);
+
+        // Get default value from Settings.Secure
+        final int defaultGpsMode = Settings.Secure.getInt(mContentResolver, SECURE_KEY_GPS_MODE,
+                GPS_MODE_NO_CHANGE);
+        mGpsMode = parser.getInt(KEY_GPS_MODE, defaultGpsMode);
+
+        // Non-device-specific parameters.
+        try {
+            parser.setString(deviceSpecificSetting);
+        } catch (IllegalArgumentException e) {
+            Slog.wtf(TAG, "Bad device specific battery saver constants: "
+                    + deviceSpecificSetting);
+        }
+
+        mScreenOnFiles = collectParams(parser, KEY_SCREEN_ON_FILE_PREFIX);
+        mScreenOffFiles = collectParams(parser, KEY_SCREEN_OFF_FILE_PREFIX);
+    }
+
+    private static ArrayMap<String, String> collectParams(
+            KeyValueListParser parser, String prefix) {
+        final ArrayMap<String, String> ret = new ArrayMap<>();
+
+        for (int i = parser.size() - 1; i >= 0; i--) {
+            final String key = parser.keyAt(i);
+            if (!key.startsWith(prefix)) {
+                continue;
+            }
+            final String path = key.substring(prefix.length());
+
+            if (!(path.startsWith("/sys/") || path.startsWith("/proc"))) {
+                Slog.wtf(TAG, "Invalid path: " + path);
+                continue;
             }
 
-            mVibrationDisabled = mParser.getBoolean(KEY_VIBRATION_DISABLED, true);
-            mAnimationDisabled = mParser.getBoolean(KEY_ANIMATION_DISABLED, true);
-            mSoundTriggerDisabled = mParser.getBoolean(KEY_SOUNDTRIGGER_DISABLED, true);
-            mFullBackupDeferred = mParser.getBoolean(KEY_FULLBACKUP_DEFERRED, true);
-            mKeyValueBackupDeferred = mParser.getBoolean(KEY_KEYVALUE_DEFERRED, true);
-            mFireWallDisabled = mParser.getBoolean(KEY_FIREWALL_DISABLED, false);
-            mAdjustBrightnessDisabled = mParser.getBoolean(KEY_ADJUST_BRIGHTNESS_DISABLED, false);
-            mAdjustBrightnessFactor = mParser.getFloat(KEY_ADJUST_BRIGHTNESS_FACTOR, 0.5f);
-            mDataSaverDisabled = mParser.getBoolean(KEY_DATASAVER_DISABLED, true);
-            mForceAllAppsStandbyJobs = mParser.getBoolean(KEY_FORCE_ALL_APPS_STANDBY_JOBS, true);
-            mForceAllAppsStandbyAlarms =
-                    mParser.getBoolean(KEY_FORCE_ALL_APPS_STANDBY_ALARMS, true);
-            mOptionalSensorsDisabled = mParser.getBoolean(KEY_OPTIONAL_SENSORS_DISABLED, true);
-
-            // Get default value from Settings.Secure
-            final int defaultGpsMode = Settings.Secure.getInt(mContentResolver, SECURE_KEY_GPS_MODE,
-                    GPS_MODE_NO_CHANGE);
-            mGpsMode = mParser.getInt(KEY_GPS_MODE, defaultGpsMode);
+            ret.put(path, parser.getString(key, ""));
         }
+        return ret;
     }
 
     /**
@@ -220,7 +350,7 @@
      * @return State data that contains battery saver data
      */
     public PowerSaveState getBatterySaverPolicy(@ServiceType int type, boolean realMode) {
-        synchronized (BatterySaverPolicy.this) {
+        synchronized (mLock) {
             final PowerSaveState.Builder builder = new PowerSaveState.Builder()
                     .setGlobalBatterySaverEnabled(realMode);
             if (!realMode) {
@@ -273,25 +403,57 @@
         }
     }
 
-    public void dump(PrintWriter pw) {
-        pw.println();
-        pw.println("Battery saver policy");
-        pw.println("  Settings " + Settings.Global.BATTERY_SAVER_CONSTANTS);
-        pw.println("  value: " + Settings.Global.getString(mContentResolver,
-                Settings.Global.BATTERY_SAVER_CONSTANTS));
+    public ArrayMap<String, String> getFileValues(boolean screenOn) {
+        synchronized (mLock) {
+            return screenOn ? mScreenOnFiles : mScreenOffFiles;
+        }
+    }
 
-        pw.println();
-        pw.println("  " + KEY_VIBRATION_DISABLED + "=" + mVibrationDisabled);
-        pw.println("  " + KEY_ANIMATION_DISABLED + "=" + mAnimationDisabled);
-        pw.println("  " + KEY_FULLBACKUP_DEFERRED + "=" + mFullBackupDeferred);
-        pw.println("  " + KEY_KEYVALUE_DEFERRED + "=" + mKeyValueBackupDeferred);
-        pw.println("  " + KEY_FIREWALL_DISABLED + "=" + mFireWallDisabled);
-        pw.println("  " + KEY_DATASAVER_DISABLED + "=" + mDataSaverDisabled);
-        pw.println("  " + KEY_ADJUST_BRIGHTNESS_DISABLED + "=" + mAdjustBrightnessDisabled);
-        pw.println("  " + KEY_ADJUST_BRIGHTNESS_FACTOR + "=" + mAdjustBrightnessFactor);
-        pw.println("  " + KEY_GPS_MODE + "=" + mGpsMode);
-        pw.println("  " + KEY_FORCE_ALL_APPS_STANDBY_JOBS + "=" + mForceAllAppsStandbyJobs);
-        pw.println("  " + KEY_FORCE_ALL_APPS_STANDBY_ALARMS + "=" + mForceAllAppsStandbyAlarms);
-        pw.println("  " + KEY_OPTIONAL_SENSORS_DISABLED + "=" + mOptionalSensorsDisabled);
+    public void dump(PrintWriter pw) {
+        synchronized (mLock) {
+            pw.println();
+            pw.println("Battery saver policy");
+            pw.println("  Settings " + Settings.Global.BATTERY_SAVER_CONSTANTS);
+            pw.println("  value: " + mSettings);
+            pw.println("  Settings " + mDeviceSpecificSettingsSource);
+            pw.println("  value: " + mDeviceSpecificSettings);
+
+            pw.println();
+            pw.println("  " + KEY_VIBRATION_DISABLED + "=" + mVibrationDisabled);
+            pw.println("  " + KEY_ANIMATION_DISABLED + "=" + mAnimationDisabled);
+            pw.println("  " + KEY_FULLBACKUP_DEFERRED + "=" + mFullBackupDeferred);
+            pw.println("  " + KEY_KEYVALUE_DEFERRED + "=" + mKeyValueBackupDeferred);
+            pw.println("  " + KEY_FIREWALL_DISABLED + "=" + mFireWallDisabled);
+            pw.println("  " + KEY_DATASAVER_DISABLED + "=" + mDataSaverDisabled);
+            pw.println("  " + KEY_ADJUST_BRIGHTNESS_DISABLED + "=" + mAdjustBrightnessDisabled);
+            pw.println("  " + KEY_ADJUST_BRIGHTNESS_FACTOR + "=" + mAdjustBrightnessFactor);
+            pw.println("  " + KEY_GPS_MODE + "=" + mGpsMode);
+            pw.println("  " + KEY_FORCE_ALL_APPS_STANDBY_JOBS + "=" + mForceAllAppsStandbyJobs);
+            pw.println("  " + KEY_FORCE_ALL_APPS_STANDBY_ALARMS + "=" + mForceAllAppsStandbyAlarms);
+            pw.println("  " + KEY_OPTIONAL_SENSORS_DISABLED + "=" + mOptionalSensorsDisabled);
+            pw.println();
+
+            pw.print("  Screen On Files:\n");
+            dumpMap(pw, "    ", mScreenOnFiles);
+            pw.println();
+
+            pw.print("  Screen Off Files:\n");
+            dumpMap(pw, "    ", mScreenOffFiles);
+            pw.println();
+        }
+    }
+
+    private void dumpMap(PrintWriter pw, String prefix, ArrayMap<String, String> map) {
+        if (map == null) {
+            return;
+        }
+        final int size = map.size();
+        for (int i = 0; i < size; i++) {
+            pw.print(prefix);
+            pw.print(map.keyAt(i));
+            pw.print(": '");
+            pw.print(map.valueAt(i));
+            pw.println("'");
+        }
     }
 }
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index a153fdf..a47b809 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -63,7 +63,6 @@
 import android.service.vr.IVrStateCallbacks;
 import android.util.EventLog;
 import android.util.KeyValueListParser;
-import android.util.Log;
 import android.util.PrintWriterPrinter;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -71,7 +70,6 @@
 import android.util.proto.ProtoOutputStream;
 import android.view.Display;
 import android.view.WindowManagerPolicy;
-import android.widget.Toast;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IAppOpsService;
@@ -92,10 +90,8 @@
 import com.android.server.am.BatteryStatsService;
 import com.android.server.lights.Light;
 import com.android.server.lights.LightsManager;
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileReader;
-import java.io.IOException;
+import com.android.server.power.batterysaver.BatterySaverController;
+
 import libcore.util.Objects;
 
 import java.io.FileDescriptor;
@@ -228,6 +224,7 @@
     private final PowerManagerHandler mHandler;
     private final AmbientDisplayConfiguration mAmbientDisplayConfiguration;
     private final BatterySaverPolicy mBatterySaverPolicy;
+    private final BatterySaverController mBatterySaverController;
 
     private LightsManager mLightsManager;
     private BatteryManagerInternal mBatteryManagerInternal;
@@ -555,9 +552,6 @@
     // True if double tap to wake is enabled
     private boolean mDoubleTapWakeEnabled;
 
-    private final ArrayList<PowerManagerInternal.LowPowerModeListener> mLowPowerModeListeners
-            = new ArrayList<PowerManagerInternal.LowPowerModeListener>();
-
     // True if we are currently in VR Mode.
     private boolean mIsVrModeEnabled;
 
@@ -645,7 +639,10 @@
         mHandler = new PowerManagerHandler(mHandlerThread.getLooper());
         mConstants = new Constants(mHandler);
         mAmbientDisplayConfiguration = new AmbientDisplayConfiguration(mContext);
+
         mBatterySaverPolicy = new BatterySaverPolicy(mHandler);
+        mBatterySaverController = new BatterySaverController(mContext,
+                BackgroundThread.get().getLooper(), mBatterySaverPolicy);
 
         synchronized (mLock) {
             mWakeLockSuspendBlocker = createSuspendBlockerLocked("PowerManagerService.WakeLocks");
@@ -670,7 +667,6 @@
     PowerManagerService(Context context, BatterySaverPolicy batterySaverPolicy) {
         super(context);
 
-        mBatterySaverPolicy = batterySaverPolicy;
         mContext = context;
         mHandlerThread = new ServiceThread(TAG,
                 Process.THREAD_PRIORITY_DISPLAY, false /*allowIo*/);
@@ -680,6 +676,10 @@
         mAmbientDisplayConfiguration = new AmbientDisplayConfiguration(mContext);
         mDisplaySuspendBlocker = null;
         mWakeLockSuspendBlocker = null;
+
+        mBatterySaverPolicy = batterySaverPolicy;
+        mBatterySaverController = new BatterySaverController(context,
+                BackgroundThread.getHandler().getLooper(), batterySaverPolicy);
     }
 
     @Override
@@ -752,6 +752,7 @@
             mDisplayManagerInternal.initPowerManagement(
                     mDisplayPowerCallbacks, mHandler, sensorManager);
 
+
             // Go.
             readConfigurationLocked();
             updateSettingsLocked();
@@ -761,7 +762,9 @@
 
         final ContentResolver resolver = mContext.getContentResolver();
         mConstants.start(resolver);
-        mBatterySaverPolicy.start(resolver);
+
+        mBatterySaverController.systemReady();
+        mBatterySaverPolicy.systemReady(mContext);
 
         // Register for settings changes.
         resolver.registerContentObserver(Settings.Secure.getUriFor(
@@ -996,43 +999,9 @@
 
         if (mLowPowerModeEnabled != lowPowerModeEnabled) {
             mLowPowerModeEnabled = lowPowerModeEnabled;
-            powerHintInternal(PowerHint.LOW_POWER, lowPowerModeEnabled ? 1 : 0);
-            postAfterBootCompleted(new Runnable() {
-                @Override
-                public void run() {
-                    Intent intent = new Intent(PowerManager.ACTION_POWER_SAVE_MODE_CHANGING)
-                            .putExtra(PowerManager.EXTRA_POWER_SAVE_MODE, mLowPowerModeEnabled)
-                            .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
-                    mContext.sendBroadcast(intent);
-                    ArrayList<PowerManagerInternal.LowPowerModeListener> listeners;
-                    synchronized (mLock) {
-                        listeners = new ArrayList<PowerManagerInternal.LowPowerModeListener>(
-                                mLowPowerModeListeners);
-                    }
-                    for (int i = 0; i < listeners.size(); i++) {
-                        final PowerManagerInternal.LowPowerModeListener listener = listeners.get(i);
-                        final PowerSaveState result =
-                                mBatterySaverPolicy.getBatterySaverPolicy(
-                                        listener.getServiceType(), lowPowerModeEnabled);
-                        listener.onLowPowerModeChanged(result);
-                    }
-                    intent = new Intent(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED);
-                    intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
-                    mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
-                    // Send internal version that requires signature permission.
-                    intent = new Intent(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED_INTERNAL);
-                    intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
-                    mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
-                            Manifest.permission.DEVICE_POWER);
 
-                    // STOPSHIP Remove the toast.
-                    if (mLowPowerModeEnabled) {
-                        Toast.makeText(mContext,
-                                com.android.internal.R.string.battery_saver_warning,
-                                Toast.LENGTH_LONG).show();
-                    }
-                }
-            });
+            postAfterBootCompleted(() ->
+                    mBatterySaverController.enableBatterySaver(mLowPowerModeEnabled));
         }
     }
 
@@ -3136,7 +3105,7 @@
         mIsVrModeEnabled = enabled;
     }
 
-    private void powerHintInternal(int hintId, int data) {
+    public static void powerHintInternal(int hintId, int data) {
         nativeSendPowerHint(hintId, data);
     }
 
@@ -4405,7 +4374,7 @@
          * Gets the reason for the last time the phone had to reboot.
          *
          * @return The reason the phone last shut down as an int or
-         * {@link PowerManager.SHUTDOWN_REASON_UNKNOWN} if the file could not be opened.
+         * {@link PowerManager#SHUTDOWN_REASON_UNKNOWN} if the file could not be opened.
          */
         @Override // Binder call
         public int getLastShutdownReason() {
@@ -4728,9 +4697,7 @@
 
         @Override
         public void registerLowPowerModeObserver(LowPowerModeListener listener) {
-            synchronized (mLock) {
-                mLowPowerModeListeners.add(listener);
-            }
+            mBatterySaverController.addListener(listener);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java b/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java
new file mode 100644
index 0000000..b3e8538
--- /dev/null
+++ b/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2017 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.batterysaver;
+
+import android.Manifest;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.power.V1_0.PowerHint;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.PowerManagerInternal.LowPowerModeListener;
+import android.os.PowerSaveState;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.Slog;
+import android.widget.Toast;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+import com.android.server.power.BatterySaverPolicy;
+import com.android.server.power.BatterySaverPolicy.BatterySaverPolicyListener;
+import com.android.server.power.PowerManagerService;
+
+import java.util.ArrayList;
+
+/**
+ * Responsible for battery saver mode transition logic.
+ */
+public class BatterySaverController implements BatterySaverPolicyListener {
+    static final String TAG = "BatterySaverController";
+
+    static final boolean DEBUG = false; // DO NOT MERGE WITH TRUE
+
+    private final Object mLock = new Object();
+    private final Context mContext;
+    private final MyHandler mHandler;
+    private final FileUpdater mFileUpdater;
+
+    private PowerManager mPowerManager;
+
+    private final BatterySaverPolicy mBatterySaverPolicy;
+
+    @GuardedBy("mLock")
+    private final ArrayList<LowPowerModeListener> mListeners = new ArrayList<>();
+
+    @GuardedBy("mLock")
+    private boolean mEnabled;
+
+    /**
+     * Keep track of the previous enabled state, which we use to decide when to send broadcasts,
+     * which we don't want to send only when the screen state changes.
+     */
+    @GuardedBy("mLock")
+    private boolean mWasEnabled;
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            switch (intent.getAction()) {
+                case Intent.ACTION_SCREEN_ON:
+                case Intent.ACTION_SCREEN_OFF:
+                    mHandler.postStateChanged();
+                    break;
+            }
+        }
+    };
+
+    /**
+     * Constructor.
+     */
+    public BatterySaverController(Context context, Looper looper, BatterySaverPolicy policy) {
+        mContext = context;
+        mHandler = new MyHandler(looper);
+        mBatterySaverPolicy = policy;
+        mBatterySaverPolicy.addListener(this);
+        mFileUpdater = new FileUpdater(context);
+    }
+
+    /**
+     * Add a listener.
+     */
+    public void addListener(LowPowerModeListener listener) {
+        synchronized (mLock) {
+            mListeners.add(listener);
+        }
+    }
+
+    /**
+     * Called by {@link PowerManagerService} on system ready..
+     */
+    public void systemReady() {
+        final IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON);
+        filter.addAction(Intent.ACTION_SCREEN_OFF);
+        mContext.registerReceiver(mReceiver, filter);
+    }
+
+    private PowerManager getPowerManager() {
+        if (mPowerManager == null) {
+            mPowerManager =
+                    Preconditions.checkNotNull(mContext.getSystemService(PowerManager.class));
+        }
+        return mPowerManager;
+    }
+
+    @Override
+    public void onBatterySaverPolicyChanged(BatterySaverPolicy policy) {
+        mHandler.postStateChanged();
+    }
+
+    private class MyHandler extends Handler {
+        private final int MSG_STATE_CHANGED = 1;
+
+        public MyHandler(Looper looper) {
+            super(looper);
+        }
+
+        public void postStateChanged() {
+            obtainMessage(MSG_STATE_CHANGED).sendToTarget();
+        }
+
+        @Override
+        public void dispatchMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_STATE_CHANGED:
+                    handleBatterySaverStateChanged();
+                    break;
+            }
+        }
+    }
+
+    /**
+     * Called by {@link PowerManagerService} to update the battery saver stete.
+     */
+    public void enableBatterySaver(boolean enable) {
+        synchronized (mLock) {
+            if (mEnabled == enable) {
+                return;
+            }
+            mEnabled = enable;
+
+            mHandler.postStateChanged();
+        }
+    }
+
+    /**
+     * Dispatch power save events to the listeners.
+     *
+     * This is always called on the handler thread.
+     */
+    void handleBatterySaverStateChanged() {
+        final LowPowerModeListener[] listeners;
+
+        final boolean wasEnabled;
+        final boolean enabled;
+        final boolean isScreenOn = getPowerManager().isInteractive();
+        final ArrayMap<String, String> fileValues;
+
+        synchronized (mLock) {
+            Slog.i(TAG, "Battery saver enabled: screen on=" + isScreenOn);
+
+            listeners = mListeners.toArray(new LowPowerModeListener[mListeners.size()]);
+            wasEnabled = mWasEnabled;
+            enabled = mEnabled;
+
+            if (enabled) {
+                fileValues = mBatterySaverPolicy.getFileValues(isScreenOn);
+            } else {
+                fileValues = null;
+            }
+        }
+
+        PowerManagerService.powerHintInternal(PowerHint.LOW_POWER, enabled ? 1 : 0);
+
+        if (enabled) {
+            // STOPSHIP Remove the toast.
+            Toast.makeText(mContext,
+                    com.android.internal.R.string.battery_saver_warning,
+                    Toast.LENGTH_LONG).show();
+        }
+
+        if (fileValues == null || fileValues.size() == 0) {
+            mFileUpdater.restoreDefault();
+        } else {
+            mFileUpdater.writeFiles(fileValues);
+        }
+
+        if (enabled != wasEnabled) {
+            if (DEBUG) {
+                Slog.i(TAG, "Sending broadcasts for mode: " + enabled);
+            }
+
+            // Send the broadcasts and notify the listeners. We only do this when the battery saver
+            // mode changes, but not when only the screen state changes.
+            Intent intent = new Intent(PowerManager.ACTION_POWER_SAVE_MODE_CHANGING)
+                    .putExtra(PowerManager.EXTRA_POWER_SAVE_MODE, enabled)
+                    .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+            mContext.sendBroadcast(intent);
+
+            intent = new Intent(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED);
+            intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+            mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
+
+            // Send internal version that requires signature permission.
+            intent = new Intent(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED_INTERNAL);
+            intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+            mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
+                    Manifest.permission.DEVICE_POWER);
+
+
+            for (LowPowerModeListener listener : listeners) {
+                final PowerSaveState result =
+                        mBatterySaverPolicy.getBatterySaverPolicy(
+                                listener.getServiceType(), enabled);
+                listener.onLowPowerModeChanged(result);
+            }
+        }
+
+        synchronized (mLock) {
+            mWasEnabled = enabled;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/power/batterysaver/FileUpdater.java b/services/core/java/com/android/server/power/batterysaver/FileUpdater.java
new file mode 100644
index 0000000..cfe8fc4
--- /dev/null
+++ b/services/core/java/com/android/server/power/batterysaver/FileUpdater.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2017 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.batterysaver;
+
+import android.content.Context;
+import android.util.ArrayMap;
+import android.util.Slog;
+
+/**
+ * Used by {@link BatterySaverController} to write values to /sys/ (and possibly /proc/ too) files
+ * with retry and to restore the original values.
+ *
+ * TODO Implement it
+ */
+public class FileUpdater {
+    private static final String TAG = BatterySaverController.TAG;
+
+    private static final boolean DEBUG = BatterySaverController.DEBUG;
+
+    private final Object mLock = new Object();
+    private final Context mContext;
+
+    public FileUpdater(Context context) {
+        mContext = context;
+    }
+
+    public void writeFiles(ArrayMap<String, String> fileValues) {
+        if (DEBUG) {
+            final int size = fileValues.size();
+            for (int i = 0; i < size; i++) {
+                Slog.d(TAG, "Writing '" + fileValues.valueAt(i)
+                        + "' to '" + fileValues.keyAt(i) + "'");
+            }
+        }
+    }
+
+    public void restoreDefault() {
+        if (DEBUG) {
+            Slog.d(TAG, "Resetting file default values");
+        }
+    }
+}
diff --git a/services/tests/servicestests/res/values/strings.xml b/services/tests/servicestests/res/values/strings.xml
index 1253d44..3ac56bb 100644
--- a/services/tests/servicestests/res/values/strings.xml
+++ b/services/tests/servicestests/res/values/strings.xml
@@ -28,4 +28,8 @@
     <string name="test_account_type2_authenticator_label">AccountManagerService Test Account Type2</string>
     <string name="test_account_type1">com.android.server.accounts.account_manager_service_test.account.type1</string>
     <string name="test_account_type2">com.android.server.accounts.account_manager_service_test.account.type2</string>
+
+    <string name="config_batterySaverDeviceSpecificConfig_1"></string>
+    <string name="config_batterySaverDeviceSpecificConfig_2">file-off:/sys/a=1,file-off:/sys/b=2</string>
+    <string name="config_batterySaverDeviceSpecificConfig_3">file-off:/sys/a=3,file-on:/proc/c=4,/abc=3</string>
 </resources>
diff --git a/services/tests/servicestests/src/com/android/server/power/BatterySaverPolicyTest.java b/services/tests/servicestests/src/com/android/server/power/BatterySaverPolicyTest.java
index 50ac41c..5b6225e7 100644
--- a/services/tests/servicestests/src/com/android/server/power/BatterySaverPolicyTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/BatterySaverPolicyTest.java
@@ -18,8 +18,13 @@
 import android.os.PowerManager.ServiceType;
 import android.os.PowerSaveState;
 import android.os.Handler;
+import android.provider.Settings;
+import android.provider.Settings.Global;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
+import android.util.ArrayMap;
+
+import com.android.frameworks.servicestests.R;
 
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
@@ -36,7 +41,7 @@
     private static final float DEFAULT_BRIGHTNESS_FACTOR = 0.5f;
     private static final float PRECISION = 0.001f;
     private static final int GPS_MODE = 0;
-    private static final int DEFAULT_GPS_MODE = 1;
+    private static final int DEFAULT_GPS_MODE = 0;
     private static final String BATTERY_SAVER_CONSTANTS = "vibration_disabled=true,"
             + "animation_disabled=false,"
             + "soundtrigger_disabled=true,"
@@ -49,15 +54,34 @@
             + "gps_mode=0";
     private static final String BATTERY_SAVER_INCORRECT_CONSTANTS = "vi*,!=,,true";
 
+    private class BatterySaverPolicyForTest extends BatterySaverPolicy {
+        public BatterySaverPolicyForTest(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        String getGlobalSetting(String key) {
+            return mMockGlobalSettings.get(key);
+        }
+
+        @Override
+        int getDeviceSpecificConfigResId() {
+            return mDeviceSpecificConfigResId;
+        }
+    }
+
     @Mock
     Handler mHandler;
-    private BatterySaverPolicy mBatterySaverPolicy;
+    private BatterySaverPolicyForTest mBatterySaverPolicy;
+
+    private final ArrayMap<String, String> mMockGlobalSettings = new ArrayMap<>();
+    private int mDeviceSpecificConfigResId = R.string.config_batterySaverDeviceSpecificConfig_1;
 
     public void setUp() throws Exception {
         super.setUp();
         MockitoAnnotations.initMocks(this);
-        mBatterySaverPolicy = new BatterySaverPolicy(mHandler);
-        mBatterySaverPolicy.start(getContext().getContentResolver());
+        mBatterySaverPolicy = new BatterySaverPolicyForTest(mHandler);
+        mBatterySaverPolicy.systemReady(getContext());
     }
 
     @SmallTest
@@ -102,7 +126,7 @@
 
     @SmallTest
     public void testGetBatterySaverPolicy_PolicyDataSaver_DefaultValueCorrect() {
-        mBatterySaverPolicy.updateConstants("");
+        mBatterySaverPolicy.updateConstantsLocked("", "");
         final PowerSaveState batterySaverStateOn =
                 mBatterySaverPolicy.getBatterySaverPolicy(ServiceType.DATA_SAVER, BATTERY_SAVER_ON);
         assertThat(batterySaverStateOn.batterySaverEnabled).isFalse();
@@ -132,7 +156,7 @@
 
     @SmallTest
     public void testUpdateConstants_getCorrectData() {
-        mBatterySaverPolicy.updateConstants(BATTERY_SAVER_CONSTANTS);
+        mBatterySaverPolicy.updateConstantsLocked(BATTERY_SAVER_CONSTANTS, "");
 
         final PowerSaveState vibrationState =
                 mBatterySaverPolicy.getBatterySaverPolicy(ServiceType.VIBRATION, BATTERY_SAVER_ON);
@@ -177,12 +201,12 @@
     @SmallTest
     public void testUpdateConstants_IncorrectData_NotCrash() {
         //Should not crash
-        mBatterySaverPolicy.updateConstants(BATTERY_SAVER_INCORRECT_CONSTANTS);
-        mBatterySaverPolicy.updateConstants(null);
+        mBatterySaverPolicy.updateConstantsLocked(BATTERY_SAVER_INCORRECT_CONSTANTS, "");
+        mBatterySaverPolicy.updateConstantsLocked(null, "");
     }
 
     private void testServiceDefaultValue(@ServiceType int type) {
-        mBatterySaverPolicy.updateConstants("");
+        mBatterySaverPolicy.updateConstantsLocked("", "");
         final PowerSaveState batterySaverStateOn =
                 mBatterySaverPolicy.getBatterySaverPolicy(type, BATTERY_SAVER_ON);
         assertThat(batterySaverStateOn.batterySaverEnabled).isTrue();
@@ -191,4 +215,37 @@
                 mBatterySaverPolicy.getBatterySaverPolicy(type, BATTERY_SAVER_OFF);
         assertThat(batterySaverStateOff.batterySaverEnabled).isFalse();
     }
+
+    public void testDeviceSpecific() {
+        mDeviceSpecificConfigResId = R.string.config_batterySaverDeviceSpecificConfig_1;
+        mMockGlobalSettings.put(Global.BATTERY_SAVER_CONSTANTS, "");
+        mMockGlobalSettings.put(Global.BATTERY_SAVER_DEVICE_SPECIFIC_CONSTANTS, "");
+
+        mBatterySaverPolicy.onChangeForTest();
+        assertThat(mBatterySaverPolicy.getFileValues(true).toString()).isEqualTo("{}");
+        assertThat(mBatterySaverPolicy.getFileValues(false).toString()).isEqualTo("{}");
+
+
+        mDeviceSpecificConfigResId = R.string.config_batterySaverDeviceSpecificConfig_2;
+
+        mBatterySaverPolicy.onChangeForTest();
+        assertThat(mBatterySaverPolicy.getFileValues(true).toString()).isEqualTo("{}");
+        assertThat(mBatterySaverPolicy.getFileValues(false).toString())
+                .isEqualTo("{/sys/a=1, /sys/b=2}");
+
+
+        mDeviceSpecificConfigResId = R.string.config_batterySaverDeviceSpecificConfig_3;
+
+        mBatterySaverPolicy.onChangeForTest();
+        assertThat(mBatterySaverPolicy.getFileValues(true).toString()).isEqualTo("{/proc/c=4}");
+        assertThat(mBatterySaverPolicy.getFileValues(false).toString()).isEqualTo("{/sys/a=3}");
+
+
+        mMockGlobalSettings.put(Global.BATTERY_SAVER_DEVICE_SPECIFIC_CONSTANTS,
+                "file-on:/proc/z=4");
+
+        mBatterySaverPolicy.onChangeForTest();
+        assertThat(mBatterySaverPolicy.getFileValues(true).toString()).isEqualTo("{/proc/z=4}");
+        assertThat(mBatterySaverPolicy.getFileValues(false).toString()).isEqualTo("{}");
+    }
 }
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 b60d5bf..5039e42 100644
--- a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
@@ -16,32 +16,28 @@
 
 package com.android.server.power;
 
-import android.content.Context;
+import static android.os.PowerManagerInternal.WAKEFULNESS_ASLEEP;
+import static android.os.PowerManagerInternal.WAKEFULNESS_AWAKE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.when;
+
 import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
 import android.os.PowerManager;
 import android.os.PowerSaveState;
 import android.os.SystemProperties;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
-import android.text.TextUtils;
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
+
+import com.android.server.power.batterysaver.BatterySaverController;
+
 import org.junit.Rule;
-import org.junit.rules.TemporaryFolder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import static android.os.PowerManagerInternal.WAKEFULNESS_ASLEEP;
-import static android.os.PowerManagerInternal.WAKEFULNESS_AWAKE;
-import static android.os.PowerManagerInternal.WAKEFULNESS_DOZING;
-import static android.os.PowerManagerInternal.WAKEFULNESS_DREAMING;
-import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.Matchers.anyBoolean;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.when;
-
 /**
  * Tests for {@link com.android.server.power.PowerManagerService}
  */