summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKatherine Kuan <katherinekuan@google.com>2011-07-09 17:36:26 -0700
committerKatherine Kuan <katherinekuan@google.com>2011-08-26 20:04:00 -0700
commit37bddc2fa266fa0378fcd85ac5047b1fb0af2dc0 (patch)
tree68f35cd5f7730e10aa17fea4d3be6169fd98569f
parent3fe0cee4e2106dafd601bbd670d44cf3cb4fee60 (diff)
downloadpackages_apps_Contacts-37bddc2fa266fa0378fcd85ac5047b1fb0af2dc0.tar.gz
packages_apps_Contacts-37bddc2fa266fa0378fcd85ac5047b1fb0af2dc0.tar.bz2
packages_apps_Contacts-37bddc2fa266fa0378fcd85ac5047b1fb0af2dc0.zip
Add contact detail to existing contact
- Works for phone numbers and email addresses, otherwise route intent to full contact editor - Contact card icon / "OK" button first saves the change and then goes to the contact card - Port over from GB branch, updated references to non-existent classes, added new layout to look more like QuickContacts/contact tile, applied to ContactSelectionActivity instead of contacts list, added support for non-editable contacts, fix layout for phone landscape and tablet, inflate the right editor layout based on the DataKind instead of emedding a TextFieldEditorView directly inside the XML, use AsyncTask instead of WeakAsyncTask - Fixed NonPhoneActivity intent flags so that the activity results will work correctly (will allow contact card to be launched if user requested to do so and the cancel button in the ConfirmAddDetailActivity returns the user to the picker) - Fixed activity theme for NonPhoneActivity to not show a title bar - For a contact made up of multiple raw contacts, find the first editable contact to add it to (instead of always picking the first one which could be non-editable). Bug: 4295003 Change-Id: I111eaf6bbc78861c2b6a27c93086d00697869ebb
-rw-r--r--AndroidManifest.xml8
-rw-r--r--res/layout/confirm_add_detail_activity.xml153
-rw-r--r--res/values-sw580dp/styles.xml10
-rw-r--r--res/values-w470dp/styles.xml27
-rw-r--r--res/values/styles.xml18
-rw-r--r--src/com/android/contacts/activities/ConfirmAddDetailActivity.java786
-rw-r--r--src/com/android/contacts/activities/ContactSelectionActivity.java64
-rw-r--r--src/com/android/contacts/activities/NonPhoneActivity.java2
-rw-r--r--src/com/android/contacts/editor/LabeledEditorView.java8
9 files changed, 1068 insertions, 8 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index e6336bca3..40d5c2659 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -493,6 +493,14 @@
</intent-filter>
</activity>
+ <!-- Confirm that a single detail will be added to an existing contact -->
+ <activity
+ android:name=".activities.ConfirmAddDetailActivity"
+ android:label="@string/activity_title_confirm_add_detail"
+ android:theme="@style/ConfirmAddDetailDialogTheme"
+ android:windowSoftInputMode="adjustResize"
+ android:exported="false"/>
+
<!-- Create a new or edit an existing contact -->
<activity
android:name=".activities.ContactEditorActivity"
diff --git a/res/layout/confirm_add_detail_activity.xml b/res/layout/confirm_add_detail_activity.xml
new file mode 100644
index 000000000..6bbaac5ec
--- /dev/null
+++ b/res/layout/confirm_add_detail_activity.xml
@@ -0,0 +1,153 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<!-- Layout for confirming the addition of a piece of information to an existing contact. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:ex="http://schemas.android.com/apk/res/com.android.contacts"
+ android:id="@+id/root_view"
+ android:orientation="vertical"
+ android:visibility="invisible"
+ style="@style/ConfirmAddDetailViewStyle">
+
+ <!--
+ The header contains the contact photo, name, a link to the contact card, and
+ possibly an extra data field to disambiguate contacts with the same name.
+ -->
+ <RelativeLayout
+ style="@style/ConfirmAddDetailHeaderViewStyle">
+
+ <ImageView
+ android:id="@+id/photo"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="centerCrop"
+ android:src="@drawable/ic_contact_picture" />
+
+ <View
+ android:id="@+id/photo_text_bar"
+ android:layout_width="0dip"
+ android:layout_height="42dip"
+ android:layout_alignBottom="@id/photo"
+ android:layout_alignLeft="@id/photo"
+ android:layout_alignRight="@id/photo"
+ android:background="#7F000000" />
+
+ <ImageButton
+ android:id="@+id/open_details_button"
+ android:src="@drawable/ic_contacts_holo_dark"
+ android:background="?android:attr/selectableItemBackground"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_marginRight="16dip"
+ android:layout_marginBottom="5dip"
+ android:layout_alignBottom="@id/photo_text_bar"
+ android:layout_alignRight="@id/photo_text_bar" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="42dip"
+ android:orientation="vertical"
+ android:layout_alignBottom="@id/photo"
+ android:layout_alignLeft="@id/photo"
+ android:layout_toLeftOf="@id/open_details_button"
+ android:paddingRight="8dip"
+ android:paddingLeft="8dip">
+
+ <TextView
+ android:id="@+id/name"
+ android:layout_width="wrap_content"
+ android:layout_height="0dip"
+ android:layout_weight="1"
+ android:paddingLeft="8dip"
+ android:gravity="center_vertical"
+ android:textColor="@android:color/white"
+ android:textSize="16sp"
+ android:singleLine="true" />
+
+ <TextView
+ android:id="@+id/extra_info"
+ android:layout_width="wrap_content"
+ android:layout_height="0dip"
+ android:layout_weight="1"
+ android:paddingLeft="8dip"
+ android:gravity="center_vertical"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="@android:color/white"
+ android:singleLine="true"
+ android:paddingBottom="4dip"
+ android:visibility="gone" />
+
+ </LinearLayout>
+
+ <View
+ android:id="@+id/open_details_push_layer"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?android:attr/selectableItemBackground" />
+
+ </RelativeLayout>
+
+ <!-- Message that gets displayed if the contact is read-only (instead of showing the editor) -->
+ <TextView android:id="@+id/read_only_warning"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="60dip"
+ android:visibility="gone"
+ android:padding="15dip"
+ android:textAppearance="?android:attr/textAppearanceSmall"/>
+
+ <!-- Container for a single detail field editor when the contact is not read-only -->
+ <FrameLayout
+ android:id="@+id/editor_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="60dip"
+ android:layout_marginTop="4dip"
+ android:layout_marginRight="15dip"/>
+
+ <View
+ android:id="@+id/divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:background="?android:attr/listDivider"/>
+
+ <!-- Action buttons -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ style="?android:attr/buttonBarStyle">
+
+ <Button
+ android:id="@+id/btn_cancel"
+ style="?android:attr/buttonBarButtonStyle"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@android:string/cancel" />
+
+ <Button
+ android:id="@+id/btn_done"
+ style="?android:attr/buttonBarButtonStyle"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@android:string/ok" />
+
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/values-sw580dp/styles.xml b/res/values-sw580dp/styles.xml
index 5f18d2256..8e56bac6e 100644
--- a/res/values-sw580dp/styles.xml
+++ b/res/values-sw580dp/styles.xml
@@ -113,6 +113,16 @@
<item name="android:windowContentOverlay">@null</item>
</style>
+ <style name="ConfirmAddDetailViewStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ </style>
+
+ <style name="ConfirmAddDetailHeaderViewStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">200dip</item>
+ </style>
+
<style name="BackgroundOnly" parent="@android:Theme.Holo.Light">
<item name="android:windowBackground">@null</item>
<item name="android:windowContentOverlay">@null</item>
diff --git a/res/values-w470dp/styles.xml b/res/values-w470dp/styles.xml
new file mode 100644
index 000000000..dab78028a
--- /dev/null
+++ b/res/values-w470dp/styles.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+<resources>
+ <style name="ConfirmAddDetailViewStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">match_parent</item>
+ </style>
+
+ <style name="ConfirmAddDetailHeaderViewStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">0dip</item>
+ <item name="android:layout_weight">1</item>
+ </style>
+</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 90cb0714d..51b2c0c97 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -205,10 +205,14 @@
<item name="android:background">@android:color/transparent</item>
</style>
- <style name="NonPhoneActivityTheme" parent="@android:Theme.Translucent">
+ <style name="NonPhoneActivityTheme" parent="@android:Theme.Translucent.NoTitleBar">
</style>
- <style name="NonPhoneDialogTheme" parent="@android:Theme.Dialog">
+ <style name="NonPhoneDialogTheme" parent="@android:Theme.Holo.Light.Dialog">
+ </style>
+
+ <style name="ConfirmAddDetailDialogTheme" parent="@android:style/Theme.Holo.Light.Dialog.MinWidth">
+ <item name="android:windowCloseOnTouchOutside">true</item>
</style>
<style name="SectionDivider">
@@ -259,6 +263,16 @@
<item name="android:layout_weight">1</item>
</style>
+ <style name="ConfirmAddDetailViewStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ </style>
+
+ <style name="ConfirmAddDetailHeaderViewStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">150dip</item>
+ </style>
+
<style name="DialtactsActionBarStyle" parent="android:Widget.Holo.ActionBar">
<item name="android:backgroundSplit">@drawable/ab_bottom_opaque_dark_holo</item>
<item name="android:backgroundStacked">@drawable/ab_stacked_opaque_dark_holo</item>
diff --git a/src/com/android/contacts/activities/ConfirmAddDetailActivity.java b/src/com/android/contacts/activities/ConfirmAddDetailActivity.java
new file mode 100644
index 000000000..e97a718c0
--- /dev/null
+++ b/src/com/android/contacts/activities/ConfirmAddDetailActivity.java
@@ -0,0 +1,786 @@
+/*
+ * Copyright (C) 2011 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 com.android.contacts.activities;
+
+import com.android.contacts.R;
+import com.android.contacts.editor.Editor;
+import com.android.contacts.editor.ViewIdGenerator;
+import com.android.contacts.model.AccountType;
+import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.DataKind;
+import com.android.contacts.model.EntityDelta;
+import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.model.EntityDeltaList;
+import com.android.contacts.model.EntityModifier;
+import com.android.contacts.util.DialogManager;
+import com.android.contacts.util.EmptyService;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.AsyncQueryHandler;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * This is a dialog-themed activity for confirming the addition of a detail to an existing contact
+ * (once the user has selected this contact from a list of all contacts). The incoming intent
+ * must have an extra with max 1 phone or email specified, using
+ * {@link ContactsContract.Intents.Insert.PHONE} with type
+ * {@link ContactsContract.Intents.Insert.PHONE_TYPE} or
+ * {@link ContactsContract.Intents.Insert.EMAIL} with type
+ * {@link ContactsContract.Intents.Insert.EMAIL_TYPE} intent keys.
+ */
+public class ConfirmAddDetailActivity extends Activity implements
+ DialogManager.DialogShowingViewActivity {
+
+ private static final String TAG = ConfirmAddDetailActivity.class.getSimpleName();
+
+ private static final String LEGACY_CONTACTS_AUTHORITY = "contacts";
+
+ private LayoutInflater mInflater;
+ private View mRootView;
+ private TextView mDisplayNameView;
+ private TextView mReadOnlyWarningView;
+ private ImageView mPhotoView;
+ private ViewGroup mEditorContainerView;
+
+ private AccountTypeManager mAccountTypeManager;
+ private ContentResolver mContentResolver;
+
+ private AccountType mEditableAccountType;
+ private EntityDelta mState;
+ private Uri mContactUri;
+ private long mContactId;
+ private String mDisplayName;
+ private boolean mIsReadyOnly;
+
+ private QueryHandler mQueryHandler;
+ private EntityDeltaList mEntityDeltaList;
+
+ private String mMimetype = Phone.CONTENT_ITEM_TYPE;
+
+ /**
+ * DialogManager may be needed if the user wants to apply a "custom" label to the contact detail
+ */
+ private final DialogManager mDialogManager = new DialogManager(this);
+
+ /**
+ * PhotoQuery contains the projection used for retrieving the name and photo
+ * ID of a contact.
+ */
+ private interface ContactQuery {
+ final String[] COLUMNS = new String[] {
+ Contacts._ID,
+ Contacts.LOOKUP_KEY,
+ Contacts.PHOTO_ID,
+ Contacts.DISPLAY_NAME,
+ };
+ final int _ID = 0;
+ final int LOOKUP_KEY = 1;
+ final int PHOTO_ID = 2;
+ final int DISPLAY_NAME = 3;
+ }
+
+ /**
+ * PhotoQuery contains the projection used for retrieving the raw bytes of
+ * the contact photo.
+ */
+ private interface PhotoQuery {
+ final String[] COLUMNS = new String[] {
+ Photo.PHOTO
+ };
+
+ final int PHOTO = 0;
+ }
+
+ /**
+ * ExtraInfoQuery contains the projection used for retrieving the extra info
+ * on a contact (only needed if someone else exists with the same name as
+ * this contact).
+ */
+ private interface ExtraInfoQuery {
+ final String[] COLUMNS = new String[] {
+ RawContacts.CONTACT_ID,
+ Data.MIMETYPE,
+ Data.DATA1,
+ };
+ final int CONTACT_ID = 0;
+ final int MIMETYPE = 1;
+ final int DATA1 = 2;
+ }
+
+ /**
+ * List of mimetypes to use in order of priority to display for a contact in
+ * a disambiguation case. For example, if the contact does not have a
+ * nickname, use the email field, and etc.
+ */
+ private static final String[] sMimeTypePriorityList = new String[] { Nickname.CONTENT_ITEM_TYPE,
+ Email.CONTENT_ITEM_TYPE, Im.CONTENT_ITEM_TYPE, StructuredPostal.CONTENT_ITEM_TYPE,
+ Phone.CONTENT_ITEM_TYPE };
+
+ private static final int TOKEN_CONTACT_INFO = 0;
+ private static final int TOKEN_PHOTO_QUERY = 1;
+ private static final int TOKEN_DISAMBIGUATION_QUERY = 2;
+ private static final int TOKEN_EXTRA_INFO_QUERY = 3;
+
+ private final OnClickListener mDetailsButtonClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mIsReadyOnly) {
+ onSaveCompleted(true);
+ } else {
+ doSaveAction();
+ }
+ }
+ };
+
+ private final OnClickListener mDoneButtonClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ doSaveAction();
+ }
+ };
+
+ private final OnClickListener mCancelButtonClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mContentResolver = getContentResolver();
+
+ final Intent intent = getIntent();
+ mContactUri = intent.getData();
+
+ if (mContactUri == null) {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+
+ Bundle extras = intent.getExtras();
+ if (extras != null) {
+ if (extras.containsKey(ContactsContract.Intents.Insert.PHONE)) {
+ mMimetype = Phone.CONTENT_ITEM_TYPE;
+ } else if (extras.containsKey(ContactsContract.Intents.Insert.EMAIL)) {
+ mMimetype = Email.CONTENT_ITEM_TYPE;
+ } else {
+ throw new IllegalStateException("Error: No valid mimetype found in intent extras");
+ }
+ }
+
+ mAccountTypeManager = AccountTypeManager.getInstance(this);
+
+ setContentView(R.layout.confirm_add_detail_activity);
+
+ mRootView = findViewById(R.id.root_view);
+ mReadOnlyWarningView = (TextView) findViewById(R.id.read_only_warning);
+
+ // Setup "header" (containing contact info) to save the detail and then go to the editor
+ findViewById(R.id.open_details_push_layer).setOnClickListener(mDetailsButtonClickListener);
+
+ // Setup "done" button to save the detail to the contact and exit.
+ findViewById(R.id.btn_done).setOnClickListener(mDoneButtonClickListener);
+
+ // Setup "cancel" button to return to previous activity.
+ findViewById(R.id.btn_cancel).setOnClickListener(mCancelButtonClickListener);
+
+ // Retrieve references to all the Views in the dialog activity.
+ mDisplayNameView = (TextView) findViewById(R.id.name);
+ mPhotoView = (ImageView) findViewById(R.id.photo);
+ mEditorContainerView = (ViewGroup) findViewById(R.id.editor_container);
+
+ startContactQuery(mContactUri, true);
+
+ new QueryEntitiesTask(this).execute(intent);
+ }
+
+ @Override
+ public DialogManager getDialogManager() {
+ return mDialogManager;
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id, Bundle args) {
+ if (DialogManager.isManagedId(id)) return mDialogManager.onCreateDialog(id, args);
+
+ // Nobody knows about the Dialog
+ Log.w(TAG, "Unknown dialog requested, id: " + id + ", args: " + args);
+ return null;
+ }
+
+ /**
+ * Reset the query handler by creating a new QueryHandler instance.
+ */
+ private void resetAsyncQueryHandler() {
+ // the api AsyncQueryHandler.cancelOperation() doesn't really work. Since we really
+ // need the old async queries to be cancelled, let's do it the hard way.
+ mQueryHandler = new QueryHandler(mContentResolver);
+ }
+
+ /**
+ * Internal method to query contact by Uri.
+ *
+ * @param contactUri the contact uri
+ * @param resetQueryHandler whether to use a new AsyncQueryHandler or not
+ */
+ private void startContactQuery(Uri contactUri, boolean resetQueryHandler) {
+ if (resetQueryHandler) {
+ resetAsyncQueryHandler();
+ }
+
+ mQueryHandler.startQuery(TOKEN_CONTACT_INFO, contactUri, contactUri, ContactQuery.COLUMNS,
+ null, null, null);
+ }
+
+ /**
+ * Internal method to query contact photo by photo id and uri.
+ *
+ * @param photoId the photo id.
+ * @param lookupKey the lookup uri.
+ * @param resetQueryHandler whether to use a new AsyncQueryHandler or not.
+ */
+ private void startPhotoQuery(long photoId, Uri lookupKey, boolean resetQueryHandler) {
+ if (resetQueryHandler) {
+ resetAsyncQueryHandler();
+ }
+
+ mQueryHandler.startQuery(TOKEN_PHOTO_QUERY, lookupKey,
+ ContentUris.withAppendedId(Data.CONTENT_URI, photoId),
+ PhotoQuery.COLUMNS, null, null, null);
+ }
+
+ /**
+ * Internal method to query for contacts with a given display name.
+ *
+ * @param contactDisplayName the display name to look for.
+ */
+ private void startDisambiguationQuery(String contactDisplayName) {
+ // Apply a limit of 1 result to the query because we only need to
+ // determine whether or not at least one other contact has the same
+ // name. We don't need to find ALL other contacts with the same name.
+ Builder builder = Contacts.CONTENT_URI.buildUpon();
+ builder.appendQueryParameter("limit", String.valueOf(1));
+ Uri uri = builder.build();
+
+ mQueryHandler.startQuery(TOKEN_DISAMBIGUATION_QUERY, null, uri,
+ new String[] { Contacts._ID } /* unused projection but a valid one was needed */,
+ Contacts.DISPLAY_NAME_PRIMARY + " = ? and " + Contacts.PHOTO_ID + " is null and "
+ + Contacts._ID + " <> ?",
+ new String[] { contactDisplayName, String.valueOf(mContactId) }, null);
+ }
+
+ /**
+ * Internal method to query for extra data fields for this contact.
+ */
+ private void startExtraInfoQuery() {
+ mQueryHandler.startQuery(TOKEN_EXTRA_INFO_QUERY, null, Data.CONTENT_URI,
+ ExtraInfoQuery.COLUMNS, RawContacts.CONTACT_ID + " = ?",
+ new String[] { String.valueOf(mContactId) }, null);
+ }
+
+ private static class QueryEntitiesTask extends AsyncTask<Intent, Void, EntityDeltaList> {
+
+ private ConfirmAddDetailActivity activityTarget;
+ private String mSelection;
+
+ public QueryEntitiesTask(ConfirmAddDetailActivity target) {
+ activityTarget = target;
+ }
+
+ @Override
+ protected EntityDeltaList doInBackground(Intent... params) {
+
+ final Intent intent = params[0];
+
+ final ContentResolver resolver = activityTarget.getContentResolver();
+
+ // Handle both legacy and new authorities
+ final Uri data = intent.getData();
+ final String authority = data.getAuthority();
+ final String mimeType = intent.resolveType(resolver);
+
+ mSelection = "0";
+ String selectionArg = null;
+ if (ContactsContract.AUTHORITY.equals(authority)) {
+ if (Contacts.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ // Handle selected aggregate
+ final long contactId = ContentUris.parseId(data);
+ selectionArg = String.valueOf(contactId);
+ mSelection = RawContacts.CONTACT_ID + "=?";
+ } else if (RawContacts.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ final long rawContactId = ContentUris.parseId(data);
+ final long contactId = queryForContactId(resolver, rawContactId);
+ selectionArg = String.valueOf(contactId);
+ mSelection = RawContacts.CONTACT_ID + "=?";
+ }
+ } else if (android.provider.Contacts.AUTHORITY.equals(authority)) {
+ final long rawContactId = ContentUris.parseId(data);
+ selectionArg = String.valueOf(rawContactId);
+ mSelection = Data.RAW_CONTACT_ID + "=?";
+ }
+
+ return EntityDeltaList.fromQuery(activityTarget.getContentResolver(), mSelection,
+ new String[] { selectionArg }, null);
+ }
+
+ private static long queryForContactId(ContentResolver resolver, long rawContactId) {
+ Cursor contactIdCursor = null;
+ long contactId = -1;
+ try {
+ contactIdCursor = resolver.query(RawContacts.CONTENT_URI,
+ new String[] { RawContacts.CONTACT_ID },
+ RawContacts._ID + "=?", new String[] { String.valueOf(rawContactId) },
+ null);
+ if (contactIdCursor != null && contactIdCursor.moveToFirst()) {
+ contactId = contactIdCursor.getLong(0);
+ }
+ } finally {
+ if (contactIdCursor != null) {
+ contactIdCursor.close();
+ }
+ }
+ return contactId;
+ }
+
+ @Override
+ protected void onPostExecute(EntityDeltaList entityList) {
+ if (activityTarget.isFinishing()) {
+ return;
+ }
+ activityTarget.setEntityDeltaList(entityList);
+ activityTarget.findEditableRawContact();
+ activityTarget.parseExtras();
+ activityTarget.bindEditor();
+ }
+ }
+
+ private class QueryHandler extends AsyncQueryHandler {
+
+ public QueryHandler(ContentResolver cr) {
+ super(cr);
+ }
+
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ try {
+ if (this != mQueryHandler) {
+ Log.d(TAG, "onQueryComplete: discard result, the query handler is reset!");
+ return;
+ }
+ if (ConfirmAddDetailActivity.this.isFinishing()) {
+ return;
+ }
+
+ switch (token) {
+ case TOKEN_PHOTO_QUERY: {
+ // Set the photo
+ Bitmap photoBitmap = null;
+ if (cursor != null && cursor.moveToFirst()
+ && !cursor.isNull(PhotoQuery.PHOTO)) {
+ byte[] photoData = cursor.getBlob(PhotoQuery.PHOTO);
+ photoBitmap = BitmapFactory.decodeByteArray(photoData, 0,
+ photoData.length, null);
+ }
+
+ if (photoBitmap != null) {
+ mPhotoView.setImageBitmap(photoBitmap);
+ }
+
+ break;
+ }
+ case TOKEN_CONTACT_INFO: {
+ // Set the contact's name
+ if (cursor != null && cursor.moveToFirst()) {
+ // Get the cursor values
+ mDisplayName = cursor.getString(ContactQuery.DISPLAY_NAME);
+ final long photoId = cursor.getLong(ContactQuery.PHOTO_ID);
+
+ // If there is no photo ID, then do a disambiguation
+ // query because other contacts could have the same
+ // name as this contact.
+ if (photoId == 0) {
+ mContactId = cursor.getLong(ContactQuery._ID);
+ startDisambiguationQuery(mDisplayName);
+ } else {
+ // Otherwise do the photo query.
+ Uri lookupUri = Contacts.getLookupUri(mContactId,
+ cursor.getString(ContactQuery.LOOKUP_KEY));
+ startPhotoQuery(photoId, lookupUri,
+ false /* don't reset query handler */);
+ // Display the name because there is no
+ // disambiguation query.
+ setDisplayName();
+ onLoadDataFinished();
+ }
+ }
+ break;
+ }
+ case TOKEN_DISAMBIGUATION_QUERY: {
+ // If a cursor was returned with more than 0 results,
+ // then at least one other contact exists with the same
+ // name as this contact. Extra info on this contact must
+ // be displayed to disambiguate the contact, so retrieve
+ // those additional fields. Otherwise, no other contacts
+ // with this name exists, so do nothing further.
+ if (cursor != null && cursor.getCount() > 0) {
+ startExtraInfoQuery();
+ } else {
+ // If there are no other contacts with this name,
+ // then display the name.
+ setDisplayName();
+ onLoadDataFinished();
+ }
+ break;
+ }
+ case TOKEN_EXTRA_INFO_QUERY: {
+ // This case should only occur if there are one or more
+ // other contacts with the same contact name.
+ if (cursor != null && cursor.moveToFirst()) {
+ HashMap<String, String> hashMapCursorData = new
+ HashMap<String, String>();
+
+ // Convert the cursor data into a hashmap of
+ // (mimetype, data value) pairs. If a contact has
+ // multiple values with the same mimetype, it's fine
+ // to override that hashmap entry because we only
+ // need one value of that type.
+ while (!cursor.isAfterLast()) {
+ final String mimeType = cursor.getString(ExtraInfoQuery.MIMETYPE);
+ if (!TextUtils.isEmpty(mimeType)) {
+ String value = cursor.getString(ExtraInfoQuery.DATA1);
+ if (!TextUtils.isEmpty(value)) {
+ // As a special case, phone numbers
+ // should be formatted in a specific way.
+ if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ value = PhoneNumberUtils.formatNumber(value);
+ }
+ hashMapCursorData.put(mimeType, value);
+ }
+ }
+ cursor.moveToNext();
+ }
+
+ // Find the first non-empty field according to the
+ // mimetype priority list and display this under the
+ // contact's display name to disambiguate the contact.
+ for (String mimeType : sMimeTypePriorityList) {
+ if (hashMapCursorData.containsKey(mimeType)) {
+ setDisplayName();
+ setExtraInfoField(hashMapCursorData.get(mimeType));
+ break;
+ }
+ }
+ onLoadDataFinished();
+ }
+ break;
+ }
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+ }
+
+ public void setEntityDeltaList(EntityDeltaList entityList) {
+ mEntityDeltaList = entityList;
+ }
+
+ public void findEditableRawContact() {
+ if (mEntityDeltaList == null) {
+ return;
+ }
+ for (EntityDelta state : mEntityDeltaList) {
+ final String accountType = state.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+ final String dataSet = state.getValues().getAsString(RawContacts.DATA_SET);
+ final AccountType type = mAccountTypeManager.getAccountType(accountType, dataSet);
+
+ // Raw contacts that are not from external sources should be editable.
+ if (!type.isExternal()) {
+ mEditableAccountType = type;
+ mState = state;
+ return;
+ }
+ }
+ }
+
+ public void parseExtras() {
+ if (mEditableAccountType == null || mState == null) {
+ return;
+ }
+ // Handle any incoming values that should be inserted
+ final Bundle extras = getIntent().getExtras();
+ if (extras != null && extras.size() > 0) {
+ // If there are any intent extras, add them as additional fields in the EntityDelta.
+ EntityModifier.parseExtras(this, mEditableAccountType, mState, extras);
+ }
+ }
+
+ /**
+ * Rebuild the editor to match our underlying {@link #mEntityDeltaList} object.
+ */
+ private void bindEditor() {
+ if (mEntityDeltaList == null) {
+ return;
+ }
+
+ // If no valid raw contact (to insert the data) was found, we won't have an editable
+ // account type to use. In this case, display an error message and hide the "OK" button.
+ if (mEditableAccountType == null) {
+ mIsReadyOnly = true;
+ mReadOnlyWarningView.setText(getString(R.string.contact_read_only));
+ mReadOnlyWarningView.setVisibility(View.VISIBLE);
+ mEditorContainerView.setVisibility(View.GONE);
+ findViewById(R.id.btn_done).setVisibility(View.GONE);
+ // Nothing more to be done, just show the UI
+ onLoadDataFinished();
+ return;
+ }
+
+ // Otherwise display an editor that allows the user to add the data to this raw contact.
+ for (DataKind kind : mEditableAccountType.getSortedDataKinds()) {
+ // Skip kind that are not editable
+ if (!kind.editable) continue;
+ if (mMimetype.equals(kind.mimeType)) {
+ for (ValuesDelta valuesDelta : mState.getMimeEntries(mMimetype)) {
+ // Skip entries that aren't visible
+ if (!valuesDelta.isVisible()) continue;
+ if (valuesDelta.isInsert()) {
+ inflateEditorView(kind, valuesDelta, mState);
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates an EditorView for the given entry. This function must be used while constructing
+ * the views corresponding to the the object-model. The resulting EditorView is also added
+ * to the end of mEditors
+ */
+ private void inflateEditorView(DataKind dataKind, ValuesDelta valuesDelta, EntityDelta state) {
+ final View view = mInflater.inflate(dataKind.editorLayoutResourceId, mEditorContainerView,
+ false);
+
+ if (view instanceof Editor) {
+ Editor editor = (Editor) view;
+ // Don't allow deletion of the field because there is only 1 detail in this editor.
+ editor.setDeletable(false);
+ editor.setValues(dataKind, valuesDelta, state, false, new ViewIdGenerator());
+ }
+
+ mEditorContainerView.addView(view);
+ }
+
+ /**
+ * Set the display name to the correct TextView. Don't do this until it is
+ * certain there is no need for a disambiguation field (otherwise the screen
+ * will flicker because the name will be centered and then moved upwards).
+ */
+ private void setDisplayName() {
+ mDisplayNameView.setText(mDisplayName);
+ }
+
+ /**
+ * Set the TextView (for extra contact info) with the given value and make the
+ * TextView visible.
+ */
+ private void setExtraInfoField(String value) {
+ TextView extraTextView = (TextView) findViewById(R.id.extra_info);
+ extraTextView.setVisibility(View.VISIBLE);
+ extraTextView.setText(value);
+ }
+
+ /**
+ * Shows all the contents of the dialog to the user at one time. This should only be called
+ * once all the queries have completed, otherwise the screen will flash as additional data
+ * comes in.
+ */
+ private void onLoadDataFinished() {
+ mRootView.setVisibility(View.VISIBLE);
+ }
+
+ /**
+ * Saves or creates the contact based on the mode, and if successful
+ * finishes the activity.
+ */
+ private void doSaveAction() {
+ final PersistTask task = new PersistTask(this, mAccountTypeManager);
+ task.execute(mEntityDeltaList);
+ }
+
+
+ /**
+ * Background task for persisting edited contact data, using the changes
+ * defined by a set of {@link EntityDelta}. This task starts
+ * {@link EmptyService} to make sure the background thread can finish
+ * persisting in cases where the system wants to reclaim our process.
+ */
+ public static class PersistTask extends AsyncTask<EntityDeltaList, Void, Integer> {
+ // In the future, use ContactSaver instead of WeakAsyncTask because of
+ // the danger of the activity being null during a save action
+ private static final int PERSIST_TRIES = 3;
+
+ private static final int RESULT_UNCHANGED = 0;
+ private static final int RESULT_SUCCESS = 1;
+ private static final int RESULT_FAILURE = 2;
+
+ private ConfirmAddDetailActivity activityTarget;
+ private WeakReference<ProgressDialog> mProgress;
+
+ private AccountTypeManager mAccountTypeManager;
+
+ public PersistTask(ConfirmAddDetailActivity target, AccountTypeManager accountTypeManager) {
+ activityTarget = target;
+ mAccountTypeManager = accountTypeManager;
+ }
+
+ @Override
+ protected void onPreExecute() {
+ mProgress = new WeakReference<ProgressDialog>(ProgressDialog.show(activityTarget, null,
+ activityTarget.getText(R.string.savingContact)));
+
+ // Before starting this task, start an empty service to protect our
+ // process from being reclaimed by the system.
+ final Context context = activityTarget;
+ context.startService(new Intent(context, EmptyService.class));
+ }
+
+ @Override
+ protected Integer doInBackground(EntityDeltaList... params) {
+ final Context context = activityTarget;
+ final ContentResolver resolver = context.getContentResolver();
+
+ EntityDeltaList state = params[0];
+
+ // Trim any empty fields, and RawContacts, before persisting
+ EntityModifier.trimEmpty(state, mAccountTypeManager);
+
+ // Attempt to persist changes
+ int tries = 0;
+ Integer result = RESULT_FAILURE;
+ while (tries++ < PERSIST_TRIES) {
+ try {
+ // Build operations and try applying
+ final ArrayList<ContentProviderOperation> diff = state.buildDiff();
+ ContentProviderResult[] results = null;
+ if (!diff.isEmpty()) {
+ results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
+ }
+
+ result = (diff.size() > 0) ? RESULT_SUCCESS : RESULT_UNCHANGED;
+ break;
+
+ } catch (RemoteException e) {
+ // Something went wrong, bail without success
+ Log.e(TAG, "Problem persisting user edits", e);
+ break;
+
+ } catch (OperationApplicationException e) {
+ // Version consistency failed, bail without success
+ Log.e(TAG, "Version consistency failed", e);
+ break;
+ }
+ }
+
+ return result;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void onPostExecute(Integer result) {
+ final Context context = activityTarget;
+
+ // Dismiss the progress dialog
+ mProgress.get().dismiss();
+
+ // Show a toast message based on the success or failure of the save action.
+ if (result == RESULT_SUCCESS) {
+ Toast.makeText(context, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
+ } else if (result == RESULT_FAILURE) {
+ Toast.makeText(context, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
+ }
+
+ // Stop the service that was protecting us
+ context.stopService(new Intent(context, EmptyService.class));
+ activityTarget.onSaveCompleted(result != RESULT_FAILURE);
+ }
+ }
+
+ /**
+ * This method is intended to be executed after the background task for saving edited info has
+ * finished. The method sets the activity result (and intent if applicable) and finishes the
+ * activity.
+ * @param success is true if the save task completed successfully, or false otherwise.
+ */
+ private void onSaveCompleted(boolean success) {
+ if (success) {
+ Intent intent = new Intent(Intent.ACTION_VIEW, mContactUri);
+ setResult(RESULT_OK, intent);
+ } else {
+ setResult(RESULT_CANCELED);
+ }
+ finish();
+ }
+} \ No newline at end of file
diff --git a/src/com/android/contacts/activities/ContactSelectionActivity.java b/src/com/android/contacts/activities/ContactSelectionActivity.java
index fbb9b662d..0ab5881a9 100644
--- a/src/com/android/contacts/activities/ContactSelectionActivity.java
+++ b/src/com/android/contacts/activities/ContactSelectionActivity.java
@@ -32,11 +32,13 @@ import com.android.contacts.list.PhoneNumberPickerFragment;
import com.android.contacts.list.PostalAddressPickerFragment;
import com.android.contacts.widget.ContextMenuAdapter;
+import android.app.Activity;
import android.app.Fragment;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Intents.Insert;
import android.text.TextUtils;
import android.view.MenuItem;
import android.view.View;
@@ -45,6 +47,8 @@ import android.widget.Button;
import android.widget.SearchView;
import android.widget.SearchView.OnQueryTextListener;
+import java.util.Set;
+
/**
* Displays a list of contacts (or phone numbers or postal addresses) for the
* purposes of selecting one.
@@ -53,6 +57,8 @@ public class ContactSelectionActivity extends ContactsActivity
implements View.OnCreateContextMenuListener, OnQueryTextListener, OnClickListener {
private static final String TAG = "ContactSelectionActivity";
+ private static final int SUBACTIVITY_ADD_TO_EXISTING_CONTACT = 0;
+
private static final String KEY_ACTION_CODE = "actionCode";
private static final int DEFAULT_DIRECTORY_RESULT_LIMIT = 20;
@@ -314,8 +320,22 @@ public class ContactSelectionActivity extends ContactsActivity
@Override
public void onEditContactAction(Uri contactLookupUri) {
- Intent intent = new Intent(Intent.ACTION_EDIT, contactLookupUri);
- startActivityAndForwardResult(intent);
+ Bundle extras = getIntent().getExtras();
+ if (launchAddToContactDialog(extras)) {
+ // Show a confirmation dialog to add the value(s) to the existing contact.
+ Intent intent = new Intent(ContactSelectionActivity.this,
+ ConfirmAddDetailActivity.class);
+ intent.setData(contactLookupUri);
+ if (extras != null) {
+ intent.putExtras(extras);
+ }
+ // Wait for the activity result because we want to keep the picker open (in case the
+ // user cancels adding the info to a contact and wants to pick someone else).
+ startActivityForResult(intent, SUBACTIVITY_ADD_TO_EXISTING_CONTACT);
+ } else {
+ // Otherwise launch the full contact editor.
+ startActivityAndForwardResult(new Intent(Intent.ACTION_EDIT, contactLookupUri));
+ }
}
@Override
@@ -327,6 +347,33 @@ public class ContactSelectionActivity extends ContactsActivity
public void onShortcutIntentCreated(Intent intent) {
returnPickerResult(intent);
}
+
+ /**
+ * Returns true if is a single email or single phone number provided in the {@link Intent}
+ * extras bundle so that a pop-up confirmation dialog can be used to add the data to
+ * a contact. Otherwise return false if there are other intent extras that require launching
+ * the full contact editor.
+ */
+ private boolean launchAddToContactDialog(Bundle extras) {
+ if (extras == null) {
+ return false;
+ }
+ Set<String> intentExtraKeys = extras.keySet();
+ int numIntentExtraKeys = intentExtraKeys.size();
+ if (numIntentExtraKeys == 2) {
+ boolean hasPhone = intentExtraKeys.contains(Insert.PHONE) &&
+ intentExtraKeys.contains(Insert.PHONE_TYPE);
+ boolean hasEmail = intentExtraKeys.contains(Insert.EMAIL) &&
+ intentExtraKeys.contains(Insert.EMAIL_TYPE);
+ return hasPhone || hasEmail;
+ } else if (numIntentExtraKeys == 1) {
+ return intentExtraKeys.contains(Insert.PHONE) ||
+ intentExtraKeys.contains(Insert.EMAIL);
+ }
+ // Having 0 or more than 2 intent extra keys means that we should launch
+ // the full contact editor to properly handle the intent extras.
+ return false;
+ }
}
private final class PhoneNumberPickerActionListener implements
@@ -415,4 +462,17 @@ public class ContactSelectionActivity extends ContactsActivity
finish();
}
}
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == SUBACTIVITY_ADD_TO_EXISTING_CONTACT) {
+ if (resultCode == Activity.RESULT_OK) {
+ if (data != null) {
+ startActivity(data);
+ }
+ finish();
+ }
+ }
+ }
}
diff --git a/src/com/android/contacts/activities/NonPhoneActivity.java b/src/com/android/contacts/activities/NonPhoneActivity.java
index 922be4763..3a5429224 100644
--- a/src/com/android/contacts/activities/NonPhoneActivity.java
+++ b/src/com/android/contacts/activities/NonPhoneActivity.java
@@ -82,8 +82,6 @@ public class NonPhoneActivity extends ContactsActivity {
final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
intent.setType(Contacts.CONTENT_ITEM_TYPE);
intent.putExtra(Insert.PHONE, getArgumentPhoneNumber());
- intent.setFlags(
- Intent.FLAG_ACTIVITY_FORWARD_RESULT | Intent.FLAG_ACTIVITY_NO_HISTORY);
startActivity(intent);
}
dismiss();
diff --git a/src/com/android/contacts/editor/LabeledEditorView.java b/src/com/android/contacts/editor/LabeledEditorView.java
index 6452c802a..1bea06070 100644
--- a/src/com/android/contacts/editor/LabeledEditorView.java
+++ b/src/com/android/contacts/editor/LabeledEditorView.java
@@ -293,10 +293,14 @@ public abstract class LabeledEditorView extends LinearLayout implements Editor,
boolean isEmpty = isEmpty();
if (mWasEmpty != isEmpty) {
if (isEmpty) {
- mListener.onRequest(EditorListener.FIELD_TURNED_EMPTY);
+ if (mListener != null) {
+ mListener.onRequest(EditorListener.FIELD_TURNED_EMPTY);
+ }
if (mIsDeletable) mDeleteContainer.setVisibility(View.INVISIBLE);
} else {
- mListener.onRequest(EditorListener.FIELD_TURNED_NON_EMPTY);
+ if (mListener != null) {
+ mListener.onRequest(EditorListener.FIELD_TURNED_NON_EMPTY);
+ }
if (mIsDeletable) mDeleteContainer.setVisibility(View.VISIBLE);
}
mWasEmpty = isEmpty;