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