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 com.android.contacts.ContactLoader; 20 import com.android.contacts.ContactLoader.Result; 21 import com.android.contacts.ContactPhotoManager; 22 import com.android.contacts.R; 23 import com.android.contacts.preference.ContactsPreferences; 24 import com.android.contacts.util.ContactBadgeUtil; 25 import com.android.contacts.util.HtmlUtils; 26 import com.android.contacts.util.MoreMath; 27 import com.android.contacts.util.StreamItemEntry; 28 import com.android.contacts.util.StreamItemPhotoEntry; 29 import com.google.common.annotations.VisibleForTesting; 30 31 import android.content.ContentUris; 32 import android.content.ContentValues; 33 import android.content.Context; 34 import android.content.Entity; 35 import android.content.Entity.NamedContentValues; 36 import android.content.pm.PackageManager; 37 import android.content.pm.PackageManager.NameNotFoundException; 38 import android.content.res.Resources; 39 import android.content.res.Resources.NotFoundException; 40 import android.graphics.drawable.Drawable; 41 import android.net.Uri; 42 import android.provider.ContactsContract; 43 import android.provider.ContactsContract.CommonDataKinds.Organization; 44 import android.provider.ContactsContract.Data; 45 import android.provider.ContactsContract.DisplayNameSources; 46 import android.provider.ContactsContract.StreamItems; 47 import android.text.Html; 48 import android.text.Html.ImageGetter; 49 import android.text.TextUtils; 50 import android.util.Log; 51 import android.view.LayoutInflater; 52 import android.view.MenuItem; 53 import android.view.View; 54 import android.view.ViewGroup; 55 import android.widget.ImageView; 56 import android.widget.ListView; 57 import android.widget.TextView; 58 59 import 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 */ 66 public 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 StreamPhotoTag(StreamItemEntry streamItem, StreamItemPhotoEntry streamItemPhoto)76 public StreamPhotoTag(StreamItemEntry streamItem, StreamItemPhotoEntry streamItemPhoto) { 77 this.streamItem = streamItem; 78 this.streamItemPhoto = streamItemPhoto; 79 } 80 getStreamItemPhotoUri()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 ContactDetailDisplayUtils()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 */ getDisplayName(Context context, Result contactData)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 */ getPhoneticName(Context context, Result contactData)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 */ getAttribution(Context context, Result contactData)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 */ getCompany(Context context, Result contactData)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 */ configureStarredImageView(ImageView starredView, boolean isDirectoryEntry, boolean isUserProfile, boolean isStarred)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 */ configureStarredMenuItem(MenuItem starredMenuItem, boolean isDirectoryEntry, boolean isUserProfile, boolean isStarred)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 */ setSocialSnippet(Context context, Result contactData, TextView statusView, ImageView statusPhotoView)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. */ createStreamItemView(LayoutInflater inflater, Context context, View convertView, StreamItemEntry streamItem, View.OnClickListener photoClickListener)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. */ loadPhoto(ContactPhotoManager contactPhotoManager, final StreamItemEntry streamItem, final StreamItemPhotoEntry streamItemPhoto, View photoContainer, int imageViewId, View.OnClickListener photoClickListener)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 addStreamItemText(Context context, StreamItemEntry streamItem, View rootView)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 */ setDisplayName(Context context, Result contactData, TextView textView)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 */ setCompanyName(Context context, Result contactData, TextView textView)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 */ setPhoneticName(Context context, Result contactData, TextView textView)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 */ setAttribution(Context context, Result contactData, TextView textView)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 */ setDataOrHideIfNone(CharSequence textToDisplay, TextView textView)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 getImageGetter(Context context)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 DefaultImageGetter(PackageManager packageManager)451 public DefaultImageGetter(PackageManager packageManager) { 452 mPackageManager = packageManager; 453 } 454 455 @Override getDrawable(String source)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. */ getResourceDrawable(Resources resources, int resId)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. */ getResourcesForResourceName(String packageName)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 */ setAlphaOnViewBackground(View view, float alpha)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 */ getFirstListItemOffset(ListView listView)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 */ requestToMoveToOffset(ListView listView, int offset)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