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