Home | History | Annotate | Download | only in app
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.app;
     18 
     19 import android.annotation.Nullable;
     20 import android.content.SharedPreferences;
     21 import android.os.FileUtils;
     22 import android.os.Looper;
     23 import android.system.ErrnoException;
     24 import android.system.Os;
     25 import android.system.StructStat;
     26 import android.system.StructTimespec;
     27 import android.util.Log;
     28 
     29 import com.android.internal.annotations.GuardedBy;
     30 import com.android.internal.util.ExponentiallyBucketedHistogram;
     31 import com.android.internal.util.XmlUtils;
     32 
     33 import dalvik.system.BlockGuard;
     34 
     35 import libcore.io.IoUtils;
     36 
     37 import org.xmlpull.v1.XmlPullParserException;
     38 
     39 import java.io.BufferedInputStream;
     40 import java.io.File;
     41 import java.io.FileInputStream;
     42 import java.io.FileNotFoundException;
     43 import java.io.FileOutputStream;
     44 import java.io.IOException;
     45 import java.util.ArrayList;
     46 import java.util.HashMap;
     47 import java.util.HashSet;
     48 import java.util.List;
     49 import java.util.Map;
     50 import java.util.Set;
     51 import java.util.WeakHashMap;
     52 import java.util.concurrent.CountDownLatch;
     53 
     54 final 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