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