2 * Copyright (C) 2010 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
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.util.Log;
28 import com.google.android.collect.Maps;
30 import com.android.internal.annotations.GuardedBy;
31 import com.android.internal.util.XmlUtils;
33 import dalvik.system.BlockGuard;
35 import org.xmlpull.v1.XmlPullParserException;
37 import java.io.BufferedInputStream;
39 import java.io.FileInputStream;
40 import java.io.FileNotFoundException;
41 import java.io.FileOutputStream;
42 import java.io.IOException;
43 import java.util.ArrayList;
44 import java.util.HashMap;
45 import java.util.HashSet;
46 import java.util.List;
49 import java.util.WeakHashMap;
50 import java.util.concurrent.CountDownLatch;
52 import libcore.io.IoUtils;
54 final class SharedPreferencesImpl implements SharedPreferences {
55 private static final String TAG = "SharedPreferencesImpl";
56 private static final boolean DEBUG = false;
58 // Lock ordering rules:
59 // - acquire SharedPreferencesImpl.this before EditorImpl.this
60 // - acquire mWritingToDiskLock before EditorImpl.this
62 private final File mFile;
63 private final File mBackupFile;
64 private final int mMode;
66 private Map<String, Object> mMap; // guarded by 'this'
67 private int mDiskWritesInFlight = 0; // guarded by 'this'
68 private boolean mLoaded = false; // guarded by 'this'
69 private long mStatTimestamp; // guarded by 'this'
70 private long mStatSize; // guarded by 'this'
72 private final Object mWritingToDiskLock = new Object();
73 private static final Object mContent = new Object();
74 private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners =
75 new WeakHashMap<OnSharedPreferenceChangeListener, Object>();
77 /** Current memory state (always increasing) */
79 private long mCurrentMemoryStateGeneration;
81 /** Latest memory state that was committed to disk */
82 @GuardedBy("mWritingToDiskLock")
83 private long mDiskStateGeneration;
85 SharedPreferencesImpl(File file, int mode) {
87 mBackupFile = makeBackupFile(file);
94 private void startLoadFromDisk() {
98 new Thread("SharedPreferencesImpl-load") {
105 private void loadFromDisk() {
106 synchronized (SharedPreferencesImpl.this) {
110 if (mBackupFile.exists()) {
112 mBackupFile.renameTo(mFile);
117 if (mFile.exists() && !mFile.canRead()) {
118 Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
122 StructStat stat = null;
124 stat = Os.stat(mFile.getPath());
125 if (mFile.canRead()) {
126 BufferedInputStream str = null;
128 str = new BufferedInputStream(
129 new FileInputStream(mFile), 16*1024);
130 map = XmlUtils.readMapXml(str);
131 } catch (XmlPullParserException | IOException e) {
132 Log.w(TAG, "getSharedPreferences", e);
134 IoUtils.closeQuietly(str);
137 } catch (ErrnoException e) {
141 synchronized (SharedPreferencesImpl.this) {
145 mStatTimestamp = stat.st_mtime;
146 mStatSize = stat.st_size;
148 mMap = new HashMap<>();
154 static File makeBackupFile(File prefsFile) {
155 return new File(prefsFile.getPath() + ".bak");
158 void startReloadIfChangedUnexpectedly() {
159 synchronized (this) {
160 // TODO: wait for any pending writes to disk?
161 if (!hasFileChangedUnexpectedly()) {
168 // Has the file changed out from under us? i.e. writes that
169 // we didn't instigate.
170 private boolean hasFileChangedUnexpectedly() {
171 synchronized (this) {
172 if (mDiskWritesInFlight > 0) {
173 // If we know we caused it, it's not unexpected.
174 if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected.");
179 final StructStat stat;
182 * Metadata operations don't usually count as a block guard
183 * violation, but we explicitly want this one.
185 BlockGuard.getThreadPolicy().onReadFromDisk();
186 stat = Os.stat(mFile.getPath());
187 } catch (ErrnoException e) {
191 synchronized (this) {
192 return mStatTimestamp != stat.st_mtime || mStatSize != stat.st_size;
196 public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
198 mListeners.put(listener, mContent);
202 public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
204 mListeners.remove(listener);
208 private void awaitLoadedLocked() {
210 // Raise an explicit StrictMode onReadFromDisk for this
211 // thread, since the real read will be in a different
212 // thread and otherwise ignored by StrictMode.
213 BlockGuard.getThreadPolicy().onReadFromDisk();
218 } catch (InterruptedException unused) {
223 public Map<String, ?> getAll() {
224 synchronized (this) {
226 //noinspection unchecked
227 return new HashMap<String, Object>(mMap);
232 public String getString(String key, @Nullable String defValue) {
233 synchronized (this) {
235 String v = (String)mMap.get(key);
236 return v != null ? v : defValue;
241 public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
242 synchronized (this) {
244 Set<String> v = (Set<String>) mMap.get(key);
245 return v != null ? v : defValues;
249 public int getInt(String key, int defValue) {
250 synchronized (this) {
252 Integer v = (Integer)mMap.get(key);
253 return v != null ? v : defValue;
256 public long getLong(String key, long defValue) {
257 synchronized (this) {
259 Long v = (Long)mMap.get(key);
260 return v != null ? v : defValue;
263 public float getFloat(String key, float defValue) {
264 synchronized (this) {
266 Float v = (Float)mMap.get(key);
267 return v != null ? v : defValue;
270 public boolean getBoolean(String key, boolean defValue) {
271 synchronized (this) {
273 Boolean v = (Boolean)mMap.get(key);
274 return v != null ? v : defValue;
278 public boolean contains(String key) {
279 synchronized (this) {
281 return mMap.containsKey(key);
285 public Editor edit() {
286 // TODO: remove the need to call awaitLoadedLocked() when
287 // requesting an editor. will require some work on the
288 // Editor, but then we should be able to do:
290 // context.getSharedPreferences(..).edit().putString(..).apply()
292 // ... all without blocking.
293 synchronized (this) {
297 return new EditorImpl();
300 // Return value from EditorImpl#commitToMemory()
301 private static class MemoryCommitResult {
302 public long memoryStateGeneration;
303 public List<String> keysModified; // may be null
304 public Set<OnSharedPreferenceChangeListener> listeners; // may be null
305 public Map<?, ?> mapToWriteToDisk;
306 public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
307 public volatile boolean writeToDiskResult = false;
309 public void setDiskWriteResult(boolean result) {
310 writeToDiskResult = result;
311 writtenToDiskLatch.countDown();
315 public final class EditorImpl implements Editor {
316 private final Map<String, Object> mModified = Maps.newHashMap();
317 private boolean mClear = false;
319 public Editor putString(String key, @Nullable String value) {
320 synchronized (this) {
321 mModified.put(key, value);
325 public Editor putStringSet(String key, @Nullable Set<String> values) {
326 synchronized (this) {
328 (values == null) ? null : new HashSet<String>(values));
332 public Editor putInt(String key, int value) {
333 synchronized (this) {
334 mModified.put(key, value);
338 public Editor putLong(String key, long value) {
339 synchronized (this) {
340 mModified.put(key, value);
344 public Editor putFloat(String key, float value) {
345 synchronized (this) {
346 mModified.put(key, value);
350 public Editor putBoolean(String key, boolean value) {
351 synchronized (this) {
352 mModified.put(key, value);
357 public Editor remove(String key) {
358 synchronized (this) {
359 mModified.put(key, this);
364 public Editor clear() {
365 synchronized (this) {
371 public void apply() {
372 final MemoryCommitResult mcr = commitToMemory();
373 final Runnable awaitCommit = new Runnable() {
376 mcr.writtenToDiskLatch.await();
377 } catch (InterruptedException ignored) {
382 QueuedWork.add(awaitCommit);
384 Runnable postWriteRunnable = new Runnable() {
387 QueuedWork.remove(awaitCommit);
391 SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
393 // Okay to notify the listeners before it's hit disk
394 // because the listeners should always get the same
395 // SharedPreferences instance back, which has the
396 // changes reflected in memory.
397 notifyListeners(mcr);
400 // Returns true if any changes were made
401 private MemoryCommitResult commitToMemory() {
402 MemoryCommitResult mcr = new MemoryCommitResult();
403 synchronized (SharedPreferencesImpl.this) {
404 // We optimistically don't make a deep copy until
405 // a memory commit comes in when we're already
407 if (mDiskWritesInFlight > 0) {
408 // We can't modify our mMap as a currently
409 // in-flight write owns it. Clone it before
411 // noinspection unchecked
412 mMap = new HashMap<String, Object>(mMap);
414 mcr.mapToWriteToDisk = mMap;
415 mDiskWritesInFlight++;
417 boolean hasListeners = mListeners.size() > 0;
419 mcr.keysModified = new ArrayList<String>();
421 new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
424 synchronized (this) {
425 boolean changesMade = false;
428 if (!mMap.isEmpty()) {
435 for (Map.Entry<String, Object> e : mModified.entrySet()) {
436 String k = e.getKey();
437 Object v = e.getValue();
438 // "this" is the magic value for a removal mutation. In addition,
439 // setting a value to "null" for a given key is specified to be
440 // equivalent to calling remove on that key.
441 if (v == this || v == null) {
442 if (!mMap.containsKey(k)) {
447 if (mMap.containsKey(k)) {
448 Object existingValue = mMap.get(k);
449 if (existingValue != null && existingValue.equals(v)) {
458 mcr.keysModified.add(k);
465 mCurrentMemoryStateGeneration++;
468 mcr.memoryStateGeneration = mCurrentMemoryStateGeneration;
474 public boolean commit() {
475 MemoryCommitResult mcr = commitToMemory();
476 SharedPreferencesImpl.this.enqueueDiskWrite(
477 mcr, null /* sync write on this thread okay */);
479 mcr.writtenToDiskLatch.await();
480 } catch (InterruptedException e) {
483 notifyListeners(mcr);
484 return mcr.writeToDiskResult;
487 private void notifyListeners(final MemoryCommitResult mcr) {
488 if (mcr.listeners == null || mcr.keysModified == null ||
489 mcr.keysModified.size() == 0) {
492 if (Looper.myLooper() == Looper.getMainLooper()) {
493 for (int i = mcr.keysModified.size() - 1; i >= 0; i--) {
494 final String key = mcr.keysModified.get(i);
495 for (OnSharedPreferenceChangeListener listener : mcr.listeners) {
496 if (listener != null) {
497 listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, key);
502 // Run this function on the main thread.
503 ActivityThread.sMainThreadHandler.post(new Runnable() {
505 notifyListeners(mcr);
513 * Enqueue an already-committed-to-memory result to be written
516 * They will be written to disk one-at-a-time in the order
517 * that they're enqueued.
519 * @param postWriteRunnable if non-null, we're being called
520 * from apply() and this is the runnable to run after
521 * the write proceeds. if null (from a regular commit()),
522 * then we're allowed to do this disk write on the main
523 * thread (which in addition to reducing allocations and
524 * creating a background thread, this has the advantage that
525 * we catch them in userdebug StrictMode reports to convert
526 * them where possible to apply() ...)
528 private void enqueueDiskWrite(final MemoryCommitResult mcr,
529 final Runnable postWriteRunnable) {
530 final boolean isFromSyncCommit = (postWriteRunnable == null);
532 final Runnable writeToDiskRunnable = new Runnable() {
534 synchronized (mWritingToDiskLock) {
535 writeToFile(mcr, isFromSyncCommit);
537 synchronized (SharedPreferencesImpl.this) {
538 mDiskWritesInFlight--;
540 if (postWriteRunnable != null) {
541 postWriteRunnable.run();
546 // Typical #commit() path with fewer allocations, doing a write on
547 // the current thread.
548 if (isFromSyncCommit) {
549 boolean wasEmpty = false;
550 synchronized (SharedPreferencesImpl.this) {
551 wasEmpty = mDiskWritesInFlight == 1;
554 writeToDiskRunnable.run();
560 Log.d(TAG, "added " + mcr.memoryStateGeneration + " -> " + mFile.getName());
563 QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
566 private static FileOutputStream createFileOutputStream(File file) {
567 FileOutputStream str = null;
569 str = new FileOutputStream(file);
570 } catch (FileNotFoundException e) {
571 File parent = file.getParentFile();
572 if (!parent.mkdir()) {
573 Log.e(TAG, "Couldn't create directory for SharedPreferences file " + file);
576 FileUtils.setPermissions(
578 FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
581 str = new FileOutputStream(file);
582 } catch (FileNotFoundException e2) {
583 Log.e(TAG, "Couldn't create SharedPreferences file " + file, e2);
589 // Note: must hold mWritingToDiskLock
590 private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
591 // Rename the current file so it may be used as a backup during the next read
592 if (mFile.exists()) {
593 boolean needsWrite = false;
595 // Only need to write if the disk state is older than this commit
596 if (mDiskStateGeneration < mcr.memoryStateGeneration) {
597 if (isFromSyncCommit) {
600 synchronized (this) {
601 // No need to persist intermediate states. Just wait for the latest state to
603 if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
612 Log.d(TAG, "skipped " + mcr.memoryStateGeneration + " -> " + mFile.getName());
614 mcr.setDiskWriteResult(true);
618 if (!mBackupFile.exists()) {
619 if (!mFile.renameTo(mBackupFile)) {
620 Log.e(TAG, "Couldn't rename file " + mFile
621 + " to backup file " + mBackupFile);
622 mcr.setDiskWriteResult(false);
630 // Attempt to write the file, delete the backup and return true as atomically as
631 // possible. If any exception occurs, delete the new file; next time we will restore
634 FileOutputStream str = createFileOutputStream(mFile);
636 mcr.setDiskWriteResult(false);
639 XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
643 Log.d(TAG, "wrote " + mcr.memoryStateGeneration + " -> " + mFile.getName());
647 ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
649 final StructStat stat = Os.stat(mFile.getPath());
650 synchronized (this) {
651 mStatTimestamp = stat.st_mtime;
652 mStatSize = stat.st_size;
654 } catch (ErrnoException e) {
657 // Writing was successful, delete the backup file if there is one.
658 mBackupFile.delete();
660 mDiskStateGeneration = mcr.memoryStateGeneration;
662 mcr.setDiskWriteResult(true);
665 } catch (XmlPullParserException e) {
666 Log.w(TAG, "writeToFile: Got exception:", e);
667 } catch (IOException e) {
668 Log.w(TAG, "writeToFile: Got exception:", e);
670 // Clean up an unsuccessfully written file
671 if (mFile.exists()) {
672 if (!mFile.delete()) {
673 Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
676 mcr.setDiskWriteResult(false);