• 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.ContentUris;
20 import android.content.Context;
21 import android.content.pm.PackageManager;
22 import android.content.pm.PackageManager.NameNotFoundException;
23 import android.content.res.Resources;
24 import android.content.res.Resources.NotFoundException;
25 import android.graphics.drawable.Drawable;
26 import android.net.Uri;
27 import android.provider.ContactsContract;
28 import android.provider.ContactsContract.DisplayNameSources;
29 import android.provider.ContactsContract.StreamItems;
30 import android.text.Html;
31 import android.text.Html.ImageGetter;
32 import android.text.TextUtils;
33 import android.util.Log;
34 import android.view.LayoutInflater;
35 import android.view.MenuItem;
36 import android.view.View;
37 import android.view.ViewGroup;
38 import android.widget.ImageView;
39 import android.widget.ListView;
40 import android.widget.TextView;
41 
42 import com.android.contacts.common.ContactPhotoManager;
43 import com.android.contacts.R;
44 import com.android.contacts.model.Contact;
45 import com.android.contacts.model.RawContact;
46 import com.android.contacts.model.dataitem.DataItem;
47 import com.android.contacts.model.dataitem.OrganizationDataItem;
48 import com.android.contacts.common.preference.ContactsPreferences;
49 import com.android.contacts.util.ContactBadgeUtil;
50 import com.android.contacts.util.HtmlUtils;
51 import com.android.contacts.util.MoreMath;
52 import com.android.contacts.util.StreamItemEntry;
53 import com.android.contacts.util.StreamItemPhotoEntry;
54 import com.google.common.annotations.VisibleForTesting;
55 import com.google.common.collect.Iterables;
56 
57 import java.util.List;
58 
59 /**
60  * This class contains utility methods to bind high-level contact details
61  * (meaning name, phonetic name, job, and attribution) from a
62  * {@link Contact} data object to appropriate {@link View}s.
63  */
64 public class ContactDetailDisplayUtils {
65     private static final String TAG = "ContactDetailDisplayUtils";
66 
67     /**
68      * Tag object used for stream item photos.
69      */
70     public static class StreamPhotoTag {
71         public final StreamItemEntry streamItem;
72         public final StreamItemPhotoEntry streamItemPhoto;
73 
StreamPhotoTag(StreamItemEntry streamItem, StreamItemPhotoEntry streamItemPhoto)74         public StreamPhotoTag(StreamItemEntry streamItem, StreamItemPhotoEntry streamItemPhoto) {
75             this.streamItem = streamItem;
76             this.streamItemPhoto = streamItemPhoto;
77         }
78 
getStreamItemPhotoUri()79         public Uri getStreamItemPhotoUri() {
80             final Uri.Builder builder = StreamItems.CONTENT_URI.buildUpon();
81             ContentUris.appendId(builder, streamItem.getId());
82             builder.appendPath(StreamItems.StreamItemPhotos.CONTENT_DIRECTORY);
83             ContentUris.appendId(builder, streamItemPhoto.getId());
84             return builder.build();
85         }
86     }
87 
ContactDetailDisplayUtils()88     private ContactDetailDisplayUtils() {
89         // Disallow explicit creation of this class.
90     }
91 
92     /**
93      * Returns the display name of the contact, using the current display order setting.
94      * Returns res/string/missing_name if there is no display name.
95      */
getDisplayName(Context context, Contact contactData)96     public static CharSequence getDisplayName(Context context, Contact contactData) {
97         CharSequence displayName = contactData.getDisplayName();
98         CharSequence altDisplayName = contactData.getAltDisplayName();
99         ContactsPreferences prefs = new ContactsPreferences(context);
100         CharSequence styledName = "";
101         if (!TextUtils.isEmpty(displayName) && !TextUtils.isEmpty(altDisplayName)) {
102             if (prefs.getDisplayOrder() == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
103                 styledName = displayName;
104             } else {
105                 styledName = altDisplayName;
106             }
107         } else {
108             styledName = context.getResources().getString(R.string.missing_name);
109         }
110         return styledName;
111     }
112 
113     /**
114      * Returns the phonetic name of the contact or null if there isn't one.
115      */
getPhoneticName(Context context, Contact contactData)116     public static String getPhoneticName(Context context, Contact contactData) {
117         String phoneticName = contactData.getPhoneticName();
118         if (!TextUtils.isEmpty(phoneticName)) {
119             return phoneticName;
120         }
121         return null;
122     }
123 
124     /**
125      * Returns the attribution string for the contact, which may specify the contact directory that
126      * the contact came from. Returns null if there is none applicable.
127      */
getAttribution(Context context, Contact contactData)128     public static String getAttribution(Context context, Contact contactData) {
129         if (contactData.isDirectoryEntry()) {
130             String directoryDisplayName = contactData.getDirectoryDisplayName();
131             String directoryType = contactData.getDirectoryType();
132             String displayName = !TextUtils.isEmpty(directoryDisplayName)
133                     ? directoryDisplayName
134                     : directoryType;
135             return context.getString(R.string.contact_directory_description, displayName);
136         }
137         return null;
138     }
139 
140     /**
141      * Returns the organization of the contact. If several organizations are given,
142      * the first one is used. Returns null if not applicable.
143      */
getCompany(Context context, Contact contactData)144     public static String getCompany(Context context, Contact contactData) {
145         final boolean displayNameIsOrganization = contactData.getDisplayNameSource()
146                 == DisplayNameSources.ORGANIZATION;
147         for (RawContact rawContact : contactData.getRawContacts()) {
148             for (DataItem dataItem : Iterables.filter(
149                     rawContact.getDataItems(), OrganizationDataItem.class)) {
150                 OrganizationDataItem organization = (OrganizationDataItem) dataItem;
151                 final String company = organization.getCompany();
152                 final String title = organization.getTitle();
153                 final String combined;
154                 // We need to show company and title in a combined string. However, if the
155                 // DisplayName is already the organization, it mirrors company or (if company
156                 // is empty title). Make sure we don't show what's already shown as DisplayName
157                 if (TextUtils.isEmpty(company)) {
158                     combined = displayNameIsOrganization ? null : title;
159                 } else {
160                     if (TextUtils.isEmpty(title)) {
161                         combined = displayNameIsOrganization ? null : company;
162                     } else {
163                         if (displayNameIsOrganization) {
164                             combined = title;
165                         } else {
166                             combined = context.getString(
167                                     R.string.organization_company_and_title,
168                                     company, title);
169                         }
170                     }
171                 }
172 
173                 if (!TextUtils.isEmpty(combined)) {
174                     return combined;
175                 }
176             }
177         }
178         return null;
179     }
180 
181     /**
182      * Sets the starred state of this contact.
183      */
configureStarredImageView(ImageView starredView, boolean isDirectoryEntry, boolean isUserProfile, boolean isStarred)184     public static void configureStarredImageView(ImageView starredView, boolean isDirectoryEntry,
185             boolean isUserProfile, boolean isStarred) {
186         // Check if the starred state should be visible
187         if (!isDirectoryEntry && !isUserProfile) {
188             starredView.setVisibility(View.VISIBLE);
189             final int resId = isStarred
190                     ? R.drawable.btn_star_on_normal_holo_light
191                     : R.drawable.btn_star_off_normal_holo_light;
192             starredView.setImageResource(resId);
193             starredView.setTag(isStarred);
194             starredView.setContentDescription(starredView.getResources().getString(
195                     isStarred ? R.string.menu_removeStar : R.string.menu_addStar));
196         } else {
197             starredView.setVisibility(View.GONE);
198         }
199     }
200 
201     /**
202      * Sets the starred state of this contact.
203      */
configureStarredMenuItem(MenuItem starredMenuItem, boolean isDirectoryEntry, boolean isUserProfile, boolean isStarred)204     public static void configureStarredMenuItem(MenuItem starredMenuItem, boolean isDirectoryEntry,
205             boolean isUserProfile, boolean isStarred) {
206         // Check if the starred state should be visible
207         if (!isDirectoryEntry && !isUserProfile) {
208             starredMenuItem.setVisible(true);
209             final int resId = isStarred
210                     ? R.drawable.btn_star_on_normal_holo_dark
211                     : R.drawable.btn_star_off_normal_holo_dark;
212             starredMenuItem.setIcon(resId);
213             starredMenuItem.setChecked(isStarred);
214             starredMenuItem.setTitle(isStarred ? R.string.menu_removeStar : R.string.menu_addStar);
215         } else {
216             starredMenuItem.setVisible(false);
217         }
218     }
219 
220     /**
221      * Set the social snippet text. If there isn't one, then set the view to gone.
222      */
setSocialSnippet(Context context, Contact contactData, TextView statusView, ImageView statusPhotoView)223     public static void setSocialSnippet(Context context, Contact contactData, TextView statusView,
224             ImageView statusPhotoView) {
225         if (statusView == null) {
226             return;
227         }
228 
229         CharSequence snippet = null;
230         String photoUri = null;
231         if (!contactData.getStreamItems().isEmpty()) {
232             StreamItemEntry firstEntry = contactData.getStreamItems().get(0);
233             snippet = HtmlUtils.fromHtml(context, firstEntry.getText());
234             if (!firstEntry.getPhotos().isEmpty()) {
235                 StreamItemPhotoEntry firstPhoto = firstEntry.getPhotos().get(0);
236                 photoUri = firstPhoto.getPhotoUri();
237 
238                 // If displaying an image, hide the snippet text.
239                 snippet = null;
240             }
241         }
242         setDataOrHideIfNone(snippet, statusView);
243         if (photoUri != null) {
244             ContactPhotoManager.getInstance(context).loadPhoto(
245                     statusPhotoView, Uri.parse(photoUri), -1, false,
246                     ContactPhotoManager.DEFAULT_BLANK);
247             statusPhotoView.setVisibility(View.VISIBLE);
248         } else {
249             statusPhotoView.setVisibility(View.GONE);
250         }
251     }
252 
253     /** Creates the view that represents a stream item. */
createStreamItemView(LayoutInflater inflater, Context context, View convertView, StreamItemEntry streamItem, View.OnClickListener photoClickListener)254     public static View createStreamItemView(LayoutInflater inflater, Context context,
255             View convertView, StreamItemEntry streamItem, View.OnClickListener photoClickListener) {
256 
257         // Try to recycle existing views.
258         final View container;
259         if (convertView != null) {
260             container = convertView;
261         } else {
262             container = inflater.inflate(R.layout.stream_item_container, null, false);
263         }
264 
265         final ContactPhotoManager contactPhotoManager = ContactPhotoManager.getInstance(context);
266         final List<StreamItemPhotoEntry> photos = streamItem.getPhotos();
267         final int photoCount = photos.size();
268 
269         // Add the text part.
270         addStreamItemText(context, streamItem, container);
271 
272         // Add images.
273         final ViewGroup imageRows = (ViewGroup) container.findViewById(R.id.stream_item_image_rows);
274 
275         if (photoCount == 0) {
276             // This stream item only has text.
277             imageRows.setVisibility(View.GONE);
278         } else {
279             // This stream item has text and photos.
280             imageRows.setVisibility(View.VISIBLE);
281 
282             // Number of image rows needed, which is cailing(photoCount / 2)
283             final int numImageRows = (photoCount + 1) / 2;
284 
285             // Actual image rows.
286             final int numOldImageRows = imageRows.getChildCount();
287 
288             // Make sure we have enough stream_item_row_images.
289             if (numOldImageRows == numImageRows) {
290                 // Great, we have the just enough number of rows...
291 
292             } else if (numOldImageRows < numImageRows) {
293                 // Need to add more image rows.
294                 for (int i = numOldImageRows; i < numImageRows; i++) {
295                     View imageRow = inflater.inflate(R.layout.stream_item_row_images, imageRows,
296                             true);
297                 }
298             } else {
299                 // We have exceeding image rows.  Hide them.
300                 for (int i = numImageRows; i < numOldImageRows; i++) {
301                     imageRows.getChildAt(i).setVisibility(View.GONE);
302                 }
303             }
304 
305             // Put images, two by two.
306             for (int i = 0; i < photoCount; i += 2) {
307                 final View imageRow = imageRows.getChildAt(i / 2);
308                 // Reused image rows may not visible, so make sure they're shown.
309                 imageRow.setVisibility(View.VISIBLE);
310 
311                 // Show first image.
312                 loadPhoto(contactPhotoManager, streamItem, photos.get(i), imageRow,
313                         R.id.stream_item_first_image, photoClickListener);
314                 final View secondContainer = imageRow.findViewById(R.id.second_image_container);
315                 if (i + 1 < photoCount) {
316                     // Show the second image too.
317                     loadPhoto(contactPhotoManager, streamItem, photos.get(i + 1), imageRow,
318                             R.id.stream_item_second_image, photoClickListener);
319                     secondContainer.setVisibility(View.VISIBLE);
320                 } else {
321                     // Hide the second image, but it still has to occupy the space.
322                     secondContainer.setVisibility(View.INVISIBLE);
323                 }
324             }
325         }
326 
327         return container;
328     }
329 
330     /** Loads a photo into an image view. The image view is identified by the given id. */
loadPhoto(ContactPhotoManager contactPhotoManager, final StreamItemEntry streamItem, final StreamItemPhotoEntry streamItemPhoto, View photoContainer, int imageViewId, View.OnClickListener photoClickListener)331     private static void loadPhoto(ContactPhotoManager contactPhotoManager,
332             final StreamItemEntry streamItem, final StreamItemPhotoEntry streamItemPhoto,
333             View photoContainer, int imageViewId, View.OnClickListener photoClickListener) {
334         final View frame = photoContainer.findViewById(imageViewId);
335         final View pushLayerView = frame.findViewById(R.id.push_layer);
336         final ImageView imageView = (ImageView) frame.findViewById(R.id.image);
337         if (photoClickListener != null) {
338             pushLayerView.setOnClickListener(photoClickListener);
339             pushLayerView.setTag(new StreamPhotoTag(streamItem, streamItemPhoto));
340             pushLayerView.setFocusable(true);
341             pushLayerView.setEnabled(true);
342         } else {
343             pushLayerView.setOnClickListener(null);
344             pushLayerView.setTag(null);
345             pushLayerView.setFocusable(false);
346             // setOnClickListener makes it clickable, so we need to overwrite it
347             pushLayerView.setClickable(false);
348             pushLayerView.setEnabled(false);
349         }
350         contactPhotoManager.loadPhoto(imageView, Uri.parse(streamItemPhoto.getPhotoUri()), -1,
351                 false, ContactPhotoManager.DEFAULT_BLANK);
352     }
353 
354     @VisibleForTesting
addStreamItemText(Context context, StreamItemEntry streamItem, View rootView)355     static View addStreamItemText(Context context, StreamItemEntry streamItem, View rootView) {
356         TextView htmlView = (TextView) rootView.findViewById(R.id.stream_item_html);
357         TextView attributionView = (TextView) rootView.findViewById(
358                 R.id.stream_item_attribution);
359         TextView commentsView = (TextView) rootView.findViewById(R.id.stream_item_comments);
360         ImageGetter imageGetter = new DefaultImageGetter(context.getPackageManager());
361 
362         // Stream item text
363         setDataOrHideIfNone(streamItem.getDecodedText(), htmlView);
364         // Attribution
365         setDataOrHideIfNone(ContactBadgeUtil.getSocialDate(streamItem, context),
366                 attributionView);
367         // Comments
368         setDataOrHideIfNone(streamItem.getDecodedComments(), commentsView);
369         return rootView;
370     }
371 
372     /**
373      * Sets the display name of this contact to the given {@link TextView}. If
374      * there is none, then set the view to gone.
375      */
setDisplayName(Context context, Contact contactData, TextView textView)376     public static void setDisplayName(Context context, Contact contactData, TextView textView) {
377         if (textView == null) {
378             return;
379         }
380         setDataOrHideIfNone(getDisplayName(context, contactData), textView);
381     }
382 
383     /**
384      * Sets the company and job title of this contact to the given {@link TextView}. If
385      * there is none, then set the view to gone.
386      */
setCompanyName(Context context, Contact contactData, TextView textView)387     public static void setCompanyName(Context context, Contact contactData, TextView textView) {
388         if (textView == null) {
389             return;
390         }
391         setDataOrHideIfNone(getCompany(context, contactData), textView);
392     }
393 
394     /**
395      * Sets the phonetic name of this contact to the given {@link TextView}. If
396      * there is none, then set the view to gone.
397      */
setPhoneticName(Context context, Contact contactData, TextView textView)398     public static void setPhoneticName(Context context, Contact contactData, TextView textView) {
399         if (textView == null) {
400             return;
401         }
402         setDataOrHideIfNone(getPhoneticName(context, contactData), textView);
403     }
404 
405     /**
406      * Sets the attribution contact to the given {@link TextView}. If
407      * there is none, then set the view to gone.
408      */
setAttribution(Context context, Contact contactData, TextView textView)409     public static void setAttribution(Context context, Contact contactData, TextView textView) {
410         if (textView == null) {
411             return;
412         }
413         setDataOrHideIfNone(getAttribution(context, contactData), textView);
414     }
415 
416     /**
417      * Helper function to display the given text in the {@link TextView} or
418      * hides the {@link TextView} if the text is empty or null.
419      */
setDataOrHideIfNone(CharSequence textToDisplay, TextView textView)420     private static void setDataOrHideIfNone(CharSequence textToDisplay, TextView textView) {
421         if (!TextUtils.isEmpty(textToDisplay)) {
422             textView.setText(textToDisplay);
423             textView.setVisibility(View.VISIBLE);
424         } else {
425             textView.setText(null);
426             textView.setVisibility(View.GONE);
427         }
428     }
429 
430     private static Html.ImageGetter sImageGetter;
431 
getImageGetter(Context context)432     public static Html.ImageGetter getImageGetter(Context context) {
433         if (sImageGetter == null) {
434             sImageGetter = new DefaultImageGetter(context.getPackageManager());
435         }
436         return sImageGetter;
437     }
438 
439     /** Fetcher for images from resources to be included in HTML text. */
440     private static class DefaultImageGetter implements Html.ImageGetter {
441         /** The scheme used to load resources. */
442         private static final String RES_SCHEME = "res";
443 
444         private final PackageManager mPackageManager;
445 
DefaultImageGetter(PackageManager packageManager)446         public DefaultImageGetter(PackageManager packageManager) {
447             mPackageManager = packageManager;
448         }
449 
450         @Override
getDrawable(String source)451         public Drawable getDrawable(String source) {
452             // Returning null means that a default image will be used.
453             Uri uri;
454             try {
455                 uri = Uri.parse(source);
456             } catch (Throwable e) {
457                 Log.d(TAG, "Could not parse image source: " + source);
458                 return null;
459             }
460             if (!RES_SCHEME.equals(uri.getScheme())) {
461                 Log.d(TAG, "Image source does not correspond to a resource: " + source);
462                 return null;
463             }
464             // The URI authority represents the package name.
465             String packageName = uri.getAuthority();
466 
467             Resources resources = getResourcesForResourceName(packageName);
468             if (resources == null) {
469                 Log.d(TAG, "Could not parse image source: " + source);
470                 return null;
471             }
472 
473             List<String> pathSegments = uri.getPathSegments();
474             if (pathSegments.size() != 1) {
475                 Log.d(TAG, "Could not parse image source: " + source);
476                 return null;
477             }
478 
479             final String name = pathSegments.get(0);
480             final int resId = resources.getIdentifier(name, "drawable", packageName);
481 
482             if (resId == 0) {
483                 // Use the default image icon in this case.
484                 Log.d(TAG, "Cannot resolve resource identifier: " + source);
485                 return null;
486             }
487 
488             try {
489                 return getResourceDrawable(resources, resId);
490             } catch (NotFoundException e) {
491                 Log.d(TAG, "Resource not found: " + source, e);
492                 return null;
493             }
494         }
495 
496         /** Returns the drawable associated with the given id. */
getResourceDrawable(Resources resources, int resId)497         private Drawable getResourceDrawable(Resources resources, int resId)
498                 throws NotFoundException {
499             Drawable drawable = resources.getDrawable(resId);
500             drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
501             return drawable;
502         }
503 
504         /** Returns the {@link Resources} of the package of the given resource name. */
getResourcesForResourceName(String packageName)505         private Resources getResourcesForResourceName(String packageName) {
506             try {
507                 return mPackageManager.getResourcesForApplication(packageName);
508             } catch (NameNotFoundException e) {
509                 Log.d(TAG, "Could not find package: " + packageName);
510                 return null;
511             }
512         }
513     }
514 
515     /**
516      * Sets an alpha value on the view.
517      */
setAlphaOnViewBackground(View view, float alpha)518     public static void setAlphaOnViewBackground(View view, float alpha) {
519         if (view != null) {
520             // Convert alpha layer to a black background HEX color with an alpha value for better
521             // performance (i.e. use setBackgroundColor() instead of setAlpha())
522             view.setBackgroundColor((int) (MoreMath.clamp(alpha, 0.0f, 1.0f) * 255) << 24);
523         }
524     }
525 
526     /**
527      * Returns the top coordinate of the first item in the {@link ListView}. If the first item
528      * in the {@link ListView} is not visible or there are no children in the list, then return
529      * Integer.MIN_VALUE. Note that the returned value will be <= 0 because the first item in the
530      * list cannot have a positive offset.
531      */
getFirstListItemOffset(ListView listView)532     public static int getFirstListItemOffset(ListView listView) {
533         if (listView == null || listView.getChildCount() == 0 ||
534                 listView.getFirstVisiblePosition() != 0) {
535             return Integer.MIN_VALUE;
536         }
537         return listView.getChildAt(0).getTop();
538     }
539 
540     /**
541      * Tries to scroll the first item in the list to the given offset (this can be a no-op if the
542      * list is already in the correct position).
543      * @param listView that should be scrolled
544      * @param offset which should be <= 0
545      */
requestToMoveToOffset(ListView listView, int offset)546     public static void requestToMoveToOffset(ListView listView, int offset) {
547         // We try to offset the list if the first item in the list is showing (which is presumed
548         // to have a larger height than the desired offset). If the first item in the list is not
549         // visible, then we simply do not scroll the list at all (since it can get complicated to
550         // compute how many items in the list will equal the given offset). Potentially
551         // some animation elsewhere will make the transition smoother for the user to compensate
552         // for this simplification.
553         if (listView == null || listView.getChildCount() == 0 ||
554                 listView.getFirstVisiblePosition() != 0 || offset > 0) {
555             return;
556         }
557 
558         // As an optimization, check if the first item is already at the given offset.
559         if (listView.getChildAt(0).getTop() == offset) {
560             return;
561         }
562 
563         listView.setSelectionFromTop(0, offset);
564     }
565 }
566