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.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 
     74         public StreamPhotoTag(StreamItemEntry streamItem, StreamItemPhotoEntry streamItemPhoto) {
     75             this.streamItem = streamItem;
     76             this.streamItemPhoto = streamItemPhoto;
     77         }
     78 
     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 
     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      */
     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      */
    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             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      */
    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      */
    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      */
    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      */
    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. */
    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. */
    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
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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 
    446         public DefaultImageGetter(PackageManager packageManager) {
    447             mPackageManager = packageManager;
    448         }
    449 
    450         @Override
    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. */
    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. */
    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      */
    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      */
    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      */
    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