QuickContactActivity.java revision 71032f3fb7038995297666602773ae023c1351c4
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.app.Activity; 20import android.app.Fragment; 21import android.app.FragmentManager; 22import android.app.LoaderManager.LoaderCallbacks; 23import android.content.ActivityNotFoundException; 24import android.content.ContentUris; 25import android.content.Context; 26import android.content.Intent; 27import android.content.Loader; 28import android.content.pm.PackageManager; 29import android.graphics.Rect; 30import android.graphics.drawable.Drawable; 31import android.net.Uri; 32import android.os.Bundle; 33import android.os.Handler; 34import android.provider.ContactsContract.CommonDataKinds.Email; 35import android.provider.ContactsContract.CommonDataKinds.Phone; 36import android.provider.ContactsContract.CommonDataKinds.SipAddress; 37import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 38import android.provider.ContactsContract.CommonDataKinds.Website; 39import android.provider.ContactsContract.Contacts; 40import android.provider.ContactsContract.DisplayNameSources; 41import android.provider.ContactsContract.Intents.Insert; 42import android.provider.ContactsContract.Directory; 43import android.provider.ContactsContract.QuickContact; 44import android.provider.ContactsContract.RawContacts; 45import android.support.v13.app.FragmentPagerAdapter; 46import android.support.v4.view.PagerAdapter; 47import android.support.v4.view.ViewPager; 48import android.support.v4.view.ViewPager.SimpleOnPageChangeListener; 49import android.text.TextUtils; 50import android.util.Log; 51import android.view.MotionEvent; 52import android.view.View; 53import android.view.View.OnClickListener; 54import android.view.ViewGroup; 55import android.view.WindowManager; 56import android.widget.HorizontalScrollView; 57import android.widget.ImageView; 58import android.widget.LinearLayout; 59import android.widget.RelativeLayout; 60import android.widget.TextView; 61import android.widget.Toast; 62 63import com.android.contacts.ContactSaveService; 64import com.android.contacts.common.Collapser; 65import com.android.contacts.R; 66import com.android.contacts.common.model.AccountTypeManager; 67import com.android.contacts.common.model.Contact; 68import com.android.contacts.common.model.ContactLoader; 69import com.android.contacts.common.model.RawContact; 70import com.android.contacts.common.model.account.AccountType; 71import com.android.contacts.common.model.dataitem.DataItem; 72import com.android.contacts.common.model.dataitem.DataKind; 73import com.android.contacts.common.model.dataitem.EmailDataItem; 74import com.android.contacts.common.model.dataitem.ImDataItem; 75import com.android.contacts.common.util.Constants; 76import com.android.contacts.common.util.DataStatus; 77import com.android.contacts.common.util.UriUtils; 78import com.android.contacts.quickcontact.ExpandingEntryCardView.Entry; 79import com.android.contacts.util.ImageViewDrawableSetter; 80import com.android.contacts.util.SchedulingUtils; 81import com.android.contacts.common.util.StopWatch; 82 83import com.google.common.base.Preconditions; 84import com.google.common.collect.Lists; 85 86import java.util.ArrayList; 87import java.util.HashMap; 88import java.util.HashSet; 89import java.util.List; 90import java.util.Set; 91 92// TODO: Save selected tab index during rotation 93 94/** 95 * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads 96 * data asynchronously, and then shows a popup with details centered around 97 * {@link Intent#getSourceBounds()}. 98 */ 99public class QuickContactActivity extends Activity { 100 private static final String TAG = "QuickContact"; 101 102 private static final boolean TRACE_LAUNCH = false; 103 private static final String TRACE_TAG = "quickcontact"; 104 private static final int POST_DRAW_WAIT_DURATION = 60; 105 private static final boolean ENABLE_STOPWATCH = false; 106 107 108 @SuppressWarnings("deprecation") 109 private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY; 110 111 private Uri mLookupUri; 112 private String[] mExcludeMimes; 113 private List<String> mSortedActionMimeTypes = Lists.newArrayList(); 114 115 private View mPhotoContainer; 116 117 private ImageView mPhotoView; 118 private ImageView mEditOrAddContactImage; 119 private ImageView mStarImage; 120 private ExpandingEntryCardView mCommunicationCard; 121 122 private Contact mContactData; 123 private ContactLoader mContactLoader; 124 125 private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter(); 126 127 /** 128 * Keeps the default action per mimetype. Empty if no default actions are set 129 */ 130 private HashMap<String, Action> mDefaultsMap = new HashMap<String, Action>(); 131 132 /** 133 * Set of {@link Action} that are associated with the aggregate currently 134 * displayed by this dialog, represented as a map from {@link String} 135 * MIME-type to a list of {@link Action}. 136 */ 137 private ActionMultiMap mActions = new ActionMultiMap(); 138 139 /** 140 * {@link #LEADING_MIMETYPES} and {@link #TRAILING_MIMETYPES} are used to sort MIME-types. 141 * 142 * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog, 143 * in the order specified here.</p> 144 * 145 * <p>The ones in {@link #TRAILING_MIMETYPES} appear in the end of the dialog, in the order 146 * specified here.</p> 147 * 148 * <p>The rest go between them, in the order in the array.</p> 149 */ 150 private static final List<String> LEADING_MIMETYPES = Lists.newArrayList( 151 Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE); 152 153 /** See {@link #LEADING_MIMETYPES}. */ 154 private static final List<String> TRAILING_MIMETYPES = Lists.newArrayList( 155 StructuredPostal.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE); 156 157 /** Id for the background loader */ 158 private static final int LOADER_ID = 0; 159 160 private StopWatch mStopWatch = ENABLE_STOPWATCH 161 ? StopWatch.start("QuickContact") : StopWatch.getNullStopWatch(); 162 163 final OnClickListener mEditContactClickHandler = new OnClickListener() { 164 @Override 165 public void onClick(View v) { 166 final Intent intent = new Intent(Intent.ACTION_EDIT, mLookupUri); 167 mContactLoader.cacheResult(); 168 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 169 startActivity(intent); 170 } 171 }; 172 173 final OnClickListener mAddToContactsClickHandler = new OnClickListener() { 174 @Override 175 public void onClick(View v) { 176 if (mContactData == null) { 177 Log.e(TAG, "Empty contact data when trying to add to contact"); 178 return; 179 } 180 final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); 181 intent.setType(Contacts.CONTENT_ITEM_TYPE); 182 183 // Only pre-fill the name field if the provided display name is an organization 184 // name or better (e.g. structured name, nickname) 185 if (mContactData.getDisplayNameSource() >= DisplayNameSources.ORGANIZATION) { 186 intent.putExtra(Insert.NAME, mContactData.getDisplayName()); 187 } 188 intent.putExtra(Insert.DATA, mContactData.getContentValues()); 189 startActivity(intent); 190 } 191 }; 192 193 final OnClickListener mEntryClickHandler = new OnClickListener() { 194 @Override 195 public void onClick(View v) { 196 Log.i(TAG, "mEntryClickHandler onClick"); 197 Object intent = v.getTag(); 198 if (intent == null || !(intent instanceof Intent)) { 199 return; 200 } 201 startActivity((Intent) intent); 202 } 203 }; 204 205 @Override 206 protected void onCreate(Bundle icicle) { 207 mStopWatch.lap("c"); // create start 208 super.onCreate(icicle); 209 210 mStopWatch.lap("sc"); // super.onCreate 211 212 if (TRACE_LAUNCH) android.os.Debug.startMethodTracing(TRACE_TAG); 213 214 // Parse intent 215 final Intent intent = getIntent(); 216 217 Uri lookupUri = intent.getData(); 218 219 // Check to see whether it comes from the old version. 220 if (lookupUri != null && LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) { 221 final long rawContactId = ContentUris.parseId(lookupUri); 222 lookupUri = RawContacts.getContactLookupUri(getContentResolver(), 223 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); 224 } 225 226 mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri"); 227 228 mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES); 229 230 mStopWatch.lap("i"); // intent parsed 231 232 mContactLoader = (ContactLoader) getLoaderManager().initLoader( 233 LOADER_ID, null, mLoaderCallbacks); 234 235 mStopWatch.lap("ld"); // loader started 236 237 // Show QuickContact in front of soft input 238 getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, 239 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 240 241 setContentView(R.layout.quickcontact_activity); 242 243 mStopWatch.lap("l"); // layout inflated 244 245 mEditOrAddContactImage = (ImageView) findViewById(R.id.contact_edit_image); 246 mStarImage = (ImageView) findViewById(R.id.quickcontact_star_button); 247 mCommunicationCard = (ExpandingEntryCardView) findViewById(R.id.communication_card); 248 mCommunicationCard.setTitle(getResources().getString(R.string.communication_card_title)); 249 250 mEditOrAddContactImage.setOnClickListener(mEditContactClickHandler); 251 mCommunicationCard.setOnClickListener(mEntryClickHandler); 252 253 // find and prepare correct header view 254 mPhotoContainer = findViewById(R.id.photo_container); 255 256 setHeaderNameText(R.id.name, R.string.missing_name); 257 258 mPhotoView = (ImageView) mPhotoContainer.findViewById(R.id.photo); 259 mPhotoView.setOnClickListener(mEditContactClickHandler); 260 261 mStopWatch.lap("v"); // view initialized 262 263 // TODO: Use some sort of fading in for the layout and content during animation 264 /*SchedulingUtils.doAfterLayout(mFloatingLayout, new Runnable() { 265 @Override 266 public void run() { 267 mFloatingLayout.fadeInBackground(); 268 } 269 });*/ 270 271 mStopWatch.lap("cf"); // onCreate finished 272 } 273 274 /** Assign this string to the view if it is not empty. */ 275 private void setHeaderNameText(int id, int resId) { 276 setHeaderNameText(id, getText(resId)); 277 } 278 279 /** Assign this string to the view if it is not empty. */ 280 private void setHeaderNameText(int id, CharSequence value) { 281 final View view = mPhotoContainer.findViewById(id); 282 if (view instanceof TextView) { 283 if (!TextUtils.isEmpty(value)) { 284 ((TextView)view).setText(value); 285 } 286 } 287 } 288 289 /** 290 * Check if the given MIME-type appears in the list of excluded MIME-types 291 * that the most-recent caller requested. 292 */ 293 private boolean isMimeExcluded(String mimeType) { 294 if (mExcludeMimes == null) return false; 295 for (String excludedMime : mExcludeMimes) { 296 if (TextUtils.equals(excludedMime, mimeType)) { 297 return true; 298 } 299 } 300 return false; 301 } 302 303 /** 304 * Handle the result from the ContactLoader 305 */ 306 private void bindData(Contact data) { 307 mContactData = data; 308 final ResolveCache cache = ResolveCache.getInstance(this); 309 final Context context = this; 310 311 mEditOrAddContactImage.setVisibility(isMimeExcluded(Contacts.CONTENT_ITEM_TYPE) ? 312 View.GONE : View.VISIBLE); 313 final boolean isStarred = data.getStarred(); 314 if (isStarred) { 315 mStarImage.setImageResource(R.drawable.ic_favorite_on_lt); 316 mStarImage.setContentDescription( 317 getResources().getString(R.string.menu_removeStar)); 318 } else { 319 mStarImage.setImageResource(R.drawable.ic_favorite_off_lt); 320 mStarImage.setContentDescription( 321 getResources().getString(R.string.menu_addStar)); 322 } 323 final Uri lookupUri = data.getLookupUri(); 324 325 // If this is a json encoded URI, there is no local contact to star 326 if (UriUtils.isEncodedContactUri(lookupUri)) { 327 mStarImage.setVisibility(View.GONE); 328 329 // If directory export support is not allowed, then don't allow the user to add 330 // to contacts 331 if (mContactData.getDirectoryExportSupport() == Directory.EXPORT_SUPPORT_NONE) { 332 configureHeaderClickActions(false); 333 } else { 334 configureHeaderClickActions(true); 335 } 336 } else { 337 configureHeaderClickActions(false); 338 mStarImage.setVisibility(View.VISIBLE); 339 mStarImage.setOnClickListener(new OnClickListener() { 340 @Override 341 public void onClick(View view) { 342 // Toggle "starred" state 343 // Make sure there is a contact 344 if (lookupUri != null) { 345 // Changes the state of the image already before sending updates to the 346 // database 347 if (isStarred) { 348 mStarImage.setImageResource(R.drawable.ic_favorite_off_lt); 349 } else { 350 mStarImage.setImageResource(R.drawable.ic_favorite_on_lt); 351 } 352 353 // Now perform the real save 354 final Intent intent = ContactSaveService.createSetStarredIntent(context, 355 lookupUri, !isStarred); 356 context.startService(intent); 357 } 358 } 359 }); 360 } 361 362 mDefaultsMap.clear(); 363 364 mStopWatch.lap("sph"); // Start photo setting 365 366 mPhotoSetter.setupContactPhoto(data, mPhotoView); 367 368 mStopWatch.lap("ph"); // Photo set 369 370 for (RawContact rawContact : data.getRawContacts()) { 371 for (DataItem dataItem : rawContact.getDataItems()) { 372 final String mimeType = dataItem.getMimeType(); 373 final AccountType accountType = rawContact.getAccountType(this); 374 final DataKind dataKind = AccountTypeManager.getInstance(this) 375 .getKindOrFallback(accountType, mimeType); 376 377 // Skip this data item if MIME-type excluded 378 if (isMimeExcluded(mimeType)) continue; 379 380 final long dataId = dataItem.getId(); 381 final boolean isPrimary = dataItem.isPrimary(); 382 final boolean isSuperPrimary = dataItem.isSuperPrimary(); 383 384 if (dataKind != null) { 385 // Build an action for this data entry, find a mapping to a UI 386 // element, build its summary from the cursor, and collect it 387 // along with all others of this MIME-type. 388 final Action action = new DataAction(context, dataItem, dataKind); 389 final boolean wasAdded = considerAdd(action, cache, isSuperPrimary); 390 if (wasAdded) { 391 // Remember the default 392 if (isSuperPrimary || (isPrimary && (mDefaultsMap.get(mimeType) == null))) { 393 mDefaultsMap.put(mimeType, action); 394 } 395 } 396 } 397 398 // Handle Email rows with presence data as Im entry 399 final DataStatus status = data.getStatuses().get(dataId); 400 if (status != null && dataItem instanceof EmailDataItem) { 401 final EmailDataItem email = (EmailDataItem) dataItem; 402 final ImDataItem im = ImDataItem.createFromEmail(email); 403 if (dataKind != null) { 404 final DataAction action = new DataAction(context, im, dataKind); 405 action.setPresence(status.getPresence()); 406 considerAdd(action, cache, isSuperPrimary); 407 } 408 } 409 } 410 } 411 412 mStopWatch.lap("e"); // Entities inflated 413 414 // Collapse Action Lists (remove e.g. duplicate e-mail addresses from different sources) 415 for (List<Action> actionChildren : mActions.values()) { 416 Collapser.collapseList(actionChildren); 417 } 418 419 mStopWatch.lap("c"); // List collapsed 420 421 setHeaderNameText(R.id.name, data.getDisplayName()); 422 423 // List of Entry that makes up the ExpandingEntryCardView 424 final List<Entry> entries = new ArrayList<>(); 425 // All the mime-types to add. 426 final Set<String> containedTypes = new HashSet<String>(mActions.keySet()); 427 mSortedActionMimeTypes.clear(); 428 // First, add LEADING_MIMETYPES, which are most common. 429 for (String mimeType : LEADING_MIMETYPES) { 430 if (containedTypes.contains(mimeType)) { 431 mSortedActionMimeTypes.add(mimeType); 432 containedTypes.remove(mimeType); 433 entries.addAll(actionsToEntries(mActions.get(mimeType))); 434 } 435 } 436 437 // Add all the remaining ones that are not TRAILING 438 for (String mimeType : containedTypes.toArray(new String[containedTypes.size()])) { 439 if (!TRAILING_MIMETYPES.contains(mimeType)) { 440 mSortedActionMimeTypes.add(mimeType); 441 containedTypes.remove(mimeType); 442 entries.addAll(actionsToEntries(mActions.get(mimeType))); 443 } 444 } 445 446 // Then, add TRAILING_MIMETYPES, which are least common. 447 for (String mimeType : TRAILING_MIMETYPES) { 448 if (containedTypes.contains(mimeType)) { 449 containedTypes.remove(mimeType); 450 mSortedActionMimeTypes.add(mimeType); 451 entries.addAll(actionsToEntries(mActions.get(mimeType))); 452 } 453 } 454 mCommunicationCard.initialize(entries, /* numInitialVisibleEntries = */ 2, 455 /* isExpanded = */ false, /* themeColor = */ 0); 456 457 final boolean hasData = !mSortedActionMimeTypes.isEmpty(); 458 mCommunicationCard.setVisibility(hasData ? View.VISIBLE: View.GONE); 459 } 460 461 /** 462 * Consider adding the given {@link Action}, which will only happen if 463 * {@link PackageManager} finds an application to handle 464 * {@link Action#getIntent()}. 465 * @param action the action to handle 466 * @param resolveCache cache of applications that can handle actions 467 * @param front indicates whether to add the action to the front of the list 468 * @return true if action has been added 469 */ 470 private boolean considerAdd(Action action, ResolveCache resolveCache, boolean front) { 471 if (resolveCache.hasResolve(action)) { 472 mActions.put(action.getMimeType(), action, front); 473 return true; 474 } 475 return false; 476 } 477 478 /** 479 * Bind the correct image resource and click handlers to the header views 480 * 481 * @param canAdd Whether or not the user can directly add information in this quick contact 482 * to their local contacts 483 */ 484 private void configureHeaderClickActions(boolean canAdd) { 485 if (canAdd) { 486 mEditOrAddContactImage.setImageResource(R.drawable.ic_person_add_24dp); 487 mEditOrAddContactImage.setOnClickListener(mAddToContactsClickHandler); 488 mPhotoView.setOnClickListener(mAddToContactsClickHandler); 489 } else { 490 mEditOrAddContactImage.setImageResource(R.drawable.ic_create_24dp); 491 mEditOrAddContactImage.setOnClickListener(mEditContactClickHandler); 492 mPhotoView.setOnClickListener(mEditContactClickHandler); 493 } 494 } 495 496 /** 497 * Converts a list of Action into a list of Entry 498 * @param actions The list of Action to convert 499 * @return The converted list of Entry 500 */ 501 private List<Entry> actionsToEntries(List<Action> actions) { 502 List<Entry> entries = new ArrayList<>(); 503 for (Action action : actions) { 504 entries.add(new Entry(ResolveCache.getInstance(this).getIcon(action), 505 action.getMimeType(), action.getSubtitle().toString(), 506 action.getBody().toString(), action.getIntent(), /* isEditable= */ false)); 507 } 508 return entries; 509 } 510 511 private LoaderCallbacks<Contact> mLoaderCallbacks = 512 new LoaderCallbacks<Contact>() { 513 @Override 514 public void onLoaderReset(Loader<Contact> loader) { 515 } 516 517 @Override 518 public void onLoadFinished(Loader<Contact> loader, Contact data) { 519 mStopWatch.lap("lf"); // onLoadFinished 520 if (isFinishing()) { 521 return; 522 } 523 if (data.isError()) { 524 // This shouldn't ever happen, so throw an exception. The {@link ContactLoader} 525 // should log the actual exception. 526 throw new IllegalStateException("Failed to load contact", data.getException()); 527 } 528 if (data.isNotFound()) { 529 Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri()); 530 Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage, 531 Toast.LENGTH_LONG).show(); 532 return; 533 } 534 535 bindData(data); 536 537 mStopWatch.lap("bd"); // bindData finished 538 539 if (TRACE_LAUNCH) android.os.Debug.stopMethodTracing(); 540 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { 541 Log.d(Constants.PERFORMANCE_TAG, "QuickContact shown"); 542 } 543 544 // Data bound and ready, pull curtain to show. Put this on the Handler to ensure 545 // that the layout passes are completed 546 // TODO: Add animation here 547 /*SchedulingUtils.doAfterLayout(mFloatingLayout, new Runnable() { 548 @Override 549 public void run() { 550 mFloatingLayout.showContent(new Runnable() { 551 @Override 552 public void run() { 553 mContactLoader.upgradeToFullContact(); 554 } 555 }); 556 } 557 });*/ 558 mStopWatch.stopAndLog(TAG, 0); 559 mStopWatch = StopWatch.getNullStopWatch(); // We're done with it. 560 } 561 562 @Override 563 public Loader<Contact> onCreateLoader(int id, Bundle args) { 564 if (mLookupUri == null) { 565 Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early"); 566 } 567 return new ContactLoader(getApplicationContext(), mLookupUri, 568 false /*loadGroupMetaData*/, false /*loadInvitableAccountTypes*/, 569 false /*postViewNotification*/, true /*computeFormattedPhoneNumber*/); 570 } 571 }; 572} 573