• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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