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