diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index b9aad113c6777..8cb8b0ed0ad46 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7761,6 +7761,22 @@ public final class Settings { */ public static final String BLUETOOTH_ON_WHILE_DRIVING = "bluetooth_on_while_driving"; + /** + * The number of times (integer) the user has manually enabled battery saver. + * @hide + */ + public static final String LOW_POWER_MANUAL_ACTIVATION_COUNT = + "low_power_manual_activation_count"; + + /** + * Whether the "first time battery saver warning" dialog needs to be shown (0: default) + * or not (1). + * + * @hide + */ + public static final String LOW_POWER_WARNING_ACKNOWLEDGED = + "low_power_warning_acknowledged"; + /** * This are the settings to be backed up. * diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index aac4092db08fa..1b110701cce02 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -4475,7 +4475,7 @@ Deleted by your admin - To help improve battery life, Battery Saver reduces your device’s performance and limits vibration, location services, and most background data. Email, messaging, and other apps that rely on syncing may not update unless you open them.\n\nBattery Saver turns off automatically when your device is charging. + To extend battery life, Battery Saver reduces your device\'s performance and limits or turns off vibration, location services, and background data. Email, messaging, and other apps that rely on syncing may not update unless you open them.\n\nBattery Saver turns off automatically when your device is charging. To help reduce data usage, Data Saver prevents some apps from sending or receiving data in the background. An app you’re currently using can access data, but may do so less frequently. This may mean, for example, that images don’t display until you tap them. diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java index 2036e2f95225b..deafbd3271efb 100644 --- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java +++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java @@ -581,7 +581,9 @@ public class SettingsBackupTest { Settings.Secure.KEYGUARD_SLICE_URI, Settings.Secure.PARENTAL_CONTROL_ENABLED, Settings.Secure.PARENTAL_CONTROL_REDIRECT_URL, - Settings.Secure.BLUETOOTH_ON_WHILE_DRIVING); + Settings.Secure.BLUETOOTH_ON_WHILE_DRIVING, + Settings.Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, + Settings.Secure.LOW_POWER_WARNING_ACKNOWLEDGED); @Test public void systemSettingsBackedUpOrBlacklisted() { diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java new file mode 100644 index 0000000000000..e2c7747e3231c --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018 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.settingslib.fuelgauge; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.os.PowerManager; +import android.provider.Settings.Secure; +import android.util.Log; + +/** + * Utilities related to battery saver. + */ +public class BatterySaverUtils { + private static final String TAG = "BatterySaverUtils"; + + private BatterySaverUtils() { + } + + private static final boolean DEBUG = false; + + // Broadcast action for SystemUI to show the battery saver confirmation dialog. + public static final String ACTION_SHOW_START_SAVER_CONFIRMATION = "PNW.startSaverConfirmation"; + + /** + * Enable / disable battery saver by user request. + * - If it's the first time and needFirstTimeWarning, show the first time dialog. + * - If it's 4th time through 8th time, show the schedule suggestion notification. + * + * @param enable true to disable battery saver. + * + * @return true if the request succeeded. + */ + public static synchronized boolean setPowerSaveMode(Context context, + boolean enable, boolean needFirstTimeWarning) { + if (DEBUG) { + Log.d(TAG, "Battery saver turning " + (enable ? "ON" : "OFF")); + } + final ContentResolver cr = context.getContentResolver(); + + if (enable && needFirstTimeWarning && maybeShowBatterySaverConfirmation(context)) { + return false; + } + if (enable && !needFirstTimeWarning) { + setBatterySaverConfirmationAcknowledged(context); + } + + if (context.getSystemService(PowerManager.class).setPowerSaveMode(enable)) { + if (enable) { + Secure.putInt(cr, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, + Secure.getInt(cr, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, 0) + 1); + + // TODO If enabling, and the count is between 4 and 8 (inclusive), then + // show the "battery saver schedule suggestion" notification. + } + + return true; + } + return false; + } + + private static boolean maybeShowBatterySaverConfirmation(Context context) { + if (Secure.getInt(context.getContentResolver(), + Secure.LOW_POWER_WARNING_ACKNOWLEDGED, 0) != 0) { + return false; // Already shown. + } + final Intent i = new Intent(ACTION_SHOW_START_SAVER_CONFIRMATION); + context.sendBroadcast(i); + + return true; + } + + private static void setBatterySaverConfirmationAcknowledged(Context context) { + Secure.putInt(context.getContentResolver(), Secure.LOW_POWER_WARNING_ACKNOWLEDGED, 1); + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java new file mode 100644 index 0000000000000..b33df3031ce9e --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2018 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.settingslib.fuelgauge; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.os.PowerManager; +import android.provider.Settings.Secure; + +import com.android.settingslib.SettingsLibRobolectricTestRunner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + + +@RunWith(SettingsLibRobolectricTestRunner.class) +public class BatterySaverUtilsTest { + @Mock + Context mMockContext; + + @Mock + ContentResolver mMockResolver; + + @Mock + PowerManager mMockPowerManager; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + when(mMockContext.getContentResolver()).thenReturn(mMockResolver); + when(mMockContext.getSystemService(eq(PowerManager.class))).thenReturn(mMockPowerManager); + when(mMockPowerManager.setPowerSaveMode(anyBoolean())).thenReturn(true); + } + + @Test + public void testSetPowerSaveMode_enable_firstCall_needWarning() throws Exception { + Secure.putString(mMockResolver, Secure.LOW_POWER_WARNING_ACKNOWLEDGED, "null"); + Secure.putString(mMockResolver, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, "null"); + + assertEquals(false, BatterySaverUtils.setPowerSaveMode(mMockContext, true, true)); + + verify(mMockContext, times(1)).sendBroadcast(any(Intent.class)); + verify(mMockPowerManager, times(0)).setPowerSaveMode(anyBoolean()); + + // They shouldn't have changed. + assertEquals(-1, + Secure.getInt(mMockResolver, Secure.LOW_POWER_WARNING_ACKNOWLEDGED, -1)); + assertEquals(-2, + Secure.getInt(mMockResolver, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, -2)); + } + + @Test + public void testSetPowerSaveMode_enable_secondCall_needWarning() throws Exception { + Secure.putInt(mMockResolver, Secure.LOW_POWER_WARNING_ACKNOWLEDGED, 1); // Already acked. + Secure.putString(mMockResolver, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, "null"); + + assertEquals(true, BatterySaverUtils.setPowerSaveMode(mMockContext, true, true)); + + verify(mMockContext, times(0)).sendBroadcast(any(Intent.class)); + verify(mMockPowerManager, times(1)).setPowerSaveMode(eq(true)); + + assertEquals(1, Secure.getInt(mMockResolver, Secure.LOW_POWER_WARNING_ACKNOWLEDGED, -1)); + assertEquals(1, Secure.getInt(mMockResolver, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, -2)); + } + + @Test + public void testSetPowerSaveMode_enable_thridCall_needWarning() throws Exception { + Secure.putInt(mMockResolver, Secure.LOW_POWER_WARNING_ACKNOWLEDGED, 1); // Already acked. + Secure.putInt(mMockResolver, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, 1); + + assertEquals(true, BatterySaverUtils.setPowerSaveMode(mMockContext, true, true)); + + verify(mMockContext, times(0)).sendBroadcast(any(Intent.class)); + verify(mMockPowerManager, times(1)).setPowerSaveMode(eq(true)); + + assertEquals(1, Secure.getInt(mMockResolver, Secure.LOW_POWER_WARNING_ACKNOWLEDGED, -1)); + assertEquals(2, Secure.getInt(mMockResolver, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, -2)); + } + + @Test + public void testSetPowerSaveMode_enable_firstCall_noWarning() throws Exception { + Secure.putString(mMockResolver, Secure.LOW_POWER_WARNING_ACKNOWLEDGED, "null"); + Secure.putString(mMockResolver, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, "null"); + + assertEquals(true, BatterySaverUtils.setPowerSaveMode(mMockContext, true, false)); + + verify(mMockContext, times(0)).sendBroadcast(any(Intent.class)); + verify(mMockPowerManager, times(1)).setPowerSaveMode(eq(true)); + + assertEquals(1, Secure.getInt(mMockResolver, Secure.LOW_POWER_WARNING_ACKNOWLEDGED, -1)); + assertEquals(1, Secure.getInt(mMockResolver, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, -2)); + } + + @Test + public void testSetPowerSaveMode_disable_firstCall_noWarning() throws Exception { + Secure.putString(mMockResolver, Secure.LOW_POWER_WARNING_ACKNOWLEDGED, "null"); + Secure.putString(mMockResolver, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, "null"); + + // When disabling, needFirstTimeWarning doesn't matter. + assertEquals(true, BatterySaverUtils.setPowerSaveMode(mMockContext, false, false)); + + verify(mMockContext, times(0)).sendBroadcast(any(Intent.class)); + verify(mMockPowerManager, times(1)).setPowerSaveMode(eq(false)); + + assertEquals(-1, Secure.getInt(mMockResolver, Secure.LOW_POWER_WARNING_ACKNOWLEDGED, -1)); + assertEquals(-2, + Secure.getInt(mMockResolver, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, -2)); + } + + @Test + public void testSetPowerSaveMode_disable_firstCall_needWarning() throws Exception { + Secure.putString(mMockResolver, Secure.LOW_POWER_WARNING_ACKNOWLEDGED, "null"); + Secure.putString(mMockResolver, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, "null"); + + // When disabling, needFirstTimeWarning doesn't matter. + assertEquals(true, BatterySaverUtils.setPowerSaveMode(mMockContext, false, true)); + + verify(mMockContext, times(0)).sendBroadcast(any(Intent.class)); + verify(mMockPowerManager, times(1)).setPowerSaveMode(eq(false)); + + assertEquals(-1, Secure.getInt(mMockResolver, Secure.LOW_POWER_WARNING_ACKNOWLEDGED, -1)); + assertEquals(-2, + Secure.getInt(mMockResolver, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, -2)); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java index 9a3a825b138cb..b89e15da67293 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java @@ -27,7 +27,6 @@ import android.content.DialogInterface.OnDismissListener; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioAttributes; -import android.os.AsyncTask; import android.os.Handler; import android.os.Looper; import android.os.PowerManager; @@ -37,6 +36,7 @@ import android.util.Slog; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.settingslib.Utils; +import com.android.settingslib.fuelgauge.BatterySaverUtils; import com.android.settingslib.utils.PowerUtil; import com.android.systemui.R; import com.android.systemui.SystemUI; @@ -72,6 +72,8 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { "PNW.clickedThermalShutdownWarning"; private static final String ACTION_DISMISSED_THERMAL_SHUTDOWN_WARNING = "PNW.dismissedThermalShutdownWarning"; + private static final String ACTION_SHOW_START_SAVER_CONFIRMATION = + BatterySaverUtils.ACTION_SHOW_START_SAVER_CONFIRMATION; private static final AudioAttributes AUDIO_ATTRIBUTES = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) @@ -404,7 +406,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { d.setTitle(R.string.battery_saver_confirmation_title); d.setMessage(com.android.internal.R.string.battery_saver_description); d.setNegativeButton(android.R.string.cancel, null); - d.setPositiveButton(R.string.battery_saver_confirmation_ok, mStartSaverMode); + d.setPositiveButton(R.string.battery_saver_confirmation_ok, mStartSaverModeNoConfirmation); d.setShowForAllUsers(true); d.setOnDismissListener(new OnDismissListener() { @Override @@ -416,8 +418,8 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { mSaverConfirmation = d; } - private void setSaverMode(boolean mode) { - mPowerMan.setPowerSaveMode(mode); + private void setSaverMode(boolean mode, boolean needFirstTimeWarning) { + BatterySaverUtils.setPowerSaveMode(mContext, mode, needFirstTimeWarning); } private final class Receiver extends BroadcastReceiver { @@ -431,8 +433,9 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { filter.addAction(ACTION_DISMISSED_TEMP_WARNING); filter.addAction(ACTION_CLICKED_THERMAL_SHUTDOWN_WARNING); filter.addAction(ACTION_DISMISSED_THERMAL_SHUTDOWN_WARNING); + filter.addAction(ACTION_SHOW_START_SAVER_CONFIRMATION); mContext.registerReceiverAsUser(this, UserHandle.ALL, filter, - android.Manifest.permission.STATUS_BAR_SERVICE, mHandler); + android.Manifest.permission.DEVICE_POWER, mHandler); } @Override @@ -443,6 +446,9 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { dismissLowBatteryNotification(); mContext.startActivityAsUser(mOpenBatterySettings, UserHandle.CURRENT); } else if (action.equals(ACTION_START_SAVER)) { + setSaverMode(true, true); + dismissLowBatteryNotification(); + } else if (action.equals(ACTION_SHOW_START_SAVER_CONFIRMATION)) { dismissLowBatteryNotification(); showStartSaverConfirmation(); } else if (action.equals(ACTION_DISMISSED_WARNING)) { @@ -461,15 +467,6 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { } } - private final OnClickListener mStartSaverMode = new OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - AsyncTask.execute(new Runnable() { - @Override - public void run() { - setSaverMode(true); - } - }); - } - }; + private final OnClickListener mStartSaverModeNoConfirmation = + (dialog, which) -> setSaverMode(true, false); } diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java index ac86c8ae097dc..f08219a8f7422 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java @@ -137,24 +137,9 @@ public class PowerUI extends SystemUI { void updateBatteryWarningLevels() { int critLevel = mContext.getResources().getInteger( com.android.internal.R.integer.config_criticalBatteryWarningLevel); - - final ContentResolver resolver = mContext.getContentResolver(); - final int defWarnLevel = mContext.getResources().getInteger( + int warnLevel = mContext.getResources().getInteger( com.android.internal.R.integer.config_lowBatteryWarningLevel); - final int lowPowerModeTriggerLevel = Settings.Global.getInt(resolver, - Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, defWarnLevel); - // Note LOW_POWER_MODE_TRIGGER_LEVEL can take any value between 0 and 100, but - // for the UI purposes, let's cap it at 15% -- i.e. even if the trigger level is higher - // like 50%, let's not show the "low battery" notification until it hits - // config_lowBatteryWarningLevel, which is 15% by default. - // LOW_POWER_MODE_TRIGGER_LEVEL is still used in other places as-is. For example, if it's - // 50, then battery saver kicks in when the battery level hits 50%. - int warnLevel = Math.min(defWarnLevel, lowPowerModeTriggerLevel); - - if (warnLevel == 0) { - warnLevel = defWarnLevel; - } if (warnLevel < critLevel) { warnLevel = critLevel; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java index 49f880ce3320c..7221efab0bb26 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java @@ -28,6 +28,7 @@ import android.os.PowerSaveState; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; +import com.android.settingslib.fuelgauge.BatterySaverUtils; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -93,7 +94,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC @Override public void setPowerSaveMode(boolean powerSave) { - mPowerManager.setPowerSaveMode(powerSave); + BatterySaverUtils.setPowerSaveMode(mContext, powerSave, /*needFirstTimeWarning*/ true); } @Override