SettingsState.java revision 85b880035daf6a9100f5d1b66f4382f6f21497f8
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.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 @GuardedBy("mLock") 113 private long mNextId; 114 115 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 + " package: " + packageName); 283 } 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"); 357 serializer.startDocument(null, true); 358 serializer.startTag(null, TAG_SETTINGS); 359 serializer.attribute(null, ATTR_VERSION, String.valueOf(version)); 360 361 final int settingCount = settings.size(); 362 for (int i = 0; i < settingCount; i++) { 363 Setting setting = settings.valueAt(i); 364 365 serializer.startTag(null, TAG_SETTING); 366 serializer.attribute(null, ATTR_ID, setting.getId()); 367 serializer.attribute(null, ATTR_NAME, setting.getName()); 368 serializer.attribute(null, ATTR_VALUE, packValue(setting.getValue())); 369 serializer.attribute(null, ATTR_PACKAGE, packValue(setting.getPackageName())); 370 serializer.endTag(null, TAG_SETTING); 371 372 if (DEBUG_PERSISTENCE) { 373 Slog.i(LOG_TAG, "[PERSISTED]" + setting.getName() + "=" + setting.getValue()); 374 } 375 } 376 377 serializer.endTag(null, TAG_SETTINGS); 378 serializer.endDocument(); 379 destination.finishWrite(out); 380 381 if (DEBUG_PERSISTENCE) { 382 Slog.i(LOG_TAG, "[PERSIST END]"); 383 } 384 385 } catch (IOException e) { 386 Slog.w(LOG_TAG, "Failed to write settings, restoring backup", e); 387 destination.failWrite(out); 388 } finally { 389 IoUtils.closeQuietly(out); 390 } 391 } 392 393 private void readStateSyncLocked() { 394 FileInputStream in; 395 if (!mStatePersistFile.exists()) { 396 return; 397 } 398 try { 399 in = new FileInputStream(mStatePersistFile); 400 } catch (FileNotFoundException fnfe) { 401 Slog.i(LOG_TAG, "No settings state"); 402 return; 403 } 404 try { 405 XmlPullParser parser = Xml.newPullParser(); 406 parser.setInput(in, null); 407 parseStateLocked(parser); 408 } catch (XmlPullParserException | IOException ise) { 409 throw new IllegalStateException("Failed parsing settings file: " 410 + mStatePersistFile , ise); 411 } finally { 412 IoUtils.closeQuietly(in); 413 } 414 } 415 416 private void parseStateLocked(XmlPullParser parser) 417 throws IOException, XmlPullParserException { 418 parser.next(); 419 skipEmptyTextTags(parser); 420 expect(parser, XmlPullParser.START_TAG, TAG_SETTINGS); 421 422 mVersion = Integer.parseInt(parser.getAttributeValue(null, ATTR_VERSION)); 423 424 parser.next(); 425 426 while (parseSettingLocked(parser)) { 427 parser.next(); 428 } 429 430 skipEmptyTextTags(parser); 431 expect(parser, XmlPullParser.END_TAG, TAG_SETTINGS); 432 } 433 434 private boolean parseSettingLocked(XmlPullParser parser) 435 throws IOException, XmlPullParserException { 436 skipEmptyTextTags(parser); 437 if (!accept(parser, XmlPullParser.START_TAG, TAG_SETTING)) { 438 return false; 439 } 440 441 String id = parser.getAttributeValue(null, ATTR_ID); 442 String name = parser.getAttributeValue(null, ATTR_NAME); 443 String value = parser.getAttributeValue(null, ATTR_VALUE); 444 String packageName = parser.getAttributeValue(null, ATTR_PACKAGE); 445 mSettings.put(name, new Setting(name, unpackValue(value), 446 unpackValue(packageName), id)); 447 448 if (DEBUG_PERSISTENCE) { 449 Slog.i(LOG_TAG, "[RESTORED] " + name + "=" + value); 450 } 451 452 parser.next(); 453 454 skipEmptyTextTags(parser); 455 expect(parser, XmlPullParser.END_TAG, TAG_SETTING); 456 457 return true; 458 } 459 460 private void expect(XmlPullParser parser, int type, String tag) 461 throws IOException, XmlPullParserException { 462 if (!accept(parser, type, tag)) { 463 throw new XmlPullParserException("Expected event: " + type 464 + " and tag: " + tag + " but got event: " + parser.getEventType() 465 + " and tag:" + parser.getName()); 466 } 467 } 468 469 private void skipEmptyTextTags(XmlPullParser parser) 470 throws IOException, XmlPullParserException { 471 while (accept(parser, XmlPullParser.TEXT, null) 472 && "\n".equals(parser.getText())) { 473 parser.next(); 474 } 475 } 476 477 private boolean accept(XmlPullParser parser, int type, String tag) 478 throws IOException, XmlPullParserException { 479 if (parser.getEventType() != type) { 480 return false; 481 } 482 if (tag != null) { 483 if (!tag.equals(parser.getName())) { 484 return false; 485 } 486 } else if (parser.getName() != null) { 487 return false; 488 } 489 return true; 490 } 491 492 private final class MyHandler extends Handler { 493 public static final int MSG_PERSIST_SETTINGS = 1; 494 495 public MyHandler() { 496 super(BackgroundThread.getHandler().getLooper()); 497 } 498 499 @Override 500 public void handleMessage(Message message) { 501 switch (message.what) { 502 case MSG_PERSIST_SETTINGS: { 503 Runnable callback = (Runnable) message.obj; 504 doWriteState(); 505 if (callback != null) { 506 callback.run(); 507 } 508 } 509 break; 510 } 511 } 512 } 513 514 private static String packValue(String value) { 515 if (value == null) { 516 return NULL_VALUE; 517 } 518 return value; 519 } 520 521 private static String unpackValue(String value) { 522 if (NULL_VALUE.equals(value)) { 523 return null; 524 } 525 return value; 526 } 527 528 public final class Setting { 529 private String name; 530 private String value; 531 private String packageName; 532 private String id; 533 534 public Setting(String name, String value, String packageName) { 535 init(name, value, packageName, String.valueOf(mNextId++)); 536 } 537 538 public Setting(String name, String value, String packageName, String id) { 539 mNextId = Math.max(mNextId, Long.valueOf(id) + 1); 540 init(name, value, packageName, id); 541 } 542 543 private void init(String name, String value, String packageName, String id) { 544 this.name = name; 545 this.value = value; 546 this.packageName = packageName; 547 this.id = id; 548 } 549 550 public String getName() { 551 return name; 552 } 553 554 public String getValue() { 555 return value; 556 } 557 558 public String getPackageName() { 559 return packageName; 560 } 561 562 public String getId() { 563 return id; 564 } 565 566 public boolean update(String value, String packageName) { 567 if (Objects.equal(value, this.value)) { 568 return false; 569 } 570 this.value = value; 571 this.packageName = packageName; 572 this.id = String.valueOf(mNextId++); 573 return true; 574 } 575 } 576} 577