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