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