Refactor to coupler/presenter pattern. Moved framework classes to correct packages

Bug:2579760
Change-Id: Ia0c475e55830aa2d8b13cdb2685fc0981b6f6ade
diff --git a/res/layout/contact_detail.xml b/res/layout/contact_detail.xml
index 541ba1c..b4f8101 100644
--- a/res/layout/contact_detail.xml
+++ b/res/layout/contact_detail.xml
@@ -14,7 +14,7 @@
      limitations under the License.
 -->
 
-<com.android.contacts.views.detail.ContactDetailView xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/contact_details"
     android:orientation="vertical"
     android:layout_width="match_parent"
@@ -49,5 +49,5 @@
         />
     </ScrollView>
             
-</com.android.contacts.views.detail.ContactDetailView>
+</LinearLayout>
 
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/com/android/contacts/mvcframework/Loader.java b/src/android/app/patterns/Loader.java
similarity index 71%
rename from src/com/android/contacts/mvcframework/Loader.java
rename to src/android/app/patterns/Loader.java
index fab2f46..dfc35e9 100644
--- a/src/com/android/contacts/mvcframework/Loader.java
+++ b/src/android/app/patterns/Loader.java
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.contacts.mvcframework;
+package android.app.patterns;
 
 import android.content.Context;
 import android.database.ContentObserver;
@@ -42,37 +42,60 @@
     }
 
     public interface OnLoadCompleteListener<D> {
-        public void onLoadComplete(int id, D data);
+        /**
+         * 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(mId, data);
-
+            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
+     * @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");
-  //      }
+        if (mListener != null) {
+            throw new IllegalStateException("There is already a listener registered");
+        }
         mListener = listener;
         mId = id;
     }
@@ -95,7 +118,7 @@
      * 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.
+     * {@link #stopLoading} will stop the delivery of callbacks. 
      *
      * Must be called from the UI thread
      */
@@ -108,11 +131,15 @@
     public abstract void forceLoad();
 
     /**
-     * Stops delivery of updates.
+     * 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();
diff --git a/src/com/android/contacts/mvcframework/LoaderActivity.java b/src/android/app/patterns/LoaderActivity.java
similarity index 69%
rename from src/com/android/contacts/mvcframework/LoaderActivity.java
rename to src/android/app/patterns/LoaderActivity.java
index 0e74d62..664a8bc 100644
--- a/src/com/android/contacts/mvcframework/LoaderActivity.java
+++ b/src/android/app/patterns/LoaderActivity.java
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.contacts.mvcframework;
+package android.app.patterns;
 
 
 import android.app.Activity;
@@ -28,40 +28,61 @@
  */
 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);
-        Loader loader;
         if (info != null) {
-            loader = info.loader;
-            if (loader != null) {
-                loader.unregisterListener(this);
-                loader.destroy();
-                info.loader = null;
-            }
-        } else {
-            info = new LoaderInfo();
-            info.args = args;
+            // 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 = onCreateLoader(id, args);
-        loader.registerListener(id, this);
-        loader.startLoading();
+        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 abstract void onLoadComplete(int id, 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) {
@@ -75,6 +96,9 @@
                 onInitializeLoaders();
             }
         }
+        if (mInactiveLoaders == null) {
+            mInactiveLoaders = new HashMap<Integer, LoaderInfo>();
+        }
     }
 
     @Override
@@ -90,11 +114,12 @@
             if (loader == null) {
                loader = onCreateLoader(id, info.args);
                info.loader = loader;
-            } else {
-                loader.registerListener(id, this);
             }
+            loader.registerListener(id, this);
             loader.startLoading();
         }
+
+        mStarted = true;
     }
 
     @Override
@@ -116,6 +141,8 @@
 //                loader.stopLoading();
 //            }
         }
+
+        mStarted = false;
     }
 
     @Override
diff --git a/src/com/android/contacts/activities/ContactDetailActivity.java b/src/com/android/contacts/activities/ContactDetailActivity.java
index dc14770..d413a25 100644
--- a/src/com/android/contacts/activities/ContactDetailActivity.java
+++ b/src/com/android/contacts/activities/ContactDetailActivity.java
@@ -18,12 +18,14 @@
 
 import com.android.contacts.ContactsSearchManager;
 import com.android.contacts.R;
