SharedPreferencesImpl.java revision 4cd50b8d76e08d75df1a2e9d4902bcdc880bd1c1
1/* 2 * Copyright (C) 2010 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 android.app; 18 19import android.content.SharedPreferences; 20import android.os.FileUtils.FileStatus; 21import android.os.FileUtils; 22import android.os.Looper; 23import android.util.Log; 24 25import com.google.android.collect.Maps; 26import com.android.internal.util.XmlUtils; 27 28import dalvik.system.BlockGuard; 29 30import org.xmlpull.v1.XmlPullParserException; 31 32import java.io.File; 33import java.io.FileInputStream; 34import java.io.FileNotFoundException; 35import java.io.FileOutputStream; 36import java.io.IOException; 37import java.util.ArrayList; 38import java.util.HashMap; 39import java.util.HashSet; 40import java.util.List; 41import java.util.Map; 42import java.util.Set; 43import java.util.WeakHashMap; 44import java.util.concurrent.CountDownLatch; 45import java.util.concurrent.ExecutorService; 46 47final class SharedPreferencesImpl implements SharedPreferences { 48 private static final String TAG = "SharedPreferencesImpl"; 49 private static final boolean DEBUG = false; 50 51 // Lock ordering rules: 52 // - acquire SharedPreferencesImpl.this before EditorImpl.this 53 // - acquire mWritingToDiskLock before EditorImpl.this 54 55 private final File mFile; 56 private final File mBackupFile; 57 private final int mMode; 58 59 private Map<String, Object> mMap; // guarded by 'this' 60 private int mDiskWritesInFlight = 0; // guarded by 'this' 61 private boolean mLoaded = false; // guarded by 'this' 62 private long mStatTimestamp; // guarded by 'this' 63 private long mStatSize; // guarded by 'this' 64 65 private final Object mWritingToDiskLock = new Object(); 66 private static final Object mContent = new Object(); 67 private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners = 68 new WeakHashMap<OnSharedPreferenceChangeListener, Object>(); 69 70 SharedPreferencesImpl(File file, int mode) { 71 mFile = file; 72 mBackupFile = makeBackupFile(file); 73 mMode = mode; 74 mLoaded = false; 75 mMap = null; 76 startLoadFromDisk(); 77 } 78 79 private void startLoadFromDisk() { 80 synchronized (this) { 81 mLoaded = false; 82 } 83 new Thread("SharedPreferencesImpl-load") { 84 public void run() { 85 synchronized (SharedPreferencesImpl.this) { 86 loadFromDiskLocked(); 87 } 88 } 89 }.start(); 90 } 91 92 private void loadFromDiskLocked() { 93 if (mLoaded) { 94 return; 95 } 96 if (mBackupFile.exists()) { 97 mFile.delete(); 98 mBackupFile.renameTo(mFile); 99 } 100 101 // Debugging 102 if (mFile.exists() && !mFile.canRead()) { 103 Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission"); 104 } 105 106 Map map = null; 107 FileStatus stat = new FileStatus(); 108 if (FileUtils.getFileStatus(mFile.getPath(), stat) && mFile.canRead()) { 109 try { 110 FileInputStream str = new FileInputStream(mFile); 111 map = XmlUtils.readMapXml(str); 112 str.close(); 113 } catch (XmlPullParserException e) { 114 Log.w(TAG, "getSharedPreferences", e); 115 } catch (FileNotFoundException e) { 116 Log.w(TAG, "getSharedPreferences", e); 117 } catch (IOException e) { 118 Log.w(TAG, "getSharedPreferences", e); 119 } 120 } 121 mLoaded = true; 122 if (map != null) { 123 mMap = map; 124 mStatTimestamp = stat.mtime; 125 mStatSize = stat.size; 126 } else { 127 mMap = new HashMap<String, Object>(); 128 } 129 notifyAll(); 130 } 131 132 private static File makeBackupFile(File prefsFile) { 133 return new File(prefsFile.getPath() + ".bak"); 134 } 135 136 void startReloadIfChangedUnexpectedly() { 137 synchronized (this) { 138 // TODO: wait for any pending writes to disk? 139 if (!hasFileChangedUnexpectedly()) { 140 return; 141 } 142 startLoadFromDisk(); 143 } 144 } 145 146 // Has the file changed out from under us? i.e. writes that 147 // we didn't instigate. 148 private boolean hasFileChangedUnexpectedly() { 149 synchronized (this) { 150 if (mDiskWritesInFlight > 0) { 151 // If we know we caused it, it's not unexpected. 152 if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected."); 153 return false; 154 } 155 } 156 FileStatus stat = new FileStatus(); 157 if (!FileUtils.getFileStatus(mFile.getPath(), stat)) { 158 return true; 159 } 160 synchronized (this) { 161 return mStatTimestamp != stat.mtime || mStatSize != stat.size; 162 } 163 } 164 165 public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { 166 synchronized(this) { 167 mListeners.put(listener, mContent); 168 } 169 } 170 171 public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { 172 synchronized(this) { 173 mListeners.remove(listener); 174 } 175 } 176 177 private void awaitLoadedLocked() { 178 if (!mLoaded) { 179 // Raise an explicit StrictMode onReadFromDisk for this 180 // thread, since the real read will be in a different 181 // thread and otherwise ignored by StrictMode. 182 BlockGuard.getThreadPolicy().onReadFromDisk(); 183 } 184 while (!mLoaded) { 185 try { 186 wait(); 187 } catch (InterruptedException unused) { 188 } 189 } 190 } 191 192 public Map<String, ?> getAll() { 193 synchronized (this) { 194 awaitLoadedLocked(); 195 //noinspection unchecked 196 return new HashMap<String, Object>(mMap); 197 } 198 } 199 200 public String getString(String key, String defValue) { 201 synchronized (this) { 202 awaitLoadedLocked(); 203 String v = (String)mMap.get(key); 204 return v != null ? v : defValue; 205 } 206 } 207 208 public Set<String> getStringSet(String key, Set<String> defValues) { 209 synchronized (this) { 210 awaitLoadedLocked(); 211 Set<String> v = (Set<String>) mMap.get(key); 212 return v != null ? v : defValues; 213 } 214 } 215 216 public int getInt(String key, int defValue) { 217 synchronized (this) { 218 awaitLoadedLocked(); 219 Integer v = (Integer)mMap.get(key); 220 return v != null ? v : defValue; 221 } 222 } 223 public long getLong(String key, long defValue) { 224 synchronized (this) { 225 awaitLoadedLocked(); 226 Long v = (Long)mMap.get(key); 227 return v != null ? v : defValue; 228 } 229 } 230 public float getFloat(String key, float defValue) { 231 synchronized (this) { 232 awaitLoadedLocked(); 233 Float v = (Float)mMap.get(key); 234 return v != null ? v : defValue; 235 } 236 } 237 public boolean getBoolean(String key, boolean defValue) { 238 synchronized (this) { 239 awaitLoadedLocked(); 240 Boolean v = (Boolean)mMap.get(key); 241 return v != null ? v : defValue; 242 } 243 } 244 245 public boolean contains(String key) { 246 synchronized (this) { 247 awaitLoadedLocked(); 248 return mMap.containsKey(key); 249 } 250 } 251 252 public Editor edit() { 253 // TODO: remove the need to call awaitLoadedLocked() when 254 // requesting an editor. will require some work on the 255 // Editor, but then we should be able to do: 256 // 257 // context.getSharedPreferences(..).edit().putString(..).apply() 258 // 259 // ... all without blocking. 260 synchronized (this) { 261 awaitLoadedLocked(); 262 } 263 264 return new EditorImpl(); 265 } 266 267 // Return value from EditorImpl#commitToMemory() 268 private static class MemoryCommitResult { 269 public boolean changesMade; // any keys different? 270 public List<String> keysModified; // may be null 271 public Set<OnSharedPreferenceChangeListener> listeners; // may be null 272 public Map<?, ?> mapToWriteToDisk; 273 public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1); 274 public volatile boolean writeToDiskResult = false; 275 276 public void setDiskWriteResult(boolean result) { 277 writeToDiskResult = result; 278 writtenToDiskLatch.countDown(); 279 } 280 } 281 282 public final class EditorImpl implements Editor { 283 private final Map<String, Object> mModified = Maps.newHashMap(); 284 private boolean mClear = false; 285 286 public Editor putString(String key, String value) { 287 synchronized (this) { 288 mModified.put(key, value); 289 return this; 290 } 291 } 292 public Editor putStringSet(String key, Set<String> values) { 293 synchronized (this) { 294 mModified.put(key, values); 295 return this; 296 } 297 } 298 public Editor putInt(String key, int value) { 299 synchronized (this) { 300 mModified.put(key, value); 301 return this; 302 } 303 } 304 public Editor putLong(String key, long value) { 305 synchronized (this) { 306 mModified.put(key, value); 307 return this; 308 } 309 } 310 public Editor putFloat(String key, float value) { 311 synchronized (this) { 312 mModified.put(key, value); 313 return this; 314 } 315 } 316 public Editor putBoolean(String key, boolean value) { 317 synchronized (this) { 318 mModified.put(key, value); 319 return this; 320 } 321 } 322 323 public Editor remove(String key) { 324 synchronized (this) { 325 mModified.put(key, this); 326 return this; 327 } 328 } 329 330 public Editor clear() { 331 synchronized (this) { 332 mClear = true; 333 return this; 334 } 335 } 336 337 public void apply() { 338 final MemoryCommitResult mcr = commitToMemory(); 339 final Runnable awaitCommit = new Runnable() { 340 public void run() { 341 try { 342 mcr.writtenToDiskLatch.await(); 343 } catch (InterruptedException ignored) { 344 } 345 } 346 }; 347 348 QueuedWork.add(awaitCommit); 349 350 Runnable postWriteRunnable = new Runnable() { 351 public void run() { 352 awaitCommit.run(); 353 QueuedWork.remove(awaitCommit); 354 } 355 }; 356 357 SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); 358 359 // Okay to notify the listeners before it's hit disk 360 // because the listeners should always get the same 361 // SharedPreferences instance back, which has the 362 // changes reflected in memory. 363 notifyListeners(mcr); 364 } 365 366 // Returns true if any changes were made 367 private MemoryCommitResult commitToMemory() { 368 MemoryCommitResult mcr = new MemoryCommitResult(); 369 synchronized (SharedPreferencesImpl.this) { 370 // We optimistically don't make a deep copy until 371 // a memory commit comes in when we're already 372 // writing to disk. 373 if (mDiskWritesInFlight > 0) { 374 // We can't modify our mMap as a currently 375 // in-flight write owns it. Clone it before 376 // modifying it. 377 // noinspection unchecked 378 mMap = new HashMap<String, Object>(mMap); 379 } 380 mcr.mapToWriteToDisk = mMap; 381 mDiskWritesInFlight++; 382 383 boolean hasListeners = mListeners.size() > 0; 384 if (hasListeners) { 385 mcr.keysModified = new ArrayList<String>(); 386 mcr.listeners = 387 new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet()); 388 } 389 390 synchronized (this) { 391 if (mClear) { 392 if (!mMap.isEmpty()) { 393 mcr.changesMade = true; 394 mMap.clear(); 395 } 396 mClear = false; 397 } 398 399 for (Map.Entry<String, Object> e : mModified.entrySet()) { 400 String k = e.getKey(); 401 Object v = e.getValue(); 402 if (v == this) { // magic value for a removal mutation 403 if (!mMap.containsKey(k)) { 404 continue; 405 } 406 mMap.remove(k); 407 } else { 408 boolean isSame = false; 409 if (mMap.containsKey(k)) { 410 Object existingValue = mMap.get(k); 411 if (existingValue != null && existingValue.equals(v)) { 412 continue; 413 } 414 } 415 mMap.put(k, v); 416 } 417 418 mcr.changesMade = true; 419 if (hasListeners) { 420 mcr.keysModified.add(k); 421 } 422 } 423 424 mModified.clear(); 425 } 426 } 427 return mcr; 428 } 429 430 public boolean commit() { 431 MemoryCommitResult mcr = commitToMemory(); 432 SharedPreferencesImpl.this.enqueueDiskWrite( 433 mcr, null /* sync write on this thread okay */); 434 try { 435 mcr.writtenToDiskLatch.await(); 436 } catch (InterruptedException e) { 437 return false; 438 } 439 notifyListeners(mcr); 440 return mcr.writeToDiskResult; 441 } 442 443 private void notifyListeners(final MemoryCommitResult mcr) { 444 if (mcr.listeners == null || mcr.keysModified == null || 445 mcr.keysModified.size() == 0) { 446 return; 447 } 448 if (Looper.myLooper() == Looper.getMainLooper()) { 449 for (int i = mcr.keysModified.size() - 1; i >= 0; i--) { 450 final String key = mcr.keysModified.get(i); 451 for (OnSharedPreferenceChangeListener listener : mcr.listeners) { 452 if (listener != null) { 453 listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, key); 454 } 455 } 456 } 457 } else { 458 // Run this function on the main thread. 459 ActivityThread.sMainThreadHandler.post(new Runnable() { 460 public void run() { 461 notifyListeners(mcr); 462 } 463 }); 464 } 465 } 466 } 467 468 /** 469 * Enqueue an already-committed-to-memory result to be written 470 * to disk. 471 * 472 * They will be written to disk one-at-a-time in the order 473 * that they're enqueued. 474 * 475 * @param postWriteRunnable if non-null, we're being called 476 * from apply() and this is the runnable to run after 477 * the write proceeds. if null (from a regular commit()), 478 * then we're allowed to do this disk write on the main 479 * thread (which in addition to reducing allocations and 480 * creating a background thread, this has the advantage that 481 * we catch them in userdebug StrictMode reports to convert 482 * them where possible to apply() ...) 483 */ 484 private void enqueueDiskWrite(final MemoryCommitResult mcr, 485 final Runnable postWriteRunnable) { 486 final Runnable writeToDiskRunnable = new Runnable() { 487 public void run() { 488 synchronized (mWritingToDiskLock) { 489 writeToFile(mcr); 490 } 491 synchronized (SharedPreferencesImpl.this) { 492 mDiskWritesInFlight--; 493 } 494 if (postWriteRunnable != null) { 495 postWriteRunnable.run(); 496 } 497 } 498 }; 499 500 final boolean isFromSyncCommit = (postWriteRunnable == null); 501 502 // Typical #commit() path with fewer allocations, doing a write on 503 // the current thread. 504 if (isFromSyncCommit) { 505 boolean wasEmpty = false; 506 synchronized (SharedPreferencesImpl.this) { 507 wasEmpty = mDiskWritesInFlight == 1; 508 } 509 if (wasEmpty) { 510 writeToDiskRunnable.run(); 511 return; 512 } 513 } 514 515 QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable); 516 } 517 518 private static FileOutputStream createFileOutputStream(File file) { 519 FileOutputStream str = null; 520 try { 521 str = new FileOutputStream(file); 522 } catch (FileNotFoundException e) { 523 File parent = file.getParentFile(); 524 if (!parent.mkdir()) { 525 Log.e(TAG, "Couldn't create directory for SharedPreferences file " + file); 526 return null; 527 } 528 FileUtils.setPermissions( 529 parent.getPath(), 530 FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH, 531 -1, -1); 532 try { 533 str = new FileOutputStream(file); 534 } catch (FileNotFoundException e2) { 535 Log.e(TAG, "Couldn't create SharedPreferences file " + file, e2); 536 } 537 } 538 return str; 539 } 540 541 // Note: must hold mWritingToDiskLock 542 private void writeToFile(MemoryCommitResult mcr) { 543 // Rename the current file so it may be used as a backup during the next read 544 if (mFile.exists()) { 545 if (!mcr.changesMade) { 546 // If the file already exists, but no changes were 547 // made to the underlying map, it's wasteful to 548 // re-write the file. Return as if we wrote it 549 // out. 550 mcr.setDiskWriteResult(true); 551 return; 552 } 553 if (!mBackupFile.exists()) { 554 if (!mFile.renameTo(mBackupFile)) { 555 Log.e(TAG, "Couldn't rename file " + mFile 556 + " to backup file " + mBackupFile); 557 mcr.setDiskWriteResult(false); 558 return; 559 } 560 } else { 561 mFile.delete(); 562 } 563 } 564 565 // Attempt to write the file, delete the backup and return true as atomically as 566 // possible. If any exception occurs, delete the new file; next time we will restore 567 // from the backup. 568 try { 569 FileOutputStream str = createFileOutputStream(mFile); 570 if (str == null) { 571 mcr.setDiskWriteResult(false); 572 return; 573 } 574 XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str); 575 FileUtils.sync(str); 576 str.close(); 577 ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0); 578 FileStatus stat = new FileStatus(); 579 if (FileUtils.getFileStatus(mFile.getPath(), stat)) { 580 synchronized (this) { 581 mStatTimestamp = stat.mtime; 582 mStatSize = stat.size; 583 } 584 } 585 // Writing was successful, delete the backup file if there is one. 586 mBackupFile.delete(); 587 mcr.setDiskWriteResult(true); 588 return; 589 } catch (XmlPullParserException e) { 590 Log.w(TAG, "writeToFile: Got exception:", e); 591 } catch (IOException e) { 592 Log.w(TAG, "writeToFile: Got exception:", e); 593 } 594 // Clean up an unsuccessfully written file 595 if (mFile.exists()) { 596 if (!mFile.delete()) { 597 Log.e(TAG, "Couldn't clean up partially-written file " + mFile); 598 } 599 } 600 mcr.setDiskWriteResult(false); 601 } 602} 603