1 /* 2 * Copyright (C) 2011 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 17 package com.android.contacts.detail; 18 19 import com.android.contacts.ContactLoader; 20 import com.android.contacts.ContactLoader.Result; 21 import com.android.contacts.ContactPhotoManager; 22 import com.android.contacts.R; 23 import com.android.contacts.preference.ContactsPreferences; 24 import com.android.contacts.util.ContactBadgeUtil; 25 import com.android.contacts.util.HtmlUtils; 26 import com.android.contacts.util.StreamItemEntry; 27 import com.android.contacts.util.StreamItemPhotoEntry; 28 import com.google.common.annotations.VisibleForTesting; 29 30 import android.content.ContentUris; 31 import android.content.ContentValues; 32 import android.content.Context; 33 import android.content.Entity; 34 import android.content.Entity.NamedContentValues; 35 import android.content.pm.PackageManager; 36 import android.content.pm.PackageManager.NameNotFoundException; 37 import android.content.res.Resources; 38 import android.content.res.Resources.NotFoundException; 39 import android.graphics.Bitmap; 40 import android.graphics.BitmapFactory; 41 import android.graphics.drawable.Drawable; 42 import android.net.Uri; 43 import android.provider.ContactsContract; 44 import android.provider.ContactsContract.CommonDataKinds.Organization; 45 import android.provider.ContactsContract.Data; 46 import android.provider.ContactsContract.DisplayNameSources; 47 import android.provider.ContactsContract.StreamItems; 48 import android.text.Html; 49 import android.text.Html.ImageGetter; 50 import android.text.TextUtils; 51 import android.util.Log; 52 import android.view.LayoutInflater; 53 import android.view.View; 54 import android.view.ViewGroup; 55 import android.view.animation.AccelerateInterpolator; 56 import android.view.animation.AlphaAnimation; 57 import android.widget.CheckBox; 58 import android.widget.ImageView; 59 import android.widget.LinearLayout; 60 import android.widget.ListView; 61 import android.widget.TextView; 62 63 import java.util.List; 64 65 /** 66 * This class contains utility methods to bind high-level contact details 67 * (meaning name, phonetic name, job, and attribution) from a 68 * {@link ContactLoader.Result} data object to appropriate {@link View}s. 69 */ 70 public class ContactDetailDisplayUtils { 71 private static final String TAG = "ContactDetailDisplayUtils"; 72 73 private static final int PHOTO_FADE_IN_ANIMATION_DURATION_MILLIS = 100; 74 75 /** 76 * Tag object used for stream item photos. 77 */ 78 public static class StreamPhotoTag { 79 public final StreamItemEntry streamItem; 80 public final StreamItemPhotoEntry streamItemPhoto; 81 82 public StreamPhotoTag(StreamItemEntry streamItem, StreamItemPhotoEntry streamItemPhoto) { 83 this.streamItem = streamItem; 84 this.streamItemPhoto = streamItemPhoto; 85 } 86 87 public Uri getStreamItemPhotoUri() { 88 final Uri.Builder builder = StreamItems.CONTENT_URI.buildUpon(); 89 ContentUris.appendId(builder, streamItem.getId()); 90 builder.appendPath(StreamItems.StreamItemPhotos.CONTENT_DIRECTORY); 91 ContentUris.appendId(builder, streamItemPhoto.getId()); 92 return builder.build(); 93 } 94 } 95 96 private ContactDetailDisplayUtils() { 97 // Disallow explicit creation of this class. 98 } 99 100 /** 101 * Returns the display name of the contact, using the current display order setting. 102 * Returns res/string/missing_name if there is no display name. 103 */ 104 public static CharSequence getDisplayName(Context context, Result contactData) { 105 CharSequence displayName = contactData.getDisplayName(); 106 CharSequence altDisplayName = contactData.getAltDisplayName(); 107 ContactsPreferences prefs = new ContactsPreferences(context); 108 CharSequence styledName = ""; 109 if (!TextUtils.isEmpty(displayName) && !TextUtils.isEmpty(altDisplayName)) { 110 if (prefs.getDisplayOrder() == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) { 111 styledName = displayName; 112 } else { 113 styledName = altDisplayName; 114 } 115 } else { 116 styledName = context.getResources().getString(R.string.missing_name); 117 } 118 return styledName; 119 } 120 121 /** 122 * Returns the phonetic name of the contact or null if there isn't one. 123 */ 124 public static String getPhoneticName(Context context, Result contactData) { 125 String phoneticName = contactData.getPhoneticName(); 126 if (!TextUtils.isEmpty(phoneticName)) { 127 return phoneticName; 128 } 129 return null; 130 } 131 132 /** 133 * Returns the attribution string for the contact, which may specify the contact directory that 134 * the contact came from. Returns null if there is none applicable. 135 */ 136 public static String getAttribution(Context context, Result contactData) { 137 if (contactData.isDirectoryEntry()) { 138 String directoryDisplayName = contactData.getDirectoryDisplayName(); 139 String directoryType = contactData.getDirectoryType(); 140 String displayName = !TextUtils.isEmpty(directoryDisplayName) 141 ? directoryDisplayName 142 : directoryType; 143 return context.getString(R.string.contact_directory_description, displayName); 144 } 145 return null; 146 } 147 148 /** 149 * Returns the organization of the contact. If several organizations are given, 150 * the first one is used. Returns null if not applicable. 151 */ 152 public static String getCompany(Context context, Result contactData) { 153 final boolean displayNameIsOrganization = contactData.getDisplayNameSource() 154 == DisplayNameSources.ORGANIZATION; 155 for (Entity entity : contactData.getEntities()) { 156 for (NamedContentValues subValue : entity.getSubValues()) { 157 final ContentValues entryValues = subValue.values; 158 final String mimeType = entryValues.getAsString(Data.MIMETYPE); 159 160 if (Organization.CONTENT_ITEM_TYPE.equals(mimeType)) { 161 final String company = entryValues.getAsString(Organization.COMPANY); 162 final String title = entryValues.getAsString(Organization.TITLE); 163 final String combined; 164 // We need to show company and title in a combined string. However, if the 165 // DisplayName is already the organization, it mirrors company or (if company 166 // is empty title). Make sure we don't show what's already shown as DisplayName 167 if (TextUtils.isEmpty(company)) { 168 combined = displayNameIsOrganization ? null : title; 169 } else { 170 if (TextUtils.isEmpty(title)) { 171 combined = displayNameIsOrganization ? null : company; 172 } else { 173 if (displayNameIsOrganization) { 174 combined = title; 175 } else { 176 combined = context.getString( 177 R.string.organization_company_and_title, 178 company, title); 179 } 180 } 181 } 182 183 if (!TextUtils.isEmpty(combined)) { 184 return combined; 185 } 186 } 187 } 188 } 189 return null; 190 } 191 192 /** 193 * Sets the contact photo to display in the given {@link ImageView}. If bitmap is null, the 194 * default placeholder image is shown. 195 */ 196 public static void setPhoto(Context context, Result contactData, ImageView photoView) { 197 if (contactData.isLoadingPhoto()) { 198 photoView.setImageBitmap(null); 199 return; 200 } 201 byte[] photo = contactData.getPhotoBinaryData(); 202 Bitmap bitmap = photo != null ? BitmapFactory.decodeByteArray(photo, 0, photo.length) 203 : ContactBadgeUtil.loadDefaultAvatarPhoto(context, true, false); 204 boolean fadeIn = contactData.isDirectoryEntry(); 205 if (photoView.getDrawable() == null && fadeIn) { 206 AlphaAnimation animation = new AlphaAnimation(0, 1); 207 animation.setDuration(PHOTO_FADE_IN_ANIMATION_DURATION_MILLIS); 208 animation.setInterpolator(new AccelerateInterpolator()); 209 photoView.startAnimation(animation); 210 } 211 photoView.setImageBitmap(bitmap); 212 } 213 214 /** 215 * Sets the starred state of this contact. 216 */ 217 public static void setStarred(Result contactData, CheckBox starredView) { 218 // Check if the starred state should be visible 219 if (!contactData.isDirectoryEntry() && !contactData.isUserProfile()) { 220 starredView.setVisibility(View.VISIBLE); 221 starredView.setChecked(contactData.getStarred()); 222 } else { 223 starredView.setVisibility(View.GONE); 224 } 225 } 226 227 /** 228 * Set the social snippet text. If there isn't one, then set the view to gone. 229 */ 230 public static void setSocialSnippet(Context context, Result contactData, TextView statusView, 231 ImageView statusPhotoView) { 232 if (statusView == null) { 233 return; 234 } 235 236 CharSequence snippet = null; 237 String photoUri = null; 238 if (!contactData.getStreamItems().isEmpty()) { 239 StreamItemEntry firstEntry = contactData.getStreamItems().get(0); 240 snippet = HtmlUtils.fromHtml(context, firstEntry.getText()); 241 if (!firstEntry.getPhotos().isEmpty()) { 242 StreamItemPhotoEntry firstPhoto = firstEntry.getPhotos().get(0); 243 photoUri = firstPhoto.getPhotoUri(); 244 245 // If displaying an image, hide the snippet text. 246 snippet = null; 247 } 248 } 249 setDataOrHideIfNone(snippet, statusView); 250 if (photoUri != null) { 251 ContactPhotoManager.getInstance(context).loadPhoto( 252 statusPhotoView, Uri.parse(photoUri), true, false, 253 ContactPhotoManager.DEFAULT_BLANK); 254 statusPhotoView.setVisibility(View.VISIBLE); 255 } else { 256 statusPhotoView.setVisibility(View.GONE); 257 } 258 } 259 260 /** Creates the view that represents a stream item. */ 261 public static View createStreamItemView(LayoutInflater inflater, Context context, 262 StreamItemEntry streamItem, LinearLayout parent, 263 View.OnClickListener photoClickListener) { 264 View container = inflater.inflate(R.layout.stream_item_container, parent, false); 265 ViewGroup contentTable = (ViewGroup) container.findViewById(R.id.stream_item_content); 266 267 ContactPhotoManager contactPhotoManager = ContactPhotoManager.getInstance(context); 268 List<StreamItemPhotoEntry> photos = streamItem.getPhotos(); 269 final int photoCount = photos.size(); 270 271 // This stream item only has text. 272 if (photoCount == 0) { 273 View textOnlyContainer = inflater.inflate(R.layout.stream_item_row_text, contentTable, 274 false); 275 addStreamItemText(context, streamItem, textOnlyContainer); 276 contentTable.addView(textOnlyContainer); 277 } else { 278 // This stream item has text and photos. Process the photos, two at a time. 279 for (int index = 0; index < photoCount; index += 2) { 280 final StreamItemPhotoEntry firstPhoto = photos.get(index); 281 if (index + 1 < photoCount) { 282 // Put in two photos, side by side. 283 final StreamItemPhotoEntry secondPhoto = photos.get(index + 1); 284 View photoContainer = inflater.inflate(R.layout.stream_item_row_two_images, 285 contentTable, false); 286 loadPhoto(contactPhotoManager, streamItem, firstPhoto, photoContainer, 287 R.id.stream_item_first_image, photoClickListener); 288 loadPhoto(contactPhotoManager, streamItem, secondPhoto, photoContainer, 289 R.id.stream_item_second_image, photoClickListener); 290 contentTable.addView(photoContainer); 291 } else { 292 // Put in a single photo 293 View photoContainer = inflater.inflate( 294 R.layout.stream_item_row_one_image, contentTable, false); 295 loadPhoto(contactPhotoManager, streamItem, firstPhoto, photoContainer, 296 R.id.stream_item_first_image, photoClickListener); 297 contentTable.addView(photoContainer); 298 } 299 } 300 301 // Add text, comments, and attribution if applicable 302 View textContainer = inflater.inflate(R.layout.stream_item_row_text, contentTable, 303 false); 304 // Add extra padding between the text and the images 305 int extraVerticalPadding = context.getResources().getDimensionPixelSize( 306 R.dimen.detail_update_section_between_items_vertical_padding); 307 textContainer.setPadding(textContainer.getPaddingLeft(), 308 textContainer.getPaddingTop() + extraVerticalPadding, 309 textContainer.getPaddingRight(), 310 textContainer.getPaddingBottom()); 311 addStreamItemText(context, streamItem, textContainer); 312 contentTable.addView(textContainer); 313 } 314 315 if (parent != null) { 316 parent.addView(container); 317 } 318 319 return container; 320 } 321 322 /** Loads a photo into an image view. The image view is identified by the given id. */ 323 private static void loadPhoto(ContactPhotoManager contactPhotoManager, 324 final StreamItemEntry streamItem, final StreamItemPhotoEntry streamItemPhoto, 325 View photoContainer, int imageViewId, View.OnClickListener photoClickListener) { 326 final View frame = photoContainer.findViewById(imageViewId); 327 final View pushLayerView = frame.findViewById(R.id.push_layer); 328 final ImageView imageView = (ImageView) frame.findViewById(R.id.image); 329 if (photoClickListener != null) { 330 pushLayerView.setOnClickListener(photoClickListener); 331 pushLayerView.setTag(new StreamPhotoTag(streamItem, streamItemPhoto)); 332 pushLayerView.setFocusable(true); 333 pushLayerView.setEnabled(true); 334 } else { 335 pushLayerView.setOnClickListener(null); 336 pushLayerView.setTag(null); 337 pushLayerView.setFocusable(false); 338 // setOnClickListener makes it clickable, so we need to overwrite it 339 pushLayerView.setClickable(false); 340 pushLayerView.setEnabled(false); 341 } 342 contactPhotoManager.loadPhoto(imageView, Uri.parse(streamItemPhoto.getPhotoUri()), true, 343 false, ContactPhotoManager.DEFAULT_BLANK); 344 } 345 346 @VisibleForTesting 347 static View addStreamItemText(Context context, StreamItemEntry streamItem, View rootView) { 348 TextView htmlView = (TextView) rootView.findViewById(R.id.stream_item_html); 349 TextView attributionView = (TextView) rootView.findViewById( 350 R.id.stream_item_attribution); 351 TextView commentsView = (TextView) rootView.findViewById(R.id.stream_item_comments); 352 ImageGetter imageGetter = new DefaultImageGetter(context.getPackageManager()); 353 354 // Stream item text 355 setDataOrHideIfNone(HtmlUtils.fromHtml(context, streamItem.getText(), imageGetter, null), 356 htmlView); 357 // Attribution 358 setDataOrHideIfNone(ContactBadgeUtil.getSocialDate(streamItem, context), 359 attributionView); 360 // Comments 361 setDataOrHideIfNone(HtmlUtils.fromHtml(context, streamItem.getComments(), imageGetter, 362 null), commentsView); 363 return rootView; 364 } 365 366 /** 367 * Sets the display name of this contact to the given {@link TextView}. If 368 * there is none, then set the view to gone. 369 */ 370 public static void setDisplayName(Context context, Result contactData, TextView textView) { 371 if (textView == null) { 372 return; 373 } 374 setDataOrHideIfNone(getDisplayName(context, contactData), textView); 375 } 376 377 /** 378 * Sets the company and job title of this contact to the given {@link TextView}. If 379 * there is none, then set the view to gone. 380 */ 381 public static void setCompanyName(Context context, Result contactData, TextView textView) { 382 if (textView == null) { 383 return; 384 } 385 setDataOrHideIfNone(getCompany(context, contactData), textView); 386 } 387 388 /** 389 * Sets the phonetic name of this contact to the given {@link TextView}. If 390 * there is none, then set the view to gone. 391 */ 392 public static void setPhoneticName(Context context, Result contactData, TextView textView) { 393 if (textView == null) { 394 return; 395 } 396 setDataOrHideIfNone(getPhoneticName(context, contactData), textView); 397 } 398 399 /** 400 * Sets the attribution contact to the given {@link TextView}. If 401 * there is none, then set the view to gone. 402 */ 403 public static void setAttribution(Context context, Result contactData, TextView textView) { 404 if (textView == null) { 405 return; 406 } 407 setDataOrHideIfNone(getAttribution(context, contactData), textView); 408 } 409 410 /** 411 * Helper function to display the given text in the {@link TextView} or 412 * hides the {@link TextView} if the text is empty or null. 413 */ 414 private static void setDataOrHideIfNone(CharSequence textToDisplay, TextView textView) { 415 if (!TextUtils.isEmpty(textToDisplay)) { 416 textView.setText(textToDisplay); 417 textView.setVisibility(View.VISIBLE); 418 } else { 419 textView.setText(null); 420 textView.setVisibility(View.GONE); 421 } 422 } 423 424 /** Fetcher for images from resources to be included in HTML text. */ 425 private static class DefaultImageGetter implements Html.ImageGetter { 426 /** The scheme used to load resources. */ 427 private static final String RES_SCHEME = "res"; 428 429 private final PackageManager mPackageManager; 430 431 public DefaultImageGetter(PackageManager packageManager) { 432 mPackageManager = packageManager; 433 } 434 435 @Override 436 public Drawable getDrawable(String source) { 437 // Returning null means that a default image will be used. 438 Uri uri; 439 try { 440 uri = Uri.parse(source); 441 } catch (Throwable e) { 442 Log.d(TAG, "Could not parse image source: " + source); 443 return null; 444 } 445 if (!RES_SCHEME.equals(uri.getScheme())) { 446 Log.d(TAG, "Image source does not correspond to a resource: " + source); 447 return null; 448 } 449 // The URI authority represents the package name. 450 String packageName = uri.getAuthority(); 451 452 Resources resources = getResourcesForResourceName(packageName); 453 if (resources == null) { 454 Log.d(TAG, "Could not parse image source: " + source); 455 return null; 456 } 457 458 List<String> pathSegments = uri.getPathSegments(); 459 if (pathSegments.size() != 1) { 460 Log.d(TAG, "Could not parse image source: " + source); 461 return null; 462 } 463 464 final String name = pathSegments.get(0); 465 final int resId = resources.getIdentifier(name, "drawable", packageName); 466 467 if (resId == 0) { 468 // Use the default image icon in this case. 469 Log.d(TAG, "Cannot resolve resource identifier: " + source); 470 return null; 471 } 472 473 try { 474 return getResourceDrawable(resources, resId); 475 } catch (NotFoundException e) { 476 Log.d(TAG, "Resource not found: " + source, e); 477 return null; 478 } 479 } 480 481 /** Returns the drawable associated with the given id. */ 482 private Drawable getResourceDrawable(Resources resources, int resId) 483 throws NotFoundException { 484 Drawable drawable = resources.getDrawable(resId); 485 drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); 486 return drawable; 487 } 488 489 /** Returns the {@link Resources} of the package of the given resource name. */ 490 private Resources getResourcesForResourceName(String packageName) { 491 try { 492 return mPackageManager.getResourcesForApplication(packageName); 493 } catch (NameNotFoundException e) { 494 Log.d(TAG, "Could not find package: " + packageName); 495 return null; 496 } 497 } 498 } 499 500 /** 501 * Sets an alpha value on the view. 502 */ 503 public static void setAlphaOnViewBackground(View view, float alpha) { 504 if (view != null) { 505 // Convert alpha layer to a black background HEX color with an alpha value for better 506 // performance (i.e. use setBackgroundColor() instead of setAlpha()) 507 view.setBackgroundColor((int) (alpha * 255) << 24); 508 } 509 } 510 511 /** 512 * Returns the top coordinate of the first item in the {@link ListView}. If the first item 513 * in the {@link ListView} is not visible or there are no children in the list, then return 514 * Integer.MIN_VALUE. Note that the returned value will be <= 0 because the first item in the 515 * list cannot have a positive offset. 516 */ 517 public static int getFirstListItemOffset(ListView listView) { 518 if (listView == null || listView.getChildCount() == 0 || 519 listView.getFirstVisiblePosition() != 0) { 520 return Integer.MIN_VALUE; 521 } 522 return listView.getChildAt(0).getTop(); 523 } 524 525 /** 526 * Tries to scroll the first item in the list to the given offset (this can be a no-op if the 527 * list is already in the correct position). 528 * @param listView that should be scrolled 529 * @param offset which should be <= 0 530 */ 531 public static void requestToMoveToOffset(ListView listView, int offset) { 532 // We try to offset the list if the first item in the list is showing (which is presumed 533 // to have a larger height than the desired offset). If the first item in the list is not 534 // visible, then we simply do not scroll the list at all (since it can get complicated to 535 // compute how many items in the list will equal the given offset). Potentially 536 // some animation elsewhere will make the transition smoother for the user to compensate 537 // for this simplification. 538 if (listView == null || listView.getChildCount() == 0 || 539 listView.getFirstVisiblePosition() != 0 || offset > 0) { 540 return; 541 } 542 543 // As an optimization, check if the first item is already at the given offset. 544 if (listView.getChildAt(0).getTop() == offset) { 545 return; 546 } 547 548 listView.setSelectionFromTop(0, offset); 549 } 550 } 551