From d3da44032084b6dd280487280553dcdbd7933e3e Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 10 Nov 2010 08:27:11 -0800 Subject: Move SharedPreferencesImpl out of ContextImpl.java Change-Id: I3a58ec4c9501e906c133e841b5c5ec6bced04a02 --- core/java/android/app/ContextImpl.java | 487 +-------------------- core/java/android/app/SharedPreferencesImpl.java | 518 +++++++++++++++++++++++ 2 files changed, 521 insertions(+), 484 deletions(-) create mode 100644 core/java/android/app/SharedPreferencesImpl.java (limited to 'core') 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 @@ package android.app; 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.io.InputStream; 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 @@ class ContextImpl extends Context { 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 @@ class ContextImpl extends Context { 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 @@ class ContextImpl extends Context { 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 @@ class ContextImpl extends Context { private static HashMap > sStringCache = new HashMap >(); } - - // ---------------------------------------------------------------------- - // ---------------------------------------------------------------------- - // ---------------------------------------------------------------------- - - 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 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 mListeners; - - SharedPreferencesImpl( - File file, int mode, Map initialContents) { - mFile = file; - mBackupFile = makeBackupFile(file); - mMode = mode; - mLoaded = initialContents != null; - mMap = initialContents != null ? initialContents : new HashMap(); - FileStatus stat = new FileStatus(); - if (FileUtils.getFileStatus(file.getPath(), stat)) { - mStatTimestamp = stat.mtime; - } - mListeners = new WeakHashMap(); - } - - // 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 getAll() { - synchronized(this) { - //noinspection unchecked - return new HashMap(mMap); - } - } - - public String getString(String key, String defValue) { - synchronized (this) { - String v = (String)mMap.get(key); - return v != null ? v : defValue; - } - } - - public Set getStringSet(String key, Set defValues) { - synchronized (this) { - Set v = (Set) 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 keysModified; // may be null - public Set 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 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 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(mMap); - } - mcr.mapToWriteToDisk = mMap; - mDiskWritesInFlight++; - - boolean hasListeners = mListeners.size() > 0; - if (hasListeners) { - mcr.keysModified = new ArrayList(); - mcr.listeners = - new HashSet(mListeners.keySet()); - } - - synchronized (this) { - if (mClear) { - if (!mMap.isEmpty()) { - mcr.changesMade = true; - mMap.clear(); - } - mClear = false; - } - - for (Entry 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 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 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(); + FileStatus stat = new FileStatus(); + if (FileUtils.getFileStatus(file.getPath(), stat)) { + mStatTimestamp = stat.mtime; + } + mListeners = new WeakHashMap(); + } + + // 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 getAll() { + synchronized(this) { + //noinspection unchecked + return new HashMap(mMap); + } + } + + public String getString(String key, String defValue) { + synchronized (this) { + String v = (String)mMap.get(key); + return v != null ? v : defValue; + } + } + + public Set getStringSet(String key, Set defValues) { + synchronized (this) { + Set v = (Set) 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 keysModified; // may be null + public Set 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 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 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(mMap); + } + mcr.mapToWriteToDisk = mMap; + mDiskWritesInFlight++; + + boolean hasListeners = mListeners.size() > 0; + if (hasListeners) { + mcr.keysModified = new ArrayList(); + mcr.listeners = + new HashSet(mListeners.keySet()); + } + + synchronized (this) { + if (mClear) { + if (!mMap.isEmpty()) { + mcr.changesMade = true; + mMap.clear(); + } + mClear = false; + } + + for (Map.Entry 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); + } +} -- cgit v1.1