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