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