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.MoreMath; 27import com.android.contacts.util.StreamItemEntry; 28import com.android.contacts.util.StreamItemPhotoEntry; 29import com.google.common.annotations.VisibleForTesting; 30 31import android.content.ContentUris; 32import android.content.ContentValues; 33import android.content.Context; 34import android.content.Entity; 35import android.content.Entity.NamedContentValues; 36import android.content.pm.PackageManager; 37import android.content.pm.PackageManager.NameNotFoundException; 38import android.content.res.Resources; 39import android.content.res.Resources.NotFoundException; 40import android.graphics.drawable.Drawable; 41import android.net.Uri; 42import android.provider.ContactsContract; 43import android.provider.ContactsContract.CommonDataKinds.Organization; 44import android.provider.ContactsContract.Data; 45import android.provider.ContactsContract.DisplayNameSources; 46import android.provider.ContactsContract.StreamItems; 47import android.text.Html; 48import android.text.Html.ImageGetter; 49import android.text.TextUtils; 50import android.util.Log; 51import android.view.LayoutInflater; 52import android.view.MenuItem; 53import android.view.View; 54import android.view.ViewGroup; 55import android.widget.ImageView; 56import android.widget.ListView; 57import android.widget.TextView; 58 59import java.util.List; 60 61/** 62 * This class contains utility methods to bind high-level contact details 63 * (meaning name, phonetic name, job, and attribution) from a 64 * {@link ContactLoader.Result} data object to appropriate {@link View}s. 65 */ 66public class ContactDetailDisplayUtils { 67 private static final String TAG = "ContactDetailDisplayUtils"; 68 69 /** 70 * Tag object used for stream item photos. 71 */ 72 public static class StreamPhotoTag { 73 public final StreamItemEntry streamItem; 74 public final StreamItemPhotoEntry streamItemPhoto; 75 76 public StreamPhotoTag(StreamItemEntry streamItem, StreamItemPhotoEntry streamItemPhoto) { 77 this.streamItem = streamItem; 78 this.streamItemPhoto = streamItemPhoto; 79 } 80 81 public Uri getStreamItemPhotoUri() { 82 final Uri.Builder builder = StreamItems.CONTENT_URI.buildUpon(); 83 ContentUris.appendId(builder, streamItem.getId()); 84 builder.appendPath(StreamItems.StreamItemPhotos.CONTENT_DIRECTORY); 85 ContentUris.appendId(builder, streamItemPhoto.getId()); 86 return builder.build(); 87 } 88 } 89 90 private ContactDetailDisplayUtils() { 91 // Disallow explicit creation of this class. 92 } 93 94 /** 95 * Returns the display name of the contact, using the current display order setting. 96 * Returns res/string/missing_name if there is no display name. 97 */ 98 public static CharSequence getDisplayName(Context context, Result contactData) { 99 CharSequence displayName = contactData.getDisplayName(); 100 CharSequence altDisplayName = contactData.getAltDisplayName(); 101 ContactsPreferences prefs = new ContactsPreferences(context); 102 CharSequence styledName = ""; 103 if (!TextUtils.isEmpty(displayName) && !TextUtils.isEmpty(altDisplayName)) { 104 if (prefs.getDisplayOrder() == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) { 105 styledName = displayName; 106 } else { 107 styledName = altDisplayName; 108 } 109 } else { 110 styledName = context.getResources().getString(R.string.missing_name); 111 } 112 return styledName; 113 } 114 115 /** 116 * Returns the phonetic name of the contact or null if there isn't one. 117 */ 118 public static String getPhoneticName(Context context, Result contactData) { 119 String phoneticName = contactData.getPhoneticName(); 120 if (!TextUtils.isEmpty(phoneticName)) { 121 return phoneticName; 122 } 123 return null; 124 } 125 126 /** 127 * Returns the attribution string for the contact, which may specify the contact directory that 128 * the contact came from. Returns null if there is none applicable. 129 */ 130 public static String getAttribution(Context context, Result contactData) { 131 if (contactData.isDirectoryEntry()) { 132 String directoryDisplayName = contactData.getDirectoryDisplayName(); 133 String directoryType = contactData.getDirectoryType(); 134 String displayName = !TextUtils.isEmpty(directoryDisplayName) 135 ? directoryDisplayName 136 : directoryType; 137 return context.getString(R.string.contact_directory_description, displayName); 138 } 139 return null; 140 } 141 142 /** 143 * Returns the organization of the contact. If several organizations are given, 144 * the first one is used. Returns null if not applicable. 145 */ 146 public static String getCompany(Context context, Result contactData) { 147 final boolean displayNameIsOrganization = contactData.getDisplayNameSource() 148 == DisplayNameSources.ORGANIZATION; 149 for (Entity entity : contactData.getEntities()) { 150 for (NamedContentValues subValue : entity.getSubValues()) { 151 final ContentValues entryValues = subValue.values; 152 final String mimeType = entryValues.getAsString(Data.MIMETYPE); 153 154 if (Organization.CONTENT_ITEM_TYPE.equals(mimeType)) { 155 final String company = entryValues.getAsString(Organization.COMPANY); 156 final String title = entryValues.getAsString(Organization.TITLE); 157 final String combined; 158 // We need to show company and title in a combined string. However, if the 159 // DisplayName is already the organization, it mirrors company or (if company 160 // is empty title). Make sure we don't show what's already shown as DisplayName 161 if (TextUtils.isEmpty(company)) { 162 combined = displayNameIsOrganization ? null : title; 163 } else { 164 if (TextUtils.isEmpty(title)) { 165 combined = displayNameIsOrganization ? null : company; 166 } else { 167 if (displayNameIsOrganization) { 168 combined = title; 169 } else { 170 combined = context.getString( 171 R.string.organization_company_and_title, 172 company, title); 173 } 174 } 175 } 176 177 if (!TextUtils.isEmpty(combined)) { 178 return combined; 179 } 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_dark 216 : R.drawable.btn_star_off_normal_holo_dark; 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, Result 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, Result 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, Result 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, Result 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, Result 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