QuickContactActivity.java revision d9662a844162bb99df65e7976dfa335e64ca5440
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 com.android.contacts.Collapser; 20import com.android.contacts.ContactLoader; 21import com.android.contacts.R; 22import com.android.contacts.model.AccountTypeManager; 23import com.android.contacts.model.DataKind; 24import com.android.contacts.util.DataStatus; 25import com.android.contacts.util.ImageViewDrawableSetter; 26import com.android.contacts.util.SchedulingUtils; 27import com.google.common.base.Preconditions; 28import com.google.common.collect.Lists; 29 30import android.app.Activity; 31import android.app.Fragment; 32import android.app.FragmentManager; 33import android.app.LoaderManager.LoaderCallbacks; 34import android.content.ActivityNotFoundException; 35import android.content.ContentUris; 36import android.content.ContentValues; 37import android.content.Context; 38import android.content.Entity; 39import android.content.Entity.NamedContentValues; 40import android.content.Intent; 41import android.content.Loader; 42import android.content.pm.PackageManager; 43import android.graphics.Rect; 44import android.graphics.drawable.Drawable; 45import android.net.Uri; 46import android.os.Bundle; 47import android.os.Handler; 48import android.provider.ContactsContract.CommonDataKinds.Email; 49import android.provider.ContactsContract.CommonDataKinds.Im; 50import android.provider.ContactsContract.CommonDataKinds.Phone; 51import android.provider.ContactsContract.CommonDataKinds.SipAddress; 52import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 53import android.provider.ContactsContract.CommonDataKinds.Website; 54import android.provider.ContactsContract.Contacts; 55import android.provider.ContactsContract.Data; 56import android.provider.ContactsContract.QuickContact; 57import android.provider.ContactsContract.RawContacts; 58import android.support.v13.app.FragmentPagerAdapter; 59import android.support.v4.view.ViewPager; 60import android.support.v4.view.ViewPager.SimpleOnPageChangeListener; 61import android.text.TextUtils; 62import android.util.Log; 63import android.view.MotionEvent; 64import android.view.View; 65import android.view.View.OnClickListener; 66import android.view.ViewGroup; 67import android.view.WindowManager; 68import android.widget.HorizontalScrollView; 69import android.widget.ImageButton; 70import android.widget.ImageView; 71import android.widget.RelativeLayout; 72import android.widget.TextView; 73import android.widget.Toast; 74 75import java.util.HashMap; 76import java.util.HashSet; 77import java.util.List; 78import java.util.Set; 79 80// TODO: Save selected tab index during rotation 81 82/** 83 * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads 84 * data asynchronously, and then shows a popup with details centered around 85 * {@link Intent#getSourceBounds()}. 86 */ 87public class QuickContactActivity extends Activity { 88 private static final String TAG = "QuickContact"; 89 90 private static final boolean TRACE_LAUNCH = false; 91 private static final String TRACE_TAG = "quickcontact"; 92 private static final int POST_DRAW_WAIT_DURATION = 60; 93 94 @SuppressWarnings("deprecation") 95 private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY; 96 97 private Uri mLookupUri; 98 private String[] mExcludeMimes; 99 private List<String> mSortedActionMimeTypes = Lists.newArrayList(); 100 101 private boolean mHasFinishedAnimatingIn = false; 102 private boolean mHasStartedAnimatingOut = false; 103 104 private FloatingChildLayout mFloatingLayout; 105 106 private View mPhotoContainer; 107 private ViewGroup mTrack; 108 private HorizontalScrollView mTrackScroller; 109 private View mSelectedTabRectangle; 110 private View mLineAfterTrack; 111 112 private ImageButton mOpenDetailsButton; 113 private ImageButton mOpenDetailsPushLayerButton; 114 private ViewPager mListPager; 115 116 private ContactLoader mContactLoader; 117 118 private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter(); 119 120 /** 121 * Keeps the default action per mimetype. Empty if no default actions are set 122 */ 123 private HashMap<String, Action> mDefaultsMap = new HashMap<String, Action>(); 124 125 /** 126 * Set of {@link Action} that are associated with the aggregate currently 127 * displayed by this dialog, represented as a map from {@link String} 128 * MIME-type to a list of {@link Action}. 129 */ 130 private ActionMultiMap mActions = new ActionMultiMap(); 131 132 /** 133 * {@link #LEADING_MIMETYPES} and {@link #TRAILING_MIMETYPES} are used to sort MIME-types. 134 * 135 * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog, 136 * in the order specified here.</p> 137 * 138 * <p>The ones in {@link #TRAILING_MIMETYPES} appear in the end of the dialog, in the order 139 * specified here.</p> 140 * 141 * <p>The rest go between them, in the order in the array.</p> 142 */ 143 private static final List<String> LEADING_MIMETYPES = Lists.newArrayList( 144 Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE); 145 146 /** See {@link #LEADING_MIMETYPES}. */ 147 private static final List<String> TRAILING_MIMETYPES = Lists.newArrayList( 148 StructuredPostal.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE); 149 150 /** Id for the background loader */ 151 private static final int LOADER_ID = 0; 152 153 @Override 154 protected void onCreate(Bundle icicle) { 155 super.onCreate(icicle); 156 157 // Show QuickContact in front of soft input 158 getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, 159 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 160 161 setContentView(R.layout.quickcontact_activity); 162 163 mFloatingLayout = (FloatingChildLayout) findViewById(R.id.floating_layout); 164 mTrack = (ViewGroup) findViewById(R.id.track); 165 mTrackScroller = (HorizontalScrollView) findViewById(R.id.track_scroller); 166 mOpenDetailsButton = (ImageButton) findViewById(R.id.open_details_button); 167 mOpenDetailsPushLayerButton = (ImageButton) findViewById(R.id.open_details_push_layer); 168 mListPager = (ViewPager) findViewById(R.id.item_list_pager); 169 mSelectedTabRectangle = findViewById(R.id.selected_tab_rectangle); 170 mLineAfterTrack = findViewById(R.id.line_after_track); 171 172 mFloatingLayout.setOnOutsideTouchListener(new View.OnTouchListener() { 173 @Override 174 public boolean onTouch(View v, MotionEvent event) { 175 return handleOutsideTouch(); 176 } 177 }); 178 179 final OnClickListener openDetailsClickHandler = new OnClickListener() { 180 @Override 181 public void onClick(View v) { 182 final Intent intent = new Intent(Intent.ACTION_VIEW, mLookupUri); 183 mContactLoader.cacheResult(); 184 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 185 startActivity(intent); 186 hide(false); 187 } 188 }; 189 mOpenDetailsButton.setOnClickListener(openDetailsClickHandler); 190 mOpenDetailsPushLayerButton.setOnClickListener(openDetailsClickHandler); 191 mListPager.setAdapter(new ViewPagerAdapter(getFragmentManager())); 192 mListPager.setOnPageChangeListener(new PageChangeListener()); 193 194 show(); 195 } 196 197 private void show() { 198 199 if (TRACE_LAUNCH) { 200 android.os.Debug.startMethodTracing(TRACE_TAG); 201 } 202 203 final Intent intent = getIntent(); 204 205 Uri lookupUri = intent.getData(); 206 207 // Check to see whether it comes from the old version. 208 if (LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) { 209 final long rawContactId = ContentUris.parseId(lookupUri); 210 lookupUri = RawContacts.getContactLookupUri(getContentResolver(), 211 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); 212 } 213 214 mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri"); 215 216 // Read requested parameters for displaying 217 final Rect targetScreen = intent.getSourceBounds(); 218 Preconditions.checkNotNull(targetScreen, "missing targetScreen"); 219 mFloatingLayout.setChildTargetScreen(targetScreen); 220 221 mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES); 222 223 // find and prepare correct header view 224 mPhotoContainer = findViewById(R.id.photo_container); 225 setHeaderNameText(R.id.name, R.string.missing_name); 226 227 mContactLoader = (ContactLoader) getLoaderManager().initLoader( 228 LOADER_ID, null, mLoaderCallbacks); 229 } 230 231 private boolean handleOutsideTouch() { 232 if (!mHasFinishedAnimatingIn) return false; 233 if (mHasStartedAnimatingOut) return false; 234 235 mHasStartedAnimatingOut = true; 236 hide(true); 237 return true; 238 } 239 240 private void hide(boolean withAnimation) { 241 // cancel any pending queries 242 getLoaderManager().destroyLoader(LOADER_ID); 243 244 if (withAnimation) { 245 mFloatingLayout.hideChild(new Runnable() { 246 @Override 247 public void run() { 248 // Wait until the final animation frame has been drawn, otherwise 249 // there is jank as the framework transitions to the next Activity. 250 SchedulingUtils.doAfterDraw(mFloatingLayout, new Runnable() { 251 @Override 252 public void run() { 253 // Unfortunately, we need to also use postDelayed() to wait a moment 254 // for the frame to be drawn, else the framework's activity-transition 255 // animation will kick in before the final frame is available to it. 256 // This seems unavoidable. The problem isn't merely that there is no 257 // post-draw listener API; if that were so, it would be sufficient to 258 // call post() instead of postDelayed(). 259 new Handler().postDelayed(new Runnable() { 260 @Override 261 public void run() { 262 finish(); 263 } 264 }, POST_DRAW_WAIT_DURATION); 265 } 266 }); 267 } 268 }); 269 } else { 270 mFloatingLayout.hideChild(null); 271 finish(); 272 } 273 } 274 275 @Override 276 public void onBackPressed() { 277 hide(true); 278 } 279 280 /** Assign this string to the view if it is not empty. */ 281 private void setHeaderNameText(int id, int resId) { 282 setHeaderNameText(id, getText(resId)); 283 } 284 285 /** Assign this string to the view if it is not empty. */ 286 private void setHeaderNameText(int id, CharSequence value) { 287 final View view = mPhotoContainer.findViewById(id); 288 if (view instanceof TextView) { 289 if (!TextUtils.isEmpty(value)) { 290 ((TextView)view).setText(value); 291 } 292 } 293 } 294 295 /** 296 * Check if the given MIME-type appears in the list of excluded MIME-types 297 * that the most-recent caller requested. 298 */ 299 private boolean isMimeExcluded(String mimeType) { 300 if (mExcludeMimes == null) return false; 301 for (String excludedMime : mExcludeMimes) { 302 if (TextUtils.equals(excludedMime, mimeType)) { 303 return true; 304 } 305 } 306 return false; 307 } 308 309 /** 310 * Handle the result from the ContactLoader 311 */ 312 private void bindData(ContactLoader.Result data) { 313 final ResolveCache cache = ResolveCache.getInstance(this); 314 final Context context = this; 315 316 mOpenDetailsButton.setVisibility(isMimeExcluded(Contacts.CONTENT_ITEM_TYPE) ? View.GONE 317 : View.VISIBLE); 318 319 mDefaultsMap.clear(); 320 321 final AccountTypeManager accountTypes = AccountTypeManager.getInstance( 322 context.getApplicationContext()); 323 final ImageView photoView = (ImageView) mPhotoContainer.findViewById(R.id.photo); 324 mPhotoSetter.setupContactPhoto(data, photoView); 325 326 for (Entity entity : data.getEntities()) { 327 final ContentValues entityValues = entity.getEntityValues(); 328 final String accountType = entityValues.getAsString(RawContacts.ACCOUNT_TYPE); 329 final String dataSet = entityValues.getAsString(RawContacts.DATA_SET); 330 for (NamedContentValues subValue : entity.getSubValues()) { 331 final ContentValues entryValues = subValue.values; 332 final String mimeType = entryValues.getAsString(Data.MIMETYPE); 333 334 // Skip this data item if MIME-type excluded 335 if (isMimeExcluded(mimeType)) continue; 336 337 final long dataId = entryValues.getAsLong(Data._ID); 338 final Integer primary = entryValues.getAsInteger(Data.IS_PRIMARY); 339 final boolean isPrimary = primary != null && primary != 0; 340 final Integer superPrimary = entryValues.getAsInteger(Data.IS_SUPER_PRIMARY); 341 final boolean isSuperPrimary = superPrimary != null && superPrimary != 0; 342 343 final DataKind kind = 344 accountTypes.getKindOrFallback(accountType, dataSet, mimeType); 345 346 if (kind != null) { 347 // Build an action for this data entry, find a mapping to a UI 348 // element, build its summary from the cursor, and collect it 349 // along with all others of this MIME-type. 350 final Action action = new DataAction(context, mimeType, kind, dataId, 351 entryValues); 352 final boolean wasAdded = considerAdd(action, cache, isSuperPrimary); 353 if (wasAdded) { 354 // Remember the default 355 if (isSuperPrimary || (isPrimary && (mDefaultsMap.get(mimeType) == null))) { 356 mDefaultsMap.put(mimeType, action); 357 } 358 } 359 } 360 361 // Handle Email rows with presence data as Im entry 362 final DataStatus status = data.getStatuses().get(dataId); 363 if (status != null && Email.CONTENT_ITEM_TYPE.equals(mimeType)) { 364 final DataKind imKind = accountTypes.getKindOrFallback(accountType, dataSet, 365 Im.CONTENT_ITEM_TYPE); 366 if (imKind != null) { 367 final DataAction action = new DataAction(context, Im.CONTENT_ITEM_TYPE, 368 imKind, dataId, entryValues); 369 action.setPresence(status.getPresence()); 370 considerAdd(action, cache, isSuperPrimary); 371 } 372 } 373 } 374 } 375 376 // Collapse Action Lists (remove e.g. duplicate e-mail addresses from different sources) 377 for (List<Action> actionChildren : mActions.values()) { 378 Collapser.collapseList(actionChildren); 379 } 380 381 setHeaderNameText(R.id.name, data.getDisplayName()); 382 383 // All the mime-types to add. 384 final Set<String> containedTypes = new HashSet<String>(mActions.keySet()); 385 mSortedActionMimeTypes.clear(); 386 // First, add LEADING_MIMETYPES, which are most common. 387 for (String mimeType : LEADING_MIMETYPES) { 388 if (containedTypes.contains(mimeType)) { 389 mSortedActionMimeTypes.add(mimeType); 390 containedTypes.remove(mimeType); 391 } 392 } 393 394 // Add all the remaining ones that are not TRAILING 395 for (String mimeType : containedTypes.toArray(new String[containedTypes.size()])) { 396 if (!TRAILING_MIMETYPES.contains(mimeType)) { 397 mSortedActionMimeTypes.add(mimeType); 398 containedTypes.remove(mimeType); 399 } 400 } 401 402 // Then, add TRAILING_MIMETYPES, which are least common. 403 for (String mimeType : TRAILING_MIMETYPES) { 404 if (containedTypes.contains(mimeType)) { 405 containedTypes.remove(mimeType); 406 mSortedActionMimeTypes.add(mimeType); 407 } 408 } 409 410 // Add buttons for each mimetype 411 mTrack.removeAllViews(); 412 for (String mimeType : mSortedActionMimeTypes) { 413 final View actionView = inflateAction(mimeType, cache, mTrack); 414 mTrack.addView(actionView); 415 } 416 417 final boolean hasData = !mSortedActionMimeTypes.isEmpty(); 418 mTrackScroller.setVisibility(hasData ? View.VISIBLE : View.GONE); 419 mSelectedTabRectangle.setVisibility(hasData ? View.VISIBLE : View.GONE); 420 mLineAfterTrack.setVisibility(hasData ? View.VISIBLE : View.GONE); 421 mListPager.setVisibility(hasData ? View.VISIBLE : View.GONE); 422 } 423 424 /** 425 * Consider adding the given {@link Action}, which will only happen if 426 * {@link PackageManager} finds an application to handle 427 * {@link Action#getIntent()}. 428 * @param action the action to handle 429 * @param resolveCache cache of applications that can handle actions 430 * @param front indicates whether to add the action to the front of the list 431 * @return true if action has been added 432 */ 433 private boolean considerAdd(Action action, ResolveCache resolveCache, boolean front) { 434 if (resolveCache.hasResolve(action)) { 435 mActions.put(action.getMimeType(), action, front); 436 return true; 437 } 438 return false; 439 } 440 441 /** 442 * Inflate the in-track view for the action of the given MIME-type, collapsing duplicate values. 443 * Will use the icon provided by the {@link DataKind}. 444 */ 445 private View inflateAction(String mimeType, ResolveCache resolveCache, ViewGroup root) { 446 final CheckableImageView typeView = (CheckableImageView) getLayoutInflater().inflate( 447 R.layout.quickcontact_track_button, root, false); 448 449 List<Action> children = mActions.get(mimeType); 450 typeView.setTag(mimeType); 451 final Action firstInfo = children.get(0); 452 453 // Set icon and listen for clicks 454 final CharSequence descrip = resolveCache.getDescription(firstInfo); 455 final Drawable icon = resolveCache.getIcon(firstInfo); 456 typeView.setChecked(false); 457 typeView.setContentDescription(descrip); 458 typeView.setImageDrawable(icon); 459 typeView.setOnClickListener(mTypeViewClickListener); 460 461 return typeView; 462 } 463 464 private CheckableImageView getActionViewAt(int position) { 465 return (CheckableImageView) mTrack.getChildAt(position); 466 } 467 468 @Override 469 public void onAttachFragment(Fragment fragment) { 470 final QuickContactListFragment listFragment = (QuickContactListFragment) fragment; 471 listFragment.setListener(mListFragmentListener); 472 } 473 474 private LoaderCallbacks<ContactLoader.Result> mLoaderCallbacks = 475 new LoaderCallbacks<ContactLoader.Result>() { 476 @Override 477 public void onLoaderReset(Loader<ContactLoader.Result> loader) { 478 } 479 480 @Override 481 public void onLoadFinished(Loader<ContactLoader.Result> loader, ContactLoader.Result data) { 482 if (isFinishing()) { 483 hide(false); 484 return; 485 } 486 if (data.isError()) { 487 // This shouldn't ever happen, so throw an exception. The {@link ContactLoader} 488 // should log the actual exception. 489 throw new IllegalStateException("Failed to load contact", data.getException()); 490 } 491 if (data.isNotFound()) { 492 Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri()); 493 Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage, 494 Toast.LENGTH_LONG).show(); 495 hide(false); 496 return; 497 } 498 499 bindData(data); 500 501 if (TRACE_LAUNCH) { 502 android.os.Debug.stopMethodTracing(); 503 } 504 505 // Data bound and ready, pull curtain to show. Put this on the Handler to ensure 506 // that the layout passes are completed 507 SchedulingUtils.doAfterLayout(mFloatingLayout, new Runnable() { 508 @Override 509 public void run() { 510 mFloatingLayout.showChild(new Runnable() { 511 @Override 512 public void run() { 513 mHasFinishedAnimatingIn = true; 514 mContactLoader.upgradeToFullContact(); 515 } 516 }); 517 } 518 }); 519 } 520 521 @Override 522 public Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) { 523 if (mLookupUri == null) { 524 Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early"); 525 } 526 return new ContactLoader(getApplicationContext(), mLookupUri); 527 } 528 }; 529 530 /** A type (e.g. Call/Addresses was clicked) */ 531 private final OnClickListener mTypeViewClickListener = new OnClickListener() { 532 @Override 533 public void onClick(View view) { 534 final CheckableImageView actionView = (CheckableImageView)view; 535 final String mimeType = (String) actionView.getTag(); 536 int index = mSortedActionMimeTypes.indexOf(mimeType); 537 mListPager.setCurrentItem(index, true); 538 } 539 }; 540 541 private class ViewPagerAdapter extends FragmentPagerAdapter { 542 public ViewPagerAdapter(FragmentManager fragmentManager) { 543 super(fragmentManager); 544 } 545 546 @Override 547 public Fragment getItem(int position) { 548 QuickContactListFragment fragment = new QuickContactListFragment(); 549 final String mimeType = mSortedActionMimeTypes.get(position); 550 final List<Action> actions = mActions.get(mimeType); 551 fragment.setActions(actions); 552 return fragment; 553 } 554 555 @Override 556 public int getCount() { 557 return mSortedActionMimeTypes.size(); 558 } 559 } 560 561 private class PageChangeListener extends SimpleOnPageChangeListener { 562 @Override 563 public void onPageSelected(int position) { 564 final CheckableImageView actionView = getActionViewAt(position); 565 mTrackScroller.requestChildRectangleOnScreen(actionView, 566 new Rect(0, 0, actionView.getWidth(), actionView.getHeight()), false); 567 } 568 569 @Override 570 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 571 final RelativeLayout.LayoutParams layoutParams = 572 (RelativeLayout.LayoutParams) mSelectedTabRectangle.getLayoutParams(); 573 final int width = mSelectedTabRectangle.getWidth(); 574 layoutParams.leftMargin = (int) ((position + positionOffset) * width); 575 mSelectedTabRectangle.setLayoutParams(layoutParams); 576 } 577 } 578 579 private final QuickContactListFragment.Listener mListFragmentListener = 580 new QuickContactListFragment.Listener() { 581 @Override 582 public void onOutsideClick() { 583 // If there is no background, we want to dismiss, because to the user it seems 584 // like he had touched outside. If the ViewPager is solid however, those taps 585 // must be ignored 586 final boolean isTransparent = mListPager.getBackground() == null; 587 if (isTransparent) handleOutsideTouch(); 588 } 589 590 @Override 591 public void onItemClicked(final Action action, final boolean alternate) { 592 final Runnable startAppRunnable = new Runnable() { 593 @Override 594 public void run() { 595 try { 596 startActivity(alternate ? action.getAlternateIntent() : action.getIntent()); 597 } catch (ActivityNotFoundException e) { 598 Toast.makeText(QuickContactActivity.this, R.string.quickcontact_missing_app, 599 Toast.LENGTH_SHORT).show(); 600 } 601 602 hide(false); 603 } 604 }; 605 // Defer the action to make the window properly repaint 606 new Handler().post(startAppRunnable); 607 } 608 }; 609} 610