• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2 
3  * Copyright (C) 2009 The Android Open Source Project
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.contacts.quickcontact;
19 
20 import android.accounts.Account;
21 import android.animation.ArgbEvaluator;
22 import android.animation.ObjectAnimator;
23 import android.app.Activity;
24 import android.app.LoaderManager.LoaderCallbacks;
25 import android.app.ProgressDialog;
26 import android.app.SearchManager;
27 import android.content.ActivityNotFoundException;
28 import android.content.BroadcastReceiver;
29 import android.content.ContentUris;
30 import android.content.ContentValues;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.content.IntentFilter;
34 import android.content.Loader;
35 import android.content.pm.PackageManager;
36 import android.content.pm.ResolveInfo;
37 import android.content.pm.ShortcutInfo;
38 import android.content.pm.ShortcutManager;
39 import android.content.res.Resources;
40 import android.graphics.Bitmap;
41 import android.graphics.BitmapFactory;
42 import android.graphics.Color;
43 import android.graphics.PorterDuff;
44 import android.graphics.PorterDuffColorFilter;
45 import android.graphics.drawable.BitmapDrawable;
46 import android.graphics.drawable.ColorDrawable;
47 import android.graphics.drawable.Drawable;
48 import android.icu.text.MessageFormat;
49 import android.media.RingtoneManager;
50 import android.net.Uri;
51 import android.os.AsyncTask;
52 import android.os.Build;
53 import android.os.Bundle;
54 import android.os.Trace;
55 import android.provider.CalendarContract;
56 import android.provider.ContactsContract.CommonDataKinds.Email;
57 import android.provider.ContactsContract.CommonDataKinds.Event;
58 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
59 import android.provider.ContactsContract.CommonDataKinds.Identity;
60 import android.provider.ContactsContract.CommonDataKinds.Im;
61 import android.provider.ContactsContract.CommonDataKinds.Nickname;
62 import android.provider.ContactsContract.CommonDataKinds.Note;
63 import android.provider.ContactsContract.CommonDataKinds.Organization;
64 import android.provider.ContactsContract.CommonDataKinds.Phone;
65 import android.provider.ContactsContract.CommonDataKinds.Relation;
66 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
67 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
68 import android.provider.ContactsContract.CommonDataKinds.Website;
69 import android.provider.ContactsContract.Contacts;
70 import android.provider.ContactsContract.Data;
71 import android.provider.ContactsContract.Directory;
72 import android.provider.ContactsContract.DisplayNameSources;
73 import android.provider.ContactsContract.Intents;
74 import android.provider.ContactsContract.QuickContact;
75 import android.provider.ContactsContract.RawContacts;
76 import android.telecom.PhoneAccount;
77 import android.telecom.TelecomManager;
78 import android.text.BidiFormatter;
79 import android.text.Spannable;
80 import android.text.SpannableString;
81 import android.text.TextDirectionHeuristics;
82 import android.text.TextUtils;
83 import android.util.Log;
84 import android.view.ContextMenu;
85 import android.view.ContextMenu.ContextMenuInfo;
86 import android.view.Menu;
87 import android.view.MenuInflater;
88 import android.view.MenuItem;
89 import android.view.MotionEvent;
90 import android.view.View;
91 import android.view.View.OnClickListener;
92 import android.view.View.OnCreateContextMenuListener;
93 import android.view.WindowManager;
94 import android.widget.Toast;
95 import android.widget.Toolbar;
96 import androidx.core.content.res.ResourcesCompat;
97 import androidx.core.os.BuildCompat;
98 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
99 import androidx.palette.graphics.Palette;
100 import com.android.contacts.CallUtil;
101 import com.android.contacts.ClipboardUtils;
102 import com.android.contacts.Collapser;
103 import com.android.contacts.ContactSaveService;
104 import com.android.contacts.ContactsActivity;
105 import com.android.contacts.ContactsUtils;
106 import com.android.contacts.DynamicShortcuts;
107 import com.android.contacts.NfcHandler;
108 import com.android.contacts.R;
109 import com.android.contacts.ShortcutIntentBuilder;
110 import com.android.contacts.ShortcutIntentBuilder.OnShortcutIntentCreatedListener;
111 import com.android.contacts.activities.ContactEditorActivity;
112 import com.android.contacts.activities.ContactSelectionActivity;
113 import com.android.contacts.activities.RequestPermissionsActivity;
114 import com.android.contacts.compat.CompatUtils;
115 import com.android.contacts.compat.EventCompat;
116 import com.android.contacts.compat.MultiWindowCompat;
117 import com.android.contacts.detail.ContactDisplayUtils;
118 import com.android.contacts.dialog.CallSubjectDialog;
119 import com.android.contacts.editor.ContactEditorFragment;
120 import com.android.contacts.editor.EditorIntents;
121 import com.android.contacts.editor.EditorUiUtils;
122 import com.android.contacts.interactions.ContactDeletionInteraction;
123 import com.android.contacts.interactions.TouchPointManager;
124 import com.android.contacts.lettertiles.LetterTileDrawable;
125 import com.android.contacts.list.UiIntentActions;
126 import com.android.contacts.logging.Logger;
127 import com.android.contacts.logging.QuickContactEvent.ActionType;
128 import com.android.contacts.logging.QuickContactEvent.CardType;
129 import com.android.contacts.logging.QuickContactEvent.ContactType;
130 import com.android.contacts.logging.ScreenEvent.ScreenType;
131 import com.android.contacts.model.AccountTypeManager;
132 import com.android.contacts.model.Contact;
133 import com.android.contacts.model.ContactLoader;
134 import com.android.contacts.model.RawContact;
135 import com.android.contacts.model.account.AccountType;
136 import com.android.contacts.model.dataitem.CustomDataItem;
137 import com.android.contacts.model.dataitem.DataItem;
138 import com.android.contacts.model.dataitem.DataKind;
139 import com.android.contacts.model.dataitem.EmailDataItem;
140 import com.android.contacts.model.dataitem.EventDataItem;
141 import com.android.contacts.model.dataitem.ImDataItem;
142 import com.android.contacts.model.dataitem.NicknameDataItem;
143 import com.android.contacts.model.dataitem.NoteDataItem;
144 import com.android.contacts.model.dataitem.OrganizationDataItem;
145 import com.android.contacts.model.dataitem.PhoneDataItem;
146 import com.android.contacts.model.dataitem.RelationDataItem;
147 import com.android.contacts.model.dataitem.SipAddressDataItem;
148 import com.android.contacts.model.dataitem.StructuredNameDataItem;
149 import com.android.contacts.model.dataitem.StructuredPostalDataItem;
150 import com.android.contacts.model.dataitem.WebsiteDataItem;
151 import com.android.contacts.quickcontact.ExpandingEntryCardView.Entry;
152 import com.android.contacts.quickcontact.ExpandingEntryCardView.EntryContextMenuInfo;
153 import com.android.contacts.quickcontact.ExpandingEntryCardView.EntryTag;
154 import com.android.contacts.quickcontact.ExpandingEntryCardView.ExpandingEntryCardViewListener;
155 import com.android.contacts.quickcontact.WebAddress.ParseException;
156 import com.android.contacts.util.DateUtils;
157 import com.android.contacts.util.ImageViewDrawableSetter;
158 import com.android.contacts.util.ImplicitIntentsUtil;
159 import com.android.contacts.util.MaterialColorMapUtils;
160 import com.android.contacts.util.MaterialColorMapUtils.MaterialPalette;
161 import com.android.contacts.util.PhoneCapabilityTester;
162 import com.android.contacts.util.SchedulingUtils;
163 import com.android.contacts.util.SharedPreferenceUtil;
164 import com.android.contacts.util.StructuredPostalUtils;
165 import com.android.contacts.util.UriUtils;
166 import com.android.contacts.util.ViewUtil;
167 import com.android.contacts.widget.MultiShrinkScroller;
168 import com.android.contacts.widget.MultiShrinkScroller.MultiShrinkScrollerListener;
169 import com.android.contacts.widget.QuickContactImageView;
170 import com.android.contactsbind.HelpUtils;
171 import com.google.common.collect.Lists;
172 import java.util.ArrayList;
173 import java.util.Calendar;
174 import java.util.Collections;
175 import java.util.Comparator;
176 import java.util.Date;
177 import java.util.HashMap;
178 import java.util.List;
179 import java.util.Locale;
180 import java.util.Map;
181 
182 /**
183  * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads
184  * data asynchronously, and then shows a popup with details centered around
185  * {@link Intent#getSourceBounds()}.
186  */
187 public class QuickContactActivity extends ContactsActivity {
188 
189     /**
190      * QuickContacts immediately takes up the full screen. All possible information is shown.
191      * This value for {@link android.provider.ContactsContract.QuickContact#EXTRA_MODE}
192      * should only be used by the Contacts app.
193      */
194     public static final int MODE_FULLY_EXPANDED = 4;
195 
196     /** Used to pass the screen where the user came before launching this Activity. */
197     public static final String EXTRA_PREVIOUS_SCREEN_TYPE = "previous_screen_type";
198     /** Used to pass the Contact card action. */
199     public static final String EXTRA_ACTION_TYPE = "action_type";
200     public static final String EXTRA_THIRD_PARTY_ACTION = "third_party_action";
201 
202     /** Used to tell the QuickContact that the previous contact was edited, so it can return an
203      * activity result back to the original Activity that launched it. */
204     public static final String EXTRA_CONTACT_EDITED = "contact_edited";
205 
206     private static final String TAG = "QuickContact";
207 
208     private static final String KEY_THEME_COLOR = "theme_color";
209     private static final String KEY_PREVIOUS_CONTACT_ID = "previous_contact_id";
210 
211     private static final String KEY_SEND_TO_VOICE_MAIL_STATE = "sendToVoicemailState";
212     private static final String KEY_ARE_PHONE_OPTIONS_CHANGEABLE = "arePhoneOptionsChangable";
213     private static final String KEY_CUSTOM_RINGTONE = "customRingtone";
214 
215     private static final int ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION = 150;
216     private static final int REQUEST_CODE_CONTACT_EDITOR_ACTIVITY = 1;
217     private static final int SCRIM_COLOR = Color.argb(0xC8, 0, 0, 0);
218     private static final int REQUEST_CODE_CONTACT_SELECTION_ACTIVITY = 2;
219     private static final String MIMETYPE_SMS = "vnd.android-dir/mms-sms";
220     private static final int REQUEST_CODE_JOIN = 3;
221     private static final int REQUEST_CODE_PICK_RINGTONE = 4;
222     private static final int CARD_ENTRY_ID_EDIT_CONTACT = -2;
223     private static final int MIN_NUM_CONTACT_ENTRIES_SHOWN = 3;
224 
225     private static final int CURRENT_API_VERSION = android.os.Build.VERSION.SDK_INT;
226 
227     /** This is the Intent action to install a shortcut in the launcher. */
228     private static final String ACTION_INSTALL_SHORTCUT =
229             "com.android.launcher.action.INSTALL_SHORTCUT";
230 
231     public static final String ACTION_SPLIT_COMPLETED = "splitCompleted";
232 
233     // Phone specific option menu items
234     private boolean mSendToVoicemailState;
235     private boolean mArePhoneOptionsChangable;
236     private String mCustomRingtone;
237 
238     @SuppressWarnings("deprecation")
239     private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY;
240 
241     public static final String MIMETYPE_TACHYON =
242             "vnd.android.cursor.item/com.google.android.apps.tachyon.phone";
243     private static final String TACHYON_CALL_ACTION =
244             "com.google.android.apps.tachyon.action.CALL";
245     private static final String MIMETYPE_GPLUS_PROFILE =
246             "vnd.android.cursor.item/vnd.googleplus.profile";
247     private static final String GPLUS_PROFILE_DATA_5_VIEW_PROFILE = "view";
248     private static final String MIMETYPE_HANGOUTS =
249             "vnd.android.cursor.item/vnd.googleplus.profile.comm";
250     private static final String HANGOUTS_DATA_5_VIDEO = "hangout";
251     private static final String HANGOUTS_DATA_5_MESSAGE = "conversation";
252     private static final String CALL_ORIGIN_QUICK_CONTACTS_ACTIVITY =
253             "com.android.contacts.quickcontact.QuickContactActivity";
254     private static final String KEY_LOADER_EXTRA_EMAILS =
255         QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_EMAILS";
256 
257     // Set true in {@link #onCreate} after orientation change for later use in processIntent().
258     private boolean mIsRecreatedInstance;
259     private boolean mShortcutUsageReported = false;
260 
261     private boolean mShouldLog;
262 
263     // Used to store and log the referrer package name and the contact type.
264     private String mReferrer;
265     private int mContactType;
266 
267     /**
268      * The URI used to load the the Contact. Once the contact is loaded, use Contact#getLookupUri()
269      * instead of referencing this URI.
270      */
271     private Uri mLookupUri;
272     private String[] mExcludeMimes;
273     private int mExtraMode;
274     private String mExtraPrioritizedMimeType;
275     private int mStatusBarColor;
276     private boolean mHasAlreadyBeenOpened;
277     private boolean mOnlyOnePhoneNumber;
278     private boolean mOnlyOneEmail;
279     private ProgressDialog mProgressDialog;
280     private SaveServiceListener mListener;
281 
282     private QuickContactImageView mPhotoView;
283     private ExpandingEntryCardView mContactCard;
284     private ExpandingEntryCardView mNoContactDetailsCard;
285     private ExpandingEntryCardView mAboutCard;
286 
287     private long mPreviousContactId = 0;
288 
289     private MultiShrinkScroller mScroller;
290     private AsyncTask<Void, Void, Cp2DataCardModel> mEntriesAndActionsTask;
291 
292     /**
293      * The last copy of Cp2DataCardModel that was passed to {@link #populateContactAndAboutCard}.
294      */
295     private Cp2DataCardModel mCachedCp2DataCardModel;
296     /**
297      *  This scrim's opacity is controlled in two different ways. 1) Before the initial entrance
298      *  animation finishes, the opacity is animated by a value animator. This is designed to
299      *  distract the user from the length of the initial loading time. 2) After the initial
300      *  entrance animation, the opacity is directly related to scroll position.
301      */
302     private ColorDrawable mWindowScrim;
303     private boolean mIsEntranceAnimationFinished;
304     private MaterialColorMapUtils mMaterialColorMapUtils;
305     private boolean mIsExitAnimationInProgress;
306     private boolean mHasComputedThemeColor;
307 
308     /**
309      * Used to stop the ExpandingEntry cards from adjusting between an entry click and the intent
310      * being launched.
311      */
312     private boolean mHasIntentLaunched;
313 
314     private Contact mContactData;
315     private ContactLoader mContactLoader;
316     private PorterDuffColorFilter mColorFilter;
317     private int mColorFilterColor;
318 
319     private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter();
320 
321     /**
322      * {@link #LEADING_MIMETYPES} is used to sort MIME-types.
323      *
324      * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog,
325      * in the order specified here.</p>
326      */
327     private static final List<String> LEADING_MIMETYPES = Lists.newArrayList(
328             Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE,
329             StructuredPostal.CONTENT_ITEM_TYPE);
330 
331     private static final List<String> SORTED_ABOUT_CARD_MIMETYPES = Lists.newArrayList(
332             Nickname.CONTENT_ITEM_TYPE,
333             // Phonetic name is inserted after nickname if it is available.
334             // No mimetype for phonetic name exists.
335             Website.CONTENT_ITEM_TYPE,
336             Organization.CONTENT_ITEM_TYPE,
337             Event.CONTENT_ITEM_TYPE,
338             Relation.CONTENT_ITEM_TYPE,
339             Im.CONTENT_ITEM_TYPE,
340             GroupMembership.CONTENT_ITEM_TYPE,
341             Identity.CONTENT_ITEM_TYPE,
342             CustomDataItem.MIMETYPE_CUSTOM_FIELD,
343             Note.CONTENT_ITEM_TYPE);
344 
345     private static final BidiFormatter sBidiFormatter = BidiFormatter.getInstance();
346 
347     /** Id for the background contact loader */
348     private static final int LOADER_CONTACT_ID = 0;
349 
350     private static final String KEY_LOADER_EXTRA_PHONES =
351             QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_PHONES";
352     private static final String KEY_LOADER_EXTRA_SIP_NUMBERS =
353             QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_SIP_NUMBERS";
354 
355     private static final String FRAGMENT_TAG_SELECT_ACCOUNT = "select_account_fragment";
356 
357     final OnClickListener mEntryClickHandler = new OnClickListener() {
358         @Override
359         public void onClick(View v) {
360             final Object entryTagObject = v.getTag();
361             if (entryTagObject == null || !(entryTagObject instanceof EntryTag)) {
362                 Log.w(TAG, "EntryTag was not used correctly");
363                 return;
364             }
365             final EntryTag entryTag = (EntryTag) entryTagObject;
366             final Intent intent = entryTag.getIntent();
367             final int dataId = entryTag.getId();
368 
369             if (dataId == CARD_ENTRY_ID_EDIT_CONTACT) {
370                 editContact();
371                 return;
372             }
373 
374             // Pass the touch point through the intent for use in the InCallUI
375             if (Intent.ACTION_CALL.equals(intent.getAction())) {
376                 if (TouchPointManager.getInstance().hasValidPoint()) {
377                     Bundle extras = new Bundle();
378                     extras.putParcelable(TouchPointManager.TOUCH_POINT,
379                             TouchPointManager.getInstance().getPoint());
380                     intent.putExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extras);
381                 }
382             }
383 
384             mHasIntentLaunched = true;
385             try {
386                 final int actionType = intent.getIntExtra(EXTRA_ACTION_TYPE,
387                         ActionType.UNKNOWN_ACTION);
388                 final String thirdPartyAction = intent.getStringExtra(EXTRA_THIRD_PARTY_ACTION);
389                 Logger.logQuickContactEvent(mReferrer, mContactType,
390                         CardType.UNKNOWN_CARD, actionType, thirdPartyAction);
391                 // For the tachyon call action, we need to use startActivityForResult and not
392                 // add FLAG_ACTIVITY_NEW_TASK to the intent.
393                 if (TACHYON_CALL_ACTION.equals(intent.getAction())) {
394                     QuickContactActivity.this.startActivityForResult(intent, /* requestCode */ 0);
395                 } else {
396                     intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
397                     ImplicitIntentsUtil.startActivityInAppIfPossible(QuickContactActivity.this,
398                             intent);
399                 }
400             } catch (SecurityException ex) {
401                 Toast.makeText(QuickContactActivity.this, R.string.missing_app,
402                         Toast.LENGTH_SHORT).show();
403                 Log.e(TAG, "QuickContacts does not have permission to launch "
404                         + intent);
405             } catch (ActivityNotFoundException ex) {
406                 Toast.makeText(QuickContactActivity.this, R.string.missing_app,
407                         Toast.LENGTH_SHORT).show();
408             }
409         }
410     };
411 
412     final ExpandingEntryCardViewListener mExpandingEntryCardViewListener
413             = new ExpandingEntryCardViewListener() {
414         @Override
415         public void onCollapse(int heightDelta) {
416             mScroller.prepareForShrinkingScrollChild(heightDelta);
417         }
418 
419         @Override
420         public void onExpand() {
421             mScroller.setDisableTouchesForSuppressLayout(/* areTouchesDisabled = */ true);
422         }
423 
424         @Override
425         public void onExpandDone() {
426             mScroller.setDisableTouchesForSuppressLayout(/* areTouchesDisabled = */ false);
427         }
428     };
429 
430     private interface ContextMenuIds {
431         static final int COPY_TEXT = 0;
432         static final int CLEAR_DEFAULT = 1;
433         static final int SET_DEFAULT = 2;
434     }
435 
436     private final OnCreateContextMenuListener mEntryContextMenuListener =
437             new OnCreateContextMenuListener() {
438         @Override
439         public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
440             if (menuInfo == null) {
441                 return;
442             }
443             final EntryContextMenuInfo info = (EntryContextMenuInfo) menuInfo;
444             menu.setHeaderTitle(info.getCopyText());
445             menu.add(ContextMenu.NONE, ContextMenuIds.COPY_TEXT,
446                     ContextMenu.NONE, getString(R.string.copy_text));
447 
448             // Don't allow setting or clearing of defaults for non-editable contacts
449             if (!isContactEditable()) {
450                 return;
451             }
452 
453             final String selectedMimeType = info.getMimeType();
454 
455             // Defaults to true will only enable the detail to be copied to the clipboard.
456             boolean onlyOneOfMimeType = true;
457 
458             // Only allow primary support for Phone and Email content types
459             if (Phone.CONTENT_ITEM_TYPE.equals(selectedMimeType)) {
460                 onlyOneOfMimeType = mOnlyOnePhoneNumber;
461             } else if (Email.CONTENT_ITEM_TYPE.equals(selectedMimeType)) {
462                 onlyOneOfMimeType = mOnlyOneEmail;
463             }
464 
465             // Checking for previously set default
466             if (info.isSuperPrimary()) {
467                 menu.add(ContextMenu.NONE, ContextMenuIds.CLEAR_DEFAULT,
468                         ContextMenu.NONE, getString(R.string.clear_default));
469             } else if (!onlyOneOfMimeType) {
470                 menu.add(ContextMenu.NONE, ContextMenuIds.SET_DEFAULT,
471                         ContextMenu.NONE, getString(R.string.set_default));
472             }
473         }
474     };
475 
476     @Override
onContextItemSelected(MenuItem item)477     public boolean onContextItemSelected(MenuItem item) {
478         EntryContextMenuInfo menuInfo;
479         try {
480             menuInfo = (EntryContextMenuInfo) item.getMenuInfo();
481         } catch (ClassCastException e) {
482             Log.e(TAG, "bad menuInfo", e);
483             return false;
484         }
485 
486         switch (item.getItemId()) {
487             case ContextMenuIds.COPY_TEXT:
488                 ClipboardUtils.copyText(this, menuInfo.getCopyLabel(), menuInfo.getCopyText(),
489                         true);
490                 return true;
491             case ContextMenuIds.SET_DEFAULT:
492                 final Intent setIntent = ContactSaveService.createSetSuperPrimaryIntent(this,
493                         menuInfo.getId());
494                 this.startService(setIntent);
495                 return true;
496             case ContextMenuIds.CLEAR_DEFAULT:
497                 final Intent clearIntent = ContactSaveService.createClearPrimaryIntent(this,
498                         menuInfo.getId());
499                 this.startService(clearIntent);
500                 return true;
501             default:
502                 throw new IllegalArgumentException("Unknown menu option " + item.getItemId());
503         }
504     }
505 
506     final MultiShrinkScrollerListener mMultiShrinkScrollerListener
507             = new MultiShrinkScrollerListener() {
508         @Override
509         public void onScrolledOffBottom() {
510             finish();
511         }
512 
513         @Override
514         public void onEnterFullscreen() {
515             updateStatusBarColor();
516         }
517 
518         @Override
519         public void onExitFullscreen() {
520             updateStatusBarColor();
521         }
522 
523         @Override
524         public void onStartScrollOffBottom() {
525             mIsExitAnimationInProgress = true;
526         }
527 
528         @Override
529         public void onEntranceAnimationDone() {
530             mIsEntranceAnimationFinished = true;
531         }
532 
533         @Override
534         public void onTransparentViewHeightChange(float ratio) {
535             if (mIsEntranceAnimationFinished) {
536                 mWindowScrim.setAlpha((int) (0xFF * ratio));
537             }
538         }
539     };
540 
541 
542     /**
543      * Data items are compared to the same mimetype based off of three qualities:
544      * 1. Super primary
545      * 2. Primary
546      */
547     private final Comparator<DataItem> mWithinMimeTypeDataItemComparator =
548             new Comparator<DataItem>() {
549         @Override
550         public int compare(DataItem lhs, DataItem rhs) {
551             if (!lhs.getMimeType().equals(rhs.getMimeType())) {
552                 Log.wtf(TAG, "Comparing DataItems with different mimetypes lhs.getMimeType(): " +
553                         lhs.getMimeType() + " rhs.getMimeType(): " + rhs.getMimeType());
554                 return 0;
555             }
556 
557             if (lhs.isSuperPrimary()) {
558                 return -1;
559             } else if (rhs.isSuperPrimary()) {
560                 return 1;
561             } else if (lhs.isPrimary() && !rhs.isPrimary()) {
562                 return -1;
563             } else if (!lhs.isPrimary() && rhs.isPrimary()) {
564                 return 1;
565             }
566             return 0;
567         }
568     };
569 
570     /**
571      * Sorts among different mimetypes based off:
572      * 1. Whether one of the mimetypes is the prioritized mimetype
573      * 2. Statically defined
574      */
575     private final Comparator<List<DataItem>> mAmongstMimeTypeDataItemComparator =
576             new Comparator<List<DataItem>> () {
577         @Override
578         public int compare(List<DataItem> lhsList, List<DataItem> rhsList) {
579             final DataItem lhs = lhsList.get(0);
580             final DataItem rhs = rhsList.get(0);
581             final String lhsMimeType = lhs.getMimeType();
582             final String rhsMimeType = rhs.getMimeType();
583 
584             // 1. Whether one of the mimetypes is the prioritized mimetype
585             if (!TextUtils.isEmpty(mExtraPrioritizedMimeType) && !lhsMimeType.equals(rhsMimeType)) {
586                 if (rhsMimeType.equals(mExtraPrioritizedMimeType)) {
587                     return 1;
588                 }
589                 if (lhsMimeType.equals(mExtraPrioritizedMimeType)) {
590                     return -1;
591                 }
592             }
593 
594             // 2. Resort to a statically defined mimetype order.
595             if (!lhsMimeType.equals(rhsMimeType)) {
596                 for (String mimeType : LEADING_MIMETYPES) {
597                     if (lhsMimeType.equals(mimeType)) {
598                         return -1;
599                     } else if (rhsMimeType.equals(mimeType)) {
600                         return 1;
601                     }
602                 }
603             }
604             return 0;
605         }
606     };
607 
608     @Override
dispatchTouchEvent(MotionEvent ev)609     public boolean dispatchTouchEvent(MotionEvent ev) {
610         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
611             TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY());
612         }
613         return super.dispatchTouchEvent(ev);
614     }
615 
616     @Override
onCreate(Bundle savedInstanceState)617     protected void onCreate(Bundle savedInstanceState) {
618         Trace.beginSection("onCreate()");
619         super.onCreate(savedInstanceState);
620 
621         if (RequestPermissionsActivity.startPermissionActivityIfNeeded(this)) {
622             return;
623         }
624 
625         mIsRecreatedInstance = savedInstanceState != null;
626         if (mIsRecreatedInstance) {
627             mPreviousContactId = savedInstanceState.getLong(KEY_PREVIOUS_CONTACT_ID);
628 
629             // Phone specific options menus
630             mSendToVoicemailState = savedInstanceState.getBoolean(KEY_SEND_TO_VOICE_MAIL_STATE);
631             mArePhoneOptionsChangable =
632                     savedInstanceState.getBoolean(KEY_ARE_PHONE_OPTIONS_CHANGEABLE);
633             mCustomRingtone = savedInstanceState.getString(KEY_CUSTOM_RINGTONE);
634         }
635         mProgressDialog = new ProgressDialog(this);
636         mProgressDialog.setIndeterminate(true);
637         mProgressDialog.setCancelable(false);
638 
639         mListener = new SaveServiceListener();
640         final IntentFilter intentFilter = new IntentFilter();
641         intentFilter.addAction(ContactSaveService.BROADCAST_LINK_COMPLETE);
642         intentFilter.addAction(ContactSaveService.BROADCAST_UNLINK_COMPLETE);
643         LocalBroadcastManager.getInstance(this).registerReceiver(mListener,
644                 intentFilter);
645 
646 
647         mShouldLog = true;
648 
649         final int previousScreenType = getIntent().getIntExtra
650                 (EXTRA_PREVIOUS_SCREEN_TYPE, ScreenType.UNKNOWN);
651         Logger.logScreenView(this, ScreenType.QUICK_CONTACT, previousScreenType);
652 
653         mReferrer = getCallingPackage();
654         if (mReferrer == null && CompatUtils.isLollipopMr1Compatible() && getReferrer() != null) {
655             mReferrer = getReferrer().getAuthority();
656         }
657         mContactType = ContactType.UNKNOWN_TYPE;
658 
659         if (CompatUtils.isLollipopCompatible()) {
660             getWindow().setStatusBarColor(Color.TRANSPARENT);
661         }
662 
663         processIntent(getIntent());
664 
665         // Show QuickContact in front of soft input
666         getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
667                 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
668 
669         setContentView(R.layout.quickcontact_activity);
670 
671         mMaterialColorMapUtils = new MaterialColorMapUtils(getResources());
672 
673         mScroller = (MultiShrinkScroller) findViewById(R.id.multiscroller);
674 
675         mContactCard = (ExpandingEntryCardView) findViewById(R.id.communication_card);
676         mNoContactDetailsCard = (ExpandingEntryCardView) findViewById(R.id.no_contact_data_card);
677         mAboutCard = (ExpandingEntryCardView) findViewById(R.id.about_card);
678 
679         mNoContactDetailsCard.setOnClickListener(mEntryClickHandler);
680         mContactCard.setOnClickListener(mEntryClickHandler);
681         mContactCard.setOnCreateContextMenuListener(mEntryContextMenuListener);
682 
683         mAboutCard.setOnClickListener(mEntryClickHandler);
684         mAboutCard.setOnCreateContextMenuListener(mEntryContextMenuListener);
685 
686         mPhotoView = (QuickContactImageView) findViewById(R.id.photo);
687         final View transparentView = findViewById(R.id.transparent_view);
688         if (mScroller != null) {
689             transparentView.setOnClickListener(new OnClickListener() {
690                 @Override
691                 public void onClick(View v) {
692                     mScroller.scrollOffBottom();
693                 }
694             });
695         }
696 
697         // Allow a shadow to be shown under the toolbar.
698         ViewUtil.addRectangularOutlineProvider(findViewById(R.id.toolbar_parent), getResources());
699 
700         final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
701         setActionBar(toolbar);
702         getActionBar().setTitle(null);
703         // Put a TextView with a known resource id into the ActionBar. This allows us to easily
704         // find the correct TextView location & size later.
705         toolbar.addView(getLayoutInflater().inflate(R.layout.quickcontact_title_placeholder, null));
706 
707         mHasAlreadyBeenOpened = savedInstanceState != null;
708         mIsEntranceAnimationFinished = mHasAlreadyBeenOpened;
709         mWindowScrim = new ColorDrawable(SCRIM_COLOR);
710         mWindowScrim.setAlpha(0);
711         getWindow().setBackgroundDrawable(mWindowScrim);
712 
713         mScroller.initialize(mMultiShrinkScrollerListener, mExtraMode == MODE_FULLY_EXPANDED,
714                 /* maximumHeaderTextSize */ -1,
715                 /* shouldUpdateNameViewHeight */ true);
716         // mScroller needs to perform asynchronous measurements after initalize(), therefore
717         // we can't mark this as GONE.
718         mScroller.setVisibility(View.INVISIBLE);
719 
720         setHeaderNameText(R.string.missing_name);
721 
722         SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ true,
723                 new Runnable() {
724                     @Override
725                     public void run() {
726                         if (!mHasAlreadyBeenOpened) {
727                             // The initial scrim opacity must match the scrim opacity that would be
728                             // achieved by scrolling to the starting position.
729                             final float alphaRatio = mExtraMode == MODE_FULLY_EXPANDED ?
730                                     1 : mScroller.getStartingTransparentHeightRatio();
731                             final int duration = getResources().getInteger(
732                                     android.R.integer.config_shortAnimTime);
733                             final int desiredAlpha = (int) (0xFF * alphaRatio);
734                             ObjectAnimator o = ObjectAnimator.ofInt(mWindowScrim, "alpha", 0,
735                                     desiredAlpha).setDuration(duration);
736 
737                             o.start();
738                         }
739                     }
740                 });
741 
742         if (savedInstanceState != null) {
743             final int color = savedInstanceState.getInt(KEY_THEME_COLOR, 0);
744             SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false,
745                     new Runnable() {
746                         @Override
747                         public void run() {
748                             // Need to wait for the pre draw before setting the initial scroll
749                             // value. Prior to pre draw all scroll values are invalid.
750                             if (mHasAlreadyBeenOpened) {
751                                 mScroller.setVisibility(View.VISIBLE);
752                                 mScroller.setScroll(mScroller.getScrollNeededToBeFullScreen());
753                             }
754                             // Need to wait for pre draw for setting the theme color. Setting the
755                             // header tint before the MultiShrinkScroller has been measured will
756                             // cause incorrect tinting calculations.
757                             if (color != 0) {
758                                 setThemeColor(mMaterialColorMapUtils
759                                         .calculatePrimaryAndSecondaryColor(color));
760                             }
761                         }
762                     });
763         }
764 
765         Trace.endSection();
766     }
767 
768     @Override
onActivityResult(int requestCode, int resultCode, Intent data)769     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
770         final boolean deletedOrSplit = requestCode == REQUEST_CODE_CONTACT_EDITOR_ACTIVITY &&
771                 (resultCode == ContactDeletionInteraction.RESULT_CODE_DELETED ||
772                 resultCode == ContactEditorActivity.RESULT_CODE_SPLIT);
773         setResult(resultCode);
774         if (deletedOrSplit) {
775             finish();
776         } else if (requestCode == REQUEST_CODE_CONTACT_SELECTION_ACTIVITY &&
777                 resultCode != RESULT_CANCELED) {
778             processIntent(data);
779         } else if (requestCode == REQUEST_CODE_JOIN) {
780             // Ignore failed requests
781             if (resultCode != Activity.RESULT_OK) {
782                 return;
783             }
784             if (data != null) {
785                 joinAggregate(ContentUris.parseId(data.getData()));
786             }
787         } else if (requestCode == REQUEST_CODE_PICK_RINGTONE && data != null) {
788             final Uri pickedUri = data.getParcelableExtra(
789                         RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
790             onRingtonePicked(pickedUri);
791         }
792     }
793 
onRingtonePicked(Uri pickedUri)794     private void onRingtonePicked(Uri pickedUri) {
795         mCustomRingtone = EditorUiUtils.getRingtoneStringFromUri(pickedUri, CURRENT_API_VERSION);
796         Intent intent = ContactSaveService.createSetRingtone(
797                 this, mLookupUri, mCustomRingtone);
798         this.startService(intent);
799     }
800 
801     @Override
onNewIntent(Intent intent)802     protected void onNewIntent(Intent intent) {
803         super.onNewIntent(intent);
804         mHasAlreadyBeenOpened = true;
805         mIsEntranceAnimationFinished = true;
806         mHasComputedThemeColor = false;
807         processIntent(intent);
808     }
809 
810     @Override
onSaveInstanceState(Bundle savedInstanceState)811     public void onSaveInstanceState(Bundle savedInstanceState) {
812         super.onSaveInstanceState(savedInstanceState);
813         if (mColorFilter != null) {
814             savedInstanceState.putInt(KEY_THEME_COLOR, mColorFilterColor);
815         }
816         savedInstanceState.putLong(KEY_PREVIOUS_CONTACT_ID, mPreviousContactId);
817 
818         // Phone specific options
819         savedInstanceState.putBoolean(KEY_SEND_TO_VOICE_MAIL_STATE, mSendToVoicemailState);
820         savedInstanceState.putBoolean(KEY_ARE_PHONE_OPTIONS_CHANGEABLE, mArePhoneOptionsChangable);
821         savedInstanceState.putString(KEY_CUSTOM_RINGTONE, mCustomRingtone);
822     }
823 
processIntent(Intent intent)824     private void processIntent(Intent intent) {
825         if (intent == null) {
826             finish();
827             return;
828         }
829         if (ACTION_SPLIT_COMPLETED.equals(intent.getAction())) {
830             Toast.makeText(this, R.string.contactUnlinkedToast, Toast.LENGTH_SHORT).show();
831             finish();
832             return;
833         }
834 
835         Uri lookupUri = intent.getData();
836         if (intent.getBooleanExtra(EXTRA_CONTACT_EDITED, false)) {
837             setResult(ContactEditorActivity.RESULT_CODE_EDITED);
838         }
839 
840         // Check to see whether it comes from the old version.
841         if (lookupUri != null && LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) {
842             final long rawContactId = ContentUris.parseId(lookupUri);
843             lookupUri = RawContacts.getContactLookupUri(getContentResolver(),
844                     ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
845         }
846         mExtraMode = getIntent().getIntExtra(QuickContact.EXTRA_MODE, QuickContact.MODE_LARGE);
847         if (isMultiWindowOnPhone()) {
848             mExtraMode = QuickContact.MODE_LARGE;
849         }
850         mExtraPrioritizedMimeType =
851                 getIntent().getStringExtra(QuickContact.EXTRA_PRIORITIZED_MIMETYPE);
852         final Uri oldLookupUri = mLookupUri;
853 
854 
855         if (lookupUri == null) {
856             finish();
857             return;
858         }
859         mLookupUri = lookupUri;
860         mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES);
861         if (oldLookupUri == null) {
862             // Should not log if only orientation changes.
863             mShouldLog = !mIsRecreatedInstance;
864             mContactLoader = (ContactLoader) getLoaderManager().initLoader(
865                     LOADER_CONTACT_ID, null, mLoaderContactCallbacks);
866         } else if (oldLookupUri != mLookupUri) {
867             // Should log when reload happens, regardless of orientation change.
868             mShouldLog = true;
869             // After copying a directory contact, the contact URI changes. Therefore,
870             // we need to reload the new contact.
871             mContactLoader = (ContactLoader) (Loader<?>) getLoaderManager().getLoader(
872                     LOADER_CONTACT_ID);
873             mContactLoader.setNewLookup(mLookupUri);
874             mCachedCp2DataCardModel = null;
875         }
876         mContactLoader.forceLoad();
877     }
878 
runEntranceAnimation()879     private void runEntranceAnimation() {
880         if (mHasAlreadyBeenOpened) {
881             return;
882         }
883         mHasAlreadyBeenOpened = true;
884         mScroller.scrollUpForEntranceAnimation(/* scrollToCurrentPosition */ !isMultiWindowOnPhone()
885                 && (mExtraMode != MODE_FULLY_EXPANDED));
886     }
887 
isMultiWindowOnPhone()888     private boolean isMultiWindowOnPhone() {
889         return MultiWindowCompat.isInMultiWindowMode(this) && PhoneCapabilityTester.isPhone(this);
890     }
891 
892     /** Assign this string to the view if it is not empty. */
setHeaderNameText(int resId)893     private void setHeaderNameText(int resId) {
894         if (mScroller != null) {
895             mScroller.setTitle(getText(resId) == null ? null : getText(resId).toString(),
896                     /* isPhoneNumber= */ false);
897         }
898     }
899 
900     /** Assign this string to the view if it is not empty. */
setHeaderNameText(String value, boolean isPhoneNumber)901     private void setHeaderNameText(String value, boolean isPhoneNumber) {
902         if (!TextUtils.isEmpty(value)) {
903             if (mScroller != null) {
904                 mScroller.setTitle(value, isPhoneNumber);
905             }
906         }
907     }
908 
909     /**
910      * Check if the given MIME-type appears in the list of excluded MIME-types
911      * that the most-recent caller requested.
912      */
isMimeExcluded(String mimeType)913     private boolean isMimeExcluded(String mimeType) {
914         if (mExcludeMimes == null) return false;
915         for (String excludedMime : mExcludeMimes) {
916             if (TextUtils.equals(excludedMime, mimeType)) {
917                 return true;
918             }
919         }
920         return false;
921     }
922 
923     /**
924      * Handle the result from the ContactLoader
925      */
bindContactData(final Contact data)926     private void bindContactData(final Contact data) {
927         Trace.beginSection("bindContactData");
928 
929         final int actionType = mContactData == null ? ActionType.START : ActionType.UNKNOWN_ACTION;
930         mContactData = data;
931 
932         final int newContactType;
933         if (DirectoryContactUtil.isDirectoryContact(mContactData)) {
934             newContactType = ContactType.DIRECTORY;
935         } else if (InvisibleContactUtil.isInvisibleAndAddable(mContactData, this)) {
936             newContactType = ContactType.INVISIBLE_AND_ADDABLE;
937         } else if (isContactEditable()) {
938             newContactType = ContactType.EDITABLE;
939         } else {
940             newContactType = ContactType.UNKNOWN_TYPE;
941         }
942         if (mShouldLog && mContactType != newContactType) {
943             Logger.logQuickContactEvent(mReferrer, newContactType, CardType.UNKNOWN_CARD,
944                     actionType, /* thirdPartyAction */ null);
945         }
946         mContactType = newContactType;
947 
948         setStateForPhoneMenuItems(mContactData);
949         invalidateOptionsMenu();
950 
951         Trace.endSection();
952         Trace.beginSection("Set display photo & name");
953 
954         mPhotoView.setIsBusiness(mContactData.isDisplayNameFromOrganization());
955         mPhotoSetter.setupContactPhoto(data, mPhotoView);
956         extractAndApplyTintFromPhotoViewAsynchronously();
957         final String displayName = ContactDisplayUtils.getDisplayName(this, data).toString();
958         setHeaderNameText(
959                 displayName, mContactData.getDisplayNameSource() == DisplayNameSources.PHONE);
960         final String phoneticName = ContactDisplayUtils.getPhoneticName(this, data);
961         if (mScroller != null) {
962             // Show phonetic name only when it doesn't equal the display name.
963             if (!TextUtils.isEmpty(phoneticName) && !phoneticName.equals(displayName)) {
964                 mScroller.setPhoneticName(phoneticName);
965             } else {
966                 mScroller.setPhoneticNameGone();
967             }
968         }
969 
970         Trace.endSection();
971 
972         mEntriesAndActionsTask = new AsyncTask<Void, Void, Cp2DataCardModel>() {
973 
974             @Override
975             protected Cp2DataCardModel doInBackground(
976                     Void... params) {
977                 return generateDataModelFromContact(data);
978             }
979 
980             @Override
981             protected void onPostExecute(Cp2DataCardModel cardDataModel) {
982                 super.onPostExecute(cardDataModel);
983                 // Check that original AsyncTask parameters are still valid and the activity
984                 // is still running before binding to UI. A new intent could invalidate
985                 // the results, for example.
986                 if (data == mContactData && !isCancelled()) {
987                     bindDataToCards(cardDataModel);
988                     showActivity();
989                 }
990             }
991         };
992         mEntriesAndActionsTask.execute();
993         NfcHandler.register(this, mContactData.getLookupUri());
994     }
995 
bindDataToCards(Cp2DataCardModel cp2DataCardModel)996     private void bindDataToCards(Cp2DataCardModel cp2DataCardModel) {
997         final Map<String, List<DataItem>> dataItemsMap = cp2DataCardModel.dataItemsMap;
998 
999         final List<DataItem> phoneDataItems = dataItemsMap.get(Phone.CONTENT_ITEM_TYPE);
1000         mOnlyOnePhoneNumber = phoneDataItems != null && phoneDataItems.size() == 1;
1001 
1002         final List<DataItem> emailDataItems = dataItemsMap.get(Email.CONTENT_ITEM_TYPE);
1003         mOnlyOneEmail = emailDataItems != null && emailDataItems.size() == 1;
1004 
1005         populateContactAndAboutCard(cp2DataCardModel, /* shouldAddPhoneticName */ true);
1006     }
1007 
showActivity()1008     private void showActivity() {
1009         if (mScroller != null) {
1010             mScroller.setVisibility(View.VISIBLE);
1011             SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false,
1012                     new Runnable() {
1013                         @Override
1014                         public void run() {
1015                             runEntranceAnimation();
1016                         }
1017                     });
1018         }
1019     }
1020 
buildAboutCardEntries(Map<String, List<DataItem>> dataItemsMap)1021     private List<List<Entry>> buildAboutCardEntries(Map<String, List<DataItem>> dataItemsMap) {
1022         final List<List<Entry>> aboutCardEntries = new ArrayList<>();
1023         for (String mimetype : SORTED_ABOUT_CARD_MIMETYPES) {
1024             final List<DataItem> mimeTypeItems = dataItemsMap.get(mimetype);
1025             if (mimeTypeItems == null) {
1026                 continue;
1027             }
1028             // Set aboutCardTitleOut = null, since SORTED_ABOUT_CARD_MIMETYPES doesn't contain
1029             // the name mimetype.
1030             final List<Entry> aboutEntries = dataItemsToEntries(mimeTypeItems,
1031                     /* aboutCardTitleOut = */ null);
1032             if (aboutEntries.size() > 0) {
1033                 aboutCardEntries.add(aboutEntries);
1034             }
1035         }
1036         return aboutCardEntries;
1037     }
1038 
1039     @Override
onResume()1040     protected void onResume() {
1041         super.onResume();
1042         // If returning from a launched activity, repopulate the contact and about card
1043         if (mHasIntentLaunched) {
1044             mHasIntentLaunched = false;
1045             populateContactAndAboutCard(mCachedCp2DataCardModel, /* shouldAddPhoneticName */ false);
1046         }
1047 
1048         maybeShowProgressDialog();
1049     }
1050 
1051 
1052     @Override
onPause()1053     protected void onPause() {
1054         super.onPause();
1055         dismissProgressBar();
1056     }
1057 
populateContactAndAboutCard(Cp2DataCardModel cp2DataCardModel, boolean shouldAddPhoneticName)1058     private void populateContactAndAboutCard(Cp2DataCardModel cp2DataCardModel,
1059             boolean shouldAddPhoneticName) {
1060         mCachedCp2DataCardModel = cp2DataCardModel;
1061         if (mHasIntentLaunched || cp2DataCardModel == null) {
1062             return;
1063         }
1064         Trace.beginSection("bind contact card");
1065 
1066         final List<List<Entry>> contactCardEntries = cp2DataCardModel.contactCardEntries;
1067         final List<List<Entry>> aboutCardEntries = cp2DataCardModel.aboutCardEntries;
1068         final String customAboutCardName = cp2DataCardModel.customAboutCardName;
1069 
1070         if (contactCardEntries.size() > 0) {
1071             mContactCard.initialize(contactCardEntries,
1072                     /* numInitialVisibleEntries = */ MIN_NUM_CONTACT_ENTRIES_SHOWN,
1073                     /* isExpanded = */ mContactCard.isExpanded(),
1074                     /* isAlwaysExpanded = */ true,
1075                     mExpandingEntryCardViewListener,
1076                     mScroller);
1077             if (mContactCard.getVisibility() == View.GONE && mShouldLog) {
1078                 Logger.logQuickContactEvent(mReferrer, mContactType, CardType.CONTACT,
1079                         ActionType.UNKNOWN_ACTION, /* thirdPartyAction */ null);
1080             }
1081             mContactCard.setVisibility(View.VISIBLE);
1082         } else {
1083             mContactCard.setVisibility(View.GONE);
1084         }
1085         Trace.endSection();
1086 
1087         Trace.beginSection("bind about card");
1088         // Phonetic name is not a data item, so the entry needs to be created separately
1089         // But if mCachedCp2DataCardModel is passed to this method (e.g. returning from editor
1090         // without saving any changes), then it should include phoneticName and the phoneticName
1091         // shouldn't be changed. If this is the case, we shouldn't add it again. b/27459294
1092         final String phoneticName = mContactData.getPhoneticName();
1093         if (shouldAddPhoneticName && !TextUtils.isEmpty(phoneticName)) {
1094             Entry phoneticEntry = new Entry(/* viewId = */ -1,
1095                     /* icon = */ null,
1096                     getResources().getString(R.string.name_phonetic),
1097                     phoneticName,
1098                     /* subHeaderIcon = */ null,
1099                     /* text = */ null,
1100                     /* textIcon = */ null,
1101                     /* primaryContentDescription = */ null,
1102                     /* intent = */ null,
1103                     /* alternateIcon = */ null,
1104                     /* alternateIntent = */ null,
1105                     /* alternateContentDescription = */ null,
1106                     /* shouldApplyColor = */ false,
1107                     /* isEditable = */ false,
1108                     /* EntryContextMenuInfo = */ new EntryContextMenuInfo(phoneticName,
1109                             getResources().getString(R.string.name_phonetic),
1110                             /* mimeType = */ null, /* id = */ -1, /* isPrimary = */ false),
1111                     /* thirdIcon = */ null,
1112                     /* thirdIntent = */ null,
1113                     /* thirdContentDescription = */ null,
1114                     /* thirdAction = */ Entry.ACTION_NONE,
1115                     /* thirdExtras = */ null,
1116                     /* shouldApplyThirdIconColor = */ true,
1117                     /* iconResourceId = */  0);
1118             List<Entry> phoneticList = new ArrayList<>();
1119             phoneticList.add(phoneticEntry);
1120             // Phonetic name comes after nickname. Check to see if the first entry type is nickname
1121             if (aboutCardEntries.size() > 0 && aboutCardEntries.get(0).get(0).getHeader().equals(
1122                     getResources().getString(R.string.header_nickname_entry))) {
1123                 aboutCardEntries.add(1, phoneticList);
1124             } else {
1125                 aboutCardEntries.add(0, phoneticList);
1126             }
1127         }
1128 
1129         if (!TextUtils.isEmpty(customAboutCardName)) {
1130             mAboutCard.setTitle(customAboutCardName);
1131         }
1132 
1133         mAboutCard.initialize(aboutCardEntries,
1134                 /* numInitialVisibleEntries = */ 1,
1135                 /* isExpanded = */ true,
1136                 /* isAlwaysExpanded = */ true,
1137                 mExpandingEntryCardViewListener,
1138                 mScroller);
1139 
1140         if (contactCardEntries.size() == 0 && aboutCardEntries.size() == 0) {
1141             initializeNoContactDetailCard(cp2DataCardModel.areAllRawContactsSimAccounts);
1142         } else {
1143             mNoContactDetailsCard.setVisibility(View.GONE);
1144         }
1145 
1146         // Show the About card if it has entries
1147         if (aboutCardEntries.size() > 0) {
1148             if (mAboutCard.getVisibility() == View.GONE && mShouldLog) {
1149                 Logger.logQuickContactEvent(mReferrer, mContactType, CardType.ABOUT,
1150                         ActionType.UNKNOWN_ACTION, /* thirdPartyAction */ null);
1151             }
1152             mAboutCard.setVisibility(View.VISIBLE);
1153         }
1154         Trace.endSection();
1155     }
1156 
1157     /**
1158      * Create a card that shows "Add email" and "Add phone number" entries in grey.
1159      * When contact is a SIM contact, only shows "Add phone number".
1160      */
initializeNoContactDetailCard(boolean areAllRawContactsSimAccounts)1161     private void initializeNoContactDetailCard(boolean areAllRawContactsSimAccounts) {
1162         final Drawable phoneIcon = ResourcesCompat.getDrawable(getResources(),
1163                 R.drawable.quantum_ic_phone_vd_theme_24, null).mutate();
1164         final Entry phonePromptEntry = new Entry(CARD_ENTRY_ID_EDIT_CONTACT,
1165                 phoneIcon, getString(R.string.quickcontact_add_phone_number),
1166                 /* subHeader = */ null, /* subHeaderIcon = */ null, /* text = */ null,
1167                 /* textIcon = */ null, /* primaryContentDescription = */ null,
1168                 getEditContactIntent(),
1169                 /* alternateIcon = */ null, /* alternateIntent = */ null,
1170                 /* alternateContentDescription = */ null, /* shouldApplyColor = */ true,
1171                 /* isEditable = */ false, /* EntryContextMenuInfo = */ null,
1172                 /* thirdIcon = */ null, /* thirdIntent = */ null,
1173                 /* thirdContentDescription = */ null,
1174                 /* thirdAction = */ Entry.ACTION_NONE,
1175                 /* thirdExtras = */ null,
1176                 /* shouldApplyThirdIconColor = */ true,
1177                 R.drawable.quantum_ic_phone_vd_theme_24);
1178 
1179         final List<List<Entry>> promptEntries = new ArrayList<>();
1180         promptEntries.add(new ArrayList<Entry>(1));
1181         promptEntries.get(0).add(phonePromptEntry);
1182 
1183         if (!areAllRawContactsSimAccounts) {
1184             final Drawable emailIcon = ResourcesCompat.getDrawable(getResources(),
1185                     R.drawable.quantum_ic_email_vd_theme_24, null).mutate();
1186             final Entry emailPromptEntry = new Entry(CARD_ENTRY_ID_EDIT_CONTACT,
1187                     emailIcon, getString(R.string.quickcontact_add_email), /* subHeader = */ null,
1188                     /* subHeaderIcon = */ null,
1189                     /* text = */ null, /* textIcon = */ null, /* primaryContentDescription = */ null,
1190                     getEditContactIntent(), /* alternateIcon = */ null,
1191                     /* alternateIntent = */ null, /* alternateContentDescription = */ null,
1192                     /* shouldApplyColor = */ true, /* isEditable = */ false,
1193                     /* EntryContextMenuInfo = */ null, /* thirdIcon = */ null,
1194                     /* thirdIntent = */ null, /* thirdContentDescription = */ null,
1195                     /* thirdAction = */ Entry.ACTION_NONE, /* thirdExtras = */ null,
1196                     /* shouldApplyThirdIconColor = */ true,
1197                     R.drawable.quantum_ic_email_vd_theme_24);
1198 
1199             promptEntries.add(new ArrayList<Entry>(1));
1200             promptEntries.get(1).add(emailPromptEntry);
1201         }
1202 
1203         final int subHeaderTextColor = getResources().getColor(
1204                 R.color.quickcontact_entry_sub_header_text_color);
1205         final PorterDuffColorFilter greyColorFilter =
1206                 new PorterDuffColorFilter(subHeaderTextColor, PorterDuff.Mode.SRC_ATOP);
1207         mNoContactDetailsCard.initialize(promptEntries, 2, /* isExpanded = */ true,
1208                 /* isAlwaysExpanded = */ true, mExpandingEntryCardViewListener, mScroller);
1209         if (mNoContactDetailsCard.getVisibility() == View.GONE && mShouldLog) {
1210             Logger.logQuickContactEvent(mReferrer, mContactType, CardType.NO_CONTACT,
1211                     ActionType.UNKNOWN_ACTION, /* thirdPartyAction */ null);
1212         }
1213         mNoContactDetailsCard.setVisibility(View.VISIBLE);
1214         mNoContactDetailsCard.setEntryHeaderColor(subHeaderTextColor);
1215         mNoContactDetailsCard.setColorAndFilter(subHeaderTextColor, greyColorFilter);
1216     }
1217 
1218     /**
1219      * Builds the {@link DataItem}s Map out of the Contact.
1220      * @param data The contact to build the data from.
1221      * @return A pair containing a list of data items sorted within mimetype and sorted
1222      *  amongst mimetype. The map goes from mimetype string to the sorted list of data items within
1223      *  mimetype
1224      */
generateDataModelFromContact( Contact data)1225     private Cp2DataCardModel generateDataModelFromContact(
1226             Contact data) {
1227         Trace.beginSection("Build data items map");
1228 
1229         final Map<String, List<DataItem>> dataItemsMap = new HashMap<>();
1230         final boolean tachyonEnabled = CallUtil.isTachyonEnabled(this);
1231 
1232         for (RawContact rawContact : data.getRawContacts()) {
1233             for (DataItem dataItem : rawContact.getDataItems()) {
1234                 dataItem.setRawContactId(rawContact.getId());
1235 
1236                 final String mimeType = dataItem.getMimeType();
1237                 if (mimeType == null) continue;
1238 
1239                 if (!MIMETYPE_TACHYON.equals(mimeType)) {
1240                     // Only validate non-Tachyon mimetypes.
1241                     final AccountType accountType = rawContact.getAccountType(this);
1242                     final DataKind dataKind = AccountTypeManager.getInstance(this)
1243                             .getKindOrFallback(accountType, mimeType);
1244                     if (dataKind == null) continue;
1245 
1246                     dataItem.setDataKind(dataKind);
1247 
1248                     final boolean hasData = !TextUtils.isEmpty(dataItem.buildDataString(this,
1249                             dataKind));
1250 
1251                     if (isMimeExcluded(mimeType) || !hasData) continue;
1252                 } else if (!tachyonEnabled) {
1253                     // If tachyon isn't enabled, skip its mimetypes.
1254                     continue;
1255                 }
1256 
1257                 List<DataItem> dataItemListByType = dataItemsMap.get(mimeType);
1258                 if (dataItemListByType == null) {
1259                     dataItemListByType = new ArrayList<>();
1260                     dataItemsMap.put(mimeType, dataItemListByType);
1261                 }
1262                 dataItemListByType.add(dataItem);
1263             }
1264         }
1265         Trace.endSection();
1266         bindReachability(dataItemsMap);
1267 
1268         Trace.beginSection("sort within mimetypes");
1269         /*
1270          * Sorting is a multi part step. The end result is to a have a sorted list of the most
1271          * used data items, one per mimetype. Then, within each mimetype, the list of data items
1272          * for that type is also sorted, based off of {super primary, primary, times used} in that
1273          * order.
1274          */
1275         final List<List<DataItem>> dataItemsList = new ArrayList<>();
1276         for (List<DataItem> mimeTypeDataItems : dataItemsMap.values()) {
1277             // Remove duplicate data items
1278             Collapser.collapseList(mimeTypeDataItems, this);
1279             // Sort within mimetype
1280             Collections.sort(mimeTypeDataItems, mWithinMimeTypeDataItemComparator);
1281             // Add to the list of data item lists
1282             dataItemsList.add(mimeTypeDataItems);
1283         }
1284         Trace.endSection();
1285 
1286         Trace.beginSection("sort amongst mimetypes");
1287         // Sort amongst mimetypes to bubble up the top data items for the contact card
1288         Collections.sort(dataItemsList, mAmongstMimeTypeDataItemComparator);
1289         Trace.endSection();
1290 
1291         Trace.beginSection("cp2 data items to entries");
1292 
1293         final List<List<Entry>> contactCardEntries = new ArrayList<>();
1294         final List<List<Entry>> aboutCardEntries = buildAboutCardEntries(dataItemsMap);
1295         final MutableString aboutCardName = new MutableString();
1296 
1297         for (int i = 0; i < dataItemsList.size(); ++i) {
1298             final List<DataItem> dataItemsByMimeType = dataItemsList.get(i);
1299             final DataItem topDataItem = dataItemsByMimeType.get(0);
1300             if (SORTED_ABOUT_CARD_MIMETYPES.contains(topDataItem.getMimeType())) {
1301                 // About card mimetypes are built in buildAboutCardEntries, skip here
1302                 continue;
1303             } else {
1304                 List<Entry> contactEntries = dataItemsToEntries(dataItemsList.get(i),
1305                         aboutCardName);
1306                 if (contactEntries.size() > 0) {
1307                     contactCardEntries.add(contactEntries);
1308                 }
1309             }
1310         }
1311 
1312         Trace.endSection();
1313 
1314         final Cp2DataCardModel dataModel = new Cp2DataCardModel();
1315         dataModel.customAboutCardName = aboutCardName.value;
1316         dataModel.aboutCardEntries = aboutCardEntries;
1317         dataModel.contactCardEntries = contactCardEntries;
1318         dataModel.dataItemsMap = dataItemsMap;
1319         dataModel.areAllRawContactsSimAccounts = data.areAllRawContactsSimAccounts(this);
1320         return dataModel;
1321     }
1322 
1323     /**
1324      * Bind the custom data items to each {@link PhoneDataItem} that is Tachyon reachable, the data
1325      * will be needed when creating the {@link Entry} for the {@link PhoneDataItem}.
1326      */
bindReachability(Map<String, List<DataItem>> dataItemsMap)1327     private void bindReachability(Map<String, List<DataItem>> dataItemsMap) {
1328         final List<DataItem> phoneItems = dataItemsMap.get(Phone.CONTENT_ITEM_TYPE);
1329         final List<DataItem> tachyonItems = dataItemsMap.get(MIMETYPE_TACHYON);
1330         if (phoneItems != null && tachyonItems != null) {
1331             for (DataItem phone : phoneItems) {
1332                 if (phone instanceof PhoneDataItem && ((PhoneDataItem) phone).getNumber() != null) {
1333                     for (DataItem tachyonItem : tachyonItems) {
1334                         if (((PhoneDataItem) phone).getNumber().equals(
1335                                 tachyonItem.getContentValues().getAsString(Data.DATA1))) {
1336                             ((PhoneDataItem) phone).setTachyonReachable(true);
1337                             ((PhoneDataItem) phone).setReachableDataItem(tachyonItem);
1338                         }
1339                     }
1340                 }
1341             }
1342         }
1343     }
1344 
1345     /**
1346      * Class used to hold the About card and Contact cards' data model that gets generated
1347      * on a background thread. All data is from CP2.
1348      */
1349     private static class Cp2DataCardModel {
1350         /**
1351          * A map between a mimetype string and the corresponding list of data items. The data items
1352          * are in sorted order using mWithinMimeTypeDataItemComparator.
1353          */
1354         public Map<String, List<DataItem>> dataItemsMap;
1355         public List<List<Entry>> aboutCardEntries;
1356         public List<List<Entry>> contactCardEntries;
1357         public String customAboutCardName;
1358         public boolean areAllRawContactsSimAccounts;
1359     }
1360 
1361     private static class MutableString {
1362         public String value;
1363     }
1364 
1365     /**
1366      * Converts a {@link DataItem} into an {@link ExpandingEntryCardView.Entry} for display.
1367      * If the {@link ExpandingEntryCardView.Entry} has no visual elements, null is returned.
1368      *
1369      * This runs on a background thread. This is set as static to avoid accidentally adding
1370      * additional dependencies on unsafe things (like the Activity).
1371      *
1372      * @param dataItem The {@link DataItem} to convert.
1373      * @param secondDataItem A second {@link DataItem} to help build a full entry for some
1374      *  mimetypes
1375      * @return The {@link ExpandingEntryCardView.Entry}, or null if no visual elements are present.
1376      */
dataItemToEntry(DataItem dataItem, DataItem secondDataItem, Context context, Contact contactData, final MutableString aboutCardName)1377     private static Entry dataItemToEntry(DataItem dataItem, DataItem secondDataItem,
1378             Context context, Contact contactData,
1379             final MutableString aboutCardName) {
1380         if (contactData == null) return null;
1381         Drawable icon = null;
1382         String header = null;
1383         String subHeader = null;
1384         Drawable subHeaderIcon = null;
1385         String text = null;
1386         Drawable textIcon = null;
1387         StringBuilder primaryContentDescription = new StringBuilder();
1388         Spannable phoneContentDescription = null;
1389         Spannable smsContentDescription = null;
1390         Intent intent = null;
1391         boolean shouldApplyColor = true;
1392         boolean shouldApplyThirdIconColor = true;
1393         Drawable alternateIcon = null;
1394         Intent alternateIntent = null;
1395         StringBuilder alternateContentDescription = new StringBuilder();
1396         final boolean isEditable = false;
1397         EntryContextMenuInfo entryContextMenuInfo = null;
1398         Drawable thirdIcon = null;
1399         Intent thirdIntent = null;
1400         int thirdAction = Entry.ACTION_NONE;
1401         String thirdContentDescription = null;
1402         Bundle thirdExtras = null;
1403         int iconResourceId = 0;
1404 
1405         context = context.getApplicationContext();
1406         final Resources res = context.getResources();
1407         DataKind kind = dataItem.getDataKind();
1408 
1409         if (dataItem instanceof ImDataItem) {
1410             final ImDataItem im = (ImDataItem) dataItem;
1411             intent = ContactsUtils.buildImIntent(context, im).first;
1412             final boolean isEmail = im.isCreatedFromEmail();
1413             final int protocol;
1414             if (!im.isProtocolValid()) {
1415                 protocol = Im.PROTOCOL_CUSTOM;
1416             } else {
1417                 protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : im.getProtocol();
1418             }
1419             if (protocol == Im.PROTOCOL_CUSTOM) {
1420                 // If the protocol is custom, display the "IM" entry header as well to distinguish
1421                 // this entry from other ones
1422                 header = res.getString(R.string.header_im_entry);
1423                 subHeader = Im.getProtocolLabel(res, protocol,
1424                         im.getCustomProtocol()).toString();
1425                 text = im.getData();
1426             } else {
1427                 header = Im.getProtocolLabel(res, protocol,
1428                         im.getCustomProtocol()).toString();
1429                 subHeader = im.getData();
1430             }
1431             entryContextMenuInfo = new EntryContextMenuInfo(im.getData(), header,
1432                     dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary());
1433         } else if (dataItem instanceof OrganizationDataItem) {
1434             final OrganizationDataItem organization = (OrganizationDataItem) dataItem;
1435             header = res.getString(R.string.header_organization_entry);
1436             entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header,
1437                 dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary());
1438             text = ContactDisplayUtils
1439                 .getFormattedCompanyString(context, (OrganizationDataItem) dataItem, false);
1440         } else if (dataItem instanceof NicknameDataItem) {
1441             final NicknameDataItem nickname = (NicknameDataItem) dataItem;
1442             // Build nickname entries
1443             final boolean isNameRawContact =
1444                 (contactData.getNameRawContactId() == dataItem.getRawContactId());
1445 
1446             final boolean duplicatesTitle =
1447                 isNameRawContact
1448                 && contactData.getDisplayNameSource() == DisplayNameSources.NICKNAME;
1449 
1450             if (!duplicatesTitle) {
1451                 header = res.getString(R.string.header_nickname_entry);
1452                 subHeader = nickname.getName();
1453                 entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header,
1454                         dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary());
1455             }
1456         } else if (dataItem instanceof CustomDataItem) {
1457             final CustomDataItem customDataItem = (CustomDataItem) dataItem;
1458             final String summary = customDataItem.getSummary();
1459             header = TextUtils.isEmpty(summary)
1460                     ? res.getString(R.string.label_custom_field) : summary;
1461             subHeader = customDataItem.getContent();
1462             entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header,
1463                     dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary());
1464         } else if (dataItem instanceof NoteDataItem) {
1465             final NoteDataItem note = (NoteDataItem) dataItem;
1466             header = res.getString(R.string.header_note_entry);
1467             subHeader = note.getNote();
1468             entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header,
1469                     dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary());
1470         } else if (dataItem instanceof WebsiteDataItem) {
1471             final WebsiteDataItem website = (WebsiteDataItem) dataItem;
1472             header = res.getString(R.string.header_website_entry);
1473             subHeader = website.getUrl();
1474             entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header,
1475                     dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary());
1476             try {
1477                 final WebAddress webAddress = new WebAddress(website.buildDataStringForDisplay
1478                         (context, kind));
1479                 intent = new Intent(Intent.ACTION_VIEW, Uri.parse(webAddress.toString()));
1480             } catch (final ParseException e) {
1481                 Log.e(TAG, "Couldn't parse website: " + website.buildDataStringForDisplay(
1482                         context, kind));
1483             }
1484         } else if (dataItem instanceof EventDataItem) {
1485             final EventDataItem event = (EventDataItem) dataItem;
1486             final String dataString = event.buildDataStringForDisplay(context, kind);
1487             final Calendar cal = DateUtils.parseDate(dataString, false);
1488             if (cal != null) {
1489                 final Date nextAnniversary =
1490                         DateUtils.getNextAnnualDate(cal);
1491                 final Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon();
1492                 builder.appendPath("time");
1493                 ContentUris.appendId(builder, nextAnniversary.getTime());
1494                 intent = new Intent(Intent.ACTION_VIEW).setData(builder.build());
1495             }
1496             header = res.getString(R.string.header_event_entry);
1497             if (event.hasKindTypeColumn(kind)) {
1498                 subHeader = EventCompat.getTypeLabel(res, event.getKindTypeColumn(kind),
1499                         event.getLabel()).toString();
1500             }
1501             text = DateUtils.formatDate(context, dataString);
1502             entryContextMenuInfo = new EntryContextMenuInfo(text, header,
1503                     dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary());
1504         } else if (dataItem instanceof RelationDataItem) {
1505             final RelationDataItem relation = (RelationDataItem) dataItem;
1506             final String dataString = relation.buildDataStringForDisplay(context, kind);
1507             if (!TextUtils.isEmpty(dataString)) {
1508                 intent = new Intent(Intent.ACTION_SEARCH);
1509                 intent.putExtra(SearchManager.QUERY, dataString);
1510                 intent.setType(Contacts.CONTENT_TYPE);
1511             }
1512             header = res.getString(R.string.header_relation_entry);
1513             subHeader = relation.getName();
1514             entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header,
1515                     dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary());
1516             if (relation.hasKindTypeColumn(kind)) {
1517                 text = Relation.getTypeLabel(res,
1518                         relation.getKindTypeColumn(kind),
1519                         relation.getLabel()).toString();
1520             }
1521         } else if (dataItem instanceof PhoneDataItem) {
1522             final PhoneDataItem phone = (PhoneDataItem) dataItem;
1523             String phoneLabel = null;
1524             if (!TextUtils.isEmpty(phone.getNumber())) {
1525                 primaryContentDescription.append(res.getString(R.string.call_other)).append(" ");
1526                 header = sBidiFormatter.unicodeWrap(phone.buildDataStringForDisplay(context, kind),
1527                         TextDirectionHeuristics.LTR);
1528                 entryContextMenuInfo = new EntryContextMenuInfo(header,
1529                         res.getString(R.string.phoneLabelsGroup), dataItem.getMimeType(),
1530                         dataItem.getId(), dataItem.isSuperPrimary());
1531                 if (phone.hasKindTypeColumn(kind)) {
1532                     final int kindTypeColumn = phone.getKindTypeColumn(kind);
1533                     final String label = phone.getLabel();
1534                     phoneLabel = label;
1535                     if (kindTypeColumn == Phone.TYPE_CUSTOM && TextUtils.isEmpty(label)) {
1536                         text = "";
1537                     } else {
1538                         text = Phone.getTypeLabel(res, kindTypeColumn, label).toString();
1539                         phoneLabel= text;
1540                         primaryContentDescription.append(text).append(" ");
1541                     }
1542                 }
1543                 primaryContentDescription.append(header);
1544                 phoneContentDescription = com.android.contacts.util.ContactDisplayUtils
1545                         .getTelephoneTtsSpannable(primaryContentDescription.toString(), header);
1546                 iconResourceId = R.drawable.quantum_ic_phone_vd_theme_24;
1547                 icon = res.getDrawable(iconResourceId);
1548                 if (PhoneCapabilityTester.isPhone(context)) {
1549                     intent = CallUtil.getCallIntent(phone.getNumber());
1550                     intent.putExtra(EXTRA_ACTION_TYPE, ActionType.CALL);
1551                 }
1552                 alternateIntent = new Intent(Intent.ACTION_SENDTO,
1553                         Uri.fromParts(ContactsUtils.SCHEME_SMSTO, phone.getNumber(), null));
1554                 alternateIntent.putExtra(EXTRA_ACTION_TYPE, ActionType.SMS);
1555 
1556                 alternateIcon = res.getDrawable(R.drawable.quantum_ic_message_vd_theme_24);
1557                 alternateContentDescription.append(res.getString(R.string.sms_custom, header));
1558                 smsContentDescription = com.android.contacts.util.ContactDisplayUtils
1559                         .getTelephoneTtsSpannable(alternateContentDescription.toString(), header);
1560 
1561                 int videoCapability = CallUtil.getVideoCallingAvailability(context);
1562                 boolean isPresenceEnabled =
1563                         (videoCapability & CallUtil.VIDEO_CALLING_PRESENCE) != 0;
1564                 boolean isVideoEnabled = (videoCapability & CallUtil.VIDEO_CALLING_ENABLED) != 0;
1565                 // Check to ensure carrier presence indicates the number supports video calling.
1566                 int carrierPresence = dataItem.getCarrierPresence();
1567                 boolean isPresent = (carrierPresence & Phone.CARRIER_PRESENCE_VT_CAPABLE) != 0;
1568 
1569                 if (CallUtil.isCallWithSubjectSupported(context)) {
1570                     thirdIcon = res.getDrawable(R.drawable.quantum_ic_perm_phone_msg_vd_theme_24);
1571                     thirdAction = Entry.ACTION_CALL_WITH_SUBJECT;
1572                     thirdContentDescription =
1573                             res.getString(R.string.call_with_a_note);
1574                     // Create a bundle containing the data the call subject dialog requires.
1575                     thirdExtras = new Bundle();
1576                     thirdExtras.putLong(CallSubjectDialog.ARG_PHOTO_ID,
1577                             contactData.getPhotoId());
1578                     thirdExtras.putParcelable(CallSubjectDialog.ARG_PHOTO_URI,
1579                             UriUtils.parseUriOrNull(contactData.getPhotoUri()));
1580                     thirdExtras.putParcelable(CallSubjectDialog.ARG_CONTACT_URI,
1581                             contactData.getLookupUri());
1582                     thirdExtras.putString(CallSubjectDialog.ARG_NAME_OR_NUMBER,
1583                             contactData.getDisplayName());
1584                     thirdExtras.putBoolean(CallSubjectDialog.ARG_IS_BUSINESS, false);
1585                     thirdExtras.putString(CallSubjectDialog.ARG_NUMBER,
1586                             phone.getNumber());
1587                     thirdExtras.putString(CallSubjectDialog.ARG_DISPLAY_NUMBER,
1588                             phone.getFormattedPhoneNumber());
1589                     thirdExtras.putString(CallSubjectDialog.ARG_NUMBER_LABEL,
1590                             phoneLabel);
1591                 } else if (isVideoEnabled && (!isPresenceEnabled || isPresent)) {
1592                     thirdIcon = res.getDrawable(R.drawable.quantum_ic_videocam_vd_theme_24);
1593                     thirdAction = Entry.ACTION_INTENT;
1594                     thirdIntent = CallUtil.getVideoCallIntent(phone.getNumber(),
1595                             CALL_ORIGIN_QUICK_CONTACTS_ACTIVITY);
1596                     thirdIntent.putExtra(EXTRA_ACTION_TYPE, ActionType.VIDEOCALL);
1597                     thirdContentDescription =
1598                             res.getString(R.string.description_video_call);
1599                 } else if (CallUtil.isTachyonEnabled(context)
1600                         && ((PhoneDataItem) dataItem).isTachyonReachable()) {
1601                     thirdIcon = res.getDrawable(R.drawable.quantum_ic_videocam_vd_theme_24);
1602                     thirdAction = Entry.ACTION_INTENT;
1603                     thirdIntent = new Intent(TACHYON_CALL_ACTION);
1604                     thirdIntent.setData(
1605                             Uri.fromParts(PhoneAccount.SCHEME_TEL, phone.getNumber(), null));
1606                     thirdContentDescription = ((PhoneDataItem) dataItem).getReachableDataItem()
1607                             .getContentValues().getAsString(Data.DATA2);
1608                 }
1609             }
1610         } else if (dataItem instanceof EmailDataItem) {
1611             final EmailDataItem email = (EmailDataItem) dataItem;
1612             final String address = email.getData();
1613             if (!TextUtils.isEmpty(address)) {
1614                 primaryContentDescription.append(res.getString(R.string.email_other)).append(" ");
1615                 final Uri mailUri = Uri.fromParts(ContactsUtils.SCHEME_MAILTO, address, null);
1616                 intent = new Intent(Intent.ACTION_SENDTO, mailUri);
1617                 intent.putExtra(EXTRA_ACTION_TYPE, ActionType.EMAIL);
1618                 header = email.getAddress();
1619                 entryContextMenuInfo = new EntryContextMenuInfo(header,
1620                         res.getString(R.string.emailLabelsGroup), dataItem.getMimeType(),
1621                         dataItem.getId(), dataItem.isSuperPrimary());
1622                 if (email.hasKindTypeColumn(kind)) {
1623                     text = Email.getTypeLabel(res, email.getKindTypeColumn(kind),
1624                             email.getLabel()).toString();
1625                     primaryContentDescription.append(text).append(" ");
1626                 }
1627                 primaryContentDescription.append(header);
1628                 iconResourceId = R.drawable.quantum_ic_email_vd_theme_24;
1629                 icon = res.getDrawable(iconResourceId);
1630             }
1631         } else if (dataItem instanceof StructuredPostalDataItem) {
1632             StructuredPostalDataItem postal = (StructuredPostalDataItem) dataItem;
1633             final String postalAddress = postal.getFormattedAddress();
1634             if (!TextUtils.isEmpty(postalAddress)) {
1635                 primaryContentDescription.append(res.getString(R.string.map_other)).append(" ");
1636                 intent = StructuredPostalUtils.getViewPostalAddressIntent(postalAddress);
1637                 intent.putExtra(EXTRA_ACTION_TYPE, ActionType.ADDRESS);
1638                 header = postal.getFormattedAddress();
1639                 entryContextMenuInfo = new EntryContextMenuInfo(header,
1640                         res.getString(R.string.postalLabelsGroup), dataItem.getMimeType(),
1641                         dataItem.getId(), dataItem.isSuperPrimary());
1642                 if (postal.hasKindTypeColumn(kind)) {
1643                     text = StructuredPostal.getTypeLabel(res,
1644                             postal.getKindTypeColumn(kind), postal.getLabel()).toString();
1645                     primaryContentDescription.append(text).append(" ");
1646                 }
1647                 primaryContentDescription.append(header);
1648                 alternateIntent =
1649                         StructuredPostalUtils.getViewPostalAddressDirectionsIntent(postalAddress);
1650                 alternateIntent.putExtra(EXTRA_ACTION_TYPE, ActionType.DIRECTIONS);
1651                 alternateIcon = res.getDrawable(R.drawable.quantum_ic_directions_vd_theme_24);
1652                 alternateContentDescription.append(res.getString(
1653                         R.string.content_description_directions)).append(" ").append(header);
1654                 iconResourceId = R.drawable.quantum_ic_place_vd_theme_24;
1655                 icon = res.getDrawable(iconResourceId);
1656             }
1657         } else if (dataItem instanceof SipAddressDataItem) {
1658             final SipAddressDataItem sip = (SipAddressDataItem) dataItem;
1659             final String address = sip.getSipAddress();
1660             if (!TextUtils.isEmpty(address)) {
1661                 primaryContentDescription.append(res.getString(R.string.call_other)).append(
1662                         " ");
1663                 if (PhoneCapabilityTester.isSipPhone(context)) {
1664                     final Uri callUri = Uri.fromParts(PhoneAccount.SCHEME_SIP, address, null);
1665                     intent = CallUtil.getCallIntent(callUri);
1666                     intent.putExtra(EXTRA_ACTION_TYPE, ActionType.SIPCALL);
1667                 }
1668                 header = address;
1669                 entryContextMenuInfo = new EntryContextMenuInfo(header,
1670                         res.getString(R.string.phoneLabelsGroup), dataItem.getMimeType(),
1671                         dataItem.getId(), dataItem.isSuperPrimary());
1672                 if (sip.hasKindTypeColumn(kind)) {
1673                     text = SipAddress.getTypeLabel(res,
1674                             sip.getKindTypeColumn(kind), sip.getLabel()).toString();
1675                     primaryContentDescription.append(text).append(" ");
1676                 }
1677                 primaryContentDescription.append(header);
1678                 iconResourceId = R.drawable.quantum_ic_dialer_sip_vd_theme_24;
1679                 icon = res.getDrawable(iconResourceId);
1680             }
1681         } else if (dataItem instanceof StructuredNameDataItem) {
1682             // If the name is already set and this is not the super primary value then leave the
1683             // current value. This way we show the super primary value when we are able to.
1684             if (dataItem.isSuperPrimary() || aboutCardName.value == null
1685                     || aboutCardName.value.isEmpty()) {
1686                 final String givenName = ((StructuredNameDataItem) dataItem).getGivenName();
1687                 if (!TextUtils.isEmpty(givenName)) {
1688                     aboutCardName.value = res.getString(R.string.about_card_title) +
1689                             " " + givenName;
1690                 } else {
1691                     aboutCardName.value = res.getString(R.string.about_card_title);
1692                 }
1693             }
1694         } else if (CallUtil.isTachyonEnabled(context) && MIMETYPE_TACHYON.equals(
1695                 dataItem.getMimeType())) {
1696             // Skip these actions. They will be placed by the phone number.
1697             return null;
1698         } else {
1699             // Custom DataItem
1700             header = dataItem.buildDataStringForDisplay(context, kind);
1701             text = kind.typeColumn;
1702             intent = new Intent(Intent.ACTION_VIEW);
1703             final Uri uri = ContentUris.withAppendedId(Data.CONTENT_URI, dataItem.getId());
1704             intent.setDataAndType(uri, dataItem.getMimeType());
1705             intent.putExtra(EXTRA_ACTION_TYPE, ActionType.THIRD_PARTY);
1706             intent.putExtra(EXTRA_THIRD_PARTY_ACTION, dataItem.getMimeType());
1707 
1708             if (intent != null) {
1709                 final String mimetype = intent.getType();
1710                 // Build advanced entry for known 3p types. Otherwise default to ResolveCache icon.
1711                 if (MIMETYPE_HANGOUTS.equals(mimetype)) {
1712                     // If a secondDataItem is available, use it to build an entry with
1713                     // alternate actions
1714                     if (secondDataItem != null) {
1715                         icon = res.getDrawable(R.drawable.quantum_ic_hangout_vd_theme_24);
1716                         alternateIcon = res.getDrawable(
1717                                 R.drawable.quantum_ic_hangout_video_vd_theme_24);
1718                         final HangoutsDataItemModel itemModel =
1719                                 new HangoutsDataItemModel(intent, alternateIntent,
1720                                         dataItem, secondDataItem, alternateContentDescription,
1721                                         header, text, context);
1722 
1723                         populateHangoutsDataItemModel(itemModel);
1724                         intent = itemModel.intent;
1725                         alternateIntent = itemModel.alternateIntent;
1726                         alternateContentDescription = itemModel.alternateContentDescription;
1727                         header = itemModel.header;
1728                         text = itemModel.text;
1729                     } else {
1730                         if (HANGOUTS_DATA_5_VIDEO.equals(intent.getDataString())) {
1731                             icon = res.getDrawable(R.drawable.quantum_ic_hangout_video_vd_theme_24);
1732                         } else {
1733                             icon = res.getDrawable(R.drawable.quantum_ic_hangout_vd_theme_24);
1734                         }
1735                     }
1736                 } else {
1737                     icon = ResolveCache.getInstance(context).getIcon(
1738                             dataItem.getMimeType(), intent);
1739                     // Call mutate to create a new Drawable.ConstantState for color filtering
1740                     if (icon != null) {
1741                         icon.mutate();
1742                     }
1743                     shouldApplyColor = false;
1744 
1745                     if (!MIMETYPE_GPLUS_PROFILE.equals(mimetype)) {
1746                         entryContextMenuInfo = new EntryContextMenuInfo(header, mimetype,
1747                                 dataItem.getMimeType(), dataItem.getId(),
1748                                 dataItem.isSuperPrimary());
1749                     }
1750                 }
1751             }
1752         }
1753 
1754         if (intent != null) {
1755             // Do not set the intent is there are no resolves
1756             if (!PhoneCapabilityTester.isIntentRegistered(context, intent)) {
1757                 intent = null;
1758             }
1759         }
1760 
1761         if (alternateIntent != null) {
1762             // Do not set the alternate intent is there are no resolves
1763             if (!PhoneCapabilityTester.isIntentRegistered(context, alternateIntent)) {
1764                 alternateIntent = null;
1765             } else if (TextUtils.isEmpty(alternateContentDescription)) {
1766                 // Attempt to use package manager to find a suitable content description if needed
1767                 alternateContentDescription.append(getIntentResolveLabel(alternateIntent, context));
1768             }
1769         }
1770 
1771         // If the Entry has no visual elements, return null
1772         if (icon == null && TextUtils.isEmpty(header) && TextUtils.isEmpty(subHeader) &&
1773                 subHeaderIcon == null && TextUtils.isEmpty(text) && textIcon == null) {
1774             return null;
1775         }
1776 
1777         // Ignore dataIds from the Me profile.
1778         final int dataId = dataItem.getId() > Integer.MAX_VALUE ?
1779                 -1 : (int) dataItem.getId();
1780 
1781         return new Entry(dataId, icon, header, subHeader, subHeaderIcon, text, textIcon,
1782                 phoneContentDescription == null
1783                         ? new SpannableString(primaryContentDescription.toString())
1784                         : phoneContentDescription,
1785                 intent, alternateIcon, alternateIntent,
1786                 smsContentDescription == null
1787                         ? new SpannableString(alternateContentDescription.toString())
1788                         : smsContentDescription,
1789                 shouldApplyColor, isEditable,
1790                 entryContextMenuInfo, thirdIcon, thirdIntent, thirdContentDescription, thirdAction,
1791                 thirdExtras, shouldApplyThirdIconColor, iconResourceId);
1792     }
1793 
dataItemsToEntries(List<DataItem> dataItems, MutableString aboutCardTitleOut)1794     private List<Entry> dataItemsToEntries(List<DataItem> dataItems,
1795             MutableString aboutCardTitleOut) {
1796         // Hangouts and G+ use two data items to create one entry.
1797         if (dataItems.get(0).getMimeType().equals(MIMETYPE_GPLUS_PROFILE)) {
1798             return gPlusDataItemsToEntries(dataItems);
1799         } else if (dataItems.get(0).getMimeType().equals(MIMETYPE_HANGOUTS)) {
1800             return hangoutsDataItemsToEntries(dataItems);
1801         } else {
1802             final List<Entry> entries = new ArrayList<>();
1803             for (DataItem dataItem : dataItems) {
1804                 final Entry entry = dataItemToEntry(dataItem, /* secondDataItem = */ null,
1805                         this, mContactData, aboutCardTitleOut);
1806                 if (entry != null) {
1807                     entries.add(entry);
1808                 }
1809             }
1810             return entries;
1811         }
1812     }
1813 
1814     /**
1815      * Put the data items into buckets based on the raw contact id
1816      */
dataItemsToBucket(List<DataItem> dataItems)1817     private Map<Long, List<DataItem>> dataItemsToBucket(List<DataItem> dataItems) {
1818         final Map<Long, List<DataItem>> buckets = new HashMap<>();
1819         for (DataItem dataItem : dataItems) {
1820             List<DataItem> bucket = buckets.get(dataItem.getRawContactId());
1821             if (bucket == null) {
1822                 bucket = new ArrayList<>();
1823                 buckets.put(dataItem.getRawContactId(), bucket);
1824             }
1825             bucket.add(dataItem);
1826         }
1827         return buckets;
1828     }
1829 
1830     /**
1831      * For G+ entries, a single ExpandingEntryCardView.Entry consists of two data items. This
1832      * method use only the View profile to build entry.
1833      */
gPlusDataItemsToEntries(List<DataItem> dataItems)1834     private List<Entry> gPlusDataItemsToEntries(List<DataItem> dataItems) {
1835         final List<Entry> entries = new ArrayList<>();
1836 
1837         for (List<DataItem> bucket : dataItemsToBucket(dataItems).values()) {
1838             for (DataItem dataItem : bucket) {
1839                 if (GPLUS_PROFILE_DATA_5_VIEW_PROFILE.equals(
1840                         dataItem.getContentValues().getAsString(Data.DATA5))) {
1841                     final Entry entry = dataItemToEntry(dataItem, /* secondDataItem = */ null,
1842                             this, mContactData, /* aboutCardName = */ null);
1843                     if (entry != null) {
1844                         entries.add(entry);
1845                     }
1846                 }
1847             }
1848         }
1849         return entries;
1850     }
1851 
1852     /**
1853      * For Hangouts entries, a single ExpandingEntryCardView.Entry consists of two data items. This
1854      * method attempts to build each entry using the two data items if they are available. If there
1855      * are more or less than two data items, a fall back is used and each data item gets its own
1856      * entry.
1857      */
hangoutsDataItemsToEntries(List<DataItem> dataItems)1858     private List<Entry> hangoutsDataItemsToEntries(List<DataItem> dataItems) {
1859         final List<Entry> entries = new ArrayList<>();
1860 
1861         // Use the buckets to build entries. If a bucket contains two data items, build the special
1862         // entry, otherwise fall back to the normal entry.
1863         for (List<DataItem> bucket : dataItemsToBucket(dataItems).values()) {
1864             if (bucket.size() == 2) {
1865                 // Use the pair to build an entry
1866                 final Entry entry = dataItemToEntry(bucket.get(0),
1867                         /* secondDataItem = */ bucket.get(1), this, mContactData,
1868                         /* aboutCardName = */ null);
1869                 if (entry != null) {
1870                     entries.add(entry);
1871                 }
1872             } else {
1873                 for (DataItem dataItem : bucket) {
1874                     final Entry entry = dataItemToEntry(dataItem, /* secondDataItem = */ null,
1875                             this, mContactData, /* aboutCardName = */ null);
1876                     if (entry != null) {
1877                         entries.add(entry);
1878                     }
1879                 }
1880             }
1881         }
1882         return entries;
1883     }
1884 
1885     /**
1886      * Used for statically passing around Hangouts data items and entry fields to
1887      * populateHangoutsDataItemModel.
1888      */
1889     private static final class HangoutsDataItemModel {
1890         public Intent intent;
1891         public Intent alternateIntent;
1892         public DataItem dataItem;
1893         public DataItem secondDataItem;
1894         public StringBuilder alternateContentDescription;
1895         public String header;
1896         public String text;
1897         public Context context;
1898 
HangoutsDataItemModel(Intent intent, Intent alternateIntent, DataItem dataItem, DataItem secondDataItem, StringBuilder alternateContentDescription, String header, String text, Context context)1899         public HangoutsDataItemModel(Intent intent, Intent alternateIntent, DataItem dataItem,
1900                 DataItem secondDataItem, StringBuilder alternateContentDescription, String header,
1901                 String text, Context context) {
1902             this.intent = intent;
1903             this.alternateIntent = alternateIntent;
1904             this.dataItem = dataItem;
1905             this.secondDataItem = secondDataItem;
1906             this.alternateContentDescription = alternateContentDescription;
1907             this.header = header;
1908             this.text = text;
1909             this.context = context;
1910         }
1911     }
1912 
populateHangoutsDataItemModel( HangoutsDataItemModel dataModel)1913     private static void populateHangoutsDataItemModel(
1914             HangoutsDataItemModel dataModel) {
1915         final Intent secondIntent = new Intent(Intent.ACTION_VIEW);
1916         secondIntent.setDataAndType(ContentUris.withAppendedId(Data.CONTENT_URI,
1917                 dataModel.secondDataItem.getId()), dataModel.secondDataItem.getMimeType());
1918         secondIntent.putExtra(EXTRA_ACTION_TYPE, ActionType.THIRD_PARTY);
1919         secondIntent.putExtra(EXTRA_THIRD_PARTY_ACTION, dataModel.secondDataItem.getMimeType());
1920 
1921         // There is no guarantee the order the data items come in. Second
1922         // data item does not necessarily mean it's the alternate.
1923         // Hangouts video should be alternate. Swap if needed
1924         if (HANGOUTS_DATA_5_VIDEO.equals(
1925                 dataModel.dataItem.getContentValues().getAsString(Data.DATA5))) {
1926             dataModel.alternateIntent = dataModel.intent;
1927             dataModel.alternateContentDescription = new StringBuilder(dataModel.header);
1928 
1929             dataModel.intent = secondIntent;
1930             dataModel.header = dataModel.secondDataItem.buildDataStringForDisplay(
1931                     dataModel.context, dataModel.secondDataItem.getDataKind());
1932             dataModel.text = dataModel.secondDataItem.getDataKind().typeColumn;
1933         } else if (HANGOUTS_DATA_5_MESSAGE.equals(
1934                 dataModel.dataItem.getContentValues().getAsString(Data.DATA5))) {
1935             dataModel.alternateIntent = secondIntent;
1936             dataModel.alternateContentDescription = new StringBuilder(
1937                     dataModel.secondDataItem.buildDataStringForDisplay(dataModel.context,
1938                             dataModel.secondDataItem.getDataKind()));
1939         }
1940     }
1941 
getIntentResolveLabel(Intent intent, Context context)1942     private static String getIntentResolveLabel(Intent intent, Context context) {
1943         final List<ResolveInfo> matches = context.getPackageManager().queryIntentActivities(intent,
1944                 PackageManager.MATCH_DEFAULT_ONLY);
1945 
1946         // Pick first match, otherwise best found
1947         ResolveInfo bestResolve = null;
1948         final int size = matches.size();
1949         if (size == 1) {
1950             bestResolve = matches.get(0);
1951         } else if (size > 1) {
1952             bestResolve = ResolveCache.getInstance(context).getBestResolve(intent, matches);
1953         }
1954 
1955         if (bestResolve == null) {
1956             return null;
1957         }
1958 
1959         return String.valueOf(bestResolve.loadLabel(context.getPackageManager()));
1960     }
1961 
1962     /**
1963      * Asynchronously extract the most vibrant color from the PhotoView. Once extracted,
1964      * apply this tint to {@link MultiShrinkScroller}. This operation takes about 20-30ms
1965      * on a Nexus 5.
1966      */
extractAndApplyTintFromPhotoViewAsynchronously()1967     private void extractAndApplyTintFromPhotoViewAsynchronously() {
1968         if (mScroller == null) {
1969             return;
1970         }
1971         final Drawable imageViewDrawable = mPhotoView.getDrawable();
1972         new AsyncTask<Void, Void, MaterialPalette>() {
1973             @Override
1974             protected MaterialPalette doInBackground(Void... params) {
1975 
1976                 if (imageViewDrawable instanceof BitmapDrawable && mContactData != null
1977                         && mContactData.getThumbnailPhotoBinaryData() != null
1978                         && mContactData.getThumbnailPhotoBinaryData().length > 0) {
1979                     // Perform the color analysis on the thumbnail instead of the full sized
1980                     // image, so that our results will be as similar as possible to the Bugle
1981                     // app.
1982                     final Bitmap bitmap = BitmapFactory.decodeByteArray(
1983                             mContactData.getThumbnailPhotoBinaryData(), 0,
1984                             mContactData.getThumbnailPhotoBinaryData().length);
1985                     try {
1986                         final int primaryColor = colorFromBitmap(bitmap);
1987                         if (primaryColor != 0) {
1988                             return mMaterialColorMapUtils.calculatePrimaryAndSecondaryColor(
1989                                     primaryColor);
1990                         }
1991                     } finally {
1992                         bitmap.recycle();
1993                     }
1994                 }
1995                 if (imageViewDrawable instanceof LetterTileDrawable) {
1996                     final int primaryColor = ((LetterTileDrawable) imageViewDrawable).getColor();
1997                     return mMaterialColorMapUtils.calculatePrimaryAndSecondaryColor(primaryColor);
1998                 }
1999                 return MaterialColorMapUtils.getDefaultPrimaryAndSecondaryColors(getResources());
2000             }
2001 
2002             @Override
2003             protected void onPostExecute(MaterialPalette palette) {
2004                 super.onPostExecute(palette);
2005                 if (mHasComputedThemeColor) {
2006                     // If we had previously computed a theme color from the contact photo,
2007                     // then do not update the theme color. Changing the theme color several
2008                     // seconds after QC has started, as a result of an updated/upgraded photo,
2009                     // is a jarring experience. On the other hand, changing the theme color after
2010                     // a rotation or onNewIntent() is perfectly fine.
2011                     return;
2012                 }
2013                 // Check that the Photo has not changed. If it has changed, the new tint
2014                 // color needs to be extracted
2015                 if (imageViewDrawable == mPhotoView.getDrawable()) {
2016                     mHasComputedThemeColor = true;
2017                     setThemeColor(palette);
2018                 }
2019             }
2020         }.execute();
2021     }
2022 
setThemeColor(MaterialPalette palette)2023     private void setThemeColor(MaterialPalette palette) {
2024         // If the color is invalid, use the predefined default
2025         mColorFilterColor = palette.mPrimaryColor;
2026         mScroller.setHeaderTintColor(mColorFilterColor);
2027         mStatusBarColor = palette.mSecondaryColor;
2028         updateStatusBarColor();
2029 
2030         mColorFilter =
2031                 new PorterDuffColorFilter(mColorFilterColor, PorterDuff.Mode.SRC_ATOP);
2032         mContactCard.setColorAndFilter(mColorFilterColor, mColorFilter);
2033         mAboutCard.setColorAndFilter(mColorFilterColor, mColorFilter);
2034     }
2035 
updateStatusBarColor()2036     private void updateStatusBarColor() {
2037         if (mScroller == null || !CompatUtils.isLollipopCompatible()) {
2038             return;
2039         }
2040         final int desiredStatusBarColor;
2041         // Only use a custom status bar color if QuickContacts touches the top of the viewport.
2042         if (mScroller.getScrollNeededToBeFullScreen() <= 0) {
2043             desiredStatusBarColor = mStatusBarColor;
2044         } else {
2045             desiredStatusBarColor = Color.TRANSPARENT;
2046         }
2047         // Animate to the new color.
2048         final ObjectAnimator animation = ObjectAnimator.ofInt(getWindow(), "statusBarColor",
2049                 getWindow().getStatusBarColor(), desiredStatusBarColor);
2050         animation.setDuration(ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION);
2051         animation.setEvaluator(new ArgbEvaluator());
2052         animation.start();
2053     }
2054 
colorFromBitmap(Bitmap bitmap)2055     private int colorFromBitmap(Bitmap bitmap) {
2056         // Author of Palette recommends using 24 colors when analyzing profile photos.
2057         final int NUMBER_OF_PALETTE_COLORS = 24;
2058         final Palette palette = Palette.generate(bitmap, NUMBER_OF_PALETTE_COLORS);
2059         if (palette != null && palette.getVibrantSwatch() != null) {
2060             return palette.getVibrantSwatch().getRgb();
2061         }
2062         return 0;
2063     }
2064 
2065     private final LoaderCallbacks<Contact> mLoaderContactCallbacks =
2066             new LoaderCallbacks<Contact>() {
2067         @Override
2068         public void onLoaderReset(Loader<Contact> loader) {
2069             mContactData = null;
2070         }
2071 
2072         @Override
2073         public void onLoadFinished(Loader<Contact> loader, Contact data) {
2074             Trace.beginSection("onLoadFinished()");
2075             try {
2076 
2077                 if (isFinishing()) {
2078                     return;
2079                 }
2080                 if (data.isError()) {
2081                     // This means either the contact is invalid or we had an
2082                     // internal error such as an acore crash.
2083                     Log.i(TAG, "Failed to load contact: " + ((ContactLoader)loader).getLookupUri());
2084                     Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage,
2085                             Toast.LENGTH_LONG).show();
2086                     finish();
2087                     return;
2088                 }
2089                 if (data.isNotFound()) {
2090                     Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri());
2091                     Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage,
2092                             Toast.LENGTH_LONG).show();
2093                     finish();
2094                     return;
2095                 }
2096 
2097                 if (!mIsRecreatedInstance && !mShortcutUsageReported && data != null) {
2098                     mShortcutUsageReported = true;
2099                     DynamicShortcuts.reportShortcutUsed(QuickContactActivity.this,
2100                             data.getLookupKey());
2101                 }
2102                 bindContactData(data);
2103 
2104             } finally {
2105                 Trace.endSection();
2106             }
2107         }
2108 
2109         @Override
2110         public Loader<Contact> onCreateLoader(int id, Bundle args) {
2111             if (mLookupUri == null) {
2112                 Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early");
2113             }
2114             // Load all contact data. We need loadGroupMetaData=true to determine whether the
2115             // contact is invisible. If it is, we need to display an "Add to Contacts" MenuItem.
2116             return new ContactLoader(getApplicationContext(), mLookupUri,
2117                     true /*loadGroupMetaData*/, true /*postViewNotification*/,
2118                     true /*computeFormattedPhoneNumber*/);
2119         }
2120     };
2121 
2122     @Override
onBackPressed()2123     public void onBackPressed() {
2124         final int previousScreenType = getIntent().getIntExtra
2125                 (EXTRA_PREVIOUS_SCREEN_TYPE, ScreenType.UNKNOWN);
2126         if ((previousScreenType == ScreenType.ALL_CONTACTS
2127                 || previousScreenType == ScreenType.FAVORITES)
2128                 && !SharedPreferenceUtil.getHamburgerPromoTriggerActionHappenedBefore(this)) {
2129             SharedPreferenceUtil.setHamburgerPromoTriggerActionHappenedBefore(this);
2130         }
2131         if (mScroller != null) {
2132             if (!mIsExitAnimationInProgress) {
2133                 mScroller.scrollOffBottom();
2134             }
2135         } else {
2136             super.onBackPressed();
2137         }
2138     }
2139 
2140     @Override
finish()2141     public void finish() {
2142         super.finish();
2143 
2144         // override transitions to skip the standard window animations
2145         overridePendingTransition(0, 0);
2146     }
2147 
2148     @Override
onStop()2149     protected void onStop() {
2150         super.onStop();
2151 
2152         if (mEntriesAndActionsTask != null) {
2153             // Once the activity is stopped, we will no longer want to bind mEntriesAndActionsTask's
2154             // results on the UI thread. In some circumstances Activities are killed without
2155             // onStop() being called. This is not a problem, because in these circumstances
2156             // the entire process will be killed.
2157             mEntriesAndActionsTask.cancel(/* mayInterruptIfRunning = */ false);
2158         }
2159     }
2160 
2161     @Override
onDestroy()2162     public void onDestroy() {
2163         LocalBroadcastManager.getInstance(this).unregisterReceiver(mListener);
2164         super.onDestroy();
2165     }
2166 
2167     /**
2168      * Returns true if it is possible to edit the current contact.
2169      */
isContactEditable()2170     private boolean isContactEditable() {
2171         return mContactData != null && !mContactData.isDirectoryEntry();
2172     }
2173 
2174     /**
2175      * Returns true if it is possible to share the current contact.
2176      */
isContactShareable()2177     private boolean isContactShareable() {
2178         return mContactData != null && !mContactData.isDirectoryEntry();
2179     }
2180 
getEditContactIntent()2181     private Intent getEditContactIntent() {
2182         return EditorIntents.createEditContactIntent(QuickContactActivity.this,
2183                 mContactData.getLookupUri(),
2184                 mHasComputedThemeColor
2185                         ? new MaterialPalette(mColorFilterColor, mStatusBarColor) : null,
2186                 mContactData.getPhotoId());
2187     }
2188 
editContact()2189     private void editContact() {
2190         mHasIntentLaunched = true;
2191         mContactLoader.cacheResult();
2192         startActivityForResult(getEditContactIntent(), REQUEST_CODE_CONTACT_EDITOR_ACTIVITY);
2193     }
2194 
deleteContact()2195     private void deleteContact() {
2196         final Uri contactUri = mContactData.getLookupUri();
2197         ContactDeletionInteraction.start(this, contactUri, /* finishActivityWhenDone =*/ true);
2198     }
2199 
toggleStar(MenuItem starredMenuItem, boolean isStarred)2200     private void toggleStar(MenuItem starredMenuItem, boolean isStarred) {
2201         // To improve responsiveness, swap out the picture (and tag) in the UI already
2202         ContactDisplayUtils.configureStarredMenuItem(starredMenuItem,
2203                 mContactData.isDirectoryEntry(), mContactData.isUserProfile(), !isStarred);
2204 
2205         // Now perform the real save
2206         final Intent intent = ContactSaveService.createSetStarredIntent(
2207                 QuickContactActivity.this, mContactData.getLookupUri(), !isStarred);
2208         startService(intent);
2209 
2210         final CharSequence accessibilityText = !isStarred
2211                 ? getResources().getText(R.string.description_action_menu_add_star)
2212                 : getResources().getText(R.string.description_action_menu_remove_star);
2213         // Accessibility actions need to have an associated view. We can't access the MenuItem's
2214         // underlying view, so put this accessibility action on the root view.
2215         mScroller.announceForAccessibility(accessibilityText);
2216     }
2217 
shareContact()2218     private void shareContact() {
2219         final String lookupKey = mContactData.getLookupKey();
2220         final Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey);
2221         final Intent intent = new Intent(Intent.ACTION_SEND);
2222         intent.setType(Contacts.CONTENT_VCARD_TYPE);
2223         intent.putExtra(Intent.EXTRA_STREAM, shareUri);
2224 
2225         // Launch chooser to share contact via
2226         MessageFormat msgFormat = new MessageFormat(
2227             getResources().getString(R.string.title_share_via),
2228             Locale.getDefault());
2229         Map<String, Object> arguments = new HashMap<>();
2230         arguments.put("count", 1);
2231         CharSequence chooseTitle = msgFormat.format(arguments);
2232         final Intent chooseIntent = Intent.createChooser(intent, chooseTitle);
2233 
2234         try {
2235             mHasIntentLaunched = true;
2236             ImplicitIntentsUtil.startActivityOutsideApp(this, chooseIntent);
2237         } catch (final ActivityNotFoundException ex) {
2238             Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show();
2239         }
2240     }
2241 
2242     /**
2243      * Creates a launcher shortcut with the current contact.
2244      */
createLauncherShortcutWithContact()2245     private void createLauncherShortcutWithContact() {
2246         if (BuildCompat.isAtLeastO()) {
2247             final ShortcutManager shortcutManager = (ShortcutManager)
2248                     getSystemService(SHORTCUT_SERVICE);
2249             final DynamicShortcuts shortcuts =
2250                     new DynamicShortcuts(QuickContactActivity.this);
2251             String displayName = mContactData.getDisplayName();
2252             if (displayName == null) {
2253                 displayName = getString(R.string.missing_name);
2254             }
2255             final ShortcutInfo shortcutInfo = shortcuts.getQuickContactShortcutInfo(
2256                     mContactData.getId(), mContactData.getLookupKey(), displayName);
2257             if (shortcutInfo != null) {
2258                 shortcutManager.requestPinShortcut(shortcutInfo, null);
2259             }
2260         } else {
2261             final ShortcutIntentBuilder builder = new ShortcutIntentBuilder(this,
2262                     new OnShortcutIntentCreatedListener() {
2263 
2264                         @Override
2265                         public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) {
2266                             // Broadcast the shortcutIntent to the launcher to create a
2267                             // shortcut to this contact
2268                             shortcutIntent.setAction(ACTION_INSTALL_SHORTCUT);
2269                             QuickContactActivity.this.sendBroadcast(shortcutIntent);
2270                             // Send a toast to give feedback to the user that a shortcut to this
2271                             // contact was added to the launcher.
2272                             final String displayName = shortcutIntent
2273                                     .getStringExtra(Intent.EXTRA_SHORTCUT_NAME);
2274                             final String toastMessage = TextUtils.isEmpty(displayName)
2275                                     ? getString(R.string.createContactShortcutSuccessful_NoName)
2276                                     : getString(R.string.createContactShortcutSuccessful,
2277                                             displayName);
2278                             Toast.makeText(QuickContactActivity.this, toastMessage,
2279                                     Toast.LENGTH_SHORT).show();
2280                         }
2281                     });
2282             builder.createContactShortcutIntent(mContactData.getLookupUri());
2283         }
2284     }
2285 
isShortcutCreatable()2286     private boolean isShortcutCreatable() {
2287         if (mContactData == null || mContactData.isUserProfile() ||
2288                 mContactData.isDirectoryEntry()) {
2289             return false;
2290         }
2291 
2292         if (BuildCompat.isAtLeastO()) {
2293             final ShortcutManager manager = (ShortcutManager)
2294                     getSystemService(Context.SHORTCUT_SERVICE);
2295             return manager.isRequestPinShortcutSupported();
2296         }
2297 
2298         final Intent createShortcutIntent = new Intent();
2299         createShortcutIntent.setAction(ACTION_INSTALL_SHORTCUT);
2300         final List<ResolveInfo> receivers = getPackageManager()
2301                 .queryBroadcastReceivers(createShortcutIntent, 0);
2302         return receivers != null && receivers.size() > 0;
2303     }
2304 
setStateForPhoneMenuItems(Contact contact)2305     private void setStateForPhoneMenuItems(Contact contact) {
2306         if (contact != null) {
2307             mSendToVoicemailState = contact.isSendToVoicemail();
2308             mCustomRingtone = contact.getCustomRingtone();
2309             mArePhoneOptionsChangable = isContactEditable()
2310                     && PhoneCapabilityTester.isPhone(this);
2311         }
2312     }
2313 
2314     @Override
onCreateOptionsMenu(Menu menu)2315     public boolean onCreateOptionsMenu(Menu menu) {
2316         final MenuInflater inflater = getMenuInflater();
2317         inflater.inflate(R.menu.quickcontact, menu);
2318         return true;
2319     }
2320 
2321     @Override
onPrepareOptionsMenu(Menu menu)2322     public boolean onPrepareOptionsMenu(Menu menu) {
2323         if (mContactData != null) {
2324             final MenuItem starredMenuItem = menu.findItem(R.id.menu_star);
2325             ContactDisplayUtils.configureStarredMenuItem(starredMenuItem,
2326                     mContactData.isDirectoryEntry(), mContactData.isUserProfile(),
2327                     mContactData.getStarred());
2328 
2329             // Configure edit MenuItem
2330             final MenuItem editMenuItem = menu.findItem(R.id.menu_edit);
2331             editMenuItem.setVisible(true);
2332             if (DirectoryContactUtil.isDirectoryContact(mContactData) || InvisibleContactUtil
2333                     .isInvisibleAndAddable(mContactData, this)) {
2334                 editMenuItem.setIcon(R.drawable.quantum_ic_person_add_vd_theme_24);
2335                 editMenuItem.setTitle(R.string.menu_add_contact);
2336             } else if (isContactEditable()) {
2337                 editMenuItem.setIcon(R.drawable.quantum_ic_create_vd_theme_24);
2338                 editMenuItem.setTitle(R.string.menu_editContact);
2339             } else {
2340                 editMenuItem.setVisible(false);
2341             }
2342 
2343             // The link menu item is only visible if this has a single raw contact.
2344             final MenuItem joinMenuItem = menu.findItem(R.id.menu_join);
2345             joinMenuItem.setVisible(!InvisibleContactUtil.isInvisibleAndAddable(mContactData, this)
2346                     && isContactEditable() && !mContactData.isUserProfile()
2347                     && !mContactData.isMultipleRawContacts());
2348 
2349             // Viewing linked contacts can only happen if there are multiple raw contacts and
2350             // the link menu isn't available.
2351             final MenuItem linkedContactsMenuItem = menu.findItem(R.id.menu_linked_contacts);
2352             linkedContactsMenuItem.setVisible(mContactData.isMultipleRawContacts()
2353                     && !joinMenuItem.isVisible());
2354 
2355             final MenuItem deleteMenuItem = menu.findItem(R.id.menu_delete);
2356             deleteMenuItem.setVisible(isContactEditable() && !mContactData.isUserProfile());
2357 
2358             final MenuItem shareMenuItem = menu.findItem(R.id.menu_share);
2359             shareMenuItem.setVisible(isContactShareable());
2360 
2361             final MenuItem shortcutMenuItem = menu.findItem(R.id.menu_create_contact_shortcut);
2362             shortcutMenuItem.setVisible(isShortcutCreatable());
2363 
2364             // Hide telephony-related settings (ringtone, send to voicemail)
2365             // if we don't have a telephone
2366             final MenuItem ringToneMenuItem = menu.findItem(R.id.menu_set_ringtone);
2367             ringToneMenuItem.setVisible(!mContactData.isUserProfile() && mArePhoneOptionsChangable);
2368 
2369             final MenuItem sendToVoiceMailMenuItem = menu.findItem(R.id.menu_send_to_voicemail);
2370             sendToVoiceMailMenuItem.setVisible(
2371                     Build.VERSION.SDK_INT < Build.VERSION_CODES.M
2372                             && !mContactData.isUserProfile()
2373                             && mArePhoneOptionsChangable);
2374             sendToVoiceMailMenuItem.setTitle(mSendToVoicemailState
2375                     ? R.string.menu_unredirect_calls_to_vm : R.string.menu_redirect_calls_to_vm);
2376 
2377             final MenuItem helpMenu = menu.findItem(R.id.menu_help);
2378             helpMenu.setVisible(HelpUtils.isHelpAndFeedbackAvailable());
2379 
2380             return true;
2381         }
2382         return false;
2383     }
2384 
2385     @Override
2386     public boolean onOptionsItemSelected(MenuItem item) {
2387         final int id = item.getItemId();
2388         if (id == R.id.menu_star) {// Make sure there is a contact
2389             if (mContactData != null) {
2390                 // Read the current starred value from the UI instead of using the last
2391                 // loaded state. This allows rapid tapping without writing the same
2392                 // value several times
2393                 final boolean isStarred = item.isChecked();
2394                 Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD,
2395                         isStarred ? ActionType.UNSTAR : ActionType.STAR,
2396                             /* thirdPartyAction */ null);
2397                 toggleStar(item, isStarred);
2398             }
2399         } else if (id == R.id.menu_edit) {
2400             if (DirectoryContactUtil.isDirectoryContact(mContactData)) {
2401                 Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD,
2402                         ActionType.ADD, /* thirdPartyAction */ null);
2403 
2404                 // This action is used to launch the contact selector, with the option of
2405                 // creating a new contact. Creating a new contact is an INSERT, while selecting
2406                 // an exisiting one is an edit. The fields in the edit screen will be
2407                 // prepopulated with data.
2408 
2409                 final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
2410                 intent.setType(Contacts.CONTENT_ITEM_TYPE);
2411 
2412                 ArrayList<ContentValues> values = mContactData.getContentValues();
2413 
2414                 // Only pre-fill the name field if the provided display name is an nickname
2415                 // or better (e.g. structured name, nickname)
2416                 if (mContactData.getDisplayNameSource() >= DisplayNameSources.NICKNAME) {
2417                     intent.putExtra(Intents.Insert.NAME, mContactData.getDisplayName());
2418                 } else if (mContactData.getDisplayNameSource()
2419                         == DisplayNameSources.ORGANIZATION) {
2420                     // This is probably an organization. Instead of copying the organization
2421                     // name into a name entry, copy it into the organization entry. This
2422                     // way we will still consider the contact an organization.
2423                     final ContentValues organization = new ContentValues();
2424                     organization.put(Organization.COMPANY, mContactData.getDisplayName());
2425                     organization.put(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
2426                     values.add(organization);
2427                 }
2428 
2429                 intent.putExtra(Intents.Insert.DATA, values);
2430 
2431                 // If the contact can only export to the same account, add it to the intent.
2432                 // Otherwise the ContactEditorFragment will show a dialog for selecting
2433                 // an account.
2434                 if (mContactData.getDirectoryExportSupport() ==
2435                         Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY) {
2436                     intent.putExtra(Intents.Insert.EXTRA_ACCOUNT,
2437                             new Account(mContactData.getDirectoryAccountName(),
2438                                     mContactData.getDirectoryAccountType()));
2439                     intent.putExtra(Intents.Insert.EXTRA_DATA_SET,
2440                             mContactData.getRawContacts().get(0).getDataSet());
2441                 }
2442 
2443                 // Add this flag to disable the delete menu option on directory contact joins
2444                 // with local contacts. The delete option is ambiguous when joining contacts.
2445                 intent.putExtra(
2446                         ContactEditorFragment.INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION,
2447                         true);
2448 
2449                 intent.setPackage(getPackageName());
2450                 startActivityForResult(intent, REQUEST_CODE_CONTACT_SELECTION_ACTIVITY);
2451             } else if (InvisibleContactUtil.isInvisibleAndAddable(mContactData, this)) {
2452                 Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD,
2453                         ActionType.ADD, /* thirdPartyAction */ null);
2454                 InvisibleContactUtil.addToDefaultGroup(mContactData, this);
2455             } else if (isContactEditable()) {
2456                 Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD,
2457                         ActionType.EDIT, /* thirdPartyAction */ null);
2458                 editContact();
2459             }
2460         } else if (id == R.id.menu_join) {
2461             return doJoinContactAction();
2462         } else if (id == R.id.menu_linked_contacts) {
2463             return showRawContactPickerDialog();
2464         } else if (id == R.id.menu_delete) {
Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD, ActionType.REMOVE, null)2465             Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD,
2466                     ActionType.REMOVE, /* thirdPartyAction */ null);
2467             if (isContactEditable()) {
deleteContact()2468                 deleteContact();
2469             }
2470         } else if (id == R.id.menu_share) {
Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD, ActionType.SHARE, null)2471             Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD,
2472                     ActionType.SHARE, /* thirdPartyAction */ null);
2473             if (isContactShareable()) {
shareContact()2474                 shareContact();
2475             }
2476         } else if (id == R.id.menu_create_contact_shortcut) {
Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD, ActionType.SHORTCUT, null)2477             Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD,
2478                     ActionType.SHORTCUT, /* thirdPartyAction */ null);
2479             if (isShortcutCreatable()) {
createLauncherShortcutWithContact()2480                 createLauncherShortcutWithContact();
2481             }
2482         } else if (id == R.id.menu_set_ringtone) {
doPickRingtone()2483             doPickRingtone();
2484         } else if (id == R.id.menu_send_to_voicemail) {// Update state and save
2485             mSendToVoicemailState = !mSendToVoicemailState;
2486             item.setTitle(mSendToVoicemailState
2487                     ? R.string.menu_unredirect_calls_to_vm
2488                     : R.string.menu_redirect_calls_to_vm);
2489             final Intent intent = ContactSaveService.createSetSendToVoicemail(
2490                     this, mLookupUri, mSendToVoicemailState);
2491             this.startService(intent);
2492         } else if (id == R.id.menu_help) {
Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD, ActionType.HELP, null)2493             Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD,
2494                     ActionType.HELP, /* thirdPartyAction */ null);
HelpUtils.launchHelpAndFeedbackForContactScreen(this)2495             HelpUtils.launchHelpAndFeedbackForContactScreen(this);
2496         } else {
Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD, ActionType.UNKNOWN_ACTION, null)2497             Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD,
2498                     ActionType.UNKNOWN_ACTION, /* thirdPartyAction */ null);
2499             return super.onOptionsItemSelected(item);
2500         }
2501         return true;
2502     }
2503 
showRawContactPickerDialog()2504     private boolean showRawContactPickerDialog() {
2505         if (mContactData == null) return false;
2506         startActivityForResult(EditorIntents.createViewLinkedContactsIntent(
2507                 QuickContactActivity.this,
2508                 mContactData.getLookupUri(),
2509                 mHasComputedThemeColor
2510                         ? new MaterialPalette(mColorFilterColor, mStatusBarColor)
2511                         : null),
2512                 REQUEST_CODE_CONTACT_EDITOR_ACTIVITY);
2513         return true;
2514     }
2515 
doJoinContactAction()2516     private boolean doJoinContactAction() {
2517         if (mContactData == null) return false;
2518 
2519         mPreviousContactId = mContactData.getId();
2520         final Intent intent = new Intent(this, ContactSelectionActivity.class);
2521         intent.setAction(UiIntentActions.PICK_JOIN_CONTACT_ACTION);
2522         intent.putExtra(UiIntentActions.TARGET_CONTACT_ID_EXTRA_KEY, mPreviousContactId);
2523         startActivityForResult(intent, REQUEST_CODE_JOIN);
2524         return true;
2525     }
2526 
2527     /**
2528      * Performs aggregation with the contact selected by the user from suggestions or A-Z list.
2529      */
joinAggregate(final long contactId)2530     private void joinAggregate(final long contactId) {
2531         final Intent intent = ContactSaveService.createJoinContactsIntent(
2532                 this, mPreviousContactId, contactId, QuickContactActivity.class,
2533                 Intent.ACTION_VIEW);
2534         this.startService(intent);
2535         showLinkProgressBar();
2536     }
2537 
2538 
doPickRingtone()2539     private void doPickRingtone() {
2540         final Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
2541         // Allow user to pick 'Default'
2542         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
2543         // Show only ringtones
2544         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE);
2545         // Allow the user to pick a silent ringtone
2546         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
2547 
2548         final Uri ringtoneUri = EditorUiUtils.getRingtoneUriFromString(mCustomRingtone,
2549                 CURRENT_API_VERSION);
2550 
2551         // Put checkmark next to the current ringtone for this contact
2552         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, ringtoneUri);
2553 
2554         // Launch!
2555         try {
2556             startActivityForResult(intent, REQUEST_CODE_PICK_RINGTONE);
2557         } catch (ActivityNotFoundException ex) {
2558             Toast.makeText(this, R.string.missing_app, Toast.LENGTH_SHORT).show();
2559         }
2560     }
2561 
dismissProgressBar()2562     private void dismissProgressBar() {
2563         if (mProgressDialog != null && mProgressDialog.isShowing()) {
2564             mProgressDialog.dismiss();
2565         }
2566     }
2567 
showLinkProgressBar()2568     private void showLinkProgressBar() {
2569         mProgressDialog.setMessage(getString(R.string.contacts_linking_progress_bar));
2570         mProgressDialog.show();
2571     }
2572 
showUnlinkProgressBar()2573     private void showUnlinkProgressBar() {
2574         mProgressDialog.setMessage(getString(R.string.contacts_unlinking_progress_bar));
2575         mProgressDialog.show();
2576     }
2577 
maybeShowProgressDialog()2578     private void maybeShowProgressDialog() {
2579         if (ContactSaveService.getState().isActionPending(
2580                 ContactSaveService.ACTION_SPLIT_CONTACT)) {
2581             showUnlinkProgressBar();
2582         } else if (ContactSaveService.getState().isActionPending(
2583                 ContactSaveService.ACTION_JOIN_CONTACTS)) {
2584             showLinkProgressBar();
2585         }
2586     }
2587 
2588     private class SaveServiceListener extends BroadcastReceiver {
2589         @Override
onReceive(Context context, Intent intent)2590         public void onReceive(Context context, Intent intent) {
2591             if (Log.isLoggable(TAG, Log.DEBUG)) {
2592                 Log.d(TAG, "Got broadcast from save service " + intent);
2593             }
2594             if (ContactSaveService.BROADCAST_LINK_COMPLETE.equals(intent.getAction())
2595                     || ContactSaveService.BROADCAST_UNLINK_COMPLETE.equals(intent.getAction())) {
2596                 dismissProgressBar();
2597                 if (ContactSaveService.BROADCAST_UNLINK_COMPLETE.equals(intent.getAction())) {
2598                     finish();
2599                 }
2600             }
2601         }
2602     }
2603 }
2604