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.google.common.collect.Iterables; 20 21import com.android.contacts.R; 22import com.android.contacts.common.model.Contact; 23import com.android.contacts.common.model.RawContact; 24import com.android.contacts.common.model.dataitem.DataItem; 25import com.android.contacts.common.model.dataitem.OrganizationDataItem; 26import com.android.contacts.common.preference.ContactsPreferences; 27import com.android.contacts.util.MoreMath; 28 29import android.content.Context; 30import android.content.pm.PackageManager; 31import android.content.pm.PackageManager.NameNotFoundException; 32import android.content.res.Resources; 33import android.content.res.Resources.NotFoundException; 34import android.graphics.drawable.Drawable; 35import android.net.Uri; 36import android.provider.ContactsContract.DisplayNameSources; 37import android.text.BidiFormatter; 38import android.text.Html; 39import android.text.TextDirectionHeuristics; 40import android.text.TextUtils; 41import android.util.Log; 42import android.view.MenuItem; 43import android.view.View; 44import android.widget.ImageView; 45import android.widget.ListView; 46import android.widget.TextView; 47 48import java.util.List; 49 50/** 51 * This class contains utility methods to bind high-level contact details 52 * (meaning name, phonetic name, job, and attribution) from a 53 * {@link Contact} data object to appropriate {@link View}s. 54 */ 55public class ContactDisplayUtils { 56 private static final String TAG = "ContactDisplayUtils"; 57 private static BidiFormatter sBidiFormatter = BidiFormatter.getInstance(); 58 59 /** 60 * Returns the display name of the contact, using the current display order setting. 61 * Returns res/string/missing_name if there is no display name. 62 */ 63 public static CharSequence getDisplayName(Context context, Contact contactData) { 64 ContactsPreferences prefs = new ContactsPreferences(context); 65 final CharSequence displayName = contactData.getDisplayName(); 66 if (prefs.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY) { 67 if (!TextUtils.isEmpty(displayName)) { 68 if (contactData.getDisplayNameSource() == DisplayNameSources.PHONE) { 69 return sBidiFormatter.unicodeWrap( 70 displayName.toString(), TextDirectionHeuristics.LTR); 71 } 72 return displayName; 73 } 74 } else { 75 final CharSequence altDisplayName = contactData.getAltDisplayName(); 76 if (!TextUtils.isEmpty(altDisplayName)) { 77 return altDisplayName; 78 } 79 } 80 return context.getResources().getString(R.string.missing_name); 81 } 82 83 /** 84 * Returns the phonetic name of the contact or null if there isn't one. 85 */ 86 public static String getPhoneticName(Context context, Contact contactData) { 87 String phoneticName = contactData.getPhoneticName(); 88 if (!TextUtils.isEmpty(phoneticName)) { 89 return phoneticName; 90 } 91 return null; 92 } 93 94 /** 95 * Returns the attribution string for the contact, which may specify the contact directory that 96 * the contact came from. Returns null if there is none applicable. 97 */ 98 public static String getAttribution(Context context, Contact contactData) { 99 if (contactData.isDirectoryEntry()) { 100 String directoryDisplayName = contactData.getDirectoryDisplayName(); 101 String directoryType = contactData.getDirectoryType(); 102 final String displayName; 103 if (!TextUtils.isEmpty(directoryDisplayName)) { 104 displayName = directoryDisplayName; 105 } else if (!TextUtils.isEmpty(directoryType)) { 106 displayName = directoryType; 107 } else { 108 return null; 109 } 110 return context.getString(R.string.contact_directory_description, displayName); 111 } 112 return null; 113 } 114 115 /** 116 * Returns the organization of the contact. If several organizations are given, 117 * the first one is used. Returns null if not applicable. 118 */ 119 public static String getCompany(Context context, Contact contactData) { 120 final boolean displayNameIsOrganization = contactData.getDisplayNameSource() 121 == DisplayNameSources.ORGANIZATION; 122 for (RawContact rawContact : contactData.getRawContacts()) { 123 for (DataItem dataItem : Iterables.filter( 124 rawContact.getDataItems(), OrganizationDataItem.class)) { 125 OrganizationDataItem organization = (OrganizationDataItem) dataItem; 126 final String company = organization.getCompany(); 127 final String title = organization.getTitle(); 128 final String combined; 129 // We need to show company and title in a combined string. However, if the 130 // DisplayName is already the organization, it mirrors company or (if company 131 // is empty title). Make sure we don't show what's already shown as DisplayName 132 if (TextUtils.isEmpty(company)) { 133 combined = displayNameIsOrganization ? null : title; 134 } else { 135 if (TextUtils.isEmpty(title)) { 136 combined = displayNameIsOrganization ? null : company; 137 } else { 138 if (displayNameIsOrganization) { 139 combined = title; 140 } else { 141 combined = context.getString( 142 R.string.organization_company_and_title, 143 company, title); 144 } 145 } 146 } 147 148 if (!TextUtils.isEmpty(combined)) { 149 return combined; 150 } 151 } 152 } 153 return null; 154 } 155 156 /** 157 * Sets the starred state of this contact. 158 */ 159 public static void configureStarredImageView(ImageView starredView, boolean isDirectoryEntry, 160 boolean isUserProfile, boolean isStarred) { 161 // Check if the starred state should be visible 162 if (!isDirectoryEntry && !isUserProfile) { 163 starredView.setVisibility(View.VISIBLE); 164 final int resId = isStarred 165 ? R.drawable.btn_star_on_normal_holo_light 166 : R.drawable.btn_star_off_normal_holo_light; 167 starredView.setImageResource(resId); 168 starredView.setTag(isStarred); 169 starredView.setContentDescription(starredView.getResources().getString( 170 isStarred ? R.string.menu_removeStar : R.string.menu_addStar)); 171 } else { 172 starredView.setVisibility(View.GONE); 173 } 174 } 175 176 /** 177 * Sets the starred state of this contact. 178 */ 179 public static void configureStarredMenuItem(MenuItem starredMenuItem, boolean isDirectoryEntry, 180 boolean isUserProfile, boolean isStarred) { 181 // Check if the starred state should be visible 182 if (!isDirectoryEntry && !isUserProfile) { 183 starredMenuItem.setVisible(true); 184 final int resId = isStarred 185 ? R.drawable.ic_star_24dp 186 : R.drawable.ic_star_outline_24dp; 187 starredMenuItem.setIcon(resId); 188 starredMenuItem.setChecked(isStarred); 189 starredMenuItem.setTitle(isStarred ? R.string.menu_removeStar : R.string.menu_addStar); 190 } else { 191 starredMenuItem.setVisible(false); 192 } 193 } 194 195 /** 196 * Sets the display name of this contact to the given {@link TextView}. If 197 * there is none, then set the view to gone. 198 */ 199 public static void setDisplayName(Context context, Contact contactData, TextView textView) { 200 if (textView == null) { 201 return; 202 } 203 setDataOrHideIfNone(getDisplayName(context, contactData), textView); 204 } 205 206 /** 207 * Sets the company and job title of this contact to the given {@link TextView}. If 208 * there is none, then set the view to gone. 209 */ 210 public static void setCompanyName(Context context, Contact contactData, TextView textView) { 211 if (textView == null) { 212 return; 213 } 214 setDataOrHideIfNone(getCompany(context, contactData), textView); 215 } 216 217 /** 218 * Sets the phonetic name of this contact to the given {@link TextView}. If 219 * there is none, then set the view to gone. 220 */ 221 public static void setPhoneticName(Context context, Contact contactData, TextView textView) { 222 if (textView == null) { 223 return; 224 } 225 setDataOrHideIfNone(getPhoneticName(context, contactData), textView); 226 } 227 228 /** 229 * Sets the attribution contact to the given {@link TextView}. If 230 * there is none, then set the view to gone. 231 */ 232 public static void setAttribution(Context context, Contact contactData, TextView textView) { 233 if (textView == null) { 234 return; 235 } 236 setDataOrHideIfNone(getAttribution(context, contactData), textView); 237 } 238 239 /** 240 * Helper function to display the given text in the {@link TextView} or 241 * hides the {@link TextView} if the text is empty or null. 242 */ 243 private static void setDataOrHideIfNone(CharSequence textToDisplay, TextView textView) { 244 if (!TextUtils.isEmpty(textToDisplay)) { 245 textView.setText(textToDisplay); 246 textView.setVisibility(View.VISIBLE); 247 } else { 248 textView.setText(null); 249 textView.setVisibility(View.GONE); 250 } 251 } 252 253 private static Html.ImageGetter sImageGetter; 254 255 public static Html.ImageGetter getImageGetter(Context context) { 256 if (sImageGetter == null) { 257 sImageGetter = new DefaultImageGetter(context.getPackageManager()); 258 } 259 return sImageGetter; 260 } 261 262 /** Fetcher for images from resources to be included in HTML text. */ 263 private static class DefaultImageGetter implements Html.ImageGetter { 264 /** The scheme used to load resources. */ 265 private static final String RES_SCHEME = "res"; 266 267 private final PackageManager mPackageManager; 268 269 public DefaultImageGetter(PackageManager packageManager) { 270 mPackageManager = packageManager; 271 } 272 273 @Override 274 public Drawable getDrawable(String source) { 275 // Returning null means that a default image will be used. 276 Uri uri; 277 try { 278 uri = Uri.parse(source); 279 } catch (Throwable e) { 280 Log.d(TAG, "Could not parse image source: " + source); 281 return null; 282 } 283 if (!RES_SCHEME.equals(uri.getScheme())) { 284 Log.d(TAG, "Image source does not correspond to a resource: " + source); 285 return null; 286 } 287 // The URI authority represents the package name. 288 String packageName = uri.getAuthority(); 289 290 Resources resources = getResourcesForResourceName(packageName); 291 if (resources == null) { 292 Log.d(TAG, "Could not parse image source: " + source); 293 return null; 294 } 295 296 List<String> pathSegments = uri.getPathSegments(); 297 if (pathSegments.size() != 1) { 298 Log.d(TAG, "Could not parse image source: " + source); 299 return null; 300 } 301 302 final String name = pathSegments.get(0); 303 final int resId = resources.getIdentifier(name, "drawable", packageName); 304 305 if (resId == 0) { 306 // Use the default image icon in this case. 307 Log.d(TAG, "Cannot resolve resource identifier: " + source); 308 return null; 309 } 310 311 try { 312 return getResourceDrawable(resources, resId); 313 } catch (NotFoundException e) { 314 Log.d(TAG, "Resource not found: " + source, e); 315 return null; 316 } 317 } 318 319 /** Returns the drawable associated with the given id. */ 320 private Drawable getResourceDrawable(Resources resources, int resId) 321 throws NotFoundException { 322 Drawable drawable = resources.getDrawable(resId); 323 drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); 324 return drawable; 325 } 326 327 /** Returns the {@link Resources} of the package of the given resource name. */ 328 private Resources getResourcesForResourceName(String packageName) { 329 try { 330 return mPackageManager.getResourcesForApplication(packageName); 331 } catch (NameNotFoundException e) { 332 Log.d(TAG, "Could not find package: " + packageName); 333 return null; 334 } 335 } 336 } 337 338 /** 339 * Sets an alpha value on the view. 340 */ 341 public static void setAlphaOnViewBackground(View view, float alpha) { 342 if (view != null) { 343 // Convert alpha layer to a black background HEX color with an alpha value for better 344 // performance (i.e. use setBackgroundColor() instead of setAlpha()) 345 view.setBackgroundColor((int) (MoreMath.clamp(alpha, 0.0f, 1.0f) * 255) << 24); 346 } 347 } 348 349 /** 350 * Returns the top coordinate of the first item in the {@link ListView}. If the first item 351 * in the {@link ListView} is not visible or there are no children in the list, then return 352 * Integer.MIN_VALUE. Note that the returned value will be <= 0 because the first item in the 353 * list cannot have a positive offset. 354 */ 355 public static int getFirstListItemOffset(ListView listView) { 356 if (listView == null || listView.getChildCount() == 0 || 357 listView.getFirstVisiblePosition() != 0) { 358 return Integer.MIN_VALUE; 359 } 360 return listView.getChildAt(0).getTop(); 361 } 362 363 /** 364 * Tries to scroll the first item in the list to the given offset (this can be a no-op if the 365 * list is already in the correct position). 366 * @param listView that should be scrolled 367 * @param offset which should be <= 0 368 */ 369 public static void requestToMoveToOffset(ListView listView, int offset) { 370 // We try to offset the list if the first item in the list is showing (which is presumed 371 // to have a larger height than the desired offset). If the first item in the list is not 372 // visible, then we simply do not scroll the list at all (since it can get complicated to 373 // compute how many items in the list will equal the given offset). Potentially 374 // some animation elsewhere will make the transition smoother for the user to compensate 375 // for this simplification. 376 if (listView == null || listView.getChildCount() == 0 || 377 listView.getFirstVisiblePosition() != 0 || offset > 0) { 378 return; 379 } 380 381 // As an optimization, check if the first item is already at the given offset. 382 if (listView.getChildAt(0).getTop() == offset) { 383 return; 384 } 385 386 listView.setSelectionFromTop(0, offset); 387 } 388} 389