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