Home | History | Annotate | Download | only in batterysaver
      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      */
    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