Home | History | Annotate | Download | only in list
      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);
    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         intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, displayName);
    329 
    330         mListener.onShortcutIntentCreated(uri, intent);
    331     }
    332 
    333     private Bitmap generateQuickContactIcon(Drawable photo) {
    334 
    335         // Setup the drawing classes
    336         Bitmap bitmap = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888);
    337         Canvas canvas = new Canvas(bitmap);
    338 
    339         // Copy in the photo
    340         Rect dst = new Rect(0,0, mIconSize, mIconSize);
    341         photo.setBounds(dst);
    342         photo.draw(canvas);
    343 
    344         // Draw the icon with a rounded border
    345         RoundedBitmapDrawable roundedDrawable =
    346                 RoundedBitmapDrawableFactory.create(mResources, bitmap);
    347         roundedDrawable.setAntiAlias(true);
    348         roundedDrawable.setCornerRadius(mIconSize / 2);
    349         Bitmap roundedBitmap = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888);
    350         canvas.setBitmap(roundedBitmap);
    351         roundedDrawable.setBounds(dst);
    352         roundedDrawable.draw(canvas);
    353         canvas.setBitmap(null);
    354 
    355         return roundedBitmap;
    356     }
    357 
    358     /**
    359      * Generates a phone number shortcut icon. Adds an overlay describing the type of the phone
    360      * number, and if there is a photo also adds the call action icon.
    361      */
    362     private Bitmap generatePhoneNumberIcon(Drawable photo, int phoneType, String phoneLabel,
    363             int actionResId) {
    364         final Resources r = mContext.getResources();
    365         final float density = r.getDisplayMetrics().density;
    366 
    367         Bitmap phoneIcon = ((BitmapDrawable) r.getDrawableForDensity(actionResId, mIconDensity))
    368                 .getBitmap();
    369 
    370         Bitmap icon = generateQuickContactIcon(photo);
    371         Canvas canvas = new Canvas(icon);
    372 
    373         // Copy in the photo
    374         Paint photoPaint = new Paint();
    375         photoPaint.setDither(true);
    376         photoPaint.setFilterBitmap(true);
    377         Rect dst = new Rect(0, 0, mIconSize, mIconSize);
    378 
    379         // Create an overlay for the phone number type
    380         CharSequence overlay = Phone.getTypeLabel(r, phoneType, phoneLabel);
    381 
    382         if (overlay != null) {
    383             TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG);
    384             textPaint.setTextSize(r.getDimension(R.dimen.shortcut_overlay_text_size));
    385             textPaint.setColor(r.getColor(R.color.textColorIconOverlay));
    386             textPaint.setShadowLayer(4f, 0, 2f, r.getColor(R.color.textColorIconOverlayShadow));
    387 
    388             final FontMetricsInt fmi = textPaint.getFontMetricsInt();
    389 
    390             // First fill in a darker background around the text to be drawn
    391             final Paint workPaint = new Paint();
    392             workPaint.setColor(mOverlayTextBackgroundColor);
    393             workPaint.setStyle(Paint.Style.FILL);
    394             final int textPadding = r
    395                     .getDimensionPixelOffset(R.dimen.shortcut_overlay_text_background_padding);
    396             final int textBandHeight = (fmi.descent - fmi.ascent) + textPadding * 2;
    397             dst.set(0, mIconSize - textBandHeight, mIconSize, mIconSize);
    398             canvas.drawRect(dst, workPaint);
    399 
    400             overlay = TextUtils.ellipsize(overlay, textPaint, mIconSize, TruncateAt.END);
    401             final float textWidth = textPaint.measureText(overlay, 0, overlay.length());
    402             canvas.drawText(overlay, 0, overlay.length(), (mIconSize - textWidth) / 2, mIconSize
    403                     - fmi.descent - textPadding, textPaint);
    404         }
    405 
    406         // Draw the phone action icon as an overlay
    407         Rect src = new Rect(0, 0, phoneIcon.getWidth(), phoneIcon.getHeight());
    408         int iconWidth = icon.getWidth();
    409         dst.set(iconWidth - ((int) (20 * density)), -1,
    410                 iconWidth, ((int) (19 * density)));
    411         canvas.drawBitmap(phoneIcon, src, dst, photoPaint);
    412 
    413         canvas.setBitmap(null);
    414 
    415         return icon;
    416     }
    417 }
    418