QuickContactActivity.java revision 02477604d7cc803b6a7de10b45ed176c4e70f312
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.ArgbEvaluator; 20import android.animation.ObjectAnimator; 21import android.app.Activity; 22import android.app.Fragment; 23import android.app.LoaderManager.LoaderCallbacks; 24import android.content.ActivityNotFoundException; 25import android.content.ContentUris; 26import android.content.Intent; 27import android.content.Loader; 28import android.content.pm.PackageManager; 29import android.graphics.Bitmap; 30import android.graphics.Color; 31import android.graphics.drawable.BitmapDrawable; 32import android.graphics.drawable.ColorDrawable; 33import android.graphics.drawable.Drawable; 34import android.graphics.PorterDuff; 35import android.graphics.PorterDuffColorFilter; 36import android.net.Uri; 37import android.os.AsyncTask; 38import android.os.Bundle; 39import android.os.Trace; 40import android.provider.ContactsContract; 41import android.provider.ContactsContract.CommonDataKinds.Email; 42import android.provider.ContactsContract.CommonDataKinds.Phone; 43import android.provider.ContactsContract.CommonDataKinds.SipAddress; 44import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 45import android.provider.ContactsContract.CommonDataKinds.Website; 46import android.provider.ContactsContract.Contacts; 47import android.provider.ContactsContract.QuickContact; 48import android.provider.ContactsContract.RawContacts; 49import android.support.v7.graphics.Palette; 50import android.text.TextUtils; 51import android.util.Log; 52import android.view.Menu; 53import android.view.MenuItem; 54import android.view.MenuInflater; 55import android.view.View; 56import android.view.View.OnClickListener; 57import android.view.WindowManager; 58import android.widget.ImageView; 59import android.widget.Toast; 60import android.widget.Toolbar; 61 62import com.android.contacts.ContactSaveService; 63import com.android.contacts.ContactsActivity; 64import com.android.contacts.common.Collapser; 65import com.android.contacts.R; 66import com.android.contacts.common.editor.SelectAccountDialogFragment; 67import com.android.contacts.common.lettertiles.LetterTileDrawable; 68import com.android.contacts.common.list.ShortcutIntentBuilder; 69import com.android.contacts.common.list.ShortcutIntentBuilder.OnShortcutIntentCreatedListener; 70import com.android.contacts.common.model.AccountTypeManager; 71import com.android.contacts.common.model.Contact; 72import com.android.contacts.common.model.ContactLoader; 73import com.android.contacts.common.model.RawContact; 74import com.android.contacts.common.model.account.AccountType; 75import com.android.contacts.common.model.account.AccountWithDataSet; 76import com.android.contacts.common.model.dataitem.DataItem; 77import com.android.contacts.common.model.dataitem.DataKind; 78import com.android.contacts.common.model.dataitem.EmailDataItem; 79import com.android.contacts.common.model.dataitem.ImDataItem; 80import com.android.contacts.common.model.dataitem.PhoneDataItem; 81import com.android.contacts.common.util.DataStatus; 82import com.android.contacts.detail.ContactDetailDisplayUtils; 83import com.android.contacts.interactions.ContactDeletionInteraction; 84import com.android.contacts.interactions.ContactInteraction; 85import com.android.contacts.interactions.SmsInteractionsLoader; 86import com.android.contacts.quickcontact.ExpandingEntryCardView.Entry; 87import com.android.contacts.util.ImageViewDrawableSetter; 88import com.android.contacts.util.SchedulingUtils; 89import com.android.contacts.widget.MultiShrinkScroller; 90import com.android.contacts.widget.MultiShrinkScroller.MultiShrinkScrollerListener; 91 92import com.google.common.base.Preconditions; 93import com.google.common.collect.Lists; 94 95import java.util.ArrayList; 96import java.util.Collections; 97import java.util.Comparator; 98import java.util.HashMap; 99import java.util.HashSet; 100import java.util.List; 101import java.util.Map; 102import java.util.Set; 103 104/** 105 * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads 106 * data asynchronously, and then shows a popup with details centered around 107 * {@link Intent#getSourceBounds()}. 108 */ 109public class QuickContactActivity extends ContactsActivity { 110 111 /** 112 * QuickContacts immediately takes up the full screen. All possible information is shown. 113 * This value for {@link android.provider.ContactsContract.QuickContact#EXTRA_MODE} 114 * should only be used by the Contacts app. 115 */ 116 public static final int MODE_FULLY_EXPANDED = 4; 117 118 private static final String TAG = "QuickContact"; 119 120 private static final int ANIMATION_SLIDE_OPEN_DURATION = 250; 121 private static final int ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION = 75; 122 private static final int REQUEST_CODE_CONTACT_EDITOR_ACTIVITY = 1; 123 private static final float SYSTEM_BAR_BRIGHTNESS_FACTOR = 0.7f; 124 private static final int SHIM_COLOR = Color.argb(0x7F, 0, 0, 0); 125 126 /** This is the Intent action to install a shortcut in the launcher. */ 127 private static final String ACTION_INSTALL_SHORTCUT = 128 "com.android.launcher.action.INSTALL_SHORTCUT"; 129 130 @SuppressWarnings("deprecation") 131 private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY; 132 133 private Uri mLookupUri; 134 private String[] mExcludeMimes; 135 private int mExtraMode; 136 private int mStatusBarColor; 137 private boolean mHasAlreadyBeenOpened; 138 139 private ImageView mPhotoView; 140 private ExpandingEntryCardView mCommunicationCard; 141 private ExpandingEntryCardView mRecentCard; 142 private MultiShrinkScroller mScroller; 143 private SelectAccountDialogFragmentListener mSelectAccountFragmentListener; 144 private AsyncTask<Void, Void, Void> mEntriesAndActionsTask; 145 146 private static final int MIN_NUM_COMMUNICATION_ENTRIES_SHOWN = 3; 147 private static final int MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN = 3; 148 149 private Contact mContactData; 150 private ContactLoader mContactLoader; 151 152 private PorterDuffColorFilter mColorFilter; 153 List<Drawable> mDrawablesToTint; 154 155 private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter(); 156 157 /** 158 * Keeps the default action per mimetype. Empty if no default actions are set 159 */ 160 private HashMap<String, Action> mDefaultsMap = new HashMap<String, Action>(); 161 162 /** 163 * Set of {@link Action} that are associated with the aggregate currently 164 * displayed by this dialog, represented as a map from {@link String} 165 * MIME-type to a list of {@link Action}. 166 */ 167 private ActionMultiMap mActions = new ActionMultiMap(); 168 169 /** 170 * {@link #LEADING_MIMETYPES} and {@link #TRAILING_MIMETYPES} are used to sort MIME-types. 171 * 172 * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog, 173 * in the order specified here.</p> 174 * 175 * <p>The ones in {@link #TRAILING_MIMETYPES} appear in the end of the dialog, in the order 176 * specified here.</p> 177 * 178 * <p>The rest go between them, in the order in the array.</p> 179 */ 180 private static final List<String> LEADING_MIMETYPES = Lists.newArrayList( 181 Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE); 182 183 /** See {@link #LEADING_MIMETYPES}. */ 184 private static final List<String> TRAILING_MIMETYPES = Lists.newArrayList( 185 StructuredPostal.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE); 186 187 /** Id for the background contact loader */ 188 private static final int LOADER_CONTACT_ID = 0; 189 190 /** Id for the background Sms Loader */ 191 private static final int LOADER_SMS_ID = 1; 192 private static final String KEY_LOADER_EXTRA_SMS_PHONES = 193 QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_SMS_PHONES"; 194 private static final int MAX_SMS_RETRIEVE = 3; 195 196 private static final int[] mRecentLoaderIds = new int[LOADER_SMS_ID]; 197 private Map<Integer, List<ContactInteraction>> mRecentLoaderResults; 198 199 private static final String FRAGMENT_TAG_SELECT_ACCOUNT = "select_account_fragment"; 200 201 final OnClickListener mEntryClickHandler = new OnClickListener() { 202 @Override 203 public void onClick(View v) { 204 Log.i(TAG, "mEntryClickHandler onClick"); 205 Object intent = v.getTag(); 206 if (intent == null || !(intent instanceof Intent)) { 207 return; 208 } 209 startActivity((Intent) intent); 210 } 211 }; 212 213 /** 214 * Headless fragment used to handle account selection callbacks invoked from 215 * {@link DirectoryContactUtil}. 216 */ 217 public static class SelectAccountDialogFragmentListener extends Fragment 218 implements SelectAccountDialogFragment.Listener { 219 220 private QuickContactActivity mQuickContactActivity; 221 222 public SelectAccountDialogFragmentListener() {} 223 224 @Override 225 public void onAccountChosen(AccountWithDataSet account, Bundle extraArgs) { 226 DirectoryContactUtil.createCopy(mQuickContactActivity.mContactData.getContentValues(), 227 account, mQuickContactActivity); 228 } 229 230 @Override 231 public void onAccountSelectorCancelled() {} 232 233 /** 234 * Set the parent activity. Since rotation can cause this fragment to be used across 235 * more than one activity instance, we need to explicitly set this value instead 236 * of making this class non-static. 237 */ 238 public void setQuickContactActivity(QuickContactActivity quickContactActivity) { 239 mQuickContactActivity = quickContactActivity; 240 } 241 } 242 243 final MultiShrinkScrollerListener mMultiShrinkScrollerListener 244 = new MultiShrinkScrollerListener() { 245 @Override 246 public void onScrolledOffBottom() { 247 onBackPressed(); 248 } 249 250 @Override 251 public void onEnterFullscreen() { 252 updateStatusBarColor(); 253 } 254 255 @Override 256 public void onExitFullscreen() { 257 updateStatusBarColor(); 258 } 259 }; 260 261 @Override 262 protected void onCreate(Bundle savedInstanceState) { 263 Trace.beginSection("onCreate()"); 264 super.onCreate(savedInstanceState); 265 266 getWindow().setStatusBarColor(Color.TRANSPARENT); 267 // Since we can't disable Window animations from the Launcher, we can minimize the 268 // silliness of the animation by setting the navigation bar transparent. 269 getWindow().setNavigationBarColor(Color.TRANSPARENT); 270 271 processIntent(getIntent()); 272 273 // Show QuickContact in front of soft input 274 getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, 275 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 276 277 setContentView(R.layout.quickcontact_activity); 278 279 mCommunicationCard = (ExpandingEntryCardView) findViewById(R.id.communication_card); 280 mRecentCard = (ExpandingEntryCardView) findViewById(R.id.recent_card); 281 mScroller = (MultiShrinkScroller) findViewById(R.id.multiscroller); 282 283 mCommunicationCard.setOnClickListener(mEntryClickHandler); 284 mCommunicationCard.setTitle(getResources().getString(R.string.communication_card_title)); 285 mCommunicationCard.setExpandButtonText( 286 getResources().getString(R.string.expanding_entry_card_view_see_all)); 287 288 mRecentCard.setOnClickListener(mEntryClickHandler); 289 mRecentCard.setTitle(getResources().getString(R.string.recent_card_title)); 290 291 mPhotoView = (ImageView) findViewById(R.id.photo); 292 293 final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 294 setActionBar(toolbar); 295 setHeaderNameText(R.string.missing_name); 296 297 mHasAlreadyBeenOpened = savedInstanceState != null; 298 299 final ColorDrawable windowShim = new ColorDrawable(SHIM_COLOR); 300 getWindow().setBackgroundDrawable(windowShim); 301 if (!mHasAlreadyBeenOpened) { 302 final int duration = getResources().getInteger(android.R.integer.config_shortAnimTime); 303 ObjectAnimator.ofInt(windowShim, "alpha", 0, 0xFF).setDuration(duration).start(); 304 } 305 306 if (mScroller != null) { 307 mScroller.initialize(mMultiShrinkScrollerListener); 308 if (mHasAlreadyBeenOpened) { 309 mScroller.setVisibility(View.VISIBLE); 310 mScroller.setScroll(mScroller.getScrollNeededToBeFullScreen()); 311 } else { 312 mScroller.setVisibility(View.GONE); 313 } 314 } 315 316 mDrawablesToTint = new ArrayList<>(); 317 mSelectAccountFragmentListener= (SelectAccountDialogFragmentListener) getFragmentManager() 318 .findFragmentByTag(FRAGMENT_TAG_SELECT_ACCOUNT); 319 if (mSelectAccountFragmentListener == null) { 320 mSelectAccountFragmentListener = new SelectAccountDialogFragmentListener(); 321 getFragmentManager().beginTransaction().add(0, mSelectAccountFragmentListener, 322 FRAGMENT_TAG_SELECT_ACCOUNT).commit(); 323 mSelectAccountFragmentListener.setRetainInstance(true); 324 } 325 mSelectAccountFragmentListener.setQuickContactActivity(this); 326 327 Trace.endSection(); 328 } 329 330 protected void onActivityResult(int requestCode, int resultCode, 331 Intent data) { 332 if (requestCode == REQUEST_CODE_CONTACT_EDITOR_ACTIVITY && 333 resultCode == ContactDeletionInteraction.RESULT_CODE_DELETED) { 334 // The contact that we were showing has been deleted. 335 finish(); 336 } 337 } 338 339 @Override 340 protected void onNewIntent(Intent intent) { 341 super.onNewIntent(intent); 342 mHasAlreadyBeenOpened = true; 343 processIntent(intent); 344 } 345 346 private void processIntent(Intent intent) { 347 Uri lookupUri = intent.getData(); 348 349 // Check to see whether it comes from the old version. 350 if (lookupUri != null && LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) { 351 final long rawContactId = ContentUris.parseId(lookupUri); 352 lookupUri = RawContacts.getContactLookupUri(getContentResolver(), 353 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); 354 } 355 mExtraMode = getIntent().getIntExtra(QuickContact.EXTRA_MODE, 356 QuickContact.MODE_LARGE); 357 final Uri oldLookupUri = mLookupUri; 358 359 mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri"); 360 mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES); 361 if (oldLookupUri == null) { 362 mContactLoader = (ContactLoader) getLoaderManager().initLoader( 363 LOADER_CONTACT_ID, null, mLoaderContactCallbacks); 364 } else if (oldLookupUri != mLookupUri) { 365 // After copying a directory contact, the contact URI changes. Therefore, 366 // we need to restart the loader and reload the new contact. 367 mContactLoader = (ContactLoader) getLoaderManager().restartLoader( 368 LOADER_CONTACT_ID, null, mLoaderContactCallbacks); 369 } 370 } 371 372 private void runEntranceAnimation() { 373 if (mHasAlreadyBeenOpened) { 374 return; 375 } 376 mHasAlreadyBeenOpened = true; 377 final int bottomScroll = mScroller.getScrollUntilOffBottom() - 1; 378 final ObjectAnimator scrollAnimation 379 = ObjectAnimator.ofInt(mScroller, "scroll", -bottomScroll, 380 mExtraMode != MODE_FULLY_EXPANDED ? 0 : mScroller.getScrollNeededToBeFullScreen()); 381 scrollAnimation.setDuration(ANIMATION_SLIDE_OPEN_DURATION); 382 scrollAnimation.start(); 383 } 384 385 /** Assign this string to the view if it is not empty. */ 386 private void setHeaderNameText(int resId) { 387 getActionBar().setTitle(getText(resId)); 388 } 389 390 /** Assign this string to the view if it is not empty. */ 391 private void setHeaderNameText(CharSequence value) { 392 if (!TextUtils.isEmpty(value)) { 393 getActionBar().setTitle(value); 394 } 395 } 396 397 /** 398 * Check if the given MIME-type appears in the list of excluded MIME-types 399 * that the most-recent caller requested. 400 */ 401 private boolean isMimeExcluded(String mimeType) { 402 if (mExcludeMimes == null) return false; 403 for (String excludedMime : mExcludeMimes) { 404 if (TextUtils.equals(excludedMime, mimeType)) { 405 return true; 406 } 407 } 408 return false; 409 } 410 411 /** 412 * Handle the result from the ContactLoader 413 */ 414 private void bindContactData(final Contact data) { 415 Trace.beginSection("bindContactData"); 416 mContactData = data; 417 invalidateOptionsMenu(); 418 419 mDefaultsMap.clear(); 420 421 Trace.endSection(); 422 Trace.beginSection("Set display photo & name"); 423 424 mPhotoSetter.setupContactPhoto(data, mPhotoView); 425 extractAndApplyTintFromPhotoViewAsynchronously(); 426 setHeaderNameText(data.getDisplayName()); 427 428 Trace.endSection(); 429 430 final List<String> sortedActionMimeTypes = Lists.newArrayList(); 431 // Maintain a list of phone numbers to pass into SmsInteractionsLoader 432 final List<String> phoneNumbers = Lists.newArrayList(); 433 // List of Entry that makes up the ExpandingEntryCardView 434 final List<Entry> entries = Lists.newArrayList(); 435 436 mEntriesAndActionsTask = new AsyncTask<Void, Void, Void>() { 437 @Override 438 protected Void doInBackground(Void... params) { 439 computeEntriesAndActions(data, phoneNumbers, sortedActionMimeTypes, entries); 440 return null; 441 } 442 443 @Override 444 protected void onPostExecute(Void aVoid) { 445 super.onPostExecute(aVoid); 446 // Check that original AsyncTask parameters are still valid and the activity 447 // is still running before binding to UI. A new intent could invalidate 448 // the results, for example. 449 if (data == mContactData && !isCancelled()) { 450 bindEntriesAndActions(entries, phoneNumbers, sortedActionMimeTypes); 451 showActivity(); 452 } 453 } 454 }; 455 mEntriesAndActionsTask.execute(); 456 } 457 458 private void bindEntriesAndActions(List<Entry> entries, 459 List<String> phoneNumbers, 460 List<String> sortedActionMimeTypes) { 461 Trace.beginSection("start sms loader"); 462 463 Bundle smsExtraBundle = new Bundle(); 464 smsExtraBundle.putStringArray(KEY_LOADER_EXTRA_SMS_PHONES, 465 phoneNumbers.toArray(new String[phoneNumbers.size()])); 466 getLoaderManager().initLoader( 467 LOADER_SMS_ID, 468 smsExtraBundle, 469 mLoaderInteractionsCallbacks); 470 471 Trace.endSection(); 472 Trace.beginSection("bind communicate card"); 473 474 if (entries.size() > 0) { 475 mCommunicationCard.initialize(entries, 476 /* numInitialVisibleEntries = */ MIN_NUM_COMMUNICATION_ENTRIES_SHOWN, 477 /* isExpanded = */ false, 478 /* themeColor = */ 0); 479 } 480 481 final boolean hasData = !sortedActionMimeTypes.isEmpty(); 482 mCommunicationCard.setVisibility(hasData ? View.VISIBLE : View.GONE); 483 484 Trace.endSection(); 485 } 486 487 private void showActivity() { 488 if (mScroller != null) { 489 mScroller.setVisibility(View.VISIBLE); 490 SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false, 491 new Runnable() { 492 @Override 493 public void run() { 494 runEntranceAnimation(); 495 } 496 }); 497 } 498 } 499 500 private void computeEntriesAndActions(Contact data, List<String> phoneNumbers, 501 List<String> sortedActionMimeTypes, List<Entry> entries) { 502 Trace.beginSection("inflate entries and actions"); 503 504 final ResolveCache cache = ResolveCache.getInstance(this); 505 for (RawContact rawContact : data.getRawContacts()) { 506 for (DataItem dataItem : rawContact.getDataItems()) { 507 final String mimeType = dataItem.getMimeType(); 508 final AccountType accountType = rawContact.getAccountType(this); 509 final DataKind dataKind = AccountTypeManager.getInstance(this) 510 .getKindOrFallback(accountType, mimeType); 511 512 if (dataItem instanceof PhoneDataItem) { 513 phoneNumbers.add(((PhoneDataItem) dataItem).getNormalizedNumber()); 514 } 515 516 // Skip this data item if MIME-type excluded 517 if (isMimeExcluded(mimeType)) continue; 518 519 final long dataId = dataItem.getId(); 520 final boolean isPrimary = dataItem.isPrimary(); 521 final boolean isSuperPrimary = dataItem.isSuperPrimary(); 522 523 if (dataKind != null) { 524 // Build an action for this data entry, find a mapping to a UI 525 // element, build its summary from the cursor, and collect it 526 // along with all others of this MIME-type. 527 final Action action = new DataAction(getApplicationContext(), 528 dataItem, dataKind); 529 final boolean wasAdded = considerAdd(action, cache, isSuperPrimary); 530 if (wasAdded) { 531 // Remember the default 532 if (isSuperPrimary || (isPrimary && (mDefaultsMap.get(mimeType) == null))) { 533 mDefaultsMap.put(mimeType, action); 534 } 535 } 536 } 537 538 // Handle Email rows with presence data as Im entry 539 final DataStatus status = data.getStatuses().get(dataId); 540 if (status != null && dataItem instanceof EmailDataItem) { 541 final EmailDataItem email = (EmailDataItem) dataItem; 542 final ImDataItem im = ImDataItem.createFromEmail(email); 543 if (dataKind != null) { 544 final DataAction action = new DataAction(getApplicationContext(), 545 im, dataKind); 546 action.setPresence(status.getPresence()); 547 considerAdd(action, cache, isSuperPrimary); 548 } 549 } 550 } 551 } 552 553 Trace.endSection(); 554 Trace.beginSection("collapsing action list"); 555 556 // Collapse Action Lists (remove e.g. duplicate e-mail addresses from different sources) 557 for (List<Action> actionChildren : mActions.values()) { 558 Collapser.collapseList(actionChildren); 559 } 560 561 Trace.endSection(); 562 Trace.beginSection("sort mimetypes"); 563 564 // All the mime-types to add. 565 final Set<String> containedTypes = new HashSet<String>(mActions.keySet()); 566 // First, add LEADING_MIMETYPES, which are most common. 567 for (String mimeType : LEADING_MIMETYPES) { 568 if (containedTypes.contains(mimeType)) { 569 sortedActionMimeTypes.add(mimeType); 570 containedTypes.remove(mimeType); 571 entries.addAll(actionsToEntries(mActions.get(mimeType))); 572 } 573 } 574 575 // Add all the remaining ones that are not TRAILING 576 for (String mimeType : containedTypes.toArray(new String[containedTypes.size()])) { 577 if (!TRAILING_MIMETYPES.contains(mimeType)) { 578 sortedActionMimeTypes.add(mimeType); 579 containedTypes.remove(mimeType); 580 entries.addAll(actionsToEntries(mActions.get(mimeType))); 581 } 582 } 583 584 // Then, add TRAILING_MIMETYPES, which are least common. 585 for (String mimeType : TRAILING_MIMETYPES) { 586 if (containedTypes.contains(mimeType)) { 587 containedTypes.remove(mimeType); 588 sortedActionMimeTypes.add(mimeType); 589 entries.addAll(actionsToEntries(mActions.get(mimeType))); 590 } 591 } 592 593 Trace.endSection(); 594 } 595 596 /** 597 * Asynchronously extract the most vibrant color from the PhotoView. Once extracted, 598 * apply this tint to {@link MultiShrinkScroller}. This operation takes about 20-30ms 599 * on a Nexus 5. 600 */ 601 private void extractAndApplyTintFromPhotoViewAsynchronously() { 602 if (mScroller == null) { 603 return; 604 } 605 final Drawable imageViewDrawable = mPhotoView.getDrawable(); 606 new AsyncTask<Void, Void, Integer>() { 607 @Override 608 protected Integer doInBackground(Void... params) { 609 if (imageViewDrawable instanceof BitmapDrawable) { 610 final Bitmap bitmap = ((BitmapDrawable) imageViewDrawable).getBitmap(); 611 return colorFromBitmap(bitmap); 612 } 613 if (imageViewDrawable instanceof LetterTileDrawable) { 614 // LetterTileDrawable doesn't normally draw unless it is visible. Therefore, 615 // we need to directly ask it for its color via getColor(). We could directly 616 // return this color. However, in the future Palette#generate() may incorporate 617 // saturation boosting. So I want to use Palette#generate() for the sake of 618 // consistency. 619 final LetterTileDrawable tileDrawable = (LetterTileDrawable) imageViewDrawable; 620 final int PALETTE_BITMAP_SIZE = 1; 621 final Bitmap bitmap = Bitmap.createBitmap(PALETTE_BITMAP_SIZE, 622 PALETTE_BITMAP_SIZE, Bitmap.Config.ARGB_8888); 623 // If Palette can not extract a primary color, our UX person says we are better 624 // off using the LetterTileDrawable's non vibrant color than falling back 625 // to the app's default color. 626 final int color = colorFromBitmap(bitmap); 627 if (color == 0) { 628 return tileDrawable.getColor(); 629 } else { 630 return color; 631 } 632 } 633 return 0; 634 } 635 636 @Override 637 protected void onPostExecute(Integer color) { 638 super.onPostExecute(color); 639 mColorFilter = new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP); 640 // Make sure the color is valid. Also check that the Photo has not changed. If it 641 // has changed, the new tint color needs to be extracted 642 if (color != 0 && imageViewDrawable == mPhotoView.getDrawable()) { 643 // TODO: animate from the previous tint. 644 mScroller.setHeaderTintColor(color); 645 646 // Create a darker version of the actionbar color. HSV is device dependent 647 // and not perceptually-linear. Therefore, we can't say mStatusBarColor is 648 // 70% as bright as the action bar color. We can only say: it is a bit darker. 649 final float hsvComponents[] = new float[3]; 650 Color.colorToHSV(color, hsvComponents); 651 hsvComponents[2] *= SYSTEM_BAR_BRIGHTNESS_FACTOR; 652 mStatusBarColor = Color.HSVToColor(hsvComponents); 653 654 updateStatusBarColor(); 655 for (Drawable drawable : mDrawablesToTint) { 656 applyThemeColorIfAvailable(drawable); 657 } 658 mDrawablesToTint.clear(); 659 } 660 } 661 }.execute(); 662 } 663 664 private void updateStatusBarColor() { 665 if (mScroller == null) { 666 return; 667 } 668 final int desiredStatusBarColor; 669 // Only use a custom status bar color if QuickContacts touches the top of the viewport. 670 if (mScroller.getScrollNeededToBeFullScreen() <= 0) { 671 desiredStatusBarColor = mStatusBarColor; 672 } else { 673 desiredStatusBarColor = Color.TRANSPARENT; 674 } 675 // Animate to the new color. 676 if (desiredStatusBarColor != getWindow().getStatusBarColor()) { 677 final ObjectAnimator animation = ObjectAnimator.ofInt(getWindow(), "statusBarColor", 678 getWindow().getStatusBarColor(), desiredStatusBarColor); 679 animation.setDuration(ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION); 680 animation.setEvaluator(new ArgbEvaluator()); 681 animation.start(); 682 } 683 } 684 685 private int colorFromBitmap(Bitmap bitmap) { 686 // Author of Palette recommends using 24 colors when analyzing profile photos. 687 final int NUMBER_OF_PALETTE_COLORS = 24; 688 final Palette palette = Palette.generate(bitmap, NUMBER_OF_PALETTE_COLORS); 689 if (palette != null && palette.getVibrantColor() != null) { 690 return palette.getVibrantColor().getRgb(); 691 } 692 return 0; 693 } 694 695 /** 696 * Consider adding the given {@link Action}, which will only happen if 697 * {@link PackageManager} finds an application to handle 698 * {@link Action#getIntent()}. 699 * @param action the action to handle 700 * @param resolveCache cache of applications that can handle actions 701 * @param front indicates whether to add the action to the front of the list 702 * @return true if action has been added 703 */ 704 private boolean considerAdd(Action action, ResolveCache resolveCache, boolean front) { 705 if (resolveCache.hasResolve(action)) { 706 mActions.put(action.getMimeType(), action, front); 707 return true; 708 } 709 return false; 710 } 711 712 /** 713 * Converts a list of Action into a list of Entry 714 * @param actions The list of Action to convert 715 * @return The converted list of Entry 716 */ 717 private List<Entry> actionsToEntries(List<Action> actions) { 718 List<Entry> entries = new ArrayList<>(); 719 for (Action action : actions) { 720 String header = null; 721 String body = null; 722 String footer = null; 723 Drawable icon = null; 724 switch (action.getMimeType()) { 725 case Phone.CONTENT_ITEM_TYPE: 726 header = String.valueOf(action.getBody()); 727 footer = String.valueOf(action.getSubtitle()); 728 icon = applyThemeColorIfAvailable( 729 getResources().getDrawable(R.drawable.ic_phone_24dp)); 730 break; 731 case Email.CONTENT_ITEM_TYPE: 732 header = String.valueOf(action.getBody()); 733 footer = String.valueOf(action.getSubtitle()); 734 icon = applyThemeColorIfAvailable( 735 getResources().getDrawable(R.drawable.ic_email_24dp)); 736 break; 737 case StructuredPostal.CONTENT_ITEM_TYPE: 738 header = String.valueOf(action.getBody()); 739 footer = String.valueOf(action.getSubtitle()); 740 icon = applyThemeColorIfAvailable( 741 getResources().getDrawable(R.drawable.ic_place_24dp)); 742 break; 743 default: 744 header = String.valueOf(action.getSubtitle()); 745 footer = String.valueOf(action.getBody()); 746 icon = ResolveCache.getInstance(this).getIcon(action); 747 } 748 entries.add(new Entry(icon, header, body, footer, action.getIntent(), 749 /* isEditable= */ false)); 750 751 // Add SMS in addition to phone calls 752 if (action.getMimeType().equals(Phone.CONTENT_ITEM_TYPE)) { 753 entries.add(new Entry(applyThemeColorIfAvailable(getResources().getDrawable( 754 R.drawable.ic_message_24dp)), 755 getResources().getString(R.string.send_message), null, header, 756 action.getAlternateIntent(), /* isEditable = */ false)); 757 } 758 } 759 return entries; 760 } 761 762 private List<Entry> contactInteractionsToEntries(List<ContactInteraction> interactions) { 763 List<Entry> entries = new ArrayList<>(); 764 for (ContactInteraction interaction : interactions) { 765 entries.add(new Entry(applyThemeColorIfAvailable(interaction.getIcon(this)), 766 interaction.getViewHeader(this), 767 interaction.getViewBody(this), 768 interaction.getBodyIcon(this), 769 interaction.getViewFooter(this), 770 interaction.getFooterIcon(this), 771 interaction.getIntent(), 772 /* isEditable = */ false)); 773 } 774 return entries; 775 } 776 777 private LoaderCallbacks<Contact> mLoaderContactCallbacks = 778 new LoaderCallbacks<Contact>() { 779 @Override 780 public void onLoaderReset(Loader<Contact> loader) { 781 } 782 783 @Override 784 public void onLoadFinished(Loader<Contact> loader, Contact data) { 785 Trace.beginSection("onLoadFinished()"); 786 787 if (isFinishing()) { 788 return; 789 } 790 if (data.isError()) { 791 // This shouldn't ever happen, so throw an exception. The {@link ContactLoader} 792 // should log the actual exception. 793 throw new IllegalStateException("Failed to load contact", data.getException()); 794 } 795 if (data.isNotFound()) { 796 if (mHasAlreadyBeenOpened) { 797 finish(); 798 } else { 799 Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri()); 800 Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage, 801 Toast.LENGTH_LONG).show(); 802 } 803 return; 804 } 805 806 bindContactData(data); 807 808 Trace.endSection(); 809 } 810 811 @Override 812 public Loader<Contact> onCreateLoader(int id, Bundle args) { 813 if (mLookupUri == null) { 814 Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early"); 815 } 816 // Load all contact data. We need loadGroupMetaData=true to determine whether the 817 // contact is invisible. If it is, we need to display an "Add to Contacts" MenuItem. 818 return new ContactLoader(getApplicationContext(), mLookupUri, 819 true /*loadGroupMetaData*/, false /*loadInvitableAccountTypes*/, 820 false /*postViewNotification*/, true /*computeFormattedPhoneNumber*/); 821 } 822 }; 823 824 @Override 825 public void onBackPressed() { 826 if (mScroller != null) { 827 // TODO: implement exit animation if the scroller isn't already off the screen 828 finish(); 829 } else { 830 super.onBackPressed(); 831 } 832 } 833 834 @Override 835 public void finish() { 836 super.finish(); 837 838 // override transitions to skip the standard window animations 839 overridePendingTransition(0, 0); 840 } 841 842 private LoaderCallbacks<List<ContactInteraction>> mLoaderInteractionsCallbacks = 843 new LoaderCallbacks<List<ContactInteraction>>() { 844 845 @Override 846 public Loader<List<ContactInteraction>> onCreateLoader(int id, Bundle args) { 847 Log.v(TAG, "onCreateLoader"); 848 Loader<List<ContactInteraction>> loader = null; 849 switch (id) { 850 case LOADER_SMS_ID: 851 Log.v(TAG, "LOADER_SMS_ID"); 852 loader = new SmsInteractionsLoader( 853 QuickContactActivity.this, 854 args.getStringArray(KEY_LOADER_EXTRA_SMS_PHONES), 855 MAX_SMS_RETRIEVE); 856 break; 857 } 858 return loader; 859 } 860 861 @Override 862 public void onLoadFinished(Loader<List<ContactInteraction>> loader, 863 List<ContactInteraction> data) { 864 if (mRecentLoaderResults == null) { 865 mRecentLoaderResults = new HashMap<Integer, List<ContactInteraction>>(); 866 } 867 mRecentLoaderResults.put(loader.getId(), data); 868 869 if (isAllRecentDataLoaded()) { 870 bindRecentData(); 871 } 872 } 873 874 @Override 875 public void onLoaderReset(Loader<List<ContactInteraction>> loader) { 876 mRecentLoaderResults.remove(loader.getId()); 877 } 878 879 }; 880 881 private boolean isAllRecentDataLoaded() { 882 return mRecentLoaderResults.size() == mRecentLoaderIds.length; 883 } 884 885 private void bindRecentData() { 886 List<ContactInteraction> allInteractions = new ArrayList<>(); 887 for (List<ContactInteraction> loaderInteractions : mRecentLoaderResults.values()) { 888 allInteractions.addAll(loaderInteractions); 889 } 890 891 // Sort the interactions by most recent 892 Collections.sort(allInteractions, new Comparator<ContactInteraction>() { 893 @Override 894 public int compare(ContactInteraction a, ContactInteraction b) { 895 return a.getInteractionDate() >= b.getInteractionDate() ? -1 : 1; 896 } 897 }); 898 899 if (allInteractions.size() > 0) { 900 mRecentCard.initialize(contactInteractionsToEntries(allInteractions), 901 /* numInitialVisibleEntries = */ MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN, 902 /* isExpanded = */ false, 903 /* themeColor = */ 0); 904 mRecentCard.setVisibility(View.VISIBLE); 905 } 906 } 907 908 @Override 909 protected void onStop() { 910 super.onStop(); 911 912 if (mEntriesAndActionsTask != null) { 913 // Once the activity is stopped, we will no longer want to bind mEntriesAndActionsTask's 914 // results on the UI thread. In some circumstances Activities are killed without 915 // onStop() being called. This is not a problem, because in these circumstances 916 // the entire process will be killed. 917 mEntriesAndActionsTask.cancel(/* mayInterruptIfRunning = */ false); 918 } 919 } 920 921 /** 922 * Applies the theme color as extracted in 923 * {@link #extractAndApplyTintFromPhotoViewAsynchronously()} if available. If the color is not 924 * available, store a reference to the drawable to tint when a color becomes available. 925 */ 926 private Drawable applyThemeColorIfAvailable(Drawable drawable) { 927 if (mColorFilter != null) { 928 drawable.setColorFilter(mColorFilter); 929 } else { 930 mDrawablesToTint.add(drawable); 931 } 932 return drawable; 933 } 934 935 /** 936 * Returns true if it is possible to edit the current contact. 937 */ 938 private boolean isContactEditable() { 939 return mContactData != null && !mContactData.isDirectoryEntry(); 940 } 941 942 private void editContact() { 943 final Intent intent = new Intent(Intent.ACTION_EDIT, mLookupUri); 944 mContactLoader.cacheResult(); 945 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 946 startActivityForResult(intent, REQUEST_CODE_CONTACT_EDITOR_ACTIVITY); 947 } 948 949 private void toggleStar(MenuItem starredMenuItem) { 950 // Make sure there is a contact 951 if (mLookupUri != null) { 952 // Read the current starred value from the UI instead of using the last 953 // loaded state. This allows rapid tapping without writing the same 954 // value several times 955 final boolean isStarred = starredMenuItem.isChecked(); 956 957 // To improve responsiveness, swap out the picture (and tag) in the UI already 958 ContactDetailDisplayUtils.configureStarredMenuItem(starredMenuItem, 959 mContactData.isDirectoryEntry(), mContactData.isUserProfile(), 960 !isStarred); 961 962 // Now perform the real save 963 Intent intent = ContactSaveService.createSetStarredIntent( 964 QuickContactActivity.this, mLookupUri, !isStarred); 965 startService(intent); 966 } 967 } 968 969 /** 970 * Calls into the contacts provider to get a pre-authorized version of the given URI. 971 */ 972 private Uri getPreAuthorizedUri(Uri uri) { 973 final Bundle uriBundle = new Bundle(); 974 uriBundle.putParcelable(ContactsContract.Authorization.KEY_URI_TO_AUTHORIZE, uri); 975 final Bundle authResponse = getContentResolver().call( 976 ContactsContract.AUTHORITY_URI, 977 ContactsContract.Authorization.AUTHORIZATION_METHOD, 978 null, 979 uriBundle); 980 if (authResponse != null) { 981 return (Uri) authResponse.getParcelable( 982 ContactsContract.Authorization.KEY_AUTHORIZED_URI); 983 } else { 984 return uri; 985 } 986 } 987 private void shareContact() { 988 final String lookupKey = mContactData.getLookupKey(); 989 Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey); 990 if (mContactData.isUserProfile()) { 991 // User is sharing the profile. We don't want to force the receiver to have 992 // the highly-privileged READ_PROFILE permission, so we need to request a 993 // pre-authorized URI from the provider. 994 shareUri = getPreAuthorizedUri(shareUri); 995 } 996 997 final Intent intent = new Intent(Intent.ACTION_SEND); 998 intent.setType(Contacts.CONTENT_VCARD_TYPE); 999 intent.putExtra(Intent.EXTRA_STREAM, shareUri); 1000 1001 // Launch chooser to share contact via 1002 final CharSequence chooseTitle = getText(R.string.share_via); 1003 final Intent chooseIntent = Intent.createChooser(intent, chooseTitle); 1004 1005 try { 1006 this.startActivity(chooseIntent); 1007 } catch (ActivityNotFoundException ex) { 1008 Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show(); 1009 } 1010 } 1011 1012 /** 1013 * Creates a launcher shortcut with the current contact. 1014 */ 1015 private void createLauncherShortcutWithContact() { 1016 final ShortcutIntentBuilder builder = new ShortcutIntentBuilder(this, 1017 new OnShortcutIntentCreatedListener() { 1018 1019 @Override 1020 public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) { 1021 // Broadcast the shortcutIntent to the launcher to create a 1022 // shortcut to this contact 1023 shortcutIntent.setAction(ACTION_INSTALL_SHORTCUT); 1024 QuickContactActivity.this.sendBroadcast(shortcutIntent); 1025 1026 // Send a toast to give feedback to the user that a shortcut to this 1027 // contact was added to the launcher. 1028 Toast.makeText(QuickContactActivity.this, 1029 R.string.createContactShortcutSuccessful, 1030 Toast.LENGTH_SHORT).show(); 1031 } 1032 1033 }); 1034 builder.createContactShortcutIntent(mLookupUri); 1035 } 1036 1037 @Override 1038 public boolean onCreateOptionsMenu(Menu menu) { 1039 MenuInflater inflater = getMenuInflater(); 1040 inflater.inflate(R.menu.quickcontact, menu); 1041 return true; 1042 } 1043 1044 @Override 1045 public boolean onPrepareOptionsMenu(Menu menu) { 1046 if (mContactData != null) { 1047 final MenuItem starredMenuItem = menu.findItem(R.id.menu_star); 1048 ContactDetailDisplayUtils.configureStarredMenuItem(starredMenuItem, 1049 mContactData.isDirectoryEntry(), mContactData.isUserProfile(), 1050 mContactData.getStarred()); 1051 // Configure edit MenuItem 1052 final MenuItem editMenuItem = menu.findItem(R.id.menu_edit); 1053 editMenuItem.setVisible(true); 1054 if (DirectoryContactUtil.isDirectoryContact(mContactData) || InvisibleContactUtil 1055 .isInvisibleAndAddable(mContactData, this)) { 1056 editMenuItem.setIcon(R.drawable.ic_person_add_24dp); 1057 } else if (isContactEditable()) { 1058 editMenuItem.setIcon(R.drawable.ic_create_24dp); 1059 } else { 1060 editMenuItem.setVisible(false); 1061 } 1062 } 1063 return true; 1064 } 1065 1066 @Override 1067 public boolean onOptionsItemSelected(MenuItem item) { 1068 switch (item.getItemId()) { 1069 case R.id.menu_star: 1070 toggleStar(item); 1071 return true; 1072 case R.id.menu_edit: 1073 if (DirectoryContactUtil.isDirectoryContact(mContactData)) { 1074 DirectoryContactUtil.addToMyContacts(mContactData, this, getFragmentManager(), 1075 mSelectAccountFragmentListener); 1076 } else if (InvisibleContactUtil.isInvisibleAndAddable(mContactData, this)) { 1077 InvisibleContactUtil.addToDefaultGroup(mContactData, this); 1078 } else if (isContactEditable()) { 1079 editContact(); 1080 } 1081 return true; 1082 case R.id.menu_share: 1083 shareContact(); 1084 return true; 1085 case R.id.menu_create_contact_shortcut: 1086 createLauncherShortcutWithContact(); 1087 return true; 1088 default: 1089 return super.onOptionsItemSelected(item); 1090 } 1091 } 1092} 1093