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 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 
     76         public StreamPhotoTag(StreamItemEntry streamItem, StreamItemPhotoEntry streamItemPhoto) {
     77             this.streamItem = streamItem;
     78             this.streamItemPhoto = streamItemPhoto;
     79         }
     80 
     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 
     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      */
     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      */
    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      */
    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      */
    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      */
    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_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      */
    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. */
    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. */
    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
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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 
    451         public DefaultImageGetter(PackageManager packageManager) {
    452             mPackageManager = packageManager;
    453         }
    454 
    455         @Override
    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. */
    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. */
    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      */
    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      */
    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      */
    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