Break up view-contact-activity into smaller pieces so that they can be better recombined. Also first import of temporary mvc-framework classes that should later go into the framework.
Bug:2579760
Change-Id: I865b6194fbd28abb415e9d54622b91d719288204
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 5bb147e..a7eec1d 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -352,7 +352,7 @@
</activity>
<!-- Views the details of a single contact -->
- <activity android:name="ViewContactActivity"
+ <activity android:name=".activities.ContactDetailActivity"
android:label="@string/viewContactTitle"
android:theme="@style/TallTitleBarTheme">
diff --git a/res/layout/contact_detail.xml b/res/layout/contact_detail.xml
new file mode 100644
index 0000000..541ba1c
--- /dev/null
+++ b/res/layout/contact_detail.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 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.
+-->
+
+<com.android.contacts.views.detail.ContactDetailView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/contact_details"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.android.internal.widget.ContactHeaderWidget
+ android:id="@+id/contact_header_widget"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <ListView android:id="@android:id/list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/title_bar_shadow"
+ />
+
+ <ScrollView android:id="@android:id/empty"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fillViewport="true"
+ >
+ <TextView android:id="@+id/emptyText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/no_contact_details"
+ android:textSize="20sp"
+ android:textColor="?android:attr/textColorSecondary"
+ android:paddingLeft="10dip"
+ android:paddingRight="10dip"
+ android:paddingTop="10dip"
+ android:lineSpacingMultiplier="0.92"
+ />
+ </ScrollView>
+
+</com.android.contacts.views.detail.ContactDetailView>
+
diff --git a/src/com/android/contacts/ContactEntryAdapter.java b/src/com/android/contacts/ContactEntryAdapter.java
index 34ee505..90a41ca 100644
--- a/src/com/android/contacts/ContactEntryAdapter.java
+++ b/src/com/android/contacts/ContactEntryAdapter.java
@@ -77,7 +77,8 @@
}
}
- ContactEntryAdapter(Context context, ArrayList<ArrayList<E>> sections, boolean separators) {
+ protected ContactEntryAdapter(Context context, ArrayList<ArrayList<E>> sections,
+ boolean separators) {
mContext = context;
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mSections = sections;
diff --git a/src/com/android/contacts/activities/ContactDetailActivity.java b/src/com/android/contacts/activities/ContactDetailActivity.java
new file mode 100644
index 0000000..5dabdc7
--- /dev/null
+++ b/src/com/android/contacts/activities/ContactDetailActivity.java
@@ -0,0 +1,175 @@
+/*
+ * 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.activities;
+
+import com.android.contacts.ContactsSearchManager;
+import com.android.contacts.R;
+import com.android.contacts.mvcframework.DialogManager;
+import com.android.contacts.views.detail.ContactDetailView;
+import com.android.contacts.views.detail.ContactLoader;
+import com.android.contacts.views.detail.ContactLoader.Result;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+
+public class ContactDetailActivity extends Activity implements ContactLoader.Callbacks,
+ DialogManager.DialogShowingViewActivity {
+ private ContactDetailView mDetails;
+ private ContactLoader mLoader;
+ private Uri mUri;
+ private DialogManager mDialogManager;
+
+ private static final String TAG = "ContactDetailActivity";
+
+ private static final int DIALOG_VIEW_DIALOGS_ID1 = 1;
+ private static final int DIALOG_VIEW_DIALOGS_ID2 = 2;
+
+ @Override
+ public void onCreate(Bundle savedState) {
+ super.onCreate(savedState);
+
+ setContentView(R.layout.contact_detail);
+
+ 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));
+
+ mUri = getIntent().getData();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ if (mLoader == null) {
+ // Look for a passed along loader and create a new one if it's not there
+ mLoader = (ContactLoader) getLastNonConfigurationInstance();
+ if (mLoader == null) {
+ mLoader = new ContactLoader(this, mUri);
+ }
+ }
+ mLoader.registerCallbacks(this);
+ mLoader.startLoading();
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+
+ // Let the loader know we're done with it
+ mLoader.unregisterCallbacks(this);
+
+ // The loader isn't getting passed along to the next instance so ask it to stop loading
+ // TODO: Readd this once we have framework support
+ /*if (!isChangingConfigurations()) {
+ mLoader.stopLoading();
+ }*/
+ }
+
+ @Override
+ public Object onRetainNonConfigurationInstance() {
+ // Pass the loader along to the next guy
+ Object result = mLoader;
+ mLoader = null;
+ return result;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ if (mLoader != null) {
+ mLoader.destroy();
+ }
+ }
+
+ public void onContactLoaded(Result contact) {
+ if (contact == ContactLoader.Result.NOT_FOUND) {
+ // Item has been deleted
+ Log.i(TAG, "No contact found. Closing activity");
+ finish();
+ return;
+ }
+ mDetails.setData(contact);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // TODO: This is too hardwired.
+ if (mDetails.onCreateOptionsMenu(menu, getMenuInflater())) return true;
+
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ // TODO: This is too hardwired.
+ if (mDetails.onPrepareOptionsMenu(menu)) return true;
+
+ return super.onPrepareOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ // TODO: This is too hardwired.
+ if (mDetails.onOptionsItemSelected(item)) return true;
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ public DialogManager getDialogManager() {
+ return mDialogManager;
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id, Bundle args) {
+ return mDialogManager.onCreateDialog(id, args);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ // TODO: This is too hardwired.
+ if (mDetails.onContextItemSelected(item)) return true;
+
+ return super.onContextItemSelected(item);
+ }
+
+ @Override
+ public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData,
+ boolean globalSearch) {
+ if (globalSearch) {
+ super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
+ } else {
+ ContactsSearchManager.startSearch(this, initialQuery);
+ }
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ // TODO: This is too hardwired.
+ if (mDetails.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
new file mode 100644
index 0000000..89ae997
--- /dev/null
+++ b/src/com/android/contacts/mvcframework/CursorLoader.java
@@ -0,0 +1,117 @@
+/*
+ * 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<CursorLoader.Callbacks> {
+ private Context mContext;
+ private Cursor mCursor;
+ private ForceLoadContentObserver mObserver;
+ private boolean mClosed;
+
+ public interface Callbacks {
+ public void onCursorLoaded(Cursor cursor);
+ }
+
+ protected Context getContext() {
+ return mContext;
+ }
+
+ 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;
+ if (mCallbacks != null) {
+ // A listener is register, notify them of the result
+ mCallbacks.onCursorLoaded(cursor);
+ }
+ }
+ }
+
+ public CursorLoader(Context context) {
+ mContext = context.getApplicationContext();
+ 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) {
+ mCallbacks.onCursorLoaded(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/DialogManager.java b/src/com/android/contacts/mvcframework/DialogManager.java
similarity index 88%
rename from src/com/android/contacts/ui/DialogManager.java
rename to src/com/android/contacts/mvcframework/DialogManager.java
index 469af06..420c4b2 100644
--- a/src/com/android/contacts/ui/DialogManager.java
+++ b/src/com/android/contacts/mvcframework/DialogManager.java
@@ -1,4 +1,20 @@
-package com.android.contacts.ui;
+/*
+ * 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.app.Activity;
import android.app.Dialog;
diff --git a/src/com/android/contacts/mvcframework/Loader.java b/src/com/android/contacts/mvcframework/Loader.java
new file mode 100644
index 0000000..456b53a
--- /dev/null
+++ b/src/com/android/contacts/mvcframework/Loader.java
@@ -0,0 +1,93 @@
+/*
+ * 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.database.ContentObserver;
+import android.os.Handler;
+
+public abstract class Loader<E> {
+ protected E mCallbacks;
+
+ protected final class ForceLoadContentObserver extends ContentObserver {
+ public ForceLoadContentObserver() {
+ super(new Handler());
+ }
+
+ @Override
+ public boolean deliverSelfNotifications() {
+ return true;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ forceLoad();
+ }
+ }
+
+ /**
+ * 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 registerCallbacks(E callbacks) {
+ if (mCallbacks != null) {
+ throw new IllegalStateException("There are already callbacks registered");
+ }
+ mCallbacks = callbacks;
+ }
+
+ /**
+ * Must be called from the UI thread
+ */
+ public void unregisterCallbacks(E callbacks) {
+ if (mCallbacks == null) {
+ throw new IllegalStateException("No callbacks register");
+ }
+ if (mCallbacks != callbacks) {
+ throw new IllegalArgumentException("Attempting to unregister the wrong callbacks");
+ }
+ mCallbacks = 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.
+ */
+ public abstract void stopLoading();
+
+ /**
+ * Must be called from the UI thread
+ */
+ public abstract void destroy();
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
index 08f5ac1..993417d 100644
--- a/src/com/android/contacts/ui/EditContactActivity.java
+++ b/src/com/android/contacts/ui/EditContactActivity.java
@@ -30,6 +30,7 @@
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.EmptyService;
diff --git a/src/com/android/contacts/ui/widget/GenericEditorView.java b/src/com/android/contacts/ui/widget/GenericEditorView.java
index cfee1c3..30ef8c1 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.ui.DialogManager;
+import com.android.contacts.mvcframework.DialogManager;
+import com.android.contacts.mvcframework.DialogManager.DialogShowingView;
import com.android.contacts.ui.ViewIdGenerator;
-import com.android.contacts.ui.DialogManager.DialogShowingView;
import android.app.AlertDialog;
import android.app.Dialog;
diff --git a/src/com/android/contacts/views/detail/ContactDetailView.java b/src/com/android/contacts/views/detail/ContactDetailView.java
new file mode 100644
index 0000000..fd68bd2
--- /dev/null
+++ b/src/com/android/contacts/views/detail/ContactDetailView.java
@@ -0,0 +1,1038 @@
+/*
+ * 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.views.detail;
+
+import com.android.contacts.Collapser;
+import com.android.contacts.ContactEntryAdapter;
+import com.android.contacts.ContactOptionsActivity;
+import com.android.contacts.ContactPresenceIconUtil;
+import com.android.contacts.ContactsUtils;
+import com.android.contacts.R;
+import com.android.contacts.TypePrecedence;
+import com.android.contacts.Collapser.Collapsible;
+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.Constants;
+import com.android.contacts.util.DataStatus;
+import com.android.contacts.views.detail.ContactLoader.Result;
+import com.android.internal.telephony.ITelephony;
+import com.android.internal.widget.ContactHeaderWidget;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.ActivityNotFoundException;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Entity;
+import android.content.Intent;
+import android.content.Entity.NamedContentValues;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.net.ParseException;
+import android.net.Uri;
+import android.net.WebAddress;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.StatusUpdates;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+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;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ContextMenu.ContextMenuInfo;
+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;
+import android.widget.AdapterView.OnItemClickListener;
+
+import java.util.ArrayList;
+
+public class ContactDetailView extends LinearLayout implements OnCreateContextMenuListener,
+ OnItemClickListener, DialogManager.DialogShowingView {
+ private static final String TAG = "ContactDetailsView";
+ private static final boolean SHOW_SEPARATORS = false;
+
+ private static final String DIALOG_ID_KEY = "dialog_id";
+ private static final int DIALOG_CONFIRM_DELETE = 1;
+ private static final int DIALOG_CONFIRM_READONLY_DELETE = 2;
+ private static final int DIALOG_CONFIRM_MULTIPLE_DELETE = 3;
+ private static final int DIALOG_CONFIRM_READONLY_HIDE = 4;
+
+ private static final int MENU_ITEM_MAKE_DEFAULT = 3;
+
+ private Result mContactData;
+ private Callbacks mCallbacks;
+ private LayoutInflater mInflater;
+ private ContactHeaderWidget mContactHeaderWidget;
+ private ListView mListView;
+ private boolean mShowSmsLinksForAllPhones;
+ private ViewAdapter mAdapter;
+ private Uri mPrimaryPhoneUri = null;
+ private DialogManager mDialogManager = null;
+
+ private int mReadOnlySourcesCnt;
+ private int mWritableSourcesCnt;
+ private boolean mAllRestricted;
+ private ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>();
+ private int mNumPhoneNumbers = 0;
+
+ /**
+ * The view shown if the detail list is empty.
+ * We set this to the list view when first bind the adapter, so that it won't be shown while
+ * we're loading data.
+ */
+ private View mEmptyView;
+
+ /**
+ * A list of distinct contact IDs included in the current contact.
+ */
+ private ArrayList<Long> mRawContactIds = new ArrayList<Long>();
+ private ArrayList<ViewEntry> mPhoneEntries = new ArrayList<ViewEntry>();
+ private ArrayList<ViewEntry> mSmsEntries = new ArrayList<ViewEntry>();
+ private ArrayList<ViewEntry> mEmailEntries = new ArrayList<ViewEntry>();
+ private ArrayList<ViewEntry> mPostalEntries = new ArrayList<ViewEntry>();
+ private ArrayList<ViewEntry> mImEntries = new ArrayList<ViewEntry>();
+ private ArrayList<ViewEntry> mNicknameEntries = new ArrayList<ViewEntry>();
+ private ArrayList<ViewEntry> mOrganizationEntries = new ArrayList<ViewEntry>();
+ private ArrayList<ViewEntry> mGroupEntries = new ArrayList<ViewEntry>();
+ private ArrayList<ViewEntry> mOtherEntries = new ArrayList<ViewEntry>();
+ private ArrayList<ArrayList<ViewEntry>> mSections = new ArrayList<ArrayList<ViewEntry>>();
+
+ public ContactDetailView(Context context) {
+ super(context);
+ }
+
+ public ContactDetailView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void setData(Result data) {
+ mContactData = data;
+
+ mContactHeaderWidget.bindFromContactLookupUri(data.getUri());
+ bindData();
+ }
+
+ private void bindData() {
+
+ // Build up the contact entries
+ buildEntries();
+
+ // Collapse similar data items in select sections.
+ Collapser.collapseList(mPhoneEntries);
+ Collapser.collapseList(mSmsEntries);
+ Collapser.collapseList(mEmailEntries);
+ Collapser.collapseList(mPostalEntries);
+ Collapser.collapseList(mImEntries);
+
+ if (mAdapter == null) {
+ mAdapter = new ViewAdapter(mContext, mSections);
+ mListView.setAdapter(mAdapter);
+ } else {
+ mAdapter.setSections(mSections, SHOW_SEPARATORS);
+ }
+ mListView.setEmptyView(mEmptyView);
+ }
+
+ /**
+ * Build up the entries to display on the screen.
+ */
+ private final void buildEntries() {
+ // Clear out the old entries
+ final int numSections = mSections.size();
+ for (int i = 0; i < numSections; i++) {
+ mSections.get(i).clear();
+ }
+
+ mRawContactIds.clear();
+
+ mReadOnlySourcesCnt = 0;
+ mWritableSourcesCnt = 0;
+ mAllRestricted = true;
+ mPrimaryPhoneUri = null;
+ mNumPhoneNumbers = 0;
+
+ mWritableRawContactIds.clear();
+
+ final Sources sources = Sources.getInstance(mContext);
+
+ // Build up method entries
+ if (mContactData == null) {
+ return;
+ }
+
+ for (Entity entity: mContactData.getEntities()) {
+ final ContentValues entValues = entity.getEntityValues();
+ final String accountType = entValues.getAsString(RawContacts.ACCOUNT_TYPE);
+ final long rawContactId = entValues.getAsLong(RawContacts._ID);
+
+ // Mark when this contact has any unrestricted components
+ final boolean isRestricted = entValues.getAsInteger(RawContacts.IS_RESTRICTED) != 0;
+ if (!isRestricted) mAllRestricted = false;
+
+ if (!mRawContactIds.contains(rawContactId)) {
+ mRawContactIds.add(rawContactId);
+ }
+ ContactsSource contactsSource = sources.getInflatedSource(accountType,
+ ContactsSource.LEVEL_SUMMARY);
+ if (contactsSource != null && contactsSource.readOnly) {
+ mReadOnlySourcesCnt += 1;
+ } else {
+ mWritableSourcesCnt += 1;
+ mWritableRawContactIds.add(rawContactId);
+ }
+
+
+ for (NamedContentValues subValue : entity.getSubValues()) {
+ final ContentValues entryValues = subValue.values;
+ entryValues.put(Data.RAW_CONTACT_ID, rawContactId);
+
+ final long dataId = entryValues.getAsLong(Data._ID);
+ final String mimeType = entryValues.getAsString(Data.MIMETYPE);
+ if (mimeType == null) continue;
+
+ final DataKind kind = sources.getKindOrFallback(accountType, mimeType, mContext,
+ ContactsSource.LEVEL_MIMETYPES);
+ if (kind == null) continue;
+
+ final ViewEntry entry = ViewEntry.fromValues(mContext, mimeType, kind,
+ rawContactId, dataId, entryValues);
+
+ final boolean hasData = !TextUtils.isEmpty(entry.data);
+ final boolean isSuperPrimary = entryValues.getAsInteger(
+ Data.IS_SUPER_PRIMARY) != 0;
+
+ if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+ // Build phone entries
+ mNumPhoneNumbers++;
+
+ entry.intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
+ Uri.fromParts(Constants.SCHEME_TEL, entry.data, null));
+ entry.secondaryIntent = new Intent(Intent.ACTION_SENDTO,
+ Uri.fromParts(Constants.SCHEME_SMSTO, entry.data, null));
+
+ // Remember super-primary phone
+ if (isSuperPrimary) mPrimaryPhoneUri = entry.uri;
+
+ entry.isPrimary = isSuperPrimary;
+ mPhoneEntries.add(entry);
+
+ if (entry.type == CommonDataKinds.Phone.TYPE_MOBILE
+ || mShowSmsLinksForAllPhones) {
+ // Add an SMS entry
+ if (kind.iconAltRes > 0) {
+ entry.secondaryActionIcon = kind.iconAltRes;
+ }
+ }
+ } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+ // Build email entries
+ entry.intent = new Intent(Intent.ACTION_SENDTO,
+ Uri.fromParts(Constants.SCHEME_MAILTO, entry.data, null));
+ entry.isPrimary = isSuperPrimary;
+ mEmailEntries.add(entry);
+
+ // When Email rows have status, create additional Im row
+ final DataStatus status = mContactData.getStatuses().get(entry.id);
+ if (status != null) {
+ final String imMime = Im.CONTENT_ITEM_TYPE;
+ final DataKind imKind = sources.getKindOrFallback(accountType,
+ imMime, mContext, ContactsSource.LEVEL_MIMETYPES);
+ final ViewEntry imEntry = ViewEntry.fromValues(mContext,
+ imMime, imKind, rawContactId, dataId, entryValues);
+ imEntry.intent = ContactsUtils.buildImIntent(entryValues);
+ imEntry.applyStatus(status, false);
+ mImEntries.add(imEntry);
+ }
+ } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+ // Build postal entries
+ entry.maxLines = 4;
+ entry.intent = new Intent(Intent.ACTION_VIEW, entry.uri);
+ mPostalEntries.add(entry);
+ } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+ // Build IM entries
+ entry.intent = ContactsUtils.buildImIntent(entryValues);
+ if (TextUtils.isEmpty(entry.label)) {
+ entry.label = mContext.getString(R.string.chat).toLowerCase();
+ }
+
+ // Apply presence and status details when available
+ final DataStatus status = mContactData.getStatuses().get(entry.id);
+ if (status != null) {
+ entry.applyStatus(status, false);
+ }
+ mImEntries.add(entry);
+ } else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType) &&
+ (hasData || !TextUtils.isEmpty(entry.label))) {
+ // Build organization entries
+ final boolean isNameRawContact =
+ (mContactData.getNameRawContactId() == rawContactId);
+
+ final boolean duplicatesTitle =
+ isNameRawContact
+ && mContactData.getDisplayNameSource()
+ == DisplayNameSources.ORGANIZATION
+ && (!hasData || TextUtils.isEmpty(entry.label));
+
+ if (!duplicatesTitle) {
+ entry.uri = null;
+
+ if (TextUtils.isEmpty(entry.label)) {
+ entry.label = entry.data;
+ entry.data = "";
+ }
+
+ mOrganizationEntries.add(entry);
+ }
+ } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+ // Build nickname entries
+ final boolean isNameRawContact =
+ (mContactData.getNameRawContactId() == rawContactId);
+
+ final boolean duplicatesTitle =
+ isNameRawContact
+ && mContactData.getDisplayNameSource() == DisplayNameSources.NICKNAME;
+
+ if (!duplicatesTitle) {
+ entry.uri = null;
+ mNicknameEntries.add(entry);
+ }
+ } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+ // Build note entries
+ entry.uri = null;
+ entry.maxLines = 100;
+ mOtherEntries.add(entry);
+ } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+ // Build note entries
+ entry.uri = null;
+ entry.maxLines = 10;
+ try {
+ WebAddress webAddress = new WebAddress(entry.data);
+ entry.intent = new Intent(Intent.ACTION_VIEW,
+ Uri.parse(webAddress.toString()));
+ } catch (ParseException e) {
+ Log.e(TAG, "Couldn't parse website: " + entry.data);
+ }
+ mOtherEntries.add(entry);
+ } else {
+ // Handle showing custom rows
+ entry.intent = new Intent(Intent.ACTION_VIEW, entry.uri);
+
+ // Use social summary when requested by external source
+ final DataStatus status = mContactData.getStatuses().get(entry.id);
+ final boolean hasSocial = kind.actionBodySocial && status != null;
+ if (hasSocial) {
+ entry.applyStatus(status, true);
+ }
+
+ if (hasSocial || hasData) {
+ mOtherEntries.add(entry);
+ }
+ }
+ }
+ }
+ }
+
+ public interface Callbacks {
+ public void onPrimaryClick(ViewEntry entry);
+ public void onSecondaryClick(ViewEntry entry);
+ }
+
+ public static final class DefaultCallbacks implements Callbacks {
+ private Context mContext;
+
+ public DefaultCallbacks(Context context) {
+ mContext = context;
+ }
+
+ public void onPrimaryClick(ViewEntry entry) {
+ Intent intent = entry.intent;
+ if (intent != null) {
+ try {
+ mContext.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "No activity found for intent: " + intent);
+ }
+ }
+ }
+
+ public void onSecondaryClick(ViewEntry entry) {
+ Intent intent = entry.secondaryIntent;
+ if (intent != null) {
+ try {
+ mContext.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "No activity found for intent: " + intent);
+ }
+ }
+ }
+ }
+
+ public void setCallbacks(Callbacks 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) {
+ return null;
+ }
+ CharSequence actionHeader = kind.actionHeader.inflateUsing(context, values);
+ if (actionHeader == null) {
+ return null;
+ }
+ return lowerCase ? actionHeader.toString().toLowerCase() : actionHeader.toString();
+ }
+
+ /* package */ static String buildDataString(DataKind kind, ContentValues values,
+ Context context) {
+ if (kind.actionBody == null) {
+ return null;
+ }
+ CharSequence actionBody = kind.actionBody.inflateUsing(context, values);
+ return actionBody == null ? null : actionBody.toString();
+ }
+
+ /**
+ * A basic structure with the data for a contact entry in the list.
+ */
+ static class ViewEntry extends ContactEntryAdapter.Entry implements Collapsible<ViewEntry> {
+ public Context context = null;
+ public String resPackageName = null;
+ public int actionIcon = -1;
+ public boolean isPrimary = false;
+ public int secondaryActionIcon = -1;
+ public Intent intent;
+ public Intent secondaryIntent = null;
+ public int maxLabelLines = 1;
+ public ArrayList<Long> ids = new ArrayList<Long>();
+ public int collapseCount = 0;
+
+ public int presence = -1;
+
+ public CharSequence footerLine = null;
+
+ private ViewEntry() {
+ }
+
+ /**
+ * Build new {@link ViewEntry} and populate from the given values.
+ */
+ public static ViewEntry fromValues(Context context, String mimeType, DataKind kind,
+ long rawContactId, long dataId, ContentValues values) {
+ final ViewEntry entry = new ViewEntry();
+ entry.context = context;
+ entry.contactId = rawContactId;
+ entry.id = dataId;
+ entry.uri = ContentUris.withAppendedId(Data.CONTENT_URI, entry.id);
+ entry.mimetype = mimeType;
+ entry.label = buildActionString(kind, values, false, context);
+ entry.data = buildDataString(kind, values, context);
+
+ if (kind.typeColumn != null && values.containsKey(kind.typeColumn)) {
+ entry.type = values.getAsInteger(kind.typeColumn);
+ }
+ if (kind.iconRes > 0) {
+ entry.resPackageName = kind.resPackageName;
+ entry.actionIcon = kind.iconRes;
+ }
+
+ return entry;
+ }
+
+ /**
+ * Apply given {@link DataStatus} values over this {@link ViewEntry}
+ *
+ * @param fillData When true, the given status replaces {@link #data}
+ * and {@link #footerLine}. Otherwise only {@link #presence}
+ * is updated.
+ */
+ public ViewEntry applyStatus(DataStatus status, boolean fillData) {
+ presence = status.getPresence();
+ if (fillData && status.isValid()) {
+ this.data = status.getStatus().toString();
+ this.footerLine = status.getTimestampLabel(context);
+ }
+
+ return this;
+ }
+
+ public boolean collapseWith(ViewEntry entry) {
+ // assert equal collapse keys
+ if (!shouldCollapseWith(entry)) {
+ return false;
+ }
+
+ // Choose the label associated with the highest type precedence.
+ if (TypePrecedence.getTypePrecedence(mimetype, type)
+ > TypePrecedence.getTypePrecedence(entry.mimetype, entry.type)) {
+ type = entry.type;
+ label = entry.label;
+ }
+
+ // Choose the max of the maxLines and maxLabelLines values.
+ maxLines = Math.max(maxLines, entry.maxLines);
+ maxLabelLines = Math.max(maxLabelLines, entry.maxLabelLines);
+
+ // Choose the presence with the highest precedence.
+ if (StatusUpdates.getPresencePrecedence(presence)
+ < StatusUpdates.getPresencePrecedence(entry.presence)) {
+ presence = entry.presence;
+ }
+
+ // If any of the collapsed entries are primary make the whole thing primary.
+ isPrimary = entry.isPrimary ? true : isPrimary;
+
+ // uri, and contactdId, shouldn't make a difference. Just keep the original.
+
+ // Keep track of all the ids that have been collapsed with this one.
+ ids.add(entry.id);
+ collapseCount++;
+ return true;
+ }
+
+ public boolean shouldCollapseWith(ViewEntry entry) {
+ if (entry == null) {
+ return false;
+ }
+
+ if (!ContactsUtils.shouldCollapse(context, mimetype, data, entry.mimetype,
+ entry.data)) {
+ return false;
+ }
+
+ if (!TextUtils.equals(mimetype, entry.mimetype)
+ || !ContactsUtils.areIntentActionEqual(intent, entry.intent)
+ || !ContactsUtils.areIntentActionEqual(secondaryIntent, entry.secondaryIntent)
+ || actionIcon != entry.actionIcon) {
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ /** Cache of the children views of a row */
+ private static class ViewCache {
+ public TextView label;
+ public TextView data;
+ public TextView footer;
+ public ImageView actionIcon;
+ public ImageView presenceIcon;
+ public ImageView primaryIcon;
+ public ImageView secondaryActionButton;
+ public View secondaryActionDivider;
+
+ // Need to keep track of this too
+ public ViewEntry entry;
+ }
+
+ final class ViewAdapter extends ContactEntryAdapter<ViewEntry> implements OnClickListener {
+
+ ViewAdapter(Context context, ArrayList<ArrayList<ViewEntry>> sections) {
+ super(context, sections, SHOW_SEPARATORS);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final ViewEntry entry = getEntry(mSections, position, false);
+ final View v;
+ final ViewCache views;
+
+ // Check to see if we can reuse convertView
+ if (convertView != null) {
+ v = convertView;
+ views = (ViewCache) v.getTag();
+ } else {
+ // Create a new view if needed
+ v = mInflater.inflate(R.layout.list_item_text_icons, parent, false);
+
+ // Cache the children
+ views = new ViewCache();
+ views.label = (TextView) v.findViewById(android.R.id.text1);
+ views.data = (TextView) v.findViewById(android.R.id.text2);
+ views.footer = (TextView) v.findViewById(R.id.footer);
+ views.actionIcon = (ImageView) v.findViewById(R.id.action_icon);
+ views.primaryIcon = (ImageView) v.findViewById(R.id.primary_icon);
+ views.presenceIcon = (ImageView) v.findViewById(R.id.presence_icon);
+ views.secondaryActionButton = (ImageView) v.findViewById(
+ R.id.secondary_action_button);
+ views.secondaryActionButton.setOnClickListener(this);
+ views.secondaryActionDivider = v.findViewById(R.id.divider);
+ v.setTag(views);
+ }
+
+ // Update the entry in the view cache
+ views.entry = entry;
+
+ // Bind the data to the view
+ bindView(v, entry);
+ return v;
+ }
+
+ @Override
+ protected View newView(int position, ViewGroup parent) {
+ // getView() handles this
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ protected void bindView(View view, ViewEntry entry) {
+ final Resources resources = mContext.getResources();
+ ViewCache views = (ViewCache) view.getTag();
+
+ // Set the label
+ TextView label = views.label;
+ setMaxLines(label, entry.maxLabelLines);
+ label.setText(entry.label);
+
+ // Set the data
+ TextView data = views.data;
+ if (data != null) {
+ if (entry.mimetype.equals(Phone.CONTENT_ITEM_TYPE)
+ || entry.mimetype.equals(Constants.MIME_SMS_ADDRESS)) {
+ data.setText(PhoneNumberUtils.formatNumber(entry.data));
+ } else {
+ data.setText(entry.data);
+ }
+ setMaxLines(data, entry.maxLines);
+ }
+
+ // Set the footer
+ if (!TextUtils.isEmpty(entry.footerLine)) {
+ views.footer.setText(entry.footerLine);
+ views.footer.setVisibility(View.VISIBLE);
+ } else {
+ views.footer.setVisibility(View.GONE);
+ }
+
+ // Set the primary icon
+ views.primaryIcon.setVisibility(entry.isPrimary ? View.VISIBLE : View.GONE);
+
+ // Set the action icon
+ ImageView action = views.actionIcon;
+ if (entry.actionIcon != -1) {
+ Drawable actionIcon;
+ if (entry.resPackageName != null) {
+ // Load external resources through PackageManager
+ actionIcon = mContext.getPackageManager().getDrawable(entry.resPackageName,
+ entry.actionIcon, null);
+ } else {
+ actionIcon = resources.getDrawable(entry.actionIcon);
+ }
+ action.setImageDrawable(actionIcon);
+ action.setVisibility(View.VISIBLE);
+ } else {
+ // Things should still line up as if there was an icon, so make it invisible
+ action.setVisibility(View.INVISIBLE);
+ }
+
+ // Set the presence icon
+ Drawable presenceIcon = ContactPresenceIconUtil.getPresenceIcon(
+ mContext, entry.presence);
+ ImageView presenceIconView = views.presenceIcon;
+ if (presenceIcon != null) {
+ presenceIconView.setImageDrawable(presenceIcon);
+ presenceIconView.setVisibility(View.VISIBLE);
+ } else {
+ presenceIconView.setVisibility(View.GONE);
+ }
+
+ // Set the secondary action button
+ ImageView secondaryActionView = views.secondaryActionButton;
+ Drawable secondaryActionIcon = null;
+ if (entry.secondaryActionIcon != -1) {
+ secondaryActionIcon = resources.getDrawable(entry.secondaryActionIcon);
+ }
+ if (entry.secondaryIntent != null && secondaryActionIcon != null) {
+ secondaryActionView.setImageDrawable(secondaryActionIcon);
+ secondaryActionView.setTag(entry.secondaryIntent);
+ secondaryActionView.setVisibility(View.VISIBLE);
+ views.secondaryActionDivider.setVisibility(View.VISIBLE);
+ } else {
+ secondaryActionView.setVisibility(View.GONE);
+ views.secondaryActionDivider.setVisibility(View.GONE);
+ }
+ }
+
+ private void setMaxLines(TextView textView, int maxLines) {
+ if (maxLines == 1) {
+ textView.setSingleLine(true);
+ textView.setEllipsize(TextUtils.TruncateAt.END);
+ } else {
+ textView.setSingleLine(false);
+ textView.setMaxLines(maxLines);
+ textView.setEllipsize(null);
+ }
+ }
+
+ public void onClick(View v) {
+ Intent intent = (Intent) v.getTag();
+ mContext.startActivity(intent);
+ }
+ }
+
+ public boolean onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
+ inflater.inflate(R.menu.view, menu);
+ return true;
+ }
+
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ // Only allow edit when we have at least one raw_contact id
+ final boolean hasRawContact = (mRawContactIds.size() > 0);
+ menu.findItem(R.id.menu_edit).setEnabled(hasRawContact);
+
+ // Only allow share when unrestricted contacts available
+ menu.findItem(R.id.menu_share).setEnabled(!mAllRestricted);
+
+ return true;
+ }
+
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_edit: {
+ if (mRawContactIds.size() > 0) {
+ long rawContactIdToEdit = mRawContactIds.get(0);
+ Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
+ rawContactIdToEdit);
+ mContext.startActivity(new Intent(Intent.ACTION_EDIT, rawContactUri));
+ return true;
+ } else {
+ // There is no rawContact to edit.
+ return false;
+ }
+ }
+ case R.id.menu_delete: {
+ showDeleteConfirmationDialog();
+ return true;
+ }
+ case R.id.menu_options: {
+ final Intent intent = new Intent(mContext, ContactOptionsActivity.class);
+ intent.setData(mContactData.getLookupUri());
+ mContext.startActivity(intent);
+ return true;
+ }
+ case R.id.menu_share: {
+ if (mAllRestricted) return false;
+
+ final String lookupKey = mContactData.getLookupKey();
+ final Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey);
+
+ final Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setType(Contacts.CONTENT_VCARD_TYPE);
+ intent.putExtra(Intent.EXTRA_STREAM, shareUri);
+
+ // Launch chooser to share contact via
+ final CharSequence chooseTitle = mContext.getText(R.string.share_via);
+ final Intent chooseIntent = Intent.createChooser(intent, chooseTitle);
+
+ try {
+ mContext.startActivity(chooseIntent);
+ } catch (ActivityNotFoundException ex) {
+ Toast.makeText(mContext, R.string.share_error, Toast.LENGTH_SHORT).show();
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void showDeleteConfirmationDialog() {
+ if (mReadOnlySourcesCnt > 0 & mWritableSourcesCnt > 0) {
+ showDialog(DIALOG_CONFIRM_READONLY_DELETE);
+ } else if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt == 0) {
+ showDialog(DIALOG_CONFIRM_READONLY_HIDE);
+ } else if (mReadOnlySourcesCnt == 0 && mWritableSourcesCnt > 1) {
+ showDialog(DIALOG_CONFIRM_MULTIPLE_DELETE);
+ } else {
+ showDialog(DIALOG_CONFIRM_DELETE);
+ }
+ }
+
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ AdapterView.AdapterContextMenuInfo info;
+ try {
+ info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ } catch (ClassCastException e) {
+ Log.e(TAG, "bad menuInfo", e);
+ return;
+ }
+
+ // This can be null sometimes, don't crash...
+ if (info == null) {
+ Log.e(TAG, "bad menuInfo");
+ return;
+ }
+
+ ViewEntry entry = ContactEntryAdapter.getEntry(mSections, info.position, SHOW_SEPARATORS);
+ menu.setHeaderTitle(R.string.contactOptionsTitle);
+ if (entry.mimetype.equals(CommonDataKinds.Phone.CONTENT_ITEM_TYPE)) {
+ menu.add(0, 0, 0, R.string.menu_call).setIntent(entry.intent);
+ menu.add(0, 0, 0, R.string.menu_sendSMS).setIntent(entry.secondaryIntent);
+ if (!entry.isPrimary) {
+ menu.add(0, MENU_ITEM_MAKE_DEFAULT, 0, R.string.menu_makeDefaultNumber);
+ }
+ } else if (entry.mimetype.equals(CommonDataKinds.Email.CONTENT_ITEM_TYPE)) {
+ menu.add(0, 0, 0, R.string.menu_sendEmail).setIntent(entry.intent);
+ if (!entry.isPrimary) {
+ menu.add(0, MENU_ITEM_MAKE_DEFAULT, 0, R.string.menu_makeDefaultEmail);
+ }
+ } else if (entry.mimetype.equals(CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE)) {
+ menu.add(0, 0, 0, R.string.menu_viewAddress).setIntent(entry.intent);
+ }
+ }
+
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ ViewEntry entry = ViewAdapter.getEntry(mSections, position, SHOW_SEPARATORS);
+ if (entry == null) {
+ signalError();
+ return;
+ }
+ Intent intent = entry.intent;
+ if (intent == null) {
+ signalError();
+ return;
+ }
+ try {
+ mContext.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "No activity found for intent: " + intent);
+ signalError();
+ }
+ }
+
+ /**
+ * Signal an error to the user via a beep, or some other method.
+ */
+ private void signalError() {
+ Log.w(TAG, "Should warn the user but we can't because we do not have sonification APIs");
+ }
+
+ private final DialogInterface.OnClickListener mDeleteListener =
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ mContext.getContentResolver().delete(mContactData.getLookupUri(), null, null);
+ }
+ };
+
+ public Dialog createDialog(Bundle bundle) {
+ if (bundle == null) throw new IllegalArgumentException("bundle must not be null");
+ int dialogId = bundle.getInt(DIALOG_ID_KEY);
+ switch (dialogId) {
+ case DIALOG_CONFIRM_DELETE:
+ return new AlertDialog.Builder(mContext)
+ .setTitle(R.string.deleteConfirmation_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(R.string.deleteConfirmation)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok, mDeleteListener)
+ .setCancelable(false)
+ .create();
+ case DIALOG_CONFIRM_READONLY_DELETE:
+ return new AlertDialog.Builder(mContext)
+ .setTitle(R.string.deleteConfirmation_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(R.string.readOnlyContactDeleteConfirmation)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok, mDeleteListener)
+ .setCancelable(false)
+ .create();
+ case DIALOG_CONFIRM_MULTIPLE_DELETE:
+ return new AlertDialog.Builder(mContext)
+ .setTitle(R.string.deleteConfirmation_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(R.string.multipleContactDeleteConfirmation)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok, mDeleteListener)
+ .setCancelable(false)
+ .create();
+ case DIALOG_CONFIRM_READONLY_HIDE: {
+ return new AlertDialog.Builder(mContext)
+ .setTitle(R.string.deleteConfirmation_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(R.string.readOnlyContactWarning)
+ .setPositiveButton(android.R.string.ok, mDeleteListener)
+ .create();
+ }
+ default:
+ throw new IllegalArgumentException("Invalid dialogId: " + dialogId);
+ }
+ }
+
+ /* package */ void showDialog(int bundleDialogId) {
+ Bundle bundle = new Bundle();
+ bundle.putInt(DIALOG_ID_KEY, bundleDialogId);
+ getDialogManager().showDialogInView(this, bundle);
+ }
+
+ private DialogManager getDialogManager() {
+ if (mDialogManager == null) {
+ Context context = getContext();
+ if (!(context instanceof DialogManager.DialogShowingViewActivity)) {
+ throw new IllegalStateException(
+ "View must be hosted in an Activity that implements " +
+ "DialogManager.DialogShowingViewActivity");
+ }
+ mDialogManager = ((DialogManager.DialogShowingViewActivity)context).getDialogManager();
+ }
+ return mDialogManager;
+ }
+
+ public boolean onContextItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case MENU_ITEM_MAKE_DEFAULT: {
+ if (makeItemDefault(item)) {
+ return true;
+ }
+ break;
+ }
+ }
+
+ return false;
+ }
+
+ private boolean makeItemDefault(MenuItem item) {
+ ViewEntry entry = getViewEntryForMenuItem(item);
+ if (entry == null) {
+ return false;
+ }
+
+ // Update the primary values in the data record.
+ ContentValues values = new ContentValues(1);
+ values.put(Data.IS_SUPER_PRIMARY, 1);
+
+ mContext.getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, entry.id),
+ values, null, null);
+ return true;
+ }
+
+ private ViewEntry getViewEntryForMenuItem(MenuItem item) {
+ AdapterView.AdapterContextMenuInfo info;
+ try {
+ info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
+ } catch (ClassCastException e) {
+ Log.e(TAG, "bad menuInfo", e);
+ return null;
+ }
+
+ return ContactEntryAdapter.getEntry(mSections, info.position, SHOW_SEPARATORS);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_CALL: {
+ try {
+ ITelephony phone = ITelephony.Stub.asInterface(
+ ServiceManager.checkService("phone"));
+ if (phone != null && !phone.isIdle()) {
+ // Skip out and let the key be handled at a higher level
+ break;
+ }
+ } catch (RemoteException re) {
+ // Fall through and try to call the contact
+ }
+
+ int index = mListView.getSelectedItemPosition();
+ if (index != -1) {
+ final ViewEntry entry = ViewAdapter.getEntry(mSections, index, SHOW_SEPARATORS);
+ if (entry != null &&
+ entry.intent.getAction() == Intent.ACTION_CALL_PRIVILEGED) {
+ mContext.startActivity(entry.intent);
+ return true;
+ }
+ } else if (mPrimaryPhoneUri != null) {
+ // There isn't anything selected, call the default number
+ final Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
+ mPrimaryPhoneUri);
+ mContext.startActivity(intent);
+ return true;
+ }
+ return false;
+ }
+
+ case KeyEvent.KEYCODE_DEL: {
+ showDeleteConfirmationDialog();
+ return true;
+ }
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+}
diff --git a/src/com/android/contacts/views/detail/ContactLoader.java b/src/com/android/contacts/views/detail/ContactLoader.java
new file mode 100644
index 0000000..38e7a5d
--- /dev/null
+++ b/src/com/android/contacts/views/detail/ContactLoader.java
@@ -0,0 +1,353 @@
+/*
+ * 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.views.detail;
+
+import com.android.contacts.mvcframework.Loader;
+import com.android.contacts.util.DataStatus;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Entity;
+import android.content.EntityIterator;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.RawContactsEntity;
+import android.provider.ContactsContract.StatusUpdates;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Loads a single Contact and all it constituent RawContacts.
+ */
+public class ContactLoader extends Loader<ContactLoader.Callbacks> {
+ private Context mContext;
+ private Uri mLookupUri;
+ private Result mContact;
+ private ForceLoadContentObserver mObserver;
+ private boolean mDestroyed;
+
+ private static final String TAG = "ContactLoader";
+
+ public interface Callbacks {
+ public void onContactLoaded(Result contact);
+ }
+
+ /**
+ * The result of a load operation. Contains all data necessary to display the contact.
+ */
+ public static final class Result {
+ /**
+ * Singleton instance that represents "No Contact Found"
+ */
+ public static final Result NOT_FOUND = new Result();
+
+ private final Uri mLookupUri;
+ private final String mLookupKey;
+ private final Uri mUri;
+ private final long mId;
+ private final ArrayList<Entity> mEntities;
+ private final HashMap<Long, DataStatus> mStatuses;
+ private final long mNameRawContactId;
+ private final int mDisplayNameSource;
+
+ /**
+ * Constructor for case "no contact found". This must only be used for the
+ * final {@link Result#NOT_FOUND} singleton
+ */
+ private Result() {
+ mLookupUri = null;
+ mLookupKey = null;
+ mUri = null;
+ mId = -1;
+ mEntities = null;
+ mStatuses = null;
+ mNameRawContactId = -1;
+ mDisplayNameSource = DisplayNameSources.UNDEFINED;
+ }
+
+ /**
+ * Constructor to call when contact was found
+ */
+ private Result(Uri lookupUri, String lookupKey, Uri uri, long id, long nameRawContactId,
+ int displayNameSource) {
+ mLookupUri = lookupUri;
+ mLookupKey = lookupKey;
+ mUri = uri;
+ mId = id;
+ mEntities = new ArrayList<Entity>();
+ mStatuses = new HashMap<Long, DataStatus>();
+ mNameRawContactId = nameRawContactId;
+ mDisplayNameSource = displayNameSource;
+ }
+
+ public Uri getLookupUri() {
+ return mLookupUri;
+ }
+ public String getLookupKey() {
+ return mLookupKey;
+ }
+ public Uri getUri() {
+ return mUri;
+ }
+ public long getId() {
+ return mId;
+ }
+ public ArrayList<Entity> getEntities() {
+ return mEntities;
+ }
+ public HashMap<Long, DataStatus> getStatuses() {
+ return mStatuses;
+ }
+ public long getNameRawContactId() {
+ return mNameRawContactId;
+ }
+ public int getDisplayNameSource() {
+ return mDisplayNameSource;
+ }
+ }
+
+ interface StatusQuery {
+ final String[] PROJECTION = new String[] {
+ Data._ID, Data.STATUS, Data.STATUS_RES_PACKAGE, Data.STATUS_ICON,
+ Data.STATUS_LABEL, Data.STATUS_TIMESTAMP, Data.PRESENCE,
+ };
+
+ final int _ID = 0;
+ }
+
+ final class LoadContactTask extends AsyncTask<Void, Void, Result> {
+ @Override
+ protected Result doInBackground(Void... args) {
+ final ContentResolver resolver = mContext.getContentResolver();
+ Result result = loadContactHeaderData(resolver, mLookupUri);
+ if (result == Result.NOT_FOUND) {
+ // No record found. Try to lookup up a new record with the same lookupKey.
+ // We might have went through a sync where Ids changed
+ final Uri freshLookupUri = Contacts.getLookupUri(resolver, mLookupUri);
+ result = loadContactHeaderData(resolver, freshLookupUri);
+ if (result == Result.NOT_FOUND) {
+ // Still not found. We now believe this contact really does not exist
+ Log.e(TAG, "invalid contact uri: " + mLookupUri);
+ return Result.NOT_FOUND;
+ }
+ }
+
+ // These queries could be run in parallel (we did this until froyo). But unless
+ // we actually have two database connections there is no performance gain
+ loadSocial(resolver, result);
+ loadRawContacts(resolver, result);
+
+ return result;
+ }
+
+ /**
+ * Tries to lookup a contact using both Id and lookup key of the given Uri. Returns a
+ * valid Result instance if successful or {@link Result#NOT_FOUND} if empty
+ */
+ private Result loadContactHeaderData(final ContentResolver resolver,
+ final Uri lookupUri) {
+ if (resolver == null) throw new IllegalArgumentException("resolver must not be null");
+ if (lookupUri == null) {
+ // This can happen if the row was removed
+ return Result.NOT_FOUND;
+ }
+
+ final List<String> segments = lookupUri.getPathSegments();
+ if (segments.size() != 4) {
+ // Does not contain an Id. Return to caller so that a lookup is performed
+ Log.w(TAG, "Uri does not contain an Id, so we return to the caller who should " +
+ "perform a lookup to get a proper uri. Value: " + lookupUri);
+ return Result.NOT_FOUND;
+ }
+
+ final long uriContactId = Long.parseLong(segments.get(3));
+ final String uriLookupKey = Uri.encode(segments.get(2));
+ final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, uriContactId);
+ final Uri dataUri = Uri.withAppendedPath(contactUri, Contacts.Data.CONTENT_DIRECTORY);
+
+ final Cursor cursor = resolver.query(dataUri,
+ new String[] {
+ Contacts.NAME_RAW_CONTACT_ID,
+ Contacts.DISPLAY_NAME_SOURCE,
+ Contacts.LOOKUP_KEY
+ }, null, null, null);
+ if (cursor == null) {
+ Log.e(TAG, "No cursor returned in trySetupContactHeader/query");
+ return null;
+ }
+ try {
+ if (!cursor.moveToFirst()) {
+ Log.w(TAG, "Cursor returned by trySetupContactHeader/query is empty. " +
+ "ContactId must have changed or item has been removed");
+ return Result.NOT_FOUND;
+ }
+ String lookupKey =
+ cursor.getString(cursor.getColumnIndex(Contacts.LOOKUP_KEY));
+ if (!lookupKey.equals(uriLookupKey)) {
+ // ID and lookup key do not match
+ Log.w(TAG, "Contact with Id=" + uriContactId + " has a wrong lookupKey ("
+ + lookupKey + " instead of the expected " + uriLookupKey + ")");
+ return Result.NOT_FOUND;
+ }
+
+ long nameRawContactId = cursor.getLong(cursor.getColumnIndex(
+ Contacts.NAME_RAW_CONTACT_ID));
+ int displayNameSource = cursor.getInt(cursor.getColumnIndex(
+ Contacts.DISPLAY_NAME_SOURCE));
+
+ return new Result(lookupUri, lookupKey, contactUri, uriContactId, nameRawContactId,
+ displayNameSource);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Loads the social rows into the result structure. Expects the statuses in the
+ * result structure to be empty
+ */
+ private void loadSocial(final ContentResolver resolver, final Result result) {
+ if (result == null) throw new IllegalArgumentException("result must not be null");
+ if (resolver == null) throw new IllegalArgumentException("resolver must not be null");
+ if (result == Result.NOT_FOUND) {
+ throw new IllegalArgumentException("result must not be NOT_FOUND");
+ }
+
+ final Uri dataUri = Uri.withAppendedPath(result.getUri(),
+ Contacts.Data.CONTENT_DIRECTORY);
+ final Cursor cursor = resolver.query(dataUri, StatusQuery.PROJECTION,
+ StatusUpdates.PRESENCE + " IS NOT NULL OR " + StatusUpdates.STATUS +
+ " IS NOT NULL", null, null);
+
+ if (cursor == null) {
+ Log.e(TAG, "Social cursor is null but it shouldn't be");
+ return;
+ }
+
+ try {
+ HashMap<Long, DataStatus> statuses = result.getStatuses();
+
+ // Walk found statuses, creating internal row for each
+ while (cursor.moveToNext()) {
+ final DataStatus status = new DataStatus(cursor);
+ final long dataId = cursor.getLong(StatusQuery._ID);
+ statuses.put(dataId, status);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Loads the raw row contact rows into the result structure. Expects the entities in the
+ * result structure to be empty
+ */
+ private void loadRawContacts(final ContentResolver resolver, final Result result) {
+ if (result == null) throw new IllegalArgumentException("result must not be null");
+ if (resolver == null) throw new IllegalArgumentException("resolver must not be null");
+ if (result == Result.NOT_FOUND) {
+ throw new IllegalArgumentException("result must not be NOT_FOUND");
+ }
+
+ // Read the constituent raw contacts
+ final Cursor cursor = resolver.query(RawContactsEntity.CONTENT_URI, null,
+ RawContacts.CONTACT_ID + "=?", new String[] {
+ String.valueOf(result.mId)
+ }, null);
+ if (cursor == null) {
+ Log.e(TAG, "Raw contacts cursor is null but it shouldn't be");
+ return;
+ }
+
+ try {
+ ArrayList<Entity> entities = result.getEntities();
+ entities.ensureCapacity(cursor.getCount());
+ EntityIterator iterator = RawContacts.newEntityIterator(cursor);
+ try {
+ while (iterator.hasNext()) {
+ Entity entity = iterator.next();
+ entities.add(entity);
+ }
+ } finally {
+ iterator.close();
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Result result) {
+ // The creator isn't interested in any furether updates
+ if (mDestroyed) {
+ return;
+ }
+
+ mContact = result;
+ if (result != null) {
+ if (mObserver == null) {
+ mObserver = new ForceLoadContentObserver();
+ }
+ Log.i(TAG, "Registering content observer for " + mLookupUri);
+ mContext.getContentResolver().registerContentObserver(mLookupUri, true, mObserver);
+ mCallbacks.onContactLoaded(result);
+ }
+ }
+ }
+
+ public ContactLoader(Context context, Uri lookupUri) {
+ mContext = context.getApplicationContext();
+ mLookupUri = lookupUri;
+ }
+
+ @Override
+ public void startLoading() {
+ if (mContact != null) {
+ mCallbacks.onContactLoaded(mContact);
+ } else {
+ forceLoad();
+ }
+ }
+
+ @Override
+ public void forceLoad() {
+ new LoadContactTask().execute((Void[])null);
+ }
+
+ @Override
+ public void stopLoading() {
+ mContact = null;
+ if (mObserver != null) {
+ mContext.getContentResolver().unregisterContentObserver(mObserver);
+ }
+ }
+
+ @Override
+ public void destroy() {
+ mContact = null;
+ mDestroyed = true;
+ }
+}