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