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