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