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