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.annotation.Nullable; 20import android.content.SharedPreferences; 21import android.os.FileUtils; 22import android.os.Looper; 23import android.system.ErrnoException; 24import android.system.Os; 25import android.system.StructStat; 26import android.util.Log; 27 28import com.google.android.collect.Maps; 29import com.android.internal.util.XmlUtils; 30 31import dalvik.system.BlockGuard; 32 33import org.xmlpull.v1.XmlPullParserException; 34 35import java.io.BufferedInputStream; 36import java.io.File; 37import java.io.FileInputStream; 38import java.io.FileNotFoundException; 39import java.io.FileOutputStream; 40import java.io.IOException; 41import java.util.ArrayList; 42import java.util.HashMap; 43import java.util.HashSet; 44import java.util.List; 45import java.util.Map; 46import java.util.Set; 47import java.util.WeakHashMap; 48import java.util.concurrent.CountDownLatch; 49 50import libcore.io.IoUtils; 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 loadFromDisk(); 91 } 92 }.start(); 93 } 94 95 private void loadFromDisk() { 96 synchronized (SharedPreferencesImpl.this) { 97 if (mLoaded) { 98 return; 99 } 100 if (mBackupFile.exists()) { 101 mFile.delete(); 102 mBackupFile.renameTo(mFile); 103 } 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 = 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 | IOException e) { 122 Log.w(TAG, "getSharedPreferences", e); 123 } finally { 124 IoUtils.closeQuietly(str); 125 } 126 } 127 } catch (ErrnoException e) { 128 /* ignore */ 129 } 130 131 synchronized (SharedPreferencesImpl.this) { 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<>(); 139 } 140 notifyAll(); 141 } 142 } 143 144 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 = 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 @Nullable 222 public String getString(String key, @Nullable String defValue) { 223 synchronized (this) { 224 awaitLoadedLocked(); 225 String v = (String)mMap.get(key); 226 return v != null ? v : defValue; 227 } 228 } 229 230 @Nullable 231 public Set<String> getStringSet(String key, @Nullable Set<String> defValues) { 232 synchronized (this) { 233 awaitLoadedLocked(); 234 Set<String> v = (Set<String>) mMap.get(key); 235 return v != null ? v : defValues; 236 } 237 } 238 239 public int getInt(String key, int defValue) { 240 synchronized (this) { 241 awaitLoadedLocked(); 242 Integer v = (Integer)mMap.get(key); 243 return v != null ? v : defValue; 244 } 245 } 246 public long getLong(String key, long defValue) { 247 synchronized (this) { 248 awaitLoadedLocked(); 249 Long v = (Long)mMap.get(key); 250 return v != null ? v : defValue; 251 } 252 } 253 public float getFloat(String key, float defValue) { 254 synchronized (this) { 255 awaitLoadedLocked(); 256 Float v = (Float)mMap.get(key); 257 return v != null ? v : defValue; 258 } 259 } 260 public boolean getBoolean(String key, boolean defValue) { 261 synchronized (this) { 262 awaitLoadedLocked(); 263 Boolean v = (Boolean)mMap.get(key); 264 return v != null ? v : defValue; 265 } 266 } 267 268 public boolean contains(String key) { 269 synchronized (this) { 270 awaitLoadedLocked(); 271 return mMap.containsKey(key); 272 } 273 } 274 275 public Editor edit() { 276 // TODO: remove the need to call awaitLoadedLocked() when 277 // requesting an editor. will require some work on the 278 // Editor, but then we should be able to do: 279 // 280 // context.getSharedPreferences(..).edit().putString(..).apply() 281 // 282 // ... all without blocking. 283 synchronized (this) { 284 awaitLoadedLocked(); 285 } 286 287 return new EditorImpl(); 288 } 289 290 // Return value from EditorImpl#commitToMemory() 291 private static class MemoryCommitResult { 292 public boolean changesMade; // any keys different? 293 public List<String> keysModified; // may be null 294 public Set<OnSharedPreferenceChangeListener> listeners; // may be null 295 public Map<?, ?> mapToWriteToDisk; 296 public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1); 297 public volatile boolean writeToDiskResult = false; 298 299 public void setDiskWriteResult(boolean result) { 300 writeToDiskResult = result; 301 writtenToDiskLatch.countDown(); 302 } 303 } 304 305 public final class EditorImpl implements Editor { 306 private final Map<String, Object> mModified = Maps.newHashMap(); 307 private boolean mClear = false; 308 309 public Editor putString(String key, @Nullable String value) { 310 synchronized (this) { 311 mModified.put(key, value); 312 return this; 313 } 314 } 315 public Editor putStringSet(String key, @Nullable Set<String> values) { 316 synchronized (this) { 317 mModified.put(key, 318 (values == null) ? null : new HashSet<String>(values)); 319 return this; 320 } 321 } 322 public Editor putInt(String key, int value) { 323 synchronized (this) { 324 mModified.put(key, value); 325 return this; 326 } 327 } 328 public Editor putLong(String key, long value) { 329 synchronized (this) { 330 mModified.put(key, value); 331 return this; 332 } 333 } 334 public Editor putFloat(String key, float value) { 335 synchronized (this) { 336 mModified.put(key, value); 337 return this; 338 } 339 } 340 public Editor putBoolean(String key, boolean value) { 341 synchronized (this) { 342 mModified.put(key, value); 343 return this; 344 } 345 } 346 347 public Editor remove(String key) { 348 synchronized (this) { 349 mModified.put(key, this); 350 return this; 351 } 352 } 353 354 public Editor clear() { 355 synchronized (this) { 356 mClear = true; 357 return this; 358 } 359 } 360 361 public void apply() { 362 final MemoryCommitResult mcr = commitToMemory(); 363 final Runnable awaitCommit = new Runnable() { 364 public void run() { 365 try { 366 mcr.writtenToDiskLatch.await(); 367 } catch (InterruptedException ignored) { 368 } 369 } 370 }; 371 372 QueuedWork.add(awaitCommit); 373 374 Runnable postWriteRunnable = new Runnable() { 375 public void run() { 376 awaitCommit.run(); 377 QueuedWork.remove(awaitCommit); 378 } 379 }; 380 381 SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); 382 383 // Okay to notify the listeners before it's hit disk 384 // because the listeners should always get the same 385 // SharedPreferences instance back, which has the 386 // changes reflected in memory. 387 notifyListeners(mcr); 388 } 389 390 // Returns true if any changes were made 391 private MemoryCommitResult commitToMemory() { 392 MemoryCommitResult mcr = new MemoryCommitResult(); 393 synchronized (SharedPreferencesImpl.this) { 394 // We optimistically don't make a deep copy until 395 // a memory commit comes in when we're already 396 // writing to disk. 397 if (mDiskWritesInFlight > 0) { 398 // We can't modify our mMap as a currently 399 // in-flight write owns it. Clone it before 400 // modifying it. 401 // noinspection unchecked 402 mMap = new HashMap<String, Object>(mMap); 403 } 404 mcr.mapToWriteToDisk = mMap; 405 mDiskWritesInFlight++; 406 407 boolean hasListeners = mListeners.size() > 0; 408 if (hasListeners) { 409 mcr.keysModified = new ArrayList<String>(); 410 mcr.listeners = 411 new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet()); 412 } 413 414 synchronized (this) { 415 if (mClear) { 416 if (!mMap.isEmpty()) { 417 mcr.changesMade = true; 418 mMap.clear(); 419 } 420 mClear = false; 421 } 422 423 for (Map.Entry<String, Object> e : mModified.entrySet()) { 424 String k = e.getKey(); 425 Object v = e.getValue(); 426 // "this" is the magic value for a removal mutation. In addition, 427 // setting a value to "null" for a given key is specified to be 428 // equivalent to calling remove on that key. 429 if (v == this || v == null) { 430 if (!mMap.containsKey(k)) { 431 continue; 432 } 433 mMap.remove(k); 434 } else { 435 if (mMap.containsKey(k)) { 436 Object existingValue = mMap.get(k); 437 if (existingValue != null && existingValue.equals(v)) { 438 continue; 439 } 440 } 441 mMap.put(k, v); 442 } 443 444 mcr.changesMade = true; 445 if (hasListeners) { 446 mcr.keysModified.add(k); 447 } 448 } 449 450 mModified.clear(); 451 } 452 } 453 return mcr; 454 } 455 456 public boolean commit() { 457 MemoryCommitResult mcr = commitToMemory(); 458 SharedPreferencesImpl.this.enqueueDiskWrite( 459 mcr, null /* sync write on this thread okay */); 460 try { 461 mcr.writtenToDiskLatch.await(); 462 } catch (InterruptedException e) { 463 return false; 464 } 465 notifyListeners(mcr); 466 return mcr.writeToDiskResult; 467 } 468 469 private void notifyListeners(final MemoryCommitResult mcr) { 470 if (mcr.listeners == null || mcr.keysModified == null || 471 mcr.keysModified.size() == 0) { 472 return; 473 } 474 if (Looper.myLooper() == Looper.getMainLooper()) { 475 for (int i = mcr.keysModified.size() - 1; i >= 0; i--) { 476 final String key = mcr.keysModified.get(i); 477 for (OnSharedPreferenceChangeListener listener : mcr.listeners) { 478 if (listener != null) { 479 listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, key); 480 } 481 } 482 } 483 } else { 484 // Run this function on the main thread. 485 ActivityThread.sMainThreadHandler.post(new Runnable() { 486 public void run() { 487 notifyListeners(mcr); 488 } 489 }); 490 } 491 } 492 } 493 494 /** 495 * Enqueue an already-committed-to-memory result to be written 496 * to disk. 497 * 498 * They will be written to disk one-at-a-time in the order 499 * that they're enqueued. 500 * 501 * @param postWriteRunnable if non-null, we're being called 502 * from apply() and this is the runnable to run after 503 * the write proceeds. if null (from a regular commit()), 504 * then we're allowed to do this disk write on the main 505 * thread (which in addition to reducing allocations and 506 * creating a background thread, this has the advantage that 507 * we catch them in userdebug StrictMode reports to convert 508 * them where possible to apply() ...) 509 */ 510 private void enqueueDiskWrite(final MemoryCommitResult mcr, 511 final Runnable postWriteRunnable) { 512 final Runnable writeToDiskRunnable = new Runnable() { 513 public void run() { 514 synchronized (mWritingToDiskLock) { 515 writeToFile(mcr); 516 } 517 synchronized (SharedPreferencesImpl.this) { 518 mDiskWritesInFlight--; 519 } 520 if (postWriteRunnable != null) { 521 postWriteRunnable.run(); 522 } 523 } 524 }; 525 526 final boolean isFromSyncCommit = (postWriteRunnable == null); 527 528 // Typical #commit() path with fewer allocations, doing a write on 529 // the current thread. 530 if (isFromSyncCommit) { 531 boolean wasEmpty = false; 532 synchronized (SharedPreferencesImpl.this) { 533 wasEmpty = mDiskWritesInFlight == 1; 534 } 535 if (wasEmpty) { 536 writeToDiskRunnable.run(); 537 return; 538 } 539 } 540 541 QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable); 542 } 543 544 private static FileOutputStream createFileOutputStream(File file) { 545 FileOutputStream str = null; 546 try { 547 str = new FileOutputStream(file); 548 } catch (FileNotFoundException e) { 549 File parent = file.getParentFile(); 550 if (!parent.mkdir()) { 551 Log.e(TAG, "Couldn't create directory for SharedPreferences file " + file); 552 return null; 553 } 554 FileUtils.setPermissions( 555 parent.getPath(), 556 FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH, 557 -1, -1); 558 try { 559 str = new FileOutputStream(file); 560 } catch (FileNotFoundException e2) { 561 Log.e(TAG, "Couldn't create SharedPreferences file " + file, e2); 562 } 563 } 564 return str; 565 } 566 567 // Note: must hold mWritingToDiskLock 568 private void writeToFile(MemoryCommitResult mcr) { 569 // Rename the current file so it may be used as a backup during the next read 570 if (mFile.exists()) { 571 if (!mcr.changesMade) { 572 // If the file already exists, but no changes were 573 // made to the underlying map, it's wasteful to 574 // re-write the file. Return as if we wrote it 575 // out. 576 mcr.setDiskWriteResult(true); 577 return; 578 } 579 if (!mBackupFile.exists()) { 580 if (!mFile.renameTo(mBackupFile)) { 581 Log.e(TAG, "Couldn't rename file " + mFile 582 + " to backup file " + mBackupFile); 583 mcr.setDiskWriteResult(false); 584 return; 585 } 586 } else { 587 mFile.delete(); 588 } 589 } 590 591 // Attempt to write the file, delete the backup and return true as atomically as 592 // possible. If any exception occurs, delete the new file; next time we will restore 593 // from the backup. 594 try { 595 FileOutputStream str = createFileOutputStream(mFile); 596 if (str == null) { 597 mcr.setDiskWriteResult(false); 598 return; 599 } 600 XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str); 601 FileUtils.sync(str); 602 str.close(); 603 ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0); 604 try { 605 final StructStat stat = Os.stat(mFile.getPath()); 606 synchronized (this) { 607 mStatTimestamp = stat.st_mtime; 608 mStatSize = stat.st_size; 609 } 610 } catch (ErrnoException e) { 611 // Do nothing 612 } 613 // Writing was successful, delete the backup file if there is one. 614 mBackupFile.delete(); 615 mcr.setDiskWriteResult(true); 616 return; 617 } catch (XmlPullParserException e) { 618 Log.w(TAG, "writeToFile: Got exception:", e); 619 } catch (IOException e) { 620 Log.w(TAG, "writeToFile: Got exception:", e); 621 } 622 // Clean up an unsuccessfully written file 623 if (mFile.exists()) { 624 if (!mFile.delete()) { 625 Log.e(TAG, "Couldn't clean up partially-written file " + mFile); 626 } 627 } 628 mcr.setDiskWriteResult(false); 629 } 630} 631