Move SharedPreferencesImpl out of ContextImpl.java
Change-Id: I3a58ec4c9501e906c133e841b5c5ec6bced04a02
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index ba301e9..860c5de 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -18,7 +18,6 @@
import com.android.internal.policy.PolicyManager;
import com.android.internal.util.XmlUtils;
-import com.google.android.collect.Maps;
import org.xmlpull.v1.XmlPullParserException;
@@ -114,11 +113,9 @@
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
-import java.util.Map.Entry;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.CountDownLatch;
@@ -315,7 +312,7 @@
throw new RuntimeException("Not supported in system context");
}
- private static File makeBackupFile(File prefsFile) {
+ static File makeBackupFile(File prefsFile) {
return new File(prefsFile.getPath() + ".bak");
}
@@ -363,7 +360,7 @@
FileInputStream str = new FileInputStream(prefsFile);
map = XmlUtils.readMapXml(str);
str.close();
- } catch (org.xmlpull.v1.XmlPullParserException e) {
+ } catch (XmlPullParserException e) {
Log.w(TAG, "getSharedPreferences", e);
} catch (FileNotFoundException e) {
Log.w(TAG, "getSharedPreferences", e);
@@ -1593,7 +1590,7 @@
return mActivityToken;
}
- private static void setFilePermissionsFromMode(String name, int mode,
+ static void setFilePermissionsFromMode(String name, int mode,
int extraPermissions) {
int perms = FileUtils.S_IRUSR|FileUtils.S_IWUSR
|FileUtils.S_IRGRP|FileUtils.S_IWGRP
@@ -2727,482 +2724,4 @@
private static HashMap<ResourceName, WeakReference<CharSequence> > sStringCache
= new HashMap<ResourceName, WeakReference<CharSequence> >();
}
-
- // ----------------------------------------------------------------------
- // ----------------------------------------------------------------------
- // ----------------------------------------------------------------------
-
- private static final class SharedPreferencesImpl implements SharedPreferences {
-
- // Lock ordering rules:
- // - acquire SharedPreferencesImpl.this before EditorImpl.this
- // - acquire mWritingToDiskLock before EditorImpl.this
-
- private final File mFile;
- private final File mBackupFile;
- private final int mMode;
-
- private Map<String, Object> mMap; // guarded by 'this'
- private int mDiskWritesInFlight = 0; // guarded by 'this'
- private boolean mLoaded = false; // guarded by 'this'
- private long mStatTimestamp; // guarded by 'this'
- private long mStatSize; // guarded by 'this'
-
- private final Object mWritingToDiskLock = new Object();
- private static final Object mContent = new Object();
- private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners;
-
- SharedPreferencesImpl(
- File file, int mode, Map initialContents) {
- mFile = file;
- mBackupFile = makeBackupFile(file);
- mMode = mode;
- mLoaded = initialContents != null;
- mMap = initialContents != null ? initialContents : new HashMap<String, Object>();
- FileStatus stat = new FileStatus();
- if (FileUtils.getFileStatus(file.getPath(), stat)) {
- mStatTimestamp = stat.mtime;
- }
- mListeners = new WeakHashMap<OnSharedPreferenceChangeListener, Object>();
- }
-
- // Has this SharedPreferences ever had values assigned to it?
- boolean isLoaded() {
- synchronized (this) {
- return mLoaded;
- }
- }
-
- // Has the file changed out from under us? i.e. writes that
- // we didn't instigate.
- public boolean hasFileChangedUnexpectedly() {
- synchronized (this) {
- if (mDiskWritesInFlight > 0) {
- // If we know we caused it, it's not unexpected.
- if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected.");
- return false;
- }
- }
- FileStatus stat = new FileStatus();
- if (!FileUtils.getFileStatus(mFile.getPath(), stat)) {
- return true;
- }
- synchronized (this) {
- return mStatTimestamp != stat.mtime || mStatSize != stat.size;
- }
- }
-
- public void replace(Map newContents) {
- synchronized (this) {
- mLoaded = true;
- if (newContents != null) {
- mMap = newContents;
- }
- }
- }
-
- public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
- synchronized(this) {
- mListeners.put(listener, mContent);
- }
- }
-
- public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
- synchronized(this) {
- mListeners.remove(listener);
- }
- }
-
- public Map<String, ?> getAll() {
- synchronized(this) {
- //noinspection unchecked
- return new HashMap<String, Object>(mMap);
- }
- }
-
- public String getString(String key, String defValue) {
- synchronized (this) {
- String v = (String)mMap.get(key);
- return v != null ? v : defValue;
- }
- }
-
- public Set<String> getStringSet(String key, Set<String> defValues) {
- synchronized (this) {
- Set<String> v = (Set<String>) mMap.get(key);
- return v != null ? v : defValues;
- }
- }
-
- public int getInt(String key, int defValue) {
- synchronized (this) {
- Integer v = (Integer)mMap.get(key);
- return v != null ? v : defValue;
- }
- }
- public long getLong(String key, long defValue) {
- synchronized (this) {
- Long v = (Long)mMap.get(key);
- return v != null ? v : defValue;
- }
- }
- public float getFloat(String key, float defValue) {
- synchronized (this) {
- Float v = (Float)mMap.get(key);
- return v != null ? v : defValue;
- }
- }
- public boolean getBoolean(String key, boolean defValue) {
- synchronized (this) {
- Boolean v = (Boolean)mMap.get(key);
- return v != null ? v : defValue;
- }
- }
-
- public boolean contains(String key) {
- synchronized (this) {
- return mMap.containsKey(key);
- }
- }
-
- public Editor edit() {
- return new EditorImpl();
- }
-
- // Return value from EditorImpl#commitToMemory()
- private static class MemoryCommitResult {
- public boolean changesMade; // any keys different?
- public List<String> keysModified; // may be null
- public Set<OnSharedPreferenceChangeListener> listeners; // may be null
- public Map<?, ?> mapToWriteToDisk;
- public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
- public volatile boolean writeToDiskResult = false;
-
- public void setDiskWriteResult(boolean result) {
- writeToDiskResult = result;
- writtenToDiskLatch.countDown();
- }
- }
-
- public final class EditorImpl implements Editor {
- private final Map<String, Object> mModified = Maps.newHashMap();
- private boolean mClear = false;
-
- public Editor putString(String key, String value) {
- synchronized (this) {
- mModified.put(key, value);
- return this;
- }
- }
- public Editor putStringSet(String key, Set<String> values) {
- synchronized (this) {
- mModified.put(key, values);
- return this;
- }
- }
- public Editor putInt(String key, int value) {
- synchronized (this) {
- mModified.put(key, value);
- return this;
- }
- }
- public Editor putLong(String key, long value) {
- synchronized (this) {
- mModified.put(key, value);
- return this;
- }
- }
- public Editor putFloat(String key, float value) {
- synchronized (this) {
- mModified.put(key, value);
- return this;
- }
- }
- public Editor putBoolean(String key, boolean value) {
- synchronized (this) {
- mModified.put(key, value);
- return this;
- }
- }
-
- public Editor remove(String key) {
- synchronized (this) {
- mModified.put(key, this);
- return this;
- }
- }
-
- public Editor clear() {
- synchronized (this) {
- mClear = true;
- return this;
- }
- }
-
- public void apply() {
- final MemoryCommitResult mcr = commitToMemory();
- final Runnable awaitCommit = new Runnable() {
- public void run() {
- try {
- mcr.writtenToDiskLatch.await();
- } catch (InterruptedException ignored) {
- }
- }
- };
-
- QueuedWork.add(awaitCommit);
-
- Runnable postWriteRunnable = new Runnable() {
- public void run() {
- awaitCommit.run();
- QueuedWork.remove(awaitCommit);
- }
- };
-
- SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
-
- // Okay to notify the listeners before it's hit disk
- // because the listeners should always get the same
- // SharedPreferences instance back, which has the
- // changes reflected in memory.
- notifyListeners(mcr);
- }
-
- // Returns true if any changes were made
- private MemoryCommitResult commitToMemory() {
- MemoryCommitResult mcr = new MemoryCommitResult();
- synchronized (SharedPreferencesImpl.this) {
- // We optimistically don't make a deep copy until
- // a memory commit comes in when we're already
- // writing to disk.
- if (mDiskWritesInFlight > 0) {
- // We can't modify our mMap as a currently
- // in-flight write owns it. Clone it before
- // modifying it.
- // noinspection unchecked
- mMap = new HashMap<String, Object>(mMap);
- }
- mcr.mapToWriteToDisk = mMap;
- mDiskWritesInFlight++;
-
- boolean hasListeners = mListeners.size() > 0;
- if (hasListeners) {
- mcr.keysModified = new ArrayList<String>();
- mcr.listeners =
- new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
- }
-
- synchronized (this) {
- if (mClear) {
- if (!mMap.isEmpty()) {
- mcr.changesMade = true;
- mMap.clear();
- }
- mClear = false;
- }
-
- for (Entry<String, Object> e : mModified.entrySet()) {
- String k = e.getKey();
- Object v = e.getValue();
- if (v == this) { // magic value for a removal mutation
- if (!mMap.containsKey(k)) {
- continue;
- }
- mMap.remove(k);
- } else {
- boolean isSame = false;
- if (mMap.containsKey(k)) {
- Object existingValue = mMap.get(k);
- if (existingValue != null && existingValue.equals(v)) {
- continue;
- }
- }
- mMap.put(k, v);
- }
-
- mcr.changesMade = true;
- if (hasListeners) {
- mcr.keysModified.add(k);
- }
- }
-
- mModified.clear();
- }
- }
- return mcr;
- }
-
- public boolean commit() {
- MemoryCommitResult mcr = commitToMemory();
- SharedPreferencesImpl.this.enqueueDiskWrite(
- mcr, null /* sync write on this thread okay */);
- try {
- mcr.writtenToDiskLatch.await();
- } catch (InterruptedException e) {
- return false;
- }
- notifyListeners(mcr);
- return mcr.writeToDiskResult;
- }
-
- private void notifyListeners(final MemoryCommitResult mcr) {
- if (mcr.listeners == null || mcr.keysModified == null ||
- mcr.keysModified.size() == 0) {
- return;
- }
- if (Looper.myLooper() == Looper.getMainLooper()) {
- for (int i = mcr.keysModified.size() - 1; i >= 0; i--) {
- final String key = mcr.keysModified.get(i);
- for (OnSharedPreferenceChangeListener listener : mcr.listeners) {
- if (listener != null) {
- listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, key);
- }
- }
- }
- } else {
- // Run this function on the main thread.
- ActivityThread.sMainThreadHandler.post(new Runnable() {
- public void run() {
- notifyListeners(mcr);
- }
- });
- }
- }
- }
-
- /**
- * Enqueue an already-committed-to-memory result to be written
- * to disk.
- *
- * They will be written to disk one-at-a-time in the order
- * that they're enqueued.
- *
- * @param postWriteRunnable if non-null, we're being called
- * from apply() and this is the runnable to run after
- * the write proceeds. if null (from a regular commit()),
- * then we're allowed to do this disk write on the main
- * thread (which in addition to reducing allocations and
- * creating a background thread, this has the advantage that
- * we catch them in userdebug StrictMode reports to convert
- * them where possible to apply() ...)
- */
- private void enqueueDiskWrite(final MemoryCommitResult mcr,
- final Runnable postWriteRunnable) {
- final Runnable writeToDiskRunnable = new Runnable() {
- public void run() {
- synchronized (mWritingToDiskLock) {
- writeToFile(mcr);
- }
- synchronized (SharedPreferencesImpl.this) {
- mDiskWritesInFlight--;
- }
- if (postWriteRunnable != null) {
- postWriteRunnable.run();
- }
- }
- };
-
- final boolean isFromSyncCommit = (postWriteRunnable == null);
-
- // Typical #commit() path with fewer allocations, doing a write on
- // the current thread.
- if (isFromSyncCommit) {
- boolean wasEmpty = false;
- synchronized (SharedPreferencesImpl.this) {
- wasEmpty = mDiskWritesInFlight == 1;
- }
- if (wasEmpty) {
- writeToDiskRunnable.run();
- return;
- }
- }
-
- QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
- }
-
- private static FileOutputStream createFileOutputStream(File file) {
- FileOutputStream str = null;
- try {
- str = new FileOutputStream(file);
- } catch (FileNotFoundException e) {
- File parent = file.getParentFile();
- if (!parent.mkdir()) {
- Log.e(TAG, "Couldn't create directory for SharedPreferences file " + file);
- return null;
- }
- FileUtils.setPermissions(
- parent.getPath(),
- FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
- -1, -1);
- try {
- str = new FileOutputStream(file);
- } catch (FileNotFoundException e2) {
- Log.e(TAG, "Couldn't create SharedPreferences file " + file, e2);
- }
- }
- return str;
- }
-
- // Note: must hold mWritingToDiskLock
- private void writeToFile(MemoryCommitResult mcr) {
- // Rename the current file so it may be used as a backup during the next read
- if (mFile.exists()) {
- if (!mcr.changesMade) {
- // If the file already exists, but no changes were
- // made to the underlying map, it's wasteful to
- // re-write the file. Return as if we wrote it
- // out.
- mcr.setDiskWriteResult(true);
- return;
- }
- if (!mBackupFile.exists()) {
- if (!mFile.renameTo(mBackupFile)) {
- Log.e(TAG, "Couldn't rename file " + mFile
- + " to backup file " + mBackupFile);
- mcr.setDiskWriteResult(false);
- return;
- }
- } else {
- mFile.delete();
- }
- }
-
- // Attempt to write the file, delete the backup and return true as atomically as
- // possible. If any exception occurs, delete the new file; next time we will restore
- // from the backup.
- try {
- FileOutputStream str = createFileOutputStream(mFile);
- if (str == null) {
- mcr.setDiskWriteResult(false);
- return;
- }
- XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
- FileUtils.sync(str);
- str.close();
- setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
- FileStatus stat = new FileStatus();
- if (FileUtils.getFileStatus(mFile.getPath(), stat)) {
- synchronized (this) {
- mStatTimestamp = stat.mtime;
- mStatSize = stat.size;
- }
- }
- // Writing was successful, delete the backup file if there is one.
- mBackupFile.delete();
- mcr.setDiskWriteResult(true);
- return;
- } catch (XmlPullParserException e) {
- Log.w(TAG, "writeToFile: Got exception:", e);
- } catch (IOException e) {
- Log.w(TAG, "writeToFile: Got exception:", e);
- }
- // Clean up an unsuccessfully written file
- if (mFile.exists()) {
- if (!mFile.delete()) {
- Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
- }
- }
- mcr.setDiskWriteResult(false);
- }
- }
}
diff --git a/core/java/android/app/SharedPreferencesImpl.java b/core/java/android/app/SharedPreferencesImpl.java
new file mode 100644
index 0000000..2096a78
--- /dev/null
+++ b/core/java/android/app/SharedPreferencesImpl.java
@@ -0,0 +1,518 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.SharedPreferences;
+import android.os.FileUtils.FileStatus;
+import android.os.FileUtils;
+import android.os.Looper;
+import android.util.Log;
+
+import com.google.android.collect.Maps;
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+
+final class SharedPreferencesImpl implements SharedPreferences {
+ private static final String TAG = "SharedPreferencesImpl";
+ private static final boolean DEBUG = false;
+
+ // Lock ordering rules:
+ // - acquire SharedPreferencesImpl.this before EditorImpl.this
+ // - acquire mWritingToDiskLock before EditorImpl.this
+
+ private final File mFile;
+ private final File mBackupFile;
+ private final int mMode;
+
+ private Map<String, Object> mMap; // guarded by 'this'
+ private int mDiskWritesInFlight = 0; // guarded by 'this'
+ private boolean mLoaded = false; // guarded by 'this'
+ private long mStatTimestamp; // guarded by 'this'
+ private long mStatSize; // guarded by 'this'
+
+ private final Object mWritingToDiskLock = new Object();
+ private static final Object mContent = new Object();
+ private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners;
+
+ SharedPreferencesImpl(
+ File file, int mode, Map initialContents) {
+ mFile = file;
+ mBackupFile = ContextImpl.makeBackupFile(file);
+ mMode = mode;
+ mLoaded = initialContents != null;
+ mMap = initialContents != null ? initialContents : new HashMap<String, Object>();
+ FileStatus stat = new FileStatus();
+ if (FileUtils.getFileStatus(file.getPath(), stat)) {
+ mStatTimestamp = stat.mtime;
+ }
+ mListeners = new WeakHashMap<OnSharedPreferenceChangeListener, Object>();
+ }
+
+ // Has this SharedPreferences ever had values assigned to it?
+ boolean isLoaded() {
+ synchronized (this) {
+ return mLoaded;
+ }
+ }
+
+ // Has the file changed out from under us? i.e. writes that
+ // we didn't instigate.
+ public boolean hasFileChangedUnexpectedly() {
+ synchronized (this) {
+ if (mDiskWritesInFlight > 0) {
+ // If we know we caused it, it's not unexpected.
+ if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected.");
+ return false;
+ }
+ }
+ FileStatus stat = new FileStatus();
+ if (!FileUtils.getFileStatus(mFile.getPath(), stat)) {
+ return true;
+ }
+ synchronized (this) {
+ return mStatTimestamp != stat.mtime || mStatSize != stat.size;
+ }
+ }
+
+ public void replace(Map newContents) {
+ synchronized (this) {
+ mLoaded = true;
+ if (newContents != null) {
+ mMap = newContents;
+ }
+ }
+ }
+
+ public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+ synchronized(this) {
+ mListeners.put(listener, mContent);
+ }
+ }
+
+ public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+ synchronized(this) {
+ mListeners.remove(listener);
+ }
+ }
+
+ public Map<String, ?> getAll() {
+ synchronized(this) {
+ //noinspection unchecked
+ return new HashMap<String, Object>(mMap);
+ }
+ }
+
+ public String getString(String key, String defValue) {
+ synchronized (this) {
+ String v = (String)mMap.get(key);
+ return v != null ? v : defValue;
+ }
+ }
+
+ public Set<String> getStringSet(String key, Set<String> defValues) {
+ synchronized (this) {
+ Set<String> v = (Set<String>) mMap.get(key);
+ return v != null ? v : defValues;
+ }
+ }
+
+ public int getInt(String key, int defValue) {
+ synchronized (this) {
+ Integer v = (Integer)mMap.get(key);
+ return v != null ? v : defValue;
+ }
+ }
+ public long getLong(String key, long defValue) {
+ synchronized (this) {
+ Long v = (Long)mMap.get(key);
+ return v != null ? v : defValue;
+ }
+ }
+ public float getFloat(String key, float defValue) {
+ synchronized (this) {
+ Float v = (Float)mMap.get(key);
+ return v != null ? v : defValue;
+ }
+ }
+ public boolean getBoolean(String key, boolean defValue) {
+ synchronized (this) {
+ Boolean v = (Boolean)mMap.get(key);
+ return v != null ? v : defValue;
+ }
+ }
+
+ public boolean contains(String key) {
+ synchronized (this) {
+ return mMap.containsKey(key);
+ }
+ }
+
+ public Editor edit() {
+ return new EditorImpl();
+ }
+
+ // Return value from EditorImpl#commitToMemory()
+ private static class MemoryCommitResult {
+ public boolean changesMade; // any keys different?
+ public List<String> keysModified; // may be null
+ public Set<OnSharedPreferenceChangeListener> listeners; // may be null
+ public Map<?, ?> mapToWriteToDisk;
+ public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
+ public volatile boolean writeToDiskResult = false;
+
+ public void setDiskWriteResult(boolean result) {
+ writeToDiskResult = result;
+ writtenToDiskLatch.countDown();
+ }
+ }
+
+ public final class EditorImpl implements Editor {
+ private final Map<String, Object> mModified = Maps.newHashMap();
+ private boolean mClear = false;
+
+ public Editor putString(String key, String value) {
+ synchronized (this) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+ public Editor putStringSet(String key, Set<String> values) {
+ synchronized (this) {
+ mModified.put(key, values);
+ return this;
+ }
+ }
+ public Editor putInt(String key, int value) {
+ synchronized (this) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+ public Editor putLong(String key, long value) {
+ synchronized (this) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+ public Editor putFloat(String key, float value) {
+ synchronized (this) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+ public Editor putBoolean(String key, boolean value) {
+ synchronized (this) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+
+ public Editor remove(String key) {
+ synchronized (this) {
+ mModified.put(key, this);
+ return this;
+ }
+ }
+
+ public Editor clear() {
+ synchronized (this) {
+ mClear = true;
+ return this;
+ }
+ }
+
+ public void apply() {
+ final MemoryCommitResult mcr = commitToMemory();
+ final Runnable awaitCommit = new Runnable() {
+ public void run() {
+ try {
+ mcr.writtenToDiskLatch.await();
+ } catch (InterruptedException ignored) {
+ }
+ }
+ };
+
+ QueuedWork.add(awaitCommit);
+
+ Runnable postWriteRunnable = new Runnable() {
+ public void run() {
+ awaitCommit.run();
+ QueuedWork.remove(awaitCommit);
+ }
+ };
+
+ SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
+
+ // Okay to notify the listeners before it's hit disk
+ // because the listeners should always get the same
+ // SharedPreferences instance back, which has the
+ // changes reflected in memory.
+ notifyListeners(mcr);
+ }
+
+ // Returns true if any changes were made
+ private MemoryCommitResult commitToMemory() {
+ MemoryCommitResult mcr = new MemoryCommitResult();
+ synchronized (SharedPreferencesImpl.this) {
+ // We optimistically don't make a deep copy until
+ // a memory commit comes in when we're already
+ // writing to disk.
+ if (mDiskWritesInFlight > 0) {
+ // We can't modify our mMap as a currently
+ // in-flight write owns it. Clone it before
+ // modifying it.
+ // noinspection unchecked
+ mMap = new HashMap<String, Object>(mMap);
+ }
+ mcr.mapToWriteToDisk = mMap;
+ mDiskWritesInFlight++;
+
+ boolean hasListeners = mListeners.size() > 0;
+ if (hasListeners) {
+ mcr.keysModified = new ArrayList<String>();
+ mcr.listeners =
+ new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
+ }
+
+ synchronized (this) {
+ if (mClear) {
+ if (!mMap.isEmpty()) {
+ mcr.changesMade = true;
+ mMap.clear();
+ }
+ mClear = false;
+ }
+
+ for (Map.Entry<String, Object> e : mModified.entrySet()) {
+ String k = e.getKey();
+ Object v = e.getValue();
+ if (v == this) { // magic value for a removal mutation
+ if (!mMap.containsKey(k)) {
+ continue;
+ }
+ mMap.remove(k);
+ } else {
+ boolean isSame = false;
+ if (mMap.containsKey(k)) {
+ Object existingValue = mMap.get(k);
+ if (existingValue != null && existingValue.equals(v)) {
+ continue;
+ }
+ }
+ mMap.put(k, v);
+ }
+
+ mcr.changesMade = true;
+ if (hasListeners) {
+ mcr.keysModified.add(k);
+ }
+ }
+
+ mModified.clear();
+ }
+ }
+ return mcr;
+ }
+
+ public boolean commit() {
+ MemoryCommitResult mcr = commitToMemory();
+ SharedPreferencesImpl.this.enqueueDiskWrite(
+ mcr, null /* sync write on this thread okay */);
+ try {
+ mcr.writtenToDiskLatch.await();
+ } catch (InterruptedException e) {
+ return false;
+ }
+ notifyListeners(mcr);
+ return mcr.writeToDiskResult;
+ }
+
+ private void notifyListeners(final MemoryCommitResult mcr) {
+ if (mcr.listeners == null || mcr.keysModified == null ||
+ mcr.keysModified.size() == 0) {
+ return;
+ }
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ for (int i = mcr.keysModified.size() - 1; i >= 0; i--) {
+ final String key = mcr.keysModified.get(i);
+ for (OnSharedPreferenceChangeListener listener : mcr.listeners) {
+ if (listener != null) {
+ listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, key);
+ }
+ }
+ }
+ } else {
+ // Run this function on the main thread.
+ ActivityThread.sMainThreadHandler.post(new Runnable() {
+ public void run() {
+ notifyListeners(mcr);
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ * Enqueue an already-committed-to-memory result to be written
+ * to disk.
+ *
+ * They will be written to disk one-at-a-time in the order
+ * that they're enqueued.
+ *
+ * @param postWriteRunnable if non-null, we're being called
+ * from apply() and this is the runnable to run after
+ * the write proceeds. if null (from a regular commit()),
+ * then we're allowed to do this disk write on the main
+ * thread (which in addition to reducing allocations and
+ * creating a background thread, this has the advantage that
+ * we catch them in userdebug StrictMode reports to convert
+ * them where possible to apply() ...)
+ */
+ private void enqueueDiskWrite(final MemoryCommitResult mcr,
+ final Runnable postWriteRunnable) {
+ final Runnable writeToDiskRunnable = new Runnable() {
+ public void run() {
+ synchronized (mWritingToDiskLock) {
+ writeToFile(mcr);
+ }
+ synchronized (SharedPreferencesImpl.this) {
+ mDiskWritesInFlight--;
+ }
+ if (postWriteRunnable != null) {
+ postWriteRunnable.run();
+ }
+ }
+ };
+
+ final boolean isFromSyncCommit = (postWriteRunnable == null);
+
+ // Typical #commit() path with fewer allocations, doing a write on
+ // the current thread.
+ if (isFromSyncCommit) {
+ boolean wasEmpty = false;
+ synchronized (SharedPreferencesImpl.this) {
+ wasEmpty = mDiskWritesInFlight == 1;
+ }
+ if (wasEmpty) {
+ writeToDiskRunnable.run();
+ return;
+ }
+ }
+
+ QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
+ }
+
+ private static FileOutputStream createFileOutputStream(File file) {
+ FileOutputStream str = null;
+ try {
+ str = new FileOutputStream(file);
+ } catch (FileNotFoundException e) {
+ File parent = file.getParentFile();
+ if (!parent.mkdir()) {
+ Log.e(TAG, "Couldn't create directory for SharedPreferences file " + file);
+ return null;
+ }
+ FileUtils.setPermissions(
+ parent.getPath(),
+ FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
+ -1, -1);
+ try {
+ str = new FileOutputStream(file);
+ } catch (FileNotFoundException e2) {
+ Log.e(TAG, "Couldn't create SharedPreferences file " + file, e2);
+ }
+ }
+ return str;
+ }
+
+ // Note: must hold mWritingToDiskLock
+ private void writeToFile(MemoryCommitResult mcr) {
+ // Rename the current file so it may be used as a backup during the next read
+ if (mFile.exists()) {
+ if (!mcr.changesMade) {
+ // If the file already exists, but no changes were
+ // made to the underlying map, it's wasteful to
+ // re-write the file. Return as if we wrote it
+ // out.
+ mcr.setDiskWriteResult(true);
+ return;
+ }
+ if (!mBackupFile.exists()) {
+ if (!mFile.renameTo(mBackupFile)) {
+ Log.e(TAG, "Couldn't rename file " + mFile
+ + " to backup file " + mBackupFile);
+ mcr.setDiskWriteResult(false);
+ return;
+ }
+ } else {
+ mFile.delete();
+ }
+ }
+
+ // Attempt to write the file, delete the backup and return true as atomically as
+ // possible. If any exception occurs, delete the new file; next time we will restore
+ // from the backup.
+ try {
+ FileOutputStream str = createFileOutputStream(mFile);
+ if (str == null) {
+ mcr.setDiskWriteResult(false);
+ return;
+ }
+ XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
+ FileUtils.sync(str);
+ str.close();
+ ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
+ FileStatus stat = new FileStatus();
+ if (FileUtils.getFileStatus(mFile.getPath(), stat)) {
+ synchronized (this) {
+ mStatTimestamp = stat.mtime;
+ mStatSize = stat.size;
+ }
+ }
+ // Writing was successful, delete the backup file if there is one.
+ mBackupFile.delete();
+ mcr.setDiskWriteResult(true);
+ return;
+ } catch (XmlPullParserException e) {
+ Log.w(TAG, "writeToFile: Got exception:", e);
+ } catch (IOException e) {
+ Log.w(TAG, "writeToFile: Got exception:", e);
+ }
+ // Clean up an unsuccessfully written file
+ if (mFile.exists()) {
+ if (!mFile.delete()) {
+ Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
+ }
+ }
+ mcr.setDiskWriteResult(false);
+ }
+}