SettingsState.java revision 683914bfb13908bf380a25258cd45bcf43f13dc9
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        }
280
281        if (DEBUG) {
282            Slog.i(LOG_TAG, "Settings for package: " + packageName
283                    + " size: " + newSize + " bytes.");
284        }
285
286        mPackageToMemoryUsage.put(packageName, newSize);
287    }
288
289    private boolean hasSettingLocked(String name) {
290        return mSettings.indexOfKey(name) >= 0;
291    }
292
293    private void scheduleWriteIfNeededLocked() {
294        // If dirty then we have a write already scheduled.
295        if (!mDirty) {
296            mDirty = true;
297            writeStateAsyncLocked();
298        }
299    }
300
301    private void writeStateAsyncLocked() {
302        final long currentTimeMillis = SystemClock.uptimeMillis();
303
304        if (mWriteScheduled) {
305            mHandler.removeMessages(MyHandler.MSG_PERSIST_SETTINGS);
306
307            // If enough time passed, write without holding off anymore.
308            final long timeSinceLastNotWrittenMutationMillis = currentTimeMillis
309                    - mLastNotWrittenMutationTimeMillis;
310            if (timeSinceLastNotWrittenMutationMillis >= MAX_WRITE_SETTINGS_DELAY_MILLIS) {
311                mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS).sendToTarget();
312                return;
313            }
314
315            // Hold off a bit more as settings are frequently changing.
316            final long maxDelayMillis = Math.max(mLastNotWrittenMutationTimeMillis
317                    + MAX_WRITE_SETTINGS_DELAY_MILLIS - currentTimeMillis, 0);
318            final long writeDelayMillis = Math.min(WRITE_SETTINGS_DELAY_MILLIS, maxDelayMillis);
319
320            Message message = mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS);
321            mHandler.sendMessageDelayed(message, writeDelayMillis);
322        } else {
323            mLastNotWrittenMutationTimeMillis = currentTimeMillis;
324            Message message = mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS);
325            mHandler.sendMessageDelayed(message, WRITE_SETTINGS_DELAY_MILLIS);
326            mWriteScheduled = true;
327        }
328    }
329
330    private void doWriteState() {
331        if (DEBUG_PERSISTENCE) {
332            Slog.i(LOG_TAG, "[PERSIST START]");
333        }
334
335        AtomicFile destination = new AtomicFile(mStatePersistFile);
336
337        final int version;
338        final ArrayMap<String, Setting> settings;
339
340        synchronized (mLock) {
341            version = mVersion;
342            settings = new ArrayMap<>(mSettings);
343            mDirty = false;
344            mWriteScheduled = false;
345        }
346
347        FileOutputStream out = null;
348        try {
349            out = destination.startWrite();
350
351            XmlSerializer serializer = Xml.newSerializer();
352            serializer.setOutput(out, "utf-8");
353            serializer.startDocument(null, true);
354            serializer.startTag(null, TAG_SETTINGS);
355            serializer.attribute(null, ATTR_VERSION, String.valueOf(version));
356
357            final int settingCount = settings.size();
358            for (int i = 0; i < settingCount; i++) {
359                Setting setting = settings.valueAt(i);
360
361                serializer.startTag(null, TAG_SETTING);
362                serializer.attribute(null, ATTR_ID, setting.getId());
363                serializer.attribute(null, ATTR_NAME, setting.getName());
364                serializer.attribute(null, ATTR_VALUE, packValue(setting.getValue()));
365                serializer.attribute(null, ATTR_PACKAGE, packValue(setting.getPackageName()));
366                serializer.endTag(null, TAG_SETTING);
367
368                if (DEBUG_PERSISTENCE) {
369                    Slog.i(LOG_TAG, "[PERSISTED]" + setting.getName() + "=" + setting.getValue());
370                }
371            }
372
373            serializer.endTag(null, TAG_SETTINGS);
374            serializer.endDocument();
375            destination.finishWrite(out);
376
377            if (DEBUG_PERSISTENCE) {
378                Slog.i(LOG_TAG, "[PERSIST END]");
379            }
380
381        } catch (IOException e) {
382            Slog.w(LOG_TAG, "Failed to write settings, restoring backup", e);
383            destination.failWrite(out);
384        } finally {
385            IoUtils.closeQuietly(out);
386        }
387    }
388
389    private void readStateSyncLocked() {
390        FileInputStream in;
391        if (!mStatePersistFile.exists()) {
392            return;
393        }
394        try {
395            in = new FileInputStream(mStatePersistFile);
396        } catch (FileNotFoundException fnfe) {
397            Slog.i(LOG_TAG, "No settings state");
398            return;
399        }
400        try {
401            XmlPullParser parser = Xml.newPullParser();
402            parser.setInput(in, null);
403            parseStateLocked(parser);
404        } catch (XmlPullParserException | IOException ise) {
405            throw new IllegalStateException("Failed parsing settings file: "
406                    + mStatePersistFile , ise);
407        } finally {
408            IoUtils.closeQuietly(in);
409        }
410    }
411
412    private void parseStateLocked(XmlPullParser parser)
413            throws IOException, XmlPullParserException {
414        parser.next();
415        skipEmptyTextTags(parser);
416        expect(parser, XmlPullParser.START_TAG, TAG_SETTINGS);
417
418        mVersion = Integer.parseInt(parser.getAttributeValue(null, ATTR_VERSION));
419
420        parser.next();
421
422        while (parseSettingLocked(parser)) {
423            parser.next();
424        }
425
426        skipEmptyTextTags(parser);
427        expect(parser, XmlPullParser.END_TAG, TAG_SETTINGS);
428    }
429
430    private boolean parseSettingLocked(XmlPullParser parser)
431            throws IOException, XmlPullParserException {
432        skipEmptyTextTags(parser);
433        if (!accept(parser, XmlPullParser.START_TAG, TAG_SETTING)) {
434            return false;
435        }
436
437        String id = parser.getAttributeValue(null, ATTR_ID);
438        String name = parser.getAttributeValue(null, ATTR_NAME);
439        String value = parser.getAttributeValue(null, ATTR_VALUE);
440        String packageName = parser.getAttributeValue(null, ATTR_PACKAGE);
441        mSettings.put(name, new Setting(name, unpackValue(value),
442                unpackValue(packageName), id));
443
444        if (DEBUG_PERSISTENCE) {
445            Slog.i(LOG_TAG, "[RESTORED] " + name + "=" + value);
446        }
447
448        parser.next();
449
450        skipEmptyTextTags(parser);
451        expect(parser, XmlPullParser.END_TAG, TAG_SETTING);
452
453        return true;
454    }
455
456    private void expect(XmlPullParser parser, int type, String tag)
457            throws IOException, XmlPullParserException {
458        if (!accept(parser, type, tag)) {
459            throw new XmlPullParserException("Expected event: " + type
460                    + " and tag: " + tag + " but got event: " + parser.getEventType()
461                    + " and tag:" + parser.getName());
462        }
463    }
464
465    private void skipEmptyTextTags(XmlPullParser parser)
466            throws IOException, XmlPullParserException {
467        while (accept(parser, XmlPullParser.TEXT, null)
468                && "\n".equals(parser.getText())) {
469            parser.next();
470        }
471    }
472
473    private boolean accept(XmlPullParser parser, int type, String tag)
474            throws IOException, XmlPullParserException {
475        if (parser.getEventType() != type) {
476            return false;
477        }
478        if (tag != null) {
479            if (!tag.equals(parser.getName())) {
480                return false;
481            }
482        } else if (parser.getName() != null) {
483            return false;
484        }
485        return true;
486    }
487
488    private final class MyHandler extends Handler {
489        public static final int MSG_PERSIST_SETTINGS = 1;
490
491        public MyHandler() {
492            super(BackgroundThread.getHandler().getLooper());
493        }
494
495        @Override
496        public void handleMessage(Message message) {
497            switch (message.what) {
498                case MSG_PERSIST_SETTINGS: {
499                    Runnable callback = (Runnable) message.obj;
500                    doWriteState();
501                    if (callback != null) {
502                        callback.run();
503                    }
504                }
505                break;
506            }
507        }
508    }
509
510    private static String packValue(String value) {
511        if (value == null) {
512            return NULL_VALUE;
513        }
514        return value;
515    }
516
517    private static String unpackValue(String value) {
518        if (NULL_VALUE.equals(value)) {
519            return null;
520        }
521        return value;
522    }
523
524    public static final class Setting {
525        private static long sNextId;
526
527        private String name;
528        private String value;
529        private String packageName;
530        private String id;
531
532        public Setting(String name, String value, String packageName) {
533            init(name, value, packageName, String.valueOf(sNextId++));
534        }
535
536        public Setting(String name, String value, String packageName, String id) {
537            sNextId = Math.max(sNextId, Long.valueOf(id));
538            init(name, value, packageName, String.valueOf(sNextId));
539        }
540
541        private void init(String name, String value, String packageName, String id) {
542            this.name = name;
543            this.value = value;
544            this.packageName = packageName;
545            this.id = id;
546        }
547
548        public String getName() {
549            return name;
550        }
551
552        public String getValue() {
553            return value;
554        }
555
556        public String getPackageName() {
557            return packageName;
558        }
559
560        public String getId() {
561            return id;
562        }
563
564        public boolean update(String value, String packageName) {
565            if (Objects.equal(value, this.value)) {
566                return false;
567            }
568            this.value = value;
569            this.packageName = packageName;
570            this.id = String.valueOf(sNextId++);
571            return true;
572        }
573    }
574}
575