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