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