• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.contacts.activities;
18 
19 import android.app.Activity;
20 import android.app.Dialog;
21 import android.app.ProgressDialog;
22 import android.content.AsyncQueryHandler;
23 import android.content.ContentProviderOperation;
24 import android.content.ContentProviderResult;
25 import android.content.ContentResolver;
26 import android.content.ContentUris;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.OperationApplicationException;
30 import android.database.Cursor;
31 import android.graphics.Bitmap;
32 import android.graphics.BitmapFactory;
33 import android.net.Uri;
34 import android.net.Uri.Builder;
35 import android.os.AsyncTask;
36 import android.os.Bundle;
37 import android.os.RemoteException;
38 import android.provider.ContactsContract;
39 import android.provider.ContactsContract.CommonDataKinds.Email;
40 import android.provider.ContactsContract.CommonDataKinds.Im;
41 import android.provider.ContactsContract.CommonDataKinds.Nickname;
42 import android.provider.ContactsContract.CommonDataKinds.Phone;
43 import android.provider.ContactsContract.CommonDataKinds.Photo;
44 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
45 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
46 import android.provider.ContactsContract.Contacts;
47 import android.provider.ContactsContract.Data;
48 import android.provider.ContactsContract.RawContacts;
49 import android.provider.ContactsContract.RawContactsEntity;
50 import android.telephony.PhoneNumberUtils;
51 import android.text.TextUtils;
52 import android.util.Log;
53 import android.view.LayoutInflater;
54 import android.view.View;
55 import android.view.View.OnClickListener;
56 import android.view.ViewGroup;
57 import android.widget.ImageView;
58 import android.widget.TextView;
59 import android.widget.Toast;
60 
61 import com.android.contacts.R;
62 import com.android.contacts.editor.Editor;
63 import com.android.contacts.editor.EditorUiUtils;
64 import com.android.contacts.editor.ViewIdGenerator;
65 import com.android.contacts.common.ContactPhotoManager;
66 import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
67 import com.android.contacts.common.model.AccountTypeManager;
68 import com.android.contacts.common.model.RawContact;
69 import com.android.contacts.common.model.RawContactDelta;
70 import com.android.contacts.common.model.ValuesDelta;
71 import com.android.contacts.common.model.RawContactDeltaList;
72 import com.android.contacts.common.model.RawContactModifier;
73 import com.android.contacts.common.model.account.AccountType;
74 import com.android.contacts.common.model.account.AccountWithDataSet;
75 import com.android.contacts.common.model.dataitem.DataKind;
76 import com.android.contacts.util.DialogManager;
77 import com.android.contacts.common.util.EmptyService;
78 
79 import java.lang.ref.WeakReference;
80 import java.util.ArrayList;
81 import java.util.HashMap;
82 import java.util.List;
83 
84 /**
85  * This is a dialog-themed activity for confirming the addition of a detail to an existing contact
86  * (once the user has selected this contact from a list of all contacts). The incoming intent
87  * must have an extra with max 1 phone or email specified, using
88  * {@link android.provider.ContactsContract.Intents.Insert#PHONE} with type
89  * {@link android.provider.ContactsContract.Intents.Insert#PHONE_TYPE} or
90  * {@link android.provider.ContactsContract.Intents.Insert#EMAIL} with type
91  * {@link android.provider.ContactsContract.Intents.Insert#EMAIL_TYPE} intent keys.
92  *
93  * If the selected contact doesn't contain editable raw_contacts, it'll create a new raw_contact
94  * on the first editable account found, and the data will be added to this raw_contact.  The newly
95  * created raw_contact will be joined with the selected contact with aggregation-exceptions.
96  *
97  * TODO: Don't open this activity if there's no editable accounts.
98  * If there's no editable accounts on the system, we'll set {@link #mIsReadOnly} and the dialog
99  * just says "contact is not editable".  It's slightly misleading because this really means
100  * "there's no editable accounts", but in this case we shouldn't show the contact picker in the
101  * first place.
102  * Note when there's no accounts, it *is* okay to show the picker / dialog, because the local-only
103  * contacts are writable.
104  */
105 public class ConfirmAddDetailActivity extends Activity implements
106         DialogManager.DialogShowingViewActivity {
107 
108     private static final String TAG = "ConfirmAdd"; // The class name is too long to be a tag.
109     private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
110 
111     private LayoutInflater mInflater;
112     private View mRootView;
113     private TextView mDisplayNameView;
114     private TextView mReadOnlyWarningView;
115     private ImageView mPhotoView;
116     private ViewGroup mEditorContainerView;
117     private static WeakReference<ProgressDialog> sProgressDialog;
118 
119     private AccountTypeManager mAccountTypeManager;
120     private ContentResolver mContentResolver;
121 
122     private AccountType mEditableAccountType;
123     private Uri mContactUri;
124     private long mContactId;
125     private String mDisplayName;
126     private String mLookupKey;
127     private boolean mIsReadOnly;
128 
129     private QueryHandler mQueryHandler;
130 
131     /** {@link RawContactDeltaList} for the entire selected contact. */
132     private RawContactDeltaList mEntityDeltaList;
133 
134     /** {@link RawContactDeltaList} for the editable account */
135     private RawContactDelta mRawContactDelta;
136 
137     private String mMimetype = Phone.CONTENT_ITEM_TYPE;
138 
139     /**
140      * DialogManager may be needed if the user wants to apply a "custom" label to the contact detail
141      */
142     private final DialogManager mDialogManager = new DialogManager(this);
143 
144     /**
145      * PhotoQuery contains the projection used for retrieving the name and photo
146      * ID of a contact.
147      */
148     private interface ContactQuery {
149         final String[] COLUMNS = new String[] {
150             Contacts._ID,
151             Contacts.LOOKUP_KEY,
152             Contacts.PHOTO_ID,
153             Contacts.DISPLAY_NAME,
154         };
155         final int _ID = 0;
156         final int LOOKUP_KEY = 1;
157         final int PHOTO_ID = 2;
158         final int DISPLAY_NAME = 3;
159     }
160 
161     /**
162      * PhotoQuery contains the projection used for retrieving the raw bytes of
163      * the contact photo.
164      */
165     private interface PhotoQuery {
166         final String[] COLUMNS = new String[] {
167             Photo.PHOTO
168         };
169 
170         final int PHOTO = 0;
171     }
172 
173     /**
174      * ExtraInfoQuery contains the projection used for retrieving the extra info
175      * on a contact (only needed if someone else exists with the same name as
176      * this contact).
177      */
178     private interface ExtraInfoQuery {
179         final String[] COLUMNS = new String[] {
180             RawContacts.CONTACT_ID,
181             Data.MIMETYPE,
182             Data.DATA1,
183         };
184         final int CONTACT_ID = 0;
185         final int MIMETYPE = 1;
186         final int DATA1 = 2;
187     }
188 
189     /**
190      * List of mimetypes to use in order of priority to display for a contact in
191      * a disambiguation case. For example, if the contact does not have a
192      * nickname, use the email field, and etc.
193      */
194     private static final String[] MIME_TYPE_PRIORITY_LIST = new String[] {
195             Nickname.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE, Im.CONTENT_ITEM_TYPE,
196             StructuredPostal.CONTENT_ITEM_TYPE, Phone.CONTENT_ITEM_TYPE };
197 
198     private static final int TOKEN_CONTACT_INFO = 0;
199     private static final int TOKEN_PHOTO_QUERY = 1;
200     private static final int TOKEN_DISAMBIGUATION_QUERY = 2;
201     private static final int TOKEN_EXTRA_INFO_QUERY = 3;
202 
203     private final OnClickListener mDetailsButtonClickListener = new OnClickListener() {
204         @Override
205         public void onClick(View v) {
206             if (mIsReadOnly) {
207                 onSaveCompleted(true);
208             } else {
209                 doSaveAction();
210             }
211         }
212     };
213 
214     private final OnClickListener mDoneButtonClickListener = new OnClickListener() {
215         @Override
216         public void onClick(View v) {
217             doSaveAction();
218         }
219     };
220 
221     private final OnClickListener mCancelButtonClickListener = new OnClickListener() {
222         @Override
223         public void onClick(View v) {
224             setResult(RESULT_CANCELED);
225             finish();
226         }
227     };
228 
229     @Override
onCreate(Bundle icicle)230     protected void onCreate(Bundle icicle) {
231         super.onCreate(icicle);
232 
233         mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
234         mContentResolver = getContentResolver();
235 
236         final Intent intent = getIntent();
237         mContactUri = intent.getData();
238 
239         if (mContactUri == null) {
240             setResult(RESULT_CANCELED);
241             finish();
242         }
243 
244         Bundle extras = intent.getExtras();
245         if (extras != null) {
246             if (extras.containsKey(ContactsContract.Intents.Insert.PHONE)) {
247                 mMimetype = Phone.CONTENT_ITEM_TYPE;
248             } else if (extras.containsKey(ContactsContract.Intents.Insert.EMAIL)) {
249                 mMimetype = Email.CONTENT_ITEM_TYPE;
250             } else {
251                 throw new IllegalStateException("Error: No valid mimetype found in intent extras");
252             }
253         }
254 
255         mAccountTypeManager = AccountTypeManager.getInstance(this);
256 
257         setContentView(R.layout.confirm_add_detail_activity);
258 
259         mRootView = findViewById(R.id.root_view);
260         mReadOnlyWarningView = (TextView) findViewById(R.id.read_only_warning);
261 
262         // Setup "header" (containing contact info) to save the detail and then go to the editor
263         findViewById(R.id.open_details_push_layer).setOnClickListener(mDetailsButtonClickListener);
264 
265         // Setup "done" button to save the detail to the contact and exit.
266         findViewById(R.id.btn_done).setOnClickListener(mDoneButtonClickListener);
267 
268         // Setup "cancel" button to return to previous activity.
269         findViewById(R.id.btn_cancel).setOnClickListener(mCancelButtonClickListener);
270 
271         // Retrieve references to all the Views in the dialog activity.
272         mDisplayNameView = (TextView) findViewById(R.id.name);
273         mPhotoView = (ImageView) findViewById(R.id.photo);
274         mPhotoView.setImageDrawable(ContactPhotoManager.getDefaultAvatarDrawableForContact(
275                 getResources(), false, null));
276 
277         mEditorContainerView = (ViewGroup) findViewById(R.id.editor_container);
278 
279         resetAsyncQueryHandler();
280         startContactQuery(mContactUri);
281 
282         new QueryEntitiesTask(this).execute(intent);
283     }
284 
285     @Override
getDialogManager()286     public DialogManager getDialogManager() {
287         return mDialogManager;
288     }
289 
290     @Override
onCreateDialog(int id, Bundle args)291     protected Dialog onCreateDialog(int id, Bundle args) {
292         if (DialogManager.isManagedId(id)) return mDialogManager.onCreateDialog(id, args);
293 
294         // Nobody knows about the Dialog
295         Log.w(TAG, "Unknown dialog requested, id: " + id + ", args: " + args);
296         return null;
297     }
298 
299     /**
300      * Reset the query handler by creating a new QueryHandler instance.
301      */
resetAsyncQueryHandler()302     private void resetAsyncQueryHandler() {
303         // the api AsyncQueryHandler.cancelOperation() doesn't really work. Since we really
304         // need the old async queries to be cancelled, let's do it the hard way.
305         mQueryHandler = new QueryHandler(mContentResolver);
306     }
307 
308     /**
309      * Internal method to query contact by Uri.
310      *
311      * @param contactUri the contact uri
312      */
startContactQuery(Uri contactUri)313     private void startContactQuery(Uri contactUri) {
314         mQueryHandler.startQuery(TOKEN_CONTACT_INFO, contactUri, contactUri, ContactQuery.COLUMNS,
315                 null, null, null);
316     }
317 
318     /**
319      * Internal method to query contact photo by photo id and uri.
320      *
321      * @param photoId the photo id.
322      * @param lookupKey the lookup uri.
323      */
startPhotoQuery(long photoId, Uri lookupKey)324     private void startPhotoQuery(long photoId, Uri lookupKey) {
325         mQueryHandler.startQuery(TOKEN_PHOTO_QUERY, lookupKey,
326                 ContentUris.withAppendedId(Data.CONTENT_URI, photoId),
327                 PhotoQuery.COLUMNS, null, null, null);
328     }
329 
330     /**
331      * Internal method to query for contacts with a given display name.
332      *
333      * @param contactDisplayName the display name to look for.
334      */
startDisambiguationQuery(String contactDisplayName)335     private void startDisambiguationQuery(String contactDisplayName) {
336         // Apply a limit of 1 result to the query because we only need to
337         // determine whether or not at least one other contact has the same
338         // name. We don't need to find ALL other contacts with the same name.
339         final Builder builder = Contacts.CONTENT_URI.buildUpon();
340         builder.appendQueryParameter("limit", String.valueOf(1));
341         final Uri uri = builder.build();
342 
343         final String displayNameSelection;
344         final String[] selectionArgs;
345         if (TextUtils.isEmpty(contactDisplayName)) {
346             displayNameSelection = Contacts.DISPLAY_NAME_PRIMARY + " IS NULL";
347             selectionArgs = new String[] { String.valueOf(mContactId) };
348         } else {
349             displayNameSelection = Contacts.DISPLAY_NAME_PRIMARY + " = ?";
350             selectionArgs = new String[] { contactDisplayName, String.valueOf(mContactId) };
351         }
352         mQueryHandler.startQuery(TOKEN_DISAMBIGUATION_QUERY, null, uri,
353                 new String[] { Contacts._ID } /* unused projection but a valid one was needed */,
354                 displayNameSelection + " AND " + Contacts.PHOTO_ID + " IS NULL AND "
355                 + Contacts._ID + " <> ?", selectionArgs, null);
356     }
357 
358     /**
359      * Internal method to query for extra data fields for this contact.
360      */
startExtraInfoQuery()361     private void startExtraInfoQuery() {
362         mQueryHandler.startQuery(TOKEN_EXTRA_INFO_QUERY, null, Data.CONTENT_URI,
363                 ExtraInfoQuery.COLUMNS, RawContacts.CONTACT_ID + " = ?",
364                 new String[] { String.valueOf(mContactId) }, null);
365     }
366 
367     private static class QueryEntitiesTask extends AsyncTask<Intent, Void, RawContactDeltaList> {
368 
369         private ConfirmAddDetailActivity activityTarget;
370         private String mSelection;
371 
QueryEntitiesTask(ConfirmAddDetailActivity target)372         public QueryEntitiesTask(ConfirmAddDetailActivity target) {
373             activityTarget = target;
374         }
375 
376         @Override
doInBackground(Intent... params)377         protected RawContactDeltaList doInBackground(Intent... params) {
378 
379             final Intent intent = params[0];
380 
381             final ContentResolver resolver = activityTarget.getContentResolver();
382 
383             // Handle both legacy and new authorities
384             final Uri data = intent.getData();
385             final String authority = data.getAuthority();
386             final String mimeType = intent.resolveType(resolver);
387 
388             mSelection = "0";
389             String selectionArg = null;
390             if (ContactsContract.AUTHORITY.equals(authority)) {
391                 if (Contacts.CONTENT_ITEM_TYPE.equals(mimeType)) {
392                     // Handle selected aggregate
393                     final long contactId = ContentUris.parseId(data);
394                     selectionArg = String.valueOf(contactId);
395                     mSelection = RawContacts.CONTACT_ID + "=?";
396                 } else if (RawContacts.CONTENT_ITEM_TYPE.equals(mimeType)) {
397                     final long rawContactId = ContentUris.parseId(data);
398                     final long contactId = queryForContactId(resolver, rawContactId);
399                     selectionArg = String.valueOf(contactId);
400                     mSelection = RawContacts.CONTACT_ID + "=?";
401                 }
402             } else if (android.provider.Contacts.AUTHORITY.equals(authority)) {
403                 final long rawContactId = ContentUris.parseId(data);
404                 selectionArg = String.valueOf(rawContactId);
405                 mSelection = Data.RAW_CONTACT_ID + "=?";
406             }
407 
408             // Note that this query does not need to concern itself with whether the contact is
409             // the user's profile, since the profile does not show up in the picker.
410             return RawContactDeltaList.fromQuery(RawContactsEntity.CONTENT_URI,
411                     activityTarget.getContentResolver(), mSelection,
412                     new String[] { selectionArg }, null);
413         }
414 
queryForContactId(ContentResolver resolver, long rawContactId)415         private static long queryForContactId(ContentResolver resolver, long rawContactId) {
416             Cursor contactIdCursor = null;
417             long contactId = -1;
418             try {
419                 contactIdCursor = resolver.query(RawContacts.CONTENT_URI,
420                         new String[] { RawContacts.CONTACT_ID },
421                         RawContacts._ID + "=?", new String[] { String.valueOf(rawContactId) },
422                         null);
423                 if (contactIdCursor != null && contactIdCursor.moveToFirst()) {
424                     contactId = contactIdCursor.getLong(0);
425                 }
426             } finally {
427                 if (contactIdCursor != null) {
428                     contactIdCursor.close();
429                 }
430             }
431             return contactId;
432         }
433 
434         @Override
onPostExecute(RawContactDeltaList entityList)435         protected void onPostExecute(RawContactDeltaList entityList) {
436             if (activityTarget.isFinishing()) {
437                 return;
438             }
439             if ((entityList == null) || (entityList.size() == 0)) {
440                 Log.e(TAG, "Contact not found.");
441                 activityTarget.finish();
442                 return;
443             }
444 
445             activityTarget.setEntityDeltaList(entityList);
446         }
447     }
448 
449     private class QueryHandler extends AsyncQueryHandler {
450 
QueryHandler(ContentResolver cr)451         public QueryHandler(ContentResolver cr) {
452             super(cr);
453         }
454 
455         @Override
onQueryComplete(int token, Object cookie, Cursor cursor)456         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
457             try {
458                 if (this != mQueryHandler) {
459                     Log.d(TAG, "onQueryComplete: discard result, the query handler is reset!");
460                     return;
461                 }
462                 if (ConfirmAddDetailActivity.this.isFinishing()) {
463                     return;
464                 }
465 
466                 switch (token) {
467                     case TOKEN_PHOTO_QUERY: {
468                         // Set the photo
469                         Bitmap photoBitmap = null;
470                         if (cursor != null && cursor.moveToFirst()
471                                 && !cursor.isNull(PhotoQuery.PHOTO)) {
472                             byte[] photoData = cursor.getBlob(PhotoQuery.PHOTO);
473                             photoBitmap = BitmapFactory.decodeByteArray(photoData, 0,
474                                     photoData.length, null);
475                         }
476 
477                         if (photoBitmap != null) {
478                             mPhotoView.setImageBitmap(photoBitmap);
479                         }
480 
481                         break;
482                     }
483                     case TOKEN_CONTACT_INFO: {
484                         // Set the contact's name
485                         if (cursor != null && cursor.moveToFirst()) {
486                             // Get the cursor values
487                             mDisplayName = cursor.getString(ContactQuery.DISPLAY_NAME);
488                             mLookupKey = cursor.getString(ContactQuery.LOOKUP_KEY);
489                             setDefaultContactImage(mDisplayName, mLookupKey);
490                             final long photoId = cursor.getLong(ContactQuery.PHOTO_ID);
491 
492                             // If there is no photo ID, then do a disambiguation
493                             // query because other contacts could have the same
494                             // name as this contact.
495                             if (photoId == 0) {
496                                 mContactId = cursor.getLong(ContactQuery._ID);
497                                 startDisambiguationQuery(mDisplayName);
498                             } else if (TextUtils.isEmpty(mLookupKey)) {
499                                 finish();
500                                 return;
501                             } else {
502                                 // Otherwise do the photo query.
503                                 Uri lookupUri = Contacts.getLookupUri(mContactId, mLookupKey);
504                                 startPhotoQuery(photoId, lookupUri);
505                                 // Display the name because there is no
506                                 // disambiguation query.
507                                 setDisplayName();
508                                 showDialogContent();
509                             }
510                         }
511                         break;
512                     }
513                     case TOKEN_DISAMBIGUATION_QUERY: {
514                         // If a cursor was returned with more than 0 results,
515                         // then at least one other contact exists with the same
516                         // name as this contact. Extra info on this contact must
517                         // be displayed to disambiguate the contact, so retrieve
518                         // those additional fields. Otherwise, no other contacts
519                         // with this name exists, so do nothing further.
520                         if (cursor != null && cursor.getCount() > 0) {
521                             startExtraInfoQuery();
522                         } else {
523                             // If there are no other contacts with this name,
524                             // then display the name.
525                             setDisplayName();
526                             showDialogContent();
527                         }
528                         break;
529                     }
530                     case TOKEN_EXTRA_INFO_QUERY: {
531                         // This case should only occur if there are one or more
532                         // other contacts with the same contact name.
533                         if (cursor != null && cursor.moveToFirst()) {
534                             HashMap<String, String> hashMapCursorData = new
535                                     HashMap<String, String>();
536 
537                             // Convert the cursor data into a hashmap of
538                             // (mimetype, data value) pairs. If a contact has
539                             // multiple values with the same mimetype, it's fine
540                             // to override that hashmap entry because we only
541                             // need one value of that type.
542                             while (!cursor.isAfterLast()) {
543                                 final String mimeType = cursor.getString(ExtraInfoQuery.MIMETYPE);
544                                 if (!TextUtils.isEmpty(mimeType)) {
545                                     String value = cursor.getString(ExtraInfoQuery.DATA1);
546                                     if (!TextUtils.isEmpty(value)) {
547                                         // As a special case, phone numbers
548                                         // should be formatted in a specific way.
549                                         if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
550                                             value = PhoneNumberUtils.formatNumber(value);
551                                         }
552                                         hashMapCursorData.put(mimeType, value);
553                                     }
554                                 }
555                                 cursor.moveToNext();
556                             }
557 
558                             // Find the first non-empty field according to the
559                             // mimetype priority list and display this under the
560                             // contact's display name to disambiguate the contact.
561                             for (String mimeType : MIME_TYPE_PRIORITY_LIST) {
562                                 if (hashMapCursorData.containsKey(mimeType)) {
563                                     setDisplayName();
564                                     setExtraInfoField(hashMapCursorData.get(mimeType));
565                                     break;
566                                 }
567                             }
568                             showDialogContent();
569                         }
570                         break;
571                     }
572                 }
573             } finally {
574                 if (cursor != null) {
575                     cursor.close();
576                 }
577             }
578         }
579     }
580 
setEntityDeltaList(RawContactDeltaList entityList)581     private void setEntityDeltaList(RawContactDeltaList entityList) {
582         if (entityList == null) {
583             throw new IllegalStateException();
584         }
585         if (VERBOSE_LOGGING) {
586             Log.v(TAG, "setEntityDeltaList: " + entityList);
587         }
588 
589         mEntityDeltaList = entityList;
590 
591         // Find the editable raw_contact.
592         mRawContactDelta = mEntityDeltaList.getFirstWritableRawContact(this);
593 
594         // If no editable raw_contacts are found, create one.
595         if (mRawContactDelta == null) {
596             mRawContactDelta = addEditableRawContact(this, mEntityDeltaList);
597 
598             if ((mRawContactDelta != null) && VERBOSE_LOGGING) {
599                 Log.v(TAG, "setEntityDeltaList: created editable raw_contact " + entityList);
600             }
601         }
602 
603         if (mRawContactDelta == null) {
604             // Selected contact is read-only, and there's no editable account.
605             mIsReadOnly = true;
606             mEditableAccountType = null;
607         } else {
608             mIsReadOnly = false;
609 
610             mEditableAccountType = mRawContactDelta.getRawContactAccountType(this);
611 
612             // Handle any incoming values that should be inserted
613             final Bundle extras = getIntent().getExtras();
614             if (extras != null && extras.size() > 0) {
615                 // If there are any intent extras, add them as additional fields in the
616                 // RawContactDelta.
617                 RawContactModifier.parseExtras(this, mEditableAccountType, mRawContactDelta,
618                         extras);
619             }
620         }
621 
622         bindEditor();
623     }
624 
625     /**
626      * Create an {@link RawContactDelta} for a raw_contact on the first editable account found, and add
627      * to the list.  Also copy the structured name from an existing (read-only) raw_contact to the
628      * new one, if any of the read-only contacts has a name.
629      */
addEditableRawContact(Context context, RawContactDeltaList entityDeltaList)630     private static RawContactDelta addEditableRawContact(Context context,
631             RawContactDeltaList entityDeltaList) {
632         // First, see if there's an editable account.
633         final AccountTypeManager accounts = AccountTypeManager.getInstance(context);
634         final List<AccountWithDataSet> editableAccounts = accounts.getAccounts(true);
635         if (editableAccounts.size() == 0) {
636             // No editable account type found.  The dialog will be read-only mode.
637             return null;
638         }
639         final AccountWithDataSet editableAccount = editableAccounts.get(0);
640         final AccountType accountType = accounts.getAccountType(
641                 editableAccount.type, editableAccount.dataSet);
642 
643         // Create a new RawContactDelta for the new raw_contact.
644         final RawContact rawContact = new RawContact();
645         rawContact.setAccount(editableAccount);
646 
647         final RawContactDelta entityDelta = new RawContactDelta(ValuesDelta.fromAfter(
648                 rawContact.getValues()));
649 
650         // Then, copy the structure name from an existing (read-only) raw_contact.
651         for (RawContactDelta entity : entityDeltaList) {
652             final ArrayList<ValuesDelta> readOnlyNames =
653                     entity.getMimeEntries(StructuredName.CONTENT_ITEM_TYPE);
654             if ((readOnlyNames != null) && (readOnlyNames.size() > 0)) {
655                 final ValuesDelta readOnlyName = readOnlyNames.get(0);
656                 final ValuesDelta newName = RawContactModifier.ensureKindExists(entityDelta,
657                         accountType, StructuredName.CONTENT_ITEM_TYPE);
658 
659                 // Copy all the data fields.
660                 newName.copyStructuredNameFieldsFrom(readOnlyName);
661                 break;
662             }
663         }
664 
665         // Add the new RawContactDelta to the list.
666         entityDeltaList.add(entityDelta);
667 
668         return entityDelta;
669     }
670 
671     /**
672      * Rebuild the editor to match our underlying {@link #mEntityDeltaList} object.
673      */
bindEditor()674     private void bindEditor() {
675         if (mEntityDeltaList == null) {
676             throw new IllegalStateException();
677         }
678 
679         // If no valid raw contact (to insert the data) was found, we won't have an editable
680         // account type to use. In this case, display an error message and hide the "OK" button.
681         if (mIsReadOnly) {
682             mReadOnlyWarningView.setText(getString(R.string.contact_read_only));
683             mReadOnlyWarningView.setVisibility(View.VISIBLE);
684             mEditorContainerView.setVisibility(View.GONE);
685             findViewById(R.id.btn_done).setVisibility(View.GONE);
686             // Nothing more to be done, just show the UI
687             showDialogContent();
688             return;
689         }
690 
691         // Otherwise display an editor that allows the user to add the data to this raw contact.
692         for (DataKind kind : mEditableAccountType.getSortedDataKinds()) {
693             // Skip kind that are not editable
694             if (!kind.editable) continue;
695             if (mMimetype.equals(kind.mimeType)) {
696                 final ArrayList<ValuesDelta> deltas = mRawContactDelta.getMimeEntries(mMimetype);
697                 if (deltas != null) {
698                     for (ValuesDelta valuesDelta : deltas) {
699                         // Skip entries that aren't visible
700                         if (!valuesDelta.isVisible()) continue;
701                         if (valuesDelta.isInsert()) {
702                             inflateEditorView(kind, valuesDelta, mRawContactDelta);
703                             return;
704                         }
705                     }
706                 }
707             }
708         }
709     }
710 
711     /**
712      * Creates an EditorView for the given entry. This function must be used while constructing
713      * the views corresponding to the the object-model. The resulting EditorView is also added
714      * to the end of mEditors
715      */
inflateEditorView(DataKind dataKind, ValuesDelta valuesDelta, RawContactDelta state)716     private void inflateEditorView(DataKind dataKind, ValuesDelta valuesDelta, RawContactDelta state) {
717         final int layoutResId = EditorUiUtils.getLayoutResourceId(dataKind.mimeType);
718         final View view = mInflater.inflate(layoutResId, mEditorContainerView,
719                 false);
720 
721         if (view instanceof Editor) {
722             Editor editor = (Editor) view;
723             // Don't allow deletion of the field because there is only 1 detail in this editor.
724             editor.setDeletable(false);
725             editor.setValues(dataKind, valuesDelta, state, false, new ViewIdGenerator());
726         }
727 
728         mEditorContainerView.addView(view);
729     }
730 
731     /**
732      * Set the display name to the correct TextView. Don't do this until it is
733      * certain there is no need for a disambiguation field (otherwise the screen
734      * will flicker because the name will be centered and then moved upwards).
735      */
setDisplayName()736     private void setDisplayName() {
737         mDisplayNameView.setText(mDisplayName);
738     }
739 
740     /**
741      * Set the TextView (for extra contact info) with the given value and make the
742      * TextView visible.
743      */
setExtraInfoField(String value)744     private void setExtraInfoField(String value) {
745         TextView extraTextView = (TextView) findViewById(R.id.extra_info);
746         extraTextView.setVisibility(View.VISIBLE);
747         extraTextView.setText(value);
748     }
749 
setDefaultContactImage(String displayName, String lookupKey)750     private void setDefaultContactImage(String displayName, String lookupKey) {
751         mPhotoView.setImageDrawable(ContactPhotoManager.getDefaultAvatarDrawableForContact(
752                 getResources(), false,
753                 new DefaultImageRequest(displayName, lookupKey, false /* isCircular */)));
754     }
755 
756     /**
757      * Shows all the contents of the dialog to the user at one time. This should only be called
758      * once all the queries have completed, otherwise the screen will flash as additional data
759      * comes in.
760      */
showDialogContent()761     private void showDialogContent() {
762         mRootView.setVisibility(View.VISIBLE);
763     }
764 
765     /**
766      * Saves or creates the contact based on the mode, and if successful
767      * finishes the activity.
768      */
doSaveAction()769     private void doSaveAction() {
770         final PersistTask task = new PersistTask(this, mAccountTypeManager);
771         task.execute(mEntityDeltaList);
772     }
773 
774     /**
775      * Background task for persisting edited contact data, using the changes
776      * defined by a set of {@link RawContactDelta}. This task starts
777      * {@link EmptyService} to make sure the background thread can finish
778      * persisting in cases where the system wants to reclaim our process.
779      */
780     private static class PersistTask extends AsyncTask<RawContactDeltaList, Void, Integer> {
781         // In the future, use ContactSaver instead of WeakAsyncTask because of
782         // the danger of the activity being null during a save action
783         private static final int PERSIST_TRIES = 3;
784 
785         private static final int RESULT_UNCHANGED = 0;
786         private static final int RESULT_SUCCESS = 1;
787         private static final int RESULT_FAILURE = 2;
788 
789         private ConfirmAddDetailActivity activityTarget;
790 
791         private AccountTypeManager mAccountTypeManager;
792 
PersistTask(ConfirmAddDetailActivity target, AccountTypeManager accountTypeManager)793         public PersistTask(ConfirmAddDetailActivity target, AccountTypeManager accountTypeManager) {
794             activityTarget = target;
795             mAccountTypeManager = accountTypeManager;
796         }
797 
798         @Override
onPreExecute()799         protected void onPreExecute() {
800             sProgressDialog = new WeakReference<ProgressDialog>(ProgressDialog.show(activityTarget,
801                     null, activityTarget.getText(R.string.savingContact)));
802 
803             // Before starting this task, start an empty service to protect our
804             // process from being reclaimed by the system.
805             final Context context = activityTarget;
806             context.startService(new Intent(context, EmptyService.class));
807         }
808 
809         @Override
doInBackground(RawContactDeltaList... params)810         protected Integer doInBackground(RawContactDeltaList... params) {
811             final Context context = activityTarget;
812             final ContentResolver resolver = context.getContentResolver();
813 
814             RawContactDeltaList state = params[0];
815 
816             if (state == null) {
817                 return RESULT_FAILURE;
818             }
819 
820             // Trim any empty fields, and RawContacts, before persisting
821             RawContactModifier.trimEmpty(state, mAccountTypeManager);
822 
823             // Attempt to persist changes
824             int tries = 0;
825             Integer result = RESULT_FAILURE;
826             while (tries++ < PERSIST_TRIES) {
827                 try {
828                     // Build operations and try applying
829                     // Note: In case we've created a new raw_contact because the selected contact
830                     // is read-only, buildDiff() will create aggregation exceptions to join
831                     // the new one to the existing contact.
832                     final ArrayList<ContentProviderOperation> diff = state.buildDiff();
833                     ContentProviderResult[] results = null;
834                     if (!diff.isEmpty()) {
835                          results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
836                     }
837 
838                     result = (diff.size() > 0) ? RESULT_SUCCESS : RESULT_UNCHANGED;
839                     break;
840 
841                 } catch (RemoteException e) {
842                     // Something went wrong, bail without success
843                     Log.e(TAG, "Problem persisting user edits", e);
844                     break;
845 
846                 } catch (OperationApplicationException e) {
847                     // Version consistency failed, bail without success
848                     Log.e(TAG, "Version consistency failed", e);
849                     break;
850                 }
851             }
852 
853             return result;
854         }
855 
856         /** {@inheritDoc} */
857         @Override
onPostExecute(Integer result)858         protected void onPostExecute(Integer result) {
859             final Context context = activityTarget;
860 
861             dismissProgressDialog();
862 
863             // Show a toast message based on the success or failure of the save action.
864             if (result == RESULT_SUCCESS) {
865                 Toast.makeText(context, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
866             } else if (result == RESULT_FAILURE) {
867                 Toast.makeText(context, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
868             }
869 
870             // Stop the service that was protecting us
871             context.stopService(new Intent(context, EmptyService.class));
872             activityTarget.onSaveCompleted(result != RESULT_FAILURE);
873         }
874     }
875 
876     @Override
onStop()877     protected void onStop() {
878         super.onStop();
879         // Dismiss the progress dialog here to prevent leaking the window on orientation change.
880         dismissProgressDialog();
881     }
882 
883     /**
884      * Dismiss the progress dialog (check if it is null because it is a {@link WeakReference}).
885      */
dismissProgressDialog()886     private static void dismissProgressDialog() {
887         ProgressDialog dialog = (sProgressDialog == null) ? null : sProgressDialog.get();
888         if (dialog != null) {
889             dialog.dismiss();
890         }
891         sProgressDialog = null;
892     }
893 
894     /**
895      * This method is intended to be executed after the background task for saving edited info has
896      * finished. The method sets the activity result (and intent if applicable) and finishes the
897      * activity.
898      * @param success is true if the save task completed successfully, or false otherwise.
899      */
onSaveCompleted(boolean success)900     private void onSaveCompleted(boolean success) {
901         if (success) {
902             Intent intent = new Intent(Intent.ACTION_VIEW, mContactUri);
903             setResult(RESULT_OK, intent);
904         } else {
905             setResult(RESULT_CANCELED);
906         }
907         finish();
908     }
909 }
910