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