diff options
5 files changed, 213 insertions, 2 deletions
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index 0266131a6c0d..d7efa91fb28b 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -294,4 +294,9 @@ public abstract class ActivityManagerInternal { * @param userId The user it is allowed for. */ public abstract void setAllowAppSwitches(@NonNull String type, int uid, int userId); + + /** + * @return true if runtime was restarted, false if it's normal boot + */ + public abstract boolean isRuntimeRestarted(); } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 3c8f112cfab0..9ac241e0dc55 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -24330,6 +24330,11 @@ public class ActivityManagerService extends IActivityManager.Stub } } } + + @Override + public boolean isRuntimeRestarted() { + return mSystemServiceManager.isRuntimeRestarted(); + } } /** 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 483d5181f9c9..80bc935959a2 100644 --- a/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java +++ b/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java @@ -16,6 +16,7 @@ package com.android.server.power.batterysaver; import android.Manifest; +import android.app.ActivityManagerInternal; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -109,6 +110,9 @@ public class BatterySaverController implements BatterySaverPolicyListener { final IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON); filter.addAction(Intent.ACTION_SCREEN_OFF); mContext.registerReceiver(mReceiver, filter); + + mFileUpdater.systemReady(LocalServices.getService(ActivityManagerInternal.class) + .isRuntimeRestarted()); } private PowerManager getPowerManager() { 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 cc1b540e669b..e0ab9e93a8a6 100644 --- a/services/core/java/com/android/server/power/batterysaver/FileUpdater.java +++ b/services/core/java/com/android/server/power/batterysaver/FileUpdater.java @@ -16,19 +16,34 @@ package com.android.server.power.batterysaver; import android.content.Context; +import android.os.Environment; import android.os.Handler; import android.os.Looper; +import android.os.SystemProperties; import android.util.ArrayMap; +import android.util.AtomicFile; import android.util.Slog; +import android.util.Xml; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FastXmlSerializer; +import com.android.internal.util.XmlUtils; import com.android.server.IoThread; import libcore.io.IoUtils; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Map; @@ -47,6 +62,14 @@ public class FileUpdater { private static final boolean DEBUG = BatterySaverController.DEBUG; + /** + * If this system property is set to 1, it'll skip all file writes. This can be used when + * one needs to change max CPU frequency for benchmarking, for example. + */ + private static final String PROP_SKIP_WRITE = "debug.batterysaver.no_write_files"; + + private static final String TAG_DEFAULT_ROOT = "defaults"; + // Don't do disk access with this lock held. private final Object mLock = new Object(); @@ -93,6 +116,21 @@ public class FileUpdater { RETRY_INTERVAL_MS = retryIntervalMs; } + public void systemReady(boolean runtimeRestarted) { + synchronized (mLock) { + if (runtimeRestarted) { + // If it runtime restarted, read the original values from the disk and apply. + if (loadDefaultValuesLocked()) { + Slog.d(TAG, "Default values loaded after runtime restart; writing them..."); + restoreDefault(); + } + } else { + // Delete it, without checking the result. (file-not-exist is not an exception.) + injectDefaultValuesFilename().delete(); + } + } + } + /** * Write values to files. (Note the actual writes happen ASAP but asynchronously.) */ @@ -241,6 +279,7 @@ public class FileUpdater { } synchronized (mLock) { mDefaultValues.put(file, originalValue); + saveDefaultValuesLocked(); } return true; } @@ -252,17 +291,92 @@ public class FileUpdater { @VisibleForTesting void injectWriteToFile(String file, String value) throws IOException { + if (injectShouldSkipWrite()) { + Slog.i(TAG, "Skipped writing to '" + file + "'"); + return; + } if (DEBUG) { Slog.d(TAG, "Writing: '" + value + "' to '" + file + "'"); } try (FileWriter out = new FileWriter(file)) { out.write(value); - } catch (IOException e) { + } catch (IOException | RuntimeException e) { Slog.w(TAG, "Failed writing '" + value + "' to '" + file + "': " + e.getMessage()); throw e; } } + private void saveDefaultValuesLocked() { + final AtomicFile file = new AtomicFile(injectDefaultValuesFilename()); + + FileOutputStream outs = null; + try { + file.getBaseFile().getParentFile().mkdirs(); + outs = file.startWrite(); + + // Write to XML + XmlSerializer out = new FastXmlSerializer(); + out.setOutput(outs, StandardCharsets.UTF_8.name()); + out.startDocument(null, true); + out.startTag(null, TAG_DEFAULT_ROOT); + + XmlUtils.writeMapXml(mDefaultValues, out, null); + + // Epilogue. + out.endTag(null, TAG_DEFAULT_ROOT); + out.endDocument(); + + // Close. + file.finishWrite(outs); + } catch (IOException | XmlPullParserException | RuntimeException e) { + Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e); + file.failWrite(outs); + } + } + + @VisibleForTesting + boolean loadDefaultValuesLocked() { + final AtomicFile file = new AtomicFile(injectDefaultValuesFilename()); + if (DEBUG) { + Slog.d(TAG, "Loading from " + file.getBaseFile()); + } + Map<String, String> read = null; + try (FileInputStream in = file.openRead()) { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(in, StandardCharsets.UTF_8.name()); + + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { + if (type != XmlPullParser.START_TAG) { + continue; + } + final int depth = parser.getDepth(); + // Check the root tag + final String tag = parser.getName(); + if (depth == 1) { + if (!TAG_DEFAULT_ROOT.equals(tag)) { + Slog.e(TAG, "Invalid root tag: " + tag); + return false; + } + continue; + } + final String[] tagName = new String[1]; + read = (ArrayMap<String, String>) XmlUtils.readThisArrayMapXml(parser, + TAG_DEFAULT_ROOT, tagName, null); + } + } catch (FileNotFoundException e) { + read = null; + } catch (IOException | XmlPullParserException | RuntimeException e) { + Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e); + } + if (read != null) { + mDefaultValues.clear(); + mDefaultValues.putAll(read); + return true; + } + return false; + } + private void doWtf(String message) { injectWtf(message, null); } @@ -271,4 +385,20 @@ public class FileUpdater { void injectWtf(String message, Throwable e) { Slog.wtf(TAG, message, e); } + + File injectDefaultValuesFilename() { + final File dir = new File(Environment.getDataSystemDirectory(), "battery-saver"); + dir.mkdirs(); + return new File(dir, "default-values.xml"); + } + + @VisibleForTesting + boolean injectShouldSkipWrite() { + return SystemProperties.getBoolean(PROP_SKIP_WRITE, false); + } + + @VisibleForTesting + ArrayMap<String, String> getDefaultValuesForTest() { + return mDefaultValues; + } } 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 index 7e2a7d221c22..7324fe6de9d8 100644 --- a/services/tests/servicestests/src/com/android/server/power/batterysaver/FileUpdaterTest.java +++ b/services/tests/servicestests/src/com/android/server/power/batterysaver/FileUpdaterTest.java @@ -15,8 +15,9 @@ */ package com.android.server.power.batterysaver; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; 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; @@ -26,6 +27,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.content.Context; +import android.hardware.camera2.impl.GetCommand; import android.os.Handler; import android.os.Looper; import android.support.test.InstrumentationRegistry; @@ -40,6 +42,7 @@ import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.io.File; import java.io.IOException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -72,6 +75,17 @@ public class FileUpdaterTest { void injectWtf(String message, Throwable e) { mInjector.injectWtf(message, e); } + + @Override + File injectDefaultValuesFilename() { + return new File(InstrumentationRegistry.getContext().getCacheDir() + + "/test-default.xml"); + } + + @Override + boolean injectShouldSkipWrite() { + return false; + } } private interface Injector { @@ -334,4 +348,57 @@ public class FileUpdaterTest { reset(mInjector); testMultiWrites(); } + + @Test + public void testWriteReadDefault() 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"); + + // Clear and reload the default. + assertEquals(3, mInstance.getDefaultValuesForTest().size()); + mInstance.getDefaultValuesForTest().clear(); + assertEquals(0, mInstance.getDefaultValuesForTest().size()); + + mInstance.systemReady(/*runtimeRestarted=*/ true); + + assertEquals(3, mInstance.getDefaultValuesForTest().size()); + + // 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"); + + // Make sure the default file still exists. + assertTrue(mInstance.injectDefaultValuesFilename().exists()); + + // Simulate a clean boot. + mInstance.getDefaultValuesForTest().clear(); + assertEquals(0, mInstance.getDefaultValuesForTest().size()); + + mInstance.systemReady(/*runtimeRestarted=*/ false); + + // Default is empty, and the file is gone. + assertEquals(0, mInstance.getDefaultValuesForTest().size()); + assertFalse(mInstance.injectDefaultValuesFilename().exists()); + + // No WTF should have happened. + veriryWtf(0); + } } |