Merge "Extreme battery saver: Cap max cpu frequency."
diff --git a/services/core/java/com/android/server/power/BatterySaverPolicy.java b/services/core/java/com/android/server/power/BatterySaverPolicy.java
index 3992f8a..336df48 100644
--- a/services/core/java/com/android/server/power/BatterySaverPolicy.java
+++ b/services/core/java/com/android/server/power/BatterySaverPolicy.java
@@ -32,6 +32,7 @@
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.power.batterysaver.CpuFrequencies;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -40,15 +41,14 @@
/**
* Class to decide whether to turn on battery saver mode for specific service
*
- * TODO: We should probably make {@link #mFilesForInteractive} and {@link #mFilesForNoninteractive}
- * less flexible and just take a list of "CPU number - frequency" pairs. Being able to write
- * anything under /sys/ and /proc/ is too loose.
- *
- * Test: atest BatterySaverPolicyTest
+ * Test:
+ atest ${ANDROID_BUILD_TOP}/frameworks/base/services/tests/servicestests/src/com/android/server/power/BatterySaverPolicyTest.java
*/
public class BatterySaverPolicy extends ContentObserver {
private static final String TAG = "BatterySaverPolicy";
+ public static final boolean DEBUG = false; // DO NOT SUBMIT WITH TRUE.
+
// Value of batterySaverGpsMode such that GPS isn't affected by battery saver mode.
public static final int GPS_MODE_NO_CHANGE = 0;
// Value of batterySaverGpsMode such that GPS is disabled when battery saver mode
@@ -70,8 +70,8 @@
private static final String KEY_FORCE_ALL_APPS_STANDBY = "force_all_apps_standby";
private static final String KEY_OPTIONAL_SENSORS_DISABLED = "optional_sensors_disabled";
- private static final String KEY_FILE_FOR_INTERACTIVE_PREFIX = "file-on:";
- private static final String KEY_FILE_FOR_NONINTERACTIVE_PREFIX = "file-off:";
+ private static final String KEY_CPU_FREQ_INTERACTIVE = "cpufreq-i";
+ private static final String KEY_CPU_FREQ_NONINTERACTIVE = "cpufreq-n";
private static String mSettings;
private static String mDeviceSpecificSettings;
@@ -273,6 +273,11 @@
mSettings = setting;
mDeviceSpecificSettings = deviceSpecificSetting;
+ if (DEBUG) {
+ Slog.i(TAG, "mSettings=" + mSettings);
+ Slog.i(TAG, "mDeviceSpecificSettings=" + mDeviceSpecificSettings);
+ }
+
final KeyValueListParser parser = new KeyValueListParser(',');
// Non-device-specific parameters.
@@ -307,29 +312,11 @@
+ deviceSpecificSetting);
}
- mFilesForInteractive = collectParams(parser, KEY_FILE_FOR_INTERACTIVE_PREFIX);
- mFilesForNoninteractive = collectParams(parser, KEY_FILE_FOR_NONINTERACTIVE_PREFIX);
- }
+ mFilesForInteractive = (new CpuFrequencies()).parseString(
+ parser.getString(KEY_CPU_FREQ_INTERACTIVE, "")).toSysFileMap();
- 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;
- }
-
- ret.put(path, parser.getString(key, ""));
- }
- return ret;
+ mFilesForNoninteractive = (new CpuFrequencies()).parseString(
+ parser.getString(KEY_CPU_FREQ_NONINTERACTIVE, "")).toSysFileMap();
}
/**
@@ -399,10 +386,10 @@
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(" 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);
diff --git a/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java b/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java
index 3db6a25..b471c8d 100644
--- a/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java
+++ b/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java
@@ -49,7 +49,7 @@
public class BatterySaverController implements BatterySaverPolicyListener {
static final String TAG = "BatterySaverController";
- static final boolean DEBUG = false; // DO NOT MERGE WITH TRUE
+ static final boolean DEBUG = BatterySaverPolicy.DEBUG;
private final Object mLock = new Object();
private final Context mContext;
diff --git a/services/core/java/com/android/server/power/batterysaver/CpuFrequencies.java b/services/core/java/com/android/server/power/batterysaver/CpuFrequencies.java
new file mode 100644
index 0000000..1629486
--- /dev/null
+++ b/services/core/java/com/android/server/power/batterysaver/CpuFrequencies.java
@@ -0,0 +1,101 @@
+/*
+ * 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.util.ArrayMap;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.Map;
+
+
+/**
+ * Helper to parse a list of "core-number:frequency" pairs concatenated with / as a separator,
+ * and convert them into a map of "filename -> value" that should be written to
+ * /sys/.../scaling_max_freq.
+ *
+ * Example input: "0:1900800/4:2500000", which will be converted into:
+ * "/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq" "1900800"
+ * "/sys/devices/system/cpu/cpu4/cpufreq/scaling_max_freq" "2500000"
+ *
+ * Test:
+ atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/power/batterysaver/CpuFrequenciesTest.java
+ */
+public class CpuFrequencies {
+ private static final String TAG = "CpuFrequencies";
+
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private final ArrayMap<Integer, Long> mCoreAndFrequencies = new ArrayMap<>();
+
+ public CpuFrequencies() {
+ }
+
+ /**
+ * Parse a string.
+ */
+ public CpuFrequencies parseString(String cpuNumberAndFrequencies) {
+ synchronized (mLock) {
+ mCoreAndFrequencies.clear();
+ try {
+ for (String pair : cpuNumberAndFrequencies.split("/")) {
+ final String[] coreAndFreq = pair.split(":", 2);
+
+ if (coreAndFreq.length != 2) {
+ throw new IllegalArgumentException("Wrong format");
+ }
+ final int core = Integer.parseInt(coreAndFreq[0]);
+ final long freq = Long.parseLong(coreAndFreq[1]);
+
+ mCoreAndFrequencies.put(core, freq);
+ }
+ } catch (IllegalArgumentException e) {
+ Slog.wtf(TAG, "Invalid configuration: " + cpuNumberAndFrequencies, e);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Return a new map containing the filename-value pairs.
+ */
+ public ArrayMap<String, String> toSysFileMap() {
+ final ArrayMap<String, String> map = new ArrayMap<>();
+ addToSysFileMap(map);
+ return map;
+ }
+
+ /**
+ * Add the filename-value pairs to an existing map.
+ */
+ public void addToSysFileMap(Map<String, String> map) {
+ synchronized (mLock) {
+ final int size = mCoreAndFrequencies.size();
+
+ for (int i = 0; i < size; i++) {
+ final int core = mCoreAndFrequencies.keyAt(i);
+ final long freq = mCoreAndFrequencies.valueAt(i);
+
+ final String file = "/sys/devices/system/cpu/cpu" + Integer.toString(core) +
+ "/cpufreq/scaling_max_freq";
+
+ map.put(file, Long.toString(freq));
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/power/batterysaver/FileUpdater.java b/services/core/java/com/android/server/power/batterysaver/FileUpdater.java
index cfe8fc4..cc1b540 100644
--- a/services/core/java/com/android/server/power/batterysaver/FileUpdater.java
+++ b/services/core/java/com/android/server/power/batterysaver/FileUpdater.java
@@ -16,40 +16,259 @@
package com.android.server.power.batterysaver;
import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
import android.util.ArrayMap;
import android.util.Slog;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.IoThread;
+
+import libcore.io.IoUtils;
+
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Map;
+
/**
* Used by {@link BatterySaverController} to write values to /sys/ (and possibly /proc/ too) files
- * with retry and to restore the original values.
+ * with retries. It also support restoring to the file original values.
*
- * TODO Implement it
+ * Retries are needed because writing to "/sys/.../scaling_max_freq" returns EIO when the current
+ * frequency happens to be above the new max frequency.
+ *
+ * Test:
+ atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/power/batterysaver/FileUpdaterTest.java
*/
public class FileUpdater {
private static final String TAG = BatterySaverController.TAG;
private static final boolean DEBUG = BatterySaverController.DEBUG;
+ // Don't do disk access with this lock held.
private final Object mLock = new Object();
+
private final Context mContext;
+ private final Handler mHandler;
+
+ /**
+ * Filename -> value map that holds pending writes.
+ */
+ @GuardedBy("mLock")
+ private final ArrayMap<String, String> mPendingWrites = new ArrayMap<>();
+
+ /**
+ * Filename -> value that holds the original value of each file.
+ */
+ @GuardedBy("mLock")
+ private final ArrayMap<String, String> mDefaultValues = new ArrayMap<>();
+
+ /** Number of retries. We give up on writing after {@link #MAX_RETRIES} retries. */
+ @GuardedBy("mLock")
+ private int mRetries = 0;
+
+ private final int MAX_RETRIES;
+
+ private final long RETRY_INTERVAL_MS;
+
+ /**
+ * "Official" constructor. Don't use the other constructor in the production code.
+ */
public FileUpdater(Context context) {
- mContext = context;
+ this(context, IoThread.get().getLooper(), 10, 5000);
}
+ /**
+ * Constructor for test.
+ */
+ @VisibleForTesting
+ FileUpdater(Context context, Looper looper, int maxRetries, int retryIntervalMs) {
+ mContext = context;
+ mHandler = new Handler(looper);
+
+ MAX_RETRIES = maxRetries;
+ RETRY_INTERVAL_MS = retryIntervalMs;
+ }
+
+ /**
+ * Write values to files. (Note the actual writes happen ASAP but asynchronously.)
+ */
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) + "'");
+ synchronized (mLock) {
+ for (int i = fileValues.size() - 1; i >= 0; i--) {
+ final String file = fileValues.keyAt(i);
+ final String value = fileValues.valueAt(i);
+
+ if (DEBUG) {
+ Slog.d(TAG, "Scheduling write: '" + value + "' to '" + file + "'");
+ }
+
+ mPendingWrites.put(file, value);
+
+ }
+ mRetries = 0;
+
+ mHandler.removeCallbacks(mHandleWriteOnHandlerRunnable);
+ mHandler.post(mHandleWriteOnHandlerRunnable);
+ }
+ }
+
+ /**
+ * Restore the default values.
+ */
+ public void restoreDefault() {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slog.d(TAG, "Resetting file default values.");
+ }
+ mPendingWrites.clear();
+
+ writeFiles(mDefaultValues);
+ }
+ }
+
+ private Runnable mHandleWriteOnHandlerRunnable = () -> handleWriteOnHandler();
+
+ /** Convert map keys into a single string for debug messages. */
+ private String getKeysString(Map<String, String> source) {
+ return new ArrayList<>(source.keySet()).toString();
+ }
+
+ /** Clone an ArrayMap. */
+ private ArrayMap<String, String> cloneMap(ArrayMap<String, String> source) {
+ return new ArrayMap<>(source);
+ }
+
+ /**
+ * Called on the handler and writes {@link #mPendingWrites} to the disk.
+ *
+ * When it about to write to each file for the first time, it'll read the file and store
+ * the original value in {@link #mDefaultValues}.
+ */
+ private void handleWriteOnHandler() {
+ // We don't want to access the disk with the lock held, so copy the pending writes to
+ // a local map.
+ final ArrayMap<String, String> writes;
+ synchronized (mLock) {
+ if (mPendingWrites.size() == 0) {
+ return;
+ }
+
+ if (DEBUG) {
+ Slog.d(TAG, "Writing files: (# retries=" + mRetries + ") " +
+ getKeysString(mPendingWrites));
+ }
+
+ writes = cloneMap(mPendingWrites);
+ }
+
+ // Then write.
+
+ boolean needRetry = false;
+
+ final int size = writes.size();
+ for (int i = 0; i < size; i++) {
+ final String file = writes.keyAt(i);
+ final String value = writes.valueAt(i);
+
+ // Make sure the default value is loaded.
+ if (!ensureDefaultLoaded(file)) {
+ continue;
+ }
+
+ // Write to the file. When succeeded, remove it from the pending list.
+ // Otherwise, schedule a retry.
+ try {
+ injectWriteToFile(file, value);
+
+ removePendingWrite(file);
+ } catch (IOException e) {
+ needRetry = true;
}
}
+ if (needRetry) {
+ scheduleRetry();
+ }
}
- public void restoreDefault() {
- if (DEBUG) {
- Slog.d(TAG, "Resetting file default values");
+ private void removePendingWrite(String file) {
+ synchronized (mLock) {
+ mPendingWrites.remove(file);
}
}
+
+ private void scheduleRetry() {
+ synchronized (mLock) {
+ if (mPendingWrites.size() == 0) {
+ return; // Shouldn't happen but just in case.
+ }
+
+ mRetries++;
+ if (mRetries > MAX_RETRIES) {
+ doWtf("Gave up writing files: " + getKeysString(mPendingWrites));
+ return;
+ }
+
+ mHandler.removeCallbacks(mHandleWriteOnHandlerRunnable);
+ mHandler.postDelayed(mHandleWriteOnHandlerRunnable, RETRY_INTERVAL_MS);
+ }
+ }
+
+ /**
+ * Make sure {@link #mDefaultValues} has the default value loaded for {@code file}.
+ *
+ * @return true if the default value is loaded. false if the file cannot be read.
+ */
+ private boolean ensureDefaultLoaded(String file) {
+ // Has the default already?
+ synchronized (mLock) {
+ if (mDefaultValues.containsKey(file)) {
+ return true;
+ }
+ }
+ final String originalValue;
+ try {
+ originalValue = injectReadFromFileTrimmed(file);
+ } catch (IOException e) {
+ // If the file is not readable, assume can't write too.
+ injectWtf("Unable to read from file", e);
+
+ removePendingWrite(file);
+ return false;
+ }
+ synchronized (mLock) {
+ mDefaultValues.put(file, originalValue);
+ }
+ return true;
+ }
+
+ @VisibleForTesting
+ String injectReadFromFileTrimmed(String file) throws IOException {
+ return IoUtils.readFileAsString(file).trim();
+ }
+
+ @VisibleForTesting
+ void injectWriteToFile(String file, String value) throws IOException {
+ if (DEBUG) {
+ Slog.d(TAG, "Writing: '" + value + "' to '" + file + "'");
+ }
+ try (FileWriter out = new FileWriter(file)) {
+ out.write(value);
+ } catch (IOException e) {
+ Slog.w(TAG, "Failed writing '" + value + "' to '" + file + "': " + e.getMessage());
+ throw e;
+ }
+ }
+
+ private void doWtf(String message) {
+ injectWtf(message, null);
+ }
+
+ @VisibleForTesting
+ void injectWtf(String message, Throwable e) {
+ Slog.wtf(TAG, message, e);
+ }
}
diff --git a/services/tests/servicestests/res/values/strings.xml b/services/tests/servicestests/res/values/strings.xml
index 3ac56bb..57da0af 100644
--- a/services/tests/servicestests/res/values/strings.xml
+++ b/services/tests/servicestests/res/values/strings.xml
@@ -30,6 +30,6 @@
<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>
+ <string name="config_batterySaverDeviceSpecificConfig_2">cpufreq-n=1:123/2:456</string>
+ <string name="config_batterySaverDeviceSpecificConfig_3">cpufreq-n=2:222,cpufreq-i=3:333/4:444</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 0db19e4..20cf733 100644
--- a/services/tests/servicestests/src/com/android/server/power/BatterySaverPolicyTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/BatterySaverPolicyTest.java
@@ -237,21 +237,27 @@
mBatterySaverPolicy.onChange();
assertThat(mBatterySaverPolicy.getFileValues(true).toString()).isEqualTo("{}");
assertThat(mBatterySaverPolicy.getFileValues(false).toString())
- .isEqualTo("{/sys/a=1, /sys/b=2}");
-
+ .isEqualTo("{/sys/devices/system/cpu/cpu1/cpufreq/scaling_max_freq=123, " +
+ "/sys/devices/system/cpu/cpu2/cpufreq/scaling_max_freq=456}");
mDeviceSpecificConfigResId = R.string.config_batterySaverDeviceSpecificConfig_3;
mBatterySaverPolicy.onChange();
- assertThat(mBatterySaverPolicy.getFileValues(true).toString()).isEqualTo("{/proc/c=4}");
- assertThat(mBatterySaverPolicy.getFileValues(false).toString()).isEqualTo("{/sys/a=3}");
+ assertThat(mBatterySaverPolicy.getFileValues(true).toString())
+ .isEqualTo("{/sys/devices/system/cpu/cpu3/cpufreq/scaling_max_freq=333, " +
+ "/sys/devices/system/cpu/cpu4/cpufreq/scaling_max_freq=444}");
+ assertThat(mBatterySaverPolicy.getFileValues(false).toString())
+ .isEqualTo("{/sys/devices/system/cpu/cpu2/cpufreq/scaling_max_freq=222}");
mMockGlobalSettings.put(Global.BATTERY_SAVER_DEVICE_SPECIFIC_CONSTANTS,
- "file-on:/proc/z=4");
+ "cpufreq-i=3:1234567890/4:014/5:015");
mBatterySaverPolicy.onChange();
- assertThat(mBatterySaverPolicy.getFileValues(true).toString()).isEqualTo("{/proc/z=4}");
+ assertThat(mBatterySaverPolicy.getFileValues(true).toString())
+ .isEqualTo("{/sys/devices/system/cpu/cpu3/cpufreq/scaling_max_freq=1234567890, " +
+ "/sys/devices/system/cpu/cpu4/cpufreq/scaling_max_freq=14, " +
+ "/sys/devices/system/cpu/cpu5/cpufreq/scaling_max_freq=15}");
assertThat(mBatterySaverPolicy.getFileValues(false).toString()).isEqualTo("{}");
}
}
diff --git a/services/tests/servicestests/src/com/android/server/power/batterysaver/CpuFrequenciesTest.java b/services/tests/servicestests/src/com/android/server/power/batterysaver/CpuFrequenciesTest.java
new file mode 100644
index 0000000..f72ec34
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/power/batterysaver/CpuFrequenciesTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.ArrayMap;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/power/batterysaver/CpuFrequenciesTest.java
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class CpuFrequenciesTest {
+ private void check(ArrayMap<String, String> expected, String config) {
+ assertEquals(expected, (new CpuFrequencies().parseString(config))
+ .toSysFileMap());
+ }
+
+ @Test
+ public void test() {
+ check(new ArrayMap<>(), "");
+
+ final ArrayMap<String, String> expected = new ArrayMap<>();
+
+ expected.clear();
+ expected.put("/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq", "0");
+ check(expected, "0:0");
+
+ expected.clear();
+ expected.put("/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq", "0");
+ expected.put("/sys/devices/system/cpu/cpu1/cpufreq/scaling_max_freq", "1");
+ check(expected, "0:0/1:1");
+
+ expected.clear();
+ expected.put("/sys/devices/system/cpu/cpu2/cpufreq/scaling_max_freq", "0");
+ expected.put("/sys/devices/system/cpu/cpu1/cpufreq/scaling_max_freq", "1234567890");
+ check(expected, "2:0/1:1234567890");
+
+ expected.clear();
+ expected.put("/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq", "1900800");
+ expected.put("/sys/devices/system/cpu/cpu4/cpufreq/scaling_max_freq", "1958400");
+ check(expected, "0:1900800/4:1958400");
+
+ check(expected, "0:1900800/4:1958400/"); // Shouldn't crash.
+ check(expected, "0:1900800/4:1958400/1"); // Shouldn't crash.
+ check(expected, "0:1900800/4:1958400/a:1"); // Shouldn't crash.
+ check(expected, "0:1900800/4:1958400/1:"); // Shouldn't crash.
+ check(expected, "0:1900800/4:1958400/1:b"); // Shouldn't crash.
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/power/batterysaver/FileUpdaterTest.java b/services/tests/servicestests/src/com/android/server/power/batterysaver/FileUpdaterTest.java
new file mode 100644
index 0000000..7e2a7d2
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/power/batterysaver/FileUpdaterTest.java
@@ -0,0 +1,337 @@
+/*
+ * 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 static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.ArrayMap;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+
+/**
+ atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/power/batterysaver/FileUpdaterTest.java
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class FileUpdaterTest {
+
+ private class FileUpdaterTestable extends FileUpdater {
+ FileUpdaterTestable(Context context, Looper looper, int maxRetries, int retryIntervalMs) {
+ super(context, looper, maxRetries, retryIntervalMs);
+ }
+
+ @Override
+ String injectReadFromFileTrimmed(String file) throws IOException {
+ return mInjector.injectReadFromFileTrimmed(file);
+ }
+
+ @Override
+ void injectWriteToFile(String file, String value) throws IOException {
+ mInjector.injectWriteToFile(file, value);
+ }
+
+ @Override
+ void injectWtf(String message, Throwable e) {
+ mInjector.injectWtf(message, e);
+ }
+ }
+
+ private interface Injector {
+ String injectReadFromFileTrimmed(String file) throws IOException;
+ void injectWriteToFile(String file, String value) throws IOException;
+ void injectWtf(String message, Throwable e);
+ }
+
+ private Handler mMainHandler;
+
+ @Mock
+ private Injector mInjector;
+
+ private static final int MAX_RETRIES = 3;
+
+ private FileUpdaterTestable mInstance;
+
+ public static <T> T anyOrNull(Class<T> clazz) {
+ return ArgumentMatchers.argThat(value -> true);
+ }
+
+ public static String anyOrNullString() {
+ return ArgumentMatchers.argThat(value -> true);
+ }
+
+ @Before
+ public void setUp() {
+ mMainHandler = new Handler(Looper.getMainLooper());
+
+ MockitoAnnotations.initMocks(this);
+
+ mInstance = newInstance();
+ }
+
+ private FileUpdaterTestable newInstance() {
+ return new FileUpdaterTestable(
+ InstrumentationRegistry.getContext(),
+ Looper.getMainLooper(),
+ MAX_RETRIES,
+ 0 /* retry with no delays*/);
+ }
+
+ private void waitUntilMainHandlerDrain() throws Exception {
+ final CountDownLatch l = new CountDownLatch(1);
+ mMainHandler.post(() -> l.countDown());
+ assertTrue(l.await(5, TimeUnit.SECONDS));
+ }
+
+ private void veriryWtf(int times) {
+ verify(mInjector, times(times)).injectWtf(anyOrNullString(), anyOrNull(Throwable.class));
+ }
+
+ @Test
+ public void testNoWrites() throws Exception {
+ doReturn("111").when(mInjector).injectReadFromFileTrimmed("file1");
+ doReturn("222").when(mInjector).injectReadFromFileTrimmed("file2");
+ doReturn("333").when(mInjector).injectReadFromFileTrimmed("file3");
+
+ // Write
+ final ArrayMap<String, String> values = new ArrayMap<>();
+
+ mInstance.writeFiles(values);
+ waitUntilMainHandlerDrain();
+
+ verify(mInjector, times(0)).injectWriteToFile(anyOrNullString(), anyOrNullString());
+
+ // Reset to default
+ mInstance.restoreDefault();
+ waitUntilMainHandlerDrain();
+
+ verify(mInjector, times(0)).injectWriteToFile(anyOrNullString(), anyOrNullString());
+
+ // No WTF should have happened.
+ veriryWtf(0);
+ }
+
+ @Test
+ public void testSimpleWrite() throws Exception {
+ doReturn("111").when(mInjector).injectReadFromFileTrimmed("file1");
+ doReturn("222").when(mInjector).injectReadFromFileTrimmed("file2");
+ doReturn("333").when(mInjector).injectReadFromFileTrimmed("file3");
+
+ // Write
+ final ArrayMap<String, String> values = new ArrayMap<>();
+ values.put("file1", "11");
+
+ mInstance.writeFiles(values);
+ waitUntilMainHandlerDrain();
+
+ verify(mInjector, times(1)).injectWriteToFile("file1", "11");
+
+ // Reset to default
+ mInstance.restoreDefault();
+ waitUntilMainHandlerDrain();
+
+ verify(mInjector, times(1)).injectWriteToFile("file1", "111");
+
+ // No WTF should have happened.
+ veriryWtf(0);
+ }
+
+ @Test
+ public void testMultiWrites() throws Exception {
+ doReturn("111").when(mInjector).injectReadFromFileTrimmed("file1");
+ doReturn("222").when(mInjector).injectReadFromFileTrimmed("file2");
+ doReturn("333").when(mInjector).injectReadFromFileTrimmed("file3");
+
+ // Write
+ final ArrayMap<String, String> values = new ArrayMap<>();
+ values.put("file1", "11");
+ values.put("file2", "22");
+ values.put("file3", "33");
+
+ mInstance.writeFiles(values);
+ waitUntilMainHandlerDrain();
+
+ verify(mInjector, times(1)).injectWriteToFile("file1", "11");
+ verify(mInjector, times(1)).injectWriteToFile("file2", "22");
+ verify(mInjector, times(1)).injectWriteToFile("file3", "33");
+
+ // Reset to default
+ mInstance.restoreDefault();
+ waitUntilMainHandlerDrain();
+
+ verify(mInjector, times(1)).injectWriteToFile("file1", "111");
+ verify(mInjector, times(1)).injectWriteToFile("file2", "222");
+ verify(mInjector, times(1)).injectWriteToFile("file3", "333");
+
+ // No WTF should have happened.
+ veriryWtf(0);
+ }
+
+ @Test
+ public void testCantReadDefault() throws Exception {
+ doThrow(new IOException("can't read")).when(mInjector).injectReadFromFileTrimmed("file1");
+ doReturn("222").when(mInjector).injectReadFromFileTrimmed("file2");
+
+ // Write
+ final ArrayMap<String, String> values = new ArrayMap<>();
+ values.put("file1", "11");
+ values.put("file2", "22");
+
+ mInstance.writeFiles(values);
+ waitUntilMainHandlerDrain();
+
+ verify(mInjector, times(0)).injectWriteToFile("file1", "11");
+ verify(mInjector, times(1)).injectWriteToFile("file2", "22");
+
+ veriryWtf(1);
+
+ // Reset to default
+ mInstance.restoreDefault();
+ waitUntilMainHandlerDrain();
+
+ verify(mInjector, times(0)).injectWriteToFile("file1", "111");
+ verify(mInjector, times(1)).injectWriteToFile("file2", "222");
+
+ veriryWtf(1);
+ }
+
+ @Test
+ public void testWriteGiveUp() throws Exception {
+ doReturn("111").when(mInjector).injectReadFromFileTrimmed("file1");
+ doReturn("222").when(mInjector).injectReadFromFileTrimmed("file2");
+ doReturn("333").when(mInjector).injectReadFromFileTrimmed("fail1");
+
+ doThrow(new IOException("can't write")).when(mInjector).injectWriteToFile(
+ eq("fail1"), eq("33"));
+
+ // Write
+ final ArrayMap<String, String> values = new ArrayMap<>();
+ values.put("file1", "11");
+ values.put("file2", "22");
+ values.put("fail1", "33");
+
+ mInstance.writeFiles(values);
+ waitUntilMainHandlerDrain();
+
+ verify(mInjector, times(1)).injectWriteToFile("file1", "11");
+ verify(mInjector, times(1)).injectWriteToFile("file2", "22");
+
+ verify(mInjector, times(MAX_RETRIES + 1)).injectWriteToFile("fail1", "33");
+
+ // 1 WTF.
+ veriryWtf(1);
+
+ // Reset to default
+ mInstance.restoreDefault();
+ waitUntilMainHandlerDrain();
+
+ verify(mInjector, times(1)).injectWriteToFile("file1", "111");
+ verify(mInjector, times(1)).injectWriteToFile("file2", "222");
+
+ verify(mInjector, times(1)).injectWriteToFile("fail1", "333");
+
+ // No further WTF.
+ veriryWtf(1);
+ }
+
+ @Test
+ public void testSuccessWithRetry() throws Exception {
+ doReturn("111").when(mInjector).injectReadFromFileTrimmed("file1");
+ doReturn("222").when(mInjector).injectReadFromFileTrimmed("file2");
+ doReturn("333").when(mInjector).injectReadFromFileTrimmed("fail1");
+
+ final AtomicInteger counter = new AtomicInteger();
+ doAnswer((inv) -> {
+ if (counter.getAndIncrement() <= 1) {
+ throw new IOException();
+ }
+ return null;
+ }).when(mInjector).injectWriteToFile(eq("fail1"), eq("33"));
+
+ // Write
+ final ArrayMap<String, String> values = new ArrayMap<>();
+ values.put("file1", "11");
+ values.put("file2", "22");
+ values.put("fail1", "33");
+
+ mInstance.writeFiles(values);
+ waitUntilMainHandlerDrain();
+
+ verify(mInjector, times(1)).injectWriteToFile("file1", "11");
+ verify(mInjector, times(1)).injectWriteToFile("file2", "22");
+
+ // Should succeed after 2 retries.
+ verify(mInjector, times(3)).injectWriteToFile("fail1", "33");
+
+ // No WTF.
+ veriryWtf(0);
+
+ // Reset to default
+ mInstance.restoreDefault();
+ waitUntilMainHandlerDrain();
+
+ verify(mInjector, times(1)).injectWriteToFile("file1", "111");
+ verify(mInjector, times(1)).injectWriteToFile("file2", "222");
+ verify(mInjector, times(1)).injectWriteToFile("fail1", "333");
+
+ // Still no WTF.
+ veriryWtf(0);
+ }
+
+ @Test
+ public void testAll() throws Exception {
+ // Run multiple tests on the single target instance.
+
+ reset(mInjector);
+ testSimpleWrite();
+
+ reset(mInjector);
+ testWriteGiveUp();
+
+ reset(mInjector);
+ testMultiWrites();
+
+ reset(mInjector);
+ testSuccessWithRetry();
+
+ reset(mInjector);
+ testMultiWrites();
+ }
+}