QuickContactActivity.java revision 6095369885edcca566a812b551886e29c7ff8039
1/* 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 19import android.animation.Animator; 20import android.animation.Animator.AnimatorListener; 21import android.animation.AnimatorListenerAdapter; 22import android.animation.ArgbEvaluator; 23import android.animation.ObjectAnimator; 24import android.app.Activity; 25import android.app.Fragment; 26import android.app.LoaderManager.LoaderCallbacks; 27import android.app.SearchManager; 28import android.content.ActivityNotFoundException; 29import android.content.ComponentName; 30import android.content.ContentUris; 31import android.content.ContentValues; 32import android.content.Intent; 33import android.content.Loader; 34import android.graphics.Bitmap; 35import android.graphics.Color; 36import android.graphics.PorterDuff; 37import android.graphics.PorterDuffColorFilter; 38import android.graphics.drawable.BitmapDrawable; 39import android.graphics.drawable.ColorDrawable; 40import android.graphics.drawable.Drawable; 41import android.net.ParseException; 42import android.net.Uri; 43import android.net.WebAddress; 44import android.os.AsyncTask; 45import android.os.Bundle; 46import android.os.Trace; 47import android.provider.CalendarContract; 48import android.provider.ContactsContract; 49import android.provider.ContactsContract.CommonDataKinds.Email; 50import android.provider.ContactsContract.CommonDataKinds.Event; 51import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 52import android.provider.ContactsContract.CommonDataKinds.Identity; 53import android.provider.ContactsContract.CommonDataKinds.Im; 54import android.provider.ContactsContract.CommonDataKinds.Nickname; 55import android.provider.ContactsContract.CommonDataKinds.Note; 56import android.provider.ContactsContract.CommonDataKinds.Organization; 57import android.provider.ContactsContract.CommonDataKinds.Phone; 58import android.provider.ContactsContract.CommonDataKinds.Relation; 59import android.provider.ContactsContract.CommonDataKinds.SipAddress; 60import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 61import android.provider.ContactsContract.CommonDataKinds.Website; 62import android.provider.ContactsContract.Contacts; 63import android.provider.ContactsContract.Data; 64import android.provider.ContactsContract.DisplayNameSources; 65import android.provider.ContactsContract.DataUsageFeedback; 66import android.provider.ContactsContract.QuickContact; 67import android.provider.ContactsContract.RawContacts; 68import android.support.v7.graphics.Palette; 69import android.text.TextUtils; 70import android.util.Log; 71import android.util.Pair; 72import android.view.Menu; 73import android.view.MenuInflater; 74import android.view.MenuItem; 75import android.view.View; 76import android.view.View.OnClickListener; 77import android.view.WindowManager; 78import android.widget.ImageView; 79import android.widget.Toast; 80import android.widget.Toolbar; 81 82import com.android.contacts.ContactSaveService; 83import com.android.contacts.ContactsActivity; 84import com.android.contacts.NfcHandler; 85import com.android.contacts.R; 86import com.android.contacts.common.CallUtil; 87import com.android.contacts.common.Collapser; 88import com.android.contacts.common.ContactsUtils; 89import com.android.contacts.common.editor.SelectAccountDialogFragment; 90import com.android.contacts.common.lettertiles.LetterTileDrawable; 91import com.android.contacts.common.list.ShortcutIntentBuilder; 92import com.android.contacts.common.list.ShortcutIntentBuilder.OnShortcutIntentCreatedListener; 93import com.android.contacts.common.model.AccountTypeManager; 94import com.android.contacts.common.model.Contact; 95import com.android.contacts.common.model.ContactLoader; 96import com.android.contacts.common.model.RawContact; 97import com.android.contacts.common.model.account.AccountType; 98import com.android.contacts.common.model.account.AccountWithDataSet; 99import com.android.contacts.common.model.dataitem.DataItem; 100import com.android.contacts.common.model.dataitem.DataKind; 101import com.android.contacts.common.model.dataitem.EmailDataItem; 102import com.android.contacts.common.model.dataitem.EventDataItem; 103import com.android.contacts.common.model.dataitem.ImDataItem; 104import com.android.contacts.common.model.dataitem.NicknameDataItem; 105import com.android.contacts.common.model.dataitem.NoteDataItem; 106import com.android.contacts.common.model.dataitem.OrganizationDataItem; 107import com.android.contacts.common.model.dataitem.PhoneDataItem; 108import com.android.contacts.common.model.dataitem.RelationDataItem; 109import com.android.contacts.common.model.dataitem.SipAddressDataItem; 110import com.android.contacts.common.model.dataitem.StructuredNameDataItem; 111import com.android.contacts.common.model.dataitem.StructuredPostalDataItem; 112import com.android.contacts.common.model.dataitem.WebsiteDataItem; 113import com.android.contacts.common.util.DateUtils; 114import com.android.contacts.common.util.MaterialColorMapUtils; 115import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette; 116import com.android.contacts.detail.ContactDetailDisplayUtils; 117import com.android.contacts.interactions.CalendarInteractionsLoader; 118import com.android.contacts.interactions.CallLogInteractionsLoader; 119import com.android.contacts.interactions.ContactDeletionInteraction; 120import com.android.contacts.interactions.ContactInteraction; 121import com.android.contacts.interactions.SmsInteractionsLoader; 122import com.android.contacts.quickcontact.ExpandingEntryCardView.Entry; 123import com.android.contacts.quickcontact.ExpandingEntryCardView.ExpandingEntryCardViewListener; 124import com.android.contacts.util.ImageViewDrawableSetter; 125import com.android.contacts.util.PhoneCapabilityTester; 126import com.android.contacts.util.SchedulingUtils; 127import com.android.contacts.util.StructuredPostalUtils; 128import com.android.contacts.widget.MultiShrinkScroller; 129import com.android.contacts.widget.MultiShrinkScroller.MultiShrinkScrollerListener; 130 131import com.google.common.base.Preconditions; 132import com.google.common.collect.Lists; 133 134import java.util.ArrayList; 135import java.util.Arrays; 136import java.util.Calendar; 137import java.util.Collections; 138import java.util.Comparator; 139import java.util.Date; 140import java.util.HashMap; 141import java.util.List; 142import java.util.Map; 143 144/** 145 * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads 146 * data asynchronously, and then shows a popup with details centered around 147 * {@link Intent#getSourceBounds()}. 148 */ 149public class QuickContactActivity extends ContactsActivity { 150 151 /** 152 * QuickContacts immediately takes up the full screen. All possible information is shown. 153 * This value for {@link android.provider.ContactsContract.QuickContact#EXTRA_MODE} 154 * should only be used by the Contacts app. 155 */ 156 public static final int MODE_FULLY_EXPANDED = 4; 157 158 private static final String TAG = "QuickContact"; 159 160 private static final String KEY_THEME_COLOR = "theme_color"; 161 162 private static final int ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION = 150; 163 private static final int REQUEST_CODE_CONTACT_EDITOR_ACTIVITY = 1; 164 private static final int SCRIM_COLOR = Color.argb(0xB2, 0, 0, 0); 165 private static final String SCHEME_SMSTO = "smsto"; 166 private static final String MIMETYPE_SMS = "vnd.android-dir/mms-sms"; 167 168 /** This is the Intent action to install a shortcut in the launcher. */ 169 private static final String ACTION_INSTALL_SHORTCUT = 170 "com.android.launcher.action.INSTALL_SHORTCUT"; 171 172 @SuppressWarnings("deprecation") 173 private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY; 174 175 private static final String MIMETYPE_GPLUS_PROFILE = 176 "vnd.android.cursor.item/vnd.googleplus.profile"; 177 private static final String INTENT_DATA_GPLUS_PROFILE_ADD_TO_CIRCLE = "Add to circle"; 178 private static final String MIMETYPE_HANGOUTS = 179 "vnd.android.cursor.item/vnd.googleplus.profile.comm"; 180 private static final String INTENT_DATA_HANGOUTS_VIDEO = "Start video call"; 181 182 private Uri mLookupUri; 183 private String[] mExcludeMimes; 184 private int mExtraMode; 185 private int mStatusBarColor; 186 private boolean mHasAlreadyBeenOpened; 187 188 private ImageView mPhotoView; 189 private View mTransparentView; 190 private ExpandingEntryCardView mContactCard; 191 private ExpandingEntryCardView mNoContactDetailsCard; 192 private ExpandingEntryCardView mRecentCard; 193 private ExpandingEntryCardView mAboutCard; 194 /** 195 * This list contains all the {@link DataItem}s. Each nested list contains all data items of a 196 * specific mimetype in sorted order, using mWithinMimeTypeDataItemComparator. The mimetype 197 * lists are sorted using mAmongstMimeTypeDataItemComparator. 198 */ 199 private List<List<DataItem>> mDataItemsList; 200 /** 201 * A map between a mimetype string and the corresponding list of data items. The data items 202 * are in sorted order using mWithinMimeTypeDataItemComparator. 203 */ 204 private Map<String, List<DataItem>> mDataItemsMap; 205 private MultiShrinkScroller mScroller; 206 private SelectAccountDialogFragmentListener mSelectAccountFragmentListener; 207 private AsyncTask<Void, Void, Pair<List<List<DataItem>>, Map<String, List<DataItem>>>> 208 mEntriesAndActionsTask; 209 private ColorDrawable mWindowScrim; 210 private boolean mIsWaitingForOtherPieceOfExitAnimation; 211 private boolean mIsExitAnimationInProgress; 212 private boolean mHasComputedThemeColor; 213 private ComponentName mSmsComponent; 214 215 private Contact mContactData; 216 private ContactLoader mContactLoader; 217 private PorterDuffColorFilter mColorFilter; 218 219 private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter(); 220 221 /** 222 * {@link #LEADING_MIMETYPES} and {@link #TRAILING_MIMETYPES} are used to sort MIME-types. 223 * 224 * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog, 225 * in the order specified here.</p> 226 * 227 * <p>The ones in {@link #TRAILING_MIMETYPES} appear in the end of the dialog, in the order 228 * specified here.</p> 229 * 230 * <p>The rest go between them, in the order in the array.</p> 231 */ 232 private static final List<String> LEADING_MIMETYPES = Lists.newArrayList( 233 Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE); 234 235 /** See {@link #LEADING_MIMETYPES}. */ 236 private static final List<String> TRAILING_MIMETYPES = Lists.newArrayList( 237 StructuredPostal.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE); 238 239 private static final List<String> ABOUT_CARD_MIMETYPES = Lists.newArrayList( 240 Event.CONTENT_ITEM_TYPE, GroupMembership.CONTENT_ITEM_TYPE, Identity.CONTENT_ITEM_TYPE, 241 Im.CONTENT_ITEM_TYPE, Nickname.CONTENT_ITEM_TYPE, Note.CONTENT_ITEM_TYPE, 242 Organization.CONTENT_ITEM_TYPE, Relation.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE); 243 244 /** Id for the background contact loader */ 245 private static final int LOADER_CONTACT_ID = 0; 246 247 private static final String KEY_LOADER_EXTRA_PHONES = 248 QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_PHONES"; 249 250 /** Id for the background Sms Loader */ 251 private static final int LOADER_SMS_ID = 1; 252 private static final int MAX_SMS_RETRIEVE = 3; 253 254 /** Id for the back Calendar Loader */ 255 private static final int LOADER_CALENDAR_ID = 2; 256 private static final String KEY_LOADER_EXTRA_EMAILS = 257 QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_EMAILS"; 258 private static final int MAX_PAST_CALENDAR_RETRIEVE = 3; 259 private static final int MAX_FUTURE_CALENDAR_RETRIEVE = 3; 260 private static final long PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR = 261 180L * 24L * 60L * 60L * 1000L /* 180 days */; 262 private static final long FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR = 263 36L * 60L * 60L * 1000L /* 36 hours */; 264 265 /** Id for the background Call Log Loader */ 266 private static final int LOADER_CALL_LOG_ID = 3; 267 private static final int MAX_CALL_LOG_RETRIEVE = 3; 268 private static final int MIN_NUM_CONTACT_ENTRIES_SHOWN = 3; 269 private static final int MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN = 3; 270 private static final int CARD_ENTRY_ID_EDIT_CONTACT = -2; 271 272 273 private static final int[] mRecentLoaderIds = new int[]{ 274 LOADER_SMS_ID, 275 LOADER_CALENDAR_ID, 276 LOADER_CALL_LOG_ID}; 277 private Map<Integer, List<ContactInteraction>> mRecentLoaderResults; 278 279 private static final String FRAGMENT_TAG_SELECT_ACCOUNT = "select_account_fragment"; 280 281 final OnClickListener mEntryClickHandler = new OnClickListener() { 282 @Override 283 public void onClick(View v) { 284 // Data Id is stored as the entry view id 285 final int dataId = v.getId(); 286 if (dataId == CARD_ENTRY_ID_EDIT_CONTACT) { 287 editContact(); 288 return; 289 } 290 final Object intentObject = v.getTag(); 291 if (intentObject == null || !(intentObject instanceof Intent)) { 292 Log.w(TAG, "Intent tag was not used correctly"); 293 return; 294 } 295 final Intent intent = (Intent) intentObject; 296 297 // Default to USAGE_TYPE_CALL. Usage is summed among all types for sorting each data id 298 // so the exact usage type is not necessary in all cases 299 String usageType = DataUsageFeedback.USAGE_TYPE_CALL; 300 301 final String scheme = intent.getData().getScheme(); 302 if ((scheme != null && scheme.equals(SCHEME_SMSTO)) || 303 (intent.getType() != null && intent.getType().equals(MIMETYPE_SMS))) { 304 usageType = DataUsageFeedback.USAGE_TYPE_SHORT_TEXT; 305 } 306 307 // Data IDs start at 1 so anything less is invalid 308 if (dataId > 0) { 309 final Uri uri = DataUsageFeedback.FEEDBACK_URI.buildUpon() 310 .appendPath(String.valueOf(dataId)) 311 .appendQueryParameter(DataUsageFeedback.USAGE_TYPE, usageType) 312 .build(); 313 final boolean successful = getContentResolver().update( 314 uri, new ContentValues(), null, null) > 0; 315 if (!successful) { 316 Log.w(TAG, "DataUsageFeedback increment failed"); 317 } 318 } else { 319 Log.w(TAG, "Invalid Data ID"); 320 } 321 322 startActivity(intent); 323 } 324 }; 325 326 final ExpandingEntryCardViewListener mExpandingEntryCardViewListener 327 = new ExpandingEntryCardViewListener() { 328 @Override 329 public void onCollapse(int heightDelta) { 330 mScroller.prepareForShrinkingScrollChild(heightDelta); 331 } 332 }; 333 334 /** 335 * Headless fragment used to handle account selection callbacks invoked from 336 * {@link DirectoryContactUtil}. 337 */ 338 public static class SelectAccountDialogFragmentListener extends Fragment 339 implements SelectAccountDialogFragment.Listener { 340 341 private QuickContactActivity mQuickContactActivity; 342 343 public SelectAccountDialogFragmentListener() {} 344 345 @Override 346 public void onAccountChosen(AccountWithDataSet account, Bundle extraArgs) { 347 DirectoryContactUtil.createCopy(mQuickContactActivity.mContactData.getContentValues(), 348 account, mQuickContactActivity); 349 } 350 351 @Override 352 public void onAccountSelectorCancelled() {} 353 354 /** 355 * Set the parent activity. Since rotation can cause this fragment to be used across 356 * more than one activity instance, we need to explicitly set this value instead 357 * of making this class non-static. 358 */ 359 public void setQuickContactActivity(QuickContactActivity quickContactActivity) { 360 mQuickContactActivity = quickContactActivity; 361 } 362 } 363 364 final MultiShrinkScrollerListener mMultiShrinkScrollerListener 365 = new MultiShrinkScrollerListener() { 366 @Override 367 public void onScrolledOffBottom() { 368 if (!mIsWaitingForOtherPieceOfExitAnimation) { 369 finish(); 370 return; 371 } 372 mIsWaitingForOtherPieceOfExitAnimation = false; 373 } 374 375 @Override 376 public void onEnterFullscreen() { 377 updateStatusBarColor(); 378 } 379 380 @Override 381 public void onExitFullscreen() { 382 updateStatusBarColor(); 383 } 384 385 @Override 386 public void onStartScrollOffBottom() { 387 // Remove the window shim now that we are starting an Activity exit animation. 388 final int duration = getResources().getInteger(android.R.integer.config_shortAnimTime); 389 final ObjectAnimator animator = ObjectAnimator.ofInt(mWindowScrim, "alpha", 0xFF, 0); 390 animator.addListener(mExitWindowShimAnimationListener); 391 animator.setDuration(duration).start(); 392 mIsWaitingForOtherPieceOfExitAnimation = true; 393 mIsExitAnimationInProgress = true; 394 } 395 }; 396 397 final AnimatorListener mExitWindowShimAnimationListener = new AnimatorListenerAdapter() { 398 @Override 399 public void onAnimationEnd(Animator animation) { 400 if (!mIsWaitingForOtherPieceOfExitAnimation) { 401 finish(); 402 return; 403 } 404 mIsWaitingForOtherPieceOfExitAnimation = false; 405 } 406 }; 407 408 409 /** 410 * Data items are compared to the same mimetype based off of three qualities: 411 * 1. Super primary 412 * 2. Primary 413 * 3. Times used 414 */ 415 private final Comparator<DataItem> mWithinMimeTypeDataItemComparator = 416 new Comparator<DataItem>() { 417 @Override 418 public int compare(DataItem lhs, DataItem rhs) { 419 if (!lhs.getMimeType().equals(rhs.getMimeType())) { 420 Log.wtf(TAG, "Comparing DataItems with different mimetypes lhs.getMimeType(): " + 421 lhs.getMimeType() + " rhs.getMimeType(): " + rhs.getMimeType()); 422 return 0; 423 } 424 425 if (lhs.isSuperPrimary()) { 426 return -1; 427 } else if (rhs.isSuperPrimary()) { 428 return 1; 429 } else if (lhs.isPrimary() && !rhs.isPrimary()) { 430 return -1; 431 } else if (!lhs.isPrimary() && rhs.isPrimary()) { 432 return 1; 433 } else { 434 final int lhsTimesUsed = 435 lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed(); 436 final int rhsTimesUsed = 437 rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed(); 438 439 return rhsTimesUsed - lhsTimesUsed; 440 } 441 } 442 }; 443 444 private final Comparator<List<DataItem>> mAmongstMimeTypeDataItemComparator = 445 new Comparator<List<DataItem>> () { 446 @Override 447 public int compare(List<DataItem> lhsList, List<DataItem> rhsList) { 448 DataItem lhs = lhsList.get(0); 449 DataItem rhs = rhsList.get(0); 450 final int lhsTimesUsed = lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed(); 451 final int rhsTimesUsed = rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed(); 452 final int timesUsedDifference = rhsTimesUsed - lhsTimesUsed; 453 if (timesUsedDifference != 0) { 454 return timesUsedDifference; 455 } 456 457 final long lhsLastTimeUsed = 458 lhs.getLastTimeUsed() == null ? 0 : lhs.getLastTimeUsed(); 459 final long rhsLastTimeUsed = 460 rhs.getLastTimeUsed() == null ? 0 : rhs.getLastTimeUsed(); 461 final long lastTimeUsedDifference = rhsLastTimeUsed - lhsLastTimeUsed; 462 if (lastTimeUsedDifference > 0) { 463 return 1; 464 } else if (lastTimeUsedDifference < 0) { 465 return -1; 466 } 467 468 // Times used and last time used are the same. Resort to statically defined. 469 final String lhsMimeType = lhs.getMimeType(); 470 final String rhsMimeType = rhs.getMimeType(); 471 for (String mimeType : LEADING_MIMETYPES) { 472 if (lhsMimeType.equals(mimeType)) { 473 return -1; 474 } else if (rhsMimeType.equals(mimeType)) { 475 return 1; 476 } 477 } 478 // Trailing types come last, so flip the returns 479 for (String mimeType : TRAILING_MIMETYPES) { 480 if (lhsMimeType.equals(mimeType)) { 481 return 1; 482 } else if (rhsMimeType.equals(mimeType)) { 483 return -1; 484 } 485 } 486 return 0; 487 } 488 }; 489 490 @Override 491 protected void onCreate(Bundle savedInstanceState) { 492 Trace.beginSection("onCreate()"); 493 super.onCreate(savedInstanceState); 494 495 getWindow().setStatusBarColor(Color.TRANSPARENT); 496 497 processIntent(getIntent()); 498 499 // Show QuickContact in front of soft input 500 getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, 501 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 502 503 setContentView(R.layout.quickcontact_activity); 504 505 mSmsComponent = PhoneCapabilityTester.getSmsComponent(this); 506 507 mContactCard = (ExpandingEntryCardView) findViewById(R.id.communication_card); 508 mNoContactDetailsCard = (ExpandingEntryCardView) findViewById(R.id.no_contact_data_card); 509 mRecentCard = (ExpandingEntryCardView) findViewById(R.id.recent_card); 510 mAboutCard = (ExpandingEntryCardView) findViewById(R.id.about_card); 511 mScroller = (MultiShrinkScroller) findViewById(R.id.multiscroller); 512 513 mNoContactDetailsCard.setOnClickListener(mEntryClickHandler); 514 mContactCard.setOnClickListener(mEntryClickHandler); 515 mContactCard.setTitle(getResources().getString(R.string.communication_card_title)); 516 mContactCard.setExpandButtonText( 517 getResources().getString(R.string.expanding_entry_card_view_see_all)); 518 519 mRecentCard.setOnClickListener(mEntryClickHandler); 520 mRecentCard.setTitle(getResources().getString(R.string.recent_card_title)); 521 522 mAboutCard.setOnClickListener(mEntryClickHandler); 523 524 mPhotoView = (ImageView) findViewById(R.id.photo); 525 mTransparentView = findViewById(R.id.transparent_view); 526 if (mScroller != null) { 527 mTransparentView.setOnClickListener(new OnClickListener() { 528 @Override 529 public void onClick(View v) { 530 mScroller.scrollOffBottom(); 531 } 532 }); 533 } 534 535 final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 536 setActionBar(toolbar); 537 getActionBar().setTitle(null); 538 // Put a TextView with a known resource id into the ActionBar. This allows us to easily 539 // find the correct TextView location & size later. 540 toolbar.addView(getLayoutInflater().inflate(R.layout.quickcontact_title_placeholder, null)); 541 542 mHasAlreadyBeenOpened = savedInstanceState != null; 543 544 mWindowScrim = new ColorDrawable(SCRIM_COLOR); 545 getWindow().setBackgroundDrawable(mWindowScrim); 546 if (!mHasAlreadyBeenOpened) { 547 final int duration = getResources().getInteger(android.R.integer.config_shortAnimTime); 548 ObjectAnimator.ofInt(mWindowScrim, "alpha", 0, 0xFF).setDuration(duration).start(); 549 } 550 551 mScroller.initialize(mMultiShrinkScrollerListener, mExtraMode == MODE_FULLY_EXPANDED); 552 // mScroller needs to perform asynchronous measurements after initalize(), therefore 553 // we can't mark this as GONE. 554 mScroller.setVisibility(View.INVISIBLE); 555 556 setHeaderNameText(R.string.missing_name); 557 558 mSelectAccountFragmentListener= (SelectAccountDialogFragmentListener) getFragmentManager() 559 .findFragmentByTag(FRAGMENT_TAG_SELECT_ACCOUNT); 560 if (mSelectAccountFragmentListener == null) { 561 mSelectAccountFragmentListener = new SelectAccountDialogFragmentListener(); 562 getFragmentManager().beginTransaction().add(0, mSelectAccountFragmentListener, 563 FRAGMENT_TAG_SELECT_ACCOUNT).commit(); 564 mSelectAccountFragmentListener.setRetainInstance(true); 565 } 566 mSelectAccountFragmentListener.setQuickContactActivity(this); 567 568 if (savedInstanceState != null) { 569 final int color = savedInstanceState.getInt(KEY_THEME_COLOR, 0); 570 SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false, 571 new Runnable() { 572 @Override 573 public void run() { 574 // Need to wait for the pre draw before setting the initial scroll 575 // value. Prior to pre draw all scroll values are invalid. 576 if (mHasAlreadyBeenOpened) { 577 mScroller.setVisibility(View.VISIBLE); 578 mScroller.setScroll(mScroller.getScrollNeededToBeFullScreen()); 579 } 580 // Need to wait for pre draw for setting the theme color. Setting the 581 // header tint before the MultiShrinkScroller has been measured will 582 // cause incorrect tinting calculations. 583 if (color != 0) { 584 setThemeColor(MaterialColorMapUtils.calculateSecondaryColor(color)); 585 } 586 } 587 }); 588 } 589 590 Trace.endSection(); 591 } 592 593 protected void onActivityResult(int requestCode, int resultCode, 594 Intent data) { 595 if (requestCode == REQUEST_CODE_CONTACT_EDITOR_ACTIVITY && 596 resultCode == ContactDeletionInteraction.RESULT_CODE_DELETED) { 597 // The contact that we were showing has been deleted. 598 finish(); 599 } 600 } 601 602 @Override 603 protected void onNewIntent(Intent intent) { 604 super.onNewIntent(intent); 605 mHasAlreadyBeenOpened = true; 606 mHasComputedThemeColor = false; 607 processIntent(intent); 608 } 609 610 @Override 611 public void onSaveInstanceState(Bundle savedInstanceState) { 612 super.onSaveInstanceState(savedInstanceState); 613 if (mColorFilter != null) { 614 savedInstanceState.putInt(KEY_THEME_COLOR, mColorFilter.getColor()); 615 } 616 } 617 618 private void processIntent(Intent intent) { 619 Uri lookupUri = intent.getData(); 620 621 // Check to see whether it comes from the old version. 622 if (lookupUri != null && LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) { 623 final long rawContactId = ContentUris.parseId(lookupUri); 624 lookupUri = RawContacts.getContactLookupUri(getContentResolver(), 625 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); 626 } 627 mExtraMode = getIntent().getIntExtra(QuickContact.EXTRA_MODE, 628 QuickContact.MODE_LARGE); 629 final Uri oldLookupUri = mLookupUri; 630 631 mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri"); 632 mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES); 633 if (oldLookupUri == null) { 634 mContactLoader = (ContactLoader) getLoaderManager().initLoader( 635 LOADER_CONTACT_ID, null, mLoaderContactCallbacks); 636 } else if (oldLookupUri != mLookupUri) { 637 // After copying a directory contact, the contact URI changes. Therefore, 638 // we need to restart the loader and reload the new contact. 639 mContactLoader = (ContactLoader) getLoaderManager().restartLoader( 640 LOADER_CONTACT_ID, null, mLoaderContactCallbacks); 641 for (int interactionLoaderId : mRecentLoaderIds) { 642 getLoaderManager().destroyLoader(interactionLoaderId); 643 } 644 } 645 646 NfcHandler.register(this, mLookupUri); 647 } 648 649 private void runEntranceAnimation() { 650 if (mHasAlreadyBeenOpened) { 651 return; 652 } 653 mHasAlreadyBeenOpened = true; 654 mScroller.scrollUpForEntranceAnimation(mExtraMode != MODE_FULLY_EXPANDED); 655 } 656 657 /** Assign this string to the view if it is not empty. */ 658 private void setHeaderNameText(int resId) { 659 if (mScroller != null) { 660 mScroller.setTitle(getText(resId) == null ? null : getText(resId).toString()); 661 } 662 } 663 664 /** Assign this string to the view if it is not empty. */ 665 private void setHeaderNameText(String value) { 666 if (!TextUtils.isEmpty(value)) { 667 if (mScroller != null) { 668 mScroller.setTitle(value); 669 } 670 } 671 } 672 673 /** 674 * Check if the given MIME-type appears in the list of excluded MIME-types 675 * that the most-recent caller requested. 676 */ 677 private boolean isMimeExcluded(String mimeType) { 678 if (mExcludeMimes == null) return false; 679 for (String excludedMime : mExcludeMimes) { 680 if (TextUtils.equals(excludedMime, mimeType)) { 681 return true; 682 } 683 } 684 return false; 685 } 686 687 /** 688 * Handle the result from the ContactLoader 689 */ 690 private void bindContactData(final Contact data) { 691 Trace.beginSection("bindContactData"); 692 mContactData = data; 693 invalidateOptionsMenu(); 694 695 Trace.endSection(); 696 Trace.beginSection("Set display photo & name"); 697 698 mPhotoSetter.setupContactPhoto(data, mPhotoView); 699 extractAndApplyTintFromPhotoViewAsynchronously(); 700 analyzeWhitenessOfPhotoAsynchronously(); 701 setHeaderNameText(data.getDisplayName()); 702 703 Trace.endSection(); 704 705 mEntriesAndActionsTask = new AsyncTask<Void, Void, 706 Pair<List<List<DataItem>>, Map<String, List<DataItem>>>>() { 707 708 @Override 709 protected Pair<List<List<DataItem>>, Map<String, List<DataItem>>> doInBackground( 710 Void... params) { 711 return generateDataModelFromContact(data); 712 } 713 714 @Override 715 protected void onPostExecute(Pair<List<List<DataItem>>, 716 Map<String, List<DataItem>>> dataItemsPair) { 717 super.onPostExecute(dataItemsPair); 718 mDataItemsList = dataItemsPair.first; 719 mDataItemsMap = dataItemsPair.second; 720 // Check that original AsyncTask parameters are still valid and the activity 721 // is still running before binding to UI. A new intent could invalidate 722 // the results, for example. 723 if (data == mContactData && !isCancelled()) { 724 bindDataToCards(); 725 showActivity(); 726 } 727 } 728 }; 729 mEntriesAndActionsTask.execute(); 730 } 731 732 private void bindDataToCards() { 733 startInteractionLoaders(); 734 populateContactAndAboutCard(); 735 } 736 737 private void startInteractionLoaders() { 738 final List<DataItem> phoneDataItems = mDataItemsMap.get(Phone.CONTENT_ITEM_TYPE); 739 String[] phoneNumbers = null; 740 if (phoneDataItems != null) { 741 phoneNumbers = new String[phoneDataItems.size()]; 742 for (int i = 0; i < phoneDataItems.size(); ++i) { 743 phoneNumbers[i] = ((PhoneDataItem) phoneDataItems.get(i)).getNumber(); 744 } 745 } 746 final Bundle phonesExtraBundle = new Bundle(); 747 phonesExtraBundle.putStringArray(KEY_LOADER_EXTRA_PHONES, phoneNumbers); 748 749 Trace.beginSection("start sms loader"); 750 getLoaderManager().initLoader( 751 LOADER_SMS_ID, 752 phonesExtraBundle, 753 mLoaderInteractionsCallbacks); 754 Trace.endSection(); 755 756 Trace.beginSection("start call log loader"); 757 getLoaderManager().initLoader( 758 LOADER_CALL_LOG_ID, 759 phonesExtraBundle, 760 mLoaderInteractionsCallbacks); 761 Trace.endSection(); 762 763 764 Trace.beginSection("start calendar loader"); 765 final List<DataItem> emailDataItems = mDataItemsMap.get(Email.CONTENT_ITEM_TYPE); 766 String[] emailAddresses = null; 767 if (emailDataItems != null) { 768 emailAddresses = new String[emailDataItems.size()]; 769 for (int i = 0; i < emailDataItems.size(); ++i) { 770 emailAddresses[i] = ((EmailDataItem) emailDataItems.get(i)).getAddress(); 771 } 772 } 773 final Bundle emailsExtraBundle = new Bundle(); 774 emailsExtraBundle.putStringArray(KEY_LOADER_EXTRA_EMAILS, emailAddresses); 775 getLoaderManager().initLoader( 776 LOADER_CALENDAR_ID, 777 emailsExtraBundle, 778 mLoaderInteractionsCallbacks); 779 Trace.endSection(); 780 } 781 782 private void showActivity() { 783 if (mScroller != null) { 784 mScroller.setVisibility(View.VISIBLE); 785 SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false, 786 new Runnable() { 787 @Override 788 public void run() { 789 runEntranceAnimation(); 790 } 791 }); 792 } 793 } 794 795 private void populateContactAndAboutCard() { 796 Trace.beginSection("bind contact card"); 797 798 final List<List<Entry>> contactCardEntries = new ArrayList<>(); 799 final List<List<Entry>> aboutCardEntries = new ArrayList<>(); 800 801 for (int i = 0; i < mDataItemsList.size(); ++i) { 802 final List<DataItem> dataItemsByMimeType = mDataItemsList.get(i); 803 final DataItem topDataItem = dataItemsByMimeType.get(0); 804 if (ABOUT_CARD_MIMETYPES.contains(topDataItem.getMimeType())) { 805 List<Entry> aboutEntries = dataItemsToEntries(mDataItemsList.get(i)); 806 if (aboutEntries.size() > 0) { 807 aboutCardEntries.add(aboutEntries); 808 } 809 } else { 810 List<Entry> contactEntries = dataItemsToEntries(mDataItemsList.get(i)); 811 if (contactEntries.size() > 0) { 812 contactCardEntries.add(contactEntries); 813 } 814 } 815 } 816 817 if (contactCardEntries.size() > 0) { 818 mContactCard.initialize(contactCardEntries, 819 /* numInitialVisibleEntries = */ MIN_NUM_CONTACT_ENTRIES_SHOWN, 820 /* isExpanded = */ false, 821 mExpandingEntryCardViewListener); 822 mContactCard.setVisibility(View.VISIBLE); 823 } else { 824 mContactCard.setVisibility(View.GONE); 825 } 826 Trace.endSection(); 827 828 Trace.beginSection("bind about card"); 829 mAboutCard.initialize(aboutCardEntries, 830 /* numInitialVisibleEntries = */ 1, 831 /* isExpanded = */ true, 832 mExpandingEntryCardViewListener); 833 834 if (contactCardEntries.size() == 0 && aboutCardEntries.size() == 0) { 835 initializeNoContactDetailCard(); 836 } else { 837 mNoContactDetailsCard.setVisibility(View.GONE); 838 } 839 840 Trace.endSection(); 841 } 842 843 /** 844 * Create a card that shows "Add email" and "Add phone number" entries in grey. 845 */ 846 private void initializeNoContactDetailCard() { 847 final Drawable phoneIcon = getResources().getDrawable( 848 R.drawable.ic_phone_24dp).mutate(); 849 final Entry phonePromptEntry = new Entry(CARD_ENTRY_ID_EDIT_CONTACT, 850 phoneIcon, getString(R.string.quickcontact_add_phone_number), 851 /* subHeader = */ null, /* text = */ null, 852 getEditContactIntent(), /* isEditable = */ false); 853 854 final Drawable emailIcon = getResources().getDrawable( 855 R.drawable.ic_email_24dp).mutate(); 856 final Entry emailPromptEntry = new Entry(CARD_ENTRY_ID_EDIT_CONTACT, 857 emailIcon, getString(R.string.quickcontact_add_email), /* subHeader = */ null, 858 /* text = */ null, getEditContactIntent(), /* isEditable = */ false); 859 860 final List<List<Entry>> promptEntries = new ArrayList<>(); 861 promptEntries.add(new ArrayList<Entry>(1)); 862 promptEntries.add(new ArrayList<Entry>(1)); 863 promptEntries.get(0).add(phonePromptEntry); 864 promptEntries.get(1).add(emailPromptEntry); 865 866 final int subHeaderTextColor = getResources().getColor( 867 R.color.quickcontact_entry_sub_header_text_color); 868 final PorterDuffColorFilter greyColorFilter = 869 new PorterDuffColorFilter(subHeaderTextColor, PorterDuff.Mode.SRC_ATOP); 870 mNoContactDetailsCard.initialize(promptEntries, 2, /* isExpanded = */ false, 871 mExpandingEntryCardViewListener); 872 mNoContactDetailsCard.setVisibility(View.VISIBLE); 873 mNoContactDetailsCard.setEntryHeaderColor(subHeaderTextColor); 874 mNoContactDetailsCard.setColorAndFilter(subHeaderTextColor, greyColorFilter); 875 } 876 877 /** 878 * Builds the {@link DataItem}s Map out of the Contact. 879 * @param data The contact to build the data from. 880 * @return A pair containing a list of data items sorted within mimetype and sorted 881 * amongst mimetype. The map goes from mimetype string to the sorted list of data items within 882 * mimetype 883 */ 884 private Pair<List<List<DataItem>>, Map<String, List<DataItem>>> generateDataModelFromContact( 885 Contact data) { 886 Trace.beginSection("Build data items map"); 887 888 final Map<String, List<DataItem>> dataItemsMap = new HashMap<>(); 889 890 final ResolveCache cache = ResolveCache.getInstance(this); 891 for (RawContact rawContact : data.getRawContacts()) { 892 for (DataItem dataItem : rawContact.getDataItems()) { 893 dataItem.setRawContactId(rawContact.getId()); 894 895 final String mimeType = dataItem.getMimeType(); 896 if (mimeType == null) continue; 897 898 final AccountType accountType = rawContact.getAccountType(this); 899 final DataKind dataKind = AccountTypeManager.getInstance(this) 900 .getKindOrFallback(accountType, mimeType); 901 if (dataKind == null) continue; 902 903 dataItem.setDataKind(dataKind); 904 905 final boolean hasData = !TextUtils.isEmpty(dataItem.buildDataString(this, 906 dataKind)); 907 908 if (isMimeExcluded(mimeType) || !hasData) continue; 909 910 List<DataItem> dataItemListByType = dataItemsMap.get(mimeType); 911 if (dataItemListByType == null) { 912 dataItemListByType = new ArrayList<>(); 913 dataItemsMap.put(mimeType, dataItemListByType); 914 } 915 dataItemListByType.add(dataItem); 916 } 917 } 918 Trace.endSection(); 919 920 Trace.beginSection("sort within mimetypes"); 921 /* 922 * Sorting is a multi part step. The end result is to a have a sorted list of the most 923 * used data items, one per mimetype. Then, within each mimetype, the list of data items 924 * for that type is also sorted, based off of {super primary, primary, times used} in that 925 * order. 926 */ 927 final List<List<DataItem>> dataItemsList = new ArrayList<>(); 928 for (List<DataItem> mimeTypeDataItems : dataItemsMap.values()) { 929 // Remove duplicate data items 930 Collapser.collapseList(mimeTypeDataItems, this); 931 // Sort within mimetype 932 Collections.sort(mimeTypeDataItems, mWithinMimeTypeDataItemComparator); 933 // Add to the list of data item lists 934 dataItemsList.add(mimeTypeDataItems); 935 } 936 Trace.endSection(); 937 938 Trace.beginSection("sort amongst mimetypes"); 939 // Sort amongst mimetypes to bubble up the top data items for the contact card 940 Collections.sort(dataItemsList, mAmongstMimeTypeDataItemComparator); 941 Trace.endSection(); 942 943 return new Pair<>(dataItemsList, dataItemsMap); 944 } 945 946 /** 947 * Converts a {@link DataItem} into an {@link ExpandingEntryCardView.Entry} for display. 948 * If the {@link ExpandingEntryCardView.Entry} has no visual elements, null is returned. 949 * @param dataItem The {@link DataItem} to convert. 950 * @return The {@link ExpandingEntryCardView.Entry}, or null if no visual elements are present. 951 */ 952 private Entry dataItemToEntry(DataItem dataItem) { 953 Drawable icon = null; 954 String header = null; 955 String subHeader = null; 956 Drawable subHeaderIcon = null; 957 String text = null; 958 Drawable textIcon = null; 959 Intent intent = null; 960 final boolean isEditable = false; 961 962 DataKind kind = dataItem.getDataKind(); 963 964 if (dataItem instanceof ImDataItem) { 965 final ImDataItem im = (ImDataItem) dataItem; 966 intent = ContactsUtils.buildImIntent(this, im).first; 967 header = getResources().getString(R.string.header_im_entry); 968 final boolean isEmail = im.isCreatedFromEmail(); 969 final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : im.getProtocol(); 970 subHeader = Im.getProtocolLabel(getResources(), protocol, 971 im.getCustomProtocol()).toString(); 972 } else if (dataItem instanceof OrganizationDataItem) { 973 final OrganizationDataItem organization = (OrganizationDataItem) dataItem; 974 header = getResources().getString(R.string.header_organization_entry); 975 subHeader = organization.getCompany(); 976 text = organization.getTitle(); 977 } else if (dataItem instanceof NicknameDataItem) { 978 final NicknameDataItem nickname = (NicknameDataItem) dataItem; 979 // Build nickname entries 980 final boolean isNameRawContact = 981 (mContactData.getNameRawContactId() == dataItem.getRawContactId()); 982 983 final boolean duplicatesTitle = 984 isNameRawContact 985 && mContactData.getDisplayNameSource() == DisplayNameSources.NICKNAME; 986 987 if (!duplicatesTitle) { 988 header = getResources().getString(R.string.header_nickname_entry); 989 subHeader = nickname.getName(); 990 } 991 } else if (dataItem instanceof NoteDataItem) { 992 final NoteDataItem note = (NoteDataItem) dataItem; 993 header = getResources().getString(R.string.header_note_entry); 994 subHeader = note.getNote(); 995 } else if (dataItem instanceof WebsiteDataItem) { 996 final WebsiteDataItem website = (WebsiteDataItem) dataItem; 997 header = getResources().getString(R.string.header_website_entry); 998 subHeader = website.getUrl(); 999 try { 1000 final WebAddress webAddress = new WebAddress(website.buildDataString(this, kind)); 1001 intent = new Intent(Intent.ACTION_VIEW, Uri.parse(webAddress.toString())); 1002 } catch (final ParseException e) { 1003 Log.e(TAG, "Couldn't parse website: " + website.buildDataString(this, kind)); 1004 } 1005 } else if (dataItem instanceof EventDataItem) { 1006 final EventDataItem event = (EventDataItem) dataItem; 1007 final String dataString = event.buildDataString(this, kind); 1008 final Calendar cal = DateUtils.parseDate(dataString, false); 1009 if (cal != null) { 1010 final Date nextAnniversary = 1011 DateUtils.getNextAnnualDate(cal); 1012 final Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon(); 1013 builder.appendPath("time"); 1014 ContentUris.appendId(builder, nextAnniversary.getTime()); 1015 intent = new Intent(Intent.ACTION_VIEW).setData(builder.build()); 1016 } 1017 header = getResources().getString(R.string.header_event_entry); 1018 if (event.hasKindTypeColumn(kind)) { 1019 subHeader = getResources().getString(Event.getTypeResource( 1020 event.getKindTypeColumn(kind))); 1021 } 1022 text = DateUtils.formatDate(this, dataString); 1023 } else if (dataItem instanceof RelationDataItem) { 1024 final RelationDataItem relation = (RelationDataItem) dataItem; 1025 final String dataString = relation.buildDataString(this, kind); 1026 if (!TextUtils.isEmpty(dataString)) { 1027 intent = new Intent(Intent.ACTION_SEARCH); 1028 intent.putExtra(SearchManager.QUERY, dataString); 1029 intent.setType(Contacts.CONTENT_TYPE); 1030 } 1031 header = getResources().getString(R.string.header_relation_entry); 1032 subHeader = relation.getName(); 1033 if (relation.hasKindTypeColumn(kind)) { 1034 text = Relation.getTypeLabel(getResources(), relation.getKindTypeColumn(kind), 1035 relation.getLabel()).toString(); 1036 } 1037 } else if (dataItem instanceof PhoneDataItem) { 1038 final PhoneDataItem phone = (PhoneDataItem) dataItem; 1039 if (!TextUtils.isEmpty(phone.getNumber())) { 1040 header = phone.buildDataString(this, kind); 1041 if (phone.hasKindTypeColumn(kind)) { 1042 text = Phone.getTypeLabel(getResources(), phone.getKindTypeColumn(kind), 1043 phone.getLabel()).toString(); 1044 } 1045 icon = getResources().getDrawable(R.drawable.ic_phone_24dp); 1046 if (PhoneCapabilityTester.isPhone(this)) { 1047 intent = CallUtil.getCallIntent(phone.getNumber()); 1048 } 1049 } 1050 } else if (dataItem instanceof EmailDataItem) { 1051 final EmailDataItem email = (EmailDataItem) dataItem; 1052 final String address = email.getData(); 1053 if (!TextUtils.isEmpty(address)) { 1054 final Uri mailUri = Uri.fromParts(CallUtil.SCHEME_MAILTO, address, null); 1055 intent = new Intent(Intent.ACTION_SENDTO, mailUri); 1056 header = email.getAddress(); 1057 if (email.hasKindTypeColumn(kind)) { 1058 text = Email.getTypeLabel(getResources(), email.getKindTypeColumn(kind), 1059 email.getLabel()).toString(); 1060 } 1061 icon = getResources().getDrawable(R.drawable.ic_email_24dp); 1062 } 1063 } else if (dataItem instanceof StructuredPostalDataItem) { 1064 StructuredPostalDataItem postal = (StructuredPostalDataItem) dataItem; 1065 final String postalAddress = postal.getFormattedAddress(); 1066 if (!TextUtils.isEmpty(postalAddress)) { 1067 intent = StructuredPostalUtils.getViewPostalAddressIntent(postalAddress); 1068 header = postal.getFormattedAddress(); 1069 if (postal.hasKindTypeColumn(kind)) { 1070 text = StructuredPostal.getTypeLabel(getResources(), 1071 postal.getKindTypeColumn(kind), postal.getLabel()).toString(); 1072 } 1073 icon = getResources().getDrawable(R.drawable.ic_place_24dp); 1074 } 1075 } else if (dataItem instanceof SipAddressDataItem) { 1076 if (PhoneCapabilityTester.isSipPhone(this)) { 1077 final SipAddressDataItem sip = (SipAddressDataItem) dataItem; 1078 final String address = sip.getSipAddress(); 1079 if (!TextUtils.isEmpty(address)) { 1080 final Uri callUri = Uri.fromParts(CallUtil.SCHEME_SIP, address, null); 1081 intent = CallUtil.getCallIntent(callUri); 1082 // Note that this item will get a SIP-specific variant 1083 // of the "call phone" icon, rather than the standard 1084 // app icon for the Phone app (which we show for 1085 // regular phone numbers.) That's because the phone 1086 // app explicitly specifies an android:icon attribute 1087 // for the SIP-related intent-filters in its manifest. 1088 } 1089 icon = ResolveCache.getInstance(this).getIcon(sip.getMimeType(), intent); 1090 // Call mutate to create a new Drawable.ConstantState for color filtering 1091 if (icon != null) { 1092 icon.mutate(); 1093 } 1094 } 1095 } else if (dataItem instanceof StructuredNameDataItem) { 1096 final String givenName = ((StructuredNameDataItem) dataItem).getGivenName(); 1097 if (!TextUtils.isEmpty(givenName)) { 1098 mAboutCard.setTitle(getResources().getString(R.string.about_card_title) + 1099 " " + givenName); 1100 } else { 1101 mAboutCard.setTitle(getResources().getString(R.string.about_card_title)); 1102 } 1103 } else { 1104 // Custom DataItem 1105 header = dataItem.buildDataStringForDisplay(this, kind); 1106 text = kind.typeColumn; 1107 intent = new Intent(Intent.ACTION_VIEW); 1108 final Uri uri = ContentUris.withAppendedId(Data.CONTENT_URI, dataItem.getId()); 1109 intent.setDataAndType(uri, dataItem.getMimeType()); 1110 1111 if (intent != null) { 1112 final String mimetype = intent.getType(); 1113 1114 // Attempt to use known icons for known 3p types. Otherwise default to ResolveCache 1115 switch (mimetype) { 1116 case MIMETYPE_GPLUS_PROFILE: 1117 if (INTENT_DATA_GPLUS_PROFILE_ADD_TO_CIRCLE.equals( 1118 intent.getDataString())) { 1119 icon = getResources().getDrawable( 1120 R.drawable.ic_add_to_circles_black_24); 1121 } else { 1122 icon = getResources().getDrawable(R.drawable.ic_google_plus_24dp); 1123 } 1124 break; 1125 case MIMETYPE_HANGOUTS: 1126 if (INTENT_DATA_HANGOUTS_VIDEO.equals(intent.getDataString())) { 1127 icon = getResources().getDrawable(R.drawable.ic_hangout_video_24dp); 1128 } else { 1129 icon = getResources().getDrawable(R.drawable.ic_hangout_24dp); 1130 } 1131 break; 1132 default: 1133 icon = ResolveCache.getInstance(this).getIcon( 1134 dataItem.getMimeType(), intent); 1135 // Call mutate to create a new Drawable.ConstantState for color filtering 1136 if (icon != null) { 1137 icon.mutate(); 1138 } 1139 } 1140 } 1141 } 1142 1143 if (intent != null) { 1144 // Do not set the intent is there are no resolves 1145 if (!PhoneCapabilityTester.isIntentRegistered(this, intent)) { 1146 intent = null; 1147 } 1148 } 1149 1150 // If the Entry has no visual elements, return null 1151 if (icon == null && TextUtils.isEmpty(header) && TextUtils.isEmpty(subHeader) && 1152 subHeaderIcon == null && TextUtils.isEmpty(text) && textIcon == null) { 1153 return null; 1154 } 1155 1156 final int dataId = dataItem.getId() > Integer.MAX_VALUE ? 1157 -1 : (int) dataItem.getId(); 1158 1159 return new Entry(dataId, icon, header, subHeader, subHeaderIcon, text, textIcon, 1160 intent, isEditable); 1161 } 1162 1163 private List<Entry> dataItemsToEntries(List<DataItem> dataItems) { 1164 final List<Entry> entries = new ArrayList<>(); 1165 for (DataItem dataItem : dataItems) { 1166 final Entry entry = dataItemToEntry(dataItem); 1167 if (entry != null) { 1168 entries.add(entry); 1169 } 1170 // TODO merge secondary intents 1171 if (dataItem instanceof PhoneDataItem) { 1172 final PhoneDataItem phone = (PhoneDataItem) dataItem; 1173 Intent smsIntent = null; 1174 if (mSmsComponent != null) { 1175 smsIntent = new Intent(Intent.ACTION_SENDTO, 1176 Uri.fromParts(CallUtil.SCHEME_SMSTO, phone.getNumber(), null)); 1177 smsIntent.setComponent(mSmsComponent); 1178 } 1179 final int dataId = dataItem.getId() > Integer.MAX_VALUE ? 1180 -1 : (int) dataItem.getId(); 1181 entries.add(new Entry(dataId, 1182 getResources().getDrawable(R.drawable.ic_message_24dp), 1183 getResources().getString(R.string.send_message), 1184 /* subHeader = */ null, 1185 /* text = */ phone.buildDataString(this, 1186 dataItem.getDataKind()), 1187 smsIntent, 1188 /* isEditable = */ false)); 1189 } 1190 } 1191 return entries; 1192 } 1193 1194 /** 1195 * Asynchronously extract the most vibrant color from the PhotoView. Once extracted, 1196 * apply this tint to {@link MultiShrinkScroller}. This operation takes about 20-30ms 1197 * on a Nexus 5. 1198 */ 1199 private void extractAndApplyTintFromPhotoViewAsynchronously() { 1200 if (mScroller == null) { 1201 return; 1202 } 1203 final Drawable imageViewDrawable = mPhotoView.getDrawable(); 1204 new AsyncTask<Void, Void, MaterialPalette>() { 1205 @Override 1206 protected MaterialPalette doInBackground(Void... params) { 1207 1208 if (imageViewDrawable instanceof BitmapDrawable) { 1209 final Bitmap bitmap = ((BitmapDrawable) imageViewDrawable).getBitmap(); 1210 final int primaryColor = colorFromBitmap(bitmap); 1211 if (primaryColor != 0) { 1212 return MaterialColorMapUtils.calculatePrimaryAndSecondaryColor( 1213 primaryColor); 1214 } 1215 } 1216 if (imageViewDrawable instanceof LetterTileDrawable) { 1217 final int primaryColor = ((LetterTileDrawable) imageViewDrawable).getColor(); 1218 return MaterialColorMapUtils.calculateSecondaryColor(primaryColor); 1219 } 1220 return MaterialColorMapUtils.calculatePrimaryAndSecondaryColor( 1221 getResources().getColor(R.color.quickcontact_default_photo_tint_color)); 1222 } 1223 1224 @Override 1225 protected void onPostExecute(MaterialPalette palette) { 1226 super.onPostExecute(palette); 1227 if (mHasComputedThemeColor) { 1228 // If we had previously computed a theme color from the contact photo, 1229 // then do not update the theme color. Changing the theme color several 1230 // seconds after QC has started, as a result of an updated/upgraded photo, 1231 // is a jarring experience. On the other hand, changing the theme color after 1232 // a rotation or onNewIntent() is perfectly fine. 1233 return; 1234 } 1235 // Check that the Photo has not changed. If it has changed, the new tint 1236 // color needs to be extracted 1237 if (imageViewDrawable == mPhotoView.getDrawable()) { 1238 mHasComputedThemeColor = true; 1239 setThemeColor(palette); 1240 } 1241 } 1242 }.execute(); 1243 } 1244 1245 /** 1246 * Examine how many white pixels are in the bitmap in order to determine whether or not 1247 * we need gradient overlays on top of the image. 1248 */ 1249 private void analyzeWhitenessOfPhotoAsynchronously() { 1250 final Drawable imageViewDrawable = mPhotoView.getDrawable(); 1251 new AsyncTask<Void, Void, Boolean>() { 1252 @Override 1253 protected Boolean doInBackground(Void... params) { 1254 if (imageViewDrawable instanceof BitmapDrawable) { 1255 final Bitmap bitmap = ((BitmapDrawable) imageViewDrawable).getBitmap(); 1256 return WhitenessUtils.isBitmapWhiteAtTopOrBottom(bitmap); 1257 } 1258 return !(imageViewDrawable instanceof LetterTileDrawable); 1259 } 1260 1261 @Override 1262 protected void onPostExecute(Boolean isWhite) { 1263 super.onPostExecute(isWhite); 1264 mScroller.setUseGradient(isWhite); 1265 } 1266 }.execute(); 1267 } 1268 1269 private void setThemeColor(MaterialPalette palette) { 1270 // If the color is invalid, use the predefined default 1271 final int primaryColor = palette.mPrimaryColor; 1272 mScroller.setHeaderTintColor(primaryColor); 1273 mStatusBarColor = palette.mSecondaryColor; 1274 updateStatusBarColor(); 1275 1276 mColorFilter = 1277 new PorterDuffColorFilter(primaryColor, PorterDuff.Mode.SRC_ATOP); 1278 mContactCard.setColorAndFilter(primaryColor, mColorFilter); 1279 mRecentCard.setColorAndFilter(primaryColor, mColorFilter); 1280 mAboutCard.setColorAndFilter(primaryColor, mColorFilter); 1281 } 1282 1283 private void updateStatusBarColor() { 1284 if (mScroller == null) { 1285 return; 1286 } 1287 final int desiredStatusBarColor; 1288 // Only use a custom status bar color if QuickContacts touches the top of the viewport. 1289 if (mScroller.getScrollNeededToBeFullScreen() <= 0) { 1290 desiredStatusBarColor = mStatusBarColor; 1291 } else { 1292 desiredStatusBarColor = Color.TRANSPARENT; 1293 } 1294 // Animate to the new color. 1295 if (desiredStatusBarColor != getWindow().getStatusBarColor()) { 1296 final ObjectAnimator animation = ObjectAnimator.ofInt(getWindow(), "statusBarColor", 1297 getWindow().getStatusBarColor(), desiredStatusBarColor); 1298 animation.setDuration(ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION); 1299 animation.setEvaluator(new ArgbEvaluator()); 1300 animation.start(); 1301 } 1302 } 1303 1304 private int colorFromBitmap(Bitmap bitmap) { 1305 // Author of Palette recommends using 24 colors when analyzing profile photos. 1306 final int NUMBER_OF_PALETTE_COLORS = 24; 1307 final Palette palette = Palette.generate(bitmap, NUMBER_OF_PALETTE_COLORS); 1308 if (palette != null && palette.getVibrantSwatch() != null) { 1309 return palette.getVibrantSwatch().getRgb(); 1310 } 1311 return 0; 1312 } 1313 1314 private List<Entry> contactInteractionsToEntries(List<ContactInteraction> interactions) { 1315 final List<Entry> entries = new ArrayList<>(); 1316 for (ContactInteraction interaction : interactions) { 1317 entries.add(new Entry(/* id = */ -1, 1318 interaction.getIcon(this), 1319 interaction.getViewHeader(this), 1320 interaction.getViewBody(this), 1321 interaction.getBodyIcon(this), 1322 interaction.getViewFooter(this), 1323 interaction.getFooterIcon(this), 1324 interaction.getIntent(), 1325 /* isEditable = */ false)); 1326 } 1327 return entries; 1328 } 1329 1330 private final LoaderCallbacks<Contact> mLoaderContactCallbacks = 1331 new LoaderCallbacks<Contact>() { 1332 @Override 1333 public void onLoaderReset(Loader<Contact> loader) { 1334 } 1335 1336 @Override 1337 public void onLoadFinished(Loader<Contact> loader, Contact data) { 1338 Trace.beginSection("onLoadFinished()"); 1339 1340 if (isFinishing()) { 1341 return; 1342 } 1343 if (data.isError()) { 1344 // This shouldn't ever happen, so throw an exception. The {@link ContactLoader} 1345 // should log the actual exception. 1346 throw new IllegalStateException("Failed to load contact", data.getException()); 1347 } 1348 if (data.isNotFound()) { 1349 if (mHasAlreadyBeenOpened) { 1350 finish(); 1351 } else { 1352 Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri()); 1353 Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage, 1354 Toast.LENGTH_LONG).show(); 1355 } 1356 return; 1357 } 1358 1359 bindContactData(data); 1360 1361 Trace.endSection(); 1362 } 1363 1364 @Override 1365 public Loader<Contact> onCreateLoader(int id, Bundle args) { 1366 if (mLookupUri == null) { 1367 Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early"); 1368 } 1369 // Load all contact data. We need loadGroupMetaData=true to determine whether the 1370 // contact is invisible. If it is, we need to display an "Add to Contacts" MenuItem. 1371 return new ContactLoader(getApplicationContext(), mLookupUri, 1372 true /*loadGroupMetaData*/, false /*loadInvitableAccountTypes*/, 1373 true /*postViewNotification*/, true /*computeFormattedPhoneNumber*/); 1374 } 1375 }; 1376 1377 @Override 1378 public void onBackPressed() { 1379 if (mScroller != null) { 1380 if (!mIsExitAnimationInProgress) { 1381 mScroller.scrollOffBottom(); 1382 } 1383 } else { 1384 super.onBackPressed(); 1385 } 1386 } 1387 1388 @Override 1389 public void finish() { 1390 super.finish(); 1391 1392 // override transitions to skip the standard window animations 1393 overridePendingTransition(0, 0); 1394 } 1395 1396 private final LoaderCallbacks<List<ContactInteraction>> mLoaderInteractionsCallbacks = 1397 new LoaderCallbacks<List<ContactInteraction>>() { 1398 1399 @Override 1400 public Loader<List<ContactInteraction>> onCreateLoader(int id, Bundle args) { 1401 Log.v(TAG, "onCreateLoader"); 1402 Loader<List<ContactInteraction>> loader = null; 1403 switch (id) { 1404 case LOADER_SMS_ID: 1405 Log.v(TAG, "LOADER_SMS_ID"); 1406 loader = new SmsInteractionsLoader( 1407 QuickContactActivity.this, 1408 args.getStringArray(KEY_LOADER_EXTRA_PHONES), 1409 MAX_SMS_RETRIEVE); 1410 break; 1411 case LOADER_CALENDAR_ID: 1412 Log.v(TAG, "LOADER_CALENDAR_ID"); 1413 final String[] emailsArray = args.getStringArray(KEY_LOADER_EXTRA_EMAILS); 1414 List<String> emailsList = null; 1415 if (emailsArray != null) { 1416 emailsList = Arrays.asList(args.getStringArray(KEY_LOADER_EXTRA_EMAILS)); 1417 } 1418 loader = new CalendarInteractionsLoader( 1419 QuickContactActivity.this, 1420 emailsList, 1421 MAX_FUTURE_CALENDAR_RETRIEVE, 1422 MAX_PAST_CALENDAR_RETRIEVE, 1423 FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR, 1424 PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR); 1425 break; 1426 case LOADER_CALL_LOG_ID: 1427 Log.v(TAG, "LOADER_CALL_LOG_ID"); 1428 loader = new CallLogInteractionsLoader( 1429 QuickContactActivity.this, 1430 args.getStringArray(KEY_LOADER_EXTRA_PHONES), 1431 MAX_CALL_LOG_RETRIEVE); 1432 } 1433 return loader; 1434 } 1435 1436 @Override 1437 public void onLoadFinished(Loader<List<ContactInteraction>> loader, 1438 List<ContactInteraction> data) { 1439 if (mRecentLoaderResults == null) { 1440 mRecentLoaderResults = new HashMap<Integer, List<ContactInteraction>>(); 1441 } 1442 Log.v(TAG, "onLoadFinished ~ loader.getId() " + loader.getId() + " data.size() " + 1443 data.size()); 1444 mRecentLoaderResults.put(loader.getId(), data); 1445 1446 if (isAllRecentDataLoaded()) { 1447 bindRecentData(); 1448 } 1449 } 1450 1451 @Override 1452 public void onLoaderReset(Loader<List<ContactInteraction>> loader) { 1453 mRecentLoaderResults.remove(loader.getId()); 1454 } 1455 1456 }; 1457 1458 private boolean isAllRecentDataLoaded() { 1459 return mRecentLoaderResults.size() == mRecentLoaderIds.length; 1460 } 1461 1462 private void bindRecentData() { 1463 final List<ContactInteraction> allInteractions = new ArrayList<>(); 1464 for (List<ContactInteraction> loaderInteractions : mRecentLoaderResults.values()) { 1465 allInteractions.addAll(loaderInteractions); 1466 } 1467 1468 // Sort the interactions by most recent 1469 Collections.sort(allInteractions, new Comparator<ContactInteraction>() { 1470 @Override 1471 public int compare(ContactInteraction a, ContactInteraction b) { 1472 return a.getInteractionDate() >= b.getInteractionDate() ? -1 : 1; 1473 } 1474 }); 1475 1476 1477 List<List<Entry>> interactionsWrapper = new ArrayList<>(); 1478 interactionsWrapper.add(contactInteractionsToEntries(allInteractions)); 1479 if (allInteractions.size() > 0) { 1480 mRecentCard.initialize(interactionsWrapper, 1481 /* numInitialVisibleEntries = */ MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN, 1482 /* isExpanded = */ false, mExpandingEntryCardViewListener); 1483 mRecentCard.setVisibility(View.VISIBLE); 1484 } 1485 1486 // About card is initialized along with the contact card, but since it appears after 1487 // the recent card in the UI, we hold off until making it visible until the recent card 1488 // is also ready to avoid stuttering. 1489 if (mAboutCard.shouldShow()) { 1490 mAboutCard.setVisibility(View.VISIBLE); 1491 } else { 1492 mAboutCard.setVisibility(View.GONE); 1493 } 1494 } 1495 1496 @Override 1497 protected void onStop() { 1498 super.onStop(); 1499 1500 if (mEntriesAndActionsTask != null) { 1501 // Once the activity is stopped, we will no longer want to bind mEntriesAndActionsTask's 1502 // results on the UI thread. In some circumstances Activities are killed without 1503 // onStop() being called. This is not a problem, because in these circumstances 1504 // the entire process will be killed. 1505 mEntriesAndActionsTask.cancel(/* mayInterruptIfRunning = */ false); 1506 } 1507 } 1508 1509 /** 1510 * Returns true if it is possible to edit the current contact. 1511 */ 1512 private boolean isContactEditable() { 1513 return mContactData != null && !mContactData.isDirectoryEntry(); 1514 } 1515 1516 private Intent getEditContactIntent() { 1517 final Intent intent = new Intent(Intent.ACTION_EDIT, mLookupUri); 1518 mContactLoader.cacheResult(); 1519 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 1520 return intent; 1521 } 1522 1523 private void editContact() { 1524 startActivityForResult(getEditContactIntent(), REQUEST_CODE_CONTACT_EDITOR_ACTIVITY); 1525 } 1526 1527 private void toggleStar(MenuItem starredMenuItem) { 1528 // Make sure there is a contact 1529 if (mLookupUri != null) { 1530 // Read the current starred value from the UI instead of using the last 1531 // loaded state. This allows rapid tapping without writing the same 1532 // value several times 1533 final boolean isStarred = starredMenuItem.isChecked(); 1534 1535 // To improve responsiveness, swap out the picture (and tag) in the UI already 1536 ContactDetailDisplayUtils.configureStarredMenuItem(starredMenuItem, 1537 mContactData.isDirectoryEntry(), mContactData.isUserProfile(), 1538 !isStarred); 1539 1540 // Now perform the real save 1541 final Intent intent = ContactSaveService.createSetStarredIntent( 1542 QuickContactActivity.this, mLookupUri, !isStarred); 1543 startService(intent); 1544 } 1545 } 1546 1547 /** 1548 * Calls into the contacts provider to get a pre-authorized version of the given URI. 1549 */ 1550 private Uri getPreAuthorizedUri(Uri uri) { 1551 final Bundle uriBundle = new Bundle(); 1552 uriBundle.putParcelable(ContactsContract.Authorization.KEY_URI_TO_AUTHORIZE, uri); 1553 final Bundle authResponse = getContentResolver().call( 1554 ContactsContract.AUTHORITY_URI, 1555 ContactsContract.Authorization.AUTHORIZATION_METHOD, 1556 null, 1557 uriBundle); 1558 if (authResponse != null) { 1559 return (Uri) authResponse.getParcelable( 1560 ContactsContract.Authorization.KEY_AUTHORIZED_URI); 1561 } else { 1562 return uri; 1563 } 1564 } 1565 1566 private void shareContact() { 1567 final String lookupKey = mContactData.getLookupKey(); 1568 Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey); 1569 if (mContactData.isUserProfile()) { 1570 // User is sharing the profile. We don't want to force the receiver to have 1571 // the highly-privileged READ_PROFILE permission, so we need to request a 1572 // pre-authorized URI from the provider. 1573 shareUri = getPreAuthorizedUri(shareUri); 1574 } 1575 1576 final Intent intent = new Intent(Intent.ACTION_SEND); 1577 intent.setType(Contacts.CONTENT_VCARD_TYPE); 1578 intent.putExtra(Intent.EXTRA_STREAM, shareUri); 1579 1580 // Launch chooser to share contact via 1581 final CharSequence chooseTitle = getText(R.string.share_via); 1582 final Intent chooseIntent = Intent.createChooser(intent, chooseTitle); 1583 1584 try { 1585 this.startActivity(chooseIntent); 1586 } catch (final ActivityNotFoundException ex) { 1587 Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show(); 1588 } 1589 } 1590 1591 /** 1592 * Creates a launcher shortcut with the current contact. 1593 */ 1594 private void createLauncherShortcutWithContact() { 1595 final ShortcutIntentBuilder builder = new ShortcutIntentBuilder(this, 1596 new OnShortcutIntentCreatedListener() { 1597 1598 @Override 1599 public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) { 1600 // Broadcast the shortcutIntent to the launcher to create a 1601 // shortcut to this contact 1602 shortcutIntent.setAction(ACTION_INSTALL_SHORTCUT); 1603 QuickContactActivity.this.sendBroadcast(shortcutIntent); 1604 1605 // Send a toast to give feedback to the user that a shortcut to this 1606 // contact was added to the launcher. 1607 Toast.makeText(QuickContactActivity.this, 1608 R.string.createContactShortcutSuccessful, 1609 Toast.LENGTH_SHORT).show(); 1610 } 1611 1612 }); 1613 builder.createContactShortcutIntent(mLookupUri); 1614 } 1615 1616 @Override 1617 public boolean onCreateOptionsMenu(Menu menu) { 1618 final MenuInflater inflater = getMenuInflater(); 1619 inflater.inflate(R.menu.quickcontact, menu); 1620 return true; 1621 } 1622 1623 @Override 1624 public boolean onPrepareOptionsMenu(Menu menu) { 1625 if (mContactData != null) { 1626 final MenuItem starredMenuItem = menu.findItem(R.id.menu_star); 1627 ContactDetailDisplayUtils.configureStarredMenuItem(starredMenuItem, 1628 mContactData.isDirectoryEntry(), mContactData.isUserProfile(), 1629 mContactData.getStarred()); 1630 // Configure edit MenuItem 1631 final MenuItem editMenuItem = menu.findItem(R.id.menu_edit); 1632 editMenuItem.setVisible(true); 1633 if (DirectoryContactUtil.isDirectoryContact(mContactData) || InvisibleContactUtil 1634 .isInvisibleAndAddable(mContactData, this)) { 1635 editMenuItem.setIcon(R.drawable.ic_person_add_tinted_24dp); 1636 editMenuItem.setTitle(R.string.menu_add_contact); 1637 } else if (isContactEditable()) { 1638 editMenuItem.setIcon(R.drawable.ic_create_24dp); 1639 editMenuItem.setTitle(R.string.menu_editContact); 1640 } else { 1641 editMenuItem.setVisible(false); 1642 } 1643 return true; 1644 } 1645 return false; 1646 } 1647 1648 @Override 1649 public boolean onOptionsItemSelected(MenuItem item) { 1650 switch (item.getItemId()) { 1651 case R.id.menu_star: 1652 toggleStar(item); 1653 return true; 1654 case R.id.menu_edit: 1655 if (DirectoryContactUtil.isDirectoryContact(mContactData)) { 1656 DirectoryContactUtil.addToMyContacts(mContactData, this, getFragmentManager(), 1657 mSelectAccountFragmentListener); 1658 } else if (InvisibleContactUtil.isInvisibleAndAddable(mContactData, this)) { 1659 InvisibleContactUtil.addToDefaultGroup(mContactData, this); 1660 } else if (isContactEditable()) { 1661 editContact(); 1662 } 1663 return true; 1664 case R.id.menu_share: 1665 shareContact(); 1666 return true; 1667 case R.id.menu_create_contact_shortcut: 1668 createLauncherShortcutWithContact(); 1669 return true; 1670 default: 1671 return super.onOptionsItemSelected(item); 1672 } 1673 } 1674} 1675