• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 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.ui;
18 
19 import com.android.contacts.ContactsListActivity;
20 import com.android.contacts.ContactsUtils;
21 import com.android.contacts.R;
22 import com.android.contacts.model.ContactsSource;
23 import com.android.contacts.model.Editor;
24 import com.android.contacts.model.EntityDelta;
25 import com.android.contacts.model.EntityModifier;
26 import com.android.contacts.model.EntitySet;
27 import com.android.contacts.model.GoogleSource;
28 import com.android.contacts.model.Sources;
29 import com.android.contacts.model.ContactsSource.EditType;
30 import com.android.contacts.model.Editor.EditorListener;
31 import com.android.contacts.model.EntityDelta.ValuesDelta;
32 import com.android.contacts.ui.widget.BaseContactEditorView;
33 import com.android.contacts.ui.widget.PhotoEditorView;
34 import com.android.contacts.util.EmptyService;
35 import com.android.contacts.util.WeakAsyncTask;
36 import com.google.android.collect.Lists;
37 
38 import android.accounts.Account;
39 import android.app.Activity;
40 import android.app.AlertDialog;
41 import android.app.Dialog;
42 import android.app.ProgressDialog;
43 import android.content.ActivityNotFoundException;
44 import android.content.ContentProviderOperation;
45 import android.content.ContentProviderResult;
46 import android.content.ContentResolver;
47 import android.content.ContentUris;
48 import android.content.ContentValues;
49 import android.content.Context;
50 import android.content.DialogInterface;
51 import android.content.Entity;
52 import android.content.Intent;
53 import android.content.OperationApplicationException;
54 import android.content.ContentProviderOperation.Builder;
55 import android.database.Cursor;
56 import android.graphics.Bitmap;
57 import android.net.Uri;
58 import android.os.Bundle;
59 import android.os.RemoteException;
60 import android.provider.ContactsContract;
61 import android.provider.ContactsContract.AggregationExceptions;
62 import android.provider.ContactsContract.Contacts;
63 import android.provider.ContactsContract.RawContacts;
64 import android.provider.ContactsContract.CommonDataKinds.Email;
65 import android.provider.ContactsContract.CommonDataKinds.Phone;
66 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
67 import android.provider.ContactsContract.Contacts.Data;
68 import android.util.Log;
69 import android.view.ContextThemeWrapper;
70 import android.view.LayoutInflater;
71 import android.view.Menu;
72 import android.view.MenuInflater;
73 import android.view.MenuItem;
74 import android.view.View;
75 import android.view.ViewGroup;
76 import android.widget.ArrayAdapter;
77 import android.widget.LinearLayout;
78 import android.widget.ListAdapter;
79 import android.widget.TextView;
80 import android.widget.Toast;
81 
82 import java.lang.ref.WeakReference;
83 import java.util.ArrayList;
84 import java.util.Collections;
85 import java.util.Comparator;
86 
87 /**
88  * Activity for editing or inserting a contact.
89  */
90 public final class EditContactActivity extends Activity
91         implements View.OnClickListener, Comparator<EntityDelta> {
92     private static final String TAG = "EditContactActivity";
93 
94     /** The launch code when picking a photo and the raw data is returned */
95     private static final int PHOTO_PICKED_WITH_DATA = 3021;
96 
97     /** The launch code when a contact to join with is returned */
98     private static final int REQUEST_JOIN_CONTACT = 3022;
99 
100     private static final String KEY_EDIT_STATE = "state";
101     private static final String KEY_RAW_CONTACT_ID_REQUESTING_PHOTO = "photorequester";
102 
103     /** The result code when view activity should close after edit returns */
104     public static final int RESULT_CLOSE_VIEW_ACTIVITY = 777;
105 
106     public static final int SAVE_MODE_DEFAULT = 0;
107     public static final int SAVE_MODE_SPLIT = 1;
108     public static final int SAVE_MODE_JOIN = 2;
109 
110     private long mRawContactIdRequestingPhoto = -1;
111 
112     private static final int DIALOG_CONFIRM_DELETE = 1;
113     private static final int DIALOG_CONFIRM_READONLY_DELETE = 2;
114     private static final int DIALOG_CONFIRM_MULTIPLE_DELETE = 3;
115     private static final int DIALOG_CONFIRM_READONLY_HIDE = 4;
116 
117     String mQuerySelection;
118 
119     private long mContactIdForJoin;
120     EntitySet mState;
121 
122     /** The linear layout holding the ContactEditorViews */
123     LinearLayout mContent;
124 
125     private ArrayList<Dialog> mManagedDialogs = Lists.newArrayList();
126 
127     @Override
onCreate(Bundle icicle)128     protected void onCreate(Bundle icicle) {
129         super.onCreate(icicle);
130 
131         final Intent intent = getIntent();
132         final String action = intent.getAction();
133 
134         setContentView(R.layout.act_edit);
135 
136         // Build editor and listen for photo requests
137         mContent = (LinearLayout) findViewById(R.id.editors);
138 
139         findViewById(R.id.btn_done).setOnClickListener(this);
140         findViewById(R.id.btn_discard).setOnClickListener(this);
141 
142         // Handle initial actions only when existing state missing
143         final boolean hasIncomingState = icicle != null && icicle.containsKey(KEY_EDIT_STATE);
144 
145         if (Intent.ACTION_EDIT.equals(action) && !hasIncomingState) {
146             // Read initial state from database
147             new QueryEntitiesTask(this).execute(intent);
148         } else if (Intent.ACTION_INSERT.equals(action) && !hasIncomingState) {
149             // Trigger dialog to pick account type
150             doAddAction();
151         }
152     }
153 
154     private static class QueryEntitiesTask extends
155             WeakAsyncTask<Intent, Void, Void, EditContactActivity> {
QueryEntitiesTask(EditContactActivity target)156         public QueryEntitiesTask(EditContactActivity target) {
157             super(target);
158         }
159 
160         @Override
doInBackground(EditContactActivity target, Intent... params)161         protected Void doInBackground(EditContactActivity target, Intent... params) {
162             // Load edit details in background
163             final Context context = target;
164             final Sources sources = Sources.getInstance(context);
165             final Intent intent = params[0];
166 
167             final ContentResolver resolver = context.getContentResolver();
168 
169             // Handle both legacy and new authorities
170             final Uri data = intent.getData();
171             final String authority = data.getAuthority();
172             final String mimeType = intent.resolveType(resolver);
173 
174             String selection = "0";
175             if (ContactsContract.AUTHORITY.equals(authority)) {
176                 if (Contacts.CONTENT_ITEM_TYPE.equals(mimeType)) {
177                     // Handle selected aggregate
178                     final long contactId = ContentUris.parseId(data);
179                     selection = RawContacts.CONTACT_ID + "=" + contactId;
180                 } else if (RawContacts.CONTENT_ITEM_TYPE.equals(mimeType)) {
181                     final long rawContactId = ContentUris.parseId(data);
182                     final long contactId = ContactsUtils.queryForContactId(resolver, rawContactId);
183                     selection = RawContacts.CONTACT_ID + "=" + contactId;
184                 }
185             } else if (android.provider.Contacts.AUTHORITY.equals(authority)) {
186                 final long rawContactId = ContentUris.parseId(data);
187                 selection = Data.RAW_CONTACT_ID + "=" + rawContactId;
188             }
189 
190             target.mQuerySelection = selection;
191             target.mState = EntitySet.fromQuery(resolver, selection, null, null);
192 
193             // Handle any incoming values that should be inserted
194             final Bundle extras = intent.getExtras();
195             final boolean hasExtras = extras != null && extras.size() > 0;
196             final boolean hasState = target.mState.size() > 0;
197             if (hasExtras && hasState) {
198                 // Find source defining the first RawContact found
199                 final EntityDelta state = target.mState.get(0);
200                 final String accountType = state.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
201                 final ContactsSource source = sources.getInflatedSource(accountType,
202                         ContactsSource.LEVEL_CONSTRAINTS);
203                 EntityModifier.parseExtras(context, source, state, extras);
204             }
205 
206             return null;
207         }
208 
209         @Override
onPostExecute(EditContactActivity target, Void result)210         protected void onPostExecute(EditContactActivity target, Void result) {
211             // Bind UI to new background state
212             target.bindEditors();
213         }
214     }
215 
216 
217 
218     @Override
onSaveInstanceState(Bundle outState)219     protected void onSaveInstanceState(Bundle outState) {
220         if (hasValidState()) {
221             // Store entities with modifications
222             outState.putParcelable(KEY_EDIT_STATE, mState);
223         }
224 
225         outState.putLong(KEY_RAW_CONTACT_ID_REQUESTING_PHOTO, mRawContactIdRequestingPhoto);
226         super.onSaveInstanceState(outState);
227     }
228 
229     @Override
onRestoreInstanceState(Bundle savedInstanceState)230     protected void onRestoreInstanceState(Bundle savedInstanceState) {
231         // Read modifications from instance
232         mState = savedInstanceState.<EntitySet> getParcelable(KEY_EDIT_STATE);
233         mRawContactIdRequestingPhoto = savedInstanceState.getLong(
234                 KEY_RAW_CONTACT_ID_REQUESTING_PHOTO);
235         bindEditors();
236 
237         super.onRestoreInstanceState(savedInstanceState);
238     }
239 
240     @Override
onDestroy()241     protected void onDestroy() {
242         super.onDestroy();
243 
244         for (Dialog dialog : mManagedDialogs) {
245             if (dialog.isShowing()) {
246                 dialog.dismiss();
247             }
248         }
249     }
250 
251     @Override
onCreateDialog(int id)252     protected Dialog onCreateDialog(int id) {
253         switch (id) {
254             case DIALOG_CONFIRM_DELETE:
255                 return new AlertDialog.Builder(this)
256                         .setTitle(R.string.deleteConfirmation_title)
257                         .setIcon(android.R.drawable.ic_dialog_alert)
258                         .setMessage(R.string.deleteConfirmation)
259                         .setNegativeButton(android.R.string.cancel, null)
260                         .setPositiveButton(android.R.string.ok, new DeleteClickListener())
261                         .setCancelable(false)
262                         .create();
263             case DIALOG_CONFIRM_READONLY_DELETE:
264                 return new AlertDialog.Builder(this)
265                         .setTitle(R.string.deleteConfirmation_title)
266                         .setIcon(android.R.drawable.ic_dialog_alert)
267                         .setMessage(R.string.readOnlyContactDeleteConfirmation)
268                         .setNegativeButton(android.R.string.cancel, null)
269                         .setPositiveButton(android.R.string.ok, new DeleteClickListener())
270                         .setCancelable(false)
271                         .create();
272             case DIALOG_CONFIRM_MULTIPLE_DELETE:
273                 return new AlertDialog.Builder(this)
274                         .setTitle(R.string.deleteConfirmation_title)
275                         .setIcon(android.R.drawable.ic_dialog_alert)
276                         .setMessage(R.string.multipleContactDeleteConfirmation)
277                         .setNegativeButton(android.R.string.cancel, null)
278                         .setPositiveButton(android.R.string.ok, new DeleteClickListener())
279                         .setCancelable(false)
280                         .create();
281             case DIALOG_CONFIRM_READONLY_HIDE:
282                 return new AlertDialog.Builder(this)
283                         .setTitle(R.string.deleteConfirmation_title)
284                         .setIcon(android.R.drawable.ic_dialog_alert)
285                         .setMessage(R.string.readOnlyContactWarning)
286                         .setPositiveButton(android.R.string.ok, new DeleteClickListener())
287                         .setCancelable(false)
288                         .create();
289         }
290         return null;
291     }
292 
293     /**
294      * Start managing this {@link Dialog} along with the {@link Activity}.
295      */
startManagingDialog(Dialog dialog)296     private void startManagingDialog(Dialog dialog) {
297         synchronized (mManagedDialogs) {
298             mManagedDialogs.add(dialog);
299         }
300     }
301 
302     /**
303      * Show this {@link Dialog} and manage with the {@link Activity}.
304      */
showAndManageDialog(Dialog dialog)305     void showAndManageDialog(Dialog dialog) {
306         startManagingDialog(dialog);
307         dialog.show();
308     }
309 
310     /**
311      * Check if our internal {@link #mState} is valid, usually checked before
312      * performing user actions.
313      */
hasValidState()314     protected boolean hasValidState() {
315         return mState != null && mState.size() > 0;
316     }
317 
318     /**
319      * Rebuild the editors to match our underlying {@link #mState} object, usually
320      * called once we've parsed {@link Entity} data or have inserted a new
321      * {@link RawContacts}.
322      */
bindEditors()323     protected void bindEditors() {
324         if (!hasValidState()) return;
325 
326         final LayoutInflater inflater = (LayoutInflater) getSystemService(
327                 Context.LAYOUT_INFLATER_SERVICE);
328         final Sources sources = Sources.getInstance(this);
329 
330         // Sort the editors
331         Collections.sort(mState, this);
332 
333         // Remove any existing editors and rebuild any visible
334         mContent.removeAllViews();
335         int size = mState.size();
336         for (int i = 0; i < size; i++) {
337             // TODO ensure proper ordering of entities in the list
338             EntityDelta entity = mState.get(i);
339             final ValuesDelta values = entity.getValues();
340             if (!values.isVisible()) continue;
341 
342             final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
343             final ContactsSource source = sources.getInflatedSource(accountType,
344                     ContactsSource.LEVEL_CONSTRAINTS);
345             final long rawContactId = values.getAsLong(RawContacts._ID);
346 
347             BaseContactEditorView editor;
348             if (!source.readOnly) {
349                 editor = (BaseContactEditorView) inflater.inflate(R.layout.item_contact_editor,
350                         mContent, false);
351             } else {
352                 editor = (BaseContactEditorView) inflater.inflate(
353                         R.layout.item_read_only_contact_editor, mContent, false);
354             }
355             PhotoEditorView photoEditor = editor.getPhotoEditor();
356             photoEditor.setEditorListener(new PhotoListener(rawContactId, source.readOnly,
357                     photoEditor));
358 
359             mContent.addView(editor);
360             editor.setState(entity, source);
361         }
362 
363         // Show editor now that we've loaded state
364         mContent.setVisibility(View.VISIBLE);
365     }
366 
367     /**
368      * Class that listens to requests coming from photo editors
369      */
370     private class PhotoListener implements EditorListener, DialogInterface.OnClickListener {
371         private long mRawContactId;
372         private boolean mReadOnly;
373         private PhotoEditorView mEditor;
374 
PhotoListener(long rawContactId, boolean readOnly, PhotoEditorView editor)375         public PhotoListener(long rawContactId, boolean readOnly, PhotoEditorView editor) {
376             mRawContactId = rawContactId;
377             mReadOnly = readOnly;
378             mEditor = editor;
379         }
380 
onDeleted(Editor editor)381         public void onDeleted(Editor editor) {
382             // Do nothing
383         }
384 
onRequest(int request)385         public void onRequest(int request) {
386             if (!hasValidState()) return;
387 
388             if (request == EditorListener.REQUEST_PICK_PHOTO) {
389                 if (mEditor.hasSetPhoto()) {
390                     // There is an existing photo, offer to remove, replace, or promoto to primary
391                     createPhotoDialog().show();
392                 } else if (!mReadOnly) {
393                     // No photo set and not read-only, try to set the photo
394                     doPickPhotoAction(mRawContactId);
395                 }
396             }
397         }
398 
399         /**
400          * Prepare dialog for picking a new {@link EditType} or entering a
401          * custom label. This dialog is limited to the valid types as determined
402          * by {@link EntityModifier}.
403          */
createPhotoDialog()404         public Dialog createPhotoDialog() {
405             Context context = EditContactActivity.this;
406 
407             // Wrap our context to inflate list items using correct theme
408             final Context dialogContext = new ContextThemeWrapper(context,
409                     android.R.style.Theme_Light);
410 
411             String[] choices;
412             if (mReadOnly) {
413                 choices = new String[1];
414                 choices[0] = getString(R.string.use_photo_as_primary);
415             } else {
416                 choices = new String[3];
417                 choices[0] = getString(R.string.use_photo_as_primary);
418                 choices[1] = getString(R.string.removePicture);
419                 choices[2] = getString(R.string.changePicture);
420             }
421             final ListAdapter adapter = new ArrayAdapter<String>(dialogContext,
422                     android.R.layout.simple_list_item_1, choices);
423 
424             final AlertDialog.Builder builder = new AlertDialog.Builder(dialogContext);
425             builder.setTitle(R.string.attachToContact);
426             builder.setSingleChoiceItems(adapter, -1, this);
427             return builder.create();
428         }
429 
430         /**
431          * Called when something in the dialog is clicked
432          */
onClick(DialogInterface dialog, int which)433         public void onClick(DialogInterface dialog, int which) {
434             dialog.dismiss();
435 
436             switch (which) {
437                 case 0:
438                     // Set the photo as super primary
439                     mEditor.setSuperPrimary(true);
440 
441                     // And set all other photos as not super primary
442                     int count = mContent.getChildCount();
443                     for (int i = 0; i < count; i++) {
444                         View childView = mContent.getChildAt(i);
445                         if (childView instanceof BaseContactEditorView) {
446                             BaseContactEditorView editor = (BaseContactEditorView) childView;
447                             PhotoEditorView photoEditor = editor.getPhotoEditor();
448                             if (!photoEditor.equals(mEditor)) {
449                                 photoEditor.setSuperPrimary(false);
450                             }
451                         }
452                     }
453                     break;
454 
455                 case 1:
456                     // Remove the photo
457                     mEditor.setPhotoBitmap(null);
458                     break;
459 
460                 case 2:
461                     // Pick a new photo for the contact
462                     doPickPhotoAction(mRawContactId);
463                     break;
464             }
465         }
466     }
467 
468     /** {@inheritDoc} */
onClick(View view)469     public void onClick(View view) {
470         switch (view.getId()) {
471             case R.id.btn_done:
472                 doSaveAction(SAVE_MODE_DEFAULT);
473                 break;
474             case R.id.btn_discard:
475                 doRevertAction();
476                 break;
477         }
478     }
479 
480     /** {@inheritDoc} */
481     @Override
onBackPressed()482     public void onBackPressed() {
483         doSaveAction(SAVE_MODE_DEFAULT);
484     }
485 
486     /** {@inheritDoc} */
487     @Override
onActivityResult(int requestCode, int resultCode, Intent data)488     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
489         // Ignore failed requests
490         if (resultCode != RESULT_OK) return;
491 
492         switch (requestCode) {
493             case PHOTO_PICKED_WITH_DATA: {
494                 BaseContactEditorView requestingEditor = null;
495                 for (int i = 0; i < mContent.getChildCount(); i++) {
496                     View childView = mContent.getChildAt(i);
497                     if (childView instanceof BaseContactEditorView) {
498                         BaseContactEditorView editor = (BaseContactEditorView) childView;
499                         if (editor.getRawContactId() == mRawContactIdRequestingPhoto) {
500                             requestingEditor = editor;
501                             break;
502                         }
503                     }
504                 }
505 
506                 if (requestingEditor != null) {
507                     final Bitmap photo = data.getParcelableExtra("data");
508                     requestingEditor.setPhotoBitmap(photo);
509                     mRawContactIdRequestingPhoto = -1;
510                 } else {
511                     // The contact that requested the photo is no longer present.
512                     // TODO: Show error message
513                 }
514 
515                 break;
516             }
517 
518             case REQUEST_JOIN_CONTACT: {
519                 if (resultCode == RESULT_OK && data != null) {
520                     final long contactId = ContentUris.parseId(data.getData());
521                     joinAggregate(contactId);
522                 }
523             }
524         }
525     }
526 
527     @Override
onCreateOptionsMenu(Menu menu)528     public boolean onCreateOptionsMenu(Menu menu) {
529         super.onCreateOptionsMenu(menu);
530 
531         MenuInflater inflater = getMenuInflater();
532         inflater.inflate(R.menu.edit, menu);
533 
534 
535         return true;
536     }
537 
538     @Override
onPrepareOptionsMenu(Menu menu)539     public boolean onPrepareOptionsMenu(Menu menu) {
540         menu.findItem(R.id.menu_split).setVisible(mState != null && mState.size() > 1);
541         return true;
542     }
543 
544     @Override
onOptionsItemSelected(MenuItem item)545     public boolean onOptionsItemSelected(MenuItem item) {
546         switch (item.getItemId()) {
547             case R.id.menu_done:
548                 return doSaveAction(SAVE_MODE_DEFAULT);
549             case R.id.menu_discard:
550                 return doRevertAction();
551             case R.id.menu_add:
552                 return doAddAction();
553             case R.id.menu_delete:
554                 return doDeleteAction();
555             case R.id.menu_split:
556                 return doSplitContactAction();
557             case R.id.menu_join:
558                 return doJoinContactAction();
559         }
560         return false;
561     }
562 
563     /**
564      * Background task for persisting edited contact data, using the changes
565      * defined by a set of {@link EntityDelta}. This task starts
566      * {@link EmptyService} to make sure the background thread can finish
567      * persisting in cases where the system wants to reclaim our process.
568      */
569     public static class PersistTask extends
570             WeakAsyncTask<EntitySet, Void, Integer, EditContactActivity> {
571         private static final int PERSIST_TRIES = 3;
572 
573         private static final int RESULT_UNCHANGED = 0;
574         private static final int RESULT_SUCCESS = 1;
575         private static final int RESULT_FAILURE = 2;
576 
577         private WeakReference<ProgressDialog> progress;
578 
579         private int mSaveMode;
580         private Uri mContactLookupUri = null;
581 
PersistTask(EditContactActivity target, int saveMode)582         public PersistTask(EditContactActivity target, int saveMode) {
583             super(target);
584             mSaveMode = saveMode;
585         }
586 
587         /** {@inheritDoc} */
588         @Override
onPreExecute(EditContactActivity target)589         protected void onPreExecute(EditContactActivity target) {
590             this.progress = new WeakReference<ProgressDialog>(ProgressDialog.show(target, null,
591                     target.getText(R.string.savingContact)));
592 
593             // Before starting this task, start an empty service to protect our
594             // process from being reclaimed by the system.
595             final Context context = target;
596             context.startService(new Intent(context, EmptyService.class));
597         }
598 
599         /** {@inheritDoc} */
600         @Override
doInBackground(EditContactActivity target, EntitySet... params)601         protected Integer doInBackground(EditContactActivity target, EntitySet... params) {
602             final Context context = target;
603             final ContentResolver resolver = context.getContentResolver();
604 
605             EntitySet state = params[0];
606 
607             // Trim any empty fields, and RawContacts, before persisting
608             final Sources sources = Sources.getInstance(context);
609             EntityModifier.trimEmpty(state, sources);
610 
611             // Attempt to persist changes
612             int tries = 0;
613             Integer result = RESULT_FAILURE;
614             while (tries++ < PERSIST_TRIES) {
615                 try {
616                     // Build operations and try applying
617                     final ArrayList<ContentProviderOperation> diff = state.buildDiff();
618                     ContentProviderResult[] results = null;
619                     if (!diff.isEmpty()) {
620                          results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
621                     }
622 
623                     final long rawContactId = getRawContactId(state, diff, results);
624                     if (rawContactId != -1) {
625                         final Uri rawContactUri = ContentUris.withAppendedId(
626                                 RawContacts.CONTENT_URI, rawContactId);
627 
628                         // convert the raw contact URI to a contact URI
629                         mContactLookupUri = RawContacts.getContactLookupUri(resolver,
630                                 rawContactUri);
631                     }
632                     result = (diff.size() > 0) ? RESULT_SUCCESS : RESULT_UNCHANGED;
633                     break;
634 
635                 } catch (RemoteException e) {
636                     // Something went wrong, bail without success
637                     Log.e(TAG, "Problem persisting user edits", e);
638                     break;
639 
640                 } catch (OperationApplicationException e) {
641                     // Version consistency failed, re-parent change and try again
642                     Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
643                     final EntitySet newState = EntitySet.fromQuery(resolver,
644                             target.mQuerySelection, null, null);
645                     state = EntitySet.mergeAfter(newState, state);
646                 }
647             }
648 
649             return result;
650         }
651 
getRawContactId(EntitySet state, final ArrayList<ContentProviderOperation> diff, final ContentProviderResult[] results)652         private long getRawContactId(EntitySet state,
653                 final ArrayList<ContentProviderOperation> diff,
654                 final ContentProviderResult[] results) {
655             long rawContactId = state.findRawContactId();
656             if (rawContactId != -1) {
657                 return rawContactId;
658             }
659 
660             // we gotta do some searching for the id
661             final int diffSize = diff.size();
662             for (int i = 0; i < diffSize; i++) {
663                 ContentProviderOperation operation = diff.get(i);
664                 if (operation.getType() == ContentProviderOperation.TYPE_INSERT
665                         && operation.getUri().getEncodedPath().contains(
666                                 RawContacts.CONTENT_URI.getEncodedPath())) {
667                     return ContentUris.parseId(results[i].uri);
668                 }
669             }
670             return -1;
671         }
672 
673         /** {@inheritDoc} */
674         @Override
onPostExecute(EditContactActivity target, Integer result)675         protected void onPostExecute(EditContactActivity target, Integer result) {
676             final Context context = target;
677 
678             if (result == RESULT_SUCCESS && mSaveMode != SAVE_MODE_JOIN) {
679                 Toast.makeText(context, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
680             } else if (result == RESULT_FAILURE) {
681                 Toast.makeText(context, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
682             }
683 
684             progress.get().dismiss();
685 
686             // Stop the service that was protecting us
687             context.stopService(new Intent(context, EmptyService.class));
688 
689             target.onSaveCompleted(result != RESULT_FAILURE, mSaveMode, mContactLookupUri);
690         }
691     }
692 
693     /**
694      * Saves or creates the contact based on the mode, and if successful
695      * finishes the activity.
696      */
doSaveAction(int saveMode)697     boolean doSaveAction(int saveMode) {
698         if (!hasValidState()) return false;
699 
700         final PersistTask task = new PersistTask(this, saveMode);
701         task.execute(mState);
702 
703         return true;
704     }
705 
706     private class DeleteClickListener implements DialogInterface.OnClickListener {
707 
onClick(DialogInterface dialog, int which)708         public void onClick(DialogInterface dialog, int which) {
709             Sources sources = Sources.getInstance(EditContactActivity.this);
710             // Mark all raw contacts for deletion
711             for (EntityDelta delta : mState) {
712                 delta.markDeleted();
713             }
714             // Save the deletes
715             doSaveAction(SAVE_MODE_DEFAULT);
716             finish();
717         }
718     }
719 
onSaveCompleted(boolean success, int saveMode, Uri contactLookupUri)720     private void onSaveCompleted(boolean success, int saveMode, Uri contactLookupUri) {
721         switch (saveMode) {
722             case SAVE_MODE_DEFAULT:
723                 if (success && contactLookupUri != null) {
724                     final Intent resultIntent = new Intent();
725 
726                     final Uri requestData = getIntent().getData();
727                     final String requestAuthority = requestData == null ? null : requestData
728                             .getAuthority();
729 
730                     if (android.provider.Contacts.AUTHORITY.equals(requestAuthority)) {
731                         // Build legacy Uri when requested by caller
732                         final long contactId = ContentUris.parseId(Contacts.lookupContact(
733                                 getContentResolver(), contactLookupUri));
734                         final Uri legacyUri = ContentUris.withAppendedId(
735                                 android.provider.Contacts.People.CONTENT_URI, contactId);
736                         resultIntent.setData(legacyUri);
737                     } else {
738                         // Otherwise pass back a lookup-style Uri
739                         resultIntent.setData(contactLookupUri);
740                     }
741 
742                     setResult(RESULT_OK, resultIntent);
743                 } else {
744                     setResult(RESULT_CANCELED, null);
745                 }
746                 finish();
747                 break;
748 
749             case SAVE_MODE_SPLIT:
750                 if (success) {
751                     Intent intent = new Intent();
752                     intent.setData(contactLookupUri);
753                     setResult(RESULT_CLOSE_VIEW_ACTIVITY, intent);
754                 }
755                 finish();
756                 break;
757 
758             case SAVE_MODE_JOIN:
759                 if (success) {
760                     showJoinAggregateActivity(contactLookupUri);
761                 }
762                 break;
763         }
764     }
765 
766     /**
767      * Shows a list of aggregates that can be joined into the currently viewed aggregate.
768      *
769      * @param contactLookupUri the fresh URI for the currently edited contact (after saving it)
770      */
showJoinAggregateActivity(Uri contactLookupUri)771     public void showJoinAggregateActivity(Uri contactLookupUri) {
772         if (contactLookupUri == null) {
773             return;
774         }
775 
776         mContactIdForJoin = ContentUris.parseId(contactLookupUri);
777         Intent intent = new Intent(ContactsListActivity.JOIN_AGGREGATE);
778         intent.putExtra(ContactsListActivity.EXTRA_AGGREGATE_ID, mContactIdForJoin);
779         startActivityForResult(intent, REQUEST_JOIN_CONTACT);
780     }
781 
782     /**
783      * Performs aggregation with the contact selected by the user from suggestions or A-Z list.
784      */
joinAggregate(final long contactId)785     private void joinAggregate(final long contactId) {
786         ContentResolver resolver = getContentResolver();
787 
788         // Load raw contact IDs for all raw contacts involved - currently edited and selected
789         // in the join UIs
790         Cursor c = resolver.query(RawContacts.CONTENT_URI,
791                 new String[] {RawContacts._ID},
792                 RawContacts.CONTACT_ID + "=" + contactId
793                 + " OR " + RawContacts.CONTACT_ID + "=" + mContactIdForJoin, null, null);
794 
795         long rawContactIds[];
796         try {
797             rawContactIds = new long[c.getCount()];
798             for (int i = 0; i < rawContactIds.length; i++) {
799                 c.moveToNext();
800                 rawContactIds[i] = c.getLong(0);
801             }
802         } finally {
803             c.close();
804         }
805 
806         // For each pair of raw contacts, insert an aggregation exception
807         ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
808         for (int i = 0; i < rawContactIds.length; i++) {
809             for (int j = 0; j < rawContactIds.length; j++) {
810                 if (i != j) {
811                     buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
812                 }
813             }
814         }
815 
816         // Apply all aggregation exceptions as one batch
817         try {
818             getContentResolver().applyBatch(ContactsContract.AUTHORITY, operations);
819 
820             // We can use any of the constituent raw contacts to refresh the UI - why not the first
821             Intent intent = new Intent();
822             intent.setData(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
823 
824             // Reload the new state from database
825             new QueryEntitiesTask(this).execute(intent);
826 
827             Toast.makeText(this, R.string.contactsJoinedMessage, Toast.LENGTH_LONG).show();
828         } catch (RemoteException e) {
829             Log.e(TAG, "Failed to apply aggregation exception batch", e);
830             Toast.makeText(this, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
831         } catch (OperationApplicationException e) {
832             Log.e(TAG, "Failed to apply aggregation exception batch", e);
833             Toast.makeText(this, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
834         }
835     }
836 
837     /**
838      * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
839      */
buildJoinContactDiff(ArrayList<ContentProviderOperation> operations, long rawContactId1, long rawContactId2)840     private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
841             long rawContactId1, long rawContactId2) {
842         Builder builder =
843                 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
844         builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
845         builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
846         builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
847         operations.add(builder.build());
848     }
849 
850     /**
851      * Revert any changes the user has made, and finish the activity.
852      */
doRevertAction()853     private boolean doRevertAction() {
854         finish();
855         return true;
856     }
857 
858     /**
859      * Create a new {@link RawContacts} which will exist as another
860      * {@link EntityDelta} under the currently edited {@link Contacts}.
861      */
doAddAction()862     private boolean doAddAction() {
863         // Adding is okay when missing state
864         new AddContactTask(this).execute();
865         return true;
866     }
867 
868     /**
869      * Delete the entire contact currently being edited, which usually asks for
870      * user confirmation before continuing.
871      */
doDeleteAction()872     private boolean doDeleteAction() {
873         if (!hasValidState()) return false;
874         int readOnlySourcesCnt = 0;
875 	int writableSourcesCnt = 0;
876         Sources sources = Sources.getInstance(EditContactActivity.this);
877         for (EntityDelta delta : mState) {
878 	    final String accountType = delta.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
879             final ContactsSource contactsSource = sources.getInflatedSource(accountType,
880                     ContactsSource.LEVEL_CONSTRAINTS);
881             if (contactsSource != null && contactsSource.readOnly) {
882                 readOnlySourcesCnt += 1;
883             } else {
884                 writableSourcesCnt += 1;
885             }
886 	}
887 
888         if (readOnlySourcesCnt > 0 && writableSourcesCnt > 0) {
889 	    showDialog(DIALOG_CONFIRM_READONLY_DELETE);
890 	} else if (readOnlySourcesCnt > 0 && writableSourcesCnt == 0) {
891 	    showDialog(DIALOG_CONFIRM_READONLY_HIDE);
892 	} else if (readOnlySourcesCnt == 0 && writableSourcesCnt > 1) {
893 	    showDialog(DIALOG_CONFIRM_MULTIPLE_DELETE);
894         } else {
895 	    showDialog(DIALOG_CONFIRM_DELETE);
896 	}
897 	return true;
898     }
899 
900     /**
901      * Pick a specific photo to be added under the currently selected tab.
902      */
doPickPhotoAction(long rawContactId)903     boolean doPickPhotoAction(long rawContactId) {
904         if (!hasValidState()) return false;
905 
906         try {
907             // Launch picker to choose photo for selected contact
908             final Intent intent = ContactsUtils.getPhotoPickIntent();
909             startActivityForResult(intent, PHOTO_PICKED_WITH_DATA);
910             mRawContactIdRequestingPhoto = rawContactId;
911         } catch (ActivityNotFoundException e) {
912             Toast.makeText(this, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
913         }
914         return true;
915     }
916 
917     /** {@inheritDoc} */
onDeleted(Editor editor)918     public void onDeleted(Editor editor) {
919         // Ignore any editor deletes
920     }
921 
doSplitContactAction()922     private boolean doSplitContactAction() {
923         if (!hasValidState()) return false;
924 
925         showAndManageDialog(createSplitDialog());
926         return true;
927     }
928 
createSplitDialog()929     private Dialog createSplitDialog() {
930         final AlertDialog.Builder builder = new AlertDialog.Builder(this);
931         builder.setTitle(R.string.splitConfirmation_title);
932         builder.setIcon(android.R.drawable.ic_dialog_alert);
933         builder.setMessage(R.string.splitConfirmation);
934         builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
935             public void onClick(DialogInterface dialog, int which) {
936                 // Split the contacts
937                 mState.splitRawContacts();
938                 doSaveAction(SAVE_MODE_SPLIT);
939             }
940         });
941         builder.setNegativeButton(android.R.string.cancel, null);
942         builder.setCancelable(false);
943         return builder.create();
944     }
945 
doJoinContactAction()946     private boolean doJoinContactAction() {
947         return doSaveAction(SAVE_MODE_JOIN);
948     }
949 
950 
951 
952 
953 
954 
955 
956 
957     /**
958      * Build dialog that handles adding a new {@link RawContacts} after the user
959      * picks a specific {@link ContactsSource}.
960      */
961     private static class AddContactTask extends
962             WeakAsyncTask<Void, Void, AlertDialog.Builder, EditContactActivity> {
AddContactTask(EditContactActivity target)963         public AddContactTask(EditContactActivity target) {
964             super(target);
965         }
966 
967         @Override
doInBackground(final EditContactActivity target, Void... params)968         protected AlertDialog.Builder doInBackground(final EditContactActivity target,
969                 Void... params) {
970             final Sources sources = Sources.getInstance(target);
971 
972             // Wrap our context to inflate list items using correct theme
973             final Context dialogContext = new ContextThemeWrapper(target, android.R.style.Theme_Light);
974             final LayoutInflater dialogInflater = (LayoutInflater)dialogContext
975                     .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
976 
977             final ArrayList<Account> writable = sources.getAccounts(true);
978 
979             // No Accounts available.  Create a phone-local contact.
980             if (writable.isEmpty()) {
981                 selectAccount(null);
982                 return null;  // Don't show a dialog.
983             }
984 
985             // In the common case of a single account being writable, auto-select
986             // it without showing a dialog.
987             if (writable.size() == 1) {
988                 selectAccount(writable.get(0));
989                 return null;  // Don't show a dialog.
990             }
991 
992             final ArrayAdapter<Account> accountAdapter = new ArrayAdapter<Account>(target,
993                     android.R.layout.simple_list_item_2, writable) {
994                 @Override
995                 public View getView(int position, View convertView, ViewGroup parent) {
996                     if (convertView == null) {
997                         convertView = dialogInflater.inflate(android.R.layout.simple_list_item_2,
998                                 parent, false);
999                     }
1000 
1001                     // TODO: show icon along with title
1002                     final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
1003                     final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
1004 
1005                     final Account account = this.getItem(position);
1006                     final ContactsSource source = sources.getInflatedSource(account.type,
1007                             ContactsSource.LEVEL_SUMMARY);
1008 
1009                     text1.setText(account.name);
1010                     text2.setText(source.getDisplayLabel(target));
1011 
1012                     return convertView;
1013                 }
1014             };
1015 
1016             final DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() {
1017                 public void onClick(DialogInterface dialog, int which) {
1018                     dialog.dismiss();
1019 
1020                     // Create new contact based on selected source
1021                     final Account account = accountAdapter.getItem(which);
1022                     selectAccount(account);
1023 
1024                     // Update the UI.
1025                     EditContactActivity target = mTarget.get();
1026                     if (target != null) {
1027                         target.bindEditors();
1028                     }
1029                 }
1030             };
1031 
1032             final DialogInterface.OnCancelListener cancelListener = new DialogInterface.OnCancelListener() {
1033                 public void onCancel(DialogInterface dialog) {
1034                     // If nothing remains, close activity
1035                     if (!target.hasValidState()) {
1036                         target.finish();
1037                     }
1038                 }
1039             };
1040 
1041             // TODO: when canceled and was single add, finish()
1042             final AlertDialog.Builder builder = new AlertDialog.Builder(target);
1043             builder.setTitle(R.string.dialog_new_contact_account);
1044             builder.setSingleChoiceItems(accountAdapter, 0, clickListener);
1045             builder.setOnCancelListener(cancelListener);
1046             return builder;
1047         }
1048 
1049         /**
1050          * Sets up EditContactActivity's mState for the account selected.
1051          * Runs from a background thread.
1052          *
1053          * @param account may be null to signal a device-local contact should
1054          *     be created.
1055          */
selectAccount(Account account)1056         private void selectAccount(Account account) {
1057             EditContactActivity target = mTarget.get();
1058             if (target == null) {
1059                 return;
1060             }
1061             final Sources sources = Sources.getInstance(target);
1062             final ContentValues values = new ContentValues();
1063             if (account != null) {
1064                 values.put(RawContacts.ACCOUNT_NAME, account.name);
1065                 values.put(RawContacts.ACCOUNT_TYPE, account.type);
1066             } else {
1067                 values.putNull(RawContacts.ACCOUNT_NAME);
1068                 values.putNull(RawContacts.ACCOUNT_TYPE);
1069             }
1070 
1071             // Parse any values from incoming intent
1072             final EntityDelta insert = new EntityDelta(ValuesDelta.fromAfter(values));
1073             final ContactsSource source = sources.getInflatedSource(
1074                 account != null ? account.type : null,
1075                 ContactsSource.LEVEL_CONSTRAINTS);
1076             final Bundle extras = target.getIntent().getExtras();
1077             EntityModifier.parseExtras(target, source, insert, extras);
1078 
1079             // Ensure we have some default fields
1080             EntityModifier.ensureKindExists(insert, source, Phone.CONTENT_ITEM_TYPE);
1081             EntityModifier.ensureKindExists(insert, source, Email.CONTENT_ITEM_TYPE);
1082 
1083             // Create "My Contacts" membership for Google contacts
1084             // TODO: move this off into "templates" for each given source
1085             if (GoogleSource.ACCOUNT_TYPE.equals(source.accountType)) {
1086                 GoogleSource.attemptMyContactsMembership(insert, target);
1087             }
1088 
1089 	    // TODO: no synchronization here on target.mState.  This
1090 	    // runs in the background thread, but it's accessed from
1091 	    // multiple thread, including the UI thread.
1092             if (target.mState == null) {
1093                 // Create state if none exists yet
1094                 target.mState = EntitySet.fromSingle(insert);
1095             } else {
1096                 // Add contact onto end of existing state
1097                 target.mState.add(insert);
1098             }
1099         }
1100 
1101         @Override
onPostExecute(EditContactActivity target, AlertDialog.Builder result)1102         protected void onPostExecute(EditContactActivity target, AlertDialog.Builder result) {
1103             if (result != null) {
1104                 // Note: null is returned when no dialog is to be
1105                 // shown (no multiple accounts to select between)
1106                 target.showAndManageDialog(result.create());
1107             } else {
1108                 // Account was auto-selected on the background thread,
1109                 // but we need to update the UI still in the
1110                 // now-current UI thread.
1111                 target.bindEditors();
1112             }
1113         }
1114     }
1115 
1116 
1117 
createDeleteDialog()1118     private Dialog createDeleteDialog() {
1119         final AlertDialog.Builder builder = new AlertDialog.Builder(this);
1120         builder.setTitle(R.string.deleteConfirmation_title);
1121         builder.setIcon(android.R.drawable.ic_dialog_alert);
1122         builder.setMessage(R.string.deleteConfirmation);
1123         builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
1124             public void onClick(DialogInterface dialog, int which) {
1125                 // Mark all raw contacts for deletion
1126                 for (EntityDelta delta : mState) {
1127                     delta.markDeleted();
1128                 }
1129 
1130                 // Save the deletes
1131                 doSaveAction(SAVE_MODE_DEFAULT);
1132                 finish();
1133             }
1134         });
1135         builder.setNegativeButton(android.R.string.cancel, null);
1136         builder.setCancelable(false);
1137         return builder.create();
1138     }
1139 
1140     /**
1141      * Create dialog for selecting primary display name.
1142      */
createNameDialog()1143     private Dialog createNameDialog() {
1144         // Build set of all available display names
1145         final ArrayList<ValuesDelta> allNames = Lists.newArrayList();
1146         for (EntityDelta entity : mState) {
1147             final ArrayList<ValuesDelta> displayNames = entity
1148                     .getMimeEntries(StructuredName.CONTENT_ITEM_TYPE);
1149             allNames.addAll(displayNames);
1150         }
1151 
1152         // Wrap our context to inflate list items using correct theme
1153         final Context dialogContext = new ContextThemeWrapper(this, android.R.style.Theme_Light);
1154         final LayoutInflater dialogInflater = this.getLayoutInflater()
1155                 .cloneInContext(dialogContext);
1156 
1157         final ListAdapter nameAdapter = new ArrayAdapter<ValuesDelta>(this,
1158                 android.R.layout.simple_list_item_1, allNames) {
1159             @Override
1160             public View getView(int position, View convertView, ViewGroup parent) {
1161                 if (convertView == null) {
1162                     convertView = dialogInflater.inflate(android.R.layout.simple_list_item_1,
1163                             parent, false);
1164                 }
1165 
1166                 final ValuesDelta structuredName = this.getItem(position);
1167                 final String displayName = structuredName.getAsString(StructuredName.DISPLAY_NAME);
1168 
1169                 ((TextView)convertView).setText(displayName);
1170 
1171                 return convertView;
1172             }
1173         };
1174 
1175         final DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() {
1176             public void onClick(DialogInterface dialog, int which) {
1177                 dialog.dismiss();
1178 
1179                 // User picked display name, so make super-primary
1180                 final ValuesDelta structuredName = allNames.get(which);
1181                 structuredName.put(Data.IS_PRIMARY, 1);
1182                 structuredName.put(Data.IS_SUPER_PRIMARY, 1);
1183             }
1184         };
1185 
1186         final AlertDialog.Builder builder = new AlertDialog.Builder(this);
1187         builder.setTitle(R.string.dialog_primary_name);
1188         builder.setSingleChoiceItems(nameAdapter, 0, clickListener);
1189         return builder.create();
1190     }
1191 
1192     /**
1193      * Compare EntityDeltas for sorting the stack of editors.
1194      */
compare(EntityDelta one, EntityDelta two)1195     public int compare(EntityDelta one, EntityDelta two) {
1196         // Check direct equality
1197         if (one.equals(two)) {
1198             return 0;
1199         }
1200 
1201         final Sources sources = Sources.getInstance(this);
1202         String accountType = one.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
1203         final ContactsSource oneSource = sources.getInflatedSource(accountType,
1204                 ContactsSource.LEVEL_SUMMARY);
1205         accountType = two.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
1206         final ContactsSource twoSource = sources.getInflatedSource(accountType,
1207                 ContactsSource.LEVEL_SUMMARY);
1208 
1209         // Check read-only
1210         if (oneSource.readOnly && !twoSource.readOnly) {
1211             return 1;
1212         } else if (twoSource.readOnly && !oneSource.readOnly) {
1213             return -1;
1214         }
1215 
1216         // Check account type
1217         boolean skipAccountTypeCheck = false;
1218         boolean oneIsGoogle = oneSource instanceof GoogleSource;
1219         boolean twoIsGoogle = twoSource instanceof GoogleSource;
1220         if (oneIsGoogle && !twoIsGoogle) {
1221             return -1;
1222         } else if (twoIsGoogle && !oneIsGoogle) {
1223             return 1;
1224         } else {
1225             skipAccountTypeCheck = true;
1226         }
1227 
1228         int value;
1229         if (!skipAccountTypeCheck) {
1230             value = oneSource.accountType.compareTo(twoSource.accountType);
1231             if (value != 0) {
1232                 return value;
1233             }
1234         }
1235 
1236         // Check account name
1237         ValuesDelta oneValues = one.getValues();
1238         String oneAccount = oneValues.getAsString(RawContacts.ACCOUNT_NAME);
1239         if (oneAccount == null) oneAccount = "";
1240         ValuesDelta twoValues = two.getValues();
1241         String twoAccount = twoValues.getAsString(RawContacts.ACCOUNT_NAME);
1242         if (twoAccount == null) twoAccount = "";
1243         value = oneAccount.compareTo(twoAccount);
1244         if (value != 0) {
1245             return value;
1246         }
1247 
1248         // Both are in the same account, fall back to contact ID
1249         long oneId = oneValues.getAsLong(RawContacts._ID);
1250         long twoId = twoValues.getAsLong(RawContacts._ID);
1251         return (int)(oneId - twoId);
1252     }
1253 }
1254