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