• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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          */
onShortcutIntentCreated(Uri uri, Intent shortcutIntent)118         void onShortcutIntentCreated(Uri uri, Intent shortcutIntent);
119     }
120 
ShortcutIntentBuilder(Context context, OnShortcutIntentCreatedListener listener)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 
createContactShortcutIntent(Uri contactUri)136     public void createContactShortcutIntent(Uri contactUri) {
137         new ContactLoadingAsyncTask(contactUri).execute();
138     }
139 
createPhoneNumberShortcutIntent(Uri dataUri, String shortcutAction)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 
LoadingAsyncTask(Uri uri)155         public LoadingAsyncTask(Uri uri) {
156             mUri = uri;
157         }
158 
159         @Override
doInBackground(Void... params)160         protected Void doInBackground(Void... params) {
161             mContentType = mContext.getContentResolver().getType(mUri);
162             loadData();
163             loadPhoto();
164             return null;
165         }
166 
loadData()167         protected abstract void loadData();
168 
loadPhoto()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 {
ContactLoadingAsyncTask(Uri uri)190         public ContactLoadingAsyncTask(Uri uri) {
191             super(uri);
192         }
193 
194         @Override
loadData()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
onPostExecute(Void result)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 
PhoneNumberLoadingAsyncTask(Uri uri, String shortcutAction)222         public PhoneNumberLoadingAsyncTask(Uri uri, String shortcutAction) {
223             super(uri);
224             mShortcutAction = shortcutAction;
225         }
226 
227         @Override
loadData()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
onPostExecute(Void result)248         protected void onPostExecute(Void result) {
249             createPhoneNumberShortcutIntent(mUri, mDisplayName, mLookupKey, mBitmapData,
250                     mPhoneNumber, mPhoneType, mPhoneLabel, mShortcutAction);
251         }
252     }
253 
getPhotoDrawable(byte[] bitmapData, String displayName, String lookupKey)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 
createContactShortcutIntent(Uri contactUri, String contentType, String displayName, String lookupKey, byte[] bitmapData)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 
createPhoneNumberShortcutIntent(Uri uri, String displayName, String lookupKey, byte[] bitmapData, String phoneNumber, int phoneType, String phoneLabel, String shortcutAction)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_mirrored);
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 
329         if (TextUtils.isEmpty(displayName)) {
330             displayName = mContext.getResources().getString(R.string.missing_name);
331         }
332         if (TextUtils.equals(shortcutAction, Intent.ACTION_CALL)) {
333             intent.putExtra(Intent.EXTRA_SHORTCUT_NAME,
334                     mContext.getResources().getString(R.string.call_by_shortcut, displayName));
335         } else if (TextUtils.equals(shortcutAction, Intent.ACTION_SENDTO)) {
336             intent.putExtra(Intent.EXTRA_SHORTCUT_NAME,
337                     mContext.getResources().getString(R.string.sms_by_shortcut, displayName));
338         }
339 
340         mListener.onShortcutIntentCreated(uri, intent);
341     }
342 
generateQuickContactIcon(Drawable photo)343     private Bitmap generateQuickContactIcon(Drawable photo) {
344 
345         // Setup the drawing classes
346         Bitmap bitmap = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888);
347         Canvas canvas = new Canvas(bitmap);
348 
349         // Copy in the photo
350         Rect dst = new Rect(0,0, mIconSize, mIconSize);
351         photo.setBounds(dst);
352         photo.draw(canvas);
353 
354         // Draw the icon with a rounded border
355         RoundedBitmapDrawable roundedDrawable =
356                 RoundedBitmapDrawableFactory.create(mResources, bitmap);
357         roundedDrawable.setAntiAlias(true);
358         roundedDrawable.setCornerRadius(mIconSize / 2);
359         Bitmap roundedBitmap = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888);
360         canvas.setBitmap(roundedBitmap);
361         roundedDrawable.setBounds(dst);
362         roundedDrawable.draw(canvas);
363         canvas.setBitmap(null);
364 
365         return roundedBitmap;
366     }
367 
368     /**
369      * Generates a phone number shortcut icon. Adds an overlay describing the type of the phone
370      * number, and if there is a photo also adds the call action icon.
371      */
generatePhoneNumberIcon(Drawable photo, int phoneType, String phoneLabel, int actionResId)372     private Bitmap generatePhoneNumberIcon(Drawable photo, int phoneType, String phoneLabel,
373             int actionResId) {
374         final Resources r = mContext.getResources();
375         final float density = r.getDisplayMetrics().density;
376 
377         Bitmap phoneIcon = ((BitmapDrawable) r.getDrawableForDensity(actionResId, mIconDensity))
378                 .getBitmap();
379 
380         Bitmap icon = generateQuickContactIcon(photo);
381         Canvas canvas = new Canvas(icon);
382 
383         // Copy in the photo
384         Paint photoPaint = new Paint();
385         photoPaint.setDither(true);
386         photoPaint.setFilterBitmap(true);
387         Rect dst = new Rect(0, 0, mIconSize, mIconSize);
388 
389         // Create an overlay for the phone number type
390         CharSequence overlay = Phone.getTypeLabel(r, phoneType, phoneLabel);
391 
392         if (overlay != null) {
393             TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG);
394             textPaint.setTextSize(r.getDimension(R.dimen.shortcut_overlay_text_size));
395             textPaint.setColor(r.getColor(R.color.textColorIconOverlay));
396             textPaint.setShadowLayer(4f, 0, 2f, r.getColor(R.color.textColorIconOverlayShadow));
397 
398             final FontMetricsInt fmi = textPaint.getFontMetricsInt();
399 
400             // First fill in a darker background around the text to be drawn
401             final Paint workPaint = new Paint();
402             workPaint.setColor(mOverlayTextBackgroundColor);
403             workPaint.setStyle(Paint.Style.FILL);
404             final int textPadding = r
405                     .getDimensionPixelOffset(R.dimen.shortcut_overlay_text_background_padding);
406             final int textBandHeight = (fmi.descent - fmi.ascent) + textPadding * 2;
407             dst.set(0, mIconSize - textBandHeight, mIconSize, mIconSize);
408             canvas.drawRect(dst, workPaint);
409 
410             overlay = TextUtils.ellipsize(overlay, textPaint, mIconSize, TruncateAt.END);
411             final float textWidth = textPaint.measureText(overlay, 0, overlay.length());
412             canvas.drawText(overlay, 0, overlay.length(), (mIconSize - textWidth) / 2, mIconSize
413                     - fmi.descent - textPadding, textPaint);
414         }
415 
416         // Draw the phone action icon as an overlay
417         Rect src = new Rect(0, 0, phoneIcon.getWidth(), phoneIcon.getHeight());
418         int iconWidth = icon.getWidth();
419         dst.set(iconWidth - ((int) (20 * density)), -1,
420                 iconWidth, ((int) (19 * density)));
421         canvas.drawBitmap(phoneIcon, src, dst, photoPaint);
422 
423         canvas.setBitmap(null);
424 
425         return icon;
426     }
427 }
428