Refactor to coupler/presenter pattern. Moved framework classes to correct packages
Bug:2579760
Change-Id: Ia0c475e55830aa2d8b13cdb2685fc0981b6f6ade
diff --git a/src/android/app/patterns/AsyncTaskLoader.java b/src/android/app/patterns/AsyncTaskLoader.java
new file mode 100644
index 0000000..01b3e24
--- /dev/null
+++ b/src/android/app/patterns/AsyncTaskLoader.java
@@ -0,0 +1,61 @@
+/*
+ * 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.patterns;
+
+import android.content.Context;
+import android.os.AsyncTask;
+
+/**
+ * Abstract Loader that provides an {@link AsyncTask} to do the work.
+ *
+ * @param <D> the data type to be loaded.
+ */
+public abstract class AsyncTaskLoader<D> extends Loader<D> {
+ final class LoadListTask extends AsyncTask<Void, Void, D> {
+ /* 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.onLoadComplete(data);
+ }
+ }
+
+ public AsyncTaskLoader(Context context) {
+ super(context);
+ }
+
+ /**
+ * Called on a worker thread to perform the actual load. Implementions should not deliver the
+ * results directly, but should return them from this this method and deliver them from
+ * {@link #onPostExecute()}
+ *
+ * @return the result of the load
+ */
+ protected abstract D loadInBackground();
+
+ /**
+ * Called on the UI thread with the result of the load.
+ *
+ * @param data the result of the load
+ */
+ protected abstract void onLoadComplete(D data);
+}
diff --git a/src/android/app/patterns/CursorLoader.java b/src/android/app/patterns/CursorLoader.java
new file mode 100644
index 0000000..a1f8132
--- /dev/null
+++ b/src/android/app/patterns/CursorLoader.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.patterns;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+
+public class CursorLoader extends AsyncTaskLoader<Cursor> {
+ Cursor mCursor;
+ ForceLoadContentObserver mObserver;
+ boolean mStopped;
+ Uri mUri;
+ String[] mProjection;
+ String mSelection;
+ String[] mSelectionArgs;
+ String mSortOrder;
+
+ /* Runs on a worker thread */
+ @Override
+ protected 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
+ protected void onLoadComplete(Cursor cursor) {
+ if (mStopped) {
+ // An async query came in while the loader is stopped
+ cursor.close();
+ return;
+ }
+ mCursor = cursor;
+ 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();
+ }
+ }
+
+ /**
+ * Force an asynchronous load. Unlike {@link #startLoading()} this will ignore a previously
+ * loaded data set and load a new one.
+ */
+ @Override
+ public void forceLoad() {
+ new LoadListTask().execute((Void[]) null);
+ }
+
+ /**
+ * Must be called from the UI thread
+ */
+ @Override
+ public void stopLoading() {
+ if (mCursor != null && !mCursor.isClosed()) {
+ mCursor.close();
+ mCursor = null;
+ }
+
+ // Make sure that any outstanding loads clean themselves up properly
+ mStopped = true;
+ }
+
+ @Override
+ public void destroy() {
+ // Ensure the loader is stopped
+ stopLoading();
+ }
+}
diff --git a/src/android/app/patterns/Loader.java b/src/android/app/patterns/Loader.java
new file mode 100644
index 0000000..dfc35e9
--- /dev/null
+++ b/src/android/app/patterns/Loader.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.patterns;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.Handler;
+
+public abstract class Loader<D> {
+ private int mId;
+ private OnLoadCompleteListener<D> mListener;
+ private Context mContext;
+
+ protected 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<D> {
+ /**
+ * 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);
+ }
+
+ /**
+ * Sends the result of the load to the register listener.
+ *
+ * @param data the result of the load
+ */
+ protected void deliverResult(D data) {
+ if (mListener != null) {
+ mListener.onLoadComplete(this, 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();
+ }
+
+ /**
+ * @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<D> 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<D> 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 it's resources, making it unusable.
+ *
+ * Must be called from the UI thread
+ */
+ public abstract void destroy();
+}
\ No newline at end of file
diff --git a/src/android/app/patterns/LoaderActivity.java b/src/android/app/patterns/LoaderActivity.java
new file mode 100644
index 0000000..664a8bc
--- /dev/null
+++ b/src/android/app/patterns/LoaderActivity.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.patterns;
+
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import java.util.HashMap;
+
+/**
+ * The idea here was to abstract the generic life cycle junk needed to properly keep loaders going.
+ * It didn't work out as-is because registering the callbacks post config change didn't work.
+ */
+public abstract class LoaderActivity<D> extends Activity implements
+ Loader.OnLoadCompleteListener<D> {
+ private boolean mStarted = false;
+
+ static final class LoaderInfo {
+ public Bundle args;
+ public Loader loader;
+ }
+ private HashMap<Integer, LoaderInfo> mLoaders;
+ private HashMap<Integer, LoaderInfo> 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 void 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();
+ }
+ }
+
+ 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
+ mLoaders = (HashMap<Integer, LoaderInfo>) getLastNonConfigurationInstance();
+ if (mLoaders == null) {
+ mLoaders = new HashMap<Integer, LoaderInfo>();
+ onInitializeLoaders();
+ }
+ }
+ if (mInactiveLoaders == null) {
+ mInactiveLoaders = new HashMap<Integer, LoaderInfo>();
+ }
+ }
+
+ @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<Integer, LoaderInfo> 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<Integer, LoaderInfo> 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 (!isChangingConfigurations()) {
+// loader.stopLoading();
+// }
+ }
+
+ mStarted = false;
+ }
+
+ @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<Integer, LoaderInfo> entry : mLoaders.entrySet()) {
+ LoaderInfo info = entry.getValue();
+ Loader loader = info.loader;
+ if (loader == null) {
+ continue;
+ }
+ loader.destroy();
+ }
+ }
+ }
+}