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