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