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.text.TextPaint;
     40 import android.text.TextUtils;
     41 import android.text.TextUtils.TruncateAt;
     42 
     43 import com.android.contacts.common.CallUtil;
     44 import com.android.contacts.common.R;
     45 
     46 /**
     47  * Constructs shortcut intents.
     48  */
     49 public class ShortcutIntentBuilder {
     50 
     51     private static final String[] CONTACT_COLUMNS = {
     52         Contacts.DISPLAY_NAME,
     53         Contacts.PHOTO_ID,
     54     };
     55 
     56     private static final int CONTACT_DISPLAY_NAME_COLUMN_INDEX = 0;
     57     private static final int CONTACT_PHOTO_ID_COLUMN_INDEX = 1;
     58 
     59     private static final String[] PHONE_COLUMNS = {
     60         Phone.DISPLAY_NAME,
     61         Phone.PHOTO_ID,
     62         Phone.NUMBER,
     63         Phone.TYPE,
     64         Phone.LABEL
     65     };
     66 
     67     private static final int PHONE_DISPLAY_NAME_COLUMN_INDEX = 0;
     68     private static final int PHONE_PHOTO_ID_COLUMN_INDEX = 1;
     69     private static final int PHONE_NUMBER_COLUMN_INDEX = 2;
     70     private static final int PHONE_TYPE_COLUMN_INDEX = 3;
     71     private static final int PHONE_LABEL_COLUMN_INDEX = 4;
     72 
     73     private static final String[] PHOTO_COLUMNS = {
     74         Photo.PHOTO,
     75     };
     76 
     77     private static final int PHOTO_PHOTO_COLUMN_INDEX = 0;
     78 
     79     private static final String PHOTO_SELECTION = Photo._ID + "=?";
     80 
     81     private final OnShortcutIntentCreatedListener mListener;
     82     private final Context mContext;
     83     private int mIconSize;
     84     private final int mIconDensity;
     85     private final int mBorderWidth;
     86     private final int mBorderColor;
     87 
     88     /**
     89      * This is a hidden API of the launcher in JellyBean that allows us to disable the animation
     90      * that it would usually do, because it interferes with our own animation for QuickContact
     91      */
     92     public static final String INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION =
     93             "com.android.launcher.intent.extra.shortcut.INGORE_LAUNCH_ANIMATION";
     94 
     95     /**
     96      * Listener interface.
     97      */
     98     public interface OnShortcutIntentCreatedListener {
     99 
    100         /**
    101          * Callback for shortcut intent creation.
    102          *
    103          * @param uri the original URI for which the shortcut intent has been
    104          *            created.
    105          * @param shortcutIntent resulting shortcut intent.
    106          */
    107         void onShortcutIntentCreated(Uri uri, Intent shortcutIntent);
    108     }
    109 
    110     public ShortcutIntentBuilder(Context context, OnShortcutIntentCreatedListener listener) {
    111         mContext = context;
    112         mListener = listener;
    113 
    114         final Resources r = context.getResources();
    115         final ActivityManager am = (ActivityManager) context
    116                 .getSystemService(Context.ACTIVITY_SERVICE);
    117         mIconSize = r.getDimensionPixelSize(R.dimen.shortcut_icon_size);
    118         if (mIconSize == 0) {
    119             mIconSize = am.getLauncherLargeIconSize();
    120         }
    121         mIconDensity = am.getLauncherLargeIconDensity();
    122         mBorderWidth = r.getDimensionPixelOffset(
    123                 R.dimen.shortcut_icon_border_width);
    124         mBorderColor = r.getColor(R.color.shortcut_overlay_text_background);
    125     }
    126 
    127     public void createContactShortcutIntent(Uri contactUri) {
    128         new ContactLoadingAsyncTask(contactUri).execute();
    129     }
    130 
    131     public void createPhoneNumberShortcutIntent(Uri dataUri, String shortcutAction) {
    132         new PhoneNumberLoadingAsyncTask(dataUri, shortcutAction).execute();
    133     }
    134 
    135     /**
    136      * An asynchronous task that loads name, photo and other data from the database.
    137      */
    138     private abstract class LoadingAsyncTask extends AsyncTask<Void, Void, Void> {
    139         protected Uri mUri;
    140         protected String mContentType;
    141         protected String mDisplayName;
    142         protected byte[] mBitmapData;
    143         protected long mPhotoId;
    144 
    145         public LoadingAsyncTask(Uri uri) {
    146             mUri = uri;
    147         }
    148 
    149         @Override
    150         protected Void doInBackground(Void... params) {
    151             mContentType = mContext.getContentResolver().getType(mUri);
    152             loadData();
    153             loadPhoto();
    154             return null;
    155         }
    156 
    157         protected abstract void loadData();
    158 
    159         private void loadPhoto() {
    160             if (mPhotoId == 0) {
    161                 return;
    162             }
    163 
    164             ContentResolver resolver = mContext.getContentResolver();
    165             Cursor cursor = resolver.query(Data.CONTENT_URI, PHOTO_COLUMNS, PHOTO_SELECTION,
    166                     new String[] { String.valueOf(mPhotoId) }, null);
    167             if (cursor != null) {
    168                 try {
    169                     if (cursor.moveToFirst()) {
    170                         mBitmapData = cursor.getBlob(PHOTO_PHOTO_COLUMN_INDEX);
    171                     }
    172                 } finally {
    173                     cursor.close();
    174                 }
    175             }
    176         }
    177     }
    178 
    179     private final class ContactLoadingAsyncTask extends LoadingAsyncTask {
    180         public ContactLoadingAsyncTask(Uri uri) {
    181             super(uri);
    182         }
    183 
    184         @Override
    185         protected void loadData() {
    186             ContentResolver resolver = mContext.getContentResolver();
    187             Cursor cursor = resolver.query(mUri, CONTACT_COLUMNS, null, null, null);
    188             if (cursor != null) {
    189                 try {
    190                     if (cursor.moveToFirst()) {
    191                         mDisplayName = cursor.getString(CONTACT_DISPLAY_NAME_COLUMN_INDEX);
    192                         mPhotoId = cursor.getLong(CONTACT_PHOTO_ID_COLUMN_INDEX);
    193                     }
    194                 } finally {
    195                     cursor.close();
    196                 }
    197             }
    198         }
    199         @Override
    200         protected void onPostExecute(Void result) {
    201             createContactShortcutIntent(mUri, mContentType, mDisplayName, mBitmapData);
    202         }
    203     }
    204 
    205     private final class PhoneNumberLoadingAsyncTask extends LoadingAsyncTask {
    206         private final String mShortcutAction;
    207         private String mPhoneNumber;
    208         private int mPhoneType;
    209         private String mPhoneLabel;
    210 
    211         public PhoneNumberLoadingAsyncTask(Uri uri, String shortcutAction) {
    212             super(uri);
    213             mShortcutAction = shortcutAction;
    214         }
    215 
    216         @Override
    217         protected void loadData() {
    218             ContentResolver resolver = mContext.getContentResolver();
    219             Cursor cursor = resolver.query(mUri, PHONE_COLUMNS, null, null, null);
    220             if (cursor != null) {
    221                 try {
    222                     if (cursor.moveToFirst()) {
    223                         mDisplayName = cursor.getString(PHONE_DISPLAY_NAME_COLUMN_INDEX);
    224                         mPhotoId = cursor.getLong(PHONE_PHOTO_ID_COLUMN_INDEX);
    225                         mPhoneNumber = cursor.getString(PHONE_NUMBER_COLUMN_INDEX);
    226                         mPhoneType = cursor.getInt(PHONE_TYPE_COLUMN_INDEX);
    227                         mPhoneLabel = cursor.getString(PHONE_LABEL_COLUMN_INDEX);
    228                     }
    229                 } finally {
    230                     cursor.close();
    231                 }
    232             }
    233         }
    234 
    235         @Override
    236         protected void onPostExecute(Void result) {
    237             createPhoneNumberShortcutIntent(mUri, mDisplayName, mBitmapData, mPhoneNumber,
    238                     mPhoneType, mPhoneLabel, mShortcutAction);
    239         }
    240     }
    241 
    242     private Bitmap getPhotoBitmap(byte[] bitmapData) {
    243         Bitmap bitmap;
    244         if (bitmapData != null) {
    245             bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length, null);
    246         } else {
    247             bitmap = ((BitmapDrawable) mContext.getResources().getDrawableForDensity(
    248                     R.drawable.ic_contact_picture_holo_light, mIconDensity)).getBitmap();
    249         }
    250         return bitmap;
    251     }
    252 
    253     private void createContactShortcutIntent(Uri contactUri, String contentType, String displayName,
    254             byte[] bitmapData) {
    255         Bitmap bitmap = getPhotoBitmap(bitmapData);
    256 
    257         Intent shortcutIntent = new Intent(ContactsContract.QuickContact.ACTION_QUICK_CONTACT);
    258 
    259         // When starting from the launcher, start in a new, cleared task.
    260         // CLEAR_WHEN_TASK_RESET cannot reset the root of a task, so we
    261         // clear the whole thing preemptively here since QuickContactActivity will
    262         // finish itself when launching other detail activities.
    263         shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
    264 
    265         // Tell the launcher to not do its animation, because we are doing our own
    266         shortcutIntent.putExtra(INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION, true);
    267 
    268         shortcutIntent.setDataAndType(contactUri, contentType);
    269         shortcutIntent.putExtra(ContactsContract.QuickContact.EXTRA_MODE,
    270                 ContactsContract.QuickContact.MODE_LARGE);
    271         shortcutIntent.putExtra(ContactsContract.QuickContact.EXTRA_EXCLUDE_MIMES,
    272                 (String[]) null);
    273 
    274         final Bitmap icon = generateQuickContactIcon(bitmap);
    275 
    276         Intent intent = new Intent();
    277         intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon);
    278         intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
    279         if (TextUtils.isEmpty(displayName)) {
    280             intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, mContext.getResources().getString(
    281                     R.string.missing_name));
    282         } else {
    283             intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, displayName);
    284         }
    285 
    286         mListener.onShortcutIntentCreated(contactUri, intent);
    287     }
    288 
    289     private void createPhoneNumberShortcutIntent(Uri uri, String displayName, byte[] bitmapData,
    290             String phoneNumber, int phoneType, String phoneLabel, String shortcutAction) {
    291         Bitmap bitmap = getPhotoBitmap(bitmapData);
    292 
    293         Uri phoneUri;
    294         if (Intent.ACTION_CALL.equals(shortcutAction)) {
    295             // Make the URI a direct tel: URI so that it will always continue to work
    296             phoneUri = Uri.fromParts(CallUtil.SCHEME_TEL, phoneNumber, null);
    297             bitmap = generatePhoneNumberIcon(bitmap, phoneType, phoneLabel,
    298                     R.drawable.badge_action_call);
    299         } else {
    300             phoneUri = Uri.fromParts(CallUtil.SCHEME_SMSTO, phoneNumber, null);
    301             bitmap = generatePhoneNumberIcon(bitmap, phoneType, phoneLabel,
    302                     R.drawable.badge_action_sms);
    303         }
    304 
    305         Intent shortcutIntent = new Intent(shortcutAction, phoneUri);
    306         shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
    307 
    308         Intent intent = new Intent();
    309         intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, bitmap);
    310         intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
    311         intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, displayName);
    312 
    313         mListener.onShortcutIntentCreated(uri, intent);
    314     }
    315 
    316     private void drawBorder(Canvas canvas, Rect dst) {
    317         // Darken the border
    318         final Paint workPaint = new Paint();
    319         workPaint.setColor(mBorderColor);
    320         workPaint.setStyle(Paint.Style.STROKE);
    321         // The stroke is drawn centered on the rect bounds, and since half will be drawn outside the
    322         // bounds, we need to double the width for it to appear as intended.
    323         workPaint.setStrokeWidth(mBorderWidth * 2);
    324         canvas.drawRect(dst, workPaint);
    325     }
    326 
    327     private Bitmap generateQuickContactIcon(Bitmap photo) {
    328 
    329         // Setup the drawing classes
    330         Bitmap icon = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888);
    331         Canvas canvas = new Canvas(icon);
    332 
    333         // Copy in the photo
    334         Paint photoPaint = new Paint();
    335         photoPaint.setDither(true);
    336         photoPaint.setFilterBitmap(true);
    337         Rect src = new Rect(0,0, photo.getWidth(),photo.getHeight());
    338         Rect dst = new Rect(0,0, mIconSize, mIconSize);
    339         canvas.drawBitmap(photo, src, dst, photoPaint);
    340 
    341         drawBorder(canvas, dst);
    342 
    343         Drawable overlay = mContext.getResources().getDrawableForDensity(
    344                 com.android.internal.R.drawable.quickcontact_badge_overlay_dark, mIconDensity);
    345 
    346         overlay.setBounds(dst);
    347         overlay.draw(canvas);
    348         canvas.setBitmap(null);
    349 
    350         return icon;
    351     }
    352 
    353     /**
    354      * Generates a phone number shortcut icon. Adds an overlay describing the type of the phone
    355      * number, and if there is a photo also adds the call action icon.
    356      */
    357     private Bitmap generatePhoneNumberIcon(Bitmap photo, int phoneType, String phoneLabel,
    358             int actionResId) {
    359         final Resources r = mContext.getResources();
    360         final float density = r.getDisplayMetrics().density;
    361 
    362         Bitmap phoneIcon = ((BitmapDrawable) r.getDrawableForDensity(actionResId, mIconDensity))
    363                 .getBitmap();
    364 
    365         // Setup the drawing classes
    366         Bitmap icon = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888);
    367         Canvas canvas = new Canvas(icon);
    368 
    369         // Copy in the photo
    370         Paint photoPaint = new Paint();
    371         photoPaint.setDither(true);
    372         photoPaint.setFilterBitmap(true);
    373         Rect src = new Rect(0, 0, photo.getWidth(), photo.getHeight());
    374         Rect dst = new Rect(0, 0, mIconSize, mIconSize);
    375         canvas.drawBitmap(photo, src, dst, photoPaint);
    376 
    377         drawBorder(canvas, dst);
    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(mBorderColor);
    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 + mBorderWidth, mIconSize - textBandHeight, mIconSize - mBorderWidth,
    398                     mIconSize - mBorderWidth);
    399             canvas.drawRect(dst, workPaint);
    400 
    401             final float sidePadding = mBorderWidth;
    402             overlay = TextUtils.ellipsize(overlay, textPaint, mIconSize - 2 * sidePadding,
    403                     TruncateAt.END_SMALL);
    404             final float textWidth = textPaint.measureText(overlay, 0, overlay.length());
    405             canvas.drawText(overlay, 0, overlay.length(), (mIconSize - textWidth) / 2, mIconSize
    406                     - fmi.descent - textPadding, textPaint);
    407         }
    408 
    409         // Draw the phone action icon as an overlay
    410         src.set(0, 0, phoneIcon.getWidth(), phoneIcon.getHeight());
    411         int iconWidth = icon.getWidth();
    412         dst.set(iconWidth - ((int) (20 * density)), -1,
    413                 iconWidth, ((int) (19 * density)));
    414         dst.offset(-mBorderWidth, mBorderWidth);
    415         canvas.drawBitmap(phoneIcon, src, dst, photoPaint);
    416 
    417         canvas.setBitmap(null);
    418 
    419         return icon;
    420     }
    421 }
    422