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