-import com.android.contacts.mvcframework.DialogManager;
-import com.android.contacts.mvcframework.LoaderActivity;
-import com.android.contacts.views.detail.ContactDetailView;
+import com.android.contacts.util.DialogManager;
+import com.android.contacts.views.detail.ContactPresenter;
 import com.android.contacts.views.detail.ContactLoader;
 
 import android.app.Dialog;
+import android.app.patterns.Loader;
+import android.app.patterns.LoaderActivity;
+import android.net.Uri;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.KeyEvent;
@@ -33,7 +35,7 @@
 public class ContactDetailActivity extends LoaderActivity<ContactLoader.Result> implements
         DialogManager.DialogShowingViewActivity {
     private static final int LOADER_DETAILS = 1;
-    private ContactDetailView mDetails;
+    private ContactPresenter mCoupler;
     private DialogManager mDialogManager;
 
     private static final String TAG = "ContactDetailActivity";
@@ -49,8 +51,8 @@
 
         mDialogManager = new DialogManager(this, DIALOG_VIEW_DIALOGS_ID1, DIALOG_VIEW_DIALOGS_ID2);
 
-        mDetails = (ContactDetailView) findViewById(R.id.contact_details);
-        mDetails.setCallbacks(new ContactDetailView.DefaultCallbacks(this));
+        mCoupler = new ContactPresenter(this, findViewById(R.id.contact_details));
+        mCoupler.setController(new ContactPresenter.DefaultController(this));
     }
 
     @Override
@@ -64,13 +66,16 @@
             case LOADER_DETAILS: {
                 return new ContactLoader(this, getIntent().getData());
             }
+            default: {
+                Log.wtf(TAG, "Unknown ID in onCreateLoader: " + id);
+            }
         }
         return null;
     }
 
-
     @Override
-    public void onLoadComplete(int id, ContactLoader.Result data) {
+    public void onLoadFinished(Loader loader, ContactLoader.Result data) {
+        final int id = loader.getId();
         switch (id) {
             case LOADER_DETAILS:
                 if (data == ContactLoader.Result.NOT_FOUND) {
@@ -79,15 +84,18 @@
                     finish();
                     return;
                 }
-                mDetails.setData(data);
+                mCoupler.setData(data);
                 break;
+            default: {
+                Log.wtf(TAG, "Unknown ID in onLoadFinished: " + id);
+            }
         }
     }
 
     @Override
     public boolean onCreateOptionsMenu(Menu menu) {
         // TODO: This is too hardwired.
-        if (mDetails.onCreateOptionsMenu(menu, getMenuInflater())) return true;
+        if (mCoupler.onCreateOptionsMenu(menu, getMenuInflater())) return true;
 
         return super.onCreateOptionsMenu(menu);
     }
@@ -95,7 +103,7 @@
     @Override
     public boolean onPrepareOptionsMenu(Menu menu) {
         // TODO: This is too hardwired.
-        if (mDetails.onPrepareOptionsMenu(menu)) return true;
+        if (mCoupler.onPrepareOptionsMenu(menu)) return true;
 
         return super.onPrepareOptionsMenu(menu);
     }
@@ -103,7 +111,7 @@
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         // TODO: This is too hardwired.
-        if (mDetails.onOptionsItemSelected(item)) return true;
+        if (mCoupler.onOptionsItemSelected(item)) return true;
 
         return super.onOptionsItemSelected(item);
     }
@@ -120,7 +128,7 @@
     @Override
     public boolean onContextItemSelected(MenuItem item) {
         // TODO: This is too hardwired.
-        if (mDetails.onContextItemSelected(item)) return true;
+        if (mCoupler.onContextItemSelected(item)) return true;
 
         return super.onContextItemSelected(item);
     }
@@ -138,7 +146,7 @@
     @Override
     public boolean onKeyDown(int keyCode, KeyEvent event) {
         // TODO: This is too hardwired.
-        if (mDetails.onKeyDown(keyCode, event)) return true;
+        if (mCoupler.onKeyDown(keyCode, event)) return true;
 
         return super.onKeyDown(keyCode, event);
     }
