Merge "Move "Linked contacts" and "Saving to" text in editor." into ub-contactsdialer-a-dev
diff --git a/res/menu/edit_contact.xml b/res/menu/edit_contact.xml
index 2ff5f81..256edb6 100644
--- a/res/menu/edit_contact.xml
+++ b/res/menu/edit_contact.xml
@@ -30,11 +30,6 @@
         android:title="@string/menu_joinAggregate" />
 
     <item
-        android:id="@+id/menu_discard"
-        android:alphabeticShortcut="q"
-        android:title="@string/menu_discard" />
-
-    <item
         android:id="@+id/menu_delete"
         android:title="@string/menu_deleteContact" />
 
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 7a51cb5..3f721b3 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -207,9 +207,6 @@
     <!-- Confirmation dialog contents after users selects to delete a Writable contact. -->
     <string name="deleteConfirmation">This contact will be deleted.</string>
 
-    <!-- Menu item to indicate you want to stop editing a contact and NOT save the changes you've made [CHAR LIMIT=30] -->
-    <string name="menu_discard">Discard changes</string>
-
     <!-- Message displayed in a toast when you try to view the details of a contact that
          for some reason doesn't exist anymore. [CHAR LIMIT=NONE]-->
     <string name="invalidContactMessage">The contact doesn\'t exist.</string>
@@ -278,6 +275,15 @@
     <!-- Toast displayed when saving a contact failed. [CHAR LIMIT=NONE] -->
     <string name="contactSavedErrorToast">Couldn\'t save contact changes.</string>
 
+    <!-- Toast displayed when unlinking a contact failed. [CHAR LIMIT=NONE] -->
+    <string name="contactUnlinkErrorToast">Couldn\'t unlink contact.</string>
+
+    <!-- Toast displayed when joining a contact failed. [CHAR LIMIT=NONE] -->
+    <string name="contactJoinErrorToast">Couldn\'t join contact.</string>
+
+    <!-- Generic error default clause displayed when saving a contact failed. [CHAR LIMIT=NONE] -->
+    <string name="contactGenericErrorToast">Error saving contact.</string>
+
     <!-- Toast displayed when saving a contact photo failed. [CHAR LIMIT=NONE] -->
     <string name="contactPhotoSavedErrorToast">Couldn\'t save contact photo changes.</string>
 
diff --git a/src/com/android/contacts/editor/AggregationSuggestionEngine.java b/src/com/android/contacts/editor/AggregationSuggestionEngine.java
index 14da019..4423a61 100644
--- a/src/com/android/contacts/editor/AggregationSuggestionEngine.java
+++ b/src/com/android/contacts/editor/AggregationSuggestionEngine.java
@@ -25,6 +25,7 @@
 import android.os.HandlerThread;
 import android.os.Message;
 import android.os.Process;
+import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.Nickname;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
@@ -115,6 +116,7 @@
     private ContentObserver mContentObserver;
     private Uri mSuggestionsUri;
     private int mSuggestionsLimit = 3;
+    private boolean mPruneInvisibleContacts;
 
     public AggregationSuggestionEngine(Context context) {
         super("AggregationSuggestions", Process.THREAD_PRIORITY_BACKGROUND);
@@ -150,6 +152,10 @@
         mSuggestionsLimit = suggestionsLimit;
     }
 
+    public void setPruneInvisibleContacts (boolean pruneInvisibleContacts) {
+        mPruneInvisibleContacts = pruneInvisibleContacts;
+    }
+
     public void setListener(Listener listener) {
         mListener = listener;
     }
@@ -377,6 +383,27 @@
     }
 
     public List<Suggestion> getSuggestions() {
+        final ArrayList<Long> visibleContacts = new ArrayList<>();
+        if (mPruneInvisibleContacts) {
+            final Uri contactFilterUri = Data.CONTENT_URI.buildUpon()
+                    .appendQueryParameter(Data.VISIBLE_CONTACTS_ONLY, "true")
+                    .build();
+            final ContentResolver contentResolver = mContext.getContentResolver();
+            final Cursor contactCursor = contentResolver.query(contactFilterUri,
+                    new String[]{Data.CONTACT_ID}, null, null, null);
+            try {
+                if (contactCursor != null) {
+                    while (contactCursor.moveToNext()) {
+                        final long contactId = contactCursor.getLong(0);
+                        visibleContacts.add(contactId);
+                    }
+                }
+            } finally {
+                contactCursor.close();
+            }
+
+        }
+
         ArrayList<Suggestion> list = Lists.newArrayList();
         if (mDataCursor != null) {
             Suggestion suggestion = null;
@@ -384,6 +411,9 @@
             mDataCursor.moveToPosition(-1);
             while (mDataCursor.moveToNext()) {
                 long contactId = mDataCursor.getLong(DataQuery.CONTACT_ID);
+                if (mPruneInvisibleContacts && !visibleContacts.contains(contactId)) {
+                    continue;
+                }
                 if (contactId != currentContactId) {
                     suggestion = new Suggestion();
                     suggestion.contactId = contactId;
diff --git a/src/com/android/contacts/editor/CompactContactEditorFragment.java b/src/com/android/contacts/editor/CompactContactEditorFragment.java
index 0ca8d35..9698ada 100644
--- a/src/com/android/contacts/editor/CompactContactEditorFragment.java
+++ b/src/com/android/contacts/editor/CompactContactEditorFragment.java
@@ -19,6 +19,7 @@
 import com.android.contacts.ContactSaveService;
 import com.android.contacts.R;
 import com.android.contacts.activities.CompactContactEditorActivity;
+import com.android.contacts.activities.ContactEditorBaseActivity;
 import com.android.contacts.common.model.RawContactDelta;
 import com.android.contacts.common.model.ValuesDelta;
 import com.android.contacts.common.model.account.AccountWithDataSet;
@@ -161,8 +162,27 @@
                 SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(),
                 ((Activity) mContext).getClass(),
                 CompactContactEditorActivity.ACTION_SAVE_COMPLETED, mUpdatedPhotos);
-        mContext.startService(intent);
-
+        try {
+            mContext.startService(intent);
+        } catch (Exception exception) {
+            final int resId;
+            switch (saveMode) {
+                case ContactEditorBaseActivity.ContactEditor.SaveMode.SPLIT:
+                    resId = R.string.contactUnlinkErrorToast;
+                    break;
+                case ContactEditorBaseActivity.ContactEditor.SaveMode.RELOAD:
+                    resId = R.string.contactJoinErrorToast;
+                    break;
+                case ContactEditorBaseActivity.ContactEditor.SaveMode.CLOSE:
+                    resId = R.string.contactSavedErrorToast;
+                    break;
+                default:
+                    resId = R.string.contactGenericErrorToast;
+            }
+            Toast.makeText(mContext, resId, Toast.LENGTH_SHORT).show();
+            onCancelEditConfirmed();
+            return false;
+        }
         return true;
     }
 
@@ -229,7 +249,7 @@
 
     @Override
     public void onPhotoEditorViewClicked() {
-        if (isMultiAccountContact()) {
+        if (isEditingMultipleRawContacts()) {
             final ArrayList<CompactPhotoSelectionFragment.Photo> photos = getContent().getPhotos();
             if (photos.size() > 1) {
                 // For aggregate contacts, the user may select a new super primary photo from among
@@ -260,7 +280,7 @@
 
     private int getPhotoMode() {
         if (getContent().isWritablePhotoSet()) {
-            return isMultiAccountContact()
+            return isEditingMultipleRawContacts()
                     ? PhotoActionPopup.Modes.MULTIPLE_WRITE_ABLE_PHOTOS
                     : PhotoActionPopup.Modes.WRITE_ABLE_PHOTO;
         }
diff --git a/src/com/android/contacts/editor/CompactRawContactsEditorView.java b/src/com/android/contacts/editor/CompactRawContactsEditorView.java
index 1b2f60b..8a4d479 100644
--- a/src/com/android/contacts/editor/CompactRawContactsEditorView.java
+++ b/src/com/android/contacts/editor/CompactRawContactsEditorView.java
@@ -143,7 +143,7 @@
             mContext = context;
             mRawContactDeltas = new RawContactDeltaList();
             for (RawContactDelta rawContactDelta : rawContactDeltas) {
-                if (rawContactDelta.isVisible()) {
+                if (rawContactDelta.isVisible() && rawContactDelta.getRawContactId() > 0) {
                     mRawContactDeltas.add(rawContactDelta);
                 }
             }
@@ -749,7 +749,7 @@
                         rawContactDelta.getAccountName(),
                         rawContactDelta.getAccountType(mAccountTypeManager));
 
-        // Only one of the account header, selector, and raw contact selector should be shown
+        // Either the account header or selector should be shown, not both.
         final List<AccountWithDataSet> accounts =
                 AccountTypeManager.getInstance(getContext()).getAccounts(true);
         if (mHasNewContact && !mIsUserProfile) {
@@ -758,11 +758,21 @@
             } else {
                 addAccountHeader(accountInfo);
             }
-        } else if (rawContactDeltas.size() > 1) {
-            addRawContactAccountSelector(rawContactDeltas);
         } else {
             addAccountHeader(accountInfo);
         }
+
+        // The raw contact selector should only display linked raw contacts that can be edited in
+        // the full editor (i.e. they are not newly created raw contacts)
+        Collections.sort(rawContactDeltas, new RawContactDeltaComparator(getContext()));
+        final RawContactAccountListAdapter adapter =
+                new RawContactAccountListAdapter(getContext(), rawContactDeltas);
+        if (adapter.getCount() > 1) {
+            final String accountsSummary = getResources().getString(
+                    R.string.compact_editor_linked_contacts_selector_title,
+                    adapter.getCount());
+            addRawContactAccountSelector(accountsSummary, adapter);
+        }
     }
 
     private void addAccountHeader(Pair<String,String> accountInfo) {
@@ -847,21 +857,16 @@
         });
     }
 
-    private void addRawContactAccountSelector(final RawContactDeltaList rawContactDeltas) {
+    private void addRawContactAccountSelector(String accountsSummary,
+            final RawContactAccountListAdapter adapter) {
         mRawContactContainer.setVisibility(View.VISIBLE);
 
-        Collections.sort(rawContactDeltas, new RawContactDeltaComparator(getContext()));
-
-        final String accountsSummary = getResources().getString(
-                R.string.compact_editor_linked_contacts_selector_title, rawContactDeltas.size());
         mRawContactSummary.setText(accountsSummary);
 
         mRawContactContainer.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 final ListPopupWindow popup = new ListPopupWindow(getContext(), null);
-                final RawContactAccountListAdapter adapter =
-                        new RawContactAccountListAdapter(getContext(), rawContactDeltas);
                 popup.setWidth(mRawContactContainer.getWidth());
                 popup.setAnchorView(mRawContactContainer);
                 popup.setAdapter(adapter);
diff --git a/src/com/android/contacts/editor/ContactEditorBaseFragment.java b/src/com/android/contacts/editor/ContactEditorBaseFragment.java
index 718128b..b091249 100644
--- a/src/com/android/contacts/editor/ContactEditorBaseFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorBaseFragment.java
@@ -744,34 +744,21 @@
         final MenuItem splitMenu = menu.findItem(R.id.menu_split);
         final MenuItem joinMenu = menu.findItem(R.id.menu_join);
         final MenuItem helpMenu = menu.findItem(R.id.menu_help);
-        final MenuItem discardMenu = menu.findItem(R.id.menu_discard);
         final MenuItem sendToVoiceMailMenu = menu.findItem(R.id.menu_send_to_voicemail);
         final MenuItem ringToneMenu = menu.findItem(R.id.menu_set_ringtone);
         final MenuItem deleteMenu = menu.findItem(R.id.menu_delete);
 
         // Set visibility of menus
-        // Discard menu is only available if at least one raw contact is editable
-        discardMenu.setVisible(mState != null &&
-                mState.getFirstWritableRawContact(mContext) != null);
 
         // help menu depending on whether this is inserting or editing
         if (isInsert(mAction) || mRawContactIdToDisplayAlone != -1) {
             HelpUtils.prepareHelpMenuItem(mContext, helpMenu, R.string.help_url_people_add);
-            discardMenu.setVisible(false);
             splitMenu.setVisible(false);
             joinMenu.setVisible(false);
             deleteMenu.setVisible(false);
         } else if (isEdit(mAction)) {
             HelpUtils.prepareHelpMenuItem(mContext, helpMenu, R.string.help_url_people_edit);
-            // Split only if there is more than one raw contact, it is not a user-profile, and
-            // splitting won't result in an empty contact. For the empty contact case, we only guard
-            // against this when there is a single read-only contact in the aggregate.  If the user
-            // has joined >1 read-only contacts together, we allow them to split it,
-            // even if they have never added their own information and splitting will create a
-            // name only contact.
-            final boolean isSingleReadOnlyContact = mHasNewContact && mState.size() == 2;
-            splitMenu.setVisible(isMultiAccountContact() && !isEditingUserProfile()
-                    && !isSingleReadOnlyContact);
+            splitMenu.setVisible(canUnlinkRawContacts());
             // Cannot join a user profile
             joinMenu.setVisible(!isEditingUserProfile());
             deleteMenu.setVisible(!mDisableDeleteMenuOption && !isEditingUserProfile());
@@ -783,7 +770,7 @@
         // Save menu is invisible when there's only one read only contact in the editor.
         saveMenu.setVisible(!mRawContactDisplayAloneIsReadOnly);
 
-        if (mRawContactIdToDisplayAlone != -1) {
+        if (mRawContactIdToDisplayAlone != -1 || mIsUserProfile) {
             sendToVoiceMailMenu.setVisible(false);
             ringToneMenu.setVisible(false);
         } else {
@@ -812,8 +799,6 @@
         switch (item.getItemId()) {
             case R.id.menu_save:
                 return save(SaveMode.CLOSE);
-            case R.id.menu_discard:
-                return revert();
             case R.id.menu_delete:
                 if (mListener != null) mListener.onDeleteRequested(mLookupUri);
                 return true;
@@ -973,11 +958,23 @@
         return mNewLocalProfile || mIsUserProfile;
     }
 
-    protected boolean isMultiAccountContact() {
+    /**
+     * Whether the contact being edited spans multiple raw contacts.
+     * The may also span multiple accounts.
+     */
+    public boolean isEditingMultipleRawContacts() {
         return mState.size() > 1;
     }
 
     /**
+     * Whether the contact being edited is composed of a single read-only raw contact
+     * aggregated with a newly created writable raw contact.
+     */
+    protected boolean isEditingReadOnlyRawContactWithNewContact() {
+        return mHasNewContact && mState.size() == 2;
+    }
+
+    /**
      * Return true if there are any edits to the current contact which need to
      * be saved.
      */
@@ -987,6 +984,19 @@
     }
 
     /**
+     * We allow unlinking only if there is more than one raw contact, it is not a user-profile,
+     * and unlinking won't result in an empty contact.  For the empty contact case, we only guard
+     * against this when there is a single read-only contact in the aggregate.  If the user
+     * has joined >1 read-only contacts together, we allow them to unlink it, even if they have
+     * never added their own information and unlinking will create a name only contact.
+     */
+    protected boolean canUnlinkRawContacts() {
+        return isEditingMultipleRawContacts()
+                && !isEditingUserProfile()
+                && !isEditingReadOnlyRawContactWithNewContact();
+    }
+
+    /**
      * Determines if changes were made in the editor that need to be saved, while taking into
      * account that name changes are not real for read-only contacts.
      * See go/editing-read-only-contacts
diff --git a/src/com/android/contacts/group/GroupEditorFragment.java b/src/com/android/contacts/group/GroupEditorFragment.java
index eda5d4f..1104c02 100644
--- a/src/com/android/contacts/group/GroupEditorFragment.java
+++ b/src/com/android/contacts/group/GroupEditorFragment.java
@@ -514,24 +514,6 @@
         inflater.inflate(R.menu.edit_group, menu);
     }
 
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        switch (item.getItemId()) {
-            case R.id.menu_discard:
-                return revert();
-        }
-        return false;
-    }
-
-    private boolean revert() {
-        if (!hasNameChange() && !hasMembershipChange()) {
-            doRevertAction();
-        } else {
-            CancelEditDialogFragment.show(this);
-        }
-        return true;
-    }
-
     private void doRevertAction() {
         // When this Fragment is closed we don't want it to auto-save
         mStatus = Status.CLOSING;
diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index d501e77..bc2ef53 100644
--- a/src/com/android/contacts/quickcontact/QuickContactActivity.java
+++ b/src/com/android/contacts/quickcontact/QuickContactActivity.java
@@ -1316,6 +1316,7 @@
             mAggregationSuggestionEngine.setListener(this);
             mAggregationSuggestionEngine.setSuggestionsLimit(getResources().getInteger(
                     R.integer.quickcontact_suggestions_limit));
+            mAggregationSuggestionEngine.setPruneInvisibleContacts(true);
             mAggregationSuggestionEngine.start();
         }