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