1 /* 2 * Copyright (C) 2010 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 package com.android.contacts.common.list; 17 18 import android.app.ActivityManager; 19 import android.content.ContentResolver; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.res.Resources; 23 import android.database.Cursor; 24 import android.graphics.Bitmap; 25 import android.graphics.BitmapFactory; 26 import android.graphics.Canvas; 27 import android.graphics.Paint; 28 import android.graphics.Paint.FontMetricsInt; 29 import android.graphics.Rect; 30 import android.graphics.drawable.BitmapDrawable; 31 import android.graphics.drawable.Drawable; 32 import android.net.Uri; 33 import android.os.AsyncTask; 34 import android.provider.ContactsContract; 35 import android.provider.ContactsContract.CommonDataKinds.Phone; 36 import android.provider.ContactsContract.CommonDataKinds.Photo; 37 import android.provider.ContactsContract.Contacts; 38 import android.provider.ContactsContract.Data; 39 import android.support.v4.graphics.drawable.RoundedBitmapDrawable; 40 import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory; 41 import android.telecom.PhoneAccount; 42 import android.text.TextPaint; 43 import android.text.TextUtils; 44 import android.text.TextUtils.TruncateAt; 45 46 import com.android.contacts.common.ContactsUtils; 47 import com.android.contacts.common.ContactPhotoManager; 48 import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; 49 import com.android.contacts.common.R; 50 51 /** 52 * Constructs shortcut intents. 53 */ 54 public class ShortcutIntentBuilder { 55 56 private static final String[] CONTACT_COLUMNS = { 57 Contacts.DISPLAY_NAME, 58 Contacts.PHOTO_ID, 59 Contacts.LOOKUP_KEY 60 }; 61 62 private static final int CONTACT_DISPLAY_NAME_COLUMN_INDEX = 0; 63 private static final int CONTACT_PHOTO_ID_COLUMN_INDEX = 1; 64 private static final int CONTACT_LOOKUP_KEY_COLUMN_INDEX = 2; 65 66 private static final String[] PHONE_COLUMNS = { 67 Phone.DISPLAY_NAME, 68 Phone.PHOTO_ID, 69 Phone.NUMBER, 70 Phone.TYPE, 71 Phone.LABEL, 72 Phone.LOOKUP_KEY 73 }; 74 75 private static final int PHONE_DISPLAY_NAME_COLUMN_INDEX = 0; 76 private static final int PHONE_PHOTO_ID_COLUMN_INDEX = 1; 77 private static final int PHONE_NUMBER_COLUMN_INDEX = 2; 78 private static final int PHONE_TYPE_COLUMN_INDEX = 3; 79 private static final int PHONE_LABEL_COLUMN_INDEX = 4; 80 private static final int PHONE_LOOKUP_KEY_COLUMN_INDEX = 5; 81 82 private static final String[] PHOTO_COLUMNS = { 83 Photo.PHOTO, 84 }; 85 86 private static final int PHOTO_PHOTO_COLUMN_INDEX = 0; 87 88 private static final String PHOTO_SELECTION = Photo._ID + "=?"; 89 90 private final OnShortcutIntentCreatedListener mListener; 91 private final Context mContext; 92 private int mIconSize; 93 private final int mIconDensity; 94 private final int mOverlayTextBackgroundColor; 95 private final Resources mResources; 96 97 /** 98 * This is a hidden API of the launcher in JellyBean that allows us to disable the animation 99 * that it would usually do, because it interferes with our own animation for QuickContact. 100 * This is needed since some versions of the launcher override the intent flags and therefore 101 * ignore Intent.FLAG_ACTIVITY_NO_ANIMATION. 102 */ 103 public static final String INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION = 104 "com.android.launcher.intent.extra.shortcut.INGORE_LAUNCH_ANIMATION"; 105 106 /** 107 * Listener interface. 108 */ 109 public interface OnShortcutIntentCreatedListener { 110 111 /** 112 * Callback for shortcut intent creation. 113 * 114 * @param uri the original URI for which the shortcut intent has been 115 * created. 116 * @param shortcutIntent resulting shortcut intent. 117 */ 118 void onShortcutIntentCreated(Uri uri, Intent shortcutIntent); 119 } 120 121 public ShortcutIntentBuilder(Context context, OnShortcutIntentCreatedListener listener) { 122 mContext = context; 123 mListener = listener; 124 125 mResources = context.getResources(); 126 final ActivityManager am = (ActivityManager) context 127 .getSystemService(Context.ACTIVITY_SERVICE); 128 mIconSize = mResources.getDimensionPixelSize(R.dimen.shortcut_icon_size); 129 if (mIconSize == 0) { 130 mIconSize = am.getLauncherLargeIconSize(); 131 } 132 mIconDensity = am.getLauncherLargeIconDensity(); 133 mOverlayTextBackgroundColor = mResources.getColor(R.color.shortcut_overlay_text_background); 134 } 135 136 public void createContactShortcutIntent(Uri contactUri) { 137 new ContactLoadingAsyncTask(contactUri).execute(); 138 } 139 140 public void createPhoneNumberShortcutIntent(Uri dataUri, String shortcutAction) { 141 new PhoneNumberLoadingAsyncTask(dataUri, shortcutAction).execute(); 142 } 143 144 /** 145 * An asynchronous task that loads name, photo and other data from the database. 146 */ 147 private abstract class LoadingAsyncTask extends AsyncTask<Void, Void, Void> { 148 protected Uri mUri; 149 protected String mContentType; 150 protected String mDisplayName; 151 protected String mLookupKey; 152 protected byte[] mBitmapData; 153 protected long mPhotoId; 154 155 public LoadingAsyncTask(Uri uri) { 156 mUri = uri; 157 } 158 159 @Override 160 protected Void doInBackground(Void... params) { 161 mContentType = mContext.getContentResolver().getType(mUri); 162 loadData(); 163 loadPhoto(); 164 return null; 165 } 166 167 protected abstract void loadData(); 168 169 private void loadPhoto() { 170 if (mPhotoId == 0) { 171 return; 172 } 173 174 ContentResolver resolver = mContext.getContentResolver(); 175 Cursor cursor = resolver.query(Data.CONTENT_URI, PHOTO_COLUMNS, PHOTO_SELECTION, 176 new String[] { String.valueOf(mPhotoId) }, null); 177 if (cursor != null) { 178 try { 179 if (cursor.moveToFirst()) { 180 mBitmapData = cursor.getBlob(PHOTO_PHOTO_COLUMN_INDEX); 181 } 182 } finally { 183 cursor.close(); 184 } 185 } 186 } 187 } 188 189 private final class ContactLoadingAsyncTask extends LoadingAsyncTask { 190 public ContactLoadingAsyncTask(Uri uri) { 191 super(uri); 192 } 193 194 @Override 195 protected void loadData() { 196 ContentResolver resolver = mContext.getContentResolver(); 197 Cursor cursor = resolver.query(mUri, CONTACT_COLUMNS, null, null, null); 198 if (cursor != null) { 199 try { 200 if (cursor.moveToFirst()) { 201 mDisplayName = cursor.getString(CONTACT_DISPLAY_NAME_COLUMN_INDEX); 202 mPhotoId = cursor.getLong(CONTACT_PHOTO_ID_COLUMN_INDEX); 203 mLookupKey = cursor.getString(CONTACT_LOOKUP_KEY_COLUMN_INDEX); 204 } 205 } finally { 206 cursor.close(); 207 } 208 } 209 } 210 @Override 211 protected void onPostExecute(Void result) { 212 createContactShortcutIntent(mUri, mContentType, mDisplayName, mLookupKey, mBitmapData); 213 } 214 } 215 216 private final class PhoneNumberLoadingAsyncTask extends LoadingAsyncTask { 217 private final String mShortcutAction; 218 private String mPhoneNumber; 219 private int mPhoneType; 220 private String mPhoneLabel; 221 222 public PhoneNumberLoadingAsyncTask(Uri uri, String shortcutAction) { 223 super(uri); 224 mShortcutAction = shortcutAction; 225 } 226 227 @Override 228 protected void loadData() { 229 ContentResolver resolver = mContext.getContentResolver(); 230 Cursor cursor = resolver.query(mUri, PHONE_COLUMNS, null, null, null); 231 if (cursor != null) { 232 try { 233 if (cursor.moveToFirst()) { 234 mDisplayName = cursor.getString(PHONE_DISPLAY_NAME_COLUMN_INDEX); 235 mPhotoId = cursor.getLong(PHONE_PHOTO_ID_COLUMN_INDEX); 236 mPhoneNumber = cursor.getString(PHONE_NUMBER_COLUMN_INDEX); 237 mPhoneType = cursor.getInt(PHONE_TYPE_COLUMN_INDEX); 238 mPhoneLabel = cursor.getString(PHONE_LABEL_COLUMN_INDEX); 239 mLookupKey = cursor.getString(PHONE_LOOKUP_KEY_COLUMN_INDEX); 240 } 241 } finally { 242 cursor.close(); 243 } 244 } 245 } 246 247 @Override 248 protected void onPostExecute(Void result) { 249 createPhoneNumberShortcutIntent(mUri, mDisplayName, mLookupKey, mBitmapData, 250 mPhoneNumber, mPhoneType, mPhoneLabel, mShortcutAction); 251 } 252 } 253 254 private Drawable getPhotoDrawable(byte[] bitmapData, String displayName, String lookupKey) { 255 if (bitmapData != null) { 256 Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length, null); 257 return new BitmapDrawable(mContext.getResources(), bitmap); 258 } else { 259 return ContactPhotoManager.getDefaultAvatarDrawableForContact(mContext.getResources(), 260 false, new DefaultImageRequest(displayName, lookupKey, false)); 261 } 262 } 263 264 private void createContactShortcutIntent(Uri contactUri, String contentType, String displayName, 265 String lookupKey, byte[] bitmapData) { 266 Drawable drawable = getPhotoDrawable(bitmapData, displayName, lookupKey); 267 268 // Use an implicit intent without a package name set. It is reasonable for a disambiguation 269 // dialog to appear when opening QuickContacts from the launcher. Plus, this will be more 270 // resistant to future package name changes done to Contacts. 271 Intent shortcutIntent = new Intent(ContactsContract.QuickContact.ACTION_QUICK_CONTACT); 272 273 // When starting from the launcher, start in a new, cleared task. 274 // CLEAR_WHEN_TASK_RESET cannot reset the root of a task, so we 275 // clear the whole thing preemptively here since QuickContactActivity will 276 // finish itself when launching other detail activities. We need to use 277 // Intent.FLAG_ACTIVITY_NO_ANIMATION since not all versions of launcher will respect 278 // the INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION intent extra. 279 shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK 280 | Intent.FLAG_ACTIVITY_NO_ANIMATION); 281 282 // Tell the launcher to not do its animation, because we are doing our own 283 shortcutIntent.putExtra(INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION, true); 284 285 shortcutIntent.setDataAndType(contactUri, contentType); 286 shortcutIntent.putExtra(ContactsContract.QuickContact.EXTRA_EXCLUDE_MIMES, 287 (String[]) null); 288 289 final Bitmap icon = generateQuickContactIcon(drawable); 290 291 Intent intent = new Intent(); 292 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon); 293 intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); 294 if (TextUtils.isEmpty(displayName)) { 295 intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, mContext.getResources().getString( 296 R.string.missing_name)); 297 } else { 298 intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, displayName); 299 } 300 301 mListener.onShortcutIntentCreated(contactUri, intent); 302 } 303 304 private void createPhoneNumberShortcutIntent(Uri uri, String displayName, String lookupKey, 305 byte[] bitmapData, String phoneNumber, int phoneType, String phoneLabel, 306 String shortcutAction) { 307 Drawable drawable = getPhotoDrawable(bitmapData, displayName, lookupKey); 308 309 Bitmap bitmap; 310 Uri phoneUri; 311 if (Intent.ACTION_CALL.equals(shortcutAction)) { 312 // Make the URI a direct tel: URI so that it will always continue to work 313 phoneUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, phoneNumber, null); 314 bitmap = generatePhoneNumberIcon(drawable, phoneType, phoneLabel, 315 R.drawable.ic_call); 316 } else { 317 phoneUri = Uri.fromParts(ContactsUtils.SCHEME_SMSTO, phoneNumber, null); 318 bitmap = generatePhoneNumberIcon(drawable, phoneType, phoneLabel, 319 R.drawable.ic_message_24dp_mirrored); 320 } 321 322 Intent shortcutIntent = new Intent(shortcutAction, phoneUri); 323 shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 324 325 Intent intent = new Intent(); 326 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, bitmap); 327 intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); 328 329 if (TextUtils.isEmpty(displayName)) { 330 displayName = mContext.getResources().getString(R.string.missing_name); 331 } 332 if (TextUtils.equals(shortcutAction, Intent.ACTION_CALL)) { 333 intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, 334 mContext.getResources().getString(R.string.call_by_shortcut, displayName)); 335 } else if (TextUtils.equals(shortcutAction, Intent.ACTION_SENDTO)) { 336 intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, 337 mContext.getResources().getString(R.string.sms_by_shortcut, displayName)); 338 } 339 340 mListener.onShortcutIntentCreated(uri, intent); 341 } 342 343 private Bitmap generateQuickContactIcon(Drawable photo) { 344 345 // Setup the drawing classes 346 Bitmap bitmap = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888); 347 Canvas canvas = new Canvas(bitmap); 348 349 // Copy in the photo 350 Rect dst = new Rect(0,0, mIconSize, mIconSize); 351 photo.setBounds(dst); 352 photo.draw(canvas); 353 354 // Draw the icon with a rounded border 355 RoundedBitmapDrawable roundedDrawable = 356 RoundedBitmapDrawableFactory.create(mResources, bitmap); 357 roundedDrawable.setAntiAlias(true); 358 roundedDrawable.setCornerRadius(mIconSize / 2); 359 Bitmap roundedBitmap = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888); 360 canvas.setBitmap(roundedBitmap); 361 roundedDrawable.setBounds(dst); 362 roundedDrawable.draw(canvas); 363 canvas.setBitmap(null); 364 365 return roundedBitmap; 366 } 367 368 /** 369 * Generates a phone number shortcut icon. Adds an overlay describing the type of the phone 370 * number, and if there is a photo also adds the call action icon. 371 */ 372 private Bitmap generatePhoneNumberIcon(Drawable photo, int phoneType, String phoneLabel, 373 int actionResId) { 374 final Resources r = mContext.getResources(); 375 final float density = r.getDisplayMetrics().density; 376 377 Bitmap phoneIcon = ((BitmapDrawable) r.getDrawableForDensity(actionResId, mIconDensity)) 378 .getBitmap(); 379 380 Bitmap icon = generateQuickContactIcon(photo); 381 Canvas canvas = new Canvas(icon); 382 383 // Copy in the photo 384 Paint photoPaint = new Paint(); 385 photoPaint.setDither(true); 386 photoPaint.setFilterBitmap(true); 387 Rect dst = new Rect(0, 0, mIconSize, mIconSize); 388 389 // Create an overlay for the phone number type 390 CharSequence overlay = Phone.getTypeLabel(r, phoneType, phoneLabel); 391 392 if (overlay != null) { 393 TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); 394 textPaint.setTextSize(r.getDimension(R.dimen.shortcut_overlay_text_size)); 395 textPaint.setColor(r.getColor(R.color.textColorIconOverlay)); 396 textPaint.setShadowLayer(4f, 0, 2f, r.getColor(R.color.textColorIconOverlayShadow)); 397 398 final FontMetricsInt fmi = textPaint.getFontMetricsInt(); 399 400 // First fill in a darker background around the text to be drawn 401 final Paint workPaint = new Paint(); 402 workPaint.setColor(mOverlayTextBackgroundColor); 403 workPaint.setStyle(Paint.Style.FILL); 404 final int textPadding = r 405 .getDimensionPixelOffset(R.dimen.shortcut_overlay_text_background_padding); 406 final int textBandHeight = (fmi.descent - fmi.ascent) + textPadding * 2; 407 dst.set(0, mIconSize - textBandHeight, mIconSize, mIconSize); 408 canvas.drawRect(dst, workPaint); 409 410 overlay = TextUtils.ellipsize(overlay, textPaint, mIconSize, TruncateAt.END); 411 final float textWidth = textPaint.measureText(overlay, 0, overlay.length()); 412 canvas.drawText(overlay, 0, overlay.length(), (mIconSize - textWidth) / 2, mIconSize 413 - fmi.descent - textPadding, textPaint); 414 } 415 416 // Draw the phone action icon as an overlay 417 Rect src = new Rect(0, 0, phoneIcon.getWidth(), phoneIcon.getHeight()); 418 int iconWidth = icon.getWidth(); 419 dst.set(iconWidth - ((int) (20 * density)), -1, 420 iconWidth, ((int) (19 * density))); 421 canvas.drawBitmap(phoneIcon, src, dst, photoPaint); 422 423 canvas.setBitmap(null); 424 425 return icon; 426 } 427 } 428