diff --git a/src/com/android/contacts/mvcframework/CursorLoader.java b/src/com/android/contacts/mvcframework/CursorLoader.java
deleted file mode 100644
index 25585be..0000000
--- a/src/com/android/contacts/mvcframework/CursorLoader.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * 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 com.android.contacts.mvcframework;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.os.AsyncTask;
-
-public abstract class CursorLoader extends Loader<Cursor> {
-    Cursor mCursor;
-    ForceLoadContentObserver mObserver;
-    boolean mClosed;
-
-    final class LoadListTask extends AsyncTask<Void, Void, Cursor> {
-        /* Runs on a worker thread */
-        @Override
-        protected Cursor doInBackground(Void... params) {
-            Cursor cursor = doQueryInBackground();
-            // Ensure the data is loaded
-            if (cursor != null) {
-                cursor.getCount();
-                cursor.registerContentObserver(mObserver);
-            }
-            return cursor;
-        }
-
-        /* Runs on the UI thread */
-        @Override
-        protected void onPostExecute(Cursor cursor) {
-            if (mClosed) {
-                // An async query came in after the call to close()
-                cursor.close();
-                return;
-            }
-            mCursor = cursor;
-            deliverResult(cursor);
-        }
-    }
-
-    public CursorLoader(Context context) {
-        super(context);
-        mObserver = new ForceLoadContentObserver();
-    }
-
-    /**
-     * 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() {
-        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;
-        }
-    }
-
-    @Override
-    public void destroy() {
-        // Close up the cursor
-        stopLoading();
-        // Make sure that any outstanding loads clean themselves up properly
-        mClosed = true;
-    }
-
-    /** Called from a worker thread to execute the desired query */
-    protected abstract Cursor doQueryInBackground();
-}
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
index 993417d..ffb1b00 100644
--- a/src/com/android/contacts/ui/EditContactActivity.java
+++ b/src/com/android/contacts/ui/EditContactActivity.java
@@ -30,9 +30,9 @@
 import com.android.contacts.model.ContactsSource.EditType;
 import com.android.contacts.model.Editor.EditorListener;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
-import com.android.contacts.mvcframework.DialogManager;
 import com.android.contacts.ui.widget.BaseContactEditorView;
 import com.android.contacts.ui.widget.PhotoEditorView;
+import com.android.contacts.util.DialogManager;
 import com.android.contacts.util.EmptyService;
 import com.android.contacts.util.WeakAsyncTask;
 
diff --git a/src/com/android/contacts/ui/widget/GenericEditorView.java b/src/com/android/contacts/ui/widget/GenericEditorView.java
index 30ef8c1..7901f60 100644
--- a/src/com/android/contacts/ui/widget/GenericEditorView.java
+++ b/src/com/android/contacts/ui/widget/GenericEditorView.java
@@ -25,9 +25,9 @@
 import com.android.contacts.model.ContactsSource.EditField;
 import com.android.contacts.model.ContactsSource.EditType;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
-import com.android.contacts.mvcframework.DialogManager;
-import com.android.contacts.mvcframework.DialogManager.DialogShowingView;
 import com.android.contacts.ui.ViewIdGenerator;
+import com.android.contacts.util.DialogManager;
+import com.android.contacts.util.DialogManager.DialogShowingView;
 
 import android.app.AlertDialog;
 import android.app.Dialog;
diff --git a/src/com/android/contacts/mvcframework/DialogManager.java b/src/com/android/contacts/util/DialogManager.java
similarity index 98%
rename from src/com/android/contacts/mvcframework/DialogManager.java
rename to src/com/android/contacts/util/DialogManager.java
index 420c4b2..4c6baf3 100644
--- a/src/com/android/contacts/mvcframework/DialogManager.java
+++ b/src/com/android/contacts/util/DialogManager.java
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.contacts.mvcframework;
+package com.android.contacts.util;
 
 import android.app.Activity;
 import android.app.Dialog;
diff --git a/src/com/android/contacts/views/detail/ContactLoader.java b/src/com/android/contacts/views/detail/ContactLoader.java
index 55abd9f..bc885df 100644
--- a/src/com/android/contacts/views/detail/ContactLoader.java
+++ b/src/com/android/contacts/views/detail/ContactLoader.java
@@ -16,9 +16,9 @@
 
 package com.android.contacts.views.detail;
 
-import com.android.contacts.mvcframework.Loader;
 import com.android.contacts.util.DataStatus;
 
+import android.app.patterns.Loader;
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.Context;
diff --git a/src/com/android/contacts/views/detail/ContactDetailView.java b/src/com/android/contacts/views/detail/ContactPresenter.java
similarity index 96%
rename from src/com/android/contacts/views/detail/ContactDetailView.java
rename to src/com/android/contacts/views/detail/ContactPresenter.java
index 82d0699..d2dfda1 100644
--- a/src/com/android/contacts/views/detail/ContactDetailView.java
+++ b/src/com/android/contacts/views/detail/ContactPresenter.java
@@ -27,7 +27,7 @@
 import com.android.contacts.model.ContactsSource;
 import com.android.contacts.model.Sources;
 import com.android.contacts.model.ContactsSource.DataKind;
-import com.android.contacts.mvcframework.DialogManager;
+import com.android.contacts.util.DialogManager;
 import com.android.contacts.util.Constants;
 import com.android.contacts.util.DataStatus;
 import com.android.contacts.views.detail.ContactLoader.Result;
@@ -68,7 +68,6 @@
 import android.provider.ContactsContract.CommonDataKinds.Website;
 import android.telephony.PhoneNumberUtils;
 import android.text.TextUtils;
-import android.util.AttributeSet;
 import android.util.Log;
 import android.view.ContextMenu;
 import android.view.KeyEvent;
@@ -79,10 +78,10 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View.OnClickListener;
 import android.view.View.OnCreateContextMenuListener;
 import android.widget.AdapterView;
 import android.widget.ImageView;
-import android.widget.LinearLayout;
 import android.widget.ListView;
 import android.widget.TextView;
 import android.widget.Toast;
@@ -90,7 +89,7 @@
 
 import java.util.ArrayList;
 
-public class ContactDetailView extends LinearLayout implements OnCreateContextMenuListener,
+public class ContactPresenter implements OnCreateContextMenuListener,
         OnItemClickListener, DialogManager.DialogShowingView {
     private static final String TAG = "ContactDetailsView";
     private static final boolean SHOW_SEPARATORS = false;
@@ -103,8 +102,11 @@
 
     private static final int MENU_ITEM_MAKE_DEFAULT = 3;
 
+    private final Context mContext;
+    private final View mView;
+
     private Result mContactData;
-    private Callbacks mCallbacks;
+    private Controller mCallbacks;
     private LayoutInflater mInflater;
     private ContactHeaderWidget mContactHeaderWidget;
     private ListView mListView;
@@ -141,12 +143,38 @@
     private ArrayList<ViewEntry> mOtherEntries = new ArrayList<ViewEntry>();
     private ArrayList<ArrayList<ViewEntry>> mSections = new ArrayList<ArrayList<ViewEntry>>();
 
-    public ContactDetailView(Context context) {
-        super(context);
-    }
+    public ContactPresenter(Context context, View view) {
+        mContext = context;
+        mView = view;
 
-    public ContactDetailView(Context context, AttributeSet attrs) {
-        super(context, attrs);
+        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        mContactHeaderWidget = (ContactHeaderWidget) view.findViewById(R.id.contact_header_widget);
+        mContactHeaderWidget.showStar(true);
+        mContactHeaderWidget.setExcludeMimes(new String[] {
+            Contacts.CONTENT_ITEM_TYPE
+        });
+
+        mListView = (ListView) view.findViewById(android.R.id.list);
+        mListView.setOnCreateContextMenuListener(this);
+        mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
+        mListView.setOnItemClickListener(this);
+        // Don't set it to mListView yet.  We do so later when we bind the adapter.
+        mEmptyView = view.findViewById(android.R.id.empty);
+
+        // Build the list of sections. The order they're added to mSections dictates the
+        // order they are displayed in the list.
+        mSections.add(mPhoneEntries);
+        mSections.add(mSmsEntries);
+        mSections.add(mEmailEntries);
+        mSections.add(mImEntries);
+        mSections.add(mPostalEntries);
+        mSections.add(mNicknameEntries);
+        mSections.add(mOrganizationEntries);
+        mSections.add(mGroupEntries);
+        mSections.add(mOtherEntries);
+
+        //TODO Read this value from a preference
+        mShowSmsLinksForAllPhones = true;
     }
 
     public void setData(Result data) {
@@ -384,15 +412,15 @@
         }
     }
 
-    public interface Callbacks {
+    public interface Controller {
         public void onPrimaryClick(ViewEntry entry);
         public void onSecondaryClick(ViewEntry entry);
     }
 
-    public static final class DefaultCallbacks implements Callbacks {
+    public static final class DefaultController implements Controller {
         private Context mContext;
 
-        public DefaultCallbacks(Context context) {
+        public DefaultController(Context context) {
             mContext = context;
         }
 
@@ -419,44 +447,10 @@
         }
     }
 
-    public void setCallbacks(Callbacks callbacks) {
+    public void setController(Controller callbacks) {
         mCallbacks = callbacks;
     }
 
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        Context context = getContext();
-        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-        mContactHeaderWidget = (ContactHeaderWidget) findViewById(R.id.contact_header_widget);
-        mContactHeaderWidget.showStar(true);
-        mContactHeaderWidget.setExcludeMimes(new String[] {
-            Contacts.CONTENT_ITEM_TYPE
-        });
-
-        mListView = (ListView) findViewById(android.R.id.list);
-        mListView.setOnCreateContextMenuListener(this);
-        mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
-        mListView.setOnItemClickListener(this);
-        // Don't set it to mListView yet.  We do so later when we bind the adapter.
-        mEmptyView = findViewById(android.R.id.empty);
-
-        // Build the list of sections. The order they're added to mSections dictates the
-        // order they are displayed in the list.
-        mSections.add(mPhoneEntries);
-        mSections.add(mSmsEntries);
-        mSections.add(mEmailEntries);
-        mSections.add(mImEntries);
-        mSections.add(mPostalEntries);
-        mSections.add(mNicknameEntries);
-        mSections.add(mOrganizationEntries);
-        mSections.add(mGroupEntries);
-        mSections.add(mOtherEntries);
-
-        //TODO Read this value from a preference
-        mShowSmsLinksForAllPhones = true;
-    }
-
     /* package */ static String buildActionString(DataKind kind, ContentValues values,
             boolean lowerCase, Context context) {
         if (kind.actionHeader == null) {
@@ -931,18 +925,17 @@
     /* package */ void showDialog(int bundleDialogId) {
         Bundle bundle = new Bundle();
         bundle.putInt(DIALOG_ID_KEY, bundleDialogId);
-        getDialogManager().showDialogInView(this, bundle);
+        getDialogManager().showDialogInView(mView, bundle);
     }
 
     private DialogManager getDialogManager() {
         if (mDialogManager == null) {
-            Context context = getContext();
-            if (!(context instanceof DialogManager.DialogShowingViewActivity)) {
+            if (!(mContext instanceof DialogManager.DialogShowingViewActivity)) {
                 throw new IllegalStateException(
                         "View must be hosted in an Activity that implements " +
                         "DialogManager.DialogShowingViewActivity");
             }
-            mDialogManager = ((DialogManager.DialogShowingViewActivity)context).getDialogManager();
+            mDialogManager = ((DialogManager.DialogShowingViewActivity)mContext).getDialogManager();
         }
         return mDialogManager;
     }
@@ -987,7 +980,6 @@
         return ContactEntryAdapter.getEntry(mSections, info.position, SHOW_SEPARATORS);
     }
 
-    @Override
     public boolean onKeyDown(int keyCode, KeyEvent event) {
         switch (keyCode) {
             case KeyEvent.KEYCODE_CALL: {
@@ -1026,6 +1018,6 @@
             }
         }
 
-        return super.onKeyDown(keyCode, event);
+        return false;
     }
 }
diff --git a/tests/src/com/android/contacts/ContactDetailLoaderTest.java b/tests/src/com/android/contacts/ContactDetailLoaderTest.java
index b3a7b88..094de6a 100644
--- a/tests/src/com/android/contacts/ContactDetailLoaderTest.java
+++ b/tests/src/com/android/contacts/ContactDetailLoaderTest.java
@@ -16,11 +16,12 @@
 
 package com.android.contacts;
 
-import com.android.contacts.mvcframework.Loader.OnLoadCompleteListener;
 import com.android.contacts.tests.mocks.ContactsMockContext;
 import com.android.contacts.tests.mocks.MockContentProvider;
 import com.android.contacts.views.detail.ContactLoader;
 
+import android.app.patterns.Loader;
+import android.app.patterns.Loader.OnLoadCompleteListener;
 import android.content.ContentUris;
 import android.net.Uri;
 import android.provider.ContactsContract.CommonDataKinds;
@@ -80,7 +81,7 @@
         // A regular variable can not be written from an anonymous class, so use a 1-array
         final ContactLoader.Result[] result = new ContactLoader.Result[1];
         loader.registerListener(0, new OnLoadCompleteListener<ContactLoader.Result>() {
-            public void onLoadComplete(int id, ContactLoader.Result data) {
+            public void onLoadComplete(Loader loader, ContactLoader.Result data) {
                 result[0] = data;
             }
         });