diff options
-rw-r--r-- | core/java/android/app/Activity.java | 1 | ||||
-rw-r--r-- | core/java/android/app/ActivityThread.java | 17 | ||||
-rw-r--r-- | core/java/android/app/ContextImpl.java | 272 | ||||
-rw-r--r-- | core/java/android/app/QueuedWork.java | 91 | ||||
-rw-r--r-- | core/java/android/content/SharedPreferences.java | 7 | ||||
-rw-r--r-- | core/java/android/os/Handler.java | 4 | ||||
-rw-r--r-- | core/java/android/preference/Preference.java | 2 | ||||
-rw-r--r-- | core/java/android/server/BluetoothService.java | 2 | ||||
-rw-r--r-- | services/java/com/android/server/BootReceiver.java | 4 |
9 files changed, 323 insertions, 77 deletions
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index a3a8f09..773ff7c 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -1162,6 +1162,7 @@ public class Activity extends ContextThemeWrapper */ protected void onPause() { mCalled = true; + QueuedWork.waitToFinish(); } /** diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index c07e3d3..084f637 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -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; @@ -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; @@ -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() { @@ -1780,6 +1782,8 @@ public final class ActivityThread { } } + QueuedWork.waitToFinish(); + try { if (data.sync) { if (DEBUG_BROADCAST) Slog.i(TAG, @@ -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); @@ -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); @@ -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); diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 973c708..f33dcf5 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -116,9 +116,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) { @@ -167,8 +170,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; @@ -332,15 +335,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); if (backup.exists()) { @@ -373,10 +375,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; @@ -2696,10 +2698,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; @@ -2708,19 +2712,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() { + FileStatus stat = new FileStatus(); + if (!FileUtils.getFileStatus(mFile.getPath(), stat)) { + return true; + } synchronized (this) { - if (!FileUtils.getFileStatus(mFile.getPath(), mFileStatus)) { - return true; - } - return mTimestamp != mFileStatus.mtime; + return mTimestamp != stat.mtime; } } @@ -2747,7 +2753,7 @@ class ContextImpl extends Context { public Map<String, ?> getAll() { synchronized(this) { //noinspection unchecked - return new HashMap(mMap); + return new HashMap<String, Object>(mMap); } } @@ -2766,7 +2772,7 @@ class ContextImpl extends Context { } public long getLong(String key, long defValue) { synchronized (this) { - Long v = (Long) mMap.get(key); + Long v = (Long)mMap.get(key); return v != null ? v : defValue; } } @@ -2789,10 +2795,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); @@ -2839,30 +2866,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 = - new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet()); + 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; @@ -2872,53 +2936,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 (!isSame) { - mMap.put(k, v); - changesMade = true; + if (existingValue != null && existingValue.equals(v)) { + continue; + } } + 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); + } + }); } - - return returnValue; } } - public Editor edit() { - return new EditorImpl(); + /** + * 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); + + // 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 FileOutputStream createFileOutputStream(File file) { + private static FileOutputStream createFileOutputStream(File file) { FileOutputStream str = null; try { str = new FileOutputStream(file); @@ -2941,21 +3074,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(); @@ -2968,22 +3104,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()) { @@ -2991,7 +3131,7 @@ class ContextImpl extends Context { Log.e(TAG, "Couldn't clean up partially-written file " + mFile); } } - return false; + mcr.setDiskWriteResult(false); } } } diff --git a/core/java/android/app/QueuedWork.java b/core/java/android/app/QueuedWork.java new file mode 100644 index 0000000..af6bb1b --- /dev/null +++ b/core/java/android/app/QueuedWork.java @@ -0,0 +1,91 @@ +/* + * 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(); + } + } +} diff --git a/core/java/android/content/SharedPreferences.java b/core/java/android/content/SharedPreferences.java index f1b1490..b3db2ac 100644 --- a/core/java/android/content/SharedPreferences.java +++ b/core/java/android/content/SharedPreferences.java @@ -40,7 +40,9 @@ 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 @@ -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(); } diff --git a/core/java/android/os/Handler.java b/core/java/android/os/Handler.java index 2a32e54..3b2bf1e 100644 --- a/core/java/android/os/Handler.java +++ b/core/java/android/os/Handler.java @@ -563,13 +563,13 @@ public class Handler { return mMessenger; } } - + private final class MessengerImpl extends IMessenger.Stub { public void send(Message msg) { Handler.this.sendMessage(msg); } } - + private final Message getPostMessage(Runnable r) { Message m = Message.obtain(); m.callback = r; diff --git a/core/java/android/preference/Preference.java b/core/java/android/preference/Preference.java index 197d976..1453329 100644 --- a/core/java/android/preference/Preference.java +++ b/core/java/android/preference/Preference.java @@ -1195,7 +1195,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis private void tryCommit(SharedPreferences.Editor editor) { if (mPreferenceManager.shouldCommit()) { - editor.commit(); + editor.startCommit(); } } diff --git a/core/java/android/server/BluetoothService.java b/core/java/android/server/BluetoothService.java index f00389b..abd66ae 100644 --- a/core/java/android/server/BluetoothService.java +++ b/core/java/android/server/BluetoothService.java @@ -1737,7 +1737,7 @@ public class BluetoothService extends IBluetooth.Stub { mContext.getSharedPreferences(SHARED_PREFERENCES_NAME, mContext.MODE_PRIVATE).edit(); editor.putBoolean(SHARED_PREFERENCE_DOCK_ADDRESS + mDockAddress, true); - editor.commit(); + editor.startCommit(); } } } diff --git a/services/java/com/android/server/BootReceiver.java b/services/java/com/android/server/BootReceiver.java index f409751..d15a058 100644 --- a/services/java/com/android/server/BootReceiver.java +++ b/services/java/com/android/server/BootReceiver.java @@ -165,7 +165,9 @@ public class BootReceiver extends BroadcastReceiver { if (prefs != null) { long lastTime = prefs.getLong(filename, 0); if (lastTime == fileTime) return; // Already logged this particular file - prefs.edit().putLong(filename, fileTime).commit(); + // TODO: move all these SharedPreferences Editor commits + // outside this function to the end of logBootEvents + prefs.edit().putLong(filename, fileTime).startCommit(); } Slog.i(TAG, "Copying " + filename + " to DropBox (" + tag + ")"); |