Loading core/java/android/app/Activity.java +1 −0 Original line number Diff line number Diff line Loading @@ -1162,6 +1162,7 @@ public class Activity extends ContextThemeWrapper */ protected void onPause() { mCalled = true; QueuedWork.waitToFinish(); } /** Loading core/java/android/app/ActivityThread.java +15 −2 Original line number Diff line number Diff line Loading @@ -152,7 +152,7 @@ public final class ActivityThread { = new ArrayList<Application>(); // set of instantiated backup agents, keyed by package name final HashMap<String, BackupAgent> mBackupAgents = new HashMap<String, BackupAgent>(); static final ThreadLocal sThreadLocal = new ThreadLocal(); static final ThreadLocal<ActivityThread> sThreadLocal = new ThreadLocal(); Instrumentation mInstrumentation; String mInstrumentationAppDir = null; String mInstrumentationAppPackage = null; Loading Loading @@ -186,6 +186,8 @@ public final class ActivityThread { final GcIdler mGcIdler = new GcIdler(); boolean mGcIdlerScheduled = false; static Handler sMainThreadHandler; // set once in main() private static final class ActivityClientRecord { IBinder token; int ident; Loading Loading @@ -1111,7 +1113,7 @@ public final class ActivityThread { } public static final ActivityThread currentActivityThread() { return (ActivityThread)sThreadLocal.get(); return sThreadLocal.get(); } public static final String currentPackageName() { Loading Loading @@ -1780,6 +1782,8 @@ public final class ActivityThread { } } QueuedWork.waitToFinish(); try { if (data.sync) { if (DEBUG_BROADCAST) Slog.i(TAG, Loading Loading @@ -2007,6 +2011,9 @@ public final class ActivityThread { data.args.setExtrasClassLoader(s.getClassLoader()); } int res = s.onStartCommand(data.args, data.flags, data.startId); QueuedWork.waitToFinish(); try { ActivityManagerNative.getDefault().serviceDoneExecuting( data.token, 1, data.startId, res); Loading Loading @@ -2035,6 +2042,9 @@ public final class ActivityThread { final String who = s.getClassName(); ((ContextImpl) context).scheduleFinalCleanup(who, "Service"); } QueuedWork.waitToFinish(); try { ActivityManagerNative.getDefault().serviceDoneExecuting( token, 0, 0, 0); Loading Loading @@ -3598,6 +3608,9 @@ public final class ActivityThread { Process.setArgV0("<pre-initialized>"); Looper.prepareMainLooper(); if (sMainThreadHandler == null) { sMainThreadHandler = new Handler(); } ActivityThread thread = new ActivityThread(); thread.attach(false); Loading core/java/android/app/ContextImpl.java +212 −71 Original line number Diff line number Diff line Loading @@ -119,9 +119,12 @@ 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.Map.Entry; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicBoolean; class ReceiverRestrictedContext extends ContextWrapper { ReceiverRestrictedContext(Context base) { Loading Loading @@ -170,8 +173,8 @@ class ContextImpl extends Context { private static ThrottleManager sThrottleManager; private static WifiManager sWifiManager; private static LocationManager sLocationManager; private static final HashMap<File, SharedPreferencesImpl> sSharedPrefs = new HashMap<File, SharedPreferencesImpl>(); private static final HashMap<String, SharedPreferencesImpl> sSharedPrefs = new HashMap<String, SharedPreferencesImpl>(); private AudioManager mAudioManager; /*package*/ LoadedApk mPackageInfo; Loading Loading @@ -335,14 +338,14 @@ class ContextImpl extends Context { @Override public SharedPreferences getSharedPreferences(String name, int mode) { SharedPreferencesImpl sp; File f = getSharedPrefsFile(name); synchronized (sSharedPrefs) { sp = sSharedPrefs.get(f); sp = sSharedPrefs.get(name); if (sp != null && !sp.hasFileChanged()) { //Log.i(TAG, "Returning existing prefs " + name + ": " + sp); return sp; } } File f = getSharedPrefsFile(name); FileInputStream str = null; File backup = makeBackupFile(f); Loading Loading @@ -376,10 +379,10 @@ class ContextImpl extends Context { //Log.i(TAG, "Updating existing prefs " + name + " " + sp + ": " + map); sp.replace(map); } else { sp = sSharedPrefs.get(f); sp = sSharedPrefs.get(name); if (sp == null) { sp = new SharedPreferencesImpl(f, mode, map); sSharedPrefs.put(f, sp); sSharedPrefs.put(name, sp); } } return sp; Loading Loading @@ -2698,10 +2701,12 @@ class ContextImpl extends Context { private final File mFile; private final File mBackupFile; private final int mMode; private Map mMap; private final FileStatus mFileStatus = new FileStatus(); private long mTimestamp; private Map<String, Object> mMap; // guarded by 'this' private long mTimestamp; // guarded by 'this' private int mDiskWritesInFlight = 0; // guarded by 'this' private final Object mWritingToDiskLock = new Object(); private static final Object mContent = new Object(); private WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners; Loading @@ -2710,19 +2715,21 @@ class ContextImpl extends Context { mFile = file; mBackupFile = makeBackupFile(file); mMode = mode; mMap = initialContents != null ? initialContents : new HashMap(); if (FileUtils.getFileStatus(file.getPath(), mFileStatus)) { mTimestamp = mFileStatus.mtime; mMap = initialContents != null ? initialContents : new HashMap<String, Object>(); FileStatus stat = new FileStatus(); if (FileUtils.getFileStatus(file.getPath(), stat)) { mTimestamp = stat.mtime; } mListeners = new WeakHashMap<OnSharedPreferenceChangeListener, Object>(); } public boolean hasFileChanged() { synchronized (this) { if (!FileUtils.getFileStatus(mFile.getPath(), mFileStatus)) { FileStatus stat = new FileStatus(); if (!FileUtils.getFileStatus(mFile.getPath(), stat)) { return true; } return mTimestamp != mFileStatus.mtime; synchronized (this) { return mTimestamp != stat.mtime; } } Loading @@ -2749,7 +2756,7 @@ class ContextImpl extends Context { public Map<String, ?> getAll() { synchronized(this) { //noinspection unchecked return new HashMap(mMap); return new HashMap<String, Object>(mMap); } } Loading Loading @@ -2791,10 +2798,31 @@ class ContextImpl extends Context { } } 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; private AtomicBoolean mCommitInFlight = new AtomicBoolean(false); public Editor putString(String key, String value) { synchronized (this) { mModified.put(key, value); Loading Loading @@ -2841,30 +2869,67 @@ class ContextImpl extends Context { } public void startCommit() { // TODO: implement commit(); if (!mCommitInFlight.compareAndSet(false, true)) { throw new IllegalStateException("can't call startCommit() twice"); } public boolean commit() { boolean returnValue; final MemoryCommitResult mcr = commitToMemory(); final Runnable awaitCommit = new Runnable() { public void run() { try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException ignored) { } } }; boolean hasListeners; boolean changesMade = false; List<String> keysModified = null; Set<OnSharedPreferenceChangeListener> listeners = null; QueuedWork.add(awaitCommit); Runnable postWriteRunnable = new Runnable() { public void run() { awaitCommit.run(); mCommitInFlight.set(false); 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) { hasListeners = mListeners.size() > 0; // 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) { keysModified = new ArrayList<String>(); listeners = mcr.keysModified = new ArrayList<String>(); mcr.listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet()); } synchronized (this) { if (mClear) { if (!mMap.isEmpty()) { changesMade = true; mcr.changesMade = true; mMap.clear(); } mClear = false; Loading @@ -2874,53 +2939,122 @@ class ContextImpl extends Context { String k = e.getKey(); Object v = e.getValue(); if (v == this) { // magic value for a removal mutation if (mMap.containsKey(k)) { mMap.remove(k); changesMade = true; if (!mMap.containsKey(k)) { continue; } mMap.remove(k); } else { boolean isSame = false; if (mMap.containsKey(k)) { Object existingValue = mMap.get(k); isSame = existingValue != null && existingValue.equals(v); if (existingValue != null && existingValue.equals(v)) { continue; } if (!isSame) { mMap.put(k, v); changesMade = true; } mMap.put(k, v); } mcr.changesMade = true; if (hasListeners) { keysModified.add(k); mcr.keysModified.add(k); } } mModified.clear(); } } return mcr; } returnValue = writeFileLocked(changesMade); 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; } if (hasListeners) { for (int i = keysModified.size() - 1; i >= 0; i--) { final String key = keysModified.get(i); for (OnSharedPreferenceChangeListener listener : listeners) { 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 startCommit() 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 startCommit...) */ 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); return returnValue; // 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; } } public Editor edit() { return new EditorImpl(); QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable); } private FileOutputStream createFileOutputStream(File file) { private static FileOutputStream createFileOutputStream(File file) { FileOutputStream str = null; try { str = new FileOutputStream(file); Loading @@ -2943,21 +3077,24 @@ class ContextImpl extends Context { return str; } private boolean writeFileLocked(boolean changesMade) { // 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 (!changesMade) { 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. return true; mcr.setDiskWriteResult(true); return; } if (!mBackupFile.exists()) { if (!mFile.renameTo(mBackupFile)) { Log.e(TAG, "Couldn't rename file " + mFile + " to backup file " + mBackupFile); return false; mcr.setDiskWriteResult(false); return; } } else { mFile.delete(); Loading @@ -2970,22 +3107,26 @@ class ContextImpl extends Context { try { FileOutputStream str = createFileOutputStream(mFile); if (str == null) { return false; mcr.setDiskWriteResult(false); return; } XmlUtils.writeMapXml(mMap, str); XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str); str.close(); setFilePermissionsFromMode(mFile.getPath(), mMode, 0); if (FileUtils.getFileStatus(mFile.getPath(), mFileStatus)) { mTimestamp = mFileStatus.mtime; FileStatus stat = new FileStatus(); if (FileUtils.getFileStatus(mFile.getPath(), stat)) { synchronized (this) { mTimestamp = stat.mtime; } } // Writing was successful, delete the backup file if there is one. mBackupFile.delete(); return true; mcr.setDiskWriteResult(true); return; } catch (XmlPullParserException e) { Log.w(TAG, "writeFileLocked: Got exception:", e); Log.w(TAG, "writeToFile: Got exception:", e); } catch (IOException e) { Log.w(TAG, "writeFileLocked: Got exception:", e); Log.w(TAG, "writeToFile: Got exception:", e); } // Clean up an unsuccessfully written file if (mFile.exists()) { Loading @@ -2993,7 +3134,7 @@ class ContextImpl extends Context { Log.e(TAG, "Couldn't clean up partially-written file " + mFile); } } return false; mcr.setDiskWriteResult(false); } } } core/java/android/app/QueuedWork.java 0 → 100644 +91 −0 Original line number Diff line number Diff line /* * 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 java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * Internal utility class to keep track of process-global work that's * outstanding and hasn't been finished yet. * * This was created for writing SharedPreference edits out * asynchronously so we'd have a mechanism to wait for the writes in * Activity.onPause and similar places, but we may use this mechanism * for other things in the future. * * @hide */ public class QueuedWork { // The set of Runnables that will finish or wait on any async // activities started by the application. private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers = new ConcurrentLinkedQueue<Runnable>(); private static ExecutorService sSingleThreadExecutor = null; // lazy, guarded by class /** * Returns a single-thread Executor shared by the entire process, * creating it if necessary. */ public static ExecutorService singleThreadExecutor() { synchronized (QueuedWork.class) { if (sSingleThreadExecutor == null) { // TODO: can we give this single thread a thread name? sSingleThreadExecutor = Executors.newSingleThreadExecutor(); } return sSingleThreadExecutor; } } /** * Add a runnable to finish (or wait for) a deferred operation * started in this context earlier. Typically finished by e.g. * an Activity#onPause. Used by SharedPreferences$Editor#startCommit(). * * Note that this doesn't actually start it running. This is just * a scratch set for callers doing async work to keep updated with * what's in-flight. In the common case, caller code * (e.g. SharedPreferences) will pretty quickly call remove() * after an add(). The only time these Runnables are run is from * waitToFinish(), below. */ public static void add(Runnable finisher) { sPendingWorkFinishers.add(finisher); } public static void remove(Runnable finisher) { sPendingWorkFinishers.remove(finisher); } /** * Finishes or waits for async operations to complete. * (e.g. SharedPreferences$Editor#startCommit writes) * * Is called from the Activity base class's onPause(), after * BroadcastReceiver's onReceive, after Service command handling, * etc. (so async work is never lost) */ public static void waitToFinish() { Runnable toFinish; while ((toFinish = sPendingWorkFinishers.poll()) != null) { toFinish.run(); } } } core/java/android/content/SharedPreferences.java +3 −4 Original line number Diff line number Diff line Loading @@ -41,6 +41,8 @@ public interface SharedPreferences { * Called when a shared preference is changed, added, or removed. This * may be called even if a preference is set to its existing value. * * <p>This callback will be run on your main thread. * * @param sharedPreferences The {@link SharedPreferences} that received * the change. * @param key The key of the preference that was changed, added, or Loading Loading @@ -187,9 +189,6 @@ public interface SharedPreferences { * <p>If you call this from an {@link android.app.Activity}, * the base class will wait for any async commits to finish in * its {@link android.app.Activity#onPause}.</p> * * @return Returns true if the new values were successfully written * to persistent storage. */ void startCommit(); } Loading Loading
core/java/android/app/Activity.java +1 −0 Original line number Diff line number Diff line Loading @@ -1162,6 +1162,7 @@ public class Activity extends ContextThemeWrapper */ protected void onPause() { mCalled = true; QueuedWork.waitToFinish(); } /** Loading
core/java/android/app/ActivityThread.java +15 −2 Original line number Diff line number Diff line Loading @@ -152,7 +152,7 @@ public final class ActivityThread { = new ArrayList<Application>(); // set of instantiated backup agents, keyed by package name final HashMap<String, BackupAgent> mBackupAgents = new HashMap<String, BackupAgent>(); static final ThreadLocal sThreadLocal = new ThreadLocal(); static final ThreadLocal<ActivityThread> sThreadLocal = new ThreadLocal(); Instrumentation mInstrumentation; String mInstrumentationAppDir = null; String mInstrumentationAppPackage = null; Loading Loading @@ -186,6 +186,8 @@ public final class ActivityThread { final GcIdler mGcIdler = new GcIdler(); boolean mGcIdlerScheduled = false; static Handler sMainThreadHandler; // set once in main() private static final class ActivityClientRecord { IBinder token; int ident; Loading Loading @@ -1111,7 +1113,7 @@ public final class ActivityThread { } public static final ActivityThread currentActivityThread() { return (ActivityThread)sThreadLocal.get(); return sThreadLocal.get(); } public static final String currentPackageName() { Loading Loading @@ -1780,6 +1782,8 @@ public final class ActivityThread { } } QueuedWork.waitToFinish(); try { if (data.sync) { if (DEBUG_BROADCAST) Slog.i(TAG, Loading Loading @@ -2007,6 +2011,9 @@ public final class ActivityThread { data.args.setExtrasClassLoader(s.getClassLoader()); } int res = s.onStartCommand(data.args, data.flags, data.startId); QueuedWork.waitToFinish(); try { ActivityManagerNative.getDefault().serviceDoneExecuting( data.token, 1, data.startId, res); Loading Loading @@ -2035,6 +2042,9 @@ public final class ActivityThread { final String who = s.getClassName(); ((ContextImpl) context).scheduleFinalCleanup(who, "Service"); } QueuedWork.waitToFinish(); try { ActivityManagerNative.getDefault().serviceDoneExecuting( token, 0, 0, 0); Loading Loading @@ -3598,6 +3608,9 @@ public final class ActivityThread { Process.setArgV0("<pre-initialized>"); Looper.prepareMainLooper(); if (sMainThreadHandler == null) { sMainThreadHandler = new Handler(); } ActivityThread thread = new ActivityThread(); thread.attach(false); Loading
core/java/android/app/ContextImpl.java +212 −71 Original line number Diff line number Diff line Loading @@ -119,9 +119,12 @@ 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.Map.Entry; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicBoolean; class ReceiverRestrictedContext extends ContextWrapper { ReceiverRestrictedContext(Context base) { Loading Loading @@ -170,8 +173,8 @@ class ContextImpl extends Context { private static ThrottleManager sThrottleManager; private static WifiManager sWifiManager; private static LocationManager sLocationManager; private static final HashMap<File, SharedPreferencesImpl> sSharedPrefs = new HashMap<File, SharedPreferencesImpl>(); private static final HashMap<String, SharedPreferencesImpl> sSharedPrefs = new HashMap<String, SharedPreferencesImpl>(); private AudioManager mAudioManager; /*package*/ LoadedApk mPackageInfo; Loading Loading @@ -335,14 +338,14 @@ class ContextImpl extends Context { @Override public SharedPreferences getSharedPreferences(String name, int mode) { SharedPreferencesImpl sp; File f = getSharedPrefsFile(name); synchronized (sSharedPrefs) { sp = sSharedPrefs.get(f); sp = sSharedPrefs.get(name); if (sp != null && !sp.hasFileChanged()) { //Log.i(TAG, "Returning existing prefs " + name + ": " + sp); return sp; } } File f = getSharedPrefsFile(name); FileInputStream str = null; File backup = makeBackupFile(f); Loading Loading @@ -376,10 +379,10 @@ class ContextImpl extends Context { //Log.i(TAG, "Updating existing prefs " + name + " " + sp + ": " + map); sp.replace(map); } else { sp = sSharedPrefs.get(f); sp = sSharedPrefs.get(name); if (sp == null) { sp = new SharedPreferencesImpl(f, mode, map); sSharedPrefs.put(f, sp); sSharedPrefs.put(name, sp); } } return sp; Loading Loading @@ -2698,10 +2701,12 @@ class ContextImpl extends Context { private final File mFile; private final File mBackupFile; private final int mMode; private Map mMap; private final FileStatus mFileStatus = new FileStatus(); private long mTimestamp; private Map<String, Object> mMap; // guarded by 'this' private long mTimestamp; // guarded by 'this' private int mDiskWritesInFlight = 0; // guarded by 'this' private final Object mWritingToDiskLock = new Object(); private static final Object mContent = new Object(); private WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners; Loading @@ -2710,19 +2715,21 @@ class ContextImpl extends Context { mFile = file; mBackupFile = makeBackupFile(file); mMode = mode; mMap = initialContents != null ? initialContents : new HashMap(); if (FileUtils.getFileStatus(file.getPath(), mFileStatus)) { mTimestamp = mFileStatus.mtime; mMap = initialContents != null ? initialContents : new HashMap<String, Object>(); FileStatus stat = new FileStatus(); if (FileUtils.getFileStatus(file.getPath(), stat)) { mTimestamp = stat.mtime; } mListeners = new WeakHashMap<OnSharedPreferenceChangeListener, Object>(); } public boolean hasFileChanged() { synchronized (this) { if (!FileUtils.getFileStatus(mFile.getPath(), mFileStatus)) { FileStatus stat = new FileStatus(); if (!FileUtils.getFileStatus(mFile.getPath(), stat)) { return true; } return mTimestamp != mFileStatus.mtime; synchronized (this) { return mTimestamp != stat.mtime; } } Loading @@ -2749,7 +2756,7 @@ class ContextImpl extends Context { public Map<String, ?> getAll() { synchronized(this) { //noinspection unchecked return new HashMap(mMap); return new HashMap<String, Object>(mMap); } } Loading Loading @@ -2791,10 +2798,31 @@ class ContextImpl extends Context { } } 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; private AtomicBoolean mCommitInFlight = new AtomicBoolean(false); public Editor putString(String key, String value) { synchronized (this) { mModified.put(key, value); Loading Loading @@ -2841,30 +2869,67 @@ class ContextImpl extends Context { } public void startCommit() { // TODO: implement commit(); if (!mCommitInFlight.compareAndSet(false, true)) { throw new IllegalStateException("can't call startCommit() twice"); } public boolean commit() { boolean returnValue; final MemoryCommitResult mcr = commitToMemory(); final Runnable awaitCommit = new Runnable() { public void run() { try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException ignored) { } } }; boolean hasListeners; boolean changesMade = false; List<String> keysModified = null; Set<OnSharedPreferenceChangeListener> listeners = null; QueuedWork.add(awaitCommit); Runnable postWriteRunnable = new Runnable() { public void run() { awaitCommit.run(); mCommitInFlight.set(false); 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) { hasListeners = mListeners.size() > 0; // 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) { keysModified = new ArrayList<String>(); listeners = mcr.keysModified = new ArrayList<String>(); mcr.listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet()); } synchronized (this) { if (mClear) { if (!mMap.isEmpty()) { changesMade = true; mcr.changesMade = true; mMap.clear(); } mClear = false; Loading @@ -2874,53 +2939,122 @@ class ContextImpl extends Context { String k = e.getKey(); Object v = e.getValue(); if (v == this) { // magic value for a removal mutation if (mMap.containsKey(k)) { mMap.remove(k); changesMade = true; if (!mMap.containsKey(k)) { continue; } mMap.remove(k); } else { boolean isSame = false; if (mMap.containsKey(k)) { Object existingValue = mMap.get(k); isSame = existingValue != null && existingValue.equals(v); if (existingValue != null && existingValue.equals(v)) { continue; } if (!isSame) { mMap.put(k, v); changesMade = true; } mMap.put(k, v); } mcr.changesMade = true; if (hasListeners) { keysModified.add(k); mcr.keysModified.add(k); } } mModified.clear(); } } return mcr; } returnValue = writeFileLocked(changesMade); 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; } if (hasListeners) { for (int i = keysModified.size() - 1; i >= 0; i--) { final String key = keysModified.get(i); for (OnSharedPreferenceChangeListener listener : listeners) { 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 startCommit() 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 startCommit...) */ 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); return returnValue; // 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; } } public Editor edit() { return new EditorImpl(); QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable); } private FileOutputStream createFileOutputStream(File file) { private static FileOutputStream createFileOutputStream(File file) { FileOutputStream str = null; try { str = new FileOutputStream(file); Loading @@ -2943,21 +3077,24 @@ class ContextImpl extends Context { return str; } private boolean writeFileLocked(boolean changesMade) { // 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 (!changesMade) { 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. return true; mcr.setDiskWriteResult(true); return; } if (!mBackupFile.exists()) { if (!mFile.renameTo(mBackupFile)) { Log.e(TAG, "Couldn't rename file " + mFile + " to backup file " + mBackupFile); return false; mcr.setDiskWriteResult(false); return; } } else { mFile.delete(); Loading @@ -2970,22 +3107,26 @@ class ContextImpl extends Context { try { FileOutputStream str = createFileOutputStream(mFile); if (str == null) { return false; mcr.setDiskWriteResult(false); return; } XmlUtils.writeMapXml(mMap, str); XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str); str.close(); setFilePermissionsFromMode(mFile.getPath(), mMode, 0); if (FileUtils.getFileStatus(mFile.getPath(), mFileStatus)) { mTimestamp = mFileStatus.mtime; FileStatus stat = new FileStatus(); if (FileUtils.getFileStatus(mFile.getPath(), stat)) { synchronized (this) { mTimestamp = stat.mtime; } } // Writing was successful, delete the backup file if there is one. mBackupFile.delete(); return true; mcr.setDiskWriteResult(true); return; } catch (XmlPullParserException e) { Log.w(TAG, "writeFileLocked: Got exception:", e); Log.w(TAG, "writeToFile: Got exception:", e); } catch (IOException e) { Log.w(TAG, "writeFileLocked: Got exception:", e); Log.w(TAG, "writeToFile: Got exception:", e); } // Clean up an unsuccessfully written file if (mFile.exists()) { Loading @@ -2993,7 +3134,7 @@ class ContextImpl extends Context { Log.e(TAG, "Couldn't clean up partially-written file " + mFile); } } return false; mcr.setDiskWriteResult(false); } } }
core/java/android/app/QueuedWork.java 0 → 100644 +91 −0 Original line number Diff line number Diff line /* * 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 java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * Internal utility class to keep track of process-global work that's * outstanding and hasn't been finished yet. * * This was created for writing SharedPreference edits out * asynchronously so we'd have a mechanism to wait for the writes in * Activity.onPause and similar places, but we may use this mechanism * for other things in the future. * * @hide */ public class QueuedWork { // The set of Runnables that will finish or wait on any async // activities started by the application. private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers = new ConcurrentLinkedQueue<Runnable>(); private static ExecutorService sSingleThreadExecutor = null; // lazy, guarded by class /** * Returns a single-thread Executor shared by the entire process, * creating it if necessary. */ public static ExecutorService singleThreadExecutor() { synchronized (QueuedWork.class) { if (sSingleThreadExecutor == null) { // TODO: can we give this single thread a thread name? sSingleThreadExecutor = Executors.newSingleThreadExecutor(); } return sSingleThreadExecutor; } } /** * Add a runnable to finish (or wait for) a deferred operation * started in this context earlier. Typically finished by e.g. * an Activity#onPause. Used by SharedPreferences$Editor#startCommit(). * * Note that this doesn't actually start it running. This is just * a scratch set for callers doing async work to keep updated with * what's in-flight. In the common case, caller code * (e.g. SharedPreferences) will pretty quickly call remove() * after an add(). The only time these Runnables are run is from * waitToFinish(), below. */ public static void add(Runnable finisher) { sPendingWorkFinishers.add(finisher); } public static void remove(Runnable finisher) { sPendingWorkFinishers.remove(finisher); } /** * Finishes or waits for async operations to complete. * (e.g. SharedPreferences$Editor#startCommit writes) * * Is called from the Activity base class's onPause(), after * BroadcastReceiver's onReceive, after Service command handling, * etc. (so async work is never lost) */ public static void waitToFinish() { Runnable toFinish; while ((toFinish = sPendingWorkFinishers.poll()) != null) { toFinish.run(); } } }
core/java/android/content/SharedPreferences.java +3 −4 Original line number Diff line number Diff line Loading @@ -41,6 +41,8 @@ public interface SharedPreferences { * Called when a shared preference is changed, added, or removed. This * may be called even if a preference is set to its existing value. * * <p>This callback will be run on your main thread. * * @param sharedPreferences The {@link SharedPreferences} that received * the change. * @param key The key of the preference that was changed, added, or Loading Loading @@ -187,9 +189,6 @@ public interface SharedPreferences { * <p>If you call this from an {@link android.app.Activity}, * the base class will wait for any async commits to finish in * its {@link android.app.Activity#onPause}.</p> * * @return Returns true if the new values were successfully written * to persistent storage. */ void startCommit(); } Loading