ShortcutIntentBuilder.java revision ba2c125b07086a88a3517fcf381a3a400c42afd3
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 */
16package com.android.contacts.common.list;
17
18import android.app.ActivityManager;
19import android.content.ContentResolver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.res.Resources;
23import android.database.Cursor;
24import android.graphics.Bitmap;
25import android.graphics.BitmapFactory;
26import android.graphics.Canvas;
27import android.graphics.Paint;
28import android.graphics.Paint.FontMetricsInt;
29import android.graphics.Rect;
30import android.graphics.drawable.BitmapDrawable;
31import android.graphics.drawable.Drawable;
32import android.net.Uri;
33import android.os.AsyncTask;
34import android.provider.ContactsContract;
35import android.provider.ContactsContract.CommonDataKinds.Phone;
36import android.provider.ContactsContract.CommonDataKinds.Photo;
37import android.provider.ContactsContract.Contacts;
38import android.provider.ContactsContract.Data;
39import android.text.TextPaint;
40import android.text.TextUtils;
41import android.text.TextUtils.TruncateAt;
42
43import com.android.contacts.common.CallUtil;
44import com.android.contacts.common.R;
45
46/**
47 * Constructs shortcut intents.
48 */
49public 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