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