blob: e63d22053f32e2cc40cd205b62829050406eac9e [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
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}