blob: c8bce4c2b8e94dd279ac2e7709080e520c0a8eae [file] [log] [blame]
Daniel Lehmannedb576a2011-07-27 16:45:13 -07001/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.contacts.quickcontact;
18
Paul Soulosab54ea12014-08-28 17:22:38 -070019import android.accounts.Account;
Brian Attwell8a6f4ad2014-06-06 21:54:53 -070020import android.animation.ArgbEvaluator;
Brian Attwellb7e43642014-06-02 14:33:04 -070021import android.animation.ObjectAnimator;
Daniel Lehmannedb576a2011-07-27 16:45:13 -070022import android.app.Activity;
Brian Attwelld28851f2014-06-10 13:25:07 -070023import android.app.Fragment;
Daniel Lehmanncb8d73f2011-12-16 17:39:50 +020024import android.app.LoaderManager.LoaderCallbacks;
Paul Souloseb64a4b2014-07-07 17:03:27 -070025import android.app.SearchManager;
Brian Attwell752cccf2014-06-10 16:24:04 -070026import android.content.ActivityNotFoundException;
Daniel Lehmannedb576a2011-07-27 16:45:13 -070027import android.content.ContentUris;
Paul Soulosea5e0b72014-07-08 18:09:44 -070028import android.content.ContentValues;
Brian Attwell6bb01342014-08-20 23:16:00 -070029import android.content.Context;
Daniel Lehmannedb576a2011-07-27 16:45:13 -070030import android.content.Intent;
Daniel Lehmanncb8d73f2011-12-16 17:39:50 +020031import android.content.Loader;
Paul Soulosdd7419d2014-07-15 11:22:13 -070032import android.content.pm.PackageManager;
33import android.content.pm.ResolveInfo;
Paul Soulos23e28362014-08-29 14:57:08 -070034import android.content.res.Resources;
Brian Attwell31b2d422014-06-05 00:14:58 -070035import android.graphics.Bitmap;
Brian Attwell95c268e2014-08-26 22:04:15 -070036import android.graphics.BitmapFactory;
Brian Attwell8a6f4ad2014-06-06 21:54:53 -070037import android.graphics.Color;
Paul Souloseb64a4b2014-07-07 17:03:27 -070038import android.graphics.PorterDuff;
39import android.graphics.PorterDuffColorFilter;
Brian Attwell31b2d422014-06-05 00:14:58 -070040import android.graphics.drawable.BitmapDrawable;
Brian Attwell8a6f4ad2014-06-06 21:54:53 -070041import android.graphics.drawable.ColorDrawable;
Brian Attwell31b2d422014-06-05 00:14:58 -070042import android.graphics.drawable.Drawable;
Paul Souloseb64a4b2014-07-07 17:03:27 -070043import android.net.ParseException;
Daniel Lehmannedb576a2011-07-27 16:45:13 -070044import android.net.Uri;
Paul Souloseb64a4b2014-07-07 17:03:27 -070045import android.net.WebAddress;
Brian Attwell31b2d422014-06-05 00:14:58 -070046import android.os.AsyncTask;
Daniel Lehmannedb576a2011-07-27 16:45:13 -070047import android.os.Bundle;
Brian Attwell8a6f4ad2014-06-06 21:54:53 -070048import android.os.Trace;
Paul Souloseb64a4b2014-07-07 17:03:27 -070049import android.provider.CalendarContract;
Brian Attwell752cccf2014-06-10 16:24:04 -070050import android.provider.ContactsContract;
Daniel Lehmannedb576a2011-07-27 16:45:13 -070051import android.provider.ContactsContract.CommonDataKinds.Email;
Paul Souloseb64a4b2014-07-07 17:03:27 -070052import android.provider.ContactsContract.CommonDataKinds.Event;
53import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
54import android.provider.ContactsContract.CommonDataKinds.Identity;
55import android.provider.ContactsContract.CommonDataKinds.Im;
56import android.provider.ContactsContract.CommonDataKinds.Nickname;
57import android.provider.ContactsContract.CommonDataKinds.Note;
58import android.provider.ContactsContract.CommonDataKinds.Organization;
Daniel Lehmannedb576a2011-07-27 16:45:13 -070059import android.provider.ContactsContract.CommonDataKinds.Phone;
Paul Souloseb64a4b2014-07-07 17:03:27 -070060import android.provider.ContactsContract.CommonDataKinds.Relation;
Daniel Lehmannedb576a2011-07-27 16:45:13 -070061import android.provider.ContactsContract.CommonDataKinds.SipAddress;
62import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
63import android.provider.ContactsContract.CommonDataKinds.Website;
Brian Attwell752cccf2014-06-10 16:24:04 -070064import android.provider.ContactsContract.Contacts;
Paul Soulos60e51082014-07-10 12:33:04 -070065import android.provider.ContactsContract.Data;
Paul Soulosab54ea12014-08-28 17:22:38 -070066import android.provider.ContactsContract.Directory;
Paul Souloseb64a4b2014-07-07 17:03:27 -070067import android.provider.ContactsContract.DisplayNameSources;
Paul Soulosea5e0b72014-07-08 18:09:44 -070068import android.provider.ContactsContract.DataUsageFeedback;
Paul Soulosab54ea12014-08-28 17:22:38 -070069import android.provider.ContactsContract.Intents;
Daniel Lehmannedb576a2011-07-27 16:45:13 -070070import android.provider.ContactsContract.QuickContact;
71import android.provider.ContactsContract.RawContacts;
Brian Attwell31b2d422014-06-05 00:14:58 -070072import android.support.v7.graphics.Palette;
Jay Shrauner1cd88e32014-09-05 15:37:55 -070073import android.telecomm.PhoneAccount;
Nancy Chen70e3f4f2014-08-13 16:04:43 -070074import android.telecomm.TelecommManager;
Daniel Lehmannedb576a2011-07-27 16:45:13 -070075import android.text.TextUtils;
76import android.util.Log;
Paul Soulos2a4207f2014-07-31 17:09:05 -070077import android.view.ContextMenu;
78import android.view.ContextMenu.ContextMenuInfo;
Brian Attwelld28851f2014-06-10 13:25:07 -070079import android.view.Menu;
Brian Attwelld28851f2014-06-10 13:25:07 -070080import android.view.MenuInflater;
Paul Souloseb64a4b2014-07-07 17:03:27 -070081import android.view.MenuItem;
Nancy Chenf619e432014-08-18 20:15:12 -070082import android.view.MotionEvent;
Daniel Lehmannedb576a2011-07-27 16:45:13 -070083import android.view.View;
84import android.view.View.OnClickListener;
Paul Soulos2a4207f2014-07-31 17:09:05 -070085import android.view.View.OnCreateContextMenuListener;
Daisuke Miyakawa10d7df72011-07-29 16:11:05 -070086import android.view.WindowManager;
Daniel Lehmannedb576a2011-07-27 16:45:13 -070087import android.widget.Toast;
Brian Attwelld28851f2014-06-10 13:25:07 -070088import android.widget.Toolbar;
Daniel Lehmannedb576a2011-07-27 16:45:13 -070089
Christine Chen72b3ab12013-08-13 23:22:34 +020090import com.android.contacts.ContactSaveService;
Brian Attwelld28851f2014-06-10 13:25:07 -070091import com.android.contacts.ContactsActivity;
Brian Attwell2d150da2014-07-09 22:35:56 -070092import com.android.contacts.NfcHandler;
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070093import com.android.contacts.R;
Paul Souloseb64a4b2014-07-07 17:03:27 -070094import com.android.contacts.common.CallUtil;
Paul Soulos2a4207f2014-07-31 17:09:05 -070095import com.android.contacts.common.ClipboardUtils;
Paul Souloseb64a4b2014-07-07 17:03:27 -070096import com.android.contacts.common.Collapser;
97import com.android.contacts.common.ContactsUtils;
Brian Attwelld28851f2014-06-10 13:25:07 -070098import com.android.contacts.common.editor.SelectAccountDialogFragment;
Nancy Chen70e3f4f2014-08-13 16:04:43 -070099import com.android.contacts.common.interactions.TouchPointManager;
Brian Attwell31b2d422014-06-05 00:14:58 -0700100import com.android.contacts.common.lettertiles.LetterTileDrawable;
Brian Attwell752cccf2014-06-10 16:24:04 -0700101import com.android.contacts.common.list.ShortcutIntentBuilder;
102import com.android.contacts.common.list.ShortcutIntentBuilder.OnShortcutIntentCreatedListener;
Chiao Cheng0d5588d2012-11-26 15:34:14 -0800103import com.android.contacts.common.model.AccountTypeManager;
Yorke Leecd321f62013-10-28 15:20:15 -0700104import com.android.contacts.common.model.Contact;
105import com.android.contacts.common.model.ContactLoader;
106import com.android.contacts.common.model.RawContact;
Chiao Cheng428f0082012-11-13 18:38:56 -0800107import com.android.contacts.common.model.account.AccountType;
Brian Attwelld28851f2014-06-10 13:25:07 -0700108import com.android.contacts.common.model.account.AccountWithDataSet;
Yorke Leecd321f62013-10-28 15:20:15 -0700109import com.android.contacts.common.model.dataitem.DataItem;
Chiao Cheng428f0082012-11-13 18:38:56 -0800110import com.android.contacts.common.model.dataitem.DataKind;
Yorke Leecd321f62013-10-28 15:20:15 -0700111import com.android.contacts.common.model.dataitem.EmailDataItem;
Paul Souloseb64a4b2014-07-07 17:03:27 -0700112import com.android.contacts.common.model.dataitem.EventDataItem;
Yorke Leecd321f62013-10-28 15:20:15 -0700113import com.android.contacts.common.model.dataitem.ImDataItem;
Paul Souloseb64a4b2014-07-07 17:03:27 -0700114import com.android.contacts.common.model.dataitem.NicknameDataItem;
115import com.android.contacts.common.model.dataitem.NoteDataItem;
116import com.android.contacts.common.model.dataitem.OrganizationDataItem;
Paul Soulosb3054e52014-06-05 16:46:02 -0700117import com.android.contacts.common.model.dataitem.PhoneDataItem;
Paul Souloseb64a4b2014-07-07 17:03:27 -0700118import com.android.contacts.common.model.dataitem.RelationDataItem;
119import com.android.contacts.common.model.dataitem.SipAddressDataItem;
120import com.android.contacts.common.model.dataitem.StructuredNameDataItem;
121import com.android.contacts.common.model.dataitem.StructuredPostalDataItem;
122import com.android.contacts.common.model.dataitem.WebsiteDataItem;
123import com.android.contacts.common.util.DateUtils;
Brian Attwellfaf97392014-07-10 18:32:30 -0700124import com.android.contacts.common.util.MaterialColorMapUtils;
125import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette;
Brian Attwelld41ab8a2014-08-07 11:08:55 -0700126import com.android.contacts.common.util.ViewUtil;
Paul Soulos333091a2014-07-22 13:54:41 -0700127import com.android.contacts.detail.ContactDisplayUtils;
Paul Soulos899aa212014-06-11 12:04:43 -0700128import com.android.contacts.interactions.CalendarInteractionsLoader;
Paul Soulosab840442014-06-17 14:08:40 -0700129import com.android.contacts.interactions.CallLogInteractionsLoader;
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700130import com.android.contacts.interactions.ContactDeletionInteraction;
Paul Soulosb3054e52014-06-05 16:46:02 -0700131import com.android.contacts.interactions.ContactInteraction;
Paul Soulosb3054e52014-06-05 16:46:02 -0700132import com.android.contacts.interactions.SmsInteractionsLoader;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700133import com.android.contacts.quickcontact.ExpandingEntryCardView.Entry;
Paul Soulos2a4207f2014-07-31 17:09:05 -0700134import com.android.contacts.quickcontact.ExpandingEntryCardView.EntryContextMenuInfo;
Paul Soulos2ed2a732014-08-12 11:58:39 -0700135import com.android.contacts.quickcontact.ExpandingEntryCardView.EntryTag;
Brian Attwelle8ce6ee2014-06-27 18:26:32 -0700136import com.android.contacts.quickcontact.ExpandingEntryCardView.ExpandingEntryCardViewListener;
Chiao Chenge0b2f1e2012-06-12 13:07:56 -0700137import com.android.contacts.util.ImageViewDrawableSetter;
Paul Souloseb64a4b2014-07-07 17:03:27 -0700138import com.android.contacts.util.PhoneCapabilityTester;
Brian Attwellb7e43642014-06-02 14:33:04 -0700139import com.android.contacts.util.SchedulingUtils;
Paul Souloseb64a4b2014-07-07 17:03:27 -0700140import com.android.contacts.util.StructuredPostalUtils;
Brian Attwellb7e43642014-06-02 14:33:04 -0700141import com.android.contacts.widget.MultiShrinkScroller;
142import com.android.contacts.widget.MultiShrinkScroller.MultiShrinkScrollerListener;
Brian Attwell02eaf112014-08-27 21:03:42 -0700143import com.android.contacts.widget.QuickContactImageView;
144
Chiao Chenge0b2f1e2012-06-12 13:07:56 -0700145import com.google.common.base.Preconditions;
146import com.google.common.collect.Lists;
147
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700148import java.util.ArrayList;
Paul Soulos899aa212014-06-11 12:04:43 -0700149import java.util.Arrays;
Paul Souloseb64a4b2014-07-07 17:03:27 -0700150import java.util.Calendar;
Paul Soulosb3054e52014-06-05 16:46:02 -0700151import java.util.Collections;
152import java.util.Comparator;
Paul Souloseb64a4b2014-07-07 17:03:27 -0700153import java.util.Date;
Daniel Lehmannedb576a2011-07-27 16:45:13 -0700154import java.util.HashMap;
Daniel Lehmannedb576a2011-07-27 16:45:13 -0700155import java.util.List;
Paul Soulosb3054e52014-06-05 16:46:02 -0700156import java.util.Map;
Daniel Lehmannedb576a2011-07-27 16:45:13 -0700157
Daniel Lehmannedb576a2011-07-27 16:45:13 -0700158/**
159 * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads
160 * data asynchronously, and then shows a popup with details centered around
161 * {@link Intent#getSourceBounds()}.
162 */
Brian Attwelld28851f2014-06-10 13:25:07 -0700163public class QuickContactActivity extends ContactsActivity {
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700164
165 /**
166 * QuickContacts immediately takes up the full screen. All possible information is shown.
167 * This value for {@link android.provider.ContactsContract.QuickContact#EXTRA_MODE}
168 * should only be used by the Contacts app.
169 */
170 public static final int MODE_FULLY_EXPANDED = 4;
171
Daniel Lehmannedb576a2011-07-27 16:45:13 -0700172 private static final String TAG = "QuickContact";
173
Brian Attwell9b889e62014-06-23 18:25:32 -0700174 private static final String KEY_THEME_COLOR = "theme_color";
175
Brian Attwell9b889e62014-06-23 18:25:32 -0700176 private static final int ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION = 150;
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700177 private static final int REQUEST_CODE_CONTACT_EDITOR_ACTIVITY = 1;
Brian Attwellbfa94422014-08-19 14:36:56 -0700178 private static final int SCRIM_COLOR = Color.argb(0xC8, 0, 0, 0);
Paul Soulosab54ea12014-08-28 17:22:38 -0700179 private static final int REQUEST_CODE_CONTACT_SELECTION_ACTIVITY = 2;
Paul Soulosea5e0b72014-07-08 18:09:44 -0700180 private static final String MIMETYPE_SMS = "vnd.android-dir/mms-sms";
Makoto Onukidfe8cc82012-05-17 18:03:44 -0700181
Brian Attwell752cccf2014-06-10 16:24:04 -0700182 /** This is the Intent action to install a shortcut in the launcher. */
183 private static final String ACTION_INSTALL_SHORTCUT =
184 "com.android.launcher.action.INSTALL_SHORTCUT";
Daniel Lehmannedb576a2011-07-27 16:45:13 -0700185
186 @SuppressWarnings("deprecation")
187 private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY;
188
Paul Soulose0055662014-07-10 16:33:08 -0700189 private static final String MIMETYPE_GPLUS_PROFILE =
190 "vnd.android.cursor.item/vnd.googleplus.profile";
191 private static final String INTENT_DATA_GPLUS_PROFILE_ADD_TO_CIRCLE = "Add to circle";
192 private static final String MIMETYPE_HANGOUTS =
193 "vnd.android.cursor.item/vnd.googleplus.profile.comm";
194 private static final String INTENT_DATA_HANGOUTS_VIDEO = "Start video call";
Paul Soulos48fc9122014-08-26 13:52:36 -0700195 private static final String CALL_ORIGIN_QUICK_CONTACTS_ACTIVITY =
196 "com.android.contacts.quickcontact.QuickContactActivity";
Paul Soulose0055662014-07-10 16:33:08 -0700197
Brian Attwell63176c92014-08-18 15:14:18 -0700198 /**
199 * The URI used to load the the Contact. Once the contact is loaded, use Contact#getLookupUri()
200 * instead of referencing this URI.
201 */
Daniel Lehmannedb576a2011-07-27 16:45:13 -0700202 private Uri mLookupUri;
203 private String[] mExcludeMimes;
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700204 private int mExtraMode;
205 private int mStatusBarColor;
206 private boolean mHasAlreadyBeenOpened;
Daniel Lehmannedb576a2011-07-27 16:45:13 -0700207
Brian Attwell02eaf112014-08-27 21:03:42 -0700208 private QuickContactImageView mPhotoView;
Paul Souloseb64a4b2014-07-07 17:03:27 -0700209 private ExpandingEntryCardView mContactCard;
Brian Attwell60953692014-07-11 17:18:46 -0700210 private ExpandingEntryCardView mNoContactDetailsCard;
Paul Soulosb3054e52014-06-05 16:46:02 -0700211 private ExpandingEntryCardView mRecentCard;
Paul Souloseb64a4b2014-07-07 17:03:27 -0700212 private ExpandingEntryCardView mAboutCard;
Brian Attwellb7e43642014-06-02 14:33:04 -0700213 private MultiShrinkScroller mScroller;
Brian Attwelld28851f2014-06-10 13:25:07 -0700214 private SelectAccountDialogFragmentListener mSelectAccountFragmentListener;
Brian Attwell6bb01342014-08-20 23:16:00 -0700215 private AsyncTask<Void, Void, Cp2DataCardModel> mEntriesAndActionsTask;
Brian Attwell30e1ef12014-09-02 15:49:20 -0700216 private AsyncTask<Void, Void, Void> mRecentDataTask;
Brian Attwell6bb01342014-08-20 23:16:00 -0700217 /**
218 * The last copy of Cp2DataCardModel that was passed to {@link #populateContactAndAboutCard}.
219 */
220 private Cp2DataCardModel mCachedCp2DataCardModel;
Brian Attwellf1076dc2014-08-12 21:28:21 -0700221 /**
222 * This scrim's opacity is controlled in two different ways. 1) Before the initial entrance
223 * animation finishes, the opacity is animated by a value animator. This is designed to
224 * distract the user from the length of the initial loading time. 2) After the initial
225 * entrance animation, the opacity is directly related to scroll position.
226 */
Brian Attwell9b889e62014-06-23 18:25:32 -0700227 private ColorDrawable mWindowScrim;
Brian Attwellf1076dc2014-08-12 21:28:21 -0700228 private boolean mIsEntranceAnimationFinished;
Brian Attwella3859ed2014-07-15 13:51:55 -0700229 private MaterialColorMapUtils mMaterialColorMapUtils;
Brian Attwell8477eaf2014-06-18 15:39:50 -0700230 private boolean mIsExitAnimationInProgress;
Brian Attwell8571dd32014-06-23 23:29:10 -0700231 private boolean mHasComputedThemeColor;
Daniel Lehmannedb576a2011-07-27 16:45:13 -0700232
Paul Soulosa42ef762014-08-20 10:26:10 -0700233 /**
234 * Used to stop the ExpandingEntry cards from adjusting between an entry click and the intent
235 * being launched.
236 */
237 private boolean mHasIntentLaunched;
238
Yorke Lee6cb6f6f2013-09-26 16:26:03 -0700239 private Contact mContactData;
Daniel Lehmann9815d7f2012-04-16 18:28:03 -0700240 private ContactLoader mContactLoader;
Brian Attwell9b889e62014-06-23 18:25:32 -0700241 private PorterDuffColorFilter mColorFilter;
Daniel Lehmann9815d7f2012-04-16 18:28:03 -0700242
Josh Gargus9758a922012-03-08 17:12:42 -0800243 private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter();
244
Daniel Lehmannedb576a2011-07-27 16:45:13 -0700245 /**
Paul Souloscb4fcc72014-07-15 14:08:50 -0700246 * {@link #LEADING_MIMETYPES} is used to sort MIME-types.
Daniel Lehmannedb576a2011-07-27 16:45:13 -0700247 *
248 * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog,
249 * in the order specified here.</p>
Daniel Lehmannedb576a2011-07-27 16:45:13 -0700250 */
251 private static final List<String> LEADING_MIMETYPES = Lists.newArrayList(
Paul Souloscb4fcc72014-07-15 14:08:50 -0700252 Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE,
Paul Soulos405ae402014-07-14 16:16:36 -0700253 StructuredPostal.CONTENT_ITEM_TYPE);
Daniel Lehmannedb576a2011-07-27 16:45:13 -0700254
Paul Soulosa153dba2014-08-06 13:44:57 -0700255 private static final List<String> SORTED_ABOUT_CARD_MIMETYPES = Lists.newArrayList(
256 Nickname.CONTENT_ITEM_TYPE,
257 // Phonetic name is inserted after nickname if it is available.
258 // No mimetype for phonetic name exists.
259 Website.CONTENT_ITEM_TYPE,
260 Organization.CONTENT_ITEM_TYPE,
261 Event.CONTENT_ITEM_TYPE,
262 Relation.CONTENT_ITEM_TYPE,
263 Im.CONTENT_ITEM_TYPE,
264 GroupMembership.CONTENT_ITEM_TYPE,
265 Identity.CONTENT_ITEM_TYPE,
266 Note.CONTENT_ITEM_TYPE);
Paul Souloseb64a4b2014-07-07 17:03:27 -0700267
Paul Soulosb3054e52014-06-05 16:46:02 -0700268 /** Id for the background contact loader */
269 private static final int LOADER_CONTACT_ID = 0;
270
Paul Soulosab840442014-06-17 14:08:40 -0700271 private static final String KEY_LOADER_EXTRA_PHONES =
272 QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_PHONES";
273
Paul Soulosb3054e52014-06-05 16:46:02 -0700274 /** Id for the background Sms Loader */
275 private static final int LOADER_SMS_ID = 1;
Paul Soulosb3054e52014-06-05 16:46:02 -0700276 private static final int MAX_SMS_RETRIEVE = 3;
Paul Soulosab840442014-06-17 14:08:40 -0700277
278 /** Id for the back Calendar Loader */
Paul Soulos899aa212014-06-11 12:04:43 -0700279 private static final int LOADER_CALENDAR_ID = 2;
Paul Soulosab840442014-06-17 14:08:40 -0700280 private static final String KEY_LOADER_EXTRA_EMAILS =
281 QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_EMAILS";
Paul Soulos899aa212014-06-11 12:04:43 -0700282 private static final int MAX_PAST_CALENDAR_RETRIEVE = 3;
283 private static final int MAX_FUTURE_CALENDAR_RETRIEVE = 3;
284 private static final long PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR =
Paul Soulos40d7a652014-09-03 13:54:11 -0700285 1L * 24L * 60L * 60L * 1000L /* 1 day */;
Paul Soulos899aa212014-06-11 12:04:43 -0700286 private static final long FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR =
Paul Soulos40d7a652014-09-03 13:54:11 -0700287 7L * 24L * 60L * 60L * 1000L /* 7 days */;
Paul Soulosb3054e52014-06-05 16:46:02 -0700288
Paul Soulosab840442014-06-17 14:08:40 -0700289 /** Id for the background Call Log Loader */
290 private static final int LOADER_CALL_LOG_ID = 3;
291 private static final int MAX_CALL_LOG_RETRIEVE = 3;
Brian Attwell60953692014-07-11 17:18:46 -0700292 private static final int MIN_NUM_CONTACT_ENTRIES_SHOWN = 3;
293 private static final int MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN = 3;
294 private static final int CARD_ENTRY_ID_EDIT_CONTACT = -2;
Paul Soulosab840442014-06-17 14:08:40 -0700295
296
297 private static final int[] mRecentLoaderIds = new int[]{
298 LOADER_SMS_ID,
299 LOADER_CALENDAR_ID,
300 LOADER_CALL_LOG_ID};
Paul Soulosa8fce962014-07-15 13:37:24 -0700301 private Map<Integer, List<ContactInteraction>> mRecentLoaderResults = new HashMap<>();
Paul Soulosb3054e52014-06-05 16:46:02 -0700302
Brian Attwelld28851f2014-06-10 13:25:07 -0700303 private static final String FRAGMENT_TAG_SELECT_ACCOUNT = "select_account_fragment";
Yorke Lee6cb6f6f2013-09-26 16:26:03 -0700304
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700305 final OnClickListener mEntryClickHandler = new OnClickListener() {
306 @Override
307 public void onClick(View v) {
Paul Soulos2ed2a732014-08-12 11:58:39 -0700308 final Object entryTagObject = v.getTag();
309 if (entryTagObject == null || !(entryTagObject instanceof EntryTag)) {
310 Log.w(TAG, "EntryTag was not used correctly");
311 return;
312 }
313 final EntryTag entryTag = (EntryTag) entryTagObject;
314 final Intent intent = entryTag.getIntent();
315 final int dataId = entryTag.getId();
316
Brian Attwell60953692014-07-11 17:18:46 -0700317 if (dataId == CARD_ENTRY_ID_EDIT_CONTACT) {
318 editContact();
319 return;
320 }
Paul Soulosea5e0b72014-07-08 18:09:44 -0700321
322 // Default to USAGE_TYPE_CALL. Usage is summed among all types for sorting each data id
323 // so the exact usage type is not necessary in all cases
324 String usageType = DataUsageFeedback.USAGE_TYPE_CALL;
325
Paul Soulos20bdf9d2014-07-28 14:31:54 -0700326 final Uri intentUri = intent.getData();
327 if ((intentUri != null && intentUri.getScheme() != null &&
Jay Shrauner1cd88e32014-09-05 15:37:55 -0700328 intentUri.getScheme().equals(ContactsUtils.SCHEME_SMSTO)) ||
Paul Soulosea5e0b72014-07-08 18:09:44 -0700329 (intent.getType() != null && intent.getType().equals(MIMETYPE_SMS))) {
330 usageType = DataUsageFeedback.USAGE_TYPE_SHORT_TEXT;
331 }
332
333 // Data IDs start at 1 so anything less is invalid
334 if (dataId > 0) {
Paul Soulos20bdf9d2014-07-28 14:31:54 -0700335 final Uri dataUsageUri = DataUsageFeedback.FEEDBACK_URI.buildUpon()
Paul Soulosea5e0b72014-07-08 18:09:44 -0700336 .appendPath(String.valueOf(dataId))
337 .appendQueryParameter(DataUsageFeedback.USAGE_TYPE, usageType)
338 .build();
339 final boolean successful = getContentResolver().update(
Paul Soulos20bdf9d2014-07-28 14:31:54 -0700340 dataUsageUri, new ContentValues(), null, null) > 0;
Paul Soulosea5e0b72014-07-08 18:09:44 -0700341 if (!successful) {
342 Log.w(TAG, "DataUsageFeedback increment failed");
343 }
344 } else {
345 Log.w(TAG, "Invalid Data ID");
346 }
347
Nancy Chen70e3f4f2014-08-13 16:04:43 -0700348 // Pass the touch point through the intent for use in the InCallUI
349 if (Intent.ACTION_CALL.equals(intent.getAction())) {
Nancy Chen1f8a3fd2014-08-20 10:59:25 -0700350 if (TouchPointManager.getInstance().hasValidPoint()) {
Nancy Chen70e3f4f2014-08-13 16:04:43 -0700351 Bundle extras = new Bundle();
Nancy Chen1f8a3fd2014-08-20 10:59:25 -0700352 extras.putParcelable(TouchPointManager.TOUCH_POINT,
353 TouchPointManager.getInstance().getPoint());
Nancy Chen70e3f4f2014-08-13 16:04:43 -0700354 intent.putExtra(TelecommManager.EXTRA_OUTGOING_CALL_EXTRAS, extras);
355 }
356 }
357
Paul Soulosa42ef762014-08-20 10:26:10 -0700358 mHasIntentLaunched = true;
Paul Soulosea5e0b72014-07-08 18:09:44 -0700359 startActivity(intent);
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700360 }
361 };
362
Brian Attwelle8ce6ee2014-06-27 18:26:32 -0700363 final ExpandingEntryCardViewListener mExpandingEntryCardViewListener
364 = new ExpandingEntryCardViewListener() {
365 @Override
366 public void onCollapse(int heightDelta) {
367 mScroller.prepareForShrinkingScrollChild(heightDelta);
368 }
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700369
370 @Override
371 public void onExpand(int heightDelta) {
372 mScroller.prepareForExpandingScrollChild();
373 }
Brian Attwelle8ce6ee2014-06-27 18:26:32 -0700374 };
375
Paul Soulos2a4207f2014-07-31 17:09:05 -0700376 private final OnCreateContextMenuListener mEntryContextMenuListener =
377 new OnCreateContextMenuListener() {
378 @Override
379 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
380 if (menuInfo == null) {
381 return;
382 }
383 EntryContextMenuInfo info = (EntryContextMenuInfo) menuInfo;
384 menu.setHeaderTitle(info.getCopyText());
385 menu.add(R.string.copy_text);
386 }
387 };
388
389 @Override
390 public boolean onContextItemSelected(MenuItem item) {
391 EntryContextMenuInfo menuInfo;
392 try {
393 menuInfo = (EntryContextMenuInfo) item.getMenuInfo();
394 } catch (ClassCastException e) {
395 Log.e(TAG, "bad menuInfo", e);
396 return false;
397 }
398
399 ClipboardUtils.copyText(this, menuInfo.getCopyLabel(), menuInfo.getCopyText(), true);
400 return true;
401 }
402
Brian Attwelld28851f2014-06-10 13:25:07 -0700403 /**
404 * Headless fragment used to handle account selection callbacks invoked from
405 * {@link DirectoryContactUtil}.
406 */
407 public static class SelectAccountDialogFragmentListener extends Fragment
408 implements SelectAccountDialogFragment.Listener {
409
410 private QuickContactActivity mQuickContactActivity;
411
412 public SelectAccountDialogFragmentListener() {}
413
414 @Override
415 public void onAccountChosen(AccountWithDataSet account, Bundle extraArgs) {
416 DirectoryContactUtil.createCopy(mQuickContactActivity.mContactData.getContentValues(),
417 account, mQuickContactActivity);
418 }
419
420 @Override
421 public void onAccountSelectorCancelled() {}
422
423 /**
424 * Set the parent activity. Since rotation can cause this fragment to be used across
425 * more than one activity instance, we need to explicitly set this value instead
426 * of making this class non-static.
427 */
428 public void setQuickContactActivity(QuickContactActivity quickContactActivity) {
429 mQuickContactActivity = quickContactActivity;
430 }
431 }
432
Brian Attwellb7e43642014-06-02 14:33:04 -0700433 final MultiShrinkScrollerListener mMultiShrinkScrollerListener
434 = new MultiShrinkScrollerListener() {
435 @Override
436 public void onScrolledOffBottom() {
Brian Attwellf1076dc2014-08-12 21:28:21 -0700437 finish();
Brian Attwellb7e43642014-06-02 14:33:04 -0700438 }
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700439
440 @Override
441 public void onEnterFullscreen() {
442 updateStatusBarColor();
443 }
444
445 @Override
446 public void onExitFullscreen() {
447 updateStatusBarColor();
448 }
Brian Attwell8477eaf2014-06-18 15:39:50 -0700449
450 @Override
451 public void onStartScrollOffBottom() {
Brian Attwell8477eaf2014-06-18 15:39:50 -0700452 mIsExitAnimationInProgress = true;
453 }
Brian Attwell8477eaf2014-06-18 15:39:50 -0700454
Brian Attwell8477eaf2014-06-18 15:39:50 -0700455 @Override
Brian Attwellf1076dc2014-08-12 21:28:21 -0700456 public void onEntranceAnimationDone() {
457 mIsEntranceAnimationFinished = true;
458 }
459
460 @Override
461 public void onTransparentViewHeightChange(float ratio) {
462 if (mIsEntranceAnimationFinished) {
463 mWindowScrim.setAlpha((int) (0xFF * ratio));
Brian Attwell8477eaf2014-06-18 15:39:50 -0700464 }
Brian Attwell8477eaf2014-06-18 15:39:50 -0700465 }
Brian Attwellb7e43642014-06-02 14:33:04 -0700466 };
467
Paul Souloseb64a4b2014-07-07 17:03:27 -0700468
469 /**
470 * Data items are compared to the same mimetype based off of three qualities:
471 * 1. Super primary
472 * 2. Primary
473 * 3. Times used
474 */
475 private final Comparator<DataItem> mWithinMimeTypeDataItemComparator =
476 new Comparator<DataItem>() {
477 @Override
478 public int compare(DataItem lhs, DataItem rhs) {
479 if (!lhs.getMimeType().equals(rhs.getMimeType())) {
480 Log.wtf(TAG, "Comparing DataItems with different mimetypes lhs.getMimeType(): " +
481 lhs.getMimeType() + " rhs.getMimeType(): " + rhs.getMimeType());
482 return 0;
483 }
484
485 if (lhs.isSuperPrimary()) {
486 return -1;
487 } else if (rhs.isSuperPrimary()) {
488 return 1;
489 } else if (lhs.isPrimary() && !rhs.isPrimary()) {
490 return -1;
491 } else if (!lhs.isPrimary() && rhs.isPrimary()) {
492 return 1;
493 } else {
494 final int lhsTimesUsed =
495 lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed();
496 final int rhsTimesUsed =
497 rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed();
498
499 return rhsTimesUsed - lhsTimesUsed;
500 }
501 }
502 };
503
Paul Souloscb4fcc72014-07-15 14:08:50 -0700504 /**
505 * Sorts among different mimetypes based off:
506 * 1. Times used
507 * 2. Last time used
508 * 3. Statically defined
509 */
Paul Souloseb64a4b2014-07-07 17:03:27 -0700510 private final Comparator<List<DataItem>> mAmongstMimeTypeDataItemComparator =
511 new Comparator<List<DataItem>> () {
512 @Override
513 public int compare(List<DataItem> lhsList, List<DataItem> rhsList) {
514 DataItem lhs = lhsList.get(0);
515 DataItem rhs = rhsList.get(0);
516 final int lhsTimesUsed = lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed();
517 final int rhsTimesUsed = rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed();
518 final int timesUsedDifference = rhsTimesUsed - lhsTimesUsed;
519 if (timesUsedDifference != 0) {
520 return timesUsedDifference;
521 }
522
523 final long lhsLastTimeUsed =
524 lhs.getLastTimeUsed() == null ? 0 : lhs.getLastTimeUsed();
525 final long rhsLastTimeUsed =
526 rhs.getLastTimeUsed() == null ? 0 : rhs.getLastTimeUsed();
527 final long lastTimeUsedDifference = rhsLastTimeUsed - lhsLastTimeUsed;
528 if (lastTimeUsedDifference > 0) {
529 return 1;
530 } else if (lastTimeUsedDifference < 0) {
531 return -1;
532 }
533
534 // Times used and last time used are the same. Resort to statically defined.
535 final String lhsMimeType = lhs.getMimeType();
536 final String rhsMimeType = rhs.getMimeType();
537 for (String mimeType : LEADING_MIMETYPES) {
538 if (lhsMimeType.equals(mimeType)) {
539 return -1;
540 } else if (rhsMimeType.equals(mimeType)) {
541 return 1;
542 }
543 }
Paul Souloseb64a4b2014-07-07 17:03:27 -0700544 return 0;
545 }
546 };
547
Daniel Lehmannedb576a2011-07-27 16:45:13 -0700548 @Override
Nancy Chenf619e432014-08-18 20:15:12 -0700549 public boolean dispatchTouchEvent(MotionEvent ev) {
550 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
551 TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY());
552 }
553 return super.dispatchTouchEvent(ev);
554 }
555
556 @Override
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700557 protected void onCreate(Bundle savedInstanceState) {
558 Trace.beginSection("onCreate()");
559 super.onCreate(savedInstanceState);
Daniel Lehmannedb576a2011-07-27 16:45:13 -0700560
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700561 getWindow().setStatusBarColor(Color.TRANSPARENT);
Daniel Lehmann2426cb02012-05-10 18:41:21 -0700562
Brian Attwelld28851f2014-06-10 13:25:07 -0700563 processIntent(getIntent());
Makoto Onukidfe8cc82012-05-17 18:03:44 -0700564
Daisuke Miyakawa10d7df72011-07-29 16:11:05 -0700565 // Show QuickContact in front of soft input
566 getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
567 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
568
Daniel Lehmannedb576a2011-07-27 16:45:13 -0700569 setContentView(R.layout.quickcontact_activity);
570
Brian Attwella3859ed2014-07-15 13:51:55 -0700571 mMaterialColorMapUtils = new MaterialColorMapUtils(getResources());
572
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700573 mScroller = (MultiShrinkScroller) findViewById(R.id.multiscroller);
574
Paul Souloseb64a4b2014-07-07 17:03:27 -0700575 mContactCard = (ExpandingEntryCardView) findViewById(R.id.communication_card);
Brian Attwell60953692014-07-11 17:18:46 -0700576 mNoContactDetailsCard = (ExpandingEntryCardView) findViewById(R.id.no_contact_data_card);
Paul Soulosb3054e52014-06-05 16:46:02 -0700577 mRecentCard = (ExpandingEntryCardView) findViewById(R.id.recent_card);
Paul Souloseb64a4b2014-07-07 17:03:27 -0700578 mAboutCard = (ExpandingEntryCardView) findViewById(R.id.about_card);
Daniel Lehmannedb576a2011-07-27 16:45:13 -0700579
Brian Attwell60953692014-07-11 17:18:46 -0700580 mNoContactDetailsCard.setOnClickListener(mEntryClickHandler);
Paul Souloseb64a4b2014-07-07 17:03:27 -0700581 mContactCard.setOnClickListener(mEntryClickHandler);
Paul Souloseb64a4b2014-07-07 17:03:27 -0700582 mContactCard.setExpandButtonText(
Paul Soulos23889ba2014-06-10 14:55:32 -0700583 getResources().getString(R.string.expanding_entry_card_view_see_all));
Paul Soulos2a4207f2014-07-31 17:09:05 -0700584 mContactCard.setOnCreateContextMenuListener(mEntryContextMenuListener);
Paul Soulosb3054e52014-06-05 16:46:02 -0700585
586 mRecentCard.setOnClickListener(mEntryClickHandler);
587 mRecentCard.setTitle(getResources().getString(R.string.recent_card_title));
Daniel Lehmannedb576a2011-07-27 16:45:13 -0700588
Paul Souloseb64a4b2014-07-07 17:03:27 -0700589 mAboutCard.setOnClickListener(mEntryClickHandler);
Paul Soulos2a4207f2014-07-31 17:09:05 -0700590 mAboutCard.setOnCreateContextMenuListener(mEntryContextMenuListener);
Paul Souloseb64a4b2014-07-07 17:03:27 -0700591
Brian Attwell02eaf112014-08-27 21:03:42 -0700592 mPhotoView = (QuickContactImageView) findViewById(R.id.photo);
Brian Attwell63176c92014-08-18 15:14:18 -0700593 final View transparentView = findViewById(R.id.transparent_view);
Brian Attwell0d90afe2014-06-18 16:45:41 -0700594 if (mScroller != null) {
Brian Attwell63176c92014-08-18 15:14:18 -0700595 transparentView.setOnClickListener(new OnClickListener() {
Brian Attwell0d90afe2014-06-18 16:45:41 -0700596 @Override
597 public void onClick(View v) {
598 mScroller.scrollOffBottom();
599 }
600 });
601 }
Yorke Lee62196682013-09-06 18:34:17 -0700602
Brian Attwelld41ab8a2014-08-07 11:08:55 -0700603 // Allow a shadow to be shown under the toolbar.
604 ViewUtil.addRectangularOutlineProvider(findViewById(R.id.toolbar_parent), getResources());
605
Brian Attwelld28851f2014-06-10 13:25:07 -0700606 final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
607 setActionBar(toolbar);
Brian Attwell9b889e62014-06-23 18:25:32 -0700608 getActionBar().setTitle(null);
609 // Put a TextView with a known resource id into the ActionBar. This allows us to easily
610 // find the correct TextView location & size later.
611 toolbar.addView(getLayoutInflater().inflate(R.layout.quickcontact_title_placeholder, null));
Yorke Lee62196682013-09-06 18:34:17 -0700612
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700613 mHasAlreadyBeenOpened = savedInstanceState != null;
Brian Attwellf1076dc2014-08-12 21:28:21 -0700614 mIsEntranceAnimationFinished = mHasAlreadyBeenOpened;
Brian Attwell9b889e62014-06-23 18:25:32 -0700615 mWindowScrim = new ColorDrawable(SCRIM_COLOR);
Brian Attwellf1076dc2014-08-12 21:28:21 -0700616 mWindowScrim.setAlpha(0);
Brian Attwell9b889e62014-06-23 18:25:32 -0700617 getWindow().setBackgroundDrawable(mWindowScrim);
Makoto Onukidfe8cc82012-05-17 18:03:44 -0700618
Brian Attwellc33ae172014-07-01 14:21:37 -0700619 mScroller.initialize(mMultiShrinkScrollerListener, mExtraMode == MODE_FULLY_EXPANDED);
Brian Attwellac3ed8e2014-06-27 17:24:42 -0700620 // mScroller needs to perform asynchronous measurements after initalize(), therefore
621 // we can't mark this as GONE.
622 mScroller.setVisibility(View.INVISIBLE);
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700623
Brian Attwell9b889e62014-06-23 18:25:32 -0700624 setHeaderNameText(R.string.missing_name);
625
Brian Attwelld28851f2014-06-10 13:25:07 -0700626 mSelectAccountFragmentListener= (SelectAccountDialogFragmentListener) getFragmentManager()
627 .findFragmentByTag(FRAGMENT_TAG_SELECT_ACCOUNT);
628 if (mSelectAccountFragmentListener == null) {
629 mSelectAccountFragmentListener = new SelectAccountDialogFragmentListener();
630 getFragmentManager().beginTransaction().add(0, mSelectAccountFragmentListener,
631 FRAGMENT_TAG_SELECT_ACCOUNT).commit();
632 mSelectAccountFragmentListener.setRetainInstance(true);
633 }
634 mSelectAccountFragmentListener.setQuickContactActivity(this);
Paul Soulos23889ba2014-06-10 14:55:32 -0700635
Brian Attwellf1076dc2014-08-12 21:28:21 -0700636 SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ true,
637 new Runnable() {
638 @Override
639 public void run() {
640 if (!mHasAlreadyBeenOpened) {
641 // The initial scrim opacity must match the scrim opacity that would be
642 // achieved by scrolling to the starting position.
643 final float alphaRatio = mExtraMode == MODE_FULLY_EXPANDED ?
644 1 : mScroller.getStartingTransparentHeightRatio();
645 final int duration = getResources().getInteger(
646 android.R.integer.config_shortAnimTime);
647 final int desiredAlpha = (int) (0xFF * alphaRatio);
648 ObjectAnimator o = ObjectAnimator.ofInt(mWindowScrim, "alpha", 0,
649 desiredAlpha).setDuration(duration);
650
651 o.start();
652 }
653 }
654 });
655
Brian Attwell9b889e62014-06-23 18:25:32 -0700656 if (savedInstanceState != null) {
657 final int color = savedInstanceState.getInt(KEY_THEME_COLOR, 0);
Brian Attwellac3ed8e2014-06-27 17:24:42 -0700658 SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false,
659 new Runnable() {
660 @Override
661 public void run() {
662 // Need to wait for the pre draw before setting the initial scroll
663 // value. Prior to pre draw all scroll values are invalid.
664 if (mHasAlreadyBeenOpened) {
665 mScroller.setVisibility(View.VISIBLE);
666 mScroller.setScroll(mScroller.getScrollNeededToBeFullScreen());
667 }
668 // Need to wait for pre draw for setting the theme color. Setting the
669 // header tint before the MultiShrinkScroller has been measured will
670 // cause incorrect tinting calculations.
671 if (color != 0) {
Brian Attwella3859ed2014-07-15 13:51:55 -0700672 setThemeColor(mMaterialColorMapUtils
673 .calculatePrimaryAndSecondaryColor(color));
Brian Attwell9b889e62014-06-23 18:25:32 -0700674 }
Brian Attwellac3ed8e2014-06-27 17:24:42 -0700675 }
676 });
Brian Attwell9b889e62014-06-23 18:25:32 -0700677 }
678
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700679 Trace.endSection();
680 }
681
Paul Soulos405ae402014-07-14 16:16:36 -0700682 @Override
683 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700684 if (requestCode == REQUEST_CODE_CONTACT_EDITOR_ACTIVITY &&
685 resultCode == ContactDeletionInteraction.RESULT_CODE_DELETED) {
686 // The contact that we were showing has been deleted.
687 finish();
Paul Soulosab54ea12014-08-28 17:22:38 -0700688 } else if (requestCode == REQUEST_CODE_CONTACT_SELECTION_ACTIVITY &&
689 resultCode != RESULT_CANCELED) {
690 processIntent(data);
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700691 }
692 }
693
694 @Override
695 protected void onNewIntent(Intent intent) {
696 super.onNewIntent(intent);
697 mHasAlreadyBeenOpened = true;
Brian Attwellf1076dc2014-08-12 21:28:21 -0700698 mIsEntranceAnimationFinished = true;
Brian Attwell8571dd32014-06-23 23:29:10 -0700699 mHasComputedThemeColor = false;
Brian Attwelld28851f2014-06-10 13:25:07 -0700700 processIntent(intent);
701 }
702
Brian Attwell9b889e62014-06-23 18:25:32 -0700703 @Override
704 public void onSaveInstanceState(Bundle savedInstanceState) {
705 super.onSaveInstanceState(savedInstanceState);
706 if (mColorFilter != null) {
707 savedInstanceState.putInt(KEY_THEME_COLOR, mColorFilter.getColor());
708 }
709 }
710
Brian Attwelld28851f2014-06-10 13:25:07 -0700711 private void processIntent(Intent intent) {
712 Uri lookupUri = intent.getData();
713
714 // Check to see whether it comes from the old version.
715 if (lookupUri != null && LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) {
716 final long rawContactId = ContentUris.parseId(lookupUri);
717 lookupUri = RawContacts.getContactLookupUri(getContentResolver(),
718 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
719 }
720 mExtraMode = getIntent().getIntExtra(QuickContact.EXTRA_MODE,
721 QuickContact.MODE_LARGE);
722 final Uri oldLookupUri = mLookupUri;
723
724 mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri");
725 mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES);
726 if (oldLookupUri == null) {
727 mContactLoader = (ContactLoader) getLoaderManager().initLoader(
728 LOADER_CONTACT_ID, null, mLoaderContactCallbacks);
729 } else if (oldLookupUri != mLookupUri) {
730 // After copying a directory contact, the contact URI changes. Therefore,
731 // we need to restart the loader and reload the new contact.
Paul Soulos899aa212014-06-11 12:04:43 -0700732 for (int interactionLoaderId : mRecentLoaderIds) {
733 getLoaderManager().destroyLoader(interactionLoaderId);
734 }
Paul Soulos405ae402014-07-14 16:16:36 -0700735 mContactLoader = (ContactLoader) getLoaderManager().restartLoader(
736 LOADER_CONTACT_ID, null, mLoaderContactCallbacks);
Brian Attwelld28851f2014-06-10 13:25:07 -0700737 }
Brian Attwell2d150da2014-07-09 22:35:56 -0700738
739 NfcHandler.register(this, mLookupUri);
Daniel Lehmannedb576a2011-07-27 16:45:13 -0700740 }
741
Brian Attwellb7e43642014-06-02 14:33:04 -0700742 private void runEntranceAnimation() {
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700743 if (mHasAlreadyBeenOpened) {
744 return;
745 }
746 mHasAlreadyBeenOpened = true;
Brian Attwellc33ae172014-07-01 14:21:37 -0700747 mScroller.scrollUpForEntranceAnimation(mExtraMode != MODE_FULLY_EXPANDED);
Brian Attwellb7e43642014-06-02 14:33:04 -0700748 }
749
Katherine Kuan81281ee2011-07-28 16:20:59 -0700750 /** Assign this string to the view if it is not empty. */
Brian Attwelld28851f2014-06-10 13:25:07 -0700751 private void setHeaderNameText(int resId) {
Brian Attwell9b889e62014-06-23 18:25:32 -0700752 if (mScroller != null) {
Brian Attwellf43f5732014-06-30 11:18:20 -0700753 mScroller.setTitle(getText(resId) == null ? null : getText(resId).toString());
Brian Attwell9b889e62014-06-23 18:25:32 -0700754 }
Katherine Kuan81281ee2011-07-28 16:20:59 -0700755 }
756
757 /** Assign this string to the view if it is not empty. */
Brian Attwellf43f5732014-06-30 11:18:20 -0700758 private void setHeaderNameText(String value) {
Brian Attwelld28851f2014-06-10 13:25:07 -0700759 if (!TextUtils.isEmpty(value)) {
Brian Attwell9b889e62014-06-23 18:25:32 -0700760 if (mScroller != null) {
Brian Attwellf43f5732014-06-30 11:18:20 -0700761 mScroller.setTitle(value);
Brian Attwell9b889e62014-06-23 18:25:32 -0700762 }
Katherine Kuan81281ee2011-07-28 16:20:59 -0700763 }
764 }
765
766 /**
Daniel Lehmannedb576a2011-07-27 16:45:13 -0700767 * Check if the given MIME-type appears in the list of excluded MIME-types
768 * that the most-recent caller requested.
769 */
770 private boolean isMimeExcluded(String mimeType) {
771 if (mExcludeMimes == null) return false;
772 for (String excludedMime : mExcludeMimes) {
773 if (TextUtils.equals(excludedMime, mimeType)) {
774 return true;
775 }
776 }
777 return false;
778 }
779
780 /**
Daniel Lehmanncb8d73f2011-12-16 17:39:50 +0200781 * Handle the result from the ContactLoader
Daniel Lehmannedb576a2011-07-27 16:45:13 -0700782 */
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700783 private void bindContactData(final Contact data) {
784 Trace.beginSection("bindContactData");
Yorke Lee6cb6f6f2013-09-26 16:26:03 -0700785 mContactData = data;
Brian Attwelld28851f2014-06-10 13:25:07 -0700786 invalidateOptionsMenu();
Daniel Lehmannedb576a2011-07-27 16:45:13 -0700787
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700788 Trace.endSection();
789 Trace.beginSection("Set display photo & name");
Makoto Onukidfe8cc82012-05-17 18:03:44 -0700790
Brian Attwell02eaf112014-08-27 21:03:42 -0700791 mPhotoView.setIsBusiness(mContactData.isDisplayNameFromOrganization());
Yorke Lee62196682013-09-06 18:34:17 -0700792 mPhotoSetter.setupContactPhoto(data, mPhotoView);
Brian Attwell31b2d422014-06-05 00:14:58 -0700793 extractAndApplyTintFromPhotoViewAsynchronously();
Brian Attwell4936a922014-07-02 16:16:59 -0700794 analyzeWhitenessOfPhotoAsynchronously();
Paul Soulos333091a2014-07-22 13:54:41 -0700795 setHeaderNameText(ContactDisplayUtils.getDisplayName(this, data).toString());
Daniel Lehmannedb576a2011-07-27 16:45:13 -0700796
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700797 Trace.endSection();
Makoto Onukidfe8cc82012-05-17 18:03:44 -0700798
Brian Attwell6bb01342014-08-20 23:16:00 -0700799 mEntriesAndActionsTask = new AsyncTask<Void, Void, Cp2DataCardModel>() {
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700800
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700801 @Override
Brian Attwell6bb01342014-08-20 23:16:00 -0700802 protected Cp2DataCardModel doInBackground(
Paul Souloseb64a4b2014-07-07 17:03:27 -0700803 Void... params) {
804 return generateDataModelFromContact(data);
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700805 }
806
807 @Override
Brian Attwell6bb01342014-08-20 23:16:00 -0700808 protected void onPostExecute(Cp2DataCardModel cardDataModel) {
809 super.onPostExecute(cardDataModel);
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700810 // Check that original AsyncTask parameters are still valid and the activity
811 // is still running before binding to UI. A new intent could invalidate
812 // the results, for example.
813 if (data == mContactData && !isCancelled()) {
Brian Attwell6bb01342014-08-20 23:16:00 -0700814 bindDataToCards(cardDataModel);
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700815 showActivity();
816 }
817 }
818 };
819 mEntriesAndActionsTask.execute();
820 }
821
Brian Attwell6bb01342014-08-20 23:16:00 -0700822 private void bindDataToCards(Cp2DataCardModel cp2DataCardModel) {
823 startInteractionLoaders(cp2DataCardModel);
824 populateContactAndAboutCard(cp2DataCardModel);
Paul Souloseb64a4b2014-07-07 17:03:27 -0700825 }
826
Brian Attwell6bb01342014-08-20 23:16:00 -0700827 private void startInteractionLoaders(Cp2DataCardModel cp2DataCardModel) {
828 final Map<String, List<DataItem>> dataItemsMap = cp2DataCardModel.dataItemsMap;
829 final List<DataItem> phoneDataItems = dataItemsMap.get(Phone.CONTENT_ITEM_TYPE);
Paul Souloseb64a4b2014-07-07 17:03:27 -0700830 String[] phoneNumbers = null;
831 if (phoneDataItems != null) {
832 phoneNumbers = new String[phoneDataItems.size()];
833 for (int i = 0; i < phoneDataItems.size(); ++i) {
834 phoneNumbers[i] = ((PhoneDataItem) phoneDataItems.get(i)).getNumber();
835 }
836 }
Paul Soulosab840442014-06-17 14:08:40 -0700837 final Bundle phonesExtraBundle = new Bundle();
Paul Souloseb64a4b2014-07-07 17:03:27 -0700838 phonesExtraBundle.putStringArray(KEY_LOADER_EXTRA_PHONES, phoneNumbers);
839
840 Trace.beginSection("start sms loader");
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700841 getLoaderManager().initLoader(
842 LOADER_SMS_ID,
Paul Soulosab840442014-06-17 14:08:40 -0700843 phonesExtraBundle,
844 mLoaderInteractionsCallbacks);
845 Trace.endSection();
846
847 Trace.beginSection("start call log loader");
848 getLoaderManager().initLoader(
849 LOADER_CALL_LOG_ID,
850 phonesExtraBundle,
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700851 mLoaderInteractionsCallbacks);
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700852 Trace.endSection();
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700853
Paul Souloseb64a4b2014-07-07 17:03:27 -0700854
Paul Soulos899aa212014-06-11 12:04:43 -0700855 Trace.beginSection("start calendar loader");
Brian Attwell6bb01342014-08-20 23:16:00 -0700856 final List<DataItem> emailDataItems = dataItemsMap.get(Email.CONTENT_ITEM_TYPE);
Paul Souloseb64a4b2014-07-07 17:03:27 -0700857 String[] emailAddresses = null;
858 if (emailDataItems != null) {
859 emailAddresses = new String[emailDataItems.size()];
860 for (int i = 0; i < emailDataItems.size(); ++i) {
861 emailAddresses[i] = ((EmailDataItem) emailDataItems.get(i)).getAddress();
862 }
863 }
Paul Soulosab840442014-06-17 14:08:40 -0700864 final Bundle emailsExtraBundle = new Bundle();
Paul Souloseb64a4b2014-07-07 17:03:27 -0700865 emailsExtraBundle.putStringArray(KEY_LOADER_EXTRA_EMAILS, emailAddresses);
Paul Soulos899aa212014-06-11 12:04:43 -0700866 getLoaderManager().initLoader(
867 LOADER_CALENDAR_ID,
Paul Soulosab840442014-06-17 14:08:40 -0700868 emailsExtraBundle,
Paul Soulos899aa212014-06-11 12:04:43 -0700869 mLoaderInteractionsCallbacks);
870 Trace.endSection();
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700871 }
872
873 private void showActivity() {
874 if (mScroller != null) {
875 mScroller.setVisibility(View.VISIBLE);
876 SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false,
877 new Runnable() {
878 @Override
879 public void run() {
880 runEntranceAnimation();
881 }
882 });
883 }
884 }
885
Brian Attwell6bb01342014-08-20 23:16:00 -0700886 private List<List<Entry>> buildAboutCardEntries(Map<String, List<DataItem>> dataItemsMap) {
Paul Soulosa153dba2014-08-06 13:44:57 -0700887 final List<List<Entry>> aboutCardEntries = new ArrayList<>();
888 for (String mimetype : SORTED_ABOUT_CARD_MIMETYPES) {
Brian Attwell6bb01342014-08-20 23:16:00 -0700889 final List<DataItem> mimeTypeItems = dataItemsMap.get(mimetype);
Paul Soulosa153dba2014-08-06 13:44:57 -0700890 if (mimeTypeItems == null) {
891 continue;
892 }
Brian Attwell6bb01342014-08-20 23:16:00 -0700893 // Set aboutCardTitleOut = null, since SORTED_ABOUT_CARD_MIMETYPES doesn't contain
894 // the name mimetype.
895 final List<Entry> aboutEntries = dataItemsToEntries(mimeTypeItems,
896 /* aboutCardTitleOut = */ null);
Paul Soulosa153dba2014-08-06 13:44:57 -0700897 if (aboutEntries.size() > 0) {
898 aboutCardEntries.add(aboutEntries);
899 }
900 }
901 return aboutCardEntries;
902 }
903
Paul Soulosa42ef762014-08-20 10:26:10 -0700904 @Override
905 protected void onResume() {
906 super.onResume();
907 // If returning from a launched activity, repopulate the contact and about card
908 if (mHasIntentLaunched) {
909 mHasIntentLaunched = false;
Brian Attwell6bb01342014-08-20 23:16:00 -0700910 populateContactAndAboutCard(mCachedCp2DataCardModel);
Paul Soulosa42ef762014-08-20 10:26:10 -0700911 }
912 }
913
Brian Attwell6bb01342014-08-20 23:16:00 -0700914 private void populateContactAndAboutCard(Cp2DataCardModel cp2DataCardModel) {
915 mCachedCp2DataCardModel = cp2DataCardModel;
916 if (mHasIntentLaunched || cp2DataCardModel == null) {
Paul Soulosa42ef762014-08-20 10:26:10 -0700917 return;
918 }
Paul Souloseb64a4b2014-07-07 17:03:27 -0700919 Trace.beginSection("bind contact card");
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700920
Brian Attwell6bb01342014-08-20 23:16:00 -0700921 final List<List<Entry>> contactCardEntries = cp2DataCardModel.contactCardEntries;
922 final List<List<Entry>> aboutCardEntries = cp2DataCardModel.aboutCardEntries;
923 final String customAboutCardName = cp2DataCardModel.customAboutCardName;
Paul Souloseb64a4b2014-07-07 17:03:27 -0700924
925 if (contactCardEntries.size() > 0) {
926 mContactCard.initialize(contactCardEntries,
927 /* numInitialVisibleEntries = */ MIN_NUM_CONTACT_ENTRIES_SHOWN,
Paul Soulosc8e2a912014-07-21 14:52:20 -0700928 /* isExpanded = */ mContactCard.isExpanded(),
Paul Soulos89966b42014-07-21 12:38:50 -0700929 /* isAlwaysExpanded = */ false,
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700930 mExpandingEntryCardViewListener,
931 mScroller);
Paul Souloseb64a4b2014-07-07 17:03:27 -0700932 mContactCard.setVisibility(View.VISIBLE);
933 } else {
934 mContactCard.setVisibility(View.GONE);
935 }
936 Trace.endSection();
937
938 Trace.beginSection("bind about card");
Paul Soulosc6e11092014-07-22 09:22:27 -0700939 // Phonetic name is not a data item, so the entry needs to be created separately
940 final String phoneticName = mContactData.getPhoneticName();
941 if (!TextUtils.isEmpty(phoneticName)) {
942 Entry phoneticEntry = new Entry(/* viewId = */ -1,
943 /* icon = */ null,
944 getResources().getString(R.string.name_phonetic),
945 phoneticName,
946 /* text = */ null,
Paul Soulos23e28362014-08-29 14:57:08 -0700947 /* primaryContentDescription = */ null,
Paul Soulosc6e11092014-07-22 09:22:27 -0700948 /* intent = */ null,
949 /* alternateIcon = */ null,
950 /* alternateIntent = */ null,
951 /* alternateContentDescription = */ null,
952 /* shouldApplyColor = */ false,
Paul Soulos2a4207f2014-07-31 17:09:05 -0700953 /* isEditable = */ false,
954 /* EntryContextMenuInfo = */ new EntryContextMenuInfo(phoneticName,
Paul Soulos48fc9122014-08-26 13:52:36 -0700955 getResources().getString(R.string.name_phonetic)),
956 /* thirdIcon = */ null,
957 /* thirdIntent = */ null,
Paul Soulos48290be2014-09-08 13:44:51 -0700958 /* thirdContentDescription = */ null,
959 /* iconResourceId = */ 0);
Paul Soulosc6e11092014-07-22 09:22:27 -0700960 List<Entry> phoneticList = new ArrayList<>();
961 phoneticList.add(phoneticEntry);
Paul Soulosa153dba2014-08-06 13:44:57 -0700962 // Phonetic name comes after nickname. Check to see if the first entry type is nickname
Paul Soulosa0fa4c12014-08-07 12:01:32 -0700963 if (aboutCardEntries.size() > 0 && aboutCardEntries.get(0).get(0).getHeader().equals(
Paul Soulosa153dba2014-08-06 13:44:57 -0700964 getResources().getString(R.string.header_nickname_entry))) {
965 aboutCardEntries.add(1, phoneticList);
966 } else {
967 aboutCardEntries.add(0, phoneticList);
968 }
Paul Soulosc6e11092014-07-22 09:22:27 -0700969 }
970
Brian Attwell6bb01342014-08-20 23:16:00 -0700971 if (!TextUtils.isEmpty(customAboutCardName)) {
972 mAboutCard.setTitle(customAboutCardName);
973 }
974
Paul Souloscc5ec222014-08-25 12:02:26 -0700975 if (aboutCardEntries.size() > 0) {
976 mAboutCard.initialize(aboutCardEntries,
977 /* numInitialVisibleEntries = */ 1,
978 /* isExpanded = */ true,
979 /* isAlwaysExpanded = */ true,
980 mExpandingEntryCardViewListener,
981 mScroller);
982 }
Brian Attwell60953692014-07-11 17:18:46 -0700983
984 if (contactCardEntries.size() == 0 && aboutCardEntries.size() == 0) {
985 initializeNoContactDetailCard();
986 } else {
987 mNoContactDetailsCard.setVisibility(View.GONE);
988 }
989
Paul Soulosa8fce962014-07-15 13:37:24 -0700990 // If the Recent card is already initialized (all recent data is loaded), show the About
991 // card if it has entries. Otherwise About card visibility will be set in bindRecentData()
992 if (isAllRecentDataLoaded() && aboutCardEntries.size() > 0) {
993 mAboutCard.setVisibility(View.VISIBLE);
994 }
Paul Souloseb64a4b2014-07-07 17:03:27 -0700995 Trace.endSection();
996 }
997
998 /**
Brian Attwell60953692014-07-11 17:18:46 -0700999 * Create a card that shows "Add email" and "Add phone number" entries in grey.
1000 */
1001 private void initializeNoContactDetailCard() {
1002 final Drawable phoneIcon = getResources().getDrawable(
1003 R.drawable.ic_phone_24dp).mutate();
1004 final Entry phonePromptEntry = new Entry(CARD_ENTRY_ID_EDIT_CONTACT,
1005 phoneIcon, getString(R.string.quickcontact_add_phone_number),
Paul Soulos23e28362014-08-29 14:57:08 -07001006 /* subHeader = */ null, /* text = */ null, /* primaryContentDescription = */ null,
1007 getEditContactIntent(),
Paul Soulosdd7419d2014-07-15 11:22:13 -07001008 /* alternateIcon = */ null, /* alternateIntent = */ null,
Paul Soulos714455b2014-07-15 14:32:01 -07001009 /* alternateContentDescription = */ null, /* shouldApplyColor = */ true,
Paul Soulos48fc9122014-08-26 13:52:36 -07001010 /* isEditable = */ false, /* EntryContextMenuInfo = */ null,
1011 /* thirdIcon = */ null, /* thirdIntent = */ null,
Paul Soulos48290be2014-09-08 13:44:51 -07001012 /* thirdContentDescription = */ null, R.drawable.ic_phone_24dp);
Brian Attwell60953692014-07-11 17:18:46 -07001013
1014 final Drawable emailIcon = getResources().getDrawable(
1015 R.drawable.ic_email_24dp).mutate();
1016 final Entry emailPromptEntry = new Entry(CARD_ENTRY_ID_EDIT_CONTACT,
1017 emailIcon, getString(R.string.quickcontact_add_email), /* subHeader = */ null,
Paul Soulos23e28362014-08-29 14:57:08 -07001018 /* text = */ null, /* primaryContentDescription = */ null,
1019 getEditContactIntent(), /* alternateIcon = */ null,
Paul Soulosdd7419d2014-07-15 11:22:13 -07001020 /* alternateIntent = */ null, /* alternateContentDescription = */ null,
Paul Soulos2a4207f2014-07-31 17:09:05 -07001021 /* shouldApplyColor = */ true, /* isEditable = */ false,
Paul Soulos48fc9122014-08-26 13:52:36 -07001022 /* EntryContextMenuInfo = */ null, /* thirdIcon = */ null,
Paul Soulos48290be2014-09-08 13:44:51 -07001023 /* thirdIntent = */ null, /* thirdContentDescription = */ null,
1024 R.drawable.ic_email_24dp);
Brian Attwell60953692014-07-11 17:18:46 -07001025
1026 final List<List<Entry>> promptEntries = new ArrayList<>();
1027 promptEntries.add(new ArrayList<Entry>(1));
1028 promptEntries.add(new ArrayList<Entry>(1));
1029 promptEntries.get(0).add(phonePromptEntry);
1030 promptEntries.get(1).add(emailPromptEntry);
1031
1032 final int subHeaderTextColor = getResources().getColor(
1033 R.color.quickcontact_entry_sub_header_text_color);
1034 final PorterDuffColorFilter greyColorFilter =
1035 new PorterDuffColorFilter(subHeaderTextColor, PorterDuff.Mode.SRC_ATOP);
Paul Soulosc8e2a912014-07-21 14:52:20 -07001036 mNoContactDetailsCard.initialize(promptEntries, 2, /* isExpanded = */ true,
Paul Soulos0cda9ae2014-07-23 11:27:28 -07001037 /* isAlwaysExpanded = */ true, mExpandingEntryCardViewListener, mScroller);
Brian Attwell60953692014-07-11 17:18:46 -07001038 mNoContactDetailsCard.setVisibility(View.VISIBLE);
1039 mNoContactDetailsCard.setEntryHeaderColor(subHeaderTextColor);
1040 mNoContactDetailsCard.setColorAndFilter(subHeaderTextColor, greyColorFilter);
1041 }
1042
1043 /**
Paul Souloseb64a4b2014-07-07 17:03:27 -07001044 * Builds the {@link DataItem}s Map out of the Contact.
1045 * @param data The contact to build the data from.
1046 * @return A pair containing a list of data items sorted within mimetype and sorted
1047 * amongst mimetype. The map goes from mimetype string to the sorted list of data items within
1048 * mimetype
1049 */
Brian Attwell6bb01342014-08-20 23:16:00 -07001050 private Cp2DataCardModel generateDataModelFromContact(
Paul Souloseb64a4b2014-07-07 17:03:27 -07001051 Contact data) {
1052 Trace.beginSection("Build data items map");
1053
1054 final Map<String, List<DataItem>> dataItemsMap = new HashMap<>();
Brian Attwell8bf96e72014-06-25 15:46:47 -07001055
Brian Attwell8a6f4ad2014-06-06 21:54:53 -07001056 final ResolveCache cache = ResolveCache.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -07001057 for (RawContact rawContact : data.getRawContacts()) {
1058 for (DataItem dataItem : rawContact.getDataItems()) {
Paul Souloseb64a4b2014-07-07 17:03:27 -07001059 dataItem.setRawContactId(rawContact.getId());
1060
Maurice Chu851222a2012-06-21 11:43:08 -07001061 final String mimeType = dataItem.getMimeType();
Paul Souloseb64a4b2014-07-07 17:03:27 -07001062 if (mimeType == null) continue;
1063
Chiao Cheng47b6f702012-09-07 17:28:17 -07001064 final AccountType accountType = rawContact.getAccountType(this);
1065 final DataKind dataKind = AccountTypeManager.getInstance(this)
1066 .getKindOrFallback(accountType, mimeType);
Paul Souloseb64a4b2014-07-07 17:03:27 -07001067 if (dataKind == null) continue;
Daniel Lehmannedb576a2011-07-27 16:45:13 -07001068
Paul Souloseb64a4b2014-07-07 17:03:27 -07001069 dataItem.setDataKind(dataKind);
1070
1071 final boolean hasData = !TextUtils.isEmpty(dataItem.buildDataString(this,
1072 dataKind));
1073
1074 if (isMimeExcluded(mimeType) || !hasData) continue;
1075
1076 List<DataItem> dataItemListByType = dataItemsMap.get(mimeType);
1077 if (dataItemListByType == null) {
1078 dataItemListByType = new ArrayList<>();
1079 dataItemsMap.put(mimeType, dataItemListByType);
Paul Soulosb3054e52014-06-05 16:46:02 -07001080 }
Paul Souloseb64a4b2014-07-07 17:03:27 -07001081 dataItemListByType.add(dataItem);
Daniel Lehmannedb576a2011-07-27 16:45:13 -07001082 }
1083 }
Brian Attwell8a6f4ad2014-06-06 21:54:53 -07001084 Trace.endSection();
Makoto Onukidfe8cc82012-05-17 18:03:44 -07001085
Paul Souloseb64a4b2014-07-07 17:03:27 -07001086 Trace.beginSection("sort within mimetypes");
Paul Soulos16339332014-06-24 13:59:56 -07001087 /*
1088 * Sorting is a multi part step. The end result is to a have a sorted list of the most
Paul Souloseb64a4b2014-07-07 17:03:27 -07001089 * used data items, one per mimetype. Then, within each mimetype, the list of data items
1090 * for that type is also sorted, based off of {super primary, primary, times used} in that
1091 * order.
Paul Soulos16339332014-06-24 13:59:56 -07001092 */
Paul Souloseb64a4b2014-07-07 17:03:27 -07001093 final List<List<DataItem>> dataItemsList = new ArrayList<>();
1094 for (List<DataItem> mimeTypeDataItems : dataItemsMap.values()) {
1095 // Remove duplicate data items
1096 Collapser.collapseList(mimeTypeDataItems, this);
1097 // Sort within mimetype
1098 Collections.sort(mimeTypeDataItems, mWithinMimeTypeDataItemComparator);
1099 // Add to the list of data item lists
1100 dataItemsList.add(mimeTypeDataItems);
1101 }
1102 Trace.endSection();
Paul Soulos16339332014-06-24 13:59:56 -07001103
Paul Souloseb64a4b2014-07-07 17:03:27 -07001104 Trace.beginSection("sort amongst mimetypes");
1105 // Sort amongst mimetypes to bubble up the top data items for the contact card
1106 Collections.sort(dataItemsList, mAmongstMimeTypeDataItemComparator);
1107 Trace.endSection();
1108
Brian Attwell6bb01342014-08-20 23:16:00 -07001109 Trace.beginSection("cp2 data items to entries");
1110
1111 final List<List<Entry>> contactCardEntries = new ArrayList<>();
1112 final List<List<Entry>> aboutCardEntries = buildAboutCardEntries(dataItemsMap);
1113 final MutableString aboutCardName = new MutableString();
1114
1115 for (int i = 0; i < dataItemsList.size(); ++i) {
1116 final List<DataItem> dataItemsByMimeType = dataItemsList.get(i);
1117 final DataItem topDataItem = dataItemsByMimeType.get(0);
1118 if (SORTED_ABOUT_CARD_MIMETYPES.contains(topDataItem.getMimeType())) {
1119 // About card mimetypes are built in buildAboutCardEntries, skip here
1120 continue;
1121 } else {
1122 List<Entry> contactEntries = dataItemsToEntries(dataItemsList.get(i),
1123 aboutCardName);
1124 if (contactEntries.size() > 0) {
1125 contactCardEntries.add(contactEntries);
1126 }
1127 }
1128 }
1129
1130 Trace.endSection();
1131
1132 final Cp2DataCardModel dataModel = new Cp2DataCardModel();
1133 dataModel.customAboutCardName = aboutCardName.value;
1134 dataModel.aboutCardEntries = aboutCardEntries;
1135 dataModel.contactCardEntries = contactCardEntries;
1136 dataModel.dataItemsMap = dataItemsMap;
1137 return dataModel;
1138 }
1139
1140 /**
1141 * Class used to hold the About card and Contact cards' data model that gets generated
1142 * on a background thread. All data is from CP2.
1143 */
1144 private static class Cp2DataCardModel {
1145 /**
1146 * A map between a mimetype string and the corresponding list of data items. The data items
1147 * are in sorted order using mWithinMimeTypeDataItemComparator.
1148 */
1149 public Map<String, List<DataItem>> dataItemsMap;
1150 public List<List<Entry>> aboutCardEntries;
1151 public List<List<Entry>> contactCardEntries;
1152 public String customAboutCardName;
1153 }
1154
1155 private static class MutableString {
1156 public String value;
Paul Souloseb64a4b2014-07-07 17:03:27 -07001157 }
1158
1159 /**
1160 * Converts a {@link DataItem} into an {@link ExpandingEntryCardView.Entry} for display.
1161 * If the {@link ExpandingEntryCardView.Entry} has no visual elements, null is returned.
Brian Attwell6bb01342014-08-20 23:16:00 -07001162 *
1163 * This runs on a background thread. This is set as static to avoid accidentally adding
1164 * additional dependencies on unsafe things (like the Activity).
1165 *
Paul Souloseb64a4b2014-07-07 17:03:27 -07001166 * @param dataItem The {@link DataItem} to convert.
1167 * @return The {@link ExpandingEntryCardView.Entry}, or null if no visual elements are present.
1168 */
Brian Attwell6bb01342014-08-20 23:16:00 -07001169 private static Entry dataItemToEntry(DataItem dataItem,
1170 Context context, Contact contactData,
1171 final MutableString aboutCardName) {
Paul Souloseb64a4b2014-07-07 17:03:27 -07001172 Drawable icon = null;
1173 String header = null;
1174 String subHeader = null;
1175 Drawable subHeaderIcon = null;
1176 String text = null;
1177 Drawable textIcon = null;
Paul Soulos23e28362014-08-29 14:57:08 -07001178 StringBuilder primaryContentDescription = new StringBuilder();
Paul Souloseb64a4b2014-07-07 17:03:27 -07001179 Intent intent = null;
Paul Soulos48ebbaa2014-07-15 13:11:23 -07001180 boolean shouldApplyColor = true;
Paul Soulosdd7419d2014-07-15 11:22:13 -07001181 Drawable alternateIcon = null;
1182 Intent alternateIntent = null;
Paul Soulos23e28362014-08-29 14:57:08 -07001183 StringBuilder alternateContentDescription = new StringBuilder();
Paul Souloseb64a4b2014-07-07 17:03:27 -07001184 final boolean isEditable = false;
Paul Soulos2a4207f2014-07-31 17:09:05 -07001185 EntryContextMenuInfo entryContextMenuInfo = null;
Paul Soulos48fc9122014-08-26 13:52:36 -07001186 Drawable thirdIcon = null;
1187 Intent thirdIntent = null;
1188 String thirdContentDescription = null;
Paul Soulos48290be2014-09-08 13:44:51 -07001189 int iconResourceId = 0;
Paul Souloseb64a4b2014-07-07 17:03:27 -07001190
Brian Attwell6bb01342014-08-20 23:16:00 -07001191 context = context.getApplicationContext();
Paul Soulos23e28362014-08-29 14:57:08 -07001192 final Resources res = context.getResources();
Paul Souloseb64a4b2014-07-07 17:03:27 -07001193 DataKind kind = dataItem.getDataKind();
1194
1195 if (dataItem instanceof ImDataItem) {
1196 final ImDataItem im = (ImDataItem) dataItem;
Brian Attwell6bb01342014-08-20 23:16:00 -07001197 intent = ContactsUtils.buildImIntent(context, im).first;
Paul Souloseb64a4b2014-07-07 17:03:27 -07001198 final boolean isEmail = im.isCreatedFromEmail();
Paul Soulos7de6f852014-07-28 14:07:28 -07001199 final int protocol;
1200 if (!im.isProtocolValid()) {
1201 protocol = Im.PROTOCOL_CUSTOM;
1202 } else {
1203 protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : im.getProtocol();
1204 }
1205 if (protocol == Im.PROTOCOL_CUSTOM) {
1206 // If the protocol is custom, display the "IM" entry header as well to distinguish
1207 // this entry from other ones
Paul Soulos23e28362014-08-29 14:57:08 -07001208 header = res.getString(R.string.header_im_entry);
1209 subHeader = Im.getProtocolLabel(res, protocol,
Paul Soulos7de6f852014-07-28 14:07:28 -07001210 im.getCustomProtocol()).toString();
1211 text = im.getData();
1212 } else {
Paul Soulos23e28362014-08-29 14:57:08 -07001213 header = Im.getProtocolLabel(res, protocol,
Paul Soulos7de6f852014-07-28 14:07:28 -07001214 im.getCustomProtocol()).toString();
1215 subHeader = im.getData();
1216 }
Paul Soulos2a4207f2014-07-31 17:09:05 -07001217 entryContextMenuInfo = new EntryContextMenuInfo(im.getData(), header);
Paul Souloseb64a4b2014-07-07 17:03:27 -07001218 } else if (dataItem instanceof OrganizationDataItem) {
1219 final OrganizationDataItem organization = (OrganizationDataItem) dataItem;
Paul Soulos23e28362014-08-29 14:57:08 -07001220 header = res.getString(R.string.header_organization_entry);
Paul Souloseb64a4b2014-07-07 17:03:27 -07001221 subHeader = organization.getCompany();
Paul Soulos2a4207f2014-07-31 17:09:05 -07001222 entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header);
Paul Souloseb64a4b2014-07-07 17:03:27 -07001223 text = organization.getTitle();
1224 } else if (dataItem instanceof NicknameDataItem) {
1225 final NicknameDataItem nickname = (NicknameDataItem) dataItem;
1226 // Build nickname entries
1227 final boolean isNameRawContact =
Brian Attwell6bb01342014-08-20 23:16:00 -07001228 (contactData.getNameRawContactId() == dataItem.getRawContactId());
Paul Souloseb64a4b2014-07-07 17:03:27 -07001229
1230 final boolean duplicatesTitle =
1231 isNameRawContact
Brian Attwell6bb01342014-08-20 23:16:00 -07001232 && contactData.getDisplayNameSource() == DisplayNameSources.NICKNAME;
Paul Souloseb64a4b2014-07-07 17:03:27 -07001233
1234 if (!duplicatesTitle) {
Paul Soulos23e28362014-08-29 14:57:08 -07001235 header = res.getString(R.string.header_nickname_entry);
Paul Souloseb64a4b2014-07-07 17:03:27 -07001236 subHeader = nickname.getName();
Paul Soulos2a4207f2014-07-31 17:09:05 -07001237 entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header);
Paul Souloseb64a4b2014-07-07 17:03:27 -07001238 }
1239 } else if (dataItem instanceof NoteDataItem) {
1240 final NoteDataItem note = (NoteDataItem) dataItem;
Paul Soulos23e28362014-08-29 14:57:08 -07001241 header = res.getString(R.string.header_note_entry);
Paul Souloseb64a4b2014-07-07 17:03:27 -07001242 subHeader = note.getNote();
Paul Soulos2a4207f2014-07-31 17:09:05 -07001243 entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header);
Paul Souloseb64a4b2014-07-07 17:03:27 -07001244 } else if (dataItem instanceof WebsiteDataItem) {
1245 final WebsiteDataItem website = (WebsiteDataItem) dataItem;
Paul Soulos23e28362014-08-29 14:57:08 -07001246 header = res.getString(R.string.header_website_entry);
Paul Souloseb64a4b2014-07-07 17:03:27 -07001247 subHeader = website.getUrl();
Paul Soulos2a4207f2014-07-31 17:09:05 -07001248 entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header);
Paul Souloseb64a4b2014-07-07 17:03:27 -07001249 try {
Brian Attwell6bb01342014-08-20 23:16:00 -07001250 final WebAddress webAddress = new WebAddress(website.buildDataString(context,
1251 kind));
Paul Souloseb64a4b2014-07-07 17:03:27 -07001252 intent = new Intent(Intent.ACTION_VIEW, Uri.parse(webAddress.toString()));
1253 } catch (final ParseException e) {
Brian Attwell6bb01342014-08-20 23:16:00 -07001254 Log.e(TAG, "Couldn't parse website: " + website.buildDataString(context, kind));
Paul Souloseb64a4b2014-07-07 17:03:27 -07001255 }
1256 } else if (dataItem instanceof EventDataItem) {
1257 final EventDataItem event = (EventDataItem) dataItem;
Brian Attwell6bb01342014-08-20 23:16:00 -07001258 final String dataString = event.buildDataString(context, kind);
Paul Souloseb64a4b2014-07-07 17:03:27 -07001259 final Calendar cal = DateUtils.parseDate(dataString, false);
1260 if (cal != null) {
1261 final Date nextAnniversary =
1262 DateUtils.getNextAnnualDate(cal);
1263 final Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon();
1264 builder.appendPath("time");
1265 ContentUris.appendId(builder, nextAnniversary.getTime());
1266 intent = new Intent(Intent.ACTION_VIEW).setData(builder.build());
1267 }
Paul Soulos23e28362014-08-29 14:57:08 -07001268 header = res.getString(R.string.header_event_entry);
Paul Soulosf9d5c0e2014-07-09 11:25:59 -07001269 if (event.hasKindTypeColumn(kind)) {
Paul Soulos23e28362014-08-29 14:57:08 -07001270 subHeader = Event.getTypeLabel(res, event.getKindTypeColumn(kind),
Paul Soulos7de6f852014-07-28 14:07:28 -07001271 event.getLabel()).toString();
Paul Soulosf9d5c0e2014-07-09 11:25:59 -07001272 }
Brian Attwell6bb01342014-08-20 23:16:00 -07001273 text = DateUtils.formatDate(context, dataString);
Paul Soulosf848c492014-08-11 17:13:14 -07001274 entryContextMenuInfo = new EntryContextMenuInfo(text, header);
Paul Souloseb64a4b2014-07-07 17:03:27 -07001275 } else if (dataItem instanceof RelationDataItem) {
1276 final RelationDataItem relation = (RelationDataItem) dataItem;
Brian Attwell6bb01342014-08-20 23:16:00 -07001277 final String dataString = relation.buildDataString(context, kind);
Paul Souloseb64a4b2014-07-07 17:03:27 -07001278 if (!TextUtils.isEmpty(dataString)) {
1279 intent = new Intent(Intent.ACTION_SEARCH);
1280 intent.putExtra(SearchManager.QUERY, dataString);
1281 intent.setType(Contacts.CONTENT_TYPE);
1282 }
Paul Soulos23e28362014-08-29 14:57:08 -07001283 header = res.getString(R.string.header_relation_entry);
Paul Souloseb64a4b2014-07-07 17:03:27 -07001284 subHeader = relation.getName();
Paul Soulos2a4207f2014-07-31 17:09:05 -07001285 entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header);
Paul Soulosf9d5c0e2014-07-09 11:25:59 -07001286 if (relation.hasKindTypeColumn(kind)) {
Paul Soulos23e28362014-08-29 14:57:08 -07001287 text = Relation.getTypeLabel(res,
Brian Attwell6bb01342014-08-20 23:16:00 -07001288 relation.getKindTypeColumn(kind),
Paul Soulosf9d5c0e2014-07-09 11:25:59 -07001289 relation.getLabel()).toString();
1290 }
Paul Souloseb64a4b2014-07-07 17:03:27 -07001291 } else if (dataItem instanceof PhoneDataItem) {
1292 final PhoneDataItem phone = (PhoneDataItem) dataItem;
1293 if (!TextUtils.isEmpty(phone.getNumber())) {
Paul Soulos23e28362014-08-29 14:57:08 -07001294 primaryContentDescription.append(res.getString(R.string.call_other)).append(" ");
Brian Attwell6bb01342014-08-20 23:16:00 -07001295 header = phone.buildDataString(context, kind);
Paul Soulos2a4207f2014-07-31 17:09:05 -07001296 entryContextMenuInfo = new EntryContextMenuInfo(header,
Paul Soulos23e28362014-08-29 14:57:08 -07001297 res.getString(R.string.phoneLabelsGroup));
Paul Soulosf9d5c0e2014-07-09 11:25:59 -07001298 if (phone.hasKindTypeColumn(kind)) {
Paul Soulos23e28362014-08-29 14:57:08 -07001299 text = Phone.getTypeLabel(res, phone.getKindTypeColumn(kind),
Paul Soulosf9d5c0e2014-07-09 11:25:59 -07001300 phone.getLabel()).toString();
Paul Soulos23e28362014-08-29 14:57:08 -07001301 primaryContentDescription.append(text).append(" ");
Paul Soulosf9d5c0e2014-07-09 11:25:59 -07001302 }
Paul Soulos23e28362014-08-29 14:57:08 -07001303 primaryContentDescription.append(header);
1304 icon = res.getDrawable(R.drawable.ic_phone_24dp);
Paul Soulos48290be2014-09-08 13:44:51 -07001305 iconResourceId = R.drawable.ic_phone_24dp;
Brian Attwell6bb01342014-08-20 23:16:00 -07001306 if (PhoneCapabilityTester.isPhone(context)) {
Paul Souloseb64a4b2014-07-07 17:03:27 -07001307 intent = CallUtil.getCallIntent(phone.getNumber());
Paul Soulos16339332014-06-24 13:59:56 -07001308 }
Paul Soulosdd7419d2014-07-15 11:22:13 -07001309 alternateIntent = new Intent(Intent.ACTION_SENDTO,
Jay Shrauner1cd88e32014-09-05 15:37:55 -07001310 Uri.fromParts(ContactsUtils.SCHEME_SMSTO, phone.getNumber(), null));
Paul Soulos48fc9122014-08-26 13:52:36 -07001311
Paul Soulos23e28362014-08-29 14:57:08 -07001312 alternateIcon = res.getDrawable(R.drawable.ic_message_24dp);
1313 alternateContentDescription.append(res.getString(R.string.sms_custom, header));
Paul Soulos48fc9122014-08-26 13:52:36 -07001314
1315 // Add video call button if supported
Paul Soulosf7647302014-08-26 14:28:57 -07001316 if (CallUtil.isVideoEnabled(context)) {
Paul Soulos23e28362014-08-29 14:57:08 -07001317 thirdIcon = res.getDrawable(R.drawable.ic_videocam);
Paul Soulos48fc9122014-08-26 13:52:36 -07001318 thirdIntent = CallUtil.getVideoCallIntent(phone.getNumber(),
1319 CALL_ORIGIN_QUICK_CONTACTS_ACTIVITY);
1320 thirdContentDescription =
Paul Soulos23e28362014-08-29 14:57:08 -07001321 res.getString(R.string.description_video_call);
Paul Soulos48fc9122014-08-26 13:52:36 -07001322 }
Paul Souloseb64a4b2014-07-07 17:03:27 -07001323 }
1324 } else if (dataItem instanceof EmailDataItem) {
1325 final EmailDataItem email = (EmailDataItem) dataItem;
1326 final String address = email.getData();
1327 if (!TextUtils.isEmpty(address)) {
Paul Soulos23e28362014-08-29 14:57:08 -07001328 primaryContentDescription.append(res.getString(R.string.email_other)).append(" ");
Jay Shrauner1cd88e32014-09-05 15:37:55 -07001329 final Uri mailUri = Uri.fromParts(ContactsUtils.SCHEME_MAILTO, address, null);
Paul Souloseb64a4b2014-07-07 17:03:27 -07001330 intent = new Intent(Intent.ACTION_SENDTO, mailUri);
1331 header = email.getAddress();
Paul Soulos2a4207f2014-07-31 17:09:05 -07001332 entryContextMenuInfo = new EntryContextMenuInfo(header,
Paul Soulos23e28362014-08-29 14:57:08 -07001333 res.getString(R.string.emailLabelsGroup));
Paul Soulosf9d5c0e2014-07-09 11:25:59 -07001334 if (email.hasKindTypeColumn(kind)) {
Paul Soulos23e28362014-08-29 14:57:08 -07001335 text = Email.getTypeLabel(res, email.getKindTypeColumn(kind),
Paul Soulosf9d5c0e2014-07-09 11:25:59 -07001336 email.getLabel()).toString();
Paul Soulos23e28362014-08-29 14:57:08 -07001337 primaryContentDescription.append(text).append(" ");
Paul Soulosf9d5c0e2014-07-09 11:25:59 -07001338 }
Paul Soulos23e28362014-08-29 14:57:08 -07001339 primaryContentDescription.append(header);
1340 icon = res.getDrawable(R.drawable.ic_email_24dp);
Paul Soulos48290be2014-09-08 13:44:51 -07001341 iconResourceId = R.drawable.ic_email_24dp;
Paul Souloseb64a4b2014-07-07 17:03:27 -07001342 }
1343 } else if (dataItem instanceof StructuredPostalDataItem) {
1344 StructuredPostalDataItem postal = (StructuredPostalDataItem) dataItem;
1345 final String postalAddress = postal.getFormattedAddress();
1346 if (!TextUtils.isEmpty(postalAddress)) {
Paul Soulos23e28362014-08-29 14:57:08 -07001347 primaryContentDescription.append(res.getString(R.string.map_other)).append(" ");
Paul Souloseb64a4b2014-07-07 17:03:27 -07001348 intent = StructuredPostalUtils.getViewPostalAddressIntent(postalAddress);
1349 header = postal.getFormattedAddress();
Paul Soulos2a4207f2014-07-31 17:09:05 -07001350 entryContextMenuInfo = new EntryContextMenuInfo(header,
Paul Soulos23e28362014-08-29 14:57:08 -07001351 res.getString(R.string.postalLabelsGroup));
Paul Soulosf9d5c0e2014-07-09 11:25:59 -07001352 if (postal.hasKindTypeColumn(kind)) {
Paul Soulos23e28362014-08-29 14:57:08 -07001353 text = StructuredPostal.getTypeLabel(res,
Paul Soulosf9d5c0e2014-07-09 11:25:59 -07001354 postal.getKindTypeColumn(kind), postal.getLabel()).toString();
Paul Soulos23e28362014-08-29 14:57:08 -07001355 primaryContentDescription.append(text).append(" ");
Paul Soulosf9d5c0e2014-07-09 11:25:59 -07001356 }
Paul Soulos23e28362014-08-29 14:57:08 -07001357 primaryContentDescription.append(header);
Paul Soulos6a4d2732014-07-22 14:22:12 -07001358 alternateIntent =
1359 StructuredPostalUtils.getViewPostalAddressDirectionsIntent(postalAddress);
Paul Soulos23e28362014-08-29 14:57:08 -07001360 alternateIcon = res.getDrawable(R.drawable.ic_directions_24dp);
1361 alternateContentDescription.append(res.getString(
1362 R.string.content_description_directions)).append(" ").append(header);
1363 icon = res.getDrawable(R.drawable.ic_place_24dp);
Paul Soulos48290be2014-09-08 13:44:51 -07001364 iconResourceId = R.drawable.ic_place_24dp;
Paul Souloseb64a4b2014-07-07 17:03:27 -07001365 }
1366 } else if (dataItem instanceof SipAddressDataItem) {
Brian Attwell6bb01342014-08-20 23:16:00 -07001367 if (PhoneCapabilityTester.isSipPhone(context)) {
Paul Souloseb64a4b2014-07-07 17:03:27 -07001368 final SipAddressDataItem sip = (SipAddressDataItem) dataItem;
1369 final String address = sip.getSipAddress();
1370 if (!TextUtils.isEmpty(address)) {
Paul Soulos23e28362014-08-29 14:57:08 -07001371 primaryContentDescription.append(res.getString(R.string.call_other)).append(
1372 " ");
Jay Shrauner1cd88e32014-09-05 15:37:55 -07001373 final Uri callUri = Uri.fromParts(PhoneAccount.SCHEME_SIP, address, null);
Paul Souloseb64a4b2014-07-07 17:03:27 -07001374 intent = CallUtil.getCallIntent(callUri);
Paul Soulos1cda01b2014-07-21 12:22:00 -07001375 header = address;
Paul Soulos2a4207f2014-07-31 17:09:05 -07001376 entryContextMenuInfo = new EntryContextMenuInfo(header,
Paul Soulos23e28362014-08-29 14:57:08 -07001377 res.getString(R.string.phoneLabelsGroup));
Paul Soulos1cda01b2014-07-21 12:22:00 -07001378 if (sip.hasKindTypeColumn(kind)) {
Paul Soulos23e28362014-08-29 14:57:08 -07001379 text = SipAddress.getTypeLabel(res,
Brian Attwell6bb01342014-08-20 23:16:00 -07001380 sip.getKindTypeColumn(kind), sip.getLabel()).toString();
Paul Soulos23e28362014-08-29 14:57:08 -07001381 primaryContentDescription.append(text).append(" ");
Paul Soulos1cda01b2014-07-21 12:22:00 -07001382 }
Paul Soulos23e28362014-08-29 14:57:08 -07001383 primaryContentDescription.append(header);
1384 icon = res.getDrawable(R.drawable.ic_dialer_sip_black_24dp);
Paul Soulos48290be2014-09-08 13:44:51 -07001385 iconResourceId = R.drawable.ic_dialer_sip_black_24dp;
Paul Souloseb64a4b2014-07-07 17:03:27 -07001386 }
1387 }
1388 } else if (dataItem instanceof StructuredNameDataItem) {
1389 final String givenName = ((StructuredNameDataItem) dataItem).getGivenName();
1390 if (!TextUtils.isEmpty(givenName)) {
Paul Soulos23e28362014-08-29 14:57:08 -07001391 aboutCardName.value = res.getString(R.string.about_card_title) +
Brian Attwell6bb01342014-08-20 23:16:00 -07001392 " " + givenName;
Paul Souloseb64a4b2014-07-07 17:03:27 -07001393 } else {
Paul Soulos23e28362014-08-29 14:57:08 -07001394 aboutCardName.value = res.getString(R.string.about_card_title);
Paul Souloseb64a4b2014-07-07 17:03:27 -07001395 }
1396 } else {
1397 // Custom DataItem
Brian Attwell6bb01342014-08-20 23:16:00 -07001398 header = dataItem.buildDataStringForDisplay(context, kind);
Paul Souloseb64a4b2014-07-07 17:03:27 -07001399 text = kind.typeColumn;
1400 intent = new Intent(Intent.ACTION_VIEW);
Paul Soulos60e51082014-07-10 12:33:04 -07001401 final Uri uri = ContentUris.withAppendedId(Data.CONTENT_URI, dataItem.getId());
1402 intent.setDataAndType(uri, dataItem.getMimeType());
Paul Soulose0055662014-07-10 16:33:08 -07001403
1404 if (intent != null) {
1405 final String mimetype = intent.getType();
1406
1407 // Attempt to use known icons for known 3p types. Otherwise default to ResolveCache
1408 switch (mimetype) {
1409 case MIMETYPE_GPLUS_PROFILE:
1410 if (INTENT_DATA_GPLUS_PROFILE_ADD_TO_CIRCLE.equals(
1411 intent.getDataString())) {
Paul Soulos23e28362014-08-29 14:57:08 -07001412 icon = res.getDrawable(
Paul Soulose0055662014-07-10 16:33:08 -07001413 R.drawable.ic_add_to_circles_black_24);
Paul Soulos48290be2014-09-08 13:44:51 -07001414 iconResourceId = R.drawable.ic_add_to_circles_black_24;
Paul Soulose0055662014-07-10 16:33:08 -07001415 } else {
Paul Soulos23e28362014-08-29 14:57:08 -07001416 icon = res.getDrawable(R.drawable.ic_google_plus_24dp);
Paul Soulos48290be2014-09-08 13:44:51 -07001417 iconResourceId = R.drawable.ic_google_plus_24dp;
Paul Soulose0055662014-07-10 16:33:08 -07001418 }
1419 break;
1420 case MIMETYPE_HANGOUTS:
1421 if (INTENT_DATA_HANGOUTS_VIDEO.equals(intent.getDataString())) {
Paul Soulos23e28362014-08-29 14:57:08 -07001422 icon = res.getDrawable(R.drawable.ic_hangout_video_24dp);
Paul Soulos48290be2014-09-08 13:44:51 -07001423 iconResourceId = R.drawable.ic_hangout_video_24dp;
Paul Soulose0055662014-07-10 16:33:08 -07001424 } else {
Paul Soulos23e28362014-08-29 14:57:08 -07001425 icon = res.getDrawable(R.drawable.ic_hangout_24dp);
Paul Soulos48290be2014-09-08 13:44:51 -07001426 iconResourceId = R.drawable.ic_hangout_24dp;
Paul Soulose0055662014-07-10 16:33:08 -07001427 }
1428 break;
1429 default:
Paul Soulos2a4207f2014-07-31 17:09:05 -07001430 entryContextMenuInfo = new EntryContextMenuInfo(header, mimetype);
Brian Attwell6bb01342014-08-20 23:16:00 -07001431 icon = ResolveCache.getInstance(context).getIcon(
Paul Soulose0055662014-07-10 16:33:08 -07001432 dataItem.getMimeType(), intent);
1433 // Call mutate to create a new Drawable.ConstantState for color filtering
1434 if (icon != null) {
1435 icon.mutate();
1436 }
Paul Soulos48ebbaa2014-07-15 13:11:23 -07001437 shouldApplyColor = false;
Paul Soulose0055662014-07-10 16:33:08 -07001438 }
1439 }
Daniel Lehmannedb576a2011-07-27 16:45:13 -07001440 }
1441
Paul Souloseb64a4b2014-07-07 17:03:27 -07001442 if (intent != null) {
1443 // Do not set the intent is there are no resolves
Brian Attwell6bb01342014-08-20 23:16:00 -07001444 if (!PhoneCapabilityTester.isIntentRegistered(context, intent)) {
Paul Souloseb64a4b2014-07-07 17:03:27 -07001445 intent = null;
Paul Soulos16339332014-06-24 13:59:56 -07001446 }
Paul Souloseb64a4b2014-07-07 17:03:27 -07001447 }
Paul Soulos16339332014-06-24 13:59:56 -07001448
Paul Soulosdd7419d2014-07-15 11:22:13 -07001449 if (alternateIntent != null) {
1450 // Do not set the alternate intent is there are no resolves
Brian Attwell6bb01342014-08-20 23:16:00 -07001451 if (!PhoneCapabilityTester.isIntentRegistered(context, alternateIntent)) {
Paul Soulosdd7419d2014-07-15 11:22:13 -07001452 alternateIntent = null;
Jay Shraunerf0d04c12014-09-02 13:44:48 -07001453 } else if (TextUtils.isEmpty(alternateContentDescription)) {
1454 // Attempt to use package manager to find a suitable content description if needed
Paul Soulos23e28362014-08-29 14:57:08 -07001455 alternateContentDescription.append(getIntentResolveLabel(alternateIntent, context));
Paul Soulosdd7419d2014-07-15 11:22:13 -07001456 }
1457 }
1458
Paul Souloseb64a4b2014-07-07 17:03:27 -07001459 // If the Entry has no visual elements, return null
1460 if (icon == null && TextUtils.isEmpty(header) && TextUtils.isEmpty(subHeader) &&
1461 subHeaderIcon == null && TextUtils.isEmpty(text) && textIcon == null) {
1462 return null;
1463 }
1464
Brian Attwellfa238942014-08-12 10:21:28 -07001465 // Ignore dataIds from the Me profile.
Paul Soulosea5e0b72014-07-08 18:09:44 -07001466 final int dataId = dataItem.getId() > Integer.MAX_VALUE ?
1467 -1 : (int) dataItem.getId();
1468
Paul Soulos23e28362014-08-29 14:57:08 -07001469 return new Entry(dataId, icon, header, subHeader, subHeaderIcon, text, textIcon,
1470 primaryContentDescription.toString(), intent, alternateIcon, alternateIntent,
1471 alternateContentDescription.toString(), shouldApplyColor, isEditable,
Paul Soulos48290be2014-09-08 13:44:51 -07001472 entryContextMenuInfo, thirdIcon, thirdIntent, thirdContentDescription,
1473 iconResourceId);
Paul Souloseb64a4b2014-07-07 17:03:27 -07001474 }
1475
Brian Attwell6bb01342014-08-20 23:16:00 -07001476 private List<Entry> dataItemsToEntries(List<DataItem> dataItems,
1477 MutableString aboutCardTitleOut) {
Paul Souloseb64a4b2014-07-07 17:03:27 -07001478 final List<Entry> entries = new ArrayList<>();
1479 for (DataItem dataItem : dataItems) {
Brian Attwell6bb01342014-08-20 23:16:00 -07001480 final Entry entry = dataItemToEntry(dataItem, this, mContactData, aboutCardTitleOut);
Paul Souloseb64a4b2014-07-07 17:03:27 -07001481 if (entry != null) {
1482 entries.add(entry);
1483 }
1484 }
1485 return entries;
Daniel Lehmannedb576a2011-07-27 16:45:13 -07001486 }
1487
Brian Attwell6bb01342014-08-20 23:16:00 -07001488 private static String getIntentResolveLabel(Intent intent, Context context) {
1489 final List<ResolveInfo> matches = context.getPackageManager().queryIntentActivities(intent,
Paul Soulosdd7419d2014-07-15 11:22:13 -07001490 PackageManager.MATCH_DEFAULT_ONLY);
1491
1492 // Pick first match, otherwise best found
1493 ResolveInfo bestResolve = null;
1494 final int size = matches.size();
1495 if (size == 1) {
1496 bestResolve = matches.get(0);
1497 } else if (size > 1) {
Brian Attwell6bb01342014-08-20 23:16:00 -07001498 bestResolve = ResolveCache.getInstance(context).getBestResolve(intent, matches);
Paul Soulosdd7419d2014-07-15 11:22:13 -07001499 }
1500
1501 if (bestResolve == null) {
1502 return null;
1503 }
1504
Brian Attwell6bb01342014-08-20 23:16:00 -07001505 return String.valueOf(bestResolve.loadLabel(context.getPackageManager()));
Paul Soulosdd7419d2014-07-15 11:22:13 -07001506 }
1507
Daniel Lehmannedb576a2011-07-27 16:45:13 -07001508 /**
Brian Attwell31b2d422014-06-05 00:14:58 -07001509 * Asynchronously extract the most vibrant color from the PhotoView. Once extracted,
1510 * apply this tint to {@link MultiShrinkScroller}. This operation takes about 20-30ms
1511 * on a Nexus 5.
1512 */
1513 private void extractAndApplyTintFromPhotoViewAsynchronously() {
1514 if (mScroller == null) {
1515 return;
1516 }
1517 final Drawable imageViewDrawable = mPhotoView.getDrawable();
Brian Attwellfaf97392014-07-10 18:32:30 -07001518 new AsyncTask<Void, Void, MaterialPalette>() {
Brian Attwell31b2d422014-06-05 00:14:58 -07001519 @Override
Brian Attwellfaf97392014-07-10 18:32:30 -07001520 protected MaterialPalette doInBackground(Void... params) {
1521
Brian Attwell95c268e2014-08-26 22:04:15 -07001522 if (imageViewDrawable instanceof BitmapDrawable
1523 && mContactData.getThumbnailPhotoBinaryData() != null
1524 && mContactData.getThumbnailPhotoBinaryData().length > 0) {
1525 // Perform the color analysis on the thumbnail instead of the full sized
1526 // image, so that our results will be as similar as possible to the Bugle
1527 // app.
1528 final Bitmap bitmap = BitmapFactory.decodeByteArray(
1529 mContactData.getThumbnailPhotoBinaryData(), 0,
1530 mContactData.getThumbnailPhotoBinaryData().length);
1531 try {
1532 final int primaryColor = colorFromBitmap(bitmap);
1533 if (primaryColor != 0) {
1534 return mMaterialColorMapUtils.calculatePrimaryAndSecondaryColor(
1535 primaryColor);
1536 }
1537 } finally {
1538 bitmap.recycle();
Brian Attwellfaf97392014-07-10 18:32:30 -07001539 }
Brian Attwell8a6f4ad2014-06-06 21:54:53 -07001540 }
1541 if (imageViewDrawable instanceof LetterTileDrawable) {
Brian Attwellfaf97392014-07-10 18:32:30 -07001542 final int primaryColor = ((LetterTileDrawable) imageViewDrawable).getColor();
Brian Attwella3859ed2014-07-15 13:51:55 -07001543 return mMaterialColorMapUtils.calculatePrimaryAndSecondaryColor(primaryColor);
Brian Attwell31b2d422014-06-05 00:14:58 -07001544 }
Brian Attwella3859ed2014-07-15 13:51:55 -07001545 return MaterialColorMapUtils.getDefaultPrimaryAndSecondaryColors(getResources());
Brian Attwell31b2d422014-06-05 00:14:58 -07001546 }
1547
1548 @Override
Brian Attwellfaf97392014-07-10 18:32:30 -07001549 protected void onPostExecute(MaterialPalette palette) {
1550 super.onPostExecute(palette);
Brian Attwell8571dd32014-06-23 23:29:10 -07001551 if (mHasComputedThemeColor) {
1552 // If we had previously computed a theme color from the contact photo,
1553 // then do not update the theme color. Changing the theme color several
1554 // seconds after QC has started, as a result of an updated/upgraded photo,
1555 // is a jarring experience. On the other hand, changing the theme color after
1556 // a rotation or onNewIntent() is perfectly fine.
1557 return;
1558 }
1559 // Check that the Photo has not changed. If it has changed, the new tint
1560 // color needs to be extracted
1561 if (imageViewDrawable == mPhotoView.getDrawable()) {
1562 mHasComputedThemeColor = true;
Brian Attwellfaf97392014-07-10 18:32:30 -07001563 setThemeColor(palette);
Brian Attwell31b2d422014-06-05 00:14:58 -07001564 }
1565 }
1566 }.execute();
1567 }
1568
Brian Attwell4936a922014-07-02 16:16:59 -07001569 /**
1570 * Examine how many white pixels are in the bitmap in order to determine whether or not
1571 * we need gradient overlays on top of the image.
1572 */
1573 private void analyzeWhitenessOfPhotoAsynchronously() {
1574 final Drawable imageViewDrawable = mPhotoView.getDrawable();
1575 new AsyncTask<Void, Void, Boolean>() {
1576 @Override
1577 protected Boolean doInBackground(Void... params) {
1578 if (imageViewDrawable instanceof BitmapDrawable) {
1579 final Bitmap bitmap = ((BitmapDrawable) imageViewDrawable).getBitmap();
1580 return WhitenessUtils.isBitmapWhiteAtTopOrBottom(bitmap);
1581 }
1582 return !(imageViewDrawable instanceof LetterTileDrawable);
1583 }
1584
1585 @Override
1586 protected void onPostExecute(Boolean isWhite) {
1587 super.onPostExecute(isWhite);
1588 mScroller.setUseGradient(isWhite);
1589 }
1590 }.execute();
1591 }
1592
Brian Attwellfaf97392014-07-10 18:32:30 -07001593 private void setThemeColor(MaterialPalette palette) {
Brian Attwell9b889e62014-06-23 18:25:32 -07001594 // If the color is invalid, use the predefined default
Brian Attwellfaf97392014-07-10 18:32:30 -07001595 final int primaryColor = palette.mPrimaryColor;
1596 mScroller.setHeaderTintColor(primaryColor);
1597 mStatusBarColor = palette.mSecondaryColor;
Brian Attwell9b889e62014-06-23 18:25:32 -07001598 updateStatusBarColor();
Brian Attwell8571dd32014-06-23 23:29:10 -07001599
Brian Attwell9b889e62014-06-23 18:25:32 -07001600 mColorFilter =
Brian Attwellfaf97392014-07-10 18:32:30 -07001601 new PorterDuffColorFilter(primaryColor, PorterDuff.Mode.SRC_ATOP);
1602 mContactCard.setColorAndFilter(primaryColor, mColorFilter);
1603 mRecentCard.setColorAndFilter(primaryColor, mColorFilter);
1604 mAboutCard.setColorAndFilter(primaryColor, mColorFilter);
Brian Attwell9b889e62014-06-23 18:25:32 -07001605 }
1606
Brian Attwell8a6f4ad2014-06-06 21:54:53 -07001607 private void updateStatusBarColor() {
1608 if (mScroller == null) {
1609 return;
1610 }
1611 final int desiredStatusBarColor;
1612 // Only use a custom status bar color if QuickContacts touches the top of the viewport.
1613 if (mScroller.getScrollNeededToBeFullScreen() <= 0) {
1614 desiredStatusBarColor = mStatusBarColor;
1615 } else {
1616 desiredStatusBarColor = Color.TRANSPARENT;
1617 }
1618 // Animate to the new color.
Brian Attwell847bf2c2014-07-24 13:59:27 -07001619 final ObjectAnimator animation = ObjectAnimator.ofInt(getWindow(), "statusBarColor",
1620 getWindow().getStatusBarColor(), desiredStatusBarColor);
1621 animation.setDuration(ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION);
1622 animation.setEvaluator(new ArgbEvaluator());
1623 animation.start();
Brian Attwell8a6f4ad2014-06-06 21:54:53 -07001624 }
1625
1626 private int colorFromBitmap(Bitmap bitmap) {
1627 // Author of Palette recommends using 24 colors when analyzing profile photos.
1628 final int NUMBER_OF_PALETTE_COLORS = 24;
1629 final Palette palette = Palette.generate(bitmap, NUMBER_OF_PALETTE_COLORS);
Brian Attwella0f20f72014-07-07 11:51:52 -07001630 if (palette != null && palette.getVibrantSwatch() != null) {
1631 return palette.getVibrantSwatch().getRgb();
Brian Attwell8a6f4ad2014-06-06 21:54:53 -07001632 }
1633 return 0;
1634 }
1635
Paul Soulosb3054e52014-06-05 16:46:02 -07001636 private List<Entry> contactInteractionsToEntries(List<ContactInteraction> interactions) {
Paul Souloseb64a4b2014-07-07 17:03:27 -07001637 final List<Entry> entries = new ArrayList<>();
Paul Soulosb3054e52014-06-05 16:46:02 -07001638 for (ContactInteraction interaction : interactions) {
Paul Soulosea5e0b72014-07-08 18:09:44 -07001639 entries.add(new Entry(/* id = */ -1,
1640 interaction.getIcon(this),
Paul Soulosb3054e52014-06-05 16:46:02 -07001641 interaction.getViewHeader(this),
1642 interaction.getViewBody(this),
1643 interaction.getBodyIcon(this),
1644 interaction.getViewFooter(this),
1645 interaction.getFooterIcon(this),
Paul Soulos23e28362014-08-29 14:57:08 -07001646 interaction.getContentDescription(this),
Paul Soulosb3054e52014-06-05 16:46:02 -07001647 interaction.getIntent(),
Paul Soulosdd7419d2014-07-15 11:22:13 -07001648 /* alternateIcon = */ null,
1649 /* alternateIntent = */ null,
1650 /* alternateContentDescription = */ null,
Paul Soulos48ebbaa2014-07-15 13:11:23 -07001651 /* shouldApplyColor = */ true,
Paul Soulos2a4207f2014-07-31 17:09:05 -07001652 /* isEditable = */ false,
Paul Soulos48fc9122014-08-26 13:52:36 -07001653 /* EntryContextMenuInfo = */ null,
1654 /* thirdIcon = */ null,
1655 /* thirdIntent = */ null,
Paul Soulos48290be2014-09-08 13:44:51 -07001656 /* thirdContentDescription = */ null,
1657 interaction.getIconResourceId()));
Paul Soulosb3054e52014-06-05 16:46:02 -07001658 }
1659 return entries;
1660 }
1661
Paul Souloseb64a4b2014-07-07 17:03:27 -07001662 private final LoaderCallbacks<Contact> mLoaderContactCallbacks =
Maurice Chu851222a2012-06-21 11:43:08 -07001663 new LoaderCallbacks<Contact>() {
Daniel Lehmanncb8d73f2011-12-16 17:39:50 +02001664 @Override
Maurice Chu851222a2012-06-21 11:43:08 -07001665 public void onLoaderReset(Loader<Contact> loader) {
Paul Soulos405ae402014-07-14 16:16:36 -07001666 mContactData = null;
Daniel Lehmanncb8d73f2011-12-16 17:39:50 +02001667 }
1668
1669 @Override
Maurice Chu851222a2012-06-21 11:43:08 -07001670 public void onLoadFinished(Loader<Contact> loader, Contact data) {
Brian Attwell8a6f4ad2014-06-06 21:54:53 -07001671 Trace.beginSection("onLoadFinished()");
1672
Daniel Lehmanncb8d73f2011-12-16 17:39:50 +02001673 if (isFinishing()) {
Daniel Lehmanncb8d73f2011-12-16 17:39:50 +02001674 return;
1675 }
1676 if (data.isError()) {
1677 // This shouldn't ever happen, so throw an exception. The {@link ContactLoader}
1678 // should log the actual exception.
1679 throw new IllegalStateException("Failed to load contact", data.getException());
1680 }
1681 if (data.isNotFound()) {
Brian Attwell8a6f4ad2014-06-06 21:54:53 -07001682 if (mHasAlreadyBeenOpened) {
1683 finish();
1684 } else {
1685 Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri());
1686 Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage,
1687 Toast.LENGTH_LONG).show();
1688 }
Daniel Lehmanncb8d73f2011-12-16 17:39:50 +02001689 return;
1690 }
1691
Paul Soulosb3054e52014-06-05 16:46:02 -07001692 bindContactData(data);
Daniel Lehmanncb8d73f2011-12-16 17:39:50 +02001693
Brian Attwell8a6f4ad2014-06-06 21:54:53 -07001694 Trace.endSection();
Daniel Lehmanncb8d73f2011-12-16 17:39:50 +02001695 }
1696
1697 @Override
Maurice Chu851222a2012-06-21 11:43:08 -07001698 public Loader<Contact> onCreateLoader(int id, Bundle args) {
Daniel Lehmanncb8d73f2011-12-16 17:39:50 +02001699 if (mLookupUri == null) {
1700 Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early");
1701 }
Brian Attwelld28851f2014-06-10 13:25:07 -07001702 // Load all contact data. We need loadGroupMetaData=true to determine whether the
1703 // contact is invisible. If it is, we need to display an "Add to Contacts" MenuItem.
Yorke Leeb2b435a2012-11-12 16:47:06 -08001704 return new ContactLoader(getApplicationContext(), mLookupUri,
Brian Attwelld28851f2014-06-10 13:25:07 -07001705 true /*loadGroupMetaData*/, false /*loadInvitableAccountTypes*/,
Brian Attwell8571dd32014-06-23 23:29:10 -07001706 true /*postViewNotification*/, true /*computeFormattedPhoneNumber*/);
Daniel Lehmanncb8d73f2011-12-16 17:39:50 +02001707 }
1708 };
Paul Soulosb3054e52014-06-05 16:46:02 -07001709
Brian Attwellb7e43642014-06-02 14:33:04 -07001710 @Override
1711 public void onBackPressed() {
1712 if (mScroller != null) {
Brian Attwell8477eaf2014-06-18 15:39:50 -07001713 if (!mIsExitAnimationInProgress) {
1714 mScroller.scrollOffBottom();
1715 }
Brian Attwellb7e43642014-06-02 14:33:04 -07001716 } else {
1717 super.onBackPressed();
1718 }
1719 }
Paul Soulosb3054e52014-06-05 16:46:02 -07001720
Brian Attwell8a6f4ad2014-06-06 21:54:53 -07001721 @Override
1722 public void finish() {
1723 super.finish();
1724
1725 // override transitions to skip the standard window animations
1726 overridePendingTransition(0, 0);
1727 }
1728
Paul Souloseb64a4b2014-07-07 17:03:27 -07001729 private final LoaderCallbacks<List<ContactInteraction>> mLoaderInteractionsCallbacks =
Paul Soulosb3054e52014-06-05 16:46:02 -07001730 new LoaderCallbacks<List<ContactInteraction>>() {
1731
1732 @Override
1733 public Loader<List<ContactInteraction>> onCreateLoader(int id, Bundle args) {
1734 Log.v(TAG, "onCreateLoader");
1735 Loader<List<ContactInteraction>> loader = null;
1736 switch (id) {
1737 case LOADER_SMS_ID:
1738 Log.v(TAG, "LOADER_SMS_ID");
1739 loader = new SmsInteractionsLoader(
1740 QuickContactActivity.this,
Paul Soulosab840442014-06-17 14:08:40 -07001741 args.getStringArray(KEY_LOADER_EXTRA_PHONES),
Paul Soulosb3054e52014-06-05 16:46:02 -07001742 MAX_SMS_RETRIEVE);
1743 break;
Paul Soulos899aa212014-06-11 12:04:43 -07001744 case LOADER_CALENDAR_ID:
1745 Log.v(TAG, "LOADER_CALENDAR_ID");
Paul Soulosae4cafe2014-07-09 14:11:03 -07001746 final String[] emailsArray = args.getStringArray(KEY_LOADER_EXTRA_EMAILS);
1747 List<String> emailsList = null;
1748 if (emailsArray != null) {
1749 emailsList = Arrays.asList(args.getStringArray(KEY_LOADER_EXTRA_EMAILS));
1750 }
Paul Soulos899aa212014-06-11 12:04:43 -07001751 loader = new CalendarInteractionsLoader(
1752 QuickContactActivity.this,
Paul Soulosae4cafe2014-07-09 14:11:03 -07001753 emailsList,
Paul Soulos899aa212014-06-11 12:04:43 -07001754 MAX_FUTURE_CALENDAR_RETRIEVE,
1755 MAX_PAST_CALENDAR_RETRIEVE,
1756 FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR,
1757 PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR);
1758 break;
Paul Soulosab840442014-06-17 14:08:40 -07001759 case LOADER_CALL_LOG_ID:
1760 Log.v(TAG, "LOADER_CALL_LOG_ID");
1761 loader = new CallLogInteractionsLoader(
1762 QuickContactActivity.this,
1763 args.getStringArray(KEY_LOADER_EXTRA_PHONES),
1764 MAX_CALL_LOG_RETRIEVE);
Paul Soulosb3054e52014-06-05 16:46:02 -07001765 }
1766 return loader;
1767 }
1768
1769 @Override
1770 public void onLoadFinished(Loader<List<ContactInteraction>> loader,
1771 List<ContactInteraction> data) {
Paul Soulosb3054e52014-06-05 16:46:02 -07001772 mRecentLoaderResults.put(loader.getId(), data);
1773
1774 if (isAllRecentDataLoaded()) {
1775 bindRecentData();
1776 }
1777 }
1778
1779 @Override
1780 public void onLoaderReset(Loader<List<ContactInteraction>> loader) {
1781 mRecentLoaderResults.remove(loader.getId());
1782 }
Paul Soulosb3054e52014-06-05 16:46:02 -07001783 };
1784
1785 private boolean isAllRecentDataLoaded() {
1786 return mRecentLoaderResults.size() == mRecentLoaderIds.length;
1787 }
1788
1789 private void bindRecentData() {
Paul Souloseb64a4b2014-07-07 17:03:27 -07001790 final List<ContactInteraction> allInteractions = new ArrayList<>();
Brian Attwell30e1ef12014-09-02 15:49:20 -07001791 final List<List<Entry>> interactionsWrapper = new ArrayList<>();
Paul Soulosb3054e52014-06-05 16:46:02 -07001792
Brian Attwell30e1ef12014-09-02 15:49:20 -07001793 mRecentDataTask = new AsyncTask<Void, Void, Void>() {
Paul Soulosb3054e52014-06-05 16:46:02 -07001794 @Override
Brian Attwell30e1ef12014-09-02 15:49:20 -07001795 protected Void doInBackground(Void... params) {
1796 Trace.beginSection("sort recent loader results");
Paul Soulosb3054e52014-06-05 16:46:02 -07001797
Brian Attwell30e1ef12014-09-02 15:49:20 -07001798 for (List<ContactInteraction> loaderInteractions : mRecentLoaderResults.values()) {
1799 allInteractions.addAll(loaderInteractions);
1800 }
1801
1802 // Sort the interactions by most recent
1803 Collections.sort(allInteractions, new Comparator<ContactInteraction>() {
1804 @Override
1805 public int compare(ContactInteraction a, ContactInteraction b) {
1806 return a.getInteractionDate() >= b.getInteractionDate() ? -1 : 1;
1807 }
1808 });
1809
1810 Trace.endSection();
1811 Trace.beginSection("contactInteractionsToEntries");
1812
1813 // Wrap each interaction in its own list so that an icon is displayed for each entry
1814 for (Entry contactInteraction : contactInteractionsToEntries(allInteractions)) {
1815 List<Entry> entryListWrapper = new ArrayList<>(1);
1816 entryListWrapper.add(contactInteraction);
1817 interactionsWrapper.add(entryListWrapper);
1818 }
1819
1820 Trace.endSection();
1821 return null;
1822 }
1823
1824 @Override
1825 protected void onPostExecute(Void aVoid) {
1826 super.onPostExecute(aVoid);
1827 Trace.beginSection("initialize recents card");
1828
1829 if (allInteractions.size() > 0) {
1830 mRecentCard.initialize(interactionsWrapper,
Paul Soulosb3054e52014-06-05 16:46:02 -07001831 /* numInitialVisibleEntries = */ MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN,
Paul Soulosc8e2a912014-07-21 14:52:20 -07001832 /* isExpanded = */ mRecentCard.isExpanded(), /* isAlwaysExpanded = */ false,
Brian Attwell30e1ef12014-09-02 15:49:20 -07001833 mExpandingEntryCardViewListener, mScroller);
1834 mRecentCard.setVisibility(View.VISIBLE);
1835 }
Paul Souloseb64a4b2014-07-07 17:03:27 -07001836
Brian Attwell30e1ef12014-09-02 15:49:20 -07001837 Trace.endSection();
1838
1839 // About card is initialized along with the contact card, but since it appears after
1840 // the recent card in the UI, we hold off until making it visible until the recent
1841 // card is also ready to avoid stuttering.
1842 if (mAboutCard.shouldShow()) {
1843 mAboutCard.setVisibility(View.VISIBLE);
1844 } else {
1845 mAboutCard.setVisibility(View.GONE);
1846 }
1847 mRecentDataTask = null;
1848 }
1849 };
1850 mRecentDataTask.execute();
Paul Soulosb3054e52014-06-05 16:46:02 -07001851 }
Brian Attwell8a6f4ad2014-06-06 21:54:53 -07001852
1853 @Override
1854 protected void onStop() {
1855 super.onStop();
1856
1857 if (mEntriesAndActionsTask != null) {
1858 // Once the activity is stopped, we will no longer want to bind mEntriesAndActionsTask's
1859 // results on the UI thread. In some circumstances Activities are killed without
1860 // onStop() being called. This is not a problem, because in these circumstances
1861 // the entire process will be killed.
1862 mEntriesAndActionsTask.cancel(/* mayInterruptIfRunning = */ false);
1863 }
Brian Attwell30e1ef12014-09-02 15:49:20 -07001864 if (mRecentDataTask != null) {
1865 mRecentDataTask.cancel(/* mayInterruptIfRunning = */ false);
1866 }
Brian Attwell8a6f4ad2014-06-06 21:54:53 -07001867 }
Paul Soulos23889ba2014-06-10 14:55:32 -07001868
1869 /**
Brian Attwelld28851f2014-06-10 13:25:07 -07001870 * Returns true if it is possible to edit the current contact.
1871 */
1872 private boolean isContactEditable() {
1873 return mContactData != null && !mContactData.isDirectoryEntry();
1874 }
1875
Brian Attwella41d6d12014-07-31 15:23:13 -07001876 /**
1877 * Returns true if it is possible to share the current contact.
1878 */
1879 private boolean isContactShareable() {
1880 return mContactData != null && !mContactData.isDirectoryEntry();
1881 }
1882
Brian Attwell60953692014-07-11 17:18:46 -07001883 private Intent getEditContactIntent() {
Brian Attwell63176c92014-08-18 15:14:18 -07001884 final Intent intent = new Intent(Intent.ACTION_EDIT, mContactData.getLookupUri());
Brian Attwelld28851f2014-06-10 13:25:07 -07001885 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
Brian Attwell60953692014-07-11 17:18:46 -07001886 return intent;
1887 }
1888
1889 private void editContact() {
Paul Soulosa42ef762014-08-20 10:26:10 -07001890 mHasIntentLaunched = true;
Zheng Fuc00a0b52014-09-02 16:44:44 -07001891 mContactLoader.cacheResult();
Brian Attwell60953692014-07-11 17:18:46 -07001892 startActivityForResult(getEditContactIntent(), REQUEST_CODE_CONTACT_EDITOR_ACTIVITY);
Brian Attwelld28851f2014-06-10 13:25:07 -07001893 }
1894
1895 private void toggleStar(MenuItem starredMenuItem) {
1896 // Make sure there is a contact
Brian Attwell63176c92014-08-18 15:14:18 -07001897 if (mContactData != null) {
Brian Attwelld28851f2014-06-10 13:25:07 -07001898 // Read the current starred value from the UI instead of using the last
1899 // loaded state. This allows rapid tapping without writing the same
1900 // value several times
1901 final boolean isStarred = starredMenuItem.isChecked();
1902
1903 // To improve responsiveness, swap out the picture (and tag) in the UI already
Paul Soulos333091a2014-07-22 13:54:41 -07001904 ContactDisplayUtils.configureStarredMenuItem(starredMenuItem,
Brian Attwelld28851f2014-06-10 13:25:07 -07001905 mContactData.isDirectoryEntry(), mContactData.isUserProfile(),
1906 !isStarred);
1907
1908 // Now perform the real save
Paul Souloseb64a4b2014-07-07 17:03:27 -07001909 final Intent intent = ContactSaveService.createSetStarredIntent(
Brian Attwell63176c92014-08-18 15:14:18 -07001910 QuickContactActivity.this, mContactData.getLookupUri(), !isStarred);
Brian Attwelld28851f2014-06-10 13:25:07 -07001911 startService(intent);
Brian Attwell35ccdba2014-07-18 11:43:23 -07001912
1913 final CharSequence accessibilityText = !isStarred
1914 ? getResources().getText(R.string.description_action_menu_add_star)
1915 : getResources().getText(R.string.description_action_menu_remove_star);
1916 // Accessibility actions need to have an associated view. We can't access the MenuItem's
1917 // underlying view, so put this accessibility action on the root view.
1918 mScroller.announceForAccessibility(accessibilityText);
Brian Attwelld28851f2014-06-10 13:25:07 -07001919 }
1920 }
1921
Brian Attwell752cccf2014-06-10 16:24:04 -07001922 /**
1923 * Calls into the contacts provider to get a pre-authorized version of the given URI.
1924 */
1925 private Uri getPreAuthorizedUri(Uri uri) {
1926 final Bundle uriBundle = new Bundle();
1927 uriBundle.putParcelable(ContactsContract.Authorization.KEY_URI_TO_AUTHORIZE, uri);
1928 final Bundle authResponse = getContentResolver().call(
1929 ContactsContract.AUTHORITY_URI,
1930 ContactsContract.Authorization.AUTHORIZATION_METHOD,
1931 null,
1932 uriBundle);
1933 if (authResponse != null) {
1934 return (Uri) authResponse.getParcelable(
1935 ContactsContract.Authorization.KEY_AUTHORIZED_URI);
1936 } else {
1937 return uri;
1938 }
1939 }
Brian Attwell2d150da2014-07-09 22:35:56 -07001940
Brian Attwell752cccf2014-06-10 16:24:04 -07001941 private void shareContact() {
1942 final String lookupKey = mContactData.getLookupKey();
1943 Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey);
1944 if (mContactData.isUserProfile()) {
1945 // User is sharing the profile. We don't want to force the receiver to have
1946 // the highly-privileged READ_PROFILE permission, so we need to request a
1947 // pre-authorized URI from the provider.
1948 shareUri = getPreAuthorizedUri(shareUri);
1949 }
1950
1951 final Intent intent = new Intent(Intent.ACTION_SEND);
1952 intent.setType(Contacts.CONTENT_VCARD_TYPE);
1953 intent.putExtra(Intent.EXTRA_STREAM, shareUri);
1954
1955 // Launch chooser to share contact via
1956 final CharSequence chooseTitle = getText(R.string.share_via);
1957 final Intent chooseIntent = Intent.createChooser(intent, chooseTitle);
1958
1959 try {
Paul Soulosa42ef762014-08-20 10:26:10 -07001960 mHasIntentLaunched = true;
Brian Attwell752cccf2014-06-10 16:24:04 -07001961 this.startActivity(chooseIntent);
Paul Souloseb64a4b2014-07-07 17:03:27 -07001962 } catch (final ActivityNotFoundException ex) {
Brian Attwell752cccf2014-06-10 16:24:04 -07001963 Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show();
1964 }
1965 }
1966
1967 /**
1968 * Creates a launcher shortcut with the current contact.
1969 */
1970 private void createLauncherShortcutWithContact() {
1971 final ShortcutIntentBuilder builder = new ShortcutIntentBuilder(this,
1972 new OnShortcutIntentCreatedListener() {
1973
1974 @Override
1975 public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) {
1976 // Broadcast the shortcutIntent to the launcher to create a
1977 // shortcut to this contact
1978 shortcutIntent.setAction(ACTION_INSTALL_SHORTCUT);
1979 QuickContactActivity.this.sendBroadcast(shortcutIntent);
1980
1981 // Send a toast to give feedback to the user that a shortcut to this
1982 // contact was added to the launcher.
1983 Toast.makeText(QuickContactActivity.this,
1984 R.string.createContactShortcutSuccessful,
1985 Toast.LENGTH_SHORT).show();
1986 }
1987
1988 });
Brian Attwell63176c92014-08-18 15:14:18 -07001989 builder.createContactShortcutIntent(mContactData.getLookupUri());
Brian Attwell752cccf2014-06-10 16:24:04 -07001990 }
1991
Brian Attwelld28851f2014-06-10 13:25:07 -07001992 @Override
1993 public boolean onCreateOptionsMenu(Menu menu) {
Paul Souloseb64a4b2014-07-07 17:03:27 -07001994 final MenuInflater inflater = getMenuInflater();
Brian Attwelld28851f2014-06-10 13:25:07 -07001995 inflater.inflate(R.menu.quickcontact, menu);
1996 return true;
1997 }
1998
1999 @Override
2000 public boolean onPrepareOptionsMenu(Menu menu) {
2001 if (mContactData != null) {
2002 final MenuItem starredMenuItem = menu.findItem(R.id.menu_star);
Paul Soulos333091a2014-07-22 13:54:41 -07002003 ContactDisplayUtils.configureStarredMenuItem(starredMenuItem,
Brian Attwelld28851f2014-06-10 13:25:07 -07002004 mContactData.isDirectoryEntry(), mContactData.isUserProfile(),
2005 mContactData.getStarred());
Brian Attwella41d6d12014-07-31 15:23:13 -07002006
Brian Attwelld28851f2014-06-10 13:25:07 -07002007 // Configure edit MenuItem
2008 final MenuItem editMenuItem = menu.findItem(R.id.menu_edit);
2009 editMenuItem.setVisible(true);
2010 if (DirectoryContactUtil.isDirectoryContact(mContactData) || InvisibleContactUtil
2011 .isInvisibleAndAddable(mContactData, this)) {
Brian Attwell30cfd122014-06-13 16:42:11 -07002012 editMenuItem.setIcon(R.drawable.ic_person_add_tinted_24dp);
Brian Attwell2e4214c2014-07-10 22:03:16 -07002013 editMenuItem.setTitle(R.string.menu_add_contact);
Brian Attwelld28851f2014-06-10 13:25:07 -07002014 } else if (isContactEditable()) {
2015 editMenuItem.setIcon(R.drawable.ic_create_24dp);
Brian Attwell2e4214c2014-07-10 22:03:16 -07002016 editMenuItem.setTitle(R.string.menu_editContact);
Brian Attwelld28851f2014-06-10 13:25:07 -07002017 } else {
2018 editMenuItem.setVisible(false);
2019 }
Brian Attwella41d6d12014-07-31 15:23:13 -07002020
2021 final MenuItem shareMenuItem = menu.findItem(R.id.menu_share);
2022 shareMenuItem.setVisible(isContactShareable());
2023
Paul Soulos8a6d0022014-06-16 15:11:03 -07002024 return true;
Brian Attwelld28851f2014-06-10 13:25:07 -07002025 }
Paul Soulos8a6d0022014-06-16 15:11:03 -07002026 return false;
Brian Attwelld28851f2014-06-10 13:25:07 -07002027 }
2028
2029 @Override
2030 public boolean onOptionsItemSelected(MenuItem item) {
2031 switch (item.getItemId()) {
2032 case R.id.menu_star:
2033 toggleStar(item);
2034 return true;
2035 case R.id.menu_edit:
2036 if (DirectoryContactUtil.isDirectoryContact(mContactData)) {
Paul Soulosab54ea12014-08-28 17:22:38 -07002037 // This action is used to launch the contact selector, with the option of
2038 // creating a new contact. Creating a new contact is an INSERT, while selecting
2039 // an exisiting one is an edit. The fields in the edit screen will be
2040 // prepopulated with data.
2041
2042 final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
2043 intent.setType(Contacts.CONTENT_ITEM_TYPE);
2044
2045 // Only pre-fill the name field if the provided display name is an organization
2046 // name or better (e.g. structured name, nickname)
2047 if (mContactData.getDisplayNameSource() >= DisplayNameSources.ORGANIZATION) {
2048 intent.putExtra(Intents.Insert.NAME, mContactData.getDisplayName());
2049 }
2050 ArrayList<ContentValues> values = mContactData.getContentValues();
2051 // Last time used and times used are aggregated values from the usage stat
2052 // table. They need to be removed from data values so the SQL table can insert
2053 // properly
2054 for (ContentValues value : values) {
2055 value.remove(Data.LAST_TIME_USED);
2056 value.remove(Data.TIMES_USED);
2057 }
2058 intent.putExtra(Intents.Insert.DATA, values);
2059
2060 // If the contact can only export to the same account, add it to the intent.
2061 // Otherwise the ContactEditorFragment will show a dialog for selecting an
2062 // account.
2063 if (mContactData.getDirectoryExportSupport() ==
2064 Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY) {
2065 intent.putExtra(Intents.Insert.ACCOUNT,
2066 new Account(mContactData.getDirectoryAccountName(),
2067 mContactData.getDirectoryAccountType()));
2068 intent.putExtra(Intents.Insert.DATA_SET,
2069 mContactData.getRawContacts().get(0).getDataSet());
2070 }
2071
2072 startActivityForResult(intent, REQUEST_CODE_CONTACT_SELECTION_ACTIVITY);
Brian Attwelld28851f2014-06-10 13:25:07 -07002073 } else if (InvisibleContactUtil.isInvisibleAndAddable(mContactData, this)) {
2074 InvisibleContactUtil.addToDefaultGroup(mContactData, this);
2075 } else if (isContactEditable()) {
2076 editContact();
2077 }
2078 return true;
Brian Attwell752cccf2014-06-10 16:24:04 -07002079 case R.id.menu_share:
2080 shareContact();
2081 return true;
2082 case R.id.menu_create_contact_shortcut:
2083 createLauncherShortcutWithContact();
2084 return true;
Brian Attwelld28851f2014-06-10 13:25:07 -07002085 default:
2086 return super.onOptionsItemSelected(item);
2087 }
Paul Soulos23889ba2014-06-10 14:55:32 -07002088 }
Daniel Lehmannedb576a2011-07-27 16:45:13 -07002089}