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