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