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 */
16package com.android.server.power.batterysaver;
17
18import android.content.Context;
19import android.os.Environment;
20import android.os.Handler;
21import android.os.Looper;
22import android.os.SystemProperties;
23import android.util.ArrayMap;
24import android.util.AtomicFile;
25import android.util.Slog;
26import android.util.Xml;
27
28import com.android.internal.annotations.GuardedBy;
29import com.android.internal.annotations.VisibleForTesting;
30import com.android.internal.util.FastXmlSerializer;
31import com.android.internal.util.XmlUtils;
32import com.android.server.IoThread;
33
34import libcore.io.IoUtils;
35
36import org.xmlpull.v1.XmlPullParser;
37import org.xmlpull.v1.XmlPullParserException;
38import org.xmlpull.v1.XmlSerializer;
39
40import java.io.File;
41import java.io.FileInputStream;
42import java.io.FileNotFoundException;
43import java.io.FileOutputStream;
44import java.io.FileWriter;
45import java.io.IOException;
46import java.nio.charset.StandardCharsets;
47import java.util.ArrayList;
48import 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 */
60public 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     */
103    public FileUpdater(Context context) {
104        this(context, IoThread.get().getLooper(), 10, 5000);
105    }
106
107    /**
108     * Constructor for test.
109     */
110    @VisibleForTesting
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
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     */
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     */
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. */
174    private String getKeysString(Map<String, String> source) {
175        return new ArrayList<>(source.keySet()).toString();
176    }
177
178    /** Clone an ArrayMap. */
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     */
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
235    private void removePendingWrite(String file) {
236        synchronized (mLock) {
237            mPendingWrites.remove(file);
238        }
239    }
240
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     */
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
288    String injectReadFromFileTrimmed(String file) throws IOException {
289        return IoUtils.readFileAsString(file).trim();
290    }
291
292    @VisibleForTesting
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")
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
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
382    private void doWtf(String message) {
383        injectWtf(message, null);
384    }
385
386    @VisibleForTesting
387    void injectWtf(String message, Throwable e) {
388        Slog.wtf(TAG, message, e);
389    }
390
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
398    boolean injectShouldSkipWrite() {
399        return SystemProperties.getBoolean(PROP_SKIP_WRITE, false);
400    }
401
402    @VisibleForTesting
403    ArrayMap<String, String> getDefaultValuesForTest() {
404        return mDefaultValues;
405    }
406}
407