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.common.model.Contact; 46 import com.android.contacts.common.model.RawContact; 47 import com.android.contacts.common.model.dataitem.DataItem; 48 import com.android.contacts.common.model.dataitem.OrganizationDataItem; 49 import com.android.contacts.common.preference.ContactsPreferences; 50 import com.android.contacts.util.StreamItemEntry; 51 import com.android.contacts.util.ContactBadgeUtil; 52 import com.android.contacts.util.HtmlUtils; 53 import com.android.contacts.util.MoreMath; 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 setDataOrHideIfNone(snippet, statusView); 237 if (photoUri != null) { 238 ContactPhotoManager.getInstance(context).loadPhoto( 239 statusPhotoView, Uri.parse(photoUri), -1, false, null); 240 statusPhotoView.setVisibility(View.VISIBLE); 241 } else { 242 statusPhotoView.setVisibility(View.GONE); 243 } 244 } 245 246 /** Creates the view that represents a stream item. */ 247 public static View createStreamItemView(LayoutInflater inflater, Context context, 248 View convertView, StreamItemEntry streamItem, View.OnClickListener photoClickListener) { 249 250 // Try to recycle existing views. 251 final View container; 252 if (convertView != null) { 253 container = convertView; 254 } else { 255 container = inflater.inflate(R.layout.stream_item_container, null, false); 256 } 257 258 final ContactPhotoManager contactPhotoManager = ContactPhotoManager.getInstance(context); 259 final List<StreamItemPhotoEntry> photos = streamItem.getPhotos(); 260 final int photoCount = photos.size(); 261 262 // Add the text part. 263 addStreamItemText(context, streamItem, container); 264 265 // Add images. 266 final ViewGroup imageRows = (ViewGroup) container.findViewById(R.id.stream_item_image_rows); 267 268 if (photoCount == 0) { 269 // This stream item only has text. 270 imageRows.setVisibility(View.GONE); 271 } else { 272 // This stream item has text and photos. 273 imageRows.setVisibility(View.VISIBLE); 274 275 // Number of image rows needed, which is cailing(photoCount / 2) 276 final int numImageRows = (photoCount + 1) / 2; 277 278 // Actual image rows. 279 final int numOldImageRows = imageRows.getChildCount(); 280 281 // Make sure we have enough stream_item_row_images. 282 if (numOldImageRows == numImageRows) { 283 // Great, we have the just enough number of rows... 284 285 } else if (numOldImageRows < numImageRows) { 286 // Need to add more image rows. 287 for (int i = numOldImageRows; i < numImageRows; i++) { 288 View imageRow = inflater.inflate(R.layout.stream_item_row_images, imageRows, 289 true); 290 } 291 } else { 292 // We have exceeding image rows. Hide them. 293 for (int i = numImageRows; i < numOldImageRows; i++) { 294 imageRows.getChildAt(i).setVisibility(View.GONE); 295 } 296 } 297 298 // Put images, two by two. 299 for (int i = 0; i < photoCount; i += 2) { 300 final View imageRow = imageRows.getChildAt(i / 2); 301 // Reused image rows may not visible, so make sure they're shown. 302 imageRow.setVisibility(View.VISIBLE); 303 304 // Show first image. 305 loadPhoto(contactPhotoManager, streamItem, photos.get(i), imageRow, 306 R.id.stream_item_first_image, photoClickListener); 307 final View secondContainer = imageRow.findViewById(R.id.second_image_container); 308 if (i + 1 < photoCount) { 309 // Show the second image too. 310 loadPhoto(contactPhotoManager, streamItem, photos.get(i + 1), imageRow, 311 R.id.stream_item_second_image, photoClickListener); 312 secondContainer.setVisibility(View.VISIBLE); 313 } else { 314 // Hide the second image, but it still has to occupy the space. 315 secondContainer.setVisibility(View.INVISIBLE); 316 } 317 } 318 } 319 320 return container; 321 } 322 323 /** Loads a photo into an image view. The image view is identified by the given id. */ 324 private static void loadPhoto(ContactPhotoManager contactPhotoManager, 325 final StreamItemEntry streamItem, final StreamItemPhotoEntry streamItemPhoto, 326 View photoContainer, int imageViewId, View.OnClickListener photoClickListener) { 327 final View frame = photoContainer.findViewById(imageViewId); 328 final View pushLayerView = frame.findViewById(R.id.push_layer); 329 final ImageView imageView = (ImageView) frame.findViewById(R.id.image); 330 if (photoClickListener != null) { 331 pushLayerView.setOnClickListener(photoClickListener); 332 pushLayerView.setTag(new StreamPhotoTag(streamItem, streamItemPhoto)); 333 pushLayerView.setFocusable(true); 334 pushLayerView.setEnabled(true); 335 } else { 336 pushLayerView.setOnClickListener(null); 337 pushLayerView.setTag(null); 338 pushLayerView.setFocusable(false); 339 // setOnClickListener makes it clickable, so we need to overwrite it 340 pushLayerView.setClickable(false); 341 pushLayerView.setEnabled(false); 342 } 343 contactPhotoManager.loadPhoto(imageView, Uri.parse(streamItemPhoto.getPhotoUri()), -1, 344 false, null); 345 } 346 347 @VisibleForTesting 348 static View addStreamItemText(Context context, StreamItemEntry streamItem, View rootView) { 349 TextView htmlView = (TextView) rootView.findViewById(R.id.stream_item_html); 350 TextView attributionView = (TextView) rootView.findViewById( 351 R.id.stream_item_attribution); 352 TextView commentsView = (TextView) rootView.findViewById(R.id.stream_item_comments); 353 ImageGetter imageGetter = new DefaultImageGetter(context.getPackageManager()); 354 355 // Stream item text 356 setDataOrHideIfNone(streamItem.getDecodedText(), htmlView); 357 // Attribution 358 setDataOrHideIfNone(ContactBadgeUtil.getSocialDate(streamItem, context), 359 attributionView); 360 // Comments 361 setDataOrHideIfNone(streamItem.getDecodedComments(), commentsView); 362 return rootView; 363 } 364 365 /** 366 * Sets the display name of this contact to the given {@link TextView}. If 367 * there is none, then set the view to gone. 368 */ 369 public static void setDisplayName(Context context, Contact contactData, TextView textView) { 370 if (textView == null) { 371 return; 372 } 373 setDataOrHideIfNone(getDisplayName(context, contactData), textView); 374 } 375 376 /** 377 * Sets the company and job title of this contact to the given {@link TextView}. If 378 * there is none, then set the view to gone. 379 */ 380 public static void setCompanyName(Context context, Contact contactData, TextView textView) { 381 if (textView == null) { 382 return; 383 } 384 setDataOrHideIfNone(getCompany(context, contactData), textView); 385 } 386 387 /** 388 * Sets the phonetic name of this contact to the given {@link TextView}. If 389 * there is none, then set the view to gone. 390 */ 391 public static void setPhoneticName(Context context, Contact contactData, TextView textView) { 392 if (textView == null) { 393 return; 394 } 395 setDataOrHideIfNone(getPhoneticName(context, contactData), textView); 396 } 397 398 /** 399 * Sets the attribution contact to the given {@link TextView}. If 400 * there is none, then set the view to gone. 401 */ 402 public static void setAttribution(Context context, Contact contactData, TextView textView) { 403 if (textView == null) { 404 return; 405 } 406 setDataOrHideIfNone(getAttribution(context, contactData), textView); 407 } 408 409 /** 410 * Helper function to display the given text in the {@link TextView} or 411 * hides the {@link TextView} if the text is empty or null. 412 */ 413 private static void setDataOrHideIfNone(CharSequence textToDisplay, TextView textView) { 414 if (!TextUtils.isEmpty(textToDisplay)) { 415 textView.setText(textToDisplay); 416 textView.setVisibility(View.VISIBLE); 417 } else { 418 textView.setText(null); 419 textView.setVisibility(View.GONE); 420 } 421 } 422 423 private static Html.ImageGetter sImageGetter; 424 425 public static Html.ImageGetter getImageGetter(Context context) { 426 if (sImageGetter == null) { 427 sImageGetter = new DefaultImageGetter(context.getPackageManager()); 428 } 429 return sImageGetter; 430 } 431 432 /** Fetcher for images from resources to be included in HTML text. */ 433 private static class DefaultImageGetter implements Html.ImageGetter { 434 /** The scheme used to load resources. */ 435 private static final String RES_SCHEME = "res"; 436 437 private final PackageManager mPackageManager; 438 439 public DefaultImageGetter(PackageManager packageManager) { 440 mPackageManager = packageManager; 441 } 442 443 @Override 444 public Drawable getDrawable(String source) { 445 // Returning null means that a default image will be used. 446 Uri uri; 447 try { 448 uri = Uri.parse(source); 449 } catch (Throwable e) { 450 Log.d(TAG, "Could not parse image source: " + source); 451 return null; 452 } 453 if (!RES_SCHEME.equals(uri.getScheme())) { 454 Log.d(TAG, "Image source does not correspond to a resource: " + source); 455 return null; 456 } 457 // The URI authority represents the package name. 458 String packageName = uri.getAuthority(); 459 460 Resources resources = getResourcesForResourceName(packageName); 461 if (resources == null) { 462 Log.d(TAG, "Could not parse image source: " + source); 463 return null; 464 } 465 466 List<String> pathSegments = uri.getPathSegments(); 467 if (pathSegments.size() != 1) { 468 Log.d(TAG, "Could not parse image source: " + source); 469 return null; 470 } 471 472 final String name = pathSegments.get(0); 473 final int resId = resources.getIdentifier(name, "drawable", packageName); 474 475 if (resId == 0) { 476 // Use the default image icon in this case. 477 Log.d(TAG, "Cannot resolve resource identifier: " + source); 478 return null; 479 } 480 481 try { 482 return getResourceDrawable(resources, resId); 483 } catch (NotFoundException e) { 484 Log.d(TAG, "Resource not found: " + source, e); 485 return null; 486 } 487 } 488 489 /** Returns the drawable associated with the given id. */ 490 private Drawable getResourceDrawable(Resources resources, int resId) 491 throws NotFoundException { 492 Drawable drawable = resources.getDrawable(resId); 493 drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); 494 return drawable; 495 } 496 497 /** Returns the {@link Resources} of the package of the given resource name. */ 498 private Resources getResourcesForResourceName(String packageName) { 499 try { 500 return mPackageManager.getResourcesForApplication(packageName); 501 } catch (NameNotFoundException e) { 502 Log.d(TAG, "Could not find package: " + packageName); 503 return null; 504 } 505 } 506 } 507 508 /** 509 * Sets an alpha value on the view. 510 */ 511 public static void setAlphaOnViewBackground(View view, float alpha) { 512 if (view != null) { 513 // Convert alpha layer to a black background HEX color with an alpha value for better 514 // performance (i.e. use setBackgroundColor() instead of setAlpha()) 515 view.setBackgroundColor((int) (MoreMath.clamp(alpha, 0.0f, 1.0f) * 255) << 24); 516 } 517 } 518 519 /** 520 * Returns the top coordinate of the first item in the {@link ListView}. If the first item 521 * in the {@link ListView} is not visible or there are no children in the list, then return 522 * Integer.MIN_VALUE. Note that the returned value will be <= 0 because the first item in the 523 * list cannot have a positive offset. 524 */ 525 public static int getFirstListItemOffset(ListView listView) { 526 if (listView == null || listView.getChildCount() == 0 || 527 listView.getFirstVisiblePosition() != 0) { 528 return Integer.MIN_VALUE; 529 } 530 return listView.getChildAt(0).getTop(); 531 } 532 533 /** 534 * Tries to scroll the first item in the list to the given offset (this can be a no-op if the 535 * list is already in the correct position). 536 * @param listView that should be scrolled 537 * @param offset which should be <= 0 538 */ 539 public static void requestToMoveToOffset(ListView listView, int offset) { 540 // We try to offset the list if the first item in the list is showing (which is presumed 541 // to have a larger height than the desired offset). If the first item in the list is not 542 // visible, then we simply do not scroll the list at all (since it can get complicated to 543 // compute how many items in the list will equal the given offset). Potentially 544 // some animation elsewhere will make the transition smoother for the user to compensate 545 // for this simplification. 546 if (listView == null || listView.getChildCount() == 0 || 547 listView.getFirstVisiblePosition() != 0 || offset > 0) { 548 return; 549 } 550 551 // As an optimization, check if the first item is already at the given offset. 552 if (listView.getChildAt(0).getTop() == offset) { 553 return; 554 } 555 556 listView.setSelectionFromTop(0, offset); 557 } 558 } 559