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