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