• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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