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