| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.contacts; |
| |
| import android.app.ActivityManager; |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.ShortcutInfo; |
| import android.content.pm.ShortcutManager; |
| import android.content.res.Resources; |
| import android.database.Cursor; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.graphics.Canvas; |
| import android.graphics.Paint; |
| import android.graphics.Paint.FontMetricsInt; |
| import android.graphics.Rect; |
| import android.graphics.drawable.AdaptiveIconDrawable; |
| import android.graphics.drawable.BitmapDrawable; |
| import android.graphics.drawable.Drawable; |
| import android.graphics.drawable.Icon; |
| import android.net.Uri; |
| import android.os.AsyncTask; |
| import android.provider.ContactsContract.CommonDataKinds.Phone; |
| import android.provider.ContactsContract.CommonDataKinds.Photo; |
| import android.provider.ContactsContract.Contacts; |
| import android.provider.ContactsContract.Data; |
| import androidx.core.graphics.drawable.IconCompat; |
| import androidx.core.graphics.drawable.RoundedBitmapDrawable; |
| import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; |
| import androidx.core.os.BuildCompat; |
| import android.telecom.PhoneAccount; |
| import android.text.TextPaint; |
| import android.text.TextUtils; |
| import android.text.TextUtils.TruncateAt; |
| |
| import com.android.contacts.ContactPhotoManager.DefaultImageRequest; |
| import com.android.contacts.lettertiles.LetterTileDrawable; |
| import com.android.contacts.util.BitmapUtil; |
| import com.android.contacts.util.ImplicitIntentsUtil; |
| |
| /** |
| * Constructs shortcut intents. |
| */ |
| public class ShortcutIntentBuilder { |
| |
| private static final String[] CONTACT_COLUMNS = { |
| Contacts.DISPLAY_NAME, |
| Contacts.PHOTO_ID, |
| Contacts.LOOKUP_KEY |
| }; |
| |
| private static final int CONTACT_DISPLAY_NAME_COLUMN_INDEX = 0; |
| private static final int CONTACT_PHOTO_ID_COLUMN_INDEX = 1; |
| private static final int CONTACT_LOOKUP_KEY_COLUMN_INDEX = 2; |
| |
| private static final String[] PHONE_COLUMNS = { |
| Phone.DISPLAY_NAME, |
| Phone.PHOTO_ID, |
| Phone.NUMBER, |
| Phone.TYPE, |
| Phone.LABEL, |
| Phone.LOOKUP_KEY |
| }; |
| |
| private static final int PHONE_DISPLAY_NAME_COLUMN_INDEX = 0; |
| private static final int PHONE_PHOTO_ID_COLUMN_INDEX = 1; |
| private static final int PHONE_NUMBER_COLUMN_INDEX = 2; |
| private static final int PHONE_TYPE_COLUMN_INDEX = 3; |
| private static final int PHONE_LABEL_COLUMN_INDEX = 4; |
| private static final int PHONE_LOOKUP_KEY_COLUMN_INDEX = 5; |
| |
| private static final String[] PHOTO_COLUMNS = { |
| Photo.PHOTO, |
| }; |
| |
| private static final int PHOTO_PHOTO_COLUMN_INDEX = 0; |
| |
| private static final String PHOTO_SELECTION = Photo._ID + "=?"; |
| |
| private final OnShortcutIntentCreatedListener mListener; |
| private final Context mContext; |
| private int mIconSize; |
| private final int mIconDensity; |
| private final int mOverlayTextBackgroundColor; |
| private final Resources mResources; |
| |
| /** |
| * This is a hidden API of the launcher in JellyBean that allows us to disable the animation |
| * that it would usually do, because it interferes with our own animation for QuickContact. |
| * This is needed since some versions of the launcher override the intent flags and therefore |
| * ignore Intent.FLAG_ACTIVITY_NO_ANIMATION. |
| */ |
| public static final String INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION = |
| "com.android.launcher.intent.extra.shortcut.INGORE_LAUNCH_ANIMATION"; |
| |
| /** |
| * Listener interface. |
| */ |
| public interface OnShortcutIntentCreatedListener { |
| |
| /** |
| * Callback for shortcut intent creation. |
| * |
| * @param uri the original URI for which the shortcut intent has been |
| * created. |
| * @param shortcutIntent resulting shortcut intent. |
| */ |
| void onShortcutIntentCreated(Uri uri, Intent shortcutIntent); |
| } |
| |
| public ShortcutIntentBuilder(Context context, OnShortcutIntentCreatedListener listener) { |
| mContext = context; |
| mListener = listener; |
| |
| mResources = context.getResources(); |
| final ActivityManager am = (ActivityManager) context |
| .getSystemService(Context.ACTIVITY_SERVICE); |
| mIconSize = mResources.getDimensionPixelSize(R.dimen.shortcut_icon_size); |
| if (mIconSize == 0) { |
| mIconSize = am.getLauncherLargeIconSize(); |
| } |
| mIconDensity = am.getLauncherLargeIconDensity(); |
| mOverlayTextBackgroundColor = mResources.getColor(R.color.shortcut_overlay_text_background); |
| } |
| |
| public void createContactShortcutIntent(Uri contactUri) { |
| new ContactLoadingAsyncTask(contactUri).execute(); |
| } |
| |
| public void createPhoneNumberShortcutIntent(Uri dataUri, String shortcutAction) { |
| new PhoneNumberLoadingAsyncTask(dataUri, shortcutAction).execute(); |
| } |
| |
| /** |
| * An asynchronous task that loads name, photo and other data from the database. |
| */ |
| private abstract class LoadingAsyncTask extends AsyncTask<Void, Void, Void> { |
| protected Uri mUri; |
| protected String mContentType; |
| protected String mDisplayName; |
| protected String mLookupKey; |
| protected byte[] mBitmapData; |
| protected long mPhotoId; |
| |
| public LoadingAsyncTask(Uri uri) { |
| mUri = uri; |
| } |
| |
| @Override |
| protected Void doInBackground(Void... params) { |
| mContentType = mContext.getContentResolver().getType(mUri); |
| loadData(); |
| loadPhoto(); |
| return null; |
| } |
| |
| protected abstract void loadData(); |
| |
| private void loadPhoto() { |
| if (mPhotoId == 0) { |
| return; |
| } |
| |
| ContentResolver resolver = mContext.getContentResolver(); |
| Cursor cursor = resolver.query(Data.CONTENT_URI, PHOTO_COLUMNS, PHOTO_SELECTION, |
| new String[] { String.valueOf(mPhotoId) }, null); |
| if (cursor != null) { |
| try { |
| if (cursor.moveToFirst()) { |
| mBitmapData = cursor.getBlob(PHOTO_PHOTO_COLUMN_INDEX); |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| } |
| } |
| |
| private final class ContactLoadingAsyncTask extends LoadingAsyncTask { |
| public ContactLoadingAsyncTask(Uri uri) { |
| super(uri); |
| } |
| |
| @Override |
| protected void loadData() { |
| ContentResolver resolver = mContext.getContentResolver(); |
| Cursor cursor = resolver.query(mUri, CONTACT_COLUMNS, null, null, null); |
| if (cursor != null) { |
| try { |
| if (cursor.moveToFirst()) { |
| mDisplayName = cursor.getString(CONTACT_DISPLAY_NAME_COLUMN_INDEX); |
| mPhotoId = cursor.getLong(CONTACT_PHOTO_ID_COLUMN_INDEX); |
| mLookupKey = cursor.getString(CONTACT_LOOKUP_KEY_COLUMN_INDEX); |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| } |
| @Override |
| protected void onPostExecute(Void result) { |
| createContactShortcutIntent(mUri, mContentType, mDisplayName, mLookupKey, mBitmapData); |
| } |
| } |
| |
| private final class PhoneNumberLoadingAsyncTask extends LoadingAsyncTask { |
| private final String mShortcutAction; |
| private String mPhoneNumber; |
| private int mPhoneType; |
| private String mPhoneLabel; |
| |
| public PhoneNumberLoadingAsyncTask(Uri uri, String shortcutAction) { |
| super(uri); |
| mShortcutAction = shortcutAction; |
| } |
| |
| @Override |
| protected void loadData() { |
| ContentResolver resolver = mContext.getContentResolver(); |
| Cursor cursor = resolver.query(mUri, PHONE_COLUMNS, null, null, null); |
| if (cursor != null) { |
| try { |
| if (cursor.moveToFirst()) { |
| mDisplayName = cursor.getString(PHONE_DISPLAY_NAME_COLUMN_INDEX); |
| mPhotoId = cursor.getLong(PHONE_PHOTO_ID_COLUMN_INDEX); |
| mPhoneNumber = cursor.getString(PHONE_NUMBER_COLUMN_INDEX); |
| mPhoneType = cursor.getInt(PHONE_TYPE_COLUMN_INDEX); |
| mPhoneLabel = cursor.getString(PHONE_LABEL_COLUMN_INDEX); |
| mLookupKey = cursor.getString(PHONE_LOOKUP_KEY_COLUMN_INDEX); |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| } |
| |
| @Override |
| protected void onPostExecute(Void result) { |
| createPhoneNumberShortcutIntent(mUri, mDisplayName, mLookupKey, mBitmapData, |
| mPhoneNumber, mPhoneType, mPhoneLabel, mShortcutAction); |
| } |
| } |
| |
| private Drawable getPhotoDrawable(byte[] bitmapData, String displayName, String lookupKey) { |
| if (bitmapData != null) { |
| Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length, null); |
| return new BitmapDrawable(mContext.getResources(), bitmap); |
| } else { |
| final DefaultImageRequest request = new DefaultImageRequest(displayName, lookupKey, |
| false); |
| if (BuildCompat.isAtLeastO()) { |
| // On O, scale the image down to add the padding needed by AdaptiveIcons. |
| request.scale = LetterTileDrawable.getAdaptiveIconScale(); |
| } |
| return ContactPhotoManager.getDefaultAvatarDrawableForContact(mContext.getResources(), |
| false, request); |
| } |
| } |
| |
| private void createContactShortcutIntent(Uri contactUri, String contentType, String displayName, |
| String lookupKey, byte[] bitmapData) { |
| Intent intent = null; |
| if (TextUtils.isEmpty(displayName)) { |
| displayName = mContext.getResources().getString(R.string.missing_name); |
| } |
| if (BuildCompat.isAtLeastO()) { |
| final long contactId = ContentUris.parseId(contactUri); |
| final ShortcutManager sm = (ShortcutManager) |
| mContext.getSystemService(Context.SHORTCUT_SERVICE); |
| final DynamicShortcuts dynamicShortcuts = new DynamicShortcuts(mContext); |
| final ShortcutInfo shortcutInfo = dynamicShortcuts.getQuickContactShortcutInfo( |
| contactId, lookupKey, displayName); |
| if (shortcutInfo != null) { |
| intent = sm.createShortcutResultIntent(shortcutInfo); |
| } |
| } |
| final Drawable drawable = getPhotoDrawable(bitmapData, displayName, lookupKey); |
| |
| final Intent shortcutIntent = ImplicitIntentsUtil.getIntentForQuickContactLauncherShortcut( |
| mContext, contactUri); |
| |
| intent = intent == null ? new Intent() : intent; |
| |
| final Bitmap icon = generateQuickContactIcon(drawable); |
| if (BuildCompat.isAtLeastO()) { |
| final IconCompat compatIcon = IconCompat.createWithAdaptiveBitmap(icon); |
| compatIcon.addToShortcutIntent(intent, null, mContext); |
| } else { |
| intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon); |
| } |
| intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); |
| intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, displayName); |
| |
| mListener.onShortcutIntentCreated(contactUri, intent); |
| } |
| |
| private void createPhoneNumberShortcutIntent(Uri uri, String displayName, String lookupKey, |
| byte[] bitmapData, String phoneNumber, int phoneType, String phoneLabel, |
| String shortcutAction) { |
| final Drawable drawable = getPhotoDrawable(bitmapData, displayName, lookupKey); |
| final Bitmap icon; |
| final Uri phoneUri; |
| final String shortcutName; |
| if (TextUtils.isEmpty(displayName)) { |
| displayName = mContext.getResources().getString(R.string.missing_name); |
| } |
| |
| if (Intent.ACTION_CALL.equals(shortcutAction)) { |
| // Make the URI a direct tel: URI so that it will always continue to work |
| phoneUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, phoneNumber, null); |
| icon = generatePhoneNumberIcon(drawable, phoneType, phoneLabel, |
| R.drawable.quantum_ic_phone_vd_theme_24); |
| shortcutName = mContext.getResources() |
| .getString(R.string.call_by_shortcut, displayName); |
| } else { |
| phoneUri = Uri.fromParts(ContactsUtils.SCHEME_SMSTO, phoneNumber, null); |
| icon = generatePhoneNumberIcon(drawable, phoneType, phoneLabel, |
| R.drawable.quantum_ic_message_vd_theme_24); |
| shortcutName = mContext.getResources().getString(R.string.sms_by_shortcut, displayName); |
| } |
| |
| final Intent shortcutIntent = new Intent(shortcutAction, phoneUri); |
| shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); |
| |
| Intent intent = null; |
| IconCompat compatAdaptiveIcon = null; |
| if (BuildCompat.isAtLeastO()) { |
| compatAdaptiveIcon = IconCompat.createWithAdaptiveBitmap(icon); |
| final ShortcutManager sm = (ShortcutManager) |
| mContext.getSystemService(Context.SHORTCUT_SERVICE); |
| final String id = shortcutAction + lookupKey + phoneUri.toString().hashCode(); |
| final DynamicShortcuts dynamicShortcuts = new DynamicShortcuts(mContext); |
| final ShortcutInfo shortcutInfo = dynamicShortcuts.getActionShortcutInfo( |
| id, displayName, shortcutIntent, compatAdaptiveIcon.toIcon()); |
| if (shortcutInfo != null) { |
| intent = sm.createShortcutResultIntent(shortcutInfo); |
| } |
| } |
| |
| intent = intent == null ? new Intent() : intent; |
| // This will be non-null in O and above. |
| if (compatAdaptiveIcon != null) { |
| compatAdaptiveIcon.addToShortcutIntent(intent, null, mContext); |
| } else { |
| intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon); |
| } |
| intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); |
| intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, shortcutName); |
| |
| mListener.onShortcutIntentCreated(uri, intent); |
| } |
| |
| private Bitmap generateQuickContactIcon(Drawable photo) { |
| // Setup the drawing classes |
| Bitmap bitmap = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888); |
| Canvas canvas = new Canvas(bitmap); |
| |
| // Copy in the photo |
| Rect dst = new Rect(0,0, mIconSize, mIconSize); |
| photo.setBounds(dst); |
| photo.draw(canvas); |
| |
| // Don't put a rounded border on an icon for O |
| if (BuildCompat.isAtLeastO()) { |
| return bitmap; |
| } |
| |
| // Draw the icon with a rounded border |
| RoundedBitmapDrawable roundedDrawable = |
| RoundedBitmapDrawableFactory.create(mResources, bitmap); |
| roundedDrawable.setAntiAlias(true); |
| roundedDrawable.setCornerRadius(mIconSize / 2); |
| Bitmap roundedBitmap = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888); |
| canvas.setBitmap(roundedBitmap); |
| roundedDrawable.setBounds(dst); |
| roundedDrawable.draw(canvas); |
| canvas.setBitmap(null); |
| |
| return roundedBitmap; |
| } |
| |
| /** |
| * Generates a phone number shortcut icon. Adds an overlay describing the type of the phone |
| * number, and if there is a photo also adds the call action icon. |
| */ |
| private Bitmap generatePhoneNumberIcon(Drawable photo, int phoneType, String phoneLabel, |
| int actionResId) { |
| final Resources r = mContext.getResources(); |
| final float density = r.getDisplayMetrics().density; |
| |
| final Drawable phoneDrawable = r.getDrawableForDensity(actionResId, mIconDensity); |
| // These icons have the same height and width so either is fine for the size. |
| final Bitmap phoneIcon = |
| BitmapUtil.drawableToBitmap(phoneDrawable, phoneDrawable.getIntrinsicHeight()); |
| |
| Bitmap icon = generateQuickContactIcon(photo); |
| Canvas canvas = new Canvas(icon); |
| |
| // Copy in the photo |
| Paint photoPaint = new Paint(); |
| photoPaint.setDither(true); |
| photoPaint.setFilterBitmap(true); |
| Rect dst = new Rect(0, 0, mIconSize, mIconSize); |
| |
| // Create an overlay for the phone number type if we're pre-O. O created shortcuts have the |
| // app badge which overlaps the type overlay. |
| CharSequence overlay = Phone.getTypeLabel(r, phoneType, phoneLabel); |
| if (!BuildCompat.isAtLeastO() && overlay != null) { |
| TextPaint textPaint = new TextPaint( |
| Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); |
| textPaint.setTextSize(r.getDimension(R.dimen.shortcut_overlay_text_size)); |
| textPaint.setColor(r.getColor(R.color.textColorIconOverlay)); |
| textPaint.setShadowLayer(4f, 0, 2f, r.getColor(R.color.textColorIconOverlayShadow)); |
| |
| final FontMetricsInt fmi = textPaint.getFontMetricsInt(); |
| |
| // First fill in a darker background around the text to be drawn |
| final Paint workPaint = new Paint(); |
| workPaint.setColor(mOverlayTextBackgroundColor); |
| workPaint.setStyle(Paint.Style.FILL); |
| final int textPadding = r |
| .getDimensionPixelOffset(R.dimen.shortcut_overlay_text_background_padding); |
| final int textBandHeight = (fmi.descent - fmi.ascent) + textPadding * 2; |
| dst.set(0, mIconSize - textBandHeight, mIconSize, mIconSize); |
| canvas.drawRect(dst, workPaint); |
| |
| overlay = TextUtils.ellipsize(overlay, textPaint, mIconSize, TruncateAt.END); |
| final float textWidth = textPaint.measureText(overlay, 0, overlay.length()); |
| canvas.drawText(overlay, 0, overlay.length(), (mIconSize - textWidth) / 2, mIconSize |
| - fmi.descent - textPadding, textPaint); |
| } |
| |
| // Draw the phone action icon as an overlay |
| int iconWidth = icon.getWidth(); |
| if (BuildCompat.isAtLeastO()) { |
| // On O we need to calculate where the phone icon goes slightly differently. The whole |
| // canvas area is 108dp, a centered circle with a diameter of 66dp is the "safe zone". |
| // So we start the drawing the phone icon at |
| // 108dp - 21 dp (distance from right edge of safe zone to the edge of the canvas) |
| // - 24 dp (size of the phone icon) on the x axis (left) |
| // The y axis is simply 21dp for the distance to the safe zone (top). |
| // See go/o-icons-eng for more details and a handy picture. |
| final int left = (int) (mIconSize - (45 * density)); |
| final int top = (int) (21 * density); |
| canvas.drawBitmap(phoneIcon, left, top, photoPaint); |
| } else { |
| dst.set(iconWidth - ((int) (20 * density)), -1, |
| iconWidth, ((int) (19 * density))); |
| canvas.drawBitmap(phoneIcon, null, dst, photoPaint); |
| } |
| |
| canvas.setBitmap(null); |
| return icon; |
| } |
| } |