ContactDetailDisplayUtils.java revision be7a9d511eed5a549226b2e1bc2ebd6f65018c4c
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 17package com.android.contacts.detail; 18 19import com.android.contacts.ContactLoader; 20import com.android.contacts.ContactLoader.Result; 21import com.android.contacts.ContactPhotoManager; 22import com.android.contacts.R; 23import com.android.contacts.preference.ContactsPreferences; 24import com.android.contacts.util.ContactBadgeUtil; 25import com.android.contacts.util.HtmlUtils; 26import com.android.contacts.util.StreamItemEntry; 27import com.android.contacts.util.StreamItemPhotoEntry; 28import com.google.common.annotations.VisibleForTesting; 29 30import android.content.ContentUris; 31import android.content.ContentValues; 32import android.content.Context; 33import android.content.Entity; 34import android.content.Entity.NamedContentValues; 35import android.content.pm.PackageManager; 36import android.content.pm.PackageManager.NameNotFoundException; 37import android.content.res.Resources; 38import android.content.res.Resources.NotFoundException; 39import android.graphics.Bitmap; 40import android.graphics.BitmapFactory; 41import android.graphics.drawable.Drawable; 42import android.net.Uri; 43import android.provider.ContactsContract; 44import android.provider.ContactsContract.CommonDataKinds.Organization; 45import android.provider.ContactsContract.Data; 46import android.provider.ContactsContract.DisplayNameSources; 47import android.provider.ContactsContract.StreamItems; 48import android.text.Html; 49import android.text.Html.ImageGetter; 50import android.text.TextUtils; 51import android.util.Log; 52import android.view.LayoutInflater; 53import android.view.View; 54import android.view.ViewGroup; 55import android.view.animation.AccelerateInterpolator; 56import android.view.animation.AlphaAnimation; 57import android.widget.CheckBox; 58import android.widget.ImageView; 59import android.widget.LinearLayout; 60import android.widget.ListView; 61import android.widget.TextView; 62 63import 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 */ 70public 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.loadPlaceholderPhoto(context); 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)); 253 statusPhotoView.setVisibility(View.VISIBLE); 254 } else { 255 statusPhotoView.setVisibility(View.GONE); 256 } 257 } 258 259 /** Creates the view that represents a stream item. */ 260 public static View createStreamItemView(LayoutInflater inflater, Context context, 261 StreamItemEntry streamItem, LinearLayout parent, 262 View.OnClickListener photoClickListener) { 263 View container = inflater.inflate(R.layout.stream_item_container, parent, false); 264 ViewGroup contentTable = (ViewGroup) container.findViewById(R.id.stream_item_content); 265 266 ContactPhotoManager contactPhotoManager = ContactPhotoManager.getInstance(context); 267 List<StreamItemPhotoEntry> photos = streamItem.getPhotos(); 268 final int photoCount = photos.size(); 269 270 // This stream item only has text. 271 if (photoCount == 0) { 272 View textOnlyContainer = inflater.inflate(R.layout.stream_item_row_text, contentTable, 273 false); 274 addStreamItemText(context, streamItem, textOnlyContainer); 275 contentTable.addView(textOnlyContainer); 276 } else { 277 // This stream item has text and photos. Process the photos, two at a time. 278 for (int index = 0; index < photoCount; index += 2) { 279 final StreamItemPhotoEntry firstPhoto = photos.get(index); 280 if (index + 1 < photoCount) { 281 // Put in two photos, side by side. 282 final StreamItemPhotoEntry secondPhoto = photos.get(index + 1); 283 View photoContainer = inflater.inflate(R.layout.stream_item_row_two_images, 284 contentTable, false); 285 loadPhoto(contactPhotoManager, streamItem, firstPhoto, photoContainer, 286 R.id.stream_item_first_image, photoClickListener); 287 loadPhoto(contactPhotoManager, streamItem, secondPhoto, photoContainer, 288 R.id.stream_item_second_image, photoClickListener); 289 contentTable.addView(photoContainer); 290 } else { 291 // Put in a single photo 292 View photoContainer = inflater.inflate( 293 R.layout.stream_item_row_one_image, contentTable, false); 294 loadPhoto(contactPhotoManager, streamItem, firstPhoto, photoContainer, 295 R.id.stream_item_first_image, photoClickListener); 296 contentTable.addView(photoContainer); 297 } 298 } 299 300 // Add text, comments, and attribution if applicable 301 View textContainer = inflater.inflate(R.layout.stream_item_row_text, contentTable, 302 false); 303 // Add extra padding between the text and the images 304 int extraVerticalPadding = context.getResources().getDimensionPixelSize( 305 R.dimen.detail_update_section_between_items_vertical_padding); 306 textContainer.setPadding(textContainer.getPaddingLeft(), 307 textContainer.getPaddingTop() + extraVerticalPadding, 308 textContainer.getPaddingRight(), 309 textContainer.getPaddingBottom()); 310 addStreamItemText(context, streamItem, textContainer); 311 contentTable.addView(textContainer); 312 } 313 314 if (parent != null) { 315 parent.addView(container); 316 } 317 318 return container; 319 } 320 321 /** Loads a photo into an image view. The image view is identified by the given id. */ 322 private static void loadPhoto(ContactPhotoManager contactPhotoManager, 323 final StreamItemEntry streamItem, final StreamItemPhotoEntry streamItemPhoto, 324 View photoContainer, int imageViewId, View.OnClickListener photoClickListener) { 325 final View frame = photoContainer.findViewById(imageViewId); 326 final View pushLayerView = frame.findViewById(R.id.push_layer); 327 final ImageView imageView = (ImageView) frame.findViewById(R.id.image); 328 if (photoClickListener != null) { 329 pushLayerView.setOnClickListener(photoClickListener); 330 pushLayerView.setTag(new StreamPhotoTag(streamItem, streamItemPhoto)); 331 pushLayerView.setFocusable(true); 332 pushLayerView.setEnabled(true); 333 } else { 334 pushLayerView.setOnClickListener(null); 335 pushLayerView.setTag(null); 336 pushLayerView.setFocusable(false); 337 // setOnClickListener makes it clickable, so we need to overwrite it 338 pushLayerView.setClickable(false); 339 pushLayerView.setEnabled(false); 340 } 341 contactPhotoManager.loadPhoto(imageView, Uri.parse(streamItemPhoto.getPhotoUri())); 342 } 343 344 @VisibleForTesting 345 static View addStreamItemText(Context context, StreamItemEntry streamItem, View rootView) { 346 TextView htmlView = (TextView) rootView.findViewById(R.id.stream_item_html); 347 TextView attributionView = (TextView) rootView.findViewById( 348 R.id.stream_item_attribution); 349 TextView commentsView = (TextView) rootView.findViewById(R.id.stream_item_comments); 350 ImageGetter imageGetter = new DefaultImageGetter(context.getPackageManager()); 351 htmlView.setText(HtmlUtils.fromHtml(context, streamItem.getText(), imageGetter, null)); 352 attributionView.setText(ContactBadgeUtil.getSocialDate(streamItem, context)); 353 if (streamItem.getComments() != null) { 354 commentsView.setText(HtmlUtils.fromHtml(context, streamItem.getComments(), imageGetter, 355 null)); 356 commentsView.setVisibility(View.VISIBLE); 357 } else { 358 commentsView.setVisibility(View.GONE); 359 } 360 return rootView; 361 } 362 363 /** 364 * Sets the display name of this contact to the given {@link TextView}. If 365 * there is none, then set the view to gone. 366 */ 367 public static void setDisplayName(Context context, Result contactData, TextView textView) { 368 if (textView == null) { 369 return; 370 } 371 setDataOrHideIfNone(getDisplayName(context, contactData), textView); 372 } 373 374 /** 375 * Sets the company and job title of this contact to the given {@link TextView}. If 376 * there is none, then set the view to gone. 377 */ 378 public static void setCompanyName(Context context, Result contactData, TextView textView) { 379 if (textView == null) { 380 return; 381 } 382 setDataOrHideIfNone(getCompany(context, contactData), textView); 383 } 384 385 /** 386 * Sets the phonetic name of this contact to the given {@link TextView}. If 387 * there is none, then set the view to gone. 388 */ 389 public static void setPhoneticName(Context context, Result contactData, TextView textView) { 390 if (textView == null) { 391 return; 392 } 393 setDataOrHideIfNone(getPhoneticName(context, contactData), textView); 394 } 395 396 /** 397 * Sets the attribution contact to the given {@link TextView}. If 398 * there is none, then set the view to gone. 399 */ 400 public static void setAttribution(Context context, Result contactData, TextView textView) { 401 if (textView == null) { 402 return; 403 } 404 setDataOrHideIfNone(getAttribution(context, contactData), textView); 405 } 406 407 /** 408 * Helper function to display the given text in the {@link TextView} or 409 * hides the {@link TextView} if the text is empty or null. 410 */ 411 private static void setDataOrHideIfNone(CharSequence textToDisplay, TextView textView) { 412 if (!TextUtils.isEmpty(textToDisplay)) { 413 textView.setText(textToDisplay); 414 textView.setVisibility(View.VISIBLE); 415 } else { 416 textView.setText(null); 417 textView.setVisibility(View.GONE); 418 } 419 } 420 421 /** Fetcher for images from resources to be included in HTML text. */ 422 private static class DefaultImageGetter implements Html.ImageGetter { 423 /** The scheme used to load resources. */ 424 private static final String RES_SCHEME = "res"; 425 426 private final PackageManager mPackageManager; 427 428 public DefaultImageGetter(PackageManager packageManager) { 429 mPackageManager = packageManager; 430 } 431 432 @Override 433 public Drawable getDrawable(String source) { 434 // Returning null means that a default image will be used. 435 Uri uri; 436 try { 437 uri = Uri.parse(source); 438 } catch (Throwable e) { 439 Log.d(TAG, "Could not parse image source: " + source); 440 return null; 441 } 442 if (!RES_SCHEME.equals(uri.getScheme())) { 443 Log.d(TAG, "Image source does not correspond to a resource: " + source); 444 return null; 445 } 446 // The URI authority represents the package name. 447 String packageName = uri.getAuthority(); 448 449 Resources resources = getResourcesForResourceName(packageName); 450 if (resources == null) { 451 Log.d(TAG, "Could not parse image source: " + source); 452 return null; 453 } 454 455 List<String> pathSegments = uri.getPathSegments(); 456 if (pathSegments.size() != 1) { 457 Log.d(TAG, "Could not parse image source: " + source); 458 return null; 459 } 460 461 final String name = pathSegments.get(0); 462 final int resId = resources.getIdentifier(name, "drawable", packageName); 463 464 if (resId == 0) { 465 // Use the default image icon in this case. 466 Log.d(TAG, "Cannot resolve resource identifier: " + source); 467 return null; 468 } 469 470 try { 471 return getResourceDrawable(resources, resId); 472 } catch (NotFoundException e) { 473 Log.d(TAG, "Resource not found: " + source, e); 474 return null; 475 } 476 } 477 478 /** Returns the drawable associated with the given id. */ 479 private Drawable getResourceDrawable(Resources resources, int resId) 480 throws NotFoundException { 481 Drawable drawable = resources.getDrawable(resId); 482 drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); 483 return drawable; 484 } 485 486 /** Returns the {@link Resources} of the package of the given resource name. */ 487 private Resources getResourcesForResourceName(String packageName) { 488 try { 489 return mPackageManager.getResourcesForApplication(packageName); 490 } catch (NameNotFoundException e) { 491 Log.d(TAG, "Could not find package: " + packageName); 492 return null; 493 } 494 } 495 } 496 497 /** 498 * Sets an alpha value on the view. 499 */ 500 public static void setAlphaOnViewBackground(View view, float alpha) { 501 if (view != null) { 502 // Convert alpha layer to a black background HEX color with an alpha value for better 503 // performance (i.e. use setBackgroundColor() instead of setAlpha()) 504 view.setBackgroundColor((int) (alpha * 255) << 24); 505 } 506 } 507 508 /** 509 * Returns the top coordinate of the first item in the {@link ListView}. If the first item 510 * in the {@link ListView} is not visible or there are no children in the list, then return 511 * Integer.MIN_VALUE. Note that the returned value will be <= 0 because the first item in the 512 * list cannot have a positive offset. 513 */ 514 public static int getFirstListItemOffset(ListView listView) { 515 if (listView == null || listView.getChildCount() == 0 || 516 listView.getFirstVisiblePosition() != 0) { 517 return Integer.MIN_VALUE; 518 } 519 return listView.getChildAt(0).getTop(); 520 } 521 522 /** 523 * Tries to scroll the first item in the list to the given offset (this can be a no-op if the 524 * list is already in the correct position). 525 * @param listView that should be scrolled 526 * @param offset which should be <= 0 527 */ 528 public static void requestToMoveToOffset(ListView listView, int offset) { 529 // We try to offset the list if the first item in the list is showing (which is presumed 530 // to have a larger height than the desired offset). If the first item in the list is not 531 // visible, then we simply do not scroll the list at all (since it can get complicated to 532 // compute how many items in the list will equal the given offset). Potentially 533 // some animation elsewhere will make the transition smoother for the user to compensate 534 // for this simplification. 535 if (listView == null || listView.getChildCount() == 0 || 536 listView.getFirstVisiblePosition() != 0 || offset > 0) { 537 return; 538 } 539 540 // As an optimization, check if the first item is already at the given offset. 541 if (listView.getChildAt(0).getTop() == offset) { 542 return; 543 } 544 545 listView.setSelectionFromTop(0, offset); 546 } 547} 548