SettingsState.java revision 2849465ee19febd5135cb6ab8cb548a3c8ac6a24
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.Slog;
27import android.util.Xml;
28import com.android.internal.annotations.GuardedBy;
29import com.android.internal.os.BackgroundThread;
30import libcore.io.IoUtils;
31import libcore.util.Objects;
32import org.xmlpull.v1.XmlPullParser;
33import org.xmlpull.v1.XmlPullParserException;
34import org.xmlpull.v1.XmlSerializer;
35
36import java.io.File;
37import java.io.FileInputStream;
38import java.io.FileNotFoundException;
39import java.io.FileOutputStream;
40import java.io.IOException;
41import java.util.ArrayList;
42import java.util.List;
43
44/**
45 * This class contains the state for one type of settings. It is responsible
46 * for saving the state asynchronously to an XML file after a mutation and
47 * loading the from an XML file on construction.
48 * <p>
49 * This class uses the same lock as the settings provider to ensure that
50 * multiple changes made by the settings provider, e,g, upgrade, bulk insert,
51 * etc, are atomically persisted since the asynchronous persistence is using
52 * the same lock to grab the current state to write to disk.
53 * </p>
54 */
55final class SettingsState {
56    private static final boolean DEBUG = false;
57    private static final boolean DEBUG_PERSISTENCE = false;
58
59    private static final String LOG_TAG = "SettingsState";
60
61    private static final long WRITE_SETTINGS_DELAY_MILLIS = 200;
62    private static final long MAX_WRITE_SETTINGS_DELAY_MILLIS = 2000;
63
64    public static final int MAX_BYTES_PER_APP_PACKAGE_UNLIMITED = -1;
65    public static final int MAX_BYTES_PER_APP_PACKAGE_LIMITED = 20000;
66
67    public static final String SYSTEM_PACKAGE_NAME = "android";
68
69    public static final int VERSION_UNDEFINED = -1;
70
71    private static final String TAG_SETTINGS = "settings";
72    private static final String TAG_SETTING = "setting";
73    private static final String ATTR_PACKAGE = "package";
74
75    private static final String ATTR_VERSION = "version";
76    private static final String ATTR_ID = "id";
77    private static final String ATTR_NAME = "name";
78    private static final String ATTR_VALUE = "value";
79
80    private static final String NULL_VALUE = "null";
81
82    private final Object mLock;
83
84    private final Handler mHandler = new MyHandler();
85
86    @GuardedBy("mLock")
87    private final ArrayMap<String, Setting> mSettings = new ArrayMap<>();
88
89    @GuardedBy("mLock")
90    private final ArrayMap<String, Integer> mPackageToMemoryUsage;
91
92    @GuardedBy("mLock")
93    private final int mMaxBytesPerAppPackage;
94
95    @GuardedBy("mLock")
96    private final File mStatePersistFile;
97
98    public final int mKey;
99
100    @GuardedBy("mLock")
101    private int mVersion = VERSION_UNDEFINED;
102
103    @GuardedBy("mLock")
104    private long mLastNotWrittenMutationTimeMillis;
105
106    @GuardedBy("mLock")
107    private boolean mDirty;
108
109    @GuardedBy("mLock")
110    private boolean mWriteScheduled;
111
112    public SettingsState(Object lock, File file, int key, int maxBytesPerAppPackage) {
113        // It is important that we use the same lock as the settings provider
114        // to ensure multiple mutations on this state are atomicaly persisted
115        // as the async persistence should be blocked while we make changes.
116        mLock = lock;
117        mStatePersistFile = file;
118        mKey = key;
119        if (maxBytesPerAppPackage == MAX_BYTES_PER_APP_PACKAGE_LIMITED) {
120            mMaxBytesPerAppPackage = maxBytesPerAppPackage;
121            mPackageToMemoryUsage = new ArrayMap<>();
122        } else {
123            mMaxBytesPerAppPackage = maxBytesPerAppPackage;
124            mPackageToMemoryUsage = null;
125        }
126        synchronized (mLock) {
127            readStateSyncLocked();
128        }
129    }
130
131    // The settings provider must hold its lock when calling here.
132    public int getVersionLocked() {
133        return mVersion;
134    }
135
136    // The settings provider must hold its lock when calling here.
137    public void setVersionLocked(int version) {
138        if (version == mVersion) {
139            return;
140        }
141        mVersion = version;
142
143        scheduleWriteIfNeededLocked();
144    }
145
146    // The settings provider must hold its lock when calling here.
147    public void onPackageRemovedLocked(String packageName) {
148        boolean removedSomething = false;
149
150        final int settingCount = mSettings.size();
151        for (int i = settingCount - 1; i >= 0; i--) {
152            String name = mSettings.keyAt(i);
153            // Settings defined by use are never dropped.
154            if (Settings.System.PUBLIC_SETTINGS.contains(name)
155                    || Settings.System.PRIVATE_SETTINGS.contains(name)) {
156                continue;
157            }
158            Setting setting = mSettings.valueAt(i);
159            if (packageName.equals(setting.packageName)) {
160                mSettings.removeAt(i);
161                removedSomething = true;
162            }
163        }
164
165        if (removedSomething) {
166            scheduleWriteIfNeededLocked();
167        }
168    }
169
170    // The settings provider must hold its lock when calling here.
171    public List<String> getSettingNamesLocked() {
172        ArrayList<String> names = new ArrayList<>();
173        final int settingsCount = mSettings.size();
174        for (int i = 0; i < settingsCount; i++) {
175            String name = mSettings.keyAt(i);
176            names.add(name);
177        }
178        return names;
179    }
180
181    // The settings provider must hold its lock when calling here.
182    public Setting getSettingLocked(String name) {
183        if (TextUtils.isEmpty(name)) {
184            return null;
185        }
186        return mSettings.get(name);
187    }
188
189    // The settings provider must hold its lock when calling here.
190    public boolean updateSettingLocked(String name, String value, String packageName) {
191        if (!hasSettingLocked(name)) {
192            return false;
193        }
194
195        return insertSettingLocked(name, value, packageName);
196    }
197
198    // The settings provider must hold its lock when calling here.
199    public boolean insertSettingLocked(String name, String value, String packageName) {
200        if (TextUtils.isEmpty(name)) {
201            return false;
202        }
203
204        Setting oldState = mSettings.get(name);
205        String oldValue = (oldState != null) ? oldState.value : null;
206
207        if (oldState != null) {
208            if (!oldState.update(value, packageName)) {
209                return false;
210            }
211        } else {
212            Setting state = new Setting(name, value, packageName);
213            mSettings.put(name, state);
214        }
215
216        updateMemoryUsagePerPackageLocked(packageName, oldValue, value);
217
218        scheduleWriteIfNeededLocked();
219
220        return true;
221    }
222
223    // The settings provider must hold its lock when calling here.
224    public void persistSyncLocked() {
225        mHandler.removeMessages(MyHandler.MSG_PERSIST_SETTINGS);
226        doWriteState();
227    }
228
229    // The settings provider must hold its lock when calling here.
230    public boolean deleteSettingLocked(String name) {
231        if (TextUtils.isEmpty(name) || !hasSettingLocked(name)) {
232            return false;
233        }
234
235        Setting oldState = mSettings.remove(name);
236
237        updateMemoryUsagePerPackageLocked(oldState.packageName, oldState.value, null);
238
239        scheduleWriteIfNeededLocked();
240
241        return true;
242    }
243
244    // The settings provider must hold its lock when calling here.
245    public void destroyLocked(Runnable callback) {
246        mHandler.removeMessages(MyHandler.MSG_PERSIST_SETTINGS);
247        if (callback != null) {
248            if (mDirty) {
249                // Do it without a delay.
250                mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS,
251                        callback).sendToTarget();
252                return;
253            }
254            callback.run();
255        }
256    }
257
258    private void updateMemoryUsagePerPackageLocked(String packageName, String oldValue,
259            String newValue) {
260        if (mMaxBytesPerAppPackage == MAX_BYTES_PER_APP_PACKAGE_UNLIMITED) {
261            return;
262        }
263
264        if (SYSTEM_PACKAGE_NAME.equals(packageName)) {
265            return;
266        }
267
268        final int oldValueSize = (oldValue != null) ? oldValue.length() : 0;
269        final int newValueSize = (newValue != null) ? newValue.length() : 0;
270        final int deltaSize = newValueSize - oldValueSize;
271
272        Integer currentSize = mPackageToMemoryUsage.get(packageName);
273        final int newSize = Math.max((currentSize != null)
274                ? currentSize + deltaSize : deltaSize, 0);
275
276        if (newSize > mMaxBytesPerAppPackage) {
277            throw new IllegalStateException("You are adding too many system settings. "
278                    + "You should stop using system settings for app specific data"
279                    + " package: " + packageName);
280        }
281
282        if (DEBUG) {
283            Slog.i(LOG_TAG, "Settings for package: " + packageName
284                    + " size: " + newSize + " bytes.");
285        }
286
287        mPackageToMemoryUsage.put(packageName, newSize);
288    }
289
290    private boolean hasSettingLocked(String name) {
291        return mSettings.indexOfKey(name) >= 0;
292    }
293
294    private void scheduleWriteIfNeededLocked() {
295        // If dirty then we have a write already scheduled.
296        if (!mDirty) {
297            mDirty = true;
298            writeStateAsyncLocked();
299        }
300    }
301
302    private void writeStateAsyncLocked() {
303        final long currentTimeMillis = SystemClock.uptimeMillis();
304
305        if (mWriteScheduled) {
306            mHandler.removeMessages(MyHandler.MSG_PERSIST_SETTINGS);
307
308            // If enough time passed, write without holding off anymore.
309            final long timeSinceLastNotWrittenMutationMillis = currentTimeMillis
310                    - mLastNotWrittenMutationTimeMillis;
311            if (timeSinceLastNotWrittenMutationMillis >= MAX_WRITE_SETTINGS_DELAY_MILLIS) {
312                mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS).sendToTarget();
313                return;
314            }
315
316            // Hold off a bit more as settings are frequently changing.
317            final long maxDelayMillis = Math.max(mLastNotWrittenMutationTimeMillis
318                    + MAX_WRITE_SETTINGS_DELAY_MILLIS - currentTimeMillis, 0);
319            final long writeDelayMillis = Math.min(WRITE_SETTINGS_DELAY_MILLIS, maxDelayMillis);
320
321            Message message = mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS);
322            mHandler.sendMessageDelayed(message, writeDelayMillis);
323        } else {
324            mLastNotWrittenMutationTimeMillis = currentTimeMillis;
325            Message message = mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS);
326            mHandler.sendMessageDelayed(message, WRITE_SETTINGS_DELAY_MILLIS);
327            mWriteScheduled = true;
328        }
329    }
330
331    private void doWriteState() {
332        if (DEBUG_PERSISTENCE) {
333            Slog.i(LOG_TAG, "[PERSIST START]");
334        }
335
336        AtomicFile destination = new AtomicFile(mStatePersistFile);
337
338        final int version;
339        final ArrayMap<String, Setting> settings;
340
341        synchronized (mLock) {
342            version = mVersion;
343            settings = new ArrayMap<>(mSettings);
344            mDirty = false;
345            mWriteScheduled = false;
346        }
347
348        FileOutputStream out = null;
349        try {
350            out = destination.startWrite();
351
352            XmlSerializer serializer = Xml.newSerializer();
353            serializer.setOutput(out, "utf-8");
354            serializer.startDocument(null, true);
355            serializer.startTag(null, TAG_SETTINGS);
356            serializer.attribute(null, ATTR_VERSION, String.valueOf(version));
357
358            final int settingCount = settings.size();
359            for (int i = 0; i < settingCount; i++) {
360                Setting setting = settings.valueAt(i);
361
362                serializer.startTag(null, TAG_SETTING);
363                serializer.attribute(null, ATTR_ID, setting.getId());
364                serializer.attribute(null, ATTR_NAME, setting.getName());
365                serializer.attribute(null, ATTR_VALUE, packValue(setting.getValue()));
366                serializer.attribute(null, ATTR_PACKAGE, packValue(setting.getPackageName()));
367                serializer.endTag(null, TAG_SETTING);
368
369                if (DEBUG_PERSISTENCE) {
370                    Slog.i(LOG_TAG, "[PERSISTED]" + setting.getName() + "=" + setting.getValue());
371                }
372            }
373
374            serializer.endTag(null, TAG_SETTINGS);
375            serializer.endDocument();
376            destination.finishWrite(out);
377
378            if (DEBUG_PERSISTENCE) {
379                Slog.i(LOG_TAG, "[PERSIST END]");
380            }
381
382        } catch (IOException e) {
383            Slog.w(LOG_TAG, "Failed to write settings, restoring backup", e);
384            destination.failWrite(out);
385        } finally {
386            IoUtils.closeQuietly(out);
387        }
388    }
389
390    private void readStateSyncLocked() {
391        FileInputStream in;
392        if (!mStatePersistFile.exists()) {
393            return;
394        }
395        try {
396            in = new FileInputStream(mStatePersistFile);
397        } catch (FileNotFoundException fnfe) {
398            Slog.i(LOG_TAG, "No settings state");
399            return;
400        }
401        try {
402            XmlPullParser parser = Xml.newPullParser();
403            parser.setInput(in, null);
404            parseStateLocked(parser);
405        } catch (XmlPullParserException | IOException ise) {
406            throw new IllegalStateException("Failed parsing settings file: "
407                    + mStatePersistFile , ise);
408        } finally {
409            IoUtils.closeQuietly(in);
410        }
411    }
412
413    private void parseStateLocked(XmlPullParser parser)
414            throws IOException, XmlPullParserException {
415        parser.next();
416        skipEmptyTextTags(parser);
417        expect(parser, XmlPullParser.START_TAG, TAG_SETTINGS);
418
419        mVersion = Integer.parseInt(parser.getAttributeValue(null, ATTR_VERSION));
420
421        parser.next();
422
423        while (parseSettingLocked(parser)) {
424            parser.next();
425        }
426
427        skipEmptyTextTags(parser);
428        expect(parser, XmlPullParser.END_TAG, TAG_SETTINGS);
429    }
430
431    private boolean parseSettingLocked(XmlPullParser parser)
432            throws IOException, XmlPullParserException {
433        skipEmptyTextTags(parser);
434        if (!accept(parser, XmlPullParser.START_TAG, TAG_SETTING)) {
435            return false;
436        }
437
438        String id = parser.getAttributeValue(null, ATTR_ID);
439        String name = parser.getAttributeValue(null, ATTR_NAME);
440        String value = parser.getAttributeValue(null, ATTR_VALUE);
441        String packageName = parser.getAttributeValue(null, ATTR_PACKAGE);
442        mSettings.put(name, new Setting(name, unpackValue(value),
443                unpackValue(packageName), id));
444
445        if (DEBUG_PERSISTENCE) {
446            Slog.i(LOG_TAG, "[RESTORED] " + name + "=" + value);
447        }
448
449        parser.next();
450
451        skipEmptyTextTags(parser);
452        expect(parser, XmlPullParser.END_TAG, TAG_SETTING);
453
454        return true;
455    }
456
457    private void expect(XmlPullParser parser, int type, String tag)
458            throws IOException, XmlPullParserException {
459        if (!accept(parser, type, tag)) {
460            throw new XmlPullParserException("Expected event: " + type
461                    + " and tag: " + tag + " but got event: " + parser.getEventType()
462                    + " and tag:" + parser.getName());
463        }
464    }
465
466    private void skipEmptyTextTags(XmlPullParser parser)
467            throws IOException, XmlPullParserException {
468        while (accept(parser, XmlPullParser.TEXT, null)
469                && "\n".equals(parser.getText())) {
470            parser.next();
471        }
472    }
473
474    private boolean accept(XmlPullParser parser, int type, String tag)
475            throws IOException, XmlPullParserException {
476        if (parser.getEventType() != type) {
477            return false;
478        }
479        if (tag != null) {
480            if (!tag.equals(parser.getName())) {
481                return false;
482            }
483        } else if (parser.getName() != null) {
484            return false;
485        }
486        return true;
487    }
488
489    private final class MyHandler extends Handler {
490        public static final int MSG_PERSIST_SETTINGS = 1;
491
492        public MyHandler() {
493            super(BackgroundThread.getHandler().getLooper());
494        }
495
496        @Override
497        public void handleMessage(Message message) {
498            switch (message.what) {
499                case MSG_PERSIST_SETTINGS: {
500                    Runnable callback = (Runnable) message.obj;
501                    doWriteState();
502                    if (callback != null) {
503                        callback.run();
504                    }
505                }
506                break;
507            }
508        }
509    }
510
511    private static String packValue(String value) {
512        if (value == null) {
513            return NULL_VALUE;
514        }
515        return value;
516    }
517
518    private static String unpackValue(String value) {
519        if (NULL_VALUE.equals(value)) {
520            return null;
521        }
522        return value;
523    }
524
525    public static final class Setting {
526        private static long sNextId;
527
528        private String name;
529        private String value;
530        private String packageName;
531        private String id;
532
533        public Setting(String name, String value, String packageName) {
534            init(name, value, packageName, String.valueOf(sNextId++));
535        }
536
537        public Setting(String name, String value, String packageName, String id) {
538            sNextId = Math.max(sNextId, Long.valueOf(id));
539            init(name, value, packageName, String.valueOf(sNextId));
540        }
541
542        private void init(String name, String value, String packageName, String id) {
543            this.name = name;
544            this.value = value;
545            this.packageName = packageName;
546            this.id = id;
547        }
548
549        public String getName() {
550            return name;
551        }
552
553        public String getValue() {
554            return value;
555        }
556
557        public String getPackageName() {
558            return packageName;
559        }
560
561        public String getId() {
562            return id;
563        }
564
565        public boolean update(String value, String packageName) {
566            if (Objects.equal(value, this.value)) {
567                return false;
568            }
569            this.value = value;
570            this.packageName = packageName;
571            this.id = String.valueOf(sNextId++);
572            return true;
573        }
574    }
575}
576