diff options
author | Jeff Brown <jeffbrown@google.com> | 2012-01-31 11:48:39 -0800 |
---|---|---|
committer | Jeff Brown <jeffbrown@google.com> | 2012-02-01 16:30:14 -0800 |
commit | b19a71a20adb48c084e87d06a1e6b0dcb49170f5 (patch) | |
tree | 7b44bc9ebdf25842136091c6318c29f1c16a9ecf /core/java | |
parent | f10d69f30107f27465684630460615443783f2bc (diff) | |
download | frameworks_base-b19a71a20adb48c084e87d06a1e6b0dcb49170f5.zip frameworks_base-b19a71a20adb48c084e87d06a1e6b0dcb49170f5.tar.gz frameworks_base-b19a71a20adb48c084e87d06a1e6b0dcb49170f5.tar.bz2 |
Support automatic cancellation of Loaders.
Change-Id: I18d3f49e413f48fcdd519d15e99c238ad54d35b9
Diffstat (limited to 'core/java')
-rw-r--r-- | core/java/android/app/LoaderManager.java | 56 | ||||
-rw-r--r-- | core/java/android/content/AsyncTaskLoader.java | 146 | ||||
-rw-r--r-- | core/java/android/content/CursorLoader.java | 4 | ||||
-rw-r--r-- | core/java/android/content/Loader.java | 101 | ||||
-rw-r--r-- | core/java/android/database/DatabaseUtils.java | 6 |
5 files changed, 256 insertions, 57 deletions
diff --git a/core/java/android/app/LoaderManager.java b/core/java/android/app/LoaderManager.java index d83d2e6..ff71ee7 100644 --- a/core/java/android/app/LoaderManager.java +++ b/core/java/android/app/LoaderManager.java @@ -17,6 +17,7 @@ package android.app; import android.content.Loader; +import android.content.Loader.OnLoadCanceledListener; import android.os.Bundle; import android.util.DebugUtils; import android.util.Log; @@ -219,7 +220,8 @@ class LoaderManagerImpl extends LoaderManager { boolean mCreatingLoader; - final class LoaderInfo implements Loader.OnLoadCompleteListener<Object> { + final class LoaderInfo implements Loader.OnLoadCompleteListener<Object>, + Loader.OnLoadCanceledListener<Object> { final int mId; final Bundle mArgs; LoaderManager.LoaderCallbacks<Object> mCallbacks; @@ -271,6 +273,7 @@ class LoaderManagerImpl extends LoaderManager { } if (!mListenerRegistered) { mLoader.registerListener(mId, this); + mLoader.registerOnLoadCanceledListener(this); mListenerRegistered = true; } mLoader.startLoading(); @@ -329,11 +332,21 @@ class LoaderManagerImpl extends LoaderManager { // Let the loader know we're done with it mListenerRegistered = false; mLoader.unregisterListener(this); + mLoader.unregisterOnLoadCanceledListener(this); mLoader.stopLoading(); } } } - + + void cancel() { + if (DEBUG) Log.v(TAG, " Canceling: " + this); + if (mStarted && mLoader != null && mListenerRegistered) { + if (!mLoader.cancelLoad()) { + onLoadCanceled(mLoader); + } + } + } + void destroy() { if (DEBUG) Log.v(TAG, " Destroying: " + this); mDestroyed = true; @@ -361,6 +374,7 @@ class LoaderManagerImpl extends LoaderManager { if (mListenerRegistered) { mListenerRegistered = false; mLoader.unregisterListener(this); + mLoader.unregisterOnLoadCanceledListener(this); } mLoader.reset(); } @@ -368,8 +382,38 @@ class LoaderManagerImpl extends LoaderManager { mPendingLoader.destroy(); } } - - @Override public void onLoadComplete(Loader<Object> loader, Object data) { + + @Override + public void onLoadCanceled(Loader<Object> loader) { + if (DEBUG) Log.v(TAG, "onLoadCanceled: " + this); + + if (mDestroyed) { + if (DEBUG) Log.v(TAG, " Ignoring load canceled -- destroyed"); + return; + } + + if (mLoaders.get(mId) != this) { + // This cancellation message is not coming from the current active loader. + // We don't care about it. + if (DEBUG) Log.v(TAG, " Ignoring load canceled -- not active"); + return; + } + + LoaderInfo pending = mPendingLoader; + if (pending != null) { + // There is a new request pending and we were just + // waiting for the old one to cancel or complete before starting + // it. So now it is time, switch over to the new loader. + if (DEBUG) Log.v(TAG, " Switching to pending loader: " + pending); + mPendingLoader = null; + mLoaders.put(mId, null); + destroy(); + installLoader(pending); + } + } + + @Override + public void onLoadComplete(Loader<Object> loader, Object data) { if (DEBUG) Log.v(TAG, "onLoadComplete: " + this); if (mDestroyed) { @@ -632,7 +676,9 @@ class LoaderManagerImpl extends LoaderManager { } else { // Now we have three active loaders... we'll queue // up this request to be processed once one of the other loaders - // finishes. + // finishes or is canceled. + if (DEBUG) Log.v(TAG, " Current loader is running; attempting to cancel"); + info.cancel(); if (info.mPendingLoader != null) { if (DEBUG) Log.v(TAG, " Removing pending loader: " + info.mPendingLoader); info.mPendingLoader.destroy(); diff --git a/core/java/android/content/AsyncTaskLoader.java b/core/java/android/content/AsyncTaskLoader.java index 944ca6b..da51952 100644 --- a/core/java/android/content/AsyncTaskLoader.java +++ b/core/java/android/content/AsyncTaskLoader.java @@ -53,19 +53,33 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { static final boolean DEBUG = false; final class LoadTask extends AsyncTask<Void, Void, D> implements Runnable { + private final CountDownLatch mDone = new CountDownLatch(1); - D result; + // Set to true to indicate that the task has been posted to a handler for + // execution at a later time. Used to throttle updates. boolean waiting; - private CountDownLatch done = new CountDownLatch(1); - /* Runs on a worker thread */ @Override protected D doInBackground(Void... params) { if (DEBUG) Slog.v(TAG, this + " >>> doInBackground"); - result = AsyncTaskLoader.this.onLoadInBackground(); - if (DEBUG) Slog.v(TAG, this + " <<< doInBackground"); - return result; + try { + D data = AsyncTaskLoader.this.onLoadInBackground(); + if (DEBUG) Slog.v(TAG, this + " <<< doInBackground"); + return data; + } catch (OperationCanceledException ex) { + if (!isCancelled()) { + // onLoadInBackground threw a canceled exception spuriously. + // This is problematic because it means that the LoaderManager did not + // cancel the Loader itself and still expects to receive a result. + // Additionally, the Loader's own state will not have been updated to + // reflect the fact that the task was being canceled. + // So we treat this case as an unhandled exception. + throw ex; + } + if (DEBUG) Slog.v(TAG, this + " <<< doInBackground (was canceled)"); + return null; + } } /* Runs on the UI thread */ @@ -75,25 +89,37 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { try { AsyncTaskLoader.this.dispatchOnLoadComplete(this, data); } finally { - done.countDown(); + mDone.countDown(); } } + /* Runs on the UI thread */ @Override - protected void onCancelled() { + protected void onCancelled(D data) { if (DEBUG) Slog.v(TAG, this + " onCancelled"); try { - AsyncTaskLoader.this.dispatchOnCancelled(this, result); + AsyncTaskLoader.this.dispatchOnCancelled(this, data); } finally { - done.countDown(); + mDone.countDown(); } } + /* Runs on the UI thread, when the waiting task is posted to a handler. + * This method is only executed when task execution was deferred (waiting was true). */ @Override public void run() { waiting = false; AsyncTaskLoader.this.executePendingTask(); } + + /* Used for testing purposes to wait for the task to complete. */ + public void waitForLoader() { + try { + mDone.await(); + } catch (InterruptedException e) { + // Ignore + } + } } volatile LoadTask mTask; @@ -109,7 +135,7 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { /** * Set amount to throttle updates by. This is the minimum time from - * when the last {@link #onLoadInBackground()} call has completed until + * when the last {@link #loadInBackground()} call has completed until * a new load is scheduled. * * @param delayMS Amount of delay, in milliseconds. @@ -130,24 +156,9 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { executePendingTask(); } - /** - * Attempt to cancel the current load task. See {@link AsyncTask#cancel(boolean)} - * for more info. Must be called on the main thread of the process. - * - * <p>Cancelling is not an immediate operation, since the load is performed - * in a background thread. If there is currently a load in progress, this - * method requests that the load be cancelled, and notes this is the case; - * once the background thread has completed its work its remaining state - * will be cleared. If another load request comes in during this time, - * it will be held until the cancelled load is complete. - * - * @return Returns <tt>false</tt> if the task could not be cancelled, - * typically because it has already completed normally, or - * because {@link #startLoading()} hasn't been called; returns - * <tt>true</tt> otherwise. - */ - public boolean cancelLoad() { - if (DEBUG) Slog.v(TAG, "cancelLoad: mTask=" + mTask); + @Override + protected boolean onCancelLoad() { + if (DEBUG) Slog.v(TAG, "onCancelLoad: mTask=" + mTask); if (mTask != null) { if (mCancellingTask != null) { // There was a pending task already waiting for a previous @@ -173,7 +184,7 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { if (DEBUG) Slog.v(TAG, "cancelLoad: cancelled=" + cancelled); if (cancelled) { mCancellingTask = mTask; - onCancelLoadInBackground(); + cancelLoadInBackground(); } mTask = null; return cancelled; @@ -184,7 +195,10 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { /** * Called if the task was canceled before it was completed. Gives the class a chance - * to properly dispose of the result. + * to clean up post-cancellation and to properly dispose of the result. + * + * @param data The value that was returned by {@link #loadInBackground}, or null + * if the task threw {@link OperationCanceledException}. */ public void onCanceled(D data) { } @@ -218,6 +232,8 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { if (DEBUG) Slog.v(TAG, "Cancelled task is now canceled!"); mLastLoadCompleteTime = SystemClock.uptimeMillis(); mCancellingTask = null; + if (DEBUG) Slog.v(TAG, "Delivering cancellation"); + deliverCancellation(); executePendingTask(); } } @@ -240,38 +256,72 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { } /** + * Called on a worker thread to perform the actual load and to return + * the result of the load operation. + * + * Implementations should not deliver the result directly, but should return them + * from this method, which will eventually end up calling {@link #deliverResult} on + * the UI thread. If implementations need to process the results on the UI thread + * they may override {@link #deliverResult} and do so there. + * + * To support cancellation, this method should periodically check the value of + * {@link #isLoadInBackgroundCanceled} and terminate when it returns true. + * Subclasses may also override {@link #cancelLoadInBackground} to interrupt the load + * directly instead of polling {@link #isLoadInBackgroundCanceled}. + * + * When the load is canceled, this method may either return normally or throw + * {@link OperationCanceledException}. In either case, the {@link Loader} will + * call {@link #onCanceled} to perform post-cancellation cleanup and to dispose of the + * result object, if any. + * + * @return The result of the load operation. + * + * @throws OperationCanceledException if the load is canceled during execution. + * + * @see #isLoadInBackgroundCanceled + * @see #cancelLoadInBackground + * @see #onCanceled */ public abstract D loadInBackground(); /** - * Called on a worker thread to perform the actual load. Implementations should not deliver the - * result directly, but should return them from this method, which will eventually end up - * calling {@link #deliverResult} on the UI thread. If implementations need to process - * the results on the UI thread they may override {@link #deliverResult} and do so - * there. + * Calls {@link #loadInBackground()}. + * + * This method is reserved for use by the loader framework. + * Subclasses should override {@link #loadInBackground} instead of this method. + * + * @return The result of the load operation. * - * @return Implementations must return the result of their load operation. + * @throws OperationCanceledException if the load is canceled during execution. + * + * @see #loadInBackground */ protected D onLoadInBackground() { return loadInBackground(); } /** - * Override this method to try to abort the computation currently taking - * place on a background thread. + * Called on the main thread to abort a load in progress. + * + * Override this method to abort the current invocation of {@link #loadInBackground} + * that is running in the background on a worker thread. * - * Note that when this method is called, it is possible that {@link #loadInBackground} - * has not started yet or has already completed. + * This method should do nothing if {@link #loadInBackground} has not started + * running or if it has already finished. + * + * @see #loadInBackground */ - protected void onCancelLoadInBackground() { + public void cancelLoadInBackground() { } /** - * Returns true if the current execution of {@link #loadInBackground()} is being canceled. + * Returns true if the current invocation of {@link #loadInBackground} is being canceled. + * + * @return True if the current invocation of {@link #loadInBackground} is being canceled. * - * @return True if the current execution of {@link #loadInBackground()} is being canceled. + * @see #loadInBackground */ - protected boolean isLoadInBackgroundCanceled() { + public boolean isLoadInBackgroundCanceled() { return mCancellingTask != null; } @@ -288,11 +338,7 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { public void waitForLoader() { LoadTask task = mTask; if (task != null) { - try { - task.done.await(); - } catch (InterruptedException e) { - // Ignore - } + task.waitForLoader(); } } diff --git a/core/java/android/content/CursorLoader.java b/core/java/android/content/CursorLoader.java index 6e4aca8..e58fad7 100644 --- a/core/java/android/content/CursorLoader.java +++ b/core/java/android/content/CursorLoader.java @@ -76,8 +76,8 @@ public class CursorLoader extends AsyncTaskLoader<Cursor> { } @Override - protected void onCancelLoadInBackground() { - super.onCancelLoadInBackground(); + public void cancelLoadInBackground() { + super.cancelLoadInBackground(); synchronized (this) { if (mCancelationSignal != null) { diff --git a/core/java/android/content/Loader.java b/core/java/android/content/Loader.java index ac05682..3052414 100644 --- a/core/java/android/content/Loader.java +++ b/core/java/android/content/Loader.java @@ -52,6 +52,7 @@ import java.io.PrintWriter; public class Loader<D> { int mId; OnLoadCompleteListener<D> mListener; + OnLoadCanceledListener<D> mOnLoadCanceledListener; Context mContext; boolean mStarted = false; boolean mAbandoned = false; @@ -100,6 +101,23 @@ public class Loader<D> { } /** + * Interface that is implemented to discover when a Loader has been canceled + * before it finished loading its data. You do not normally need to implement + * this yourself; it is used in the implementation of {@link android.app.LoaderManager} + * to find out when a Loader it is managing has been canceled so that it + * can schedule the next Loader. This interface should only be used if a + * Loader is not being used in conjunction with LoaderManager. + */ + public interface OnLoadCanceledListener<D> { + /** + * Called on the thread that created the Loader when the load is canceled. + * + * @param loader the loader that canceled the load + */ + public void onLoadCanceled(Loader<D> loader); + } + + /** * Stores away the application context associated with context. * Since Loaders can be used across multiple activities it's dangerous to * store the context directly; always use {@link #getContext()} to retrieve @@ -127,6 +145,18 @@ public class Loader<D> { } /** + * Informs the registered {@link OnLoadCanceledListener} that the load has been canceled. + * Should only be called by subclasses. + * + * Must be called from the process's main thread. + */ + public void deliverCancellation() { + if (mOnLoadCanceledListener != null) { + mOnLoadCanceledListener.onLoadCanceled(this); + } + } + + /** * @return an application context retrieved from the Context passed to the constructor. */ public Context getContext() { @@ -171,6 +201,40 @@ public class Loader<D> { } /** + * Registers a listener that will receive callbacks when a load is canceled. + * The callback will be called on the process's main thread so it's safe to + * pass the results to widgets. + * + * Must be called from the process's main thread. + * + * @param listener The listener to register. + */ + public void registerOnLoadCanceledListener(OnLoadCanceledListener<D> listener) { + if (mOnLoadCanceledListener != null) { + throw new IllegalStateException("There is already a listener registered"); + } + mOnLoadCanceledListener = listener; + } + + /** + * Unregisters a listener that was previously added with + * {@link #registerOnLoadCanceledListener}. + * + * Must be called from the process's main thread. + * + * @param listener The listener to unregister. + */ + public void unregisterOnLoadCanceledListener(OnLoadCanceledListener<D> listener) { + if (mOnLoadCanceledListener == null) { + throw new IllegalStateException("No listener register"); + } + if (mOnLoadCanceledListener != listener) { + throw new IllegalArgumentException("Attempting to unregister the wrong listener"); + } + mOnLoadCanceledListener = null; + } + + /** * Return whether this load has been started. That is, its {@link #startLoading()} * has been called and no calls to {@link #stopLoading()} or * {@link #reset()} have yet been made. @@ -234,6 +298,43 @@ public class Loader<D> { } /** + * Attempt to cancel the current load task. + * Must be called on the main thread of the process. + * + * <p>Cancellation is not an immediate operation, since the load is performed + * in a background thread. If there is currently a load in progress, this + * method requests that the load be canceled, and notes this is the case; + * once the background thread has completed its work its remaining state + * will be cleared. If another load request comes in during this time, + * it will be held until the canceled load is complete. + * + * @return Returns <tt>false</tt> if the task could not be canceled, + * typically because it has already completed normally, or + * because {@link #startLoading()} hasn't been called; returns + * <tt>true</tt> otherwise. When <tt>true</tt> is returned, the task + * is still running and the {@link OnLoadCanceledListener} will be called + * when the task completes. + */ + public boolean cancelLoad() { + return onCancelLoad(); + } + + /** + * Subclasses must implement this to take care of requests to {@link #cancelLoad()}. + * This will always be called from the process's main thread. + * + * @return Returns <tt>false</tt> if the task could not be canceled, + * typically because it has already completed normally, or + * because {@link #startLoading()} hasn't been called; returns + * <tt>true</tt> otherwise. When <tt>true</tt> is returned, the task + * is still running and the {@link OnLoadCanceledListener} will be called + * when the task completes. + */ + protected boolean onCancelLoad() { + return false; + } + + /** * Force an asynchronous load. Unlike {@link #startLoading()} this will ignore a previously * loaded data set and load a new one. This simply calls through to the * implementation's {@link #onForceLoad()}. You generally should only call this diff --git a/core/java/android/database/DatabaseUtils.java b/core/java/android/database/DatabaseUtils.java index b69d9bf..0022118 100644 --- a/core/java/android/database/DatabaseUtils.java +++ b/core/java/android/database/DatabaseUtils.java @@ -21,6 +21,7 @@ import org.apache.commons.codec.binary.Hex; import android.content.ContentValues; import android.content.Context; import android.content.OperationApplicationException; +import android.content.OperationCanceledException; import android.database.sqlite.SQLiteAbortException; import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; @@ -107,6 +108,9 @@ public class DatabaseUtils { code = 9; } else if (e instanceof OperationApplicationException) { code = 10; + } else if (e instanceof OperationCanceledException) { + code = 11; + logException = false; } else { reply.writeException(e); Log.e(TAG, "Writing exception to parcel", e); @@ -178,6 +182,8 @@ public class DatabaseUtils { throw new SQLiteDiskIOException(msg); case 9: throw new SQLiteException(msg); + case 11: + throw new OperationCanceledException(msg); default: reply.readException(code, msg); } |