• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
SettingsState(Object lock, File file, int key, int maxBytesPerAppPackage, Looper looper)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.
getVersionLocked()178     public int getVersionLocked() {
179         return mVersion;
180     }
181 
getNullSetting()182     public Setting getNullSetting() {
183         return mNullSetting;
184     }
185 
186     // The settings provider must hold its lock when calling here.
setVersionLocked(int version)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.
onPackageRemovedLocked(String packageName)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.
getSettingNamesLocked()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.
getSettingLocked(String name)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.
updateSettingLocked(String name, String value, String packageName)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.
insertSettingLocked(String name, String value, String packageName)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.
persistSyncLocked()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.
deleteSettingLocked(String name)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.
destroyLocked(Runnable callback)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 
addHistoricalOperationLocked(String type, Setting setting)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 
dumpHistoricalOperations(PrintWriter pw)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 
updateMemoryUsagePerPackageLocked(String packageName, String oldValue, String newValue)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 
hasSettingLocked(String name)395     private boolean hasSettingLocked(String name) {
396         return mSettings.indexOfKey(name) >= 0;
397     }
398 
scheduleWriteIfNeededLocked()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 
writeStateAsyncLocked()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 
doWriteState()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 
writeSingleSetting(int version, XmlSerializer serializer, String id, String name, String value, String packageName)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 
setValueAttribute(int version, XmlSerializer serializer, String value)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 
getValueAttribute(XmlPullParser parser)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 
readStateSyncLocked()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 
parseStateLocked(XmlPullParser parser)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 
parseSettingsLocked(XmlPullParser parser)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 
MyHandler(Looper looper)629         public MyHandler(Looper looper) {
630             super(looper);
631         }
632 
633         @Override
handleMessage(Message message)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 
HistoricalOperation(long timestamp, String operation, Setting setting)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 
Setting(Setting other)667         public Setting(Setting other) {
668             name = other.name;
669             value = other.value;
670             packageName = other.packageName;
671             id = other.id;
672         }
673 
Setting(String name, String value, String packageName)674         public Setting(String name, String value, String packageName) {
675             init(name, value, packageName, String.valueOf(mNextId++));
676         }
677 
Setting(String name, String value, String packageName, String id)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 
init(String name, String value, String packageName, String id)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 
getName()690         public String getName() {
691             return name;
692         }
693 
getkey()694         public int getkey() {
695             return mKey;
696         }
697 
getValue()698         public String getValue() {
699             return value;
700         }
701 
getPackageName()702         public String getPackageName() {
703             return packageName;
704         }
705 
getId()706         public String getId() {
707             return id;
708         }
709 
isNull()710         public boolean isNull() {
711             return false;
712         }
713 
update(String value, String packageName)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 
toString()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      */
isBinary(String s)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 
base64Encode(String s)748     private static String base64Encode(String s) {
749         return Base64.encodeToString(toBytes(s), Base64.NO_WRAP);
750     }
751 
base64Decode(String s)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 
toBytes(String s)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 
fromBytes(byte[] bytes)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