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