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