Home | History | Annotate | Download | only in settings
      1 /*
      2  * Copyright (C) 2015 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 
     17 package com.android.providers.settings;
     18 
     19 import android.os.Build;
     20 import android.os.Handler;
     21 import android.os.Looper;
     22 import android.os.Message;
     23 import android.os.SystemClock;
     24 import android.provider.Settings;
     25 import android.text.TextUtils;
     26 import android.util.ArrayMap;
     27 import android.util.AtomicFile;
     28 import android.util.Base64;
     29 import android.util.Slog;
     30 import android.util.TimeUtils;
     31 import android.util.Xml;
     32 import com.android.internal.annotations.GuardedBy;
     33 import libcore.io.IoUtils;
     34 import libcore.util.Objects;
     35 import org.xmlpull.v1.XmlPullParser;
     36 import org.xmlpull.v1.XmlPullParserException;
     37 import org.xmlpull.v1.XmlSerializer;
     38 
     39 import java.io.File;
     40 import java.io.FileInputStream;
     41 import java.io.FileNotFoundException;
     42 import java.io.FileOutputStream;
     43 import java.io.IOException;
     44 import java.io.PrintWriter;
     45 import java.nio.charset.StandardCharsets;
     46 import java.util.ArrayList;
     47 import java.util.List;
     48 
     49 /**
     50  * This class contains the state for one type of settings. It is responsible
     51  * for saving the state asynchronously to an XML file after a mutation and
     52  * loading the from an XML file on construction.
     53  * <p>
     54  * This class uses the same lock as the settings provider to ensure that
     55  * multiple changes made by the settings provider, e,g, upgrade, bulk insert,
     56  * etc, are atomically persisted since the asynchronous persistence is using
     57  * the same lock to grab the current state to write to disk.
     58  * </p>
     59  */
     60 final class SettingsState {
     61     private static final boolean DEBUG = false;
     62     private static final boolean DEBUG_PERSISTENCE = false;
     63 
     64     private static final String LOG_TAG = "SettingsState";
     65 
     66     static final int SETTINGS_VERSION_NEW_ENCODING = 121;
     67 
     68     private static final long WRITE_SETTINGS_DELAY_MILLIS = 200;
     69     private static final long MAX_WRITE_SETTINGS_DELAY_MILLIS = 2000;
     70 
     71     public static final int MAX_BYTES_PER_APP_PACKAGE_UNLIMITED = -1;
     72     public static final int MAX_BYTES_PER_APP_PACKAGE_LIMITED = 20000;
     73 
     74     public static final String SYSTEM_PACKAGE_NAME = "android";
     75 
     76     public static final int VERSION_UNDEFINED = -1;
     77 
     78     private static final String TAG_SETTINGS = "settings";
     79     private static final String TAG_SETTING = "setting";
     80     private static final String ATTR_PACKAGE = "package";
     81 
     82     private static final String ATTR_VERSION = "version";
     83     private static final String ATTR_ID = "id";
     84     private static final String ATTR_NAME = "name";
     85 
     86     /** Non-binary value will be written in this attribute. */
     87     private static final String ATTR_VALUE = "value";
     88 
     89     /**
     90      * KXmlSerializer won't like some characters.  We encode such characters in base64 and
     91      * store in this attribute.
     92      * NOTE: A null value will have NEITHER ATTR_VALUE nor ATTR_VALUE_BASE64.
     93      */
     94     private static final String ATTR_VALUE_BASE64 = "valueBase64";
     95 
     96     // This was used in version 120 and before.
     97     private static final String NULL_VALUE_OLD_STYLE = "null";
     98 
     99     private static final int HISTORICAL_OPERATION_COUNT = 20;
    100     private static final String HISTORICAL_OPERATION_UPDATE = "update";
    101     private static final String HISTORICAL_OPERATION_DELETE = "delete";
    102     private static final String HISTORICAL_OPERATION_PERSIST = "persist";
    103     private static final String HISTORICAL_OPERATION_INITIALIZE = "initialize";
    104 
    105     private final Object mLock;
    106 
    107     private final Handler mHandler;
    108 
    109     @GuardedBy("mLock")
    110     private final ArrayMap<String, Setting> mSettings = new ArrayMap<>();
    111 
    112     @GuardedBy("mLock")
    113     private final ArrayMap<String, Integer> mPackageToMemoryUsage;
    114 
    115     @GuardedBy("mLock")
    116     private final int mMaxBytesPerAppPackage;
    117 
    118     @GuardedBy("mLock")
    119     private final File mStatePersistFile;
    120 
    121     private final Setting mNullSetting = new Setting(null, null, null) {
    122         @Override
    123         public boolean isNull() {
    124             return true;
    125         }
    126     };
    127 
    128     @GuardedBy("mLock")
    129     private final List<HistoricalOperation> mHistoricalOperations;
    130 
    131     @GuardedBy("mLock")
    132     public final int mKey;
    133 
    134     @GuardedBy("mLock")
    135     private int mVersion = VERSION_UNDEFINED;
    136 
    137     @GuardedBy("mLock")
    138     private long mLastNotWrittenMutationTimeMillis;
    139 
    140     @GuardedBy("mLock")
    141     private boolean mDirty;
    142 
    143     @GuardedBy("mLock")
    144     private boolean mWriteScheduled;
    145 
    146     @GuardedBy("mLock")
    147     private long mNextId;
    148 
    149     @GuardedBy("mLock")
    150     private int mNextHistoricalOpIdx;
    151 
    152     public SettingsState(Object lock, File file, int key, int maxBytesPerAppPackage,
    153             Looper looper) {
    154         // It is important that we use the same lock as the settings provider
    155         // to ensure multiple mutations on this state are atomicaly persisted
    156         // as the async persistence should be blocked while we make changes.
    157         mLock = lock;
    158         mStatePersistFile = file;
    159         mKey = key;
    160         mHandler = new MyHandler(looper);
    161         if (maxBytesPerAppPackage == MAX_BYTES_PER_APP_PACKAGE_LIMITED) {
    162             mMaxBytesPerAppPackage = maxBytesPerAppPackage;
    163             mPackageToMemoryUsage = new ArrayMap<>();
    164         } else {
    165             mMaxBytesPerAppPackage = maxBytesPerAppPackage;
    166             mPackageToMemoryUsage = null;
    167         }
    168 
    169         mHistoricalOperations = Build.IS_DEBUGGABLE
    170                 ? new ArrayList<>(HISTORICAL_OPERATION_COUNT) : null;
    171 
    172         synchronized (mLock) {
    173             readStateSyncLocked();
    174         }
    175     }
    176 
    177     // The settings provider must hold its lock when calling here.
    178     public int getVersionLocked() {
    179         return mVersion;
    180     }
    181 
    182     public Setting getNullSetting() {
    183         return mNullSetting;
    184     }
    185 
    186     // The settings provider must hold its lock when calling here.
    187     public void setVersionLocked(int version) {
    188         if (version == mVersion) {
    189             return;
    190         }
    191         mVersion = version;
    192 
    193         scheduleWriteIfNeededLocked();
    194     }
    195 
    196     // The settings provider must hold its lock when calling here.
    197     public void onPackageRemovedLocked(String packageName) {
    198         boolean removedSomething = false;
    199 
    200         final int settingCount = mSettings.size();
    201         for (int i = settingCount - 1; i >= 0; i--) {
    202             String name = mSettings.keyAt(i);
    203             // Settings defined by us are never dropped.
    204             if (Settings.System.PUBLIC_SETTINGS.contains(name)
    205                     || Settings.System.PRIVATE_SETTINGS.contains(name)) {
    206                 continue;
    207             }
    208             Setting setting = mSettings.valueAt(i);
    209             if (packageName.equals(setting.packageName)) {
    210                 mSettings.removeAt(i);
    211                 removedSomething = true;
    212             }
    213         }
    214 
    215         if (removedSomething) {
    216             scheduleWriteIfNeededLocked();
    217         }
    218     }
    219 
    220     // The settings provider must hold its lock when calling here.
    221     public List<String> getSettingNamesLocked() {
    222         ArrayList<String> names = new ArrayList<>();
    223         final int settingsCount = mSettings.size();
    224         for (int i = 0; i < settingsCount; i++) {
    225             String name = mSettings.keyAt(i);
    226             names.add(name);
    227         }
    228         return names;
    229     }
    230 
    231     // The settings provider must hold its lock when calling here.
    232     public Setting getSettingLocked(String name) {
    233         if (TextUtils.isEmpty(name)) {
    234             return mNullSetting;
    235         }
    236         Setting setting = mSettings.get(name);
    237         if (setting != null) {
    238             return new Setting(setting);
    239         }
    240         return mNullSetting;
    241     }
    242 
    243     // The settings provider must hold its lock when calling here.
    244     public boolean updateSettingLocked(String name, String value, String packageName) {
    245         if (!hasSettingLocked(name)) {
    246             return false;
    247         }
    248 
    249         return insertSettingLocked(name, value, packageName);
    250     }
    251 
    252     // The settings provider must hold its lock when calling here.
    253     public boolean insertSettingLocked(String name, String value, String packageName) {
    254         if (TextUtils.isEmpty(name)) {
    255             return false;
    256         }
    257 
    258         Setting oldState = mSettings.get(name);
    259         String oldValue = (oldState != null) ? oldState.value : null;
    260         Setting newState;
    261 
    262         if (oldState != null) {
    263             if (!oldState.update(value, packageName)) {
    264                 return false;
    265             }
    266             newState = oldState;
    267         } else {
    268             newState = new Setting(name, value, packageName);
    269             mSettings.put(name, newState);
    270         }
    271 
    272         addHistoricalOperationLocked(HISTORICAL_OPERATION_UPDATE, newState);
    273 
    274         updateMemoryUsagePerPackageLocked(packageName, oldValue, value);
    275 
    276         scheduleWriteIfNeededLocked();
    277 
    278         return true;
    279     }
    280 
    281     // The settings provider must hold its lock when calling here.
    282     public void persistSyncLocked() {
    283         mHandler.removeMessages(MyHandler.MSG_PERSIST_SETTINGS);
    284         doWriteState();
    285     }
    286 
    287     // The settings provider must hold its lock when calling here.
    288     public boolean deleteSettingLocked(String name) {
    289         if (TextUtils.isEmpty(name) || !hasSettingLocked(name)) {
    290             return false;
    291         }
    292 
    293         Setting oldState = mSettings.remove(name);
    294 
    295         updateMemoryUsagePerPackageLocked(oldState.packageName, oldState.value, null);
    296 
    297         addHistoricalOperationLocked(HISTORICAL_OPERATION_DELETE, oldState);
    298 
    299         scheduleWriteIfNeededLocked();
    300 
    301         return true;
    302     }
    303 
    304     // The settings provider must hold its lock when calling here.
    305     public void destroyLocked(Runnable callback) {
    306         mHandler.removeMessages(MyHandler.MSG_PERSIST_SETTINGS);
    307         if (callback != null) {
    308             if (mDirty) {
    309                 // Do it without a delay.
    310                 mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS,
    311                         callback).sendToTarget();
    312                 return;
    313             }
    314             callback.run();
    315         }
    316     }
    317 
    318     private void addHistoricalOperationLocked(String type, Setting setting) {
    319         if (mHistoricalOperations == null) {
    320             return;
    321         }
    322         HistoricalOperation operation = new HistoricalOperation(
    323                 SystemClock.elapsedRealtime(), type,
    324                 setting != null ? new Setting(setting) : null);
    325         if (mNextHistoricalOpIdx >= mHistoricalOperations.size()) {
    326             mHistoricalOperations.add(operation);
    327         } else {
    328             mHistoricalOperations.set(mNextHistoricalOpIdx, operation);
    329         }
    330         mNextHistoricalOpIdx++;
    331         if (mNextHistoricalOpIdx >= HISTORICAL_OPERATION_COUNT) {
    332             mNextHistoricalOpIdx = 0;
    333         }
    334     }
    335 
    336     public void dumpHistoricalOperations(PrintWriter pw) {
    337         synchronized (mLock) {
    338             if (mHistoricalOperations == null) {
    339                 return;
    340             }
    341             pw.println("Historical operations");
    342             final int operationCount = mHistoricalOperations.size();
    343             for (int i = 0; i < operationCount; i++) {
    344                 int index = mNextHistoricalOpIdx - 1 - i;
    345                 if (index < 0) {
    346                     index = operationCount + index;
    347                 }
    348                 HistoricalOperation operation = mHistoricalOperations.get(index);
    349                 pw.print(TimeUtils.formatForLogging(operation.mTimestamp));
    350                 pw.print(" ");
    351                 pw.print(operation.mOperation);
    352                 if (operation.mSetting != null) {
    353                     pw.print("  ");
    354                     pw.print(operation.mSetting);
    355                 }
    356                 pw.println();
    357             }
    358             pw.println();
    359             pw.println();
    360         }
    361     }
    362 
    363     private void updateMemoryUsagePerPackageLocked(String packageName, String oldValue,
    364             String newValue) {
    365         if (mMaxBytesPerAppPackage == MAX_BYTES_PER_APP_PACKAGE_UNLIMITED) {
    366             return;
    367         }
    368 
    369         if (SYSTEM_PACKAGE_NAME.equals(packageName)) {
    370             return;
    371         }
    372 
    373         final int oldValueSize = (oldValue != null) ? oldValue.length() : 0;
    374         final int newValueSize = (newValue != null) ? newValue.length() : 0;
    375         final int deltaSize = newValueSize - oldValueSize;
    376 
    377         Integer currentSize = mPackageToMemoryUsage.get(packageName);
    378         final int newSize = Math.max((currentSize != null)
    379                 ? currentSize + deltaSize : deltaSize, 0);
    380 
    381         if (newSize > mMaxBytesPerAppPackage) {
    382             throw new IllegalStateException("You are adding too many system settings. "
    383                     + "You should stop using system settings for app specific data"
    384                     + " package: " + packageName);
    385         }
    386 
    387         if (DEBUG) {
    388             Slog.i(LOG_TAG, "Settings for package: " + packageName
    389                     + " size: " + newSize + " bytes.");
    390         }
    391 
    392         mPackageToMemoryUsage.put(packageName, newSize);
    393     }
    394 
    395     private boolean hasSettingLocked(String name) {
    396         return mSettings.indexOfKey(name) >= 0;
    397     }
    398 
    399     private void scheduleWriteIfNeededLocked() {
    400         // If dirty then we have a write already scheduled.
    401         if (!mDirty) {
    402             mDirty = true;
    403             writeStateAsyncLocked();
    404         }
    405     }
    406 
    407     private void writeStateAsyncLocked() {
    408         final long currentTimeMillis = SystemClock.uptimeMillis();
    409 
    410         if (mWriteScheduled) {
    411             mHandler.removeMessages(MyHandler.MSG_PERSIST_SETTINGS);
    412 
    413             // If enough time passed, write without holding off anymore.
    414             final long timeSinceLastNotWrittenMutationMillis = currentTimeMillis
    415                     - mLastNotWrittenMutationTimeMillis;
    416             if (timeSinceLastNotWrittenMutationMillis >= MAX_WRITE_SETTINGS_DELAY_MILLIS) {
    417                 mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS).sendToTarget();
    418                 return;
    419             }
    420 
    421             // Hold off a bit more as settings are frequently changing.
    422             final long maxDelayMillis = Math.max(mLastNotWrittenMutationTimeMillis
    423                     + MAX_WRITE_SETTINGS_DELAY_MILLIS - currentTimeMillis, 0);
    424             final long writeDelayMillis = Math.min(WRITE_SETTINGS_DELAY_MILLIS, maxDelayMillis);
    425 
    426             Message message = mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS);
    427             mHandler.sendMessageDelayed(message, writeDelayMillis);
    428         } else {
    429             mLastNotWrittenMutationTimeMillis = currentTimeMillis;
    430             Message message = mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS);
    431             mHandler.sendMessageDelayed(message, WRITE_SETTINGS_DELAY_MILLIS);
    432             mWriteScheduled = true;
    433         }
    434     }
    435 
    436     private void doWriteState() {
    437         if (DEBUG_PERSISTENCE) {
    438             Slog.i(LOG_TAG, "[PERSIST START]");
    439         }
    440 
    441         AtomicFile destination = new AtomicFile(mStatePersistFile);
    442 
    443         final int version;
    444         final ArrayMap<String, Setting> settings;
    445 
    446         synchronized (mLock) {
    447             version = mVersion;
    448             settings = new ArrayMap<>(mSettings);
    449             mDirty = false;
    450             mWriteScheduled = false;
    451         }
    452 
    453         FileOutputStream out = null;
    454         try {
    455             out = destination.startWrite();
    456 
    457             XmlSerializer serializer = Xml.newSerializer();
    458             serializer.setOutput(out, StandardCharsets.UTF_8.name());
    459             serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
    460             serializer.startDocument(null, true);
    461             serializer.startTag(null, TAG_SETTINGS);
    462             serializer.attribute(null, ATTR_VERSION, String.valueOf(version));
    463 
    464             final int settingCount = settings.size();
    465             for (int i = 0; i < settingCount; i++) {
    466                 Setting setting = settings.valueAt(i);
    467 
    468                 writeSingleSetting(mVersion, serializer, setting.getId(), setting.getName(),
    469                         setting.getValue(), setting.getPackageName());
    470 
    471                 if (DEBUG_PERSISTENCE) {
    472                     Slog.i(LOG_TAG, "[PERSISTED]" + setting.getName() + "=" + setting.getValue());
    473                 }
    474             }
    475 
    476             serializer.endTag(null, TAG_SETTINGS);
    477             serializer.endDocument();
    478             destination.finishWrite(out);
    479 
    480             synchronized (mLock) {
    481                 addHistoricalOperationLocked(HISTORICAL_OPERATION_PERSIST, null);
    482             }
    483 
    484             if (DEBUG_PERSISTENCE) {
    485                 Slog.i(LOG_TAG, "[PERSIST END]");
    486             }
    487         } catch (Throwable t) {
    488             Slog.wtf(LOG_TAG, "Failed to write settings, restoring backup", t);
    489             destination.failWrite(out);
    490         } finally {
    491             IoUtils.closeQuietly(out);
    492         }
    493     }
    494 
    495     static void writeSingleSetting(int version, XmlSerializer serializer, String id,
    496             String name, String value, String packageName) throws IOException {
    497         if (id == null || isBinary(id) || name == null || isBinary(name)
    498                 || packageName == null || isBinary(packageName)) {
    499             // This shouldn't happen.
    500             return;
    501         }
    502         serializer.startTag(null, TAG_SETTING);
    503         serializer.attribute(null, ATTR_ID, id);
    504         serializer.attribute(null, ATTR_NAME, name);
    505         setValueAttribute(version, serializer, value);
    506         serializer.attribute(null, ATTR_PACKAGE, packageName);
    507         serializer.endTag(null, TAG_SETTING);
    508     }
    509 
    510     static void setValueAttribute(int version, XmlSerializer serializer, String value)
    511             throws IOException {
    512         if (version >= SETTINGS_VERSION_NEW_ENCODING) {
    513             if (value == null) {
    514                 // Null value -> No ATTR_VALUE nor ATTR_VALUE_BASE64.
    515             } else if (isBinary(value)) {
    516                 serializer.attribute(null, ATTR_VALUE_BASE64, base64Encode(value));
    517             } else {
    518                 serializer.attribute(null, ATTR_VALUE, value);
    519             }
    520         } else {
    521             // Old encoding.
    522             if (value == null) {
    523                 serializer.attribute(null, ATTR_VALUE, NULL_VALUE_OLD_STYLE);
    524             } else {
    525                 serializer.attribute(null, ATTR_VALUE, value);
    526             }
    527         }
    528     }
    529 
    530     private String getValueAttribute(XmlPullParser parser) {
    531         if (mVersion >= SETTINGS_VERSION_NEW_ENCODING) {
    532             final String value = parser.getAttributeValue(null, ATTR_VALUE);
    533             if (value != null) {
    534                 return value;
    535             }
    536             final String base64 = parser.getAttributeValue(null, ATTR_VALUE_BASE64);
    537             if (base64 != null) {
    538                 return base64Decode(base64);
    539             }
    540             // null has neither ATTR_VALUE nor ATTR_VALUE_BASE64.
    541             return null;
    542         } else {
    543             // Old encoding.
    544             final String stored = parser.getAttributeValue(null, ATTR_VALUE);
    545             if (NULL_VALUE_OLD_STYLE.equals(stored)) {
    546                 return null;
    547             } else {
    548                 return stored;
    549             }
    550         }
    551     }
    552 
    553     private void readStateSyncLocked() {
    554         FileInputStream in;
    555         if (!mStatePersistFile.exists()) {
    556             Slog.i(LOG_TAG, "No settings state " + mStatePersistFile);
    557             addHistoricalOperationLocked(HISTORICAL_OPERATION_INITIALIZE, null);
    558             return;
    559         }
    560         try {
    561             in = new AtomicFile(mStatePersistFile).openRead();
    562         } catch (FileNotFoundException fnfe) {
    563             String message = "No settings state " + mStatePersistFile;
    564             Slog.wtf(LOG_TAG, message);
    565             Slog.i(LOG_TAG, message);
    566             return;
    567         }
    568         try {
    569             XmlPullParser parser = Xml.newPullParser();
    570             parser.setInput(in, StandardCharsets.UTF_8.name());
    571             parseStateLocked(parser);
    572         } catch (XmlPullParserException | IOException e) {
    573             String message = "Failed parsing settings file: " + mStatePersistFile;
    574             Slog.wtf(LOG_TAG, message);
    575             throw new IllegalStateException(message , e);
    576         } finally {
    577             IoUtils.closeQuietly(in);
    578         }
    579     }
    580 
    581     private void parseStateLocked(XmlPullParser parser)
    582             throws IOException, XmlPullParserException {
    583         final int outerDepth = parser.getDepth();
    584         int type;
    585         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
    586                 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
    587             if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
    588                 continue;
    589             }
    590 
    591             String tagName = parser.getName();
    592             if (tagName.equals(TAG_SETTINGS)) {
    593                 parseSettingsLocked(parser);
    594             }
    595         }
    596     }
    597 
    598     private void parseSettingsLocked(XmlPullParser parser)
    599             throws IOException, XmlPullParserException {
    600 
    601         mVersion = Integer.parseInt(parser.getAttributeValue(null, ATTR_VERSION));
    602 
    603         final int outerDepth = parser.getDepth();
    604         int type;
    605         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
    606                 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
    607             if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
    608                 continue;
    609             }
    610 
    611             String tagName = parser.getName();
    612             if (tagName.equals(TAG_SETTING)) {
    613                 String id = parser.getAttributeValue(null, ATTR_ID);
    614                 String name = parser.getAttributeValue(null, ATTR_NAME);
    615                 String value = getValueAttribute(parser);
    616                 String packageName = parser.getAttributeValue(null, ATTR_PACKAGE);
    617                 mSettings.put(name, new Setting(name, value, packageName, id));
    618 
    619                 if (DEBUG_PERSISTENCE) {
    620                     Slog.i(LOG_TAG, "[RESTORED] " + name + "=" + value);
    621                 }
    622             }
    623         }
    624     }
    625 
    626     private final class MyHandler extends Handler {
    627         public static final int MSG_PERSIST_SETTINGS = 1;
    628 
    629         public MyHandler(Looper looper) {
    630             super(looper);
    631         }
    632 
    633         @Override
    634         public void handleMessage(Message message) {
    635             switch (message.what) {
    636                 case MSG_PERSIST_SETTINGS: {
    637                     Runnable callback = (Runnable) message.obj;
    638                     doWriteState();
    639                     if (callback != null) {
    640                         callback.run();
    641                     }
    642                 }
    643                 break;
    644             }
    645         }
    646     }
    647 
    648     private class HistoricalOperation {
    649         final long mTimestamp;
    650         final String mOperation;
    651         final Setting mSetting;
    652 
    653         public HistoricalOperation(long timestamp,
    654                 String operation, Setting setting) {
    655             mTimestamp = timestamp;
    656             mOperation = operation;
    657             mSetting = setting;
    658         }
    659     }
    660 
    661     class Setting {
    662         private String name;
    663         private String value;
    664         private String packageName;
    665         private String id;
    666 
    667         public Setting(Setting other) {
    668             name = other.name;
    669             value = other.value;
    670             packageName = other.packageName;
    671             id = other.id;
    672         }
    673 
    674         public Setting(String name, String value, String packageName) {
    675             init(name, value, packageName, String.valueOf(mNextId++));
    676         }
    677 
    678         public Setting(String name, String value, String packageName, String id) {
    679             mNextId = Math.max(mNextId, Long.valueOf(id) + 1);
    680             init(name, value, packageName, id);
    681         }
    682 
    683         private void init(String name, String value, String packageName, String id) {
    684             this.name = name;
    685             this.value = value;
    686             this.packageName = packageName;
    687             this.id = id;
    688         }
    689 
    690         public String getName() {
    691             return name;
    692         }
    693 
    694         public int getkey() {
    695             return mKey;
    696         }
    697 
    698         public String getValue() {
    699             return value;
    700         }
    701 
    702         public String getPackageName() {
    703             return packageName;
    704         }
    705 
    706         public String getId() {
    707             return id;
    708         }
    709 
    710         public boolean isNull() {
    711             return false;
    712         }
    713 
    714         public boolean update(String value, String packageName) {
    715             if (Objects.equal(value, this.value)) {
    716                 return false;
    717             }
    718             this.value = value;
    719             this.packageName = packageName;
    720             this.id = String.valueOf(mNextId++);
    721             return true;
    722         }
    723 
    724         public String toString() {
    725             return "Setting{name=" + value + " from " + packageName + "}";
    726         }
    727     }
    728 
    729     /**
    730      * @return TRUE if a string is considered "binary" from KXML's point of view.  NOTE DO NOT
    731      * pass null.
    732      */
    733     public static boolean isBinary(String s) {
    734         if (s == null) {
    735             throw new NullPointerException();
    736         }
    737         // See KXmlSerializer.writeEscaped
    738         for (int i = 0; i < s.length(); i++) {
    739             char c = s.charAt(i);
    740             boolean allowedInXml = (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd);
    741             if (!allowedInXml) {
    742                 return true;
    743             }
    744         }
    745         return false;
    746     }
    747 
    748     private static String base64Encode(String s) {
    749         return Base64.encodeToString(toBytes(s), Base64.NO_WRAP);
    750     }
    751 
    752     private static String base64Decode(String s) {
    753         return fromBytes(Base64.decode(s, Base64.DEFAULT));
    754     }
    755 
    756     // Note the followings are basically just UTF-16 encode/decode.  But we want to preserve
    757     // contents as-is, even if it contains broken surrogate pairs, we do it by ourselves,
    758     // since I don't know how Charset would treat them.
    759 
    760     private static byte[] toBytes(String s) {
    761         final byte[] result = new byte[s.length() * 2];
    762         int resultIndex = 0;
    763         for (int i = 0; i < s.length(); ++i) {
    764             char ch = s.charAt(i);
    765             result[resultIndex++] = (byte) (ch >> 8);
    766             result[resultIndex++] = (byte) ch;
    767         }
    768         return result;
    769     }
    770 
    771     private static String fromBytes(byte[] bytes) {
    772         final StringBuffer sb = new StringBuffer(bytes.length / 2);
    773 
    774         final int last = bytes.length - 1;
    775 
    776         for (int i = 0; i < last; i += 2) {
    777             final char ch = (char) ((bytes[i] & 0xff) << 8 | (bytes[i + 1] & 0xff));
    778             sb.append(ch);
    779         }
    780         return sb.toString();
    781     }
    782 }
    783