Home | History | Annotate | Download | only in contacts
      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.IconCompat;
     44 import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
     45 import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
     46 import android.support.v4.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          */
    124         void onShortcutIntentCreated(Uri uri, Intent shortcutIntent);
    125     }
    126 
    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 
    142     public void createContactShortcutIntent(Uri contactUri) {
    143         new ContactLoadingAsyncTask(contactUri).execute();
    144     }
    145 
    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 
    161         public LoadingAsyncTask(Uri uri) {
    162             mUri = uri;
    163         }
    164 
    165         @Override
    166         protected Void doInBackground(Void... params) {
    167             mContentType = mContext.getContentResolver().getType(mUri);
    168             loadData();
    169             loadPhoto();
    170             return null;
    171         }
    172 
    173         protected abstract void loadData();
    174 
    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 {
    196         public ContactLoadingAsyncTask(Uri uri) {
    197             super(uri);
    198         }
    199 
    200         @Override
    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
    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 
    228         public PhoneNumberLoadingAsyncTask(Uri uri, String shortcutAction) {
    229             super(uri);
    230             mShortcutAction = shortcutAction;
    231         }
    232 
    233         @Override
    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
    254         protected void onPostExecute(Void result) {
    255             createPhoneNumberShortcutIntent(mUri, mDisplayName, mLookupKey, mBitmapData,
    256                     mPhoneNumber, mPhoneType, mPhoneLabel, mShortcutAction);
    257         }
    258     }
    259 
    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 
    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);
    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 
    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;
    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);
    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 
    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      */
    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