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 17 package com.android.contacts.quickcontact; 18 19 import android.app.Activity; 20 import android.app.Fragment; 21 import android.app.FragmentManager; 22 import android.app.LoaderManager.LoaderCallbacks; 23 import android.content.ActivityNotFoundException; 24 import android.content.ContentUris; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.Loader; 28 import android.content.pm.PackageManager; 29 import android.graphics.Rect; 30 import android.graphics.drawable.Drawable; 31 import android.net.Uri; 32 import android.os.Bundle; 33 import android.os.Handler; 34 import android.provider.ContactsContract.CommonDataKinds.Email; 35 import android.provider.ContactsContract.CommonDataKinds.Phone; 36 import android.provider.ContactsContract.CommonDataKinds.SipAddress; 37 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 38 import android.provider.ContactsContract.CommonDataKinds.Website; 39 import android.provider.ContactsContract.Contacts; 40 import android.provider.ContactsContract.DisplayNameSources; 41 import android.provider.ContactsContract.Intents.Insert; 42 import android.provider.ContactsContract.Directory; 43 import android.provider.ContactsContract.QuickContact; 44 import android.provider.ContactsContract.RawContacts; 45 import android.support.v13.app.FragmentPagerAdapter; 46 import android.support.v4.view.PagerAdapter; 47 import android.support.v4.view.ViewPager; 48 import android.support.v4.view.ViewPager.SimpleOnPageChangeListener; 49 import android.text.TextUtils; 50 import android.util.Log; 51 import android.view.MotionEvent; 52 import android.view.View; 53 import android.view.View.OnClickListener; 54 import android.view.ViewGroup; 55 import android.view.WindowManager; 56 import android.widget.HorizontalScrollView; 57 import android.widget.ImageView; 58 import android.widget.RelativeLayout; 59 import android.widget.TextView; 60 import android.widget.Toast; 61 62 import com.android.contacts.ContactSaveService; 63 import com.android.contacts.common.Collapser; 64 import com.android.contacts.R; 65 import com.android.contacts.common.model.AccountTypeManager; 66 import com.android.contacts.common.model.Contact; 67 import com.android.contacts.common.model.ContactLoader; 68 import com.android.contacts.common.model.RawContact; 69 import com.android.contacts.common.model.account.AccountType; 70 import com.android.contacts.common.model.dataitem.DataItem; 71 import com.android.contacts.common.model.dataitem.DataKind; 72 import com.android.contacts.common.model.dataitem.EmailDataItem; 73 import com.android.contacts.common.model.dataitem.ImDataItem; 74 import com.android.contacts.common.util.Constants; 75 import com.android.contacts.common.util.DataStatus; 76 import com.android.contacts.common.util.UriUtils; 77 import com.android.contacts.util.ImageViewDrawableSetter; 78 import com.android.contacts.util.SchedulingUtils; 79 import com.android.contacts.common.util.StopWatch; 80 import com.google.common.base.Preconditions; 81 import com.google.common.collect.Lists; 82 83 import java.util.HashMap; 84 import java.util.HashSet; 85 import java.util.List; 86 import java.util.Set; 87 88 // TODO: Save selected tab index during rotation 89 90 /** 91 * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads 92 * data asynchronously, and then shows a popup with details centered around 93 * {@link Intent#getSourceBounds()}. 94 */ 95 public class QuickContactActivity extends Activity { 96 private static final String TAG = "QuickContact"; 97 98 private static final boolean TRACE_LAUNCH = false; 99 private static final String TRACE_TAG = "quickcontact"; 100 private static final int POST_DRAW_WAIT_DURATION = 60; 101 private static final boolean ENABLE_STOPWATCH = false; 102 103 104 @SuppressWarnings("deprecation") 105 private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY; 106 107 private Uri mLookupUri; 108 private String[] mExcludeMimes; 109 private List<String> mSortedActionMimeTypes = Lists.newArrayList(); 110 111 private FloatingChildLayout mFloatingLayout; 112 113 private View mPhotoContainer; 114 private ViewGroup mTrack; 115 private HorizontalScrollView mTrackScroller; 116 private View mSelectedTabRectangle; 117 private View mLineAfterTrack; 118 119 private ImageView mPhotoView; 120 private ImageView mOpenDetailsOrAddContactImage; 121 private ImageView mStarImage; 122 private ViewPager mListPager; 123 private ViewPagerAdapter mPagerAdapter; 124 125 private Contact mContactData; 126 private ContactLoader mContactLoader; 127 128 private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter(); 129 130 /** 131 * Keeps the default action per mimetype. Empty if no default actions are set 132 */ 133 private HashMap<String, Action> mDefaultsMap = new HashMap<String, Action>(); 134 135 /** 136 * Set of {@link Action} that are associated with the aggregate currently 137 * displayed by this dialog, represented as a map from {@link String} 138 * MIME-type to a list of {@link Action}. 139 */ 140 private ActionMultiMap mActions = new ActionMultiMap(); 141 142 /** 143 * {@link #LEADING_MIMETYPES} and {@link #TRAILING_MIMETYPES} are used to sort MIME-types. 144 * 145 * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog, 146 * in the order specified here.</p> 147 * 148 * <p>The ones in {@link #TRAILING_MIMETYPES} appear in the end of the dialog, in the order 149 * specified here.</p> 150 * 151 * <p>The rest go between them, in the order in the array.</p> 152 */ 153 private static final List<String> LEADING_MIMETYPES = Lists.newArrayList( 154 Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE); 155 156 /** See {@link #LEADING_MIMETYPES}. */ 157 private static final List<String> TRAILING_MIMETYPES = Lists.newArrayList( 158 StructuredPostal.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE); 159 160 /** Id for the background loader */ 161 private static final int LOADER_ID = 0; 162 163 private StopWatch mStopWatch = ENABLE_STOPWATCH 164 ? StopWatch.start("QuickContact") : StopWatch.getNullStopWatch(); 165 166 final OnClickListener mOpenDetailsClickHandler = new OnClickListener() { 167 @Override 168 public void onClick(View v) { 169 final Intent intent = new Intent(Intent.ACTION_VIEW, mLookupUri); 170 mContactLoader.cacheResult(); 171 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 172 startActivity(intent); 173 close(false); 174 } 175 }; 176 177 final OnClickListener mAddToContactsClickHandler = new OnClickListener() { 178 @Override 179 public void onClick(View v) { 180 if (mContactData == null) { 181 Log.e(TAG, "Empty contact data when trying to add to contact"); 182 return; 183 } 184 final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); 185 intent.setType(Contacts.CONTENT_ITEM_TYPE); 186 187 // Only pre-fill the name field if the provided display name is an organization 188 // name or better (e.g. structured name, nickname) 189 if (mContactData.getDisplayNameSource() >= DisplayNameSources.ORGANIZATION) { 190 intent.putExtra(Insert.NAME, mContactData.getDisplayName()); 191 } 192 intent.putExtra(Insert.DATA, mContactData.getContentValues()); 193 startActivity(intent); 194 } 195 }; 196 197 @Override 198 protected void onCreate(Bundle icicle) { 199 mStopWatch.lap("c"); // create start 200 super.onCreate(icicle); 201 202 mStopWatch.lap("sc"); // super.onCreate 203 204 if (TRACE_LAUNCH) android.os.Debug.startMethodTracing(TRACE_TAG); 205 206 // Parse intent 207 final Intent intent = getIntent(); 208 209 Uri lookupUri = intent.getData(); 210 211 // Check to see whether it comes from the old version. 212 if (lookupUri != null && LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) { 213 final long rawContactId = ContentUris.parseId(lookupUri); 214 lookupUri = RawContacts.getContactLookupUri(getContentResolver(), 215 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); 216 } 217 218 mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri"); 219 220 mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES); 221 222 mStopWatch.lap("i"); // intent parsed 223 224 mContactLoader = (ContactLoader) getLoaderManager().initLoader( 225 LOADER_ID, null, mLoaderCallbacks); 226 227 mStopWatch.lap("ld"); // loader started 228 229 // Show QuickContact in front of soft input 230 getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, 231 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 232 233 setContentView(R.layout.quickcontact_activity); 234 235 mStopWatch.lap("l"); // layout inflated 236 237 mFloatingLayout = (FloatingChildLayout) findViewById(R.id.floating_layout); 238 mTrack = (ViewGroup) findViewById(R.id.track); 239 mTrackScroller = (HorizontalScrollView) findViewById(R.id.track_scroller); 240 mOpenDetailsOrAddContactImage = (ImageView) findViewById(R.id.contact_details_image); 241 mStarImage = (ImageView) findViewById(R.id.quickcontact_star_button); 242 mListPager = (ViewPager) findViewById(R.id.item_list_pager); 243 mSelectedTabRectangle = findViewById(R.id.selected_tab_rectangle); 244 mLineAfterTrack = findViewById(R.id.line_after_track); 245 246 mFloatingLayout.setOnOutsideTouchListener(new View.OnTouchListener() { 247 @Override 248 public boolean onTouch(View v, MotionEvent event) { 249 handleOutsideTouch(); 250 return true; 251 } 252 }); 253 254 mOpenDetailsOrAddContactImage.setOnClickListener(mOpenDetailsClickHandler); 255 256 mPagerAdapter = new ViewPagerAdapter(getFragmentManager()); 257 mListPager.setAdapter(mPagerAdapter); 258 mListPager.setOnPageChangeListener(new PageChangeListener()); 259 260 final Rect sourceBounds = intent.getSourceBounds(); 261 if (sourceBounds != null) { 262 mFloatingLayout.setChildTargetScreen(sourceBounds); 263 } 264 265 // find and prepare correct header view 266 mPhotoContainer = findViewById(R.id.photo_container); 267 268 setHeaderNameText(R.id.name, R.string.missing_name); 269 270 mPhotoView = (ImageView) mPhotoContainer.findViewById(R.id.photo); 271 mPhotoView.setOnClickListener(mOpenDetailsClickHandler); 272 273 mStopWatch.lap("v"); // view initialized 274 275 SchedulingUtils.doAfterLayout(mFloatingLayout, new Runnable() { 276 @Override 277 public void run() { 278 mFloatingLayout.fadeInBackground(); 279 } 280 }); 281 282 mStopWatch.lap("cf"); // onCreate finished 283 } 284 285 private void handleOutsideTouch() { 286 if (mFloatingLayout.isContentFullyVisible()) { 287 close(true); 288 } 289 } 290 291 private void close(boolean withAnimation) { 292 // cancel any pending queries 293 getLoaderManager().destroyLoader(LOADER_ID); 294 295 if (withAnimation) { 296 mFloatingLayout.fadeOutBackground(); 297 final boolean animated = mFloatingLayout.hideContent(new Runnable() { 298 @Override 299 public void run() { 300 // Wait until the final animation frame has been drawn, otherwise 301 // there is jank as the framework transitions to the next Activity. 302 SchedulingUtils.doAfterDraw(mFloatingLayout, new Runnable() { 303 @Override 304 public void run() { 305 // Unfortunately, we need to also use postDelayed() to wait a moment 306 // for the frame to be drawn, else the framework's activity-transition 307 // animation will kick in before the final frame is available to it. 308 // This seems unavoidable. The problem isn't merely that there is no 309 // post-draw listener API; if that were so, it would be sufficient to 310 // call post() instead of postDelayed(). 311 new Handler().postDelayed(new Runnable() { 312 @Override 313 public void run() { 314 finish(); 315 overridePendingTransition(0, 0); 316 } 317 }, POST_DRAW_WAIT_DURATION); 318 } 319 }); 320 } 321 }); 322 if (!animated) { 323 // If we were in the wrong state, simply quit (this can happen for example 324 // if the user pushes BACK before anything has loaded) 325 finish(); 326 } 327 } else { 328 finish(); 329 } 330 } 331 332 @Override 333 public void onBackPressed() { 334 close(true); 335 } 336 337 /** Assign this string to the view if it is not empty. */ 338 private void setHeaderNameText(int id, int resId) { 339 setHeaderNameText(id, getText(resId)); 340 } 341 342 /** Assign this string to the view if it is not empty. */ 343 private void setHeaderNameText(int id, CharSequence value) { 344 final View view = mPhotoContainer.findViewById(id); 345 if (view instanceof TextView) { 346 if (!TextUtils.isEmpty(value)) { 347 ((TextView)view).setText(value); 348 } 349 } 350 } 351 352 /** 353 * Check if the given MIME-type appears in the list of excluded MIME-types 354 * that the most-recent caller requested. 355 */ 356 private boolean isMimeExcluded(String mimeType) { 357 if (mExcludeMimes == null) return false; 358 for (String excludedMime : mExcludeMimes) { 359 if (TextUtils.equals(excludedMime, mimeType)) { 360 return true; 361 } 362 } 363 return false; 364 } 365 366 /** 367 * Handle the result from the ContactLoader 368 */ 369 private void bindData(Contact data) { 370 mContactData = data; 371 final ResolveCache cache = ResolveCache.getInstance(this); 372 final Context context = this; 373 374 mOpenDetailsOrAddContactImage.setVisibility(isMimeExcluded(Contacts.CONTENT_ITEM_TYPE) ? 375 View.GONE : View.VISIBLE); 376 final boolean isStarred = data.getStarred(); 377 if (isStarred) { 378 mStarImage.setImageResource(R.drawable.ic_favorite_on_lt); 379 mStarImage.setContentDescription( 380 getResources().getString(R.string.menu_removeStar)); 381 } else { 382 mStarImage.setImageResource(R.drawable.ic_favorite_off_lt); 383 mStarImage.setContentDescription( 384 getResources().getString(R.string.menu_addStar)); 385 } 386 final Uri lookupUri = data.getLookupUri(); 387 388 // If this is a json encoded URI, there is no local contact to star 389 if (UriUtils.isEncodedContactUri(lookupUri)) { 390 mStarImage.setVisibility(View.GONE); 391 392 // If directory export support is not allowed, then don't allow the user to add 393 // to contacts 394 if (mContactData.getDirectoryExportSupport() == Directory.EXPORT_SUPPORT_NONE) { 395 configureHeaderClickActions(false); 396 } else { 397 configureHeaderClickActions(true); 398 } 399 } else { 400 configureHeaderClickActions(false); 401 mStarImage.setVisibility(View.VISIBLE); 402 mStarImage.setOnClickListener(new OnClickListener() { 403 @Override 404 public void onClick(View view) { 405 // Toggle "starred" state 406 // Make sure there is a contact 407 if (lookupUri != null) { 408 // Changes the state of the image already before sending updates to the 409 // database 410 if (isStarred) { 411 mStarImage.setImageResource(R.drawable.ic_favorite_off_lt); 412 } else { 413 mStarImage.setImageResource(R.drawable.ic_favorite_on_lt); 414 } 415 416 // Now perform the real save 417 final Intent intent = ContactSaveService.createSetStarredIntent(context, 418 lookupUri, !isStarred); 419 context.startService(intent); 420 } 421 } 422 }); 423 } 424 425 mDefaultsMap.clear(); 426 427 mStopWatch.lap("sph"); // Start photo setting 428 429 mPhotoSetter.setupContactPhoto(data, mPhotoView); 430 431 mStopWatch.lap("ph"); // Photo set 432 433 for (RawContact rawContact : data.getRawContacts()) { 434 for (DataItem dataItem : rawContact.getDataItems()) { 435 final String mimeType = dataItem.getMimeType(); 436 final AccountType accountType = rawContact.getAccountType(this); 437 final DataKind dataKind = AccountTypeManager.getInstance(this) 438 .getKindOrFallback(accountType, mimeType); 439 440 // Skip this data item if MIME-type excluded 441 if (isMimeExcluded(mimeType)) continue; 442 443 final long dataId = dataItem.getId(); 444 final boolean isPrimary = dataItem.isPrimary(); 445 final boolean isSuperPrimary = dataItem.isSuperPrimary(); 446 447 if (dataKind != null) { 448 // Build an action for this data entry, find a mapping to a UI 449 // element, build its summary from the cursor, and collect it 450 // along with all others of this MIME-type. 451 final Action action = new DataAction(context, dataItem, dataKind); 452 final boolean wasAdded = considerAdd(action, cache, isSuperPrimary); 453 if (wasAdded) { 454 // Remember the default 455 if (isSuperPrimary || (isPrimary && (mDefaultsMap.get(mimeType) == null))) { 456 mDefaultsMap.put(mimeType, action); 457 } 458 } 459 } 460 461 // Handle Email rows with presence data as Im entry 462 final DataStatus status = data.getStatuses().get(dataId); 463 if (status != null && dataItem instanceof EmailDataItem) { 464 final EmailDataItem email = (EmailDataItem) dataItem; 465 final ImDataItem im = ImDataItem.createFromEmail(email); 466 if (dataKind != null) { 467 final DataAction action = new DataAction(context, im, dataKind); 468 action.setPresence(status.getPresence()); 469 considerAdd(action, cache, isSuperPrimary); 470 } 471 } 472 } 473 } 474 475 mStopWatch.lap("e"); // Entities inflated 476 477 // Collapse Action Lists (remove e.g. duplicate e-mail addresses from different sources) 478 for (List<Action> actionChildren : mActions.values()) { 479 Collapser.collapseList(actionChildren); 480 } 481 482 mStopWatch.lap("c"); // List collapsed 483 484 setHeaderNameText(R.id.name, data.getDisplayName()); 485 486 // All the mime-types to add. 487 final Set<String> containedTypes = new HashSet<String>(mActions.keySet()); 488 mSortedActionMimeTypes.clear(); 489 // First, add LEADING_MIMETYPES, which are most common. 490 for (String mimeType : LEADING_MIMETYPES) { 491 if (containedTypes.contains(mimeType)) { 492 mSortedActionMimeTypes.add(mimeType); 493 containedTypes.remove(mimeType); 494 } 495 } 496 497 // Add all the remaining ones that are not TRAILING 498 for (String mimeType : containedTypes.toArray(new String[containedTypes.size()])) { 499 if (!TRAILING_MIMETYPES.contains(mimeType)) { 500 mSortedActionMimeTypes.add(mimeType); 501 containedTypes.remove(mimeType); 502 } 503 } 504 505 // Then, add TRAILING_MIMETYPES, which are least common. 506 for (String mimeType : TRAILING_MIMETYPES) { 507 if (containedTypes.contains(mimeType)) { 508 containedTypes.remove(mimeType); 509 mSortedActionMimeTypes.add(mimeType); 510 } 511 } 512 mPagerAdapter.notifyDataSetChanged(); 513 514 mStopWatch.lap("mt"); // Mime types initialized 515 516 // Add buttons for each mimetype 517 mTrack.removeAllViews(); 518 for (String mimeType : mSortedActionMimeTypes) { 519 final View actionView = inflateAction(mimeType, cache, mTrack, data.getDisplayName()); 520 mTrack.addView(actionView); 521 } 522 523 mStopWatch.lap("mt"); // Buttons added 524 525 final boolean hasData = !mSortedActionMimeTypes.isEmpty(); 526 mTrackScroller.setVisibility(hasData ? View.VISIBLE : View.GONE); 527 mSelectedTabRectangle.setVisibility(hasData ? View.VISIBLE : View.GONE); 528 mLineAfterTrack.setVisibility(hasData ? View.VISIBLE : View.GONE); 529 mListPager.setVisibility(hasData ? View.VISIBLE : View.GONE); 530 } 531 532 /** 533 * Consider adding the given {@link Action}, which will only happen if 534 * {@link PackageManager} finds an application to handle 535 * {@link Action#getIntent()}. 536 * @param action the action to handle 537 * @param resolveCache cache of applications that can handle actions 538 * @param front indicates whether to add the action to the front of the list 539 * @return true if action has been added 540 */ 541 private boolean considerAdd(Action action, ResolveCache resolveCache, boolean front) { 542 if (resolveCache.hasResolve(action)) { 543 mActions.put(action.getMimeType(), action, front); 544 return true; 545 } 546 return false; 547 } 548 549 /** 550 * Bind the correct image resource and click handlers to the header views 551 * 552 * @param canAdd Whether or not the user can directly add information in this quick contact 553 * to their local contacts 554 */ 555 private void configureHeaderClickActions(boolean canAdd) { 556 if (canAdd) { 557 mOpenDetailsOrAddContactImage.setImageResource(R.drawable.ic_add_contact_holo_dark); 558 mOpenDetailsOrAddContactImage.setOnClickListener(mAddToContactsClickHandler); 559 mPhotoView.setOnClickListener(mAddToContactsClickHandler); 560 } else { 561 mOpenDetailsOrAddContactImage.setImageResource(R.drawable.ic_contacts_holo_dark); 562 mOpenDetailsOrAddContactImage.setOnClickListener(mOpenDetailsClickHandler); 563 mPhotoView.setOnClickListener(mOpenDetailsClickHandler); 564 } 565 } 566 567 /** 568 * Inflate the in-track view for the action of the given MIME-type, collapsing duplicate values. 569 * Will use the icon provided by the {@link DataKind}. 570 */ 571 private View inflateAction(String mimeType, ResolveCache resolveCache, 572 ViewGroup root, String name) { 573 final CheckableImageView typeView = (CheckableImageView) getLayoutInflater().inflate( 574 R.layout.quickcontact_track_button, root, false); 575 576 List<Action> children = mActions.get(mimeType); 577 typeView.setTag(mimeType); 578 final Action firstInfo = children.get(0); 579 580 // Set icon and listen for clicks 581 final CharSequence descrip = resolveCache.getDescription(firstInfo, name); 582 final Drawable icon = resolveCache.getIcon(firstInfo); 583 typeView.setChecked(false); 584 typeView.setContentDescription(descrip); 585 typeView.setImageDrawable(icon); 586 typeView.setOnClickListener(mTypeViewClickListener); 587 588 return typeView; 589 } 590 591 private CheckableImageView getActionViewAt(int position) { 592 return (CheckableImageView) mTrack.getChildAt(position); 593 } 594 595 @Override 596 public void onAttachFragment(Fragment fragment) { 597 final QuickContactListFragment listFragment = (QuickContactListFragment) fragment; 598 listFragment.setListener(mListFragmentListener); 599 } 600 601 private LoaderCallbacks<Contact> mLoaderCallbacks = 602 new LoaderCallbacks<Contact>() { 603 @Override 604 public void onLoaderReset(Loader<Contact> loader) { 605 } 606 607 @Override 608 public void onLoadFinished(Loader<Contact> loader, Contact data) { 609 mStopWatch.lap("lf"); // onLoadFinished 610 if (isFinishing()) { 611 close(false); 612 return; 613 } 614 if (data.isError()) { 615 // This shouldn't ever happen, so throw an exception. The {@link ContactLoader} 616 // should log the actual exception. 617 throw new IllegalStateException("Failed to load contact", data.getException()); 618 } 619 if (data.isNotFound()) { 620 Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri()); 621 Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage, 622 Toast.LENGTH_LONG).show(); 623 close(false); 624 return; 625 } 626 627 bindData(data); 628 629 mStopWatch.lap("bd"); // bindData finished 630 631 if (TRACE_LAUNCH) android.os.Debug.stopMethodTracing(); 632 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { 633 Log.d(Constants.PERFORMANCE_TAG, "QuickContact shown"); 634 } 635 636 // Data bound and ready, pull curtain to show. Put this on the Handler to ensure 637 // that the layout passes are completed 638 SchedulingUtils.doAfterLayout(mFloatingLayout, new Runnable() { 639 @Override 640 public void run() { 641 mFloatingLayout.showContent(new Runnable() { 642 @Override 643 public void run() { 644 mContactLoader.upgradeToFullContact(); 645 } 646 }); 647 } 648 }); 649 mStopWatch.stopAndLog(TAG, 0); 650 mStopWatch = StopWatch.getNullStopWatch(); // We're done with it. 651 } 652 653 @Override 654 public Loader<Contact> onCreateLoader(int id, Bundle args) { 655 if (mLookupUri == null) { 656 Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early"); 657 } 658 return new ContactLoader(getApplicationContext(), mLookupUri, 659 false /*loadGroupMetaData*/, false /*loadInvitableAccountTypes*/, 660 false /*postViewNotification*/, true /*computeFormattedPhoneNumber*/); 661 } 662 }; 663 664 /** A type (e.g. Call/Addresses was clicked) */ 665 private final OnClickListener mTypeViewClickListener = new OnClickListener() { 666 @Override 667 public void onClick(View view) { 668 final CheckableImageView actionView = (CheckableImageView)view; 669 final String mimeType = (String) actionView.getTag(); 670 int index = mSortedActionMimeTypes.indexOf(mimeType); 671 mListPager.setCurrentItem(index, true); 672 } 673 }; 674 675 private class ViewPagerAdapter extends FragmentPagerAdapter { 676 public ViewPagerAdapter(FragmentManager fragmentManager) { 677 super(fragmentManager); 678 } 679 680 @Override 681 public Fragment getItem(int position) { 682 final String mimeType = mSortedActionMimeTypes.get(position); 683 QuickContactListFragment fragment = new QuickContactListFragment(mimeType); 684 final List<Action> actions = mActions.get(mimeType); 685 fragment.setActions(actions); 686 return fragment; 687 } 688 689 @Override 690 public int getCount() { 691 return mSortedActionMimeTypes.size(); 692 } 693 694 @Override 695 public int getItemPosition(Object object) { 696 final QuickContactListFragment fragment = (QuickContactListFragment) object; 697 final String mimeType = fragment.getMimeType(); 698 for (int i = 0; i < mSortedActionMimeTypes.size(); i++) { 699 if (mimeType.equals(mSortedActionMimeTypes.get(i))) { 700 return i; 701 } 702 } 703 return PagerAdapter.POSITION_NONE; 704 } 705 } 706 707 private class PageChangeListener extends SimpleOnPageChangeListener { 708 private int mScrollingState = ViewPager.SCROLL_STATE_IDLE; 709 710 @Override 711 public void onPageSelected(int position) { 712 final CheckableImageView actionView = getActionViewAt(position); 713 mTrackScroller.requestChildRectangleOnScreen(actionView, 714 new Rect(0, 0, actionView.getWidth(), actionView.getHeight()), false); 715 // Don't render rectangle if we are currently scrolling to prevent it from flickering 716 if (mScrollingState == ViewPager.SCROLL_STATE_IDLE) { 717 renderSelectedRectangle(position, 0); 718 } 719 } 720 721 @Override 722 public void onPageScrollStateChanged(int state) { 723 super.onPageScrollStateChanged(state); 724 mScrollingState = state; 725 } 726 727 @Override 728 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 729 renderSelectedRectangle(position, positionOffset); 730 } 731 732 private void renderSelectedRectangle(int position, float positionOffset) { 733 final RelativeLayout.LayoutParams layoutParams = 734 (RelativeLayout.LayoutParams) mSelectedTabRectangle.getLayoutParams(); 735 final int width = layoutParams.width; 736 layoutParams.setMarginStart((int) ((position + positionOffset) * width)); 737 mSelectedTabRectangle.setLayoutParams(layoutParams); 738 } 739 } 740 741 private final QuickContactListFragment.Listener mListFragmentListener = 742 new QuickContactListFragment.Listener() { 743 @Override 744 public void onOutsideClick() { 745 // If there is no background, we want to dismiss, because to the user it seems 746 // like he had touched outside. If the ViewPager is solid however, those taps 747 // must be ignored 748 final boolean isTransparent = mListPager.getBackground() == null; 749 if (isTransparent) handleOutsideTouch(); 750 } 751 752 @Override 753 public void onItemClicked(final Action action, final boolean alternate) { 754 final Runnable startAppRunnable = new Runnable() { 755 @Override 756 public void run() { 757 try { 758 startActivity(alternate ? action.getAlternateIntent() : action.getIntent()); 759 } catch (ActivityNotFoundException e) { 760 Toast.makeText(QuickContactActivity.this, R.string.quickcontact_missing_app, 761 Toast.LENGTH_SHORT).show(); 762 } 763 764 close(false); 765 } 766 }; 767 // Defer the action to make the window properly repaint 768 new Handler().post(startAppRunnable); 769 } 770 }; 771 } 772