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