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.support.v4.graphics.drawable.RoundedBitmapDrawable;
40import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
41import android.telecom.PhoneAccount;
42import android.text.TextPaint;
43import android.text.TextUtils;
44import android.text.TextUtils.TruncateAt;
45
46import com.android.contacts.common.ContactsUtils;
47import com.android.contacts.common.ContactPhotoManager;
48import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
49import com.android.contacts.common.R;
50
51/**
52 * Constructs shortcut intents.
53 */
54public 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        Intent shortcutIntent = new Intent(ContactsContract.QuickContact.ACTION_QUICK_CONTACT);
269
270        // When starting from the launcher, start in a new, cleared task.
271        // CLEAR_WHEN_TASK_RESET cannot reset the root of a task, so we
272        // clear the whole thing preemptively here since QuickContactActivity will
273        // finish itself when launching other detail activities. We need to use
274        // Intent.FLAG_ACTIVITY_NO_ANIMATION since not all versions of launcher will respect
275        // the INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION intent extra.
276        shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK
277                | Intent.FLAG_ACTIVITY_NO_ANIMATION);
278
279        // Tell the launcher to not do its animation, because we are doing our own
280        shortcutIntent.putExtra(INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION, true);
281
282        shortcutIntent.setDataAndType(contactUri, contentType);
283        shortcutIntent.putExtra(ContactsContract.QuickContact.EXTRA_EXCLUDE_MIMES,
284                (String[]) null);
285
286        final Bitmap icon = generateQuickContactIcon(drawable);
287
288        Intent intent = new Intent();
289        intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon);
290        intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
291        if (TextUtils.isEmpty(displayName)) {
292            intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, mContext.getResources().getString(
293                    R.string.missing_name));
294        } else {
295            intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, displayName);
296        }
297
298        mListener.onShortcutIntentCreated(contactUri, intent);
299    }
300
301    private void createPhoneNumberShortcutIntent(Uri uri, String displayName, String lookupKey,
302            byte[] bitmapData, String phoneNumber, int phoneType, String phoneLabel,
303            String shortcutAction) {
304        Drawable drawable = getPhotoDrawable(bitmapData, displayName, lookupKey);
305
306        Bitmap bitmap;
307        Uri phoneUri;
308        if (Intent.ACTION_CALL.equals(shortcutAction)) {
309            // Make the URI a direct tel: URI so that it will always continue to work
310            phoneUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, phoneNumber, null);
311            bitmap = generatePhoneNumberIcon(drawable, phoneType, phoneLabel,
312                    R.drawable.badge_action_call);
313        } else {
314            phoneUri = Uri.fromParts(ContactsUtils.SCHEME_SMSTO, phoneNumber, null);
315            bitmap = generatePhoneNumberIcon(drawable, phoneType, phoneLabel,
316                    R.drawable.badge_action_sms);
317        }
318
319        Intent shortcutIntent = new Intent(shortcutAction, phoneUri);
320        shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
321
322        Intent intent = new Intent();
323        intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, bitmap);
324        intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
325        intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, displayName);
326
327        mListener.onShortcutIntentCreated(uri, intent);
328    }
329
330    private Bitmap generateQuickContactIcon(Drawable photo) {
331
332        // Setup the drawing classes
333        Bitmap bitmap = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888);
334        Canvas canvas = new Canvas(bitmap);
335
336        // Copy in the photo
337        Rect dst = new Rect(0,0, mIconSize, mIconSize);
338        photo.setBounds(dst);
339        photo.draw(canvas);
340
341        // Draw the icon with a rounded border
342        RoundedBitmapDrawable roundedDrawable =
343                RoundedBitmapDrawableFactory.create(mResources, bitmap);
344        roundedDrawable.setAntiAlias(true);
345        roundedDrawable.setCornerRadius(mIconSize / 2);
346        Bitmap roundedBitmap = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888);
347        canvas.setBitmap(roundedBitmap);
348        roundedDrawable.setBounds(dst);
349        roundedDrawable.draw(canvas);
350        canvas.setBitmap(null);
351
352        return roundedBitmap;
353    }
354
355    /**
356     * Generates a phone number shortcut icon. Adds an overlay describing the type of the phone
357     * number, and if there is a photo also adds the call action icon.
358     */
359    private Bitmap generatePhoneNumberIcon(Drawable photo, int phoneType, String phoneLabel,
360            int actionResId) {
361        final Resources r = mContext.getResources();
362        final float density = r.getDisplayMetrics().density;
363
364        Bitmap phoneIcon = ((BitmapDrawable) r.getDrawableForDensity(actionResId, mIconDensity))
365                .getBitmap();
366
367        Bitmap icon = generateQuickContactIcon(photo);
368        Canvas canvas = new Canvas(icon);
369
370        // Copy in the photo
371        Paint photoPaint = new Paint();
372        photoPaint.setDither(true);
373        photoPaint.setFilterBitmap(true);
374        Rect dst = new Rect(0, 0, mIconSize, mIconSize);
375
376        // Create an overlay for the phone number type
377        CharSequence overlay = Phone.getTypeLabel(r, phoneType, phoneLabel);
378
379        if (overlay != null) {
380            TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG);
381            textPaint.setTextSize(r.getDimension(R.dimen.shortcut_overlay_text_size));
382            textPaint.setColor(r.getColor(R.color.textColorIconOverlay));
383            textPaint.setShadowLayer(4f, 0, 2f, r.getColor(R.color.textColorIconOverlayShadow));
384
385            final FontMetricsInt fmi = textPaint.getFontMetricsInt();
386
387            // First fill in a darker background around the text to be drawn
388            final Paint workPaint = new Paint();
389            workPaint.setColor(mOverlayTextBackgroundColor);
390            workPaint.setStyle(Paint.Style.FILL);
391            final int textPadding = r
392                    .getDimensionPixelOffset(R.dimen.shortcut_overlay_text_background_padding);
393            final int textBandHeight = (fmi.descent - fmi.ascent) + textPadding * 2;
394            dst.set(0, mIconSize - textBandHeight, mIconSize, mIconSize);
395            canvas.drawRect(dst, workPaint);
396
397            overlay = TextUtils.ellipsize(overlay, textPaint, mIconSize, TruncateAt.END);
398            final float textWidth = textPaint.measureText(overlay, 0, overlay.length());
399            canvas.drawText(overlay, 0, overlay.length(), (mIconSize - textWidth) / 2, mIconSize
400                    - fmi.descent - textPadding, textPaint);
401        }
402
403        // Draw the phone action icon as an overlay
404        Rect src = new Rect(0, 0, phoneIcon.getWidth(), phoneIcon.getHeight());
405        int iconWidth = icon.getWidth();
406        dst.set(iconWidth - ((int) (20 * density)), -1,
407                iconWidth, ((int) (19 * density)));
408        canvas.drawBitmap(phoneIcon, src, dst, photoPaint);
409
410        canvas.setBitmap(null);
411
412        return icon;
413    }
414}
415