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.Xml; 27 28 import com.android.internal.annotations.GuardedBy; 29 import com.android.internal.annotations.VisibleForTesting; 30 import com.android.internal.util.FastXmlSerializer; 31 import com.android.internal.util.XmlUtils; 32 import com.android.server.IoThread; 33 34 import libcore.io.IoUtils; 35 36 import org.xmlpull.v1.XmlPullParser; 37 import org.xmlpull.v1.XmlPullParserException; 38 import org.xmlpull.v1.XmlSerializer; 39 40 import java.io.File; 41 import java.io.FileInputStream; 42 import java.io.FileNotFoundException; 43 import java.io.FileOutputStream; 44 import java.io.FileWriter; 45 import java.io.IOException; 46 import java.nio.charset.StandardCharsets; 47 import java.util.ArrayList; 48 import java.util.Map; 49 50 /** 51 * Used by {@link BatterySaverController} to write values to /sys/ (and possibly /proc/ too) files 52 * with retries. It also support restoring to the file original values. 53 * 54 * Retries are needed because writing to "/sys/.../scaling_max_freq" returns EIO when the current 55 * frequency happens to be above the new max frequency. 56 * 57 * Test: 58 atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/power/batterysaver/FileUpdaterTest.java 59 */ 60 public class FileUpdater { 61 private static final String TAG = BatterySaverController.TAG; 62 63 private static final boolean DEBUG = BatterySaverController.DEBUG; 64 65 /** 66 * If this system property is set to 1, it'll skip all file writes. This can be used when 67 * one needs to change max CPU frequency for benchmarking, for example. 68 */ 69 private static final String PROP_SKIP_WRITE = "debug.batterysaver.no_write_files"; 70 71 private static final String TAG_DEFAULT_ROOT = "defaults"; 72 73 // Don't do disk access with this lock held. 74 private final Object mLock = new Object(); 75 76 private final Context mContext; 77 78 private final Handler mHandler; 79 80 /** 81 * Filename -> value map that holds pending writes. 82 */ 83 @GuardedBy("mLock") 84 private final ArrayMap<String, String> mPendingWrites = new ArrayMap<>(); 85 86 /** 87 * Filename -> value that holds the original value of each file. 88 */ 89 @GuardedBy("mLock") 90 private final ArrayMap<String, String> mDefaultValues = new ArrayMap<>(); 91 92 /** Number of retries. We give up on writing after {@link #MAX_RETRIES} retries. */ 93 @GuardedBy("mLock") 94 private int mRetries = 0; 95 96 private final int MAX_RETRIES; 97 98 private final long RETRY_INTERVAL_MS; 99 100 /** 101 * "Official" constructor. Don't use the other constructor in the production code. 102 */ FileUpdater(Context context)103 public FileUpdater(Context context) { 104 this(context, IoThread.get().getLooper(), 10, 5000); 105 } 106 107 /** 108 * Constructor for test. 109 */ 110 @VisibleForTesting FileUpdater(Context context, Looper looper, int maxRetries, int retryIntervalMs)111 FileUpdater(Context context, Looper looper, int maxRetries, int retryIntervalMs) { 112 mContext = context; 113 mHandler = new Handler(looper); 114 115 MAX_RETRIES = maxRetries; 116 RETRY_INTERVAL_MS = retryIntervalMs; 117 } 118 systemReady(boolean runtimeRestarted)119 public void systemReady(boolean runtimeRestarted) { 120 synchronized (mLock) { 121 if (runtimeRestarted) { 122 // If it runtime restarted, read the original values from the disk and apply. 123 if (loadDefaultValuesLocked()) { 124 Slog.d(TAG, "Default values loaded after runtime restart; writing them..."); 125 restoreDefault(); 126 } 127 } else { 128 // Delete it, without checking the result. (file-not-exist is not an exception.) 129 injectDefaultValuesFilename().delete(); 130 } 131 } 132 } 133 134 /** 135 * Write values to files. (Note the actual writes happen ASAP but asynchronously.) 136 */ writeFiles(ArrayMap<String, String> fileValues)137 public void writeFiles(ArrayMap<String, String> fileValues) { 138 synchronized (mLock) { 139 for (int i = fileValues.size() - 1; i >= 0; i--) { 140 final String file = fileValues.keyAt(i); 141 final String value = fileValues.valueAt(i); 142 143 if (DEBUG) { 144 Slog.d(TAG, "Scheduling write: '" + value + "' to '" + file + "'"); 145 } 146 147 mPendingWrites.put(file, value); 148 149 } 150 mRetries = 0; 151 152 mHandler.removeCallbacks(mHandleWriteOnHandlerRunnable); 153 mHandler.post(mHandleWriteOnHandlerRunnable); 154 } 155 } 156 157 /** 158 * Restore the default values. 159 */ restoreDefault()160 public void restoreDefault() { 161 synchronized (mLock) { 162 if (DEBUG) { 163 Slog.d(TAG, "Resetting file default values."); 164 } 165 mPendingWrites.clear(); 166 167 writeFiles(mDefaultValues); 168 } 169 } 170 171 private Runnable mHandleWriteOnHandlerRunnable = () -> handleWriteOnHandler(); 172 173 /** Convert map keys into a single string for debug messages. */ getKeysString(Map<String, String> source)174 private String getKeysString(Map<String, String> source) { 175 return new ArrayList<>(source.keySet()).toString(); 176 } 177 178 /** Clone an ArrayMap. */ cloneMap(ArrayMap<String, String> source)179 private ArrayMap<String, String> cloneMap(ArrayMap<String, String> source) { 180 return new ArrayMap<>(source); 181 } 182 183 /** 184 * Called on the handler and writes {@link #mPendingWrites} to the disk. 185 * 186 * When it about to write to each file for the first time, it'll read the file and store 187 * the original value in {@link #mDefaultValues}. 188 */ handleWriteOnHandler()189 private void handleWriteOnHandler() { 190 // We don't want to access the disk with the lock held, so copy the pending writes to 191 // a local map. 192 final ArrayMap<String, String> writes; 193 synchronized (mLock) { 194 if (mPendingWrites.size() == 0) { 195 return; 196 } 197 198 if (DEBUG) { 199 Slog.d(TAG, "Writing files: (# retries=" + mRetries + ") " + 200 getKeysString(mPendingWrites)); 201 } 202 203 writes = cloneMap(mPendingWrites); 204 } 205 206 // Then write. 207 208 boolean needRetry = false; 209 210 final int size = writes.size(); 211 for (int i = 0; i < size; i++) { 212 final String file = writes.keyAt(i); 213 final String value = writes.valueAt(i); 214 215 // Make sure the default value is loaded. 216 if (!ensureDefaultLoaded(file)) { 217 continue; 218 } 219 220 // Write to the file. When succeeded, remove it from the pending list. 221 // Otherwise, schedule a retry. 222 try { 223 injectWriteToFile(file, value); 224 225 removePendingWrite(file); 226 } catch (IOException e) { 227 needRetry = true; 228 } 229 } 230 if (needRetry) { 231 scheduleRetry(); 232 } 233 } 234 removePendingWrite(String file)235 private void removePendingWrite(String file) { 236 synchronized (mLock) { 237 mPendingWrites.remove(file); 238 } 239 } 240 scheduleRetry()241 private void scheduleRetry() { 242 synchronized (mLock) { 243 if (mPendingWrites.size() == 0) { 244 return; // Shouldn't happen but just in case. 245 } 246 247 mRetries++; 248 if (mRetries > MAX_RETRIES) { 249 doWtf("Gave up writing files: " + getKeysString(mPendingWrites)); 250 return; 251 } 252 253 mHandler.removeCallbacks(mHandleWriteOnHandlerRunnable); 254 mHandler.postDelayed(mHandleWriteOnHandlerRunnable, RETRY_INTERVAL_MS); 255 } 256 } 257 258 /** 259 * Make sure {@link #mDefaultValues} has the default value loaded for {@code file}. 260 * 261 * @return true if the default value is loaded. false if the file cannot be read. 262 */ ensureDefaultLoaded(String file)263 private boolean ensureDefaultLoaded(String file) { 264 // Has the default already? 265 synchronized (mLock) { 266 if (mDefaultValues.containsKey(file)) { 267 return true; 268 } 269 } 270 final String originalValue; 271 try { 272 originalValue = injectReadFromFileTrimmed(file); 273 } catch (IOException e) { 274 // If the file is not readable, assume can't write too. 275 injectWtf("Unable to read from file", e); 276 277 removePendingWrite(file); 278 return false; 279 } 280 synchronized (mLock) { 281 mDefaultValues.put(file, originalValue); 282 saveDefaultValuesLocked(); 283 } 284 return true; 285 } 286 287 @VisibleForTesting injectReadFromFileTrimmed(String file)288 String injectReadFromFileTrimmed(String file) throws IOException { 289 return IoUtils.readFileAsString(file).trim(); 290 } 291 292 @VisibleForTesting injectWriteToFile(String file, String value)293 void injectWriteToFile(String file, String value) throws IOException { 294 if (injectShouldSkipWrite()) { 295 Slog.i(TAG, "Skipped writing to '" + file + "'"); 296 return; 297 } 298 if (DEBUG) { 299 Slog.d(TAG, "Writing: '" + value + "' to '" + file + "'"); 300 } 301 try (FileWriter out = new FileWriter(file)) { 302 out.write(value); 303 } catch (IOException | RuntimeException e) { 304 Slog.w(TAG, "Failed writing '" + value + "' to '" + file + "': " + e.getMessage()); 305 throw e; 306 } 307 } 308 309 @GuardedBy("mLock") saveDefaultValuesLocked()310 private void saveDefaultValuesLocked() { 311 final AtomicFile file = new AtomicFile(injectDefaultValuesFilename()); 312 313 FileOutputStream outs = null; 314 try { 315 file.getBaseFile().getParentFile().mkdirs(); 316 outs = file.startWrite(); 317 318 // Write to XML 319 XmlSerializer out = new FastXmlSerializer(); 320 out.setOutput(outs, StandardCharsets.UTF_8.name()); 321 out.startDocument(null, true); 322 out.startTag(null, TAG_DEFAULT_ROOT); 323 324 XmlUtils.writeMapXml(mDefaultValues, out, null); 325 326 // Epilogue. 327 out.endTag(null, TAG_DEFAULT_ROOT); 328 out.endDocument(); 329 330 // Close. 331 file.finishWrite(outs); 332 } catch (IOException | XmlPullParserException | RuntimeException e) { 333 Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e); 334 file.failWrite(outs); 335 } 336 } 337 338 @GuardedBy("mLock") 339 @VisibleForTesting loadDefaultValuesLocked()340 boolean loadDefaultValuesLocked() { 341 final AtomicFile file = new AtomicFile(injectDefaultValuesFilename()); 342 if (DEBUG) { 343 Slog.d(TAG, "Loading from " + file.getBaseFile()); 344 } 345 Map<String, String> read = null; 346 try (FileInputStream in = file.openRead()) { 347 XmlPullParser parser = Xml.newPullParser(); 348 parser.setInput(in, StandardCharsets.UTF_8.name()); 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