• 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;
17 
18 import android.app.ActivityManager;
19 import android.content.ContentResolver;
20 import android.content.ContentUris;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.pm.ShortcutInfo;
24 import android.content.pm.ShortcutManager;
25 import android.content.res.Resources;
26 import android.database.Cursor;
27 import android.graphics.Bitmap;
28 import android.graphics.BitmapFactory;
29 import android.graphics.Canvas;
30 import android.graphics.Paint;
31 import android.graphics.Paint.FontMetricsInt;
32 import android.graphics.Rect;
33 import android.graphics.drawable.AdaptiveIconDrawable;
34 import android.graphics.drawable.BitmapDrawable;
35 import android.graphics.drawable.Drawable;
36 import android.graphics.drawable.Icon;
37 import android.net.Uri;
38 import android.os.AsyncTask;
39 import android.provider.ContactsContract.CommonDataKinds.Phone;
40 import android.provider.ContactsContract.CommonDataKinds.Photo;
41 import android.provider.ContactsContract.Contacts;
42 import android.provider.ContactsContract.Data;
43 import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
44 import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
45 import android.support.v4.os.BuildCompat;
46 import android.telecom.PhoneAccount;
47 import android.text.TextPaint;
48 import android.text.TextUtils;
49 import android.text.TextUtils.TruncateAt;
50 
51 import com.android.contacts.ContactPhotoManager.DefaultImageRequest;
52 import com.android.contacts.util.BitmapUtil;
53 import com.android.contacts.util.ImplicitIntentsUtil;
54 
55 /**
56  * Constructs shortcut intents.
57  */
58 public class ShortcutIntentBuilder {
59 
60     private static final String[] CONTACT_COLUMNS = {
61         Contacts.DISPLAY_NAME,
62         Contacts.PHOTO_ID,
63         Contacts.LOOKUP_KEY
64     };
65 
66     private static final int CONTACT_DISPLAY_NAME_COLUMN_INDEX = 0;
67     private static final int CONTACT_PHOTO_ID_COLUMN_INDEX = 1;
68     private static final int CONTACT_LOOKUP_KEY_COLUMN_INDEX = 2;
69 
70     private static final String[] PHONE_COLUMNS = {
71         Phone.DISPLAY_NAME,
72         Phone.PHOTO_ID,
73         Phone.NUMBER,
74         Phone.TYPE,
75         Phone.LABEL,
76         Phone.LOOKUP_KEY
77     };
78 
79     private static final int PHONE_DISPLAY_NAME_COLUMN_INDEX = 0;
80     private static final int PHONE_PHOTO_ID_COLUMN_INDEX = 1;
81     private static final int PHONE_NUMBER_COLUMN_INDEX = 2;
82     private static final int PHONE_TYPE_COLUMN_INDEX = 3;
83     private static final int PHONE_LABEL_COLUMN_INDEX = 4;
84     private static final int PHONE_LOOKUP_KEY_COLUMN_INDEX = 5;
85 
86     private static final String[] PHOTO_COLUMNS = {
87         Photo.PHOTO,
88     };
89 
90     private static final int PHOTO_PHOTO_COLUMN_INDEX = 0;
91 
92     private static final String PHOTO_SELECTION = Photo._ID + "=?";
93 
94     private final OnShortcutIntentCreatedListener mListener;
95     private final Context mContext;
96     private int mIconSize;
97     private final int mIconDensity;
98     private final int mOverlayTextBackgroundColor;
99     private final Resources mResources;
100 
101     /**
102      * This is a hidden API of the launcher in JellyBean that allows us to disable the animation
103      * that it would usually do, because it interferes with our own animation for QuickContact.
104      * This is needed since some versions of the launcher override the intent flags and therefore
105      * ignore Intent.FLAG_ACTIVITY_NO_ANIMATION.
106      */
107     public static final String INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION =
108             "com.android.launcher.intent.extra.shortcut.INGORE_LAUNCH_ANIMATION";
109 
110     /**
111      * Listener interface.
112      */
113     public interface OnShortcutIntentCreatedListener {
114 
115         /**
116          * Callback for shortcut intent creation.
117          *
118          * @param uri the original URI for which the shortcut intent has been
119          *            created.
120          * @param shortcutIntent resulting shortcut intent.
121          */
onShortcutIntentCreated(Uri uri, Intent shortcutIntent)122         void onShortcutIntentCreated(Uri uri, Intent shortcutIntent);
123     }
124 
ShortcutIntentBuilder(Context context, OnShortcutIntentCreatedListener listener)125     public ShortcutIntentBuilder(Context context, OnShortcutIntentCreatedListener listener) {
126         mContext = context;
127         mListener = listener;
128 
129         mResources = context.getResources();
130         final ActivityManager am = (ActivityManager) context
131                 .getSystemService(Context.ACTIVITY_SERVICE);
132         mIconSize = mResources.getDimensionPixelSize(R.dimen.shortcut_icon_size);
133         if (mIconSize == 0) {
134             mIconSize = am.getLauncherLargeIconSize();
135         }
136         mIconDensity = am.getLauncherLargeIconDensity();
137         mOverlayTextBackgroundColor = mResources.getColor(R.color.shortcut_overlay_text_background);
138     }
139 
createContactShortcutIntent(Uri contactUri)140     public void createContactShortcutIntent(Uri contactUri) {
141         new ContactLoadingAsyncTask(contactUri).execute();
142     }
143 
createPhoneNumberShortcutIntent(Uri dataUri, String shortcutAction)144     public void createPhoneNumberShortcutIntent(Uri dataUri, String shortcutAction) {
145         new PhoneNumberLoadingAsyncTask(dataUri, shortcutAction).execute();
146     }
147 
148     /**
149      * An asynchronous task that loads name, photo and other data from the database.
150      */
151     private abstract class LoadingAsyncTask extends AsyncTask<Void, Void, Void> {
152         protected Uri mUri;
153         protected String mContentType;
154         protected String mDisplayName;
155         protected String mLookupKey;
156         protected byte[] mBitmapData;
157         protected long mPhotoId;
158 
LoadingAsyncTask(Uri uri)159         public LoadingAsyncTask(Uri uri) {
160             mUri = uri;
161         }
162 
163         @Override
doInBackground(Void... params)164         protected Void doInBackground(Void... params) {
165             mContentType = mContext.getContentResolver().getType(mUri);
166             loadData();
167             loadPhoto();
168             return null;
169         }
170 
loadData()171         protected abstract void loadData();
172 
loadPhoto()173         private void loadPhoto() {
174             if (mPhotoId == 0) {
175                 return;
176             }
177 
178             ContentResolver resolver = mContext.getContentResolver();
179             Cursor cursor = resolver.query(Data.CONTENT_URI, PHOTO_COLUMNS, PHOTO_SELECTION,
180                     new String[] { String.valueOf(mPhotoId) }, null);
181             if (cursor != null) {
182                 try {
183                     if (cursor.moveToFirst()) {
184                         mBitmapData = cursor.getBlob(PHOTO_PHOTO_COLUMN_INDEX);
185                     }
186                 } finally {
187                     cursor.close();
188                 }
189             }
190         }
191     }
192 
193     private final class ContactLoadingAsyncTask extends LoadingAsyncTask {
ContactLoadingAsyncTask(Uri uri)194         public ContactLoadingAsyncTask(Uri uri) {
195             super(uri);
196         }
197 
198         @Override
loadData()199         protected void loadData() {
200             ContentResolver resolver = mContext.getContentResolver();
201             Cursor cursor = resolver.query(mUri, CONTACT_COLUMNS, null, null, null);
202             if (cursor != null) {
203                 try {
204                     if (cursor.moveToFirst()) {
205                         mDisplayName = cursor.getString(CONTACT_DISPLAY_NAME_COLUMN_INDEX);
206                         mPhotoId = cursor.getLong(CONTACT_PHOTO_ID_COLUMN_INDEX);
207                         mLookupKey = cursor.getString(CONTACT_LOOKUP_KEY_COLUMN_INDEX);
208                     }
209                 } finally {
210                     cursor.close();
211                 }
212             }
213         }
214         @Override
onPostExecute(Void result)215         protected void onPostExecute(Void result) {
216             createContactShortcutIntent(mUri, mContentType, mDisplayName, mLookupKey, mBitmapData);
217         }
218     }
219 
220     private final class PhoneNumberLoadingAsyncTask extends LoadingAsyncTask {
221         private final String mShortcutAction;
222         private String mPhoneNumber;
223         private int mPhoneType;
224         private String mPhoneLabel;
225 
PhoneNumberLoadingAsyncTask(Uri uri, String shortcutAction)226         public PhoneNumberLoadingAsyncTask(Uri uri, String shortcutAction) {
227             super(uri);
228             mShortcutAction = shortcutAction;
229         }
230 
231         @Override
loadData()232         protected void loadData() {
233             ContentResolver resolver = mContext.getContentResolver();
234             Cursor cursor = resolver.query(mUri, PHONE_COLUMNS, null, null, null);
235             if (cursor != null) {
236                 try {
237                     if (cursor.moveToFirst()) {
238                         mDisplayName = cursor.getString(PHONE_DISPLAY_NAME_COLUMN_INDEX);
239                         mPhotoId = cursor.getLong(PHONE_PHOTO_ID_COLUMN_INDEX);
240                         mPhoneNumber = cursor.getString(PHONE_NUMBER_COLUMN_INDEX);
241                         mPhoneType = cursor.getInt(PHONE_TYPE_COLUMN_INDEX);
242                         mPhoneLabel = cursor.getString(PHONE_LABEL_COLUMN_INDEX);
243                         mLookupKey = cursor.getString(PHONE_LOOKUP_KEY_COLUMN_INDEX);
244                     }
245                 } finally {
246                     cursor.close();
247                 }
248             }
249         }
250 
251         @Override
onPostExecute(Void result)252         protected void onPostExecute(Void result) {
253             createPhoneNumberShortcutIntent(mUri, mDisplayName, mLookupKey, mBitmapData,
254                     mPhoneNumber, mPhoneType, mPhoneLabel, mShortcutAction);
255         }
256     }
257 
getPhotoDrawable(byte[] bitmapData, String displayName, String lookupKey)258     private Drawable getPhotoDrawable(byte[] bitmapData, String displayName, String lookupKey) {
259         if (bitmapData != null) {
260             Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length, null);
261             return new BitmapDrawable(mContext.getResources(), bitmap);
262         } else {
263             return ContactPhotoManager.getDefaultAvatarDrawableForContact(mContext.getResources(),
264                     false, new DefaultImageRequest(displayName, lookupKey, false));
265         }
266     }
267 
createContactShortcutIntent(Uri contactUri, String contentType, String displayName, String lookupKey, byte[] bitmapData)268     private void createContactShortcutIntent(Uri contactUri, String contentType, String displayName,
269             String lookupKey, byte[] bitmapData) {
270         Intent intent = null;
271         if (BuildCompat.isAtLeastO()) {
272             final long contactId = ContentUris.parseId(contactUri);
273             final ShortcutManager sm = (ShortcutManager)
274                     mContext.getSystemService(Context.SHORTCUT_SERVICE);
275             final DynamicShortcuts dynamicShortcuts = new DynamicShortcuts(mContext);
276             final ShortcutInfo shortcutInfo = dynamicShortcuts.getQuickContactShortcutInfo(
277                     contactId, lookupKey, displayName);
278             intent = sm.createShortcutResultIntent(shortcutInfo);
279         }
280         final Drawable drawable = getPhotoDrawable(bitmapData, displayName, lookupKey);
281         if (TextUtils.isEmpty(displayName)) {
282             displayName = mContext.getResources().getString(R.string.missing_name);
283         }
284 
285         final Intent shortcutIntent = ImplicitIntentsUtil.getIntentForQuickContactLauncherShortcut(
286                 mContext, contactUri);
287 
288         final Bitmap icon = generateQuickContactIcon(drawable);
289 
290 
291         intent = intent == null ? new Intent() : intent;
292         intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon);
293         intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
294         intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, displayName);
295 
296         mListener.onShortcutIntentCreated(contactUri, intent);
297     }
298 
createPhoneNumberShortcutIntent(Uri uri, String displayName, String lookupKey, byte[] bitmapData, String phoneNumber, int phoneType, String phoneLabel, String shortcutAction)299     private void createPhoneNumberShortcutIntent(Uri uri, String displayName, String lookupKey,
300             byte[] bitmapData, String phoneNumber, int phoneType, String phoneLabel,
301             String shortcutAction) {
302         final Drawable drawable = getPhotoDrawable(bitmapData, displayName, lookupKey);
303         final Bitmap icon;
304         final Uri phoneUri;
305         final String shortcutName;
306         if (TextUtils.isEmpty(displayName)) {
307             displayName = mContext.getResources().getString(R.string.missing_name);
308         }
309 
310         if (Intent.ACTION_CALL.equals(shortcutAction)) {
311             // Make the URI a direct tel: URI so that it will always continue to work
312             phoneUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, phoneNumber, null);
313             icon = generatePhoneNumberIcon(drawable, phoneType, phoneLabel,
314                     R.drawable.quantum_ic_phone_vd_theme_24);
315             shortcutName = mContext.getResources()
316                     .getString(R.string.call_by_shortcut, displayName);
317         } else {
318             phoneUri = Uri.fromParts(ContactsUtils.SCHEME_SMSTO, phoneNumber, null);
319             icon = generatePhoneNumberIcon(drawable, phoneType, phoneLabel,
320                     R.drawable.quantum_ic_message_vd_theme_24);
321             shortcutName = mContext.getResources().getString(R.string.sms_by_shortcut, displayName);
322         }
323 
324         final Intent shortcutIntent = new Intent(shortcutAction, phoneUri);
325         shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
326 
327         Intent intent = null;
328         if (BuildCompat.isAtLeastO()) {
329             final ShortcutManager sm = (ShortcutManager)
330                     mContext.getSystemService(Context.SHORTCUT_SERVICE);
331             final String id = shortcutAction + lookupKey;
332             final DynamicShortcuts dynamicShortcuts = new DynamicShortcuts(mContext);
333             final ShortcutInfo shortcutInfo = dynamicShortcuts.getActionShortcutInfo(
334                     id, displayName, shortcutIntent, Icon.createWithAdaptiveBitmap(icon));
335             intent = sm.createShortcutResultIntent(shortcutInfo);
336         }
337 
338         intent = intent == null ? new Intent() : intent;
339         intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon);
340         intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
341         intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, shortcutName);
342 
343         mListener.onShortcutIntentCreated(uri, intent);
344     }
345 
generateQuickContactIcon(Drawable photo)346     private Bitmap generateQuickContactIcon(Drawable photo) {
347 
348         // Setup the drawing classes
349         Bitmap bitmap = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888);
350         Canvas canvas = new Canvas(bitmap);
351 
352         // Copy in the photo
353         Rect dst = new Rect(0,0, mIconSize, mIconSize);
354         photo.setBounds(dst);
355         photo.draw(canvas);
356 
357         // Draw the icon with a rounded border
358         RoundedBitmapDrawable roundedDrawable =
359                 RoundedBitmapDrawableFactory.create(mResources, bitmap);
360         roundedDrawable.setAntiAlias(true);
361         roundedDrawable.setCornerRadius(mIconSize / 2);
362         Bitmap roundedBitmap = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888);
363         canvas.setBitmap(roundedBitmap);
364         roundedDrawable.setBounds(dst);
365         roundedDrawable.draw(canvas);
366         canvas.setBitmap(null);
367 
368         return roundedBitmap;
369     }
370 
371     /**
372      * Generates a phone number shortcut icon. Adds an overlay describing the type of the phone
373      * number, and if there is a photo also adds the call action icon.
374      */
generatePhoneNumberIcon(Drawable photo, int phoneType, String phoneLabel, int actionResId)375     private Bitmap generatePhoneNumberIcon(Drawable photo, int phoneType, String phoneLabel,
376             int actionResId) {
377         final Resources r = mContext.getResources();
378         final float density = r.getDisplayMetrics().density;
379 
380         final Drawable phoneDrawable = r.getDrawableForDensity(actionResId, mIconDensity);
381         // These icons have the same height and width so either is fine for the size.
382         final Bitmap phoneIcon =
383                 BitmapUtil.drawableToBitmap(phoneDrawable, phoneDrawable.getIntrinsicHeight());
384 
385         Bitmap icon = generateQuickContactIcon(photo);
386         Canvas canvas = new Canvas(icon);
387 
388         // Copy in the photo
389         Paint photoPaint = new Paint();
390         photoPaint.setDither(true);
391         photoPaint.setFilterBitmap(true);
392         Rect dst = new Rect(0, 0, mIconSize, mIconSize);
393 
394         // Create an overlay for the phone number type if we're pre-O. O created shortcuts have the
395         // app badge which overlaps the type overlay.
396         CharSequence overlay = Phone.getTypeLabel(r, phoneType, phoneLabel);
397         if (!BuildCompat.isAtLeastO() && overlay != null) {
398             TextPaint textPaint = new TextPaint(
399                     Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG);
400             textPaint.setTextSize(r.getDimension(R.dimen.shortcut_overlay_text_size));
401             textPaint.setColor(r.getColor(R.color.textColorIconOverlay));
402             textPaint.setShadowLayer(4f, 0, 2f, r.getColor(R.color.textColorIconOverlayShadow));
403 
404             final FontMetricsInt fmi = textPaint.getFontMetricsInt();
405 
406             // First fill in a darker background around the text to be drawn
407             final Paint workPaint = new Paint();
408             workPaint.setColor(mOverlayTextBackgroundColor);
409             workPaint.setStyle(Paint.Style.FILL);
410             final int textPadding = r
411                     .getDimensionPixelOffset(R.dimen.shortcut_overlay_text_background_padding);
412             final int textBandHeight = (fmi.descent - fmi.ascent) + textPadding * 2;
413             dst.set(0, mIconSize - textBandHeight, mIconSize, mIconSize);
414             canvas.drawRect(dst, workPaint);
415 
416             overlay = TextUtils.ellipsize(overlay, textPaint, mIconSize, TruncateAt.END);
417             final float textWidth = textPaint.measureText(overlay, 0, overlay.length());
418             canvas.drawText(overlay, 0, overlay.length(), (mIconSize - textWidth) / 2, mIconSize
419                     - fmi.descent - textPadding, textPaint);
420         }
421 
422         // Draw the phone action icon as an overlay
423         int iconWidth = icon.getWidth();
424         dst.set(iconWidth - ((int) (20 * density)), -1,
425                 iconWidth, ((int) (19 * density)));
426         canvas.drawBitmap(phoneIcon, null, dst, photoPaint);
427 
428         canvas.setBitmap(null);
429         if (!BuildCompat.isAtLeastO()) {
430             return icon;
431         }
432 
433         // On >= O scale image up by AdaptiveIconDrawable.DEFAULT_VIEW_PORT_SCALE.
434         final int scale = (int) (icon.getHeight() *
435                 (1f / (1 + 2 * AdaptiveIconDrawable.getExtraInsetFraction())));
436         final Bitmap scaledBitmap = Bitmap.createBitmap(icon.getWidth() + scale,
437                 icon.getHeight() + scale, icon.getConfig());
438         Canvas scaledCanvas = new Canvas(scaledBitmap);
439         scaledCanvas.drawBitmap(icon, scale / 2, scale / 2, null);
440         return scaledBitmap;
441     }
442 }
443