diff --git a/api/current.xml b/api/current.xml index 53f1894c903c0..127787fe4cc68 100644 --- a/api/current.xml +++ b/api/current.xml @@ -19421,6 +19421,21 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The type of data returned by the Loader. If you're using multiple Loaders with + * different return types use Object and case the results. + */ +public abstract class LoaderManagingFragment extends Fragment + implements Loader.OnLoadCompleteListener { + private boolean mStarted = false; + + static final class LoaderInfo { + public Bundle args; + public Loader loader; + } + private HashMap> mLoaders; + private HashMap> mInactiveLoaders; + + /** + * Registers a loader with this activity, registers the callbacks on it, and starts it loading. + * If a loader with the same id has previously been started it will automatically be destroyed + * when the new loader completes it's work. The callback will be delivered before the old loader + * is destroyed. + */ + protected Loader startLoading(int id, Bundle args) { + LoaderInfo info = mLoaders.get(id); + if (info != null) { + // Keep track of the previous instance of this loader so we can destroy + // it when the new one completes. + mInactiveLoaders.put(id, info); + } + info = new LoaderInfo(); + info.args = args; + mLoaders.put(id, info); + Loader loader = onCreateLoader(id, args); + info.loader = loader; + if (mStarted) { + // The activity will start all existing loaders in it's onStart(), so only start them + // here if we're past that point of the activitiy's life cycle + loader.registerListener(id, this); + loader.startLoading(); + } + return loader; + } + + protected abstract Loader onCreateLoader(int id, Bundle args); + protected abstract void onInitializeLoaders(); + protected abstract void onLoadFinished(Loader loader, D data); + + public final void onLoadComplete(Loader loader, D data) { + // Notify of the new data so the app can switch out the old data before + // we try to destroy it. + onLoadFinished(loader, data); + + // Look for an inactive loader and destroy it if found + int id = loader.getId(); + LoaderInfo info = mInactiveLoaders.get(id); + if (info != null) { + Loader oldLoader = info.loader; + if (oldLoader != null) { + oldLoader.destroy(); + } + mInactiveLoaders.remove(id); + } + } + + @Override + public void onCreate(Bundle savedState) { + super.onCreate(savedState); + + if (mLoaders == null) { + // Look for a passed along loader and create a new one if it's not there +// TODO: uncomment once getLastNonConfigurationInstance method is available +// mLoaders = (HashMap) getLastNonConfigurationInstance(); + if (mLoaders == null) { + mLoaders = new HashMap>(); + onInitializeLoaders(); + } + } + if (mInactiveLoaders == null) { + mInactiveLoaders = new HashMap>(); + } + } + + @Override + public void onStart() { + super.onStart(); + + // Call out to sub classes so they can start their loaders + // Let the existing loaders know that we want to be notified when a load is complete + for (HashMap.Entry> entry : mLoaders.entrySet()) { + LoaderInfo info = entry.getValue(); + Loader loader = info.loader; + int id = entry.getKey(); + if (loader == null) { + loader = onCreateLoader(id, info.args); + info.loader = loader; + } + loader.registerListener(id, this); + loader.startLoading(); + } + + mStarted = true; + } + + @Override + public void onStop() { + super.onStop(); + + for (HashMap.Entry> entry : mLoaders.entrySet()) { + LoaderInfo info = entry.getValue(); + Loader loader = info.loader; + if (loader == null) { + continue; + } + + // Let the loader know we're done with it + loader.unregisterListener(this); + + // The loader isn't getting passed along to the next instance so ask it to stop loading + if (!getActivity().isChangingConfigurations()) { + loader.stopLoading(); + } + } + + mStarted = false; + } + + /** TO DO: This needs to be turned into a retained fragment. + @Override + public Object onRetainNonConfigurationInstance() { + // Pass the loader along to the next guy + Object result = mLoaders; + mLoaders = null; + return result; + } + **/ + + @Override + public void onDestroy() { + super.onDestroy(); + + if (mLoaders != null) { + for (HashMap.Entry> entry : mLoaders.entrySet()) { + LoaderInfo info = entry.getValue(); + Loader loader = info.loader; + if (loader == null) { + continue; + } + loader.destroy(); + } + } + } + + /** + * @return the Loader with the given id or null if no matching Loader + * is found. + */ + public Loader getLoader(int id) { + LoaderInfo loaderInfo = mLoaders.get(id); + if (loaderInfo != null) { + return mLoaders.get(id).loader; + } + return null; + } +} diff --git a/core/java/android/content/AsyncTaskLoader.java b/core/java/android/content/AsyncTaskLoader.java new file mode 100644 index 0000000000000..52a1b352f378f --- /dev/null +++ b/core/java/android/content/AsyncTaskLoader.java @@ -0,0 +1,86 @@ +/* + * 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.content; + +import android.os.AsyncTask; + +/** + * Abstract Loader that provides an {@link AsyncTask} to do the work. + * + * @param the data type to be loaded. + */ +public abstract class AsyncTaskLoader extends Loader { + final class LoadTask extends AsyncTask { + /* Runs on a worker thread */ + @Override + protected D doInBackground(Void... params) { + return AsyncTaskLoader.this.loadInBackground(); + } + + /* Runs on the UI thread */ + @Override + protected void onPostExecute(D data) { + AsyncTaskLoader.this.dispatchOnLoadComplete(data); + } + } + + LoadTask mTask; + + public AsyncTaskLoader(Context context) { + super(context); + } + + /** + * Force an asynchronous load. Unlike {@link #startLoading()} this will ignore a previously + * loaded data set and load a new one. + */ + @Override + public void forceLoad() { + mTask = new LoadTask(); + mTask.execute((Void[]) null); + } + + /** + * Attempt to cancel the current load task. See {@link AsyncTask#cancel(boolean)} + * for more info. + * + * @return false if the task could not be canceled, + * typically because it has already completed normally, or + * because {@link #startLoading()} hasn't been called, and + * true otherwise + */ + public boolean cancelLoad() { + if (mTask != null) { + return mTask.cancel(false); + } + return false; + } + + void dispatchOnLoadComplete(D data) { + mTask = null; + deliverResult(data); + } + + /** + * Called on a worker thread to perform the actual load. Implementations should not deliver the + * results directly, but should return them from this method and deliver them from + * {@link #onPostExecute()} + * + * @return the result of the load + */ + public abstract D loadInBackground(); +} diff --git a/core/java/android/content/CursorLoader.java b/core/java/android/content/CursorLoader.java new file mode 100644 index 0000000000000..e1f9dcab592f3 --- /dev/null +++ b/core/java/android/content/CursorLoader.java @@ -0,0 +1,151 @@ +/* + * 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.content; + +import android.database.Cursor; +import android.net.Uri; + +/** + * A loader that queries the {@link ContentResolver} and returns a {@link Cursor}. + */ +public class CursorLoader extends AsyncTaskLoader { + Cursor mCursor; + ForceLoadContentObserver mObserver; + boolean mStopped; + Uri mUri; + String[] mProjection; + String mSelection; + String[] mSelectionArgs; + String mSortOrder; + + /* Runs on a worker thread */ + @Override + public Cursor loadInBackground() { + Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection, + mSelectionArgs, mSortOrder); + // Ensure the cursor window is filled + if (cursor != null) { + cursor.getCount(); + cursor.registerContentObserver(mObserver); + } + return cursor; + } + + /* Runs on the UI thread */ + @Override + public void deliverResult(Cursor cursor) { + if (mStopped) { + // An async query came in while the loader is stopped + cursor.close(); + return; + } + mCursor = cursor; + super.deliverResult(cursor); + } + + public CursorLoader(Context context, Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + super(context); + mObserver = new ForceLoadContentObserver(); + mUri = uri; + mProjection = projection; + mSelection = selection; + mSelectionArgs = selectionArgs; + mSortOrder = sortOrder; + } + + /** + * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks + * will be called on the UI thread. If a previous load has been completed and is still valid + * the result may be passed to the callbacks immediately. + * + * Must be called from the UI thread + */ + @Override + public void startLoading() { + mStopped = false; + + if (mCursor != null) { + deliverResult(mCursor); + } else { + forceLoad(); + } + } + + /** + * Must be called from the UI thread + */ + @Override + public void stopLoading() { + if (mCursor != null && !mCursor.isClosed()) { + mCursor.close(); + mCursor = null; + } + + // Attempt to cancel the current load task if possible. + cancelLoad(); + + // Make sure that any outstanding loads clean themselves up properly + mStopped = true; + } + + @Override + public void destroy() { + // Ensure the loader is stopped + stopLoading(); + } + + public Uri getUri() { + return mUri; + } + + public void setUri(Uri uri) { + mUri = uri; + } + + public String[] getProjection() { + return mProjection; + } + + public void setProjection(String[] projection) { + mProjection = projection; + } + + public String getSelection() { + return mSelection; + } + + public void setSelection(String selection) { + mSelection = selection; + } + + public String[] getSelectionArgs() { + return mSelectionArgs; + } + + public void setSelectionArgs(String[] selectionArgs) { + mSelectionArgs = selectionArgs; + } + + public String getSortOrder() { + return mSortOrder; + } + + public void setSortOrder(String sortOrder) { + mSortOrder = sortOrder; + } +} diff --git a/core/java/android/content/Loader.java b/core/java/android/content/Loader.java new file mode 100644 index 0000000000000..db40e486dbdb0 --- /dev/null +++ b/core/java/android/content/Loader.java @@ -0,0 +1,154 @@ +/* + * 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.content; + +import android.database.ContentObserver; +import android.os.Handler; + +/** + * An abstract class that performs asynchronous loading of data. While Loaders are active + * they should monitor the source of their data and deliver new results when the contents + * change. + * + * @param The result returned when the load is complete + */ +public abstract class Loader { + int mId; + OnLoadCompleteListener mListener; + Context mContext; + + public final class ForceLoadContentObserver extends ContentObserver { + public ForceLoadContentObserver() { + super(new Handler()); + } + + @Override + public boolean deliverSelfNotifications() { + return true; + } + + @Override + public void onChange(boolean selfChange) { + forceLoad(); + } + } + + public interface OnLoadCompleteListener { + /** + * Called on the thread that created the Loader when the load is complete. + * + * @param loader the loader that completed the load + * @param data the result of the load + */ + public void onLoadComplete(Loader loader, D data); + } + + /** + * Stores away the application context associated with context. Since Loaders can be used + * across multiple activities it's dangerous to store the context directly. + * + * @param context used to retrieve the application context. + */ + public Loader(Context context) { + mContext = context.getApplicationContext(); + } + + /** + * Sends the result of the load to the registered listener. Should only be called by subclasses. + * + * Must be called from the UI thread. + * + * @param data the result of the load + */ + public void deliverResult(D data) { + if (mListener != null) { + mListener.onLoadComplete(this, data); + } + } + + /** + * @return an application context retrieved from the Context passed to the constructor. + */ + public Context getContext() { + return mContext; + } + + /** + * @return the ID of this loader + */ + public int getId() { + return mId; + } + + /** + * Registers a class that will receive callbacks when a load is complete. The callbacks will + * be called on the UI thread so it's safe to pass the results to widgets. + * + * Must be called from the UI thread + */ + public void registerListener(int id, OnLoadCompleteListener listener) { + if (mListener != null) { + throw new IllegalStateException("There is already a listener registered"); + } + mListener = listener; + mId = id; + } + + /** + * Must be called from the UI thread + */ + public void unregisterListener(OnLoadCompleteListener listener) { + if (mListener == null) { + throw new IllegalStateException("No listener register"); + } + if (mListener != listener) { + throw new IllegalArgumentException("Attempting to unregister the wrong listener"); + } + mListener = null; + } + + /** + * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks + * will be called on the UI thread. If a previous load has been completed and is still valid + * the result may be passed to the callbacks immediately. The loader will monitor the source of + * the data set and may deliver future callbacks if the source changes. Calling + * {@link #stopLoading} will stop the delivery of callbacks. + * + * Must be called from the UI thread + */ + public abstract void startLoading(); + + /** + * Force an asynchronous load. Unlike {@link #startLoading()} this will ignore a previously + * loaded data set and load a new one. + */ + public abstract void forceLoad(); + + /** + * Stops delivery of updates until the next time {@link #startLoading()} is called + * + * Must be called from the UI thread + */ + public abstract void stopLoading(); + + /** + * Destroys the loader and frees its resources, making it unusable. + * + * Must be called from the UI thread + */ + public abstract void destroy(); +} \ No newline at end of file