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