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