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.annotation.Nullable; 20 import android.compat.Compatibility; 21 import android.compat.annotation.ChangeId; 22 import android.compat.annotation.EnabledSince; 23 import android.compat.annotation.UnsupportedAppUsage; 24 import android.content.SharedPreferences; 25 import android.os.Build; 26 import android.os.FileUtils; 27 import android.os.Looper; 28 import android.system.ErrnoException; 29 import android.system.Os; 30 import android.system.StructStat; 31 import android.system.StructTimespec; 32 import android.util.Log; 33 34 import com.android.internal.annotations.GuardedBy; 35 import com.android.internal.util.ExponentiallyBucketedHistogram; 36 import com.android.internal.util.XmlUtils; 37 38 import dalvik.system.BlockGuard; 39 40 import libcore.io.IoUtils; 41 42 import org.xmlpull.v1.XmlPullParserException; 43 44 import java.io.BufferedInputStream; 45 import java.io.File; 46 import java.io.FileInputStream; 47 import java.io.FileNotFoundException; 48 import java.io.FileOutputStream; 49 import java.io.IOException; 50 import java.util.ArrayList; 51 import java.util.HashMap; 52 import java.util.HashSet; 53 import java.util.List; 54 import java.util.Map; 55 import java.util.Set; 56 import java.util.WeakHashMap; 57 import java.util.concurrent.CountDownLatch; 58 import java.util.concurrent.Executors; 59 import java.util.concurrent.LinkedBlockingQueue; 60 import java.util.concurrent.ThreadFactory; 61 import java.util.concurrent.ThreadPoolExecutor; 62 import java.util.concurrent.TimeUnit; 63 64 final class SharedPreferencesImpl implements SharedPreferences { 65 private static final String TAG = "SharedPreferencesImpl"; 66 private static final boolean DEBUG = false; 67 private static final Object CONTENT = new Object(); 68 69 /** If a fsync takes more than {@value #MAX_FSYNC_DURATION_MILLIS} ms, warn */ 70 private static final long MAX_FSYNC_DURATION_MILLIS = 256; 71 72 /** 73 * There will now be a callback to {@link 74 * android.content.SharedPreferences.OnSharedPreferenceChangeListener#onSharedPreferenceChanged 75 * OnSharedPreferenceChangeListener.onSharedPreferenceChanged} with a {@code null} key on 76 * {@link android.content.SharedPreferences.Editor#clear Editor.clear}. 77 */ 78 @ChangeId 79 @EnabledSince(targetSdkVersion = Build.VERSION_CODES.R) 80 private static final long CALLBACK_ON_CLEAR_CHANGE = 119147584L; 81 82 // Lock ordering rules: 83 // - acquire SharedPreferencesImpl.mLock before EditorImpl.mLock 84 // - acquire mWritingToDiskLock before EditorImpl.mLock 85 86 @UnsupportedAppUsage 87 private final File mFile; 88 private final File mBackupFile; 89 private final int mMode; 90 private final Object mLock = new Object(); 91 private final Object mWritingToDiskLock = new Object(); 92 93 @GuardedBy("mLock") 94 private Map<String, Object> mMap; 95 @GuardedBy("mLock") 96 private Throwable mThrowable; 97 98 @GuardedBy("mLock") 99 private int mDiskWritesInFlight = 0; 100 101 @GuardedBy("mLock") 102 private boolean mLoaded = false; 103 104 @GuardedBy("mLock") 105 private StructTimespec mStatTimestamp; 106 107 @GuardedBy("mLock") 108 private long mStatSize; 109 110 @GuardedBy("mLock") 111 private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners = 112 new WeakHashMap<OnSharedPreferenceChangeListener, Object>(); 113 114 /** Current memory state (always increasing) */ 115 @GuardedBy("this") 116 private long mCurrentMemoryStateGeneration; 117 118 /** Latest memory state that was committed to disk */ 119 @GuardedBy("mWritingToDiskLock") 120 private long mDiskStateGeneration; 121 122 /** Time (and number of instances) of file-system sync requests */ 123 @GuardedBy("mWritingToDiskLock") 124 private final ExponentiallyBucketedHistogram mSyncTimes = new ExponentiallyBucketedHistogram(16); 125 private int mNumSync = 0; 126 127 private static final ThreadPoolExecutor sLoadExecutor = new ThreadPoolExecutor(0, 1, 10L, 128 TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), 129 new SharedPreferencesThreadFactory()); 130 131 @UnsupportedAppUsage SharedPreferencesImpl(File file, int mode)132 SharedPreferencesImpl(File file, int mode) { 133 mFile = file; 134 mBackupFile = makeBackupFile(file); 135 mMode = mode; 136 mLoaded = false; 137 mMap = null; 138 mThrowable = null; 139 startLoadFromDisk(); 140 } 141 142 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) startLoadFromDisk()143 private void startLoadFromDisk() { 144 synchronized (mLock) { 145 mLoaded = false; 146 } 147 148 sLoadExecutor.execute(() -> { 149 loadFromDisk(); 150 }); 151 } 152 loadFromDisk()153 private void loadFromDisk() { 154 synchronized (mLock) { 155 if (mLoaded) { 156 return; 157 } 158 if (mBackupFile.exists()) { 159 mFile.delete(); 160 mBackupFile.renameTo(mFile); 161 } 162 } 163 164 // Debugging 165 if (mFile.exists() && !mFile.canRead()) { 166 Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission"); 167 } 168 169 Map<String, Object> map = null; 170 StructStat stat = null; 171 Throwable thrown = null; 172 try { 173 stat = Os.stat(mFile.getPath()); 174 if (mFile.canRead()) { 175 BufferedInputStream str = null; 176 try { 177 str = new BufferedInputStream( 178 new FileInputStream(mFile), 16 * 1024); 179 map = (Map<String, Object>) XmlUtils.readMapXml(str); 180 } catch (Exception e) { 181 Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e); 182 } finally { 183 IoUtils.closeQuietly(str); 184 } 185 } 186 } catch (ErrnoException e) { 187 // An errno exception means the stat failed. Treat as empty/non-existing by 188 // ignoring. 189 } catch (Throwable t) { 190 thrown = t; 191 } 192 193 synchronized (mLock) { 194 mLoaded = true; 195 mThrowable = thrown; 196 197 // It's important that we always signal waiters, even if we'll make 198 // them fail with an exception. The try-finally is pretty wide, but 199 // better safe than sorry. 200 try { 201 if (thrown == null) { 202 if (map != null) { 203 mMap = map; 204 mStatTimestamp = stat.st_mtim; 205 mStatSize = stat.st_size; 206 } else { 207 mMap = new HashMap<>(); 208 } 209 } 210 // In case of a thrown exception, we retain the old map. That allows 211 // any open editors to commit and store updates. 212 } catch (Throwable t) { 213 mThrowable = t; 214 } finally { 215 mLock.notifyAll(); 216 } 217 } 218 } 219 makeBackupFile(File prefsFile)220 static File makeBackupFile(File prefsFile) { 221 return new File(prefsFile.getPath() + ".bak"); 222 } 223 224 @UnsupportedAppUsage startReloadIfChangedUnexpectedly()225 void startReloadIfChangedUnexpectedly() { 226 synchronized (mLock) { 227 // TODO: wait for any pending writes to disk? 228 if (!hasFileChangedUnexpectedly()) { 229 return; 230 } 231 startLoadFromDisk(); 232 } 233 } 234 235 // Has the file changed out from under us? i.e. writes that 236 // we didn't instigate. hasFileChangedUnexpectedly()237 private boolean hasFileChangedUnexpectedly() { 238 synchronized (mLock) { 239 if (mDiskWritesInFlight > 0) { 240 // If we know we caused it, it's not unexpected. 241 if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected."); 242 return false; 243 } 244 } 245 246 final StructStat stat; 247 try { 248 /* 249 * Metadata operations don't usually count as a block guard 250 * violation, but we explicitly want this one. 251 */ 252 BlockGuard.getThreadPolicy().onReadFromDisk(); 253 stat = Os.stat(mFile.getPath()); 254 } catch (ErrnoException e) { 255 return true; 256 } 257 258 synchronized (mLock) { 259 return !stat.st_mtim.equals(mStatTimestamp) || mStatSize != stat.st_size; 260 } 261 } 262 263 @Override registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener)264 public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { 265 synchronized(mLock) { 266 mListeners.put(listener, CONTENT); 267 } 268 } 269 270 @Override unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener)271 public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { 272 synchronized(mLock) { 273 mListeners.remove(listener); 274 } 275 } 276 277 @GuardedBy("mLock") awaitLoadedLocked()278 private void awaitLoadedLocked() { 279 if (!mLoaded) { 280 // Raise an explicit StrictMode onReadFromDisk for this 281 // thread, since the real read will be in a different 282 // thread and otherwise ignored by StrictMode. 283 BlockGuard.getThreadPolicy().onReadFromDisk(); 284 } 285 while (!mLoaded) { 286 try { 287 mLock.wait(); 288 } catch (InterruptedException unused) { 289 } 290 } 291 if (mThrowable != null) { 292 throw new IllegalStateException(mThrowable); 293 } 294 } 295 296 @Override getAll()297 public Map<String, ?> getAll() { 298 synchronized (mLock) { 299 awaitLoadedLocked(); 300 //noinspection unchecked 301 return new HashMap<String, Object>(mMap); 302 } 303 } 304 305 @Override 306 @Nullable getString(String key, @Nullable String defValue)307 public String getString(String key, @Nullable String defValue) { 308 synchronized (mLock) { 309 awaitLoadedLocked(); 310 String v = (String)mMap.get(key); 311 return v != null ? v : defValue; 312 } 313 } 314 315 @Override 316 @Nullable getStringSet(String key, @Nullable Set<String> defValues)317 public Set<String> getStringSet(String key, @Nullable Set<String> defValues) { 318 synchronized (mLock) { 319 awaitLoadedLocked(); 320 Set<String> v = (Set<String>) mMap.get(key); 321 return v != null ? v : defValues; 322 } 323 } 324 325 @Override getInt(String key, int defValue)326 public int getInt(String key, int defValue) { 327 synchronized (mLock) { 328 awaitLoadedLocked(); 329 Integer v = (Integer)mMap.get(key); 330 return v != null ? v : defValue; 331 } 332 } 333 @Override getLong(String key, long defValue)334 public long getLong(String key, long defValue) { 335 synchronized (mLock) { 336 awaitLoadedLocked(); 337 Long v = (Long)mMap.get(key); 338 return v != null ? v : defValue; 339 } 340 } 341 @Override getFloat(String key, float defValue)342 public float getFloat(String key, float defValue) { 343 synchronized (mLock) { 344 awaitLoadedLocked(); 345 Float v = (Float)mMap.get(key); 346 return v != null ? v : defValue; 347 } 348 } 349 @Override getBoolean(String key, boolean defValue)350 public boolean getBoolean(String key, boolean defValue) { 351 synchronized (mLock) { 352 awaitLoadedLocked(); 353 Boolean v = (Boolean)mMap.get(key); 354 return v != null ? v : defValue; 355 } 356 } 357 358 @Override contains(String key)359 public boolean contains(String key) { 360 synchronized (mLock) { 361 awaitLoadedLocked(); 362 return mMap.containsKey(key); 363 } 364 } 365 366 @Override edit()367 public Editor edit() { 368 // TODO: remove the need to call awaitLoadedLocked() when 369 // requesting an editor. will require some work on the 370 // Editor, but then we should be able to do: 371 // 372 // context.getSharedPreferences(..).edit().putString(..).apply() 373 // 374 // ... all without blocking. 375 synchronized (mLock) { 376 awaitLoadedLocked(); 377 } 378 379 return new EditorImpl(); 380 } 381 382 // Return value from EditorImpl#commitToMemory() 383 private static class MemoryCommitResult { 384 final long memoryStateGeneration; 385 final boolean keysCleared; 386 @Nullable final List<String> keysModified; 387 @Nullable final Set<OnSharedPreferenceChangeListener> listeners; 388 final Map<String, Object> mapToWriteToDisk; 389 final CountDownLatch writtenToDiskLatch = new CountDownLatch(1); 390 391 @GuardedBy("mWritingToDiskLock") 392 volatile boolean writeToDiskResult = false; 393 boolean wasWritten = false; 394 MemoryCommitResult(long memoryStateGeneration, boolean keysCleared, @Nullable List<String> keysModified, @Nullable Set<OnSharedPreferenceChangeListener> listeners, Map<String, Object> mapToWriteToDisk)395 private MemoryCommitResult(long memoryStateGeneration, boolean keysCleared, 396 @Nullable List<String> keysModified, 397 @Nullable Set<OnSharedPreferenceChangeListener> listeners, 398 Map<String, Object> mapToWriteToDisk) { 399 this.memoryStateGeneration = memoryStateGeneration; 400 this.keysCleared = keysCleared; 401 this.keysModified = keysModified; 402 this.listeners = listeners; 403 this.mapToWriteToDisk = mapToWriteToDisk; 404 } 405 setDiskWriteResult(boolean wasWritten, boolean result)406 void setDiskWriteResult(boolean wasWritten, boolean result) { 407 this.wasWritten = wasWritten; 408 writeToDiskResult = result; 409 writtenToDiskLatch.countDown(); 410 } 411 } 412 413 public final class EditorImpl implements Editor { 414 private final Object mEditorLock = new Object(); 415 416 @GuardedBy("mEditorLock") 417 private final Map<String, Object> mModified = new HashMap<>(); 418 419 @GuardedBy("mEditorLock") 420 private boolean mClear = false; 421 422 @Override putString(String key, @Nullable String value)423 public Editor putString(String key, @Nullable String value) { 424 synchronized (mEditorLock) { 425 mModified.put(key, value); 426 return this; 427 } 428 } 429 @Override putStringSet(String key, @Nullable Set<String> values)430 public Editor putStringSet(String key, @Nullable Set<String> values) { 431 synchronized (mEditorLock) { 432 mModified.put(key, 433 (values == null) ? null : new HashSet<String>(values)); 434 return this; 435 } 436 } 437 @Override putInt(String key, int value)438 public Editor putInt(String key, int value) { 439 synchronized (mEditorLock) { 440 mModified.put(key, value); 441 return this; 442 } 443 } 444 @Override putLong(String key, long value)445 public Editor putLong(String key, long value) { 446 synchronized (mEditorLock) { 447 mModified.put(key, value); 448 return this; 449 } 450 } 451 @Override putFloat(String key, float value)452 public Editor putFloat(String key, float value) { 453 synchronized (mEditorLock) { 454 mModified.put(key, value); 455 return this; 456 } 457 } 458 @Override putBoolean(String key, boolean value)459 public Editor putBoolean(String key, boolean value) { 460 synchronized (mEditorLock) { 461 mModified.put(key, value); 462 return this; 463 } 464 } 465 466 @Override remove(String key)467 public Editor remove(String key) { 468 synchronized (mEditorLock) { 469 mModified.put(key, this); 470 return this; 471 } 472 } 473 474 @Override clear()475 public Editor clear() { 476 synchronized (mEditorLock) { 477 mClear = true; 478 return this; 479 } 480 } 481 482 @Override apply()483 public void apply() { 484 final long startTime = System.currentTimeMillis(); 485 486 final MemoryCommitResult mcr = commitToMemory(); 487 final Runnable awaitCommit = new Runnable() { 488 @Override 489 public void run() { 490 try { 491 mcr.writtenToDiskLatch.await(); 492 } catch (InterruptedException ignored) { 493 } 494 495 if (DEBUG && mcr.wasWritten) { 496 Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration 497 + " applied after " + (System.currentTimeMillis() - startTime) 498 + " ms"); 499 } 500 } 501 }; 502 503 QueuedWork.addFinisher(awaitCommit); 504 505 Runnable postWriteRunnable = new Runnable() { 506 @Override 507 public void run() { 508 awaitCommit.run(); 509 QueuedWork.removeFinisher(awaitCommit); 510 } 511 }; 512 513 SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); 514 515 // Okay to notify the listeners before it's hit disk 516 // because the listeners should always get the same 517 // SharedPreferences instance back, which has the 518 // changes reflected in memory. 519 notifyListeners(mcr); 520 } 521 522 // Returns true if any changes were made commitToMemory()523 private MemoryCommitResult commitToMemory() { 524 long memoryStateGeneration; 525 boolean keysCleared = false; 526 List<String> keysModified = null; 527 Set<OnSharedPreferenceChangeListener> listeners = null; 528 Map<String, Object> mapToWriteToDisk; 529 530 synchronized (SharedPreferencesImpl.this.mLock) { 531 // We optimistically don't make a deep copy until 532 // a memory commit comes in when we're already 533 // writing to disk. 534 if (mDiskWritesInFlight > 0) { 535 // We can't modify our mMap as a currently 536 // in-flight write owns it. Clone it before 537 // modifying it. 538 // noinspection unchecked 539 mMap = new HashMap<String, Object>(mMap); 540 } 541 mapToWriteToDisk = mMap; 542 mDiskWritesInFlight++; 543 544 boolean hasListeners = mListeners.size() > 0; 545 if (hasListeners) { 546 keysModified = new ArrayList<String>(); 547 listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet()); 548 } 549 550 synchronized (mEditorLock) { 551 boolean changesMade = false; 552 553 if (mClear) { 554 if (!mapToWriteToDisk.isEmpty()) { 555 changesMade = true; 556 mapToWriteToDisk.clear(); 557 } 558 keysCleared = true; 559 mClear = false; 560 } 561 562 for (Map.Entry<String, Object> e : mModified.entrySet()) { 563 String k = e.getKey(); 564 Object v = e.getValue(); 565 // "this" is the magic value for a removal mutation. In addition, 566 // setting a value to "null" for a given key is specified to be 567 // equivalent to calling remove on that key. 568 if (v == this || v == null) { 569 if (!mapToWriteToDisk.containsKey(k)) { 570 continue; 571 } 572 mapToWriteToDisk.remove(k); 573 } else { 574 if (mapToWriteToDisk.containsKey(k)) { 575 Object existingValue = mapToWriteToDisk.get(k); 576 if (existingValue != null && existingValue.equals(v)) { 577 continue; 578 } 579 } 580 mapToWriteToDisk.put(k, v); 581 } 582 583 changesMade = true; 584 if (hasListeners) { 585 keysModified.add(k); 586 } 587 } 588 589 mModified.clear(); 590 591 if (changesMade) { 592 mCurrentMemoryStateGeneration++; 593 } 594 595 memoryStateGeneration = mCurrentMemoryStateGeneration; 596 } 597 } 598 return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified, 599 listeners, mapToWriteToDisk); 600 } 601 602 @Override commit()603 public boolean commit() { 604 long startTime = 0; 605 606 if (DEBUG) { 607 startTime = System.currentTimeMillis(); 608 } 609 610 MemoryCommitResult mcr = commitToMemory(); 611 612 SharedPreferencesImpl.this.enqueueDiskWrite( 613 mcr, null /* sync write on this thread okay */); 614 try { 615 mcr.writtenToDiskLatch.await(); 616 } catch (InterruptedException e) { 617 return false; 618 } finally { 619 if (DEBUG) { 620 Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration 621 + " committed after " + (System.currentTimeMillis() - startTime) 622 + " ms"); 623 } 624 } 625 notifyListeners(mcr); 626 return mcr.writeToDiskResult; 627 } 628 notifyListeners(final MemoryCommitResult mcr)629 private void notifyListeners(final MemoryCommitResult mcr) { 630 if (mcr.listeners == null || (mcr.keysModified == null && !mcr.keysCleared)) { 631 return; 632 } 633 if (Looper.myLooper() == Looper.getMainLooper()) { 634 if (mcr.keysCleared && Compatibility.isChangeEnabled(CALLBACK_ON_CLEAR_CHANGE)) { 635 for (OnSharedPreferenceChangeListener listener : mcr.listeners) { 636 if (listener != null) { 637 listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, null); 638 } 639 } 640 } 641 for (int i = mcr.keysModified.size() - 1; i >= 0; i--) { 642 final String key = mcr.keysModified.get(i); 643 for (OnSharedPreferenceChangeListener listener : mcr.listeners) { 644 if (listener != null) { 645 listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, key); 646 } 647 } 648 } 649 } else { 650 // Run this function on the main thread. 651 ActivityThread.sMainThreadHandler.post(() -> notifyListeners(mcr)); 652 } 653 } 654 } 655 656 /** 657 * Enqueue an already-committed-to-memory result to be written 658 * to disk. 659 * 660 * They will be written to disk one-at-a-time in the order 661 * that they're enqueued. 662 * 663 * @param postWriteRunnable if non-null, we're being called 664 * from apply() and this is the runnable to run after 665 * the write proceeds. if null (from a regular commit()), 666 * then we're allowed to do this disk write on the main 667 * thread (which in addition to reducing allocations and 668 * creating a background thread, this has the advantage that 669 * we catch them in userdebug StrictMode reports to convert 670 * them where possible to apply() ...) 671 */ enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable)672 private void enqueueDiskWrite(final MemoryCommitResult mcr, 673 final Runnable postWriteRunnable) { 674 final boolean isFromSyncCommit = (postWriteRunnable == null); 675 676 final Runnable writeToDiskRunnable = new Runnable() { 677 @Override 678 public void run() { 679 synchronized (mWritingToDiskLock) { 680 writeToFile(mcr, isFromSyncCommit); 681 } 682 synchronized (mLock) { 683 mDiskWritesInFlight--; 684 } 685 if (postWriteRunnable != null) { 686 postWriteRunnable.run(); 687 } 688 } 689 }; 690 691 // Typical #commit() path with fewer allocations, doing a write on 692 // the current thread. 693 if (isFromSyncCommit) { 694 boolean wasEmpty = false; 695 synchronized (mLock) { 696 wasEmpty = mDiskWritesInFlight == 1; 697 } 698 if (wasEmpty) { 699 writeToDiskRunnable.run(); 700 return; 701 } 702 } 703 704 QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit); 705 } 706 createFileOutputStream(File file)707 private static FileOutputStream createFileOutputStream(File file) { 708 FileOutputStream str = null; 709 try { 710 str = new FileOutputStream(file); 711 } catch (FileNotFoundException e) { 712 File parent = file.getParentFile(); 713 if (!parent.mkdir()) { 714 Log.e(TAG, "Couldn't create directory for SharedPreferences file " + file); 715 return null; 716 } 717 FileUtils.setPermissions( 718 parent.getPath(), 719 FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH, 720 -1, -1); 721 try { 722 str = new FileOutputStream(file); 723 } catch (FileNotFoundException e2) { 724 Log.e(TAG, "Couldn't create SharedPreferences file " + file, e2); 725 } 726 } 727 return str; 728 } 729 730 @GuardedBy("mWritingToDiskLock") writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit)731 private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) { 732 long startTime = 0; 733 long existsTime = 0; 734 long backupExistsTime = 0; 735 long outputStreamCreateTime = 0; 736 long writeTime = 0; 737 long fsyncTime = 0; 738 long setPermTime = 0; 739 long fstatTime = 0; 740 long deleteTime = 0; 741 742 if (DEBUG) { 743 startTime = System.currentTimeMillis(); 744 } 745 746 boolean fileExists = mFile.exists(); 747 748 if (DEBUG) { 749 existsTime = System.currentTimeMillis(); 750 751 // Might not be set, hence init them to a default value 752 backupExistsTime = existsTime; 753 } 754 755 // Rename the current file so it may be used as a backup during the next read 756 if (fileExists) { 757 boolean needsWrite = false; 758 759 // Only need to write if the disk state is older than this commit 760 if (mDiskStateGeneration < mcr.memoryStateGeneration) { 761 if (isFromSyncCommit) { 762 needsWrite = true; 763 } else { 764 synchronized (mLock) { 765 // No need to persist intermediate states. Just wait for the latest state to 766 // be persisted. 767 if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) { 768 needsWrite = true; 769 } 770 } 771 } 772 } 773 774 if (!needsWrite) { 775 mcr.setDiskWriteResult(false, true); 776 return; 777 } 778 779 boolean backupFileExists = mBackupFile.exists(); 780 781 if (DEBUG) { 782 backupExistsTime = System.currentTimeMillis(); 783 } 784 785 if (!backupFileExists) { 786 if (!mFile.renameTo(mBackupFile)) { 787 Log.e(TAG, "Couldn't rename file " + mFile 788 + " to backup file " + mBackupFile); 789 mcr.setDiskWriteResult(false, false); 790 return; 791 } 792 } else { 793 mFile.delete(); 794 } 795 } 796 797 // Attempt to write the file, delete the backup and return true as atomically as 798 // possible. If any exception occurs, delete the new file; next time we will restore 799 // from the backup. 800 try { 801 FileOutputStream str = createFileOutputStream(mFile); 802 803 if (DEBUG) { 804 outputStreamCreateTime = System.currentTimeMillis(); 805 } 806 807 if (str == null) { 808 mcr.setDiskWriteResult(false, false); 809 return; 810 } 811 XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str); 812 813 writeTime = System.currentTimeMillis(); 814 815 FileUtils.sync(str); 816 817 fsyncTime = System.currentTimeMillis(); 818 819 str.close(); 820 ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0); 821 822 if (DEBUG) { 823 setPermTime = System.currentTimeMillis(); 824 } 825 826 try { 827 final StructStat stat = Os.stat(mFile.getPath()); 828 synchronized (mLock) { 829 mStatTimestamp = stat.st_mtim; 830 mStatSize = stat.st_size; 831 } 832 } catch (ErrnoException e) { 833 // Do nothing 834 } 835 836 if (DEBUG) { 837 fstatTime = System.currentTimeMillis(); 838 } 839 840 // Writing was successful, delete the backup file if there is one. 841 mBackupFile.delete(); 842 843 if (DEBUG) { 844 deleteTime = System.currentTimeMillis(); 845 } 846 847 mDiskStateGeneration = mcr.memoryStateGeneration; 848 849 mcr.setDiskWriteResult(true, true); 850 851 if (DEBUG) { 852 Log.d(TAG, "write: " + (existsTime - startTime) + "/" 853 + (backupExistsTime - startTime) + "/" 854 + (outputStreamCreateTime - startTime) + "/" 855 + (writeTime - startTime) + "/" 856 + (fsyncTime - startTime) + "/" 857 + (setPermTime - startTime) + "/" 858 + (fstatTime - startTime) + "/" 859 + (deleteTime - startTime)); 860 } 861 862 long fsyncDuration = fsyncTime - writeTime; 863 mSyncTimes.add((int) fsyncDuration); 864 mNumSync++; 865 866 if (DEBUG || mNumSync % 1024 == 0 || fsyncDuration > MAX_FSYNC_DURATION_MILLIS) { 867 mSyncTimes.log(TAG, "Time required to fsync " + mFile + ": "); 868 } 869 870 return; 871 } catch (XmlPullParserException e) { 872 Log.w(TAG, "writeToFile: Got exception:", e); 873 } catch (IOException e) { 874 Log.w(TAG, "writeToFile: Got exception:", e); 875 } 876 877 // Clean up an unsuccessfully written file 878 if (mFile.exists()) { 879 if (!mFile.delete()) { 880 Log.e(TAG, "Couldn't clean up partially-written file " + mFile); 881 } 882 } 883 mcr.setDiskWriteResult(false, false); 884 } 885 886 887 private static final class SharedPreferencesThreadFactory implements ThreadFactory { 888 @Override newThread(Runnable runnable)889 public Thread newThread(Runnable runnable) { 890 Thread thread = Executors.defaultThreadFactory().newThread(runnable); 891 thread.setName("SharedPreferences"); 892 return thread; 893 } 894 } 895 } 896