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