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