1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.server.power.batterysaver; 17 18 import android.content.Context; 19 import android.os.Environment; 20 import android.os.Handler; 21 import android.os.Looper; 22 import android.os.SystemProperties; 23 import android.util.ArrayMap; 24 import android.util.AtomicFile; 25 import android.util.Slog; 26 import android.util.TypedXmlPullParser; 27 import android.util.TypedXmlSerializer; 28 import android.util.Xml; 29 30 import com.android.internal.annotations.GuardedBy; 31 import com.android.internal.annotations.VisibleForTesting; 32 import com.android.internal.util.FastXmlSerializer; 33 import com.android.internal.util.XmlUtils; 34 import com.android.server.IoThread; 35 36 import libcore.io.IoUtils; 37 38 import org.xmlpull.v1.XmlPullParser; 39 import org.xmlpull.v1.XmlPullParserException; 40 import org.xmlpull.v1.XmlSerializer; 41 42 import java.io.File; 43 import java.io.FileInputStream; 44 import java.io.FileNotFoundException; 45 import java.io.FileOutputStream; 46 import java.io.FileWriter; 47 import java.io.IOException; 48 import java.nio.charset.StandardCharsets; 49 import java.util.ArrayList; 50 import java.util.Map; 51 52 /** 53 * Used by {@link BatterySaverController} to write values to /sys/ (and possibly /proc/ too) files 54 * with retries. It also support restoring to the file original values. 55 * 56 * Retries are needed because writing to "/sys/.../scaling_max_freq" returns EIO when the current 57 * frequency happens to be above the new max frequency. 58 * 59 * Test: 60 atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/power/batterysaver/FileUpdaterTest.java 61 */ 62 public class FileUpdater { 63 private static final String TAG = BatterySaverController.TAG; 64 65 private static final boolean DEBUG = BatterySaverController.DEBUG; 66 67 /** 68 * If this system property is set to 1, it'll skip all file writes. This can be used when 69 * one needs to change max CPU frequency for benchmarking, for example. 70 */ 71 private static final String PROP_SKIP_WRITE = "debug.batterysaver.no_write_files"; 72 73 private static final String TAG_DEFAULT_ROOT = "defaults"; 74 75 // Don't do disk access with this lock held. 76 private final Object mLock = new Object(); 77 78 private final Context mContext; 79 80 private final Handler mHandler; 81 82 /** 83 * Filename -> value map that holds pending writes. 84 */ 85 @GuardedBy("mLock") 86 private final ArrayMap<String, String> mPendingWrites = new ArrayMap<>(); 87 88 /** 89 * Filename -> value that holds the original value of each file. 90 */ 91 @GuardedBy("mLock") 92 private final ArrayMap<String, String> mDefaultValues = new ArrayMap<>(); 93 94 /** Number of retries. We give up on writing after {@link #MAX_RETRIES} retries. */ 95 @GuardedBy("mLock") 96 private int mRetries = 0; 97 98 private final int MAX_RETRIES; 99 100 private final long RETRY_INTERVAL_MS; 101 102 /** 103 * "Official" constructor. Don't use the other constructor in the production code. 104 */ FileUpdater(Context context)105 public FileUpdater(Context context) { 106 this(context, IoThread.get().getLooper(), 10, 5000); 107 } 108 109 /** 110 * Constructor for test. 111 */ 112 @VisibleForTesting FileUpdater(Context context, Looper looper, int maxRetries, int retryIntervalMs)113 FileUpdater(Context context, Looper looper, int maxRetries, int retryIntervalMs) { 114 mContext = context; 115 mHandler = new Handler(looper); 116 117 MAX_RETRIES = maxRetries; 118 RETRY_INTERVAL_MS = retryIntervalMs; 119 } 120 systemReady(boolean runtimeRestarted)121 public void systemReady(boolean runtimeRestarted) { 122 synchronized (mLock) { 123 if (runtimeRestarted) { 124 // If it runtime restarted, read the original values from the disk and apply. 125 if (loadDefaultValuesLocked()) { 126 Slog.d(TAG, "Default values loaded after runtime restart; writing them..."); 127 restoreDefault(); 128 } 129 } else { 130 // Delete it, without checking the result. (file-not-exist is not an exception.) 131 injectDefaultValuesFilename().delete(); 132 } 133 } 134 } 135 136 /** 137 * Write values to files. (Note the actual writes happen ASAP but asynchronously.) 138 */ writeFiles(ArrayMap<String, String> fileValues)139 public void writeFiles(ArrayMap<String, String> fileValues) { 140 synchronized (mLock) { 141 for (int i = fileValues.size() - 1; i >= 0; i--) { 142 final String file = fileValues.keyAt(i); 143 final String value = fileValues.valueAt(i); 144 145 if (DEBUG) { 146 Slog.d(TAG, "Scheduling write: '" + value + "' to '" + file + "'"); 147 } 148 149 mPendingWrites.put(file, value); 150 151 } 152 mRetries = 0; 153 154 mHandler.removeCallbacks(mHandleWriteOnHandlerRunnable); 155 mHandler.post(mHandleWriteOnHandlerRunnable); 156 } 157 } 158 159 /** 160 * Restore the default values. 161 */ restoreDefault()162 public void restoreDefault() { 163 synchronized (mLock) { 164 if (DEBUG) { 165 Slog.d(TAG, "Resetting file default values."); 166 } 167 mPendingWrites.clear(); 168 169 writeFiles(mDefaultValues); 170 } 171 } 172 173 private Runnable mHandleWriteOnHandlerRunnable = () -> handleWriteOnHandler(); 174 175 /** Convert map keys into a single string for debug messages. */ getKeysString(Map<String, String> source)176 private String getKeysString(Map<String, String> source) { 177 return new ArrayList<>(source.keySet()).toString(); 178 } 179 180 /** Clone an ArrayMap. */ cloneMap(ArrayMap<String, String> source)181 private ArrayMap<String, String> cloneMap(ArrayMap<String, String> source) { 182 return new ArrayMap<>(source); 183 } 184 185 /** 186 * Called on the handler and writes {@link #mPendingWrites} to the disk. 187 * 188 * When it about to write to each file for the first time, it'll read the file and store 189 * the original value in {@link #mDefaultValues}. 190 */ handleWriteOnHandler()191 private void handleWriteOnHandler() { 192 // We don't want to access the disk with the lock held, so copy the pending writes to 193 // a local map. 194 final ArrayMap<String, String> writes; 195 synchronized (mLock) { 196 if (mPendingWrites.size() == 0) { 197 return; 198 } 199 200 if (DEBUG) { 201 Slog.d(TAG, "Writing files: (# retries=" + mRetries + ") " + 202 getKeysString(mPendingWrites)); 203 } 204 205 writes = cloneMap(mPendingWrites); 206 } 207 208 // Then write. 209 210 boolean needRetry = false; 211 212 final int size = writes.size(); 213 for (int i = 0; i < size; i++) { 214 final String file = writes.keyAt(i); 215 final String value = writes.valueAt(i); 216 217 // Make sure the default value is loaded. 218 if (!ensureDefaultLoaded(file)) { 219 continue; 220 } 221 222 // Write to the file. When succeeded, remove it from the pending list. 223 // Otherwise, schedule a retry. 224 try { 225 injectWriteToFile(file, value); 226 227 removePendingWrite(file); 228 } catch (IOException e) { 229 needRetry = true; 230 } 231 } 232 if (needRetry) { 233 scheduleRetry(); 234 } 235 } 236 removePendingWrite(String file)237 private void removePendingWrite(String file) { 238 synchronized (mLock) { 239 mPendingWrites.remove(file); 240 } 241 } 242 scheduleRetry()243 private void scheduleRetry() { 244 synchronized (mLock) { 245 if (mPendingWrites.size() == 0) { 246 return; // Shouldn't happen but just in case. 247 } 248 249 mRetries++; 250 if (mRetries > MAX_RETRIES) { 251 doWtf("Gave up writing files: " + getKeysString(mPendingWrites)); 252 return; 253 } 254 255 mHandler.removeCallbacks(mHandleWriteOnHandlerRunnable); 256 mHandler.postDelayed(mHandleWriteOnHandlerRunnable, RETRY_INTERVAL_MS); 257 } 258 } 259 260 /** 261 * Make sure {@link #mDefaultValues} has the default value loaded for {@code file}. 262 * 263 * @return true if the default value is loaded. false if the file cannot be read. 264 */ ensureDefaultLoaded(String file)265 private boolean ensureDefaultLoaded(String file) { 266 // Has the default already? 267 synchronized (mLock) { 268 if (mDefaultValues.containsKey(file)) { 269 return true; 270 } 271 } 272 final String originalValue; 273 try { 274 originalValue = injectReadFromFileTrimmed(file); 275 } catch (IOException e) { 276 // If the file is not readable, assume can't write too. 277 injectWtf("Unable to read from file", e); 278 279 removePendingWrite(file); 280 return false; 281 } 282 synchronized (mLock) { 283 mDefaultValues.put(file, originalValue); 284 saveDefaultValuesLocked(); 285 } 286 return true; 287 } 288 289 @VisibleForTesting injectReadFromFileTrimmed(String file)290 String injectReadFromFileTrimmed(String file) throws IOException { 291 return IoUtils.readFileAsString(file).trim(); 292 } 293 294 @VisibleForTesting injectWriteToFile(String file, String value)295 void injectWriteToFile(String file, String value) throws IOException { 296 if (injectShouldSkipWrite()) { 297 Slog.i(TAG, "Skipped writing to '" + file + "'"); 298 return; 299 } 300 if (DEBUG) { 301 Slog.d(TAG, "Writing: '" + value + "' to '" + file + "'"); 302 } 303 try (FileWriter out = new FileWriter(file)) { 304 out.write(value); 305 } catch (IOException | RuntimeException e) { 306 Slog.w(TAG, "Failed writing '" + value + "' to '" + file + "': " + e.getMessage()); 307 throw e; 308 } 309 } 310 311 @GuardedBy("mLock") saveDefaultValuesLocked()312 private void saveDefaultValuesLocked() { 313 final AtomicFile file = new AtomicFile(injectDefaultValuesFilename()); 314 315 FileOutputStream outs = null; 316 try { 317 file.getBaseFile().getParentFile().mkdirs(); 318 outs = file.startWrite(); 319 320 // Write to XML 321 TypedXmlSerializer out = Xml.resolveSerializer(outs); 322 out.startDocument(null, true); 323 out.startTag(null, TAG_DEFAULT_ROOT); 324 325 XmlUtils.writeMapXml(mDefaultValues, out, null); 326 327 // Epilogue. 328 out.endTag(null, TAG_DEFAULT_ROOT); 329 out.endDocument(); 330 331 // Close. 332 file.finishWrite(outs); 333 } catch (IOException | XmlPullParserException | RuntimeException e) { 334 Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e); 335 file.failWrite(outs); 336 } 337 } 338 339 @GuardedBy("mLock") 340 @VisibleForTesting loadDefaultValuesLocked()341 boolean loadDefaultValuesLocked() { 342 final AtomicFile file = new AtomicFile(injectDefaultValuesFilename()); 343 if (DEBUG) { 344 Slog.d(TAG, "Loading from " + file.getBaseFile()); 345 } 346 Map<String, String> read = null; 347 try (FileInputStream in = file.openRead()) { 348 TypedXmlPullParser parser = Xml.resolvePullParser(in); 349 350 int type; 351 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { 352 if (type != XmlPullParser.START_TAG) { 353 continue; 354 } 355 final int depth = parser.getDepth(); 356 // Check the root tag 357 final String tag = parser.getName(); 358 if (depth == 1) { 359 if (!TAG_DEFAULT_ROOT.equals(tag)) { 360 Slog.e(TAG, "Invalid root tag: " + tag); 361 return false; 362 } 363 continue; 364 } 365 final String[] tagName = new String[1]; 366 read = (ArrayMap<String, String>) XmlUtils.readThisArrayMapXml(parser, 367 TAG_DEFAULT_ROOT, tagName, null); 368 } 369 } catch (FileNotFoundException e) { 370 read = null; 371 } catch (IOException | XmlPullParserException | RuntimeException e) { 372 Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e); 373 } 374 if (read != null) { 375 mDefaultValues.clear(); 376 mDefaultValues.putAll(read); 377 return true; 378 } 379 return false; 380 } 381 doWtf(String message)382 private void doWtf(String message) { 383 injectWtf(message, null); 384 } 385 386 @VisibleForTesting injectWtf(String message, Throwable e)387 void injectWtf(String message, Throwable e) { 388 Slog.wtf(TAG, message, e); 389 } 390 injectDefaultValuesFilename()391 File injectDefaultValuesFilename() { 392 final File dir = new File(Environment.getDataSystemDirectory(), "battery-saver"); 393 dir.mkdirs(); 394 return new File(dir, "default-values.xml"); 395 } 396 397 @VisibleForTesting injectShouldSkipWrite()398 boolean injectShouldSkipWrite() { 399 return SystemProperties.getBoolean(PROP_SKIP_WRITE, false); 400 } 401 402 @VisibleForTesting getDefaultValuesForTest()403 ArrayMap<String, String> getDefaultValuesForTest() { 404 return mDefaultValues; 405 } 406 } 407