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