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