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