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