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.ui; 18 19 import com.android.contacts.Collapser; 20 import com.android.contacts.ContactPresenceIconUtil; 21 import com.android.contacts.ContactsUtils; 22 import com.android.contacts.R; 23 import com.android.contacts.StickyTabs; 24 import com.android.contacts.model.ContactsSource; 25 import com.android.contacts.model.Sources; 26 import com.android.contacts.model.ContactsSource.DataKind; 27 import com.android.contacts.ui.widget.CheckableImageView; 28 import com.android.contacts.util.Constants; 29 import com.android.contacts.util.DataStatus; 30 import com.android.contacts.util.NotifyingAsyncQueryHandler; 31 import com.android.internal.policy.PolicyManager; 32 import com.google.android.collect.Sets; 33 34 import android.content.ActivityNotFoundException; 35 import android.content.ContentUris; 36 import android.content.ContentValues; 37 import android.content.Context; 38 import android.content.Intent; 39 import android.content.IntentFilter; 40 import android.content.pm.ApplicationInfo; 41 import android.content.pm.PackageManager; 42 import android.content.pm.ResolveInfo; 43 import android.content.res.Resources; 44 import android.database.Cursor; 45 import android.graphics.Bitmap; 46 import android.graphics.BitmapFactory; 47 import android.graphics.Rect; 48 import android.graphics.drawable.Drawable; 49 import android.net.Uri; 50 import android.provider.ContactsContract.Contacts; 51 import android.provider.ContactsContract.Data; 52 import android.provider.ContactsContract.QuickContact; 53 import android.provider.ContactsContract.RawContacts; 54 import android.provider.ContactsContract.StatusUpdates; 55 import android.provider.ContactsContract.CommonDataKinds.Email; 56 import android.provider.ContactsContract.CommonDataKinds.Im; 57 import android.provider.ContactsContract.CommonDataKinds.Phone; 58 import android.provider.ContactsContract.CommonDataKinds.Photo; 59 import android.provider.ContactsContract.CommonDataKinds.SipAddress; 60 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 61 import android.provider.ContactsContract.CommonDataKinds.Website; 62 import android.text.TextUtils; 63 import android.util.AttributeSet; 64 import android.util.Log; 65 import android.view.ContextThemeWrapper; 66 import android.view.Gravity; 67 import android.view.KeyEvent; 68 import android.view.LayoutInflater; 69 import android.view.Menu; 70 import android.view.MenuItem; 71 import android.view.MotionEvent; 72 import android.view.View; 73 import android.view.ViewGroup; 74 import android.view.ViewStub; 75 import android.view.Window; 76 import android.view.WindowManager; 77 import android.view.ViewTreeObserver.OnGlobalLayoutListener; 78 import android.view.accessibility.AccessibilityEvent; 79 import android.view.animation.Animation; 80 import android.view.animation.AnimationUtils; 81 import android.view.animation.Interpolator; 82 import android.widget.AbsListView; 83 import android.widget.AdapterView; 84 import android.widget.BaseAdapter; 85 import android.widget.CheckBox; 86 import android.widget.CompoundButton; 87 import android.widget.HorizontalScrollView; 88 import android.widget.ImageView; 89 import android.widget.ListView; 90 import android.widget.RelativeLayout; 91 import android.widget.TextView; 92 import android.widget.Toast; 93 94 import java.lang.ref.SoftReference; 95 import java.util.ArrayList; 96 import java.util.Arrays; 97 import java.util.HashMap; 98 import java.util.HashSet; 99 import java.util.LinkedList; 100 import java.util.List; 101 import java.util.Set; 102 103 /** 104 * Window that shows QuickContact dialog for a specific {@link Contacts#_ID}. 105 */ 106 public class QuickContactWindow implements Window.Callback, 107 NotifyingAsyncQueryHandler.AsyncQueryListener, View.OnClickListener, 108 AbsListView.OnItemClickListener, CompoundButton.OnCheckedChangeListener, KeyEvent.Callback, 109 OnGlobalLayoutListener { 110 private static final String TAG = "QuickContactWindow"; 111 112 /** 113 * Interface used to allow the person showing a {@link QuickContactWindow} to 114 * know when the window has been dismissed. 115 */ 116 public interface OnDismissListener { 117 public void onDismiss(QuickContactWindow dialog); 118 } 119 120 /** 121 * Custom layout the sole purpose of which is to intercept the BACK key and 122 * close QC even when the soft keyboard is open. 123 */ 124 public static class RootLayout extends RelativeLayout { 125 126 QuickContactWindow mQuickContactWindow; 127 128 public RootLayout(Context context, AttributeSet attrs) { 129 super(context, attrs); 130 } 131 132 /** 133 * Intercepts the BACK key event and dismisses QuickContact window. 134 */ 135 @Override 136 public boolean dispatchKeyEventPreIme(KeyEvent event) { 137 if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { 138 mQuickContactWindow.onBackPressed(); 139 return true; 140 } else { 141 return super.dispatchKeyEventPreIme(event); 142 } 143 } 144 } 145 146 private final Context mContext; 147 private final LayoutInflater mInflater; 148 private final WindowManager mWindowManager; 149 private Window mWindow; 150 private View mDecor; 151 private final Rect mRect = new Rect(); 152 153 private boolean mDismissed = false; 154 private boolean mQuerying = false; 155 private boolean mShowing = false; 156 157 private NotifyingAsyncQueryHandler mHandler; 158 private OnDismissListener mDismissListener; 159 private ResolveCache mResolveCache; 160 161 /** Last selected tab of the Dialtacs-Activity. This is -1 if not called out of contacts app */ 162 private int mLastSelectedContactsAppTab; 163 164 private Uri mLookupUri; 165 private Rect mAnchor; 166 167 private int mShadowHoriz; 168 private int mShadowVert; 169 private int mShadowTouch; 170 171 private int mScreenWidth; 172 private int mScreenHeight; 173 private int mRequestedY; 174 175 private boolean mHasValidSocial = false; 176 private boolean mHasData = false; 177 private boolean mMakePrimary = false; 178 179 private ImageView mArrowUp; 180 private ImageView mArrowDown; 181 182 private int mMode; 183 private RootLayout mRootView; 184 private View mHeader; 185 private HorizontalScrollView mTrackScroll; 186 private ViewGroup mTrack; 187 private Animation mTrackAnim; 188 189 private View mFooter; 190 private View mFooterDisambig; 191 private ListView mResolveList; 192 private CheckableImageView mLastAction; 193 private CheckBox mSetPrimaryCheckBox; 194 195 private int mWindowRecycled = 0; 196 private int mActionRecycled = 0; 197 198 /** 199 * Set of {@link Action} that are associated with the aggregate currently 200 * displayed by this dialog, represented as a map from {@link String} 201 * MIME-type to {@link ActionList}. 202 */ 203 private ActionMap mActions = new ActionMap(); 204 205 /** 206 * Pool of unused {@link CheckableImageView} that have previously been 207 * inflated, and are ready to be recycled through {@link #obtainView()}. 208 */ 209 private LinkedList<View> mActionPool = new LinkedList<View>(); 210 211 private String[] mExcludeMimes; 212 213 /** 214 * {@link #PRECEDING_MIMETYPES} and {@link #FOLLOWING_MIMETYPES} are used to sort MIME-types. 215 * 216 * <p>The MIME-types in {@link #PRECEDING_MIMETYPES} appear in the front of the dialog, 217 * in the order in the array. 218 * 219 * <p>The ones in {@link #FOLLOWING_MIMETYPES} appear in the end of the dialog, in alphabetical 220 * order. 221 * 222 * <p>The rest go between them, in the order in the array. 223 */ 224 private static final String[] PRECEDING_MIMETYPES = new String[] { 225 Phone.CONTENT_ITEM_TYPE, 226 SipAddress.CONTENT_ITEM_TYPE, 227 Contacts.CONTENT_ITEM_TYPE, 228 Constants.MIME_SMS_ADDRESS, 229 Email.CONTENT_ITEM_TYPE, 230 }; 231 232 /** 233 * See {@link #PRECEDING_MIMETYPES}. 234 */ 235 private static final String[] FOLLOWING_MIMETYPES = new String[] { 236 StructuredPostal.CONTENT_ITEM_TYPE, 237 Website.CONTENT_ITEM_TYPE, 238 }; 239 240 /** 241 * Specific list {@link ApplicationInfo#packageName} of apps that are 242 * prefered <strong>only</strong> for the purposes of default icons when 243 * multiple {@link ResolveInfo} are found to match. This only happens when 244 * the user has not selected a default app yet, and they will still be 245 * presented with the system disambiguation dialog. 246 */ 247 private static final HashSet<String> sPreferResolve = Sets.newHashSet( 248 "com.android.email", 249 "com.android.calendar", 250 "com.android.contacts", 251 "com.android.mms", 252 "com.android.phone", 253 "com.android.browser"); 254 255 private static final int TOKEN_DATA = 1; 256 257 static final boolean LOGD = false; 258 259 static final boolean TRACE_LAUNCH = false; 260 static final String TRACE_TAG = "quickcontact"; 261 262 /** 263 * Prepare a dialog to show in the given {@link Context}. 264 */ 265 public QuickContactWindow(Context context) { 266 mContext = new ContextThemeWrapper(context, R.style.QuickContact); 267 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 268 mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); 269 270 mWindow = PolicyManager.makeNewWindow(mContext); 271 mWindow.setCallback(this); 272 mWindow.setWindowManager(mWindowManager, null, null); 273 mWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED); 274 275 mWindow.setContentView(R.layout.quickcontact); 276 277 mRootView = (RootLayout)mWindow.findViewById(R.id.root); 278 mRootView.mQuickContactWindow = this; 279 mRootView.setFocusable(true); 280 mRootView.setFocusableInTouchMode(true); 281 mRootView.setDescendantFocusability(RootLayout.FOCUS_AFTER_DESCENDANTS); 282 283 mArrowUp = (ImageView)mWindow.findViewById(R.id.arrow_up); 284 mArrowDown = (ImageView)mWindow.findViewById(R.id.arrow_down); 285 286 mResolveCache = new ResolveCache(mContext); 287 288 final Resources res = mContext.getResources(); 289 mShadowHoriz = res.getDimensionPixelSize(R.dimen.quickcontact_shadow_horiz); 290 mShadowVert = res.getDimensionPixelSize(R.dimen.quickcontact_shadow_vert); 291 mShadowTouch = res.getDimensionPixelSize(R.dimen.quickcontact_shadow_touch); 292 293 mScreenWidth = mWindowManager.getDefaultDisplay().getWidth(); 294 mScreenHeight = mWindowManager.getDefaultDisplay().getHeight(); 295 296 mTrack = (ViewGroup)mWindow.findViewById(R.id.quickcontact); 297 mTrackScroll = (HorizontalScrollView)mWindow.findViewById(R.id.scroll); 298 299 mFooter = mWindow.findViewById(R.id.footer); 300 mFooterDisambig = mWindow.findViewById(R.id.footer_disambig); 301 mResolveList = (ListView)mWindow.findViewById(android.R.id.list); 302 mSetPrimaryCheckBox = (CheckBox)mWindow.findViewById(android.R.id.checkbox); 303 304 mSetPrimaryCheckBox.setOnCheckedChangeListener(this); 305 306 // Prepare track entrance animation 307 mTrackAnim = AnimationUtils.loadAnimation(mContext, R.anim.quickcontact); 308 mTrackAnim.setInterpolator(new Interpolator() { 309 public float getInterpolation(float t) { 310 // Pushes past the target area, then snaps back into place. 311 // Equation for graphing: 1.2-((x*1.6)-1.1)^2 312 final float inner = (t * 1.55f) - 1.1f; 313 return 1.2f - inner * inner; 314 } 315 }); 316 317 mHandler = new NotifyingAsyncQueryHandler(mContext, this); 318 } 319 320 /** 321 * Prepare a dialog to show in the given {@link Context}, and notify the 322 * given {@link OnDismissListener} each time this dialog is dismissed. 323 */ 324 public QuickContactWindow(Context context, OnDismissListener dismissListener) { 325 this(context); 326 mDismissListener = dismissListener; 327 } 328 329 public void setLastSelectedContactsAppTab(int value) { 330 mLastSelectedContactsAppTab = value; 331 } 332 333 private View getHeaderView(int mode) { 334 View header = null; 335 switch (mode) { 336 case QuickContact.MODE_SMALL: 337 header = mWindow.findViewById(R.id.header_small); 338 break; 339 case QuickContact.MODE_MEDIUM: 340 header = mWindow.findViewById(R.id.header_medium); 341 break; 342 case QuickContact.MODE_LARGE: 343 header = mWindow.findViewById(R.id.header_large); 344 break; 345 } 346 347 if (header instanceof ViewStub) { 348 // Inflate actual header if we picked a stub 349 final ViewStub stub = (ViewStub)header; 350 header = stub.inflate(); 351 } else if (header != null) { 352 header.setVisibility(View.VISIBLE); 353 } 354 355 return header; 356 } 357 358 /** 359 * Start showing a dialog for the given {@link Contacts#_ID} pointing 360 * towards the given location. 361 */ 362 public synchronized void show(Uri lookupUri, Rect anchor, int mode, String[] excludeMimes) { 363 if (mQuerying || mShowing) { 364 Log.w(TAG, "dismissing before showing"); 365 dismissInternal(); 366 } 367 368 if (TRACE_LAUNCH && !android.os.Debug.isMethodTracingActive()) { 369 android.os.Debug.startMethodTracing(TRACE_TAG); 370 } 371 372 // Validate incoming parameters 373 final boolean validMode = (mode == QuickContact.MODE_SMALL 374 || mode == QuickContact.MODE_MEDIUM || mode == QuickContact.MODE_LARGE); 375 if (!validMode) { 376 throw new IllegalArgumentException("Invalid mode, expecting MODE_LARGE, " 377 + "MODE_MEDIUM, or MODE_SMALL"); 378 } 379 380 if (anchor == null) { 381 throw new IllegalArgumentException("Missing anchor rectangle"); 382 } 383 384 // Prepare header view for requested mode 385 mLookupUri = lookupUri; 386 mAnchor = new Rect(anchor); 387 mMode = mode; 388 mExcludeMimes = excludeMimes; 389 390 mHeader = getHeaderView(mode); 391 392 setHeaderText(R.id.name, R.string.quickcontact_missing_name); 393 394 setHeaderText(R.id.status, null); 395 setHeaderText(R.id.timestamp, null); 396 397 setHeaderImage(R.id.presence, null); 398 399 resetTrack(); 400 401 // We need to have a focused view inside the QuickContact window so 402 // that the BACK key event can be intercepted 403 mRootView.requestFocus(); 404 405 mHasValidSocial = false; 406 mDismissed = false; 407 mQuerying = true; 408 409 // Start background query for data, but only select photo rows when they 410 // directly match the super-primary PHOTO_ID. 411 final Uri dataUri = getDataUri(lookupUri); 412 mHandler.cancelOperation(TOKEN_DATA); 413 414 // Only request photo data when required by mode 415 if (mMode == QuickContact.MODE_LARGE) { 416 // Select photos, but only super-primary 417 mHandler.startQuery(TOKEN_DATA, lookupUri, dataUri, DataQuery.PROJECTION, Data.MIMETYPE 418 + "!=? OR (" + Data.MIMETYPE + "=? AND " + Data._ID + "=" + Contacts.PHOTO_ID 419 + ")", new String[] { Photo.CONTENT_ITEM_TYPE, Photo.CONTENT_ITEM_TYPE }, null); 420 } else { 421 // Exclude all photos from cursor 422 mHandler.startQuery(TOKEN_DATA, lookupUri, dataUri, DataQuery.PROJECTION, Data.MIMETYPE 423 + "!=?", new String[] { Photo.CONTENT_ITEM_TYPE }, null); 424 } 425 } 426 427 /** 428 * Build a {@link Uri} into the {@link Data} table for the requested 429 * {@link Contacts#CONTENT_LOOKUP_URI} style {@link Uri}. 430 */ 431 private Uri getDataUri(Uri lookupUri) { 432 // TODO: Formalize method of extracting LOOKUP_KEY 433 final List<String> path = lookupUri.getPathSegments(); 434 final boolean validLookup = path.size() >= 3 && "lookup".equals(path.get(1)); 435 if (!validLookup) { 436 // We only accept valid lookup-style Uris 437 throw new IllegalArgumentException("Expecting lookup-style Uri"); 438 } else if (path.size() == 3) { 439 // No direct _ID provided, so force a lookup 440 lookupUri = Contacts.lookupContact(mContext.getContentResolver(), lookupUri); 441 } 442 443 final long contactId = ContentUris.parseId(lookupUri); 444 return Uri.withAppendedPath(ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId), 445 Contacts.Data.CONTENT_DIRECTORY); 446 } 447 448 /** 449 * Show the correct call-out arrow based on a {@link R.id} reference. 450 */ 451 private void showArrow(int whichArrow, int requestedX) { 452 final View showArrow = (whichArrow == R.id.arrow_up) ? mArrowUp : mArrowDown; 453 final View hideArrow = (whichArrow == R.id.arrow_up) ? mArrowDown : mArrowUp; 454 455 final int arrowWidth = mArrowUp.getMeasuredWidth(); 456 457 showArrow.setVisibility(View.VISIBLE); 458 ViewGroup.MarginLayoutParams param = (ViewGroup.MarginLayoutParams)showArrow.getLayoutParams(); 459 param.leftMargin = requestedX - arrowWidth / 2; 460 461 hideArrow.setVisibility(View.INVISIBLE); 462 } 463 464 /** 465 * Actual internal method to show this dialog. Called only by 466 * {@link #considerShowing()} when all data requirements have been met. 467 */ 468 private void showInternal() { 469 mDecor = mWindow.getDecorView(); 470 mDecor.getViewTreeObserver().addOnGlobalLayoutListener(this); 471 WindowManager.LayoutParams l = mWindow.getAttributes(); 472 473 l.width = mScreenWidth + mShadowHoriz + mShadowHoriz; 474 l.height = WindowManager.LayoutParams.WRAP_CONTENT; 475 476 // Force layout measuring pass so we have baseline numbers 477 mDecor.measure(l.width, l.height); 478 final int blockHeight = mDecor.getMeasuredHeight(); 479 480 l.gravity = Gravity.TOP | Gravity.LEFT; 481 l.x = -mShadowHoriz; 482 483 if (mAnchor.top > blockHeight) { 484 // Show downwards callout when enough room, aligning bottom block 485 // edge with top of anchor area, and adjusting to inset arrow. 486 showArrow(R.id.arrow_down, mAnchor.centerX()); 487 l.y = mAnchor.top - blockHeight + mShadowVert; 488 l.windowAnimations = R.style.QuickContactAboveAnimation; 489 490 } else { 491 // Otherwise show upwards callout, aligning block top with bottom of 492 // anchor area, and adjusting to inset arrow. 493 showArrow(R.id.arrow_up, mAnchor.centerX()); 494 l.y = mAnchor.bottom - mShadowVert; 495 l.windowAnimations = R.style.QuickContactBelowAnimation; 496 497 } 498 499 l.flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 500 | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; 501 502 mRequestedY = l.y; 503 mWindowManager.addView(mDecor, l); 504 mShowing = true; 505 mQuerying = false; 506 mDismissed = false; 507 508 mTrack.startAnimation(mTrackAnim); 509 510 if (TRACE_LAUNCH) { 511 android.os.Debug.stopMethodTracing(); 512 Log.d(TAG, "Window recycled " + mWindowRecycled + " times, chiclets " 513 + mActionRecycled + " times"); 514 } 515 } 516 517 /** {@inheritDoc} */ 518 public void onGlobalLayout() { 519 layoutInScreen(); 520 } 521 522 /** 523 * Adjust vertical {@link WindowManager.LayoutParams} to fit window as best 524 * as possible, shifting up to display content as needed. 525 */ 526 private void layoutInScreen() { 527 if (!mShowing) return; 528 529 final WindowManager.LayoutParams l = mWindow.getAttributes(); 530 final int originalY = l.y; 531 532 final int blockHeight = mDecor.getHeight(); 533 534 l.y = mRequestedY; 535 if (mRequestedY + blockHeight > mScreenHeight) { 536 // Shift up from bottom when overflowing 537 l.y = mScreenHeight - blockHeight; 538 } 539 540 if (originalY != l.y) { 541 // Only update when value is changed 542 mWindow.setAttributes(l); 543 } 544 } 545 546 /** 547 * Dismiss this dialog if showing. 548 */ 549 public synchronized void dismiss() { 550 // Notify any listeners that we've been dismissed 551 if (mDismissListener != null) { 552 mDismissListener.onDismiss(this); 553 } 554 555 dismissInternal(); 556 } 557 558 private void dismissInternal() { 559 // Remove any attached window decor for recycling 560 boolean hadDecor = mDecor != null; 561 if (hadDecor) { 562 mWindowManager.removeView(mDecor); 563 mWindowRecycled++; 564 mDecor.getViewTreeObserver().removeGlobalOnLayoutListener(this); 565 mDecor = null; 566 mWindow.closeAllPanels(); 567 } 568 mShowing = false; 569 mDismissed = true; 570 571 // Cancel any pending queries 572 mHandler.cancelOperation(TOKEN_DATA); 573 mQuerying = false; 574 575 // Completely hide header and reset track 576 mHeader.setVisibility(View.GONE); 577 resetTrack(); 578 } 579 580 /** 581 * Reset track to initial state, recycling any chiclets. 582 */ 583 private void resetTrack() { 584 // Release reference to last chiclet 585 mLastAction = null; 586 587 // Clear track actions and scroll to hard left 588 mResolveCache.clear(); 589 mActions.clear(); 590 591 // Recycle any chiclets in use 592 while (mTrack.getChildCount() > 2) { 593 this.releaseView(mTrack.getChildAt(1)); 594 mTrack.removeViewAt(1); 595 } 596 597 mTrackScroll.fullScroll(View.FOCUS_LEFT); 598 mWasDownArrow = false; 599 600 // Clear any primary requests 601 mMakePrimary = false; 602 mSetPrimaryCheckBox.setChecked(false); 603 604 setResolveVisible(false, null); 605 } 606 607 /** 608 * Consider showing this window, which will only call through to 609 * {@link #showInternal()} when all data items are present. 610 */ 611 private void considerShowing() { 612 if (mHasData && !mShowing && !mDismissed) { 613 if (mMode == QuickContact.MODE_MEDIUM && !mHasValidSocial) { 614 // Missing valid social, swap medium for small header 615 mHeader.setVisibility(View.GONE); 616 mHeader = getHeaderView(QuickContact.MODE_SMALL); 617 } 618 619 // All queries have returned, pull curtain 620 showInternal(); 621 } 622 } 623 624 /** {@inheritDoc} */ 625 public synchronized void onQueryComplete(int token, Object cookie, Cursor cursor) { 626 // Bail early when query is stale 627 if (cookie != mLookupUri) return; 628 629 if (cursor == null) { 630 // Problem while running query, so bail without showing 631 Log.w(TAG, "Missing cursor for token=" + token); 632 this.dismiss(); 633 return; 634 } 635 636 handleData(cursor); 637 mHasData = true; 638 639 if (!cursor.isClosed()) { 640 cursor.close(); 641 } 642 643 considerShowing(); 644 } 645 646 /** Assign this string to the view, if found in {@link #mHeader}. */ 647 private void setHeaderText(int id, int resId) { 648 setHeaderText(id, mContext.getResources().getText(resId)); 649 } 650 651 /** Assign this string to the view, if found in {@link #mHeader}. */ 652 private void setHeaderText(int id, CharSequence value) { 653 final View view = mHeader.findViewById(id); 654 if (view instanceof TextView) { 655 ((TextView)view).setText(value); 656 view.setVisibility(TextUtils.isEmpty(value) ? View.GONE : View.VISIBLE); 657 } 658 } 659 660 /** Assign this image to the view, if found in {@link #mHeader}. */ 661 private void setHeaderImage(int id, Drawable drawable) { 662 final View view = mHeader.findViewById(id); 663 if (view instanceof ImageView) { 664 ((ImageView)view).setImageDrawable(drawable); 665 view.setVisibility(drawable == null ? View.GONE : View.VISIBLE); 666 } 667 } 668 669 /** 670 * Find the QuickContact-specific presence icon for showing in chiclets. 671 */ 672 private Drawable getTrackPresenceIcon(int status) { 673 int resId; 674 switch (status) { 675 case StatusUpdates.AVAILABLE: 676 resId = R.drawable.quickcontact_slider_presence_active; 677 break; 678 case StatusUpdates.IDLE: 679 case StatusUpdates.AWAY: 680 resId = R.drawable.quickcontact_slider_presence_away; 681 break; 682 case StatusUpdates.DO_NOT_DISTURB: 683 resId = R.drawable.quickcontact_slider_presence_busy; 684 break; 685 case StatusUpdates.INVISIBLE: 686 resId = R.drawable.quickcontact_slider_presence_inactive; 687 break; 688 case StatusUpdates.OFFLINE: 689 default: 690 resId = R.drawable.quickcontact_slider_presence_inactive; 691 } 692 return mContext.getResources().getDrawable(resId); 693 } 694 695 /** Read {@link String} from the given {@link Cursor}. */ 696 private static String getAsString(Cursor cursor, String columnName) { 697 final int index = cursor.getColumnIndex(columnName); 698 return cursor.getString(index); 699 } 700 701 /** Read {@link Integer} from the given {@link Cursor}. */ 702 private static int getAsInt(Cursor cursor, String columnName) { 703 final int index = cursor.getColumnIndex(columnName); 704 return cursor.getInt(index); 705 } 706 707 /** 708 * Abstract definition of an action that could be performed, along with 709 * string description and icon. 710 */ 711 private interface Action extends Collapser.Collapsible<Action> { 712 public CharSequence getHeader(); 713 public CharSequence getBody(); 714 715 public String getMimeType(); 716 public Drawable getFallbackIcon(); 717 718 /** 719 * Build an {@link Intent} that will perform this action. 720 */ 721 public Intent getIntent(); 722 723 /** 724 * Checks if the contact data for this action is primary. 725 */ 726 public Boolean isPrimary(); 727 728 /** 729 * Returns a lookup (@link Uri) for the contact data item. 730 */ 731 public Uri getDataUri(); 732 } 733 734 /** 735 * Description of a specific {@link Data#_ID} item, with style information 736 * defined by a {@link DataKind}. 737 */ 738 private static class DataAction implements Action { 739 private final Context mContext; 740 private final DataKind mKind; 741 private final String mMimeType; 742 743 private CharSequence mHeader; 744 private CharSequence mBody; 745 private Intent mIntent; 746 747 private boolean mAlternate; 748 private Uri mDataUri; 749 private boolean mIsPrimary; 750 751 /** 752 * Create an action from common {@link Data} elements. 753 */ 754 public DataAction(Context context, String mimeType, DataKind kind, 755 long dataId, Cursor cursor) { 756 mContext = context; 757 mKind = kind; 758 mMimeType = mimeType; 759 760 // Inflate strings from cursor 761 mAlternate = Constants.MIME_SMS_ADDRESS.equals(mimeType); 762 if (mAlternate && mKind.actionAltHeader != null) { 763 mHeader = mKind.actionAltHeader.inflateUsing(context, cursor); 764 } else if (mKind.actionHeader != null) { 765 mHeader = mKind.actionHeader.inflateUsing(context, cursor); 766 } 767 768 if (getAsInt(cursor, Data.IS_SUPER_PRIMARY) != 0) { 769 mIsPrimary = true; 770 } 771 772 if (mKind.actionBody != null) { 773 mBody = mKind.actionBody.inflateUsing(context, cursor); 774 } 775 776 mDataUri = ContentUris.withAppendedId(Data.CONTENT_URI, dataId); 777 778 // Handle well-known MIME-types with special care 779 if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) { 780 final String number = getAsString(cursor, Phone.NUMBER); 781 if (!TextUtils.isEmpty(number)) { 782 final Uri callUri = Uri.fromParts(Constants.SCHEME_TEL, number, null); 783 mIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, callUri); 784 } 785 } else if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType)) { 786 final String address = getAsString(cursor, SipAddress.SIP_ADDRESS); 787 if (!TextUtils.isEmpty(address)) { 788 final Uri callUri = Uri.fromParts(Constants.SCHEME_SIP, address, null); 789 mIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, callUri); 790 // Note that this item will get a SIP-specific variant 791 // of the "call phone" icon, rather than the standard 792 // app icon for the Phone app (which we show for 793 // regular phone numbers.) That's because the phone 794 // app explicitly specifies an android:icon attribute 795 // for the SIP-related intent-filters in its manifest. 796 } 797 } else if (Constants.MIME_SMS_ADDRESS.equals(mimeType)) { 798 final String number = getAsString(cursor, Phone.NUMBER); 799 if (!TextUtils.isEmpty(number)) { 800 final Uri smsUri = Uri.fromParts(Constants.SCHEME_SMSTO, number, null); 801 mIntent = new Intent(Intent.ACTION_SENDTO, smsUri); 802 } 803 804 } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) { 805 final String address = getAsString(cursor, Email.DATA); 806 if (!TextUtils.isEmpty(address)) { 807 final Uri mailUri = Uri.fromParts(Constants.SCHEME_MAILTO, address, null); 808 mIntent = new Intent(Intent.ACTION_SENDTO, mailUri); 809 } 810 811 } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType)) { 812 final String url = getAsString(cursor, Website.URL); 813 if (!TextUtils.isEmpty(url)) { 814 mIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); 815 } 816 817 } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType)) { 818 final boolean isEmail = Email.CONTENT_ITEM_TYPE.equals( 819 getAsString(cursor, Data.MIMETYPE)); 820 if (isEmail || isProtocolValid(cursor)) { 821 final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : 822 getAsInt(cursor, Im.PROTOCOL); 823 824 if (isEmail) { 825 // Use Google Talk string when using Email, and clear data 826 // Uri so we don't try saving Email as primary. 827 mHeader = context.getText(R.string.chat_gtalk); 828 mDataUri = null; 829 } 830 831 String host = getAsString(cursor, Im.CUSTOM_PROTOCOL); 832 String data = getAsString(cursor, isEmail ? Email.DATA : Im.DATA); 833 if (protocol != Im.PROTOCOL_CUSTOM) { 834 // Try bringing in a well-known host for specific protocols 835 host = ContactsUtils.lookupProviderNameFromId(protocol); 836 } 837 838 if (!TextUtils.isEmpty(host) && !TextUtils.isEmpty(data)) { 839 final String authority = host.toLowerCase(); 840 final Uri imUri = new Uri.Builder().scheme(Constants.SCHEME_IMTO).authority( 841 authority).appendPath(data).build(); 842 mIntent = new Intent(Intent.ACTION_SENDTO, imUri); 843 } 844 } 845 } 846 847 if (mIntent == null) { 848 // Otherwise fall back to default VIEW action 849 mIntent = new Intent(Intent.ACTION_VIEW, mDataUri); 850 } 851 852 // Always launch as new task, since we're like a launcher 853 mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); 854 } 855 856 private boolean isProtocolValid(Cursor cursor) { 857 final int columnIndex = cursor.getColumnIndex(Im.PROTOCOL); 858 if (cursor.isNull(columnIndex)) { 859 return false; 860 } 861 try { 862 Integer.valueOf(cursor.getString(columnIndex)); 863 } catch (NumberFormatException e) { 864 return false; 865 } 866 return true; 867 } 868 869 /** {@inheritDoc} */ 870 public CharSequence getHeader() { 871 return mHeader; 872 } 873 874 /** {@inheritDoc} */ 875 public CharSequence getBody() { 876 return mBody; 877 } 878 879 /** {@inheritDoc} */ 880 public String getMimeType() { 881 return mMimeType; 882 } 883 884 /** {@inheritDoc} */ 885 public Uri getDataUri() { 886 return mDataUri; 887 } 888 889 /** {@inheritDoc} */ 890 public Boolean isPrimary() { 891 return mIsPrimary; 892 } 893 894 /** {@inheritDoc} */ 895 public Drawable getFallbackIcon() { 896 // Bail early if no valid resources 897 final String resPackageName = mKind.resPackageName; 898 if (resPackageName == null) return null; 899 900 final PackageManager pm = mContext.getPackageManager(); 901 if (mAlternate && mKind.iconAltRes != -1) { 902 return pm.getDrawable(resPackageName, mKind.iconAltRes, null); 903 } else if (mKind.iconRes != -1) { 904 return pm.getDrawable(resPackageName, mKind.iconRes, null); 905 } else { 906 return null; 907 } 908 } 909 910 /** {@inheritDoc} */ 911 public Intent getIntent() { 912 return mIntent; 913 } 914 915 /** {@inheritDoc} */ 916 public boolean collapseWith(Action other) { 917 if (!shouldCollapseWith(other)) { 918 return false; 919 } 920 return true; 921 } 922 923 /** {@inheritDoc} */ 924 public boolean shouldCollapseWith(Action t) { 925 if (t == null) { 926 return false; 927 } 928 if (!(t instanceof DataAction)) { 929 Log.e(TAG, "t must be DataAction"); 930 return false; 931 } 932 DataAction other = (DataAction)t; 933 if (!ContactsUtils.areObjectsEqual(mKind, other.mKind)) { 934 return false; 935 } 936 if (!ContactsUtils.shouldCollapse(mContext, mMimeType, mBody, other.mMimeType, 937 other.mBody)) { 938 return false; 939 } 940 if (!TextUtils.equals(mMimeType, other.mMimeType) 941 || !ContactsUtils.areIntentActionEqual(mIntent, other.mIntent) 942 ) { 943 return false; 944 } 945 return true; 946 } 947 } 948 949 /** 950 * Specific action that launches the profile card. 951 */ 952 private static class ProfileAction implements Action { 953 private final Context mContext; 954 private final Uri mLookupUri; 955 956 public ProfileAction(Context context, Uri lookupUri) { 957 mContext = context; 958 mLookupUri = lookupUri; 959 } 960 961 /** {@inheritDoc} */ 962 public CharSequence getHeader() { 963 return null; 964 } 965 966 /** {@inheritDoc} */ 967 public CharSequence getBody() { 968 return null; 969 } 970 971 /** {@inheritDoc} */ 972 public String getMimeType() { 973 return Contacts.CONTENT_ITEM_TYPE; 974 } 975 976 /** {@inheritDoc} */ 977 public Drawable getFallbackIcon() { 978 return mContext.getResources().getDrawable(R.drawable.ic_contacts_details); 979 } 980 981 /** {@inheritDoc} */ 982 public Intent getIntent() { 983 final Intent intent = new Intent(Intent.ACTION_VIEW, mLookupUri); 984 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); 985 return intent; 986 } 987 988 /** {@inheritDoc} */ 989 public Boolean isPrimary() { 990 return null; 991 } 992 993 /** {@inheritDoc} */ 994 public Uri getDataUri() { 995 return null; 996 } 997 998 /** {@inheritDoc} */ 999 public boolean collapseWith(Action t) { 1000 return false; // Never dup. 1001 } 1002 1003 /** {@inheritDoc} */ 1004 public boolean shouldCollapseWith(Action t) { 1005 return false; // Never dup. 1006 } 1007 } 1008 1009 /** 1010 * Internally hold a cache of scaled icons based on {@link PackageManager} 1011 * queries, keyed internally on MIME-type. 1012 */ 1013 private static class ResolveCache { 1014 private PackageManager mPackageManager; 1015 1016 /** 1017 * Cached entry holding the best {@link ResolveInfo} for a specific 1018 * MIME-type, along with a {@link SoftReference} to its icon. 1019 */ 1020 private static class Entry { 1021 public ResolveInfo bestResolve; 1022 public SoftReference<Drawable> icon; 1023 } 1024 1025 private HashMap<String, Entry> mCache = new HashMap<String, Entry>(); 1026 1027 public ResolveCache(Context context) { 1028 mPackageManager = context.getPackageManager(); 1029 } 1030 1031 /** 1032 * Get the {@link Entry} best associated with the given {@link Action}, 1033 * or create and populate a new one if it doesn't exist. 1034 */ 1035 protected Entry getEntry(Action action) { 1036 final String mimeType = action.getMimeType(); 1037 Entry entry = mCache.get(mimeType); 1038 if (entry != null) return entry; 1039 entry = new Entry(); 1040 1041 final Intent intent = action.getIntent(); 1042 if (intent != null) { 1043 final List<ResolveInfo> matches = mPackageManager.queryIntentActivities(intent, 1044 PackageManager.MATCH_DEFAULT_ONLY); 1045 1046 // Pick first match, otherwise best found 1047 ResolveInfo bestResolve = null; 1048 final int size = matches.size(); 1049 if (size == 1) { 1050 bestResolve = matches.get(0); 1051 } else if (size > 1) { 1052 bestResolve = getBestResolve(intent, matches); 1053 } 1054 1055 if (bestResolve != null) { 1056 final Drawable icon = bestResolve.loadIcon(mPackageManager); 1057 1058 entry.bestResolve = bestResolve; 1059 entry.icon = new SoftReference<Drawable>(icon); 1060 } 1061 } 1062 1063 mCache.put(mimeType, entry); 1064 return entry; 1065 } 1066 1067 /** 1068 * Best {@link ResolveInfo} when multiple found. Ties are broken by 1069 * selecting first from the {QuickContactWindow#sPreferResolve} list of 1070 * preferred packages, second by apps that live on the system partition, 1071 * otherwise the app from the top of the list. This is 1072 * <strong>only</strong> used for selecting a default icon for 1073 * displaying in the track, and does not shortcut the system 1074 * {@link Intent} disambiguation dialog. 1075 */ 1076 protected ResolveInfo getBestResolve(Intent intent, List<ResolveInfo> matches) { 1077 // Try finding preferred activity, otherwise detect disambig 1078 final ResolveInfo foundResolve = mPackageManager.resolveActivity(intent, 1079 PackageManager.MATCH_DEFAULT_ONLY); 1080 final boolean foundDisambig = (foundResolve.match & 1081 IntentFilter.MATCH_CATEGORY_MASK) == 0; 1082 1083 if (!foundDisambig) { 1084 // Found concrete match, so return directly 1085 return foundResolve; 1086 } 1087 1088 // Accept any package from prefer list, otherwise first system app 1089 ResolveInfo firstSystem = null; 1090 for (ResolveInfo info : matches) { 1091 final boolean isSystem = (info.activityInfo.applicationInfo.flags 1092 & ApplicationInfo.FLAG_SYSTEM) != 0; 1093 final boolean isPrefer = QuickContactWindow.sPreferResolve 1094 .contains(info.activityInfo.applicationInfo.packageName); 1095 1096 1097 1098 if (isPrefer) return info; 1099 if (isSystem && firstSystem != null) firstSystem = info; 1100 } 1101 1102 // Return first system found, otherwise first from list 1103 return firstSystem != null ? firstSystem : matches.get(0); 1104 } 1105 1106 /** 1107 * Check {@link PackageManager} to see if any apps offer to handle the 1108 * given {@link Action}. 1109 */ 1110 public boolean hasResolve(Action action) { 1111 return getEntry(action).bestResolve != null; 1112 } 1113 1114 /** 1115 * Find the best description for the given {@link Action}, usually used 1116 * for accessibility purposes. 1117 */ 1118 public CharSequence getDescription(Action action) { 1119 final CharSequence actionHeader = action.getHeader(); 1120 final ResolveInfo info = getEntry(action).bestResolve; 1121 if (!TextUtils.isEmpty(actionHeader)) { 1122 return actionHeader; 1123 } else if (info != null) { 1124 return info.loadLabel(mPackageManager); 1125 } else { 1126 return null; 1127 } 1128 } 1129 1130 /** 1131 * Return the best icon for the given {@link Action}, which is usually 1132 * based on the {@link ResolveInfo} found through a 1133 * {@link PackageManager} query. 1134 */ 1135 public Drawable getIcon(Action action) { 1136 final SoftReference<Drawable> iconRef = getEntry(action).icon; 1137 return (iconRef == null) ? null : iconRef.get(); 1138 } 1139 1140 public void clear() { 1141 mCache.clear(); 1142 } 1143 } 1144 1145 /** 1146 * Provide a strongly-typed {@link LinkedList} that holds a list of 1147 * {@link Action} objects. 1148 */ 1149 private class ActionList extends ArrayList<Action> { 1150 } 1151 1152 /** 1153 * Provide a simple way of collecting one or more {@link Action} objects 1154 * under a MIME-type key. 1155 */ 1156 private class ActionMap extends HashMap<String, ActionList> { 1157 private void collect(String mimeType, Action info) { 1158 // Create list for this MIME-type when needed 1159 ActionList collectList = get(mimeType); 1160 if (collectList == null) { 1161 collectList = new ActionList(); 1162 put(mimeType, collectList); 1163 } 1164 collectList.add(info); 1165 } 1166 } 1167 1168 /** 1169 * Check if the given MIME-type appears in the list of excluded MIME-types 1170 * that the most-recent caller requested. 1171 */ 1172 private boolean isMimeExcluded(String mimeType) { 1173 if (mExcludeMimes == null) return false; 1174 for (String excludedMime : mExcludeMimes) { 1175 if (TextUtils.equals(excludedMime, mimeType)) { 1176 return true; 1177 } 1178 } 1179 return false; 1180 } 1181 1182 /** 1183 * Handle the result from the {@link #TOKEN_DATA} query. 1184 */ 1185 private void handleData(Cursor cursor) { 1186 if (cursor == null) return; 1187 1188 if (!isMimeExcluded(Contacts.CONTENT_ITEM_TYPE)) { 1189 // Add the profile shortcut action 1190 final Action action = new ProfileAction(mContext, mLookupUri); 1191 mActions.collect(Contacts.CONTENT_ITEM_TYPE, action); 1192 } 1193 1194 final DataStatus status = new DataStatus(); 1195 final Sources sources = Sources.getInstance(mContext); 1196 final ImageView photoView = (ImageView)mHeader.findViewById(R.id.photo); 1197 1198 Bitmap photoBitmap = null; 1199 while (cursor.moveToNext()) { 1200 final long dataId = cursor.getLong(DataQuery._ID); 1201 final String accountType = cursor.getString(DataQuery.ACCOUNT_TYPE); 1202 final String mimeType = cursor.getString(DataQuery.MIMETYPE); 1203 1204 // Handle any social status updates from this row 1205 status.possibleUpdate(cursor); 1206 1207 // Skip this data item if MIME-type excluded 1208 if (isMimeExcluded(mimeType)) continue; 1209 1210 // Handle photos included as data row 1211 if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) { 1212 final int colPhoto = cursor.getColumnIndex(Photo.PHOTO); 1213 final byte[] photoBlob = cursor.getBlob(colPhoto); 1214 if (photoBlob != null) { 1215 photoBitmap = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length); 1216 } 1217 continue; 1218 } 1219 1220 final DataKind kind = sources.getKindOrFallback(accountType, mimeType, mContext, 1221 ContactsSource.LEVEL_MIMETYPES); 1222 1223 if (kind != null) { 1224 // Build an action for this data entry, find a mapping to a UI 1225 // element, build its summary from the cursor, and collect it 1226 // along with all others of this MIME-type. 1227 final Action action = new DataAction(mContext, mimeType, kind, dataId, cursor); 1228 considerAdd(action, mimeType); 1229 } 1230 1231 // If phone number, also insert as text message action 1232 if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && kind != null) { 1233 final Action action = new DataAction(mContext, Constants.MIME_SMS_ADDRESS, 1234 kind, dataId, cursor); 1235 considerAdd(action, Constants.MIME_SMS_ADDRESS); 1236 } 1237 1238 // Handle Email rows with presence data as Im entry 1239 final boolean hasPresence = !cursor.isNull(DataQuery.PRESENCE); 1240 if (hasPresence && Email.CONTENT_ITEM_TYPE.equals(mimeType)) { 1241 final DataKind imKind = sources.getKindOrFallback(accountType, 1242 Im.CONTENT_ITEM_TYPE, mContext, ContactsSource.LEVEL_MIMETYPES); 1243 if (imKind != null) { 1244 final Action action = new DataAction(mContext, Im.CONTENT_ITEM_TYPE, imKind, 1245 dataId, cursor); 1246 considerAdd(action, Im.CONTENT_ITEM_TYPE); 1247 } 1248 } 1249 } 1250 1251 if (cursor.moveToLast()) { 1252 // Read contact information from last data row 1253 final String name = cursor.getString(DataQuery.DISPLAY_NAME); 1254 final int presence = cursor.getInt(DataQuery.CONTACT_PRESENCE); 1255 final Drawable statusIcon = ContactPresenceIconUtil.getPresenceIcon(mContext, presence); 1256 1257 setHeaderText(R.id.name, name); 1258 setHeaderImage(R.id.presence, statusIcon); 1259 } 1260 1261 if (photoView != null) { 1262 // Place photo when discovered in data, otherwise hide 1263 photoView.setVisibility(photoBitmap != null ? View.VISIBLE : View.GONE); 1264 photoView.setImageBitmap(photoBitmap); 1265 } 1266 1267 mHasValidSocial = status.isValid(); 1268 if (mHasValidSocial && mMode != QuickContact.MODE_SMALL) { 1269 // Update status when valid was found 1270 setHeaderText(R.id.status, status.getStatus()); 1271 setHeaderText(R.id.timestamp, status.getTimestampLabel(mContext)); 1272 } 1273 1274 // Turn our list of actions into UI elements 1275 1276 // Index where we start adding child views. 1277 int index = mTrack.getChildCount() - 1; 1278 1279 // All the mime-types to add. 1280 final Set<String> containedTypes = new HashSet<String>(mActions.keySet()); 1281 1282 // First, add PRECEDING_MIMETYPES, which are most common. 1283 for (String mimeType : PRECEDING_MIMETYPES) { 1284 if (containedTypes.contains(mimeType)) { 1285 mTrack.addView(inflateAction(mimeType), index++); 1286 containedTypes.remove(mimeType); 1287 } 1288 } 1289 1290 // Keep the current index to append non PRECEDING/FOLLOWING items. 1291 final int indexAfterPreceding = index; 1292 1293 // Then, add FOLLOWING_MIMETYPES, which are least common. 1294 for (String mimeType : FOLLOWING_MIMETYPES) { 1295 if (containedTypes.contains(mimeType)) { 1296 mTrack.addView(inflateAction(mimeType), index++); 1297 containedTypes.remove(mimeType); 1298 } 1299 } 1300 1301 // Go back to just after PRECEDING_MIMETYPES, and append the rest. 1302 index = indexAfterPreceding; 1303 final String[] remainingTypes = containedTypes.toArray(new String[containedTypes.size()]); 1304 Arrays.sort(remainingTypes); 1305 for (String mimeType : remainingTypes) { 1306 mTrack.addView(inflateAction(mimeType), index++); 1307 } 1308 } 1309 1310 /** 1311 * Consider adding the given {@link Action}, which will only happen if 1312 * {@link PackageManager} finds an application to handle 1313 * {@link Action#getIntent()}. 1314 */ 1315 private void considerAdd(Action action, String mimeType) { 1316 if (mResolveCache.hasResolve(action)) { 1317 mActions.collect(mimeType, action); 1318 } 1319 } 1320 1321 /** 1322 * Obtain a new {@link CheckableImageView} for a new chiclet, either by 1323 * recycling one from {@link #mActionPool}, or by inflating a new one. When 1324 * finished, use {@link #releaseView(View)} to return back into the pool for 1325 * later recycling. 1326 */ 1327 private synchronized View obtainView() { 1328 View view = mActionPool.poll(); 1329 if (view == null || QuickContactActivity.FORCE_CREATE) { 1330 view = mInflater.inflate(R.layout.quickcontact_item, mTrack, false); 1331 } 1332 return view; 1333 } 1334 1335 /** 1336 * Return the given {@link CheckableImageView} into our internal pool for 1337 * possible recycling during another pass. 1338 */ 1339 private synchronized void releaseView(View view) { 1340 mActionPool.offer(view); 1341 mActionRecycled++; 1342 } 1343 1344 /** 1345 * Inflate the in-track view for the action of the given MIME-type, collapsing duplicate values. 1346 * Will use the icon provided by the {@link DataKind}. 1347 */ 1348 private View inflateAction(String mimeType) { 1349 final CheckableImageView view = (CheckableImageView)obtainView(); 1350 boolean isActionSet = false; 1351 1352 // Add direct intent if single child, otherwise flag for multiple 1353 ActionList children = mActions.get(mimeType); 1354 if (children.size() > 1) { 1355 Collapser.collapseList(children); 1356 } 1357 Action firstInfo = children.get(0); 1358 if (children.size() == 1) { 1359 view.setTag(firstInfo); 1360 } else { 1361 for (Action action : children) { 1362 if (action.isPrimary()) { 1363 view.setTag(action); 1364 isActionSet = true; 1365 break; 1366 } 1367 } 1368 if (!isActionSet) { 1369 view.setTag(children); 1370 } 1371 } 1372 1373 // Set icon and listen for clicks 1374 final CharSequence descrip = mResolveCache.getDescription(firstInfo); 1375 final Drawable icon = mResolveCache.getIcon(firstInfo); 1376 view.setChecked(false); 1377 view.setContentDescription(descrip); 1378 view.setImageDrawable(icon); 1379 view.setOnClickListener(this); 1380 return view; 1381 } 1382 1383 /** {@inheritDoc} */ 1384 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 1385 // Pass list item clicks along so that Intents are handled uniformly 1386 onClick(view); 1387 } 1388 1389 /** 1390 * Flag indicating if {@link #mArrowDown} was visible during the last call 1391 * to {@link #setResolveVisible(boolean, CheckableImageView)}. Used to 1392 * decide during a later call if the arrow should be restored. 1393 */ 1394 private boolean mWasDownArrow = false; 1395 1396 /** 1397 * Helper for showing and hiding {@link #mFooterDisambig}, which will 1398 * correctly manage {@link #mArrowDown} as needed. 1399 */ 1400 private void setResolveVisible(boolean visible, CheckableImageView actionView) { 1401 // Show or hide the resolve list if needed 1402 boolean visibleNow = mFooterDisambig.getVisibility() == View.VISIBLE; 1403 1404 if (mLastAction != null) mLastAction.setChecked(false); 1405 if (actionView != null) actionView.setChecked(true); 1406 mLastAction = actionView; 1407 1408 // Bail early if already in desired state 1409 if (visible == visibleNow) return; 1410 1411 mFooter.setVisibility(visible ? View.GONE : View.VISIBLE); 1412 mFooterDisambig.setVisibility(visible ? View.VISIBLE : View.GONE); 1413 1414 if (visible) { 1415 // If showing list, then hide and save state of down arrow 1416 mWasDownArrow = mWasDownArrow || (mArrowDown.getVisibility() == View.VISIBLE); 1417 mArrowDown.setVisibility(View.INVISIBLE); 1418 } else { 1419 // If hiding list, restore any down arrow state 1420 mArrowDown.setVisibility(mWasDownArrow ? View.VISIBLE : View.INVISIBLE); 1421 } 1422 } 1423 1424 /** {@inheritDoc} */ 1425 public void onClick(View view) { 1426 final boolean isActionView = (view instanceof CheckableImageView); 1427 final CheckableImageView actionView = isActionView ? (CheckableImageView)view : null; 1428 final Object tag = view.getTag(); 1429 if (tag instanceof Action) { 1430 // Incoming tag is concrete intent, so try launching 1431 final Action action = (Action)tag; 1432 final boolean makePrimary = mMakePrimary; 1433 1434 if (Intent.ACTION_CALL_PRIVILEGED.equals(action.getIntent().getAction())) { 1435 StickyTabs.saveTab(mContext, mLastSelectedContactsAppTab); 1436 } 1437 1438 try { 1439 mContext.startActivity(action.getIntent()); 1440 } catch (ActivityNotFoundException e) { 1441 Toast.makeText(mContext, R.string.quickcontact_missing_app, Toast.LENGTH_SHORT) 1442 .show(); 1443 } 1444 1445 // Hide the resolution list, if present 1446 setResolveVisible(false, null); 1447 this.dismiss(); 1448 1449 if (makePrimary) { 1450 ContentValues values = new ContentValues(1); 1451 values.put(Data.IS_SUPER_PRIMARY, 1); 1452 final Uri dataUri = action.getDataUri(); 1453 if (dataUri != null) { 1454 mContext.getContentResolver().update(dataUri, values, null, null); 1455 } 1456 } 1457 } else if (tag instanceof ActionList) { 1458 // Incoming tag is a MIME-type, so show resolution list 1459 final ActionList children = (ActionList)tag; 1460 1461 // Show resolution list and set adapter 1462 setResolveVisible(true, actionView); 1463 1464 mResolveList.setOnItemClickListener(this); 1465 mResolveList.setAdapter(new BaseAdapter() { 1466 public int getCount() { 1467 return children.size(); 1468 } 1469 1470 public Object getItem(int position) { 1471 return children.get(position); 1472 } 1473 1474 public long getItemId(int position) { 1475 return position; 1476 } 1477 1478 public View getView(int position, View convertView, ViewGroup parent) { 1479 if (convertView == null) { 1480 convertView = mInflater.inflate( 1481 R.layout.quickcontact_resolve_item, parent, false); 1482 } 1483 1484 // Set action title based on summary value 1485 final Action action = (Action)getItem(position); 1486 1487 TextView text1 = (TextView)convertView.findViewById(android.R.id.text1); 1488 TextView text2 = (TextView)convertView.findViewById(android.R.id.text2); 1489 1490 text1.setText(action.getHeader()); 1491 text2.setText(action.getBody()); 1492 1493 convertView.setTag(action); 1494 return convertView; 1495 } 1496 }); 1497 1498 // Make sure we resize to make room for ListView 1499 mDecor.forceLayout(); 1500 mDecor.invalidate(); 1501 1502 } 1503 } 1504 1505 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 1506 mMakePrimary = isChecked; 1507 } 1508 1509 private void onBackPressed() { 1510 // Back key will first dismiss any expanded resolve list, otherwise 1511 // it will close the entire dialog. 1512 if (mFooterDisambig.getVisibility() == View.VISIBLE) { 1513 setResolveVisible(false, null); 1514 mDecor.forceLayout(); 1515 mDecor.invalidate(); 1516 } else { 1517 dismiss(); 1518 } 1519 } 1520 1521 /** {@inheritDoc} */ 1522 public boolean dispatchKeyEvent(KeyEvent event) { 1523 if (mWindow.superDispatchKeyEvent(event)) { 1524 return true; 1525 } 1526 return event.dispatch(this, mDecor != null 1527 ? mDecor.getKeyDispatcherState() : null, this); 1528 } 1529 1530 /** {@inheritDoc} */ 1531 public boolean onKeyDown(int keyCode, KeyEvent event) { 1532 if (keyCode == KeyEvent.KEYCODE_BACK) { 1533 event.startTracking(); 1534 return true; 1535 } 1536 1537 return false; 1538 } 1539 1540 /** {@inheritDoc} */ 1541 public boolean onKeyUp(int keyCode, KeyEvent event) { 1542 if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() 1543 && !event.isCanceled()) { 1544 onBackPressed(); 1545 return true; 1546 } 1547 1548 return false; 1549 } 1550 1551 /** {@inheritDoc} */ 1552 public boolean onKeyLongPress(int keyCode, KeyEvent event) { 1553 return false; 1554 } 1555 1556 /** {@inheritDoc} */ 1557 public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) { 1558 return false; 1559 } 1560 1561 /** {@inheritDoc} */ 1562 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 1563 // TODO: make this window accessible 1564 return false; 1565 } 1566 1567 /** 1568 * Detect if the given {@link MotionEvent} is outside the boundaries of this 1569 * window, which usually means we should dismiss. 1570 */ 1571 protected void detectEventOutside(MotionEvent event) { 1572 if (event.getAction() == MotionEvent.ACTION_DOWN && mDecor != null) { 1573 // Only try detecting outside events on down-press 1574 mDecor.getHitRect(mRect); 1575 mRect.top = mRect.top + mShadowTouch; 1576 mRect.bottom = mRect.bottom - mShadowTouch; 1577 final int x = (int)event.getX(); 1578 final int y = (int)event.getY(); 1579 if (!mRect.contains(x, y)) { 1580 event.setAction(MotionEvent.ACTION_OUTSIDE); 1581 } 1582 } 1583 } 1584 1585 /** {@inheritDoc} */ 1586 public boolean dispatchTouchEvent(MotionEvent event) { 1587 detectEventOutside(event); 1588 if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { 1589 dismiss(); 1590 return true; 1591 } else { 1592 return mWindow.superDispatchTouchEvent(event); 1593 } 1594 } 1595 1596 /** {@inheritDoc} */ 1597 public boolean dispatchTrackballEvent(MotionEvent event) { 1598 return mWindow.superDispatchTrackballEvent(event); 1599 } 1600 1601 /** {@inheritDoc} */ 1602 public void onContentChanged() { 1603 } 1604 1605 /** {@inheritDoc} */ 1606 public boolean onCreatePanelMenu(int featureId, Menu menu) { 1607 return false; 1608 } 1609 1610 /** {@inheritDoc} */ 1611 public View onCreatePanelView(int featureId) { 1612 return null; 1613 } 1614 1615 /** {@inheritDoc} */ 1616 public boolean onMenuItemSelected(int featureId, MenuItem item) { 1617 return false; 1618 } 1619 1620 /** {@inheritDoc} */ 1621 public boolean onMenuOpened(int featureId, Menu menu) { 1622 return false; 1623 } 1624 1625 /** {@inheritDoc} */ 1626 public void onPanelClosed(int featureId, Menu menu) { 1627 } 1628 1629 /** {@inheritDoc} */ 1630 public boolean onPreparePanel(int featureId, View view, Menu menu) { 1631 return false; 1632 } 1633 1634 /** {@inheritDoc} */ 1635 public boolean onSearchRequested() { 1636 return false; 1637 } 1638 1639 /** {@inheritDoc} */ 1640 public void onWindowAttributesChanged(android.view.WindowManager.LayoutParams attrs) { 1641 if (mDecor != null) { 1642 mWindowManager.updateViewLayout(mDecor, attrs); 1643 } 1644 } 1645 1646 /** {@inheritDoc} */ 1647 public void onWindowFocusChanged(boolean hasFocus) { 1648 } 1649 1650 /** {@inheritDoc} */ 1651 public void onAttachedToWindow() { 1652 // No actions 1653 } 1654 1655 /** {@inheritDoc} */ 1656 public void onDetachedFromWindow() { 1657 // No actions 1658 } 1659 1660 private interface DataQuery { 1661 final String[] PROJECTION = new String[] { 1662 Data._ID, 1663 1664 RawContacts.ACCOUNT_TYPE, 1665 Contacts.STARRED, 1666 Contacts.DISPLAY_NAME, 1667 Contacts.CONTACT_PRESENCE, 1668 1669 Data.STATUS, 1670 Data.STATUS_RES_PACKAGE, 1671 Data.STATUS_ICON, 1672 Data.STATUS_LABEL, 1673 Data.STATUS_TIMESTAMP, 1674 Data.PRESENCE, 1675 1676 Data.RES_PACKAGE, 1677 Data.MIMETYPE, 1678 Data.IS_PRIMARY, 1679 Data.IS_SUPER_PRIMARY, 1680 Data.RAW_CONTACT_ID, 1681 1682 Data.DATA1, Data.DATA2, Data.DATA3, Data.DATA4, Data.DATA5, 1683 Data.DATA6, Data.DATA7, Data.DATA8, Data.DATA9, Data.DATA10, Data.DATA11, 1684 Data.DATA12, Data.DATA13, Data.DATA14, Data.DATA15, 1685 }; 1686 1687 final int _ID = 0; 1688 1689 final int ACCOUNT_TYPE = 1; 1690 final int STARRED = 2; 1691 final int DISPLAY_NAME = 3; 1692 final int CONTACT_PRESENCE = 4; 1693 1694 final int STATUS = 5; 1695 final int STATUS_RES_PACKAGE = 6; 1696 final int STATUS_ICON = 7; 1697 final int STATUS_LABEL = 8; 1698 final int STATUS_TIMESTAMP = 9; 1699 final int PRESENCE = 10; 1700 1701 final int RES_PACKAGE = 11; 1702 final int MIMETYPE = 12; 1703 final int IS_PRIMARY = 13; 1704 final int IS_SUPER_PRIMARY = 14; 1705 } 1706 } 1707