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