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