SettingsState.java revision 9e9e2e73c6ec7bece20268196dc89ad0c8bafad4
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.nio.charset.StandardCharsets; 42import java.util.ArrayList; 43import java.util.List; 44 45/** 46 * This class contains the state for one type of settings. It is responsible 47 * for saving the state asynchronously to an XML file after a mutation and 48 * loading the from an XML file on construction. 49 * <p> 50 * This class uses the same lock as the settings provider to ensure that 51 * multiple changes made by the settings provider, e,g, upgrade, bulk insert, 52 * etc, are atomically persisted since the asynchronous persistence is using 53 * the same lock to grab the current state to write to disk. 54 * </p> 55 */ 56final class SettingsState { 57 private static final boolean DEBUG = false; 58 private static final boolean DEBUG_PERSISTENCE = false; 59 60 private static final String LOG_TAG = "SettingsState"; 61 62 private static final long WRITE_SETTINGS_DELAY_MILLIS = 200; 63 private static final long MAX_WRITE_SETTINGS_DELAY_MILLIS = 2000; 64 65 public static final int MAX_BYTES_PER_APP_PACKAGE_UNLIMITED = -1; 66 public static final int MAX_BYTES_PER_APP_PACKAGE_LIMITED = 20000; 67 68 public static final String SYSTEM_PACKAGE_NAME = "android"; 69 70 public static final int VERSION_UNDEFINED = -1; 71 72 private static final String TAG_SETTINGS = "settings"; 73 private static final String TAG_SETTING = "setting"; 74 private static final String ATTR_PACKAGE = "package"; 75 76 private static final String ATTR_VERSION = "version"; 77 private static final String ATTR_ID = "id"; 78 private static final String ATTR_NAME = "name"; 79 private static final String ATTR_VALUE = "value"; 80 81 private static final String NULL_VALUE = "null"; 82 83 private final Object mLock; 84 85 private final Handler mHandler = new MyHandler(); 86 87 @GuardedBy("mLock") 88 private final ArrayMap<String, Setting> mSettings = new ArrayMap<>(); 89 90 @GuardedBy("mLock") 91 private final ArrayMap<String, Integer> mPackageToMemoryUsage; 92 93 @GuardedBy("mLock") 94 private final int mMaxBytesPerAppPackage; 95 96 @GuardedBy("mLock") 97 private final File mStatePersistFile; 98 99 public final int mKey; 100 101 @GuardedBy("mLock") 102 private int mVersion = VERSION_UNDEFINED; 103 104 @GuardedBy("mLock") 105 private long mLastNotWrittenMutationTimeMillis; 106 107 @GuardedBy("mLock") 108 private boolean mDirty; 109 110 @GuardedBy("mLock") 111 private boolean mWriteScheduled; 112 113 @GuardedBy("mLock") 114 private long mNextId; 115 116 public SettingsState(Object lock, File file, int key, int maxBytesPerAppPackage) { 117 // It is important that we use the same lock as the settings provider 118 // to ensure multiple mutations on this state are atomicaly persisted 119 // as the async persistence should be blocked while we make changes. 120 mLock = lock; 121 mStatePersistFile = file; 122 mKey = key; 123 if (maxBytesPerAppPackage == MAX_BYTES_PER_APP_PACKAGE_LIMITED) { 124 mMaxBytesPerAppPackage = maxBytesPerAppPackage; 125 mPackageToMemoryUsage = new ArrayMap<>(); 126 } else { 127 mMaxBytesPerAppPackage = maxBytesPerAppPackage; 128 mPackageToMemoryUsage = null; 129 } 130 synchronized (mLock) { 131 readStateSyncLocked(); 132 } 133 } 134 135 // The settings provider must hold its lock when calling here. 136 public int getVersionLocked() { 137 return mVersion; 138 } 139 140 // The settings provider must hold its lock when calling here. 141 public void setVersionLocked(int version) { 142 if (version == mVersion) { 143 return; 144 } 145 mVersion = version; 146 147 scheduleWriteIfNeededLocked(); 148 } 149 150 // The settings provider must hold its lock when calling here. 151 public void onPackageRemovedLocked(String packageName) { 152 boolean removedSomething = false; 153 154 final int settingCount = mSettings.size(); 155 for (int i = settingCount - 1; i >= 0; i--) { 156 String name = mSettings.keyAt(i); 157 // Settings defined by us are never dropped. 158 if (Settings.System.PUBLIC_SETTINGS.contains(name) 159 || Settings.System.PRIVATE_SETTINGS.contains(name)) { 160 continue; 161 } 162 Setting setting = mSettings.valueAt(i); 163 if (packageName.equals(setting.packageName)) { 164 mSettings.removeAt(i); 165 removedSomething = true; 166 } 167 } 168 169 if (removedSomething) { 170 scheduleWriteIfNeededLocked(); 171 } 172 } 173 174 // The settings provider must hold its lock when calling here. 175 public List<String> getSettingNamesLocked() { 176 ArrayList<String> names = new ArrayList<>(); 177 final int settingsCount = mSettings.size(); 178 for (int i = 0; i < settingsCount; i++) { 179 String name = mSettings.keyAt(i); 180 names.add(name); 181 } 182 return names; 183 } 184 185 // The settings provider must hold its lock when calling here. 186 public Setting getSettingLocked(String name) { 187 if (TextUtils.isEmpty(name)) { 188 return null; 189 } 190 return mSettings.get(name); 191 } 192 193 // The settings provider must hold its lock when calling here. 194 public boolean updateSettingLocked(String name, String value, String packageName) { 195 if (!hasSettingLocked(name)) { 196 return false; 197 } 198 199 return insertSettingLocked(name, value, packageName); 200 } 201 202 // The settings provider must hold its lock when calling here. 203 public boolean insertSettingLocked(String name, String value, String packageName) { 204 if (TextUtils.isEmpty(name)) { 205 return false; 206 } 207 208 Setting oldState = mSettings.get(name); 209 String oldValue = (oldState != null) ? oldState.value : null; 210 211 if (oldState != null) { 212 if (!oldState.update(value, packageName)) { 213 return false; 214 } 215 } else { 216 Setting state = new Setting(name, value, packageName); 217 mSettings.put(name, state); 218 } 219 220 updateMemoryUsagePerPackageLocked(packageName, oldValue, value); 221 222 scheduleWriteIfNeededLocked(); 223 224 return true; 225 } 226 227 // The settings provider must hold its lock when calling here. 228 public void persistSyncLocked() { 229 mHandler.removeMessages(MyHandler.MSG_PERSIST_SETTINGS); 230 doWriteState(); 231 } 232 233 // The settings provider must hold its lock when calling here. 234 public boolean deleteSettingLocked(String name) { 235 if (TextUtils.isEmpty(name) || !hasSettingLocked(name)) { 236 return false; 237 } 238 239 Setting oldState = mSettings.remove(name); 240 241 updateMemoryUsagePerPackageLocked(oldState.packageName, oldState.value, null); 242 243 scheduleWriteIfNeededLocked(); 244 245 return true; 246 } 247 248 // The settings provider must hold its lock when calling here. 249 public void destroyLocked(Runnable callback) { 250 mHandler.removeMessages(MyHandler.MSG_PERSIST_SETTINGS); 251 if (callback != null) { 252 if (mDirty) { 253 // Do it without a delay. 254 mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS, 255 callback).sendToTarget(); 256 return; 257 } 258 callback.run(); 259 } 260 } 261 262 private void updateMemoryUsagePerPackageLocked(String packageName, String oldValue, 263 String newValue) { 264 if (mMaxBytesPerAppPackage == MAX_BYTES_PER_APP_PACKAGE_UNLIMITED) { 265 return; 266 } 267 268 if (SYSTEM_PACKAGE_NAME.equals(packageName)) { 269 return; 270 } 271 272 final int oldValueSize = (oldValue != null) ? oldValue.length() : 0; 273 final int newValueSize = (newValue != null) ? newValue.length() : 0; 274 final int deltaSize = newValueSize - oldValueSize; 275 276 Integer currentSize = mPackageToMemoryUsage.get(packageName); 277 final int newSize = Math.max((currentSize != null) 278 ? currentSize + deltaSize : deltaSize, 0); 279 280 if (newSize > mMaxBytesPerAppPackage) { 281 throw new IllegalStateException("You are adding too many system settings. " 282 + "You should stop using system settings for app specific data" 283 + " package: " + packageName); 284 } 285 286 if (DEBUG) { 287 Slog.i(LOG_TAG, "Settings for package: " + packageName 288 + " size: " + newSize + " bytes."); 289 } 290 291 mPackageToMemoryUsage.put(packageName, newSize); 292 } 293 294 private boolean hasSettingLocked(String name) { 295 return mSettings.indexOfKey(name) >= 0; 296 } 297 298 private void scheduleWriteIfNeededLocked() { 299 // If dirty then we have a write already scheduled. 300 if (!mDirty) { 301 mDirty = true; 302 writeStateAsyncLocked(); 303 } 304 } 305 306 private void writeStateAsyncLocked() { 307 final long currentTimeMillis = SystemClock.uptimeMillis(); 308 309 if (mWriteScheduled) { 310 mHandler.removeMessages(MyHandler.MSG_PERSIST_SETTINGS); 311 312 // If enough time passed, write without holding off anymore. 313 final long timeSinceLastNotWrittenMutationMillis = currentTimeMillis 314 - mLastNotWrittenMutationTimeMillis; 315 if (timeSinceLastNotWrittenMutationMillis >= MAX_WRITE_SETTINGS_DELAY_MILLIS) { 316 mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS).sendToTarget(); 317 return; 318 } 319 320 // Hold off a bit more as settings are frequently changing. 321 final long maxDelayMillis = Math.max(mLastNotWrittenMutationTimeMillis 322 + MAX_WRITE_SETTINGS_DELAY_MILLIS - currentTimeMillis, 0); 323 final long writeDelayMillis = Math.min(WRITE_SETTINGS_DELAY_MILLIS, maxDelayMillis); 324 325 Message message = mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS); 326 mHandler.sendMessageDelayed(message, writeDelayMillis); 327 } else { 328 mLastNotWrittenMutationTimeMillis = currentTimeMillis; 329 Message message = mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS); 330 mHandler.sendMessageDelayed(message, WRITE_SETTINGS_DELAY_MILLIS); 331 mWriteScheduled = true; 332 } 333 } 334 335 private void doWriteState() { 336 if (DEBUG_PERSISTENCE) { 337 Slog.i(LOG_TAG, "[PERSIST START]"); 338 } 339 340 AtomicFile destination = new AtomicFile(mStatePersistFile); 341 342 final int version; 343 final ArrayMap<String, Setting> settings; 344 345 synchronized (mLock) { 346 version = mVersion; 347 settings = new ArrayMap<>(mSettings); 348 mDirty = false; 349 mWriteScheduled = false; 350 } 351 352 FileOutputStream out = null; 353 try { 354 out = destination.startWrite(); 355 356 XmlSerializer serializer = Xml.newSerializer(); 357 serializer.setOutput(out, StandardCharsets.UTF_8.name()); 358 serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); 359 serializer.startDocument(null, true); 360 serializer.startTag(null, TAG_SETTINGS); 361 serializer.attribute(null, ATTR_VERSION, String.valueOf(version)); 362 363 final int settingCount = settings.size(); 364 for (int i = 0; i < settingCount; i++) { 365 Setting setting = settings.valueAt(i); 366 367 serializer.startTag(null, TAG_SETTING); 368 serializer.attribute(null, ATTR_ID, setting.getId()); 369 serializer.attribute(null, ATTR_NAME, setting.getName()); 370 serializer.attribute(null, ATTR_VALUE, packValue(setting.getValue())); 371 serializer.attribute(null, ATTR_PACKAGE, packValue(setting.getPackageName())); 372 serializer.endTag(null, TAG_SETTING); 373 374 if (DEBUG_PERSISTENCE) { 375 Slog.i(LOG_TAG, "[PERSISTED]" + setting.getName() + "=" + setting.getValue()); 376 } 377 } 378 379 serializer.endTag(null, TAG_SETTINGS); 380 serializer.endDocument(); 381 destination.finishWrite(out); 382 383 if (DEBUG_PERSISTENCE) { 384 Slog.i(LOG_TAG, "[PERSIST END]"); 385 } 386 387 // Any error while writing is fatal. 388 } catch (Throwable t) { 389 Slog.wtf(LOG_TAG, "Failed to write settings, restoring backup", t); 390 destination.failWrite(out); 391 throw new IllegalStateException("Failed to write settings, restoring backup", t); 392 } finally { 393 IoUtils.closeQuietly(out); 394 } 395 } 396 397 private void readStateSyncLocked() { 398 FileInputStream in; 399 if (!mStatePersistFile.exists()) { 400 return; 401 } 402 try { 403 in = new FileInputStream(mStatePersistFile); 404 } catch (FileNotFoundException fnfe) { 405 Slog.i(LOG_TAG, "No settings state"); 406 return; 407 } 408 try { 409 XmlPullParser parser = Xml.newPullParser(); 410 parser.setInput(in, StandardCharsets.UTF_8.name()); 411 parseStateLocked(parser); 412 413 } catch (XmlPullParserException | IOException e) { 414 throw new IllegalStateException("Failed parsing settings file: " 415 + mStatePersistFile , e); 416 } finally { 417 IoUtils.closeQuietly(in); 418 } 419 } 420 421 private void parseStateLocked(XmlPullParser parser) 422 throws IOException, XmlPullParserException { 423 final int outerDepth = parser.getDepth(); 424 int type; 425 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 426 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 427 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 428 continue; 429 } 430 431 String tagName = parser.getName(); 432 if (tagName.equals(TAG_SETTINGS)) { 433 parseSettingsLocked(parser); 434 } 435 } 436 } 437 438 private void parseSettingsLocked(XmlPullParser parser) 439 throws IOException, XmlPullParserException { 440 441 mVersion = Integer.parseInt(parser.getAttributeValue(null, ATTR_VERSION)); 442 443 final int outerDepth = parser.getDepth(); 444 int type; 445 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 446 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 447 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 448 continue; 449 } 450 451 String tagName = parser.getName(); 452 if (tagName.equals(TAG_SETTING)) { 453 String id = parser.getAttributeValue(null, ATTR_ID); 454 String name = parser.getAttributeValue(null, ATTR_NAME); 455 String value = parser.getAttributeValue(null, ATTR_VALUE); 456 String packageName = parser.getAttributeValue(null, ATTR_PACKAGE); 457 mSettings.put(name, new Setting(name, unpackValue(value), 458 unpackValue(packageName), id)); 459 460 if (DEBUG_PERSISTENCE) { 461 Slog.i(LOG_TAG, "[RESTORED] " + name + "=" + value); 462 } 463 } 464 } 465 } 466 467 private final class MyHandler extends Handler { 468 public static final int MSG_PERSIST_SETTINGS = 1; 469 470 public MyHandler() { 471 super(BackgroundThread.getHandler().getLooper()); 472 } 473 474 @Override 475 public void handleMessage(Message message) { 476 switch (message.what) { 477 case MSG_PERSIST_SETTINGS: { 478 Runnable callback = (Runnable) message.obj; 479 doWriteState(); 480 if (callback != null) { 481 callback.run(); 482 } 483 } 484 break; 485 } 486 } 487 } 488 489 private static String packValue(String value) { 490 if (value == null) { 491 return NULL_VALUE; 492 } 493 return value; 494 } 495 496 private static String unpackValue(String value) { 497 if (NULL_VALUE.equals(value)) { 498 return null; 499 } 500 return value; 501 } 502 503 public final class Setting { 504 private String name; 505 private String value; 506 private String packageName; 507 private String id; 508 509 public Setting(String name, String value, String packageName) { 510 init(name, value, packageName, String.valueOf(mNextId++)); 511 } 512 513 public Setting(String name, String value, String packageName, String id) { 514 mNextId = Math.max(mNextId, Long.valueOf(id) + 1); 515 init(name, value, packageName, id); 516 } 517 518 private void init(String name, String value, String packageName, String id) { 519 this.name = name; 520 this.value = value; 521 this.packageName = packageName; 522 this.id = id; 523 } 524 525 public String getName() { 526 return name; 527 } 528 529 public String getValue() { 530 return value; 531 } 532 533 public String getPackageName() { 534 return packageName; 535 } 536 537 public String getId() { 538 return id; 539 } 540 541 public boolean update(String value, String packageName) { 542 if (Objects.equal(value, this.value)) { 543 return false; 544 } 545 this.value = value; 546 this.packageName = packageName; 547 this.id = String.valueOf(mNextId++); 548 return true; 549 } 550 } 551} 552