Home | History | Annotate | Download | only in quickcontact
      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
    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
    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
    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
    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 
    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
    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
    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 
    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 
   1038     private void destroyInteractionLoaders() {
   1039         for (int interactionLoaderId : mRecentLoaderIds) {
   1040             getLoaderManager().destroyLoader(interactionLoaderId);
   1041         }
   1042     }
   1043 
   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 
   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. */
   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. */
   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      */
   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      */
   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 
   1161     private void bindDataToCards(Cp2DataCardModel cp2DataCardModel) {
   1162         startInteractionLoaders(cp2DataCardModel);
   1163         populateContactAndAboutCard(cp2DataCardModel, /* shouldAddPhoneticName */ true);
   1164     }
   1165 
   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 
   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 
   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
   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
   1283     protected void onPause() {
   1284         super.onPause();
   1285         dismissProgressBar();
   1286     }
   1287 
   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      */
   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      */
   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      */
   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      */
   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 
   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      */
   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      */
   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      */
   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 
   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 
   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 
   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      */
   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 
   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 
   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 
   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 
   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
   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
   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 
   2467     private boolean isAllRecentDataLoaded() {
   2468         return mRecentLoaderResults.size() == mRecentLoaderIds.length;
   2469     }
   2470 
   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
   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
   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      */
   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      */
   2641     private boolean isContactShareable() {
   2642         return mContactData != null && !mContactData.isDirectoryEntry();
   2643     }
   2644 
   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 
   2653     private void editContact() {
   2654         mHasIntentLaunched = true;
   2655         mContactLoader.cacheResult();
   2656         startActivityForResult(getEditContactIntent(), REQUEST_CODE_CONTACT_EDITOR_ACTIVITY);
   2657     }
   2658 
   2659     private void deleteContact() {
   2660         final Uri contactUri = mContactData.getLookupUri();
   2661         ContactDeletionInteraction.start(this, contactUri, /* finishActivityWhenDone =*/ true);
   2662     }
   2663 
   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 
   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      */
   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 
   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 
   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
   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
   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) {
   2932             Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD,
   2933                     ActionType.REMOVE, /* thirdPartyAction */ null);
   2934             if (isContactEditable()) {
   2935                 deleteContact();
   2936             }
   2937         } else if (id == R.id.menu_share) {
   2938             Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD,
   2939                     ActionType.SHARE, /* thirdPartyAction */ null);
   2940             if (isContactShareable()) {
   2941                 shareContact();
   2942             }
   2943         } else if (id == R.id.menu_create_contact_shortcut) {
   2944             Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD,
   2945                     ActionType.SHORTCUT, /* thirdPartyAction */ null);
   2946             if (isShortcutCreatable()) {
   2947                 createLauncherShortcutWithContact();
   2948             }
   2949         } else if (id == R.id.menu_set_ringtone) {
   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) {
   2960             Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD,
   2961                     ActionType.HELP, /* thirdPartyAction */ null);
   2962             HelpUtils.launchHelpAndFeedbackForContactScreen(this);
   2963         } else {
   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 
   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 
   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      */
   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 
   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 
   3029     private void dismissProgressBar() {
   3030         if (mProgressDialog != null && mProgressDialog.isShowing()) {
   3031             mProgressDialog.dismiss();
   3032         }
   3033     }
   3034 
   3035     private void showLinkProgressBar() {
   3036         mProgressDialog.setMessage(getString(R.string.contacts_linking_progress_bar));
   3037         mProgressDialog.show();
   3038     }
   3039 
   3040     private void showUnlinkProgressBar() {
   3041         mProgressDialog.setMessage(getString(R.string.contacts_unlinking_progress_bar));
   3042         mProgressDialog.show();
   3043     }
   3044 
   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
   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