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