/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.contacts.detail; import android.content.ContentUris; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.content.res.Resources.NotFoundException; import android.graphics.drawable.Drawable; import android.net.Uri; import android.provider.ContactsContract; import android.provider.ContactsContract.DisplayNameSources; import android.provider.ContactsContract.StreamItems; import android.text.Html; import android.text.Html.ImageGetter; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import com.android.contacts.common.ContactPhotoManager; import com.android.contacts.R; import com.android.contacts.model.Contact; import com.android.contacts.model.RawContact; import com.android.contacts.model.dataitem.DataItem; import com.android.contacts.model.dataitem.OrganizationDataItem; import com.android.contacts.common.preference.ContactsPreferences; import com.android.contacts.util.ContactBadgeUtil; import com.android.contacts.util.HtmlUtils; import com.android.contacts.util.MoreMath; import com.android.contacts.util.StreamItemEntry; import com.android.contacts.util.StreamItemPhotoEntry; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Iterables; import java.util.List; /** * This class contains utility methods to bind high-level contact details * (meaning name, phonetic name, job, and attribution) from a * {@link Contact} data object to appropriate {@link View}s. */ public class ContactDetailDisplayUtils { private static final String TAG = "ContactDetailDisplayUtils"; /** * Tag object used for stream item photos. */ public static class StreamPhotoTag { public final StreamItemEntry streamItem; public final StreamItemPhotoEntry streamItemPhoto; public StreamPhotoTag(StreamItemEntry streamItem, StreamItemPhotoEntry streamItemPhoto) { this.streamItem = streamItem; this.streamItemPhoto = streamItemPhoto; } public Uri getStreamItemPhotoUri() { final Uri.Builder builder = StreamItems.CONTENT_URI.buildUpon(); ContentUris.appendId(builder, streamItem.getId()); builder.appendPath(StreamItems.StreamItemPhotos.CONTENT_DIRECTORY); ContentUris.appendId(builder, streamItemPhoto.getId()); return builder.build(); } } private ContactDetailDisplayUtils() { // Disallow explicit creation of this class. } /** * Returns the display name of the contact, using the current display order setting. * Returns res/string/missing_name if there is no display name. */ public static CharSequence getDisplayName(Context context, Contact contactData) { CharSequence displayName = contactData.getDisplayName(); CharSequence altDisplayName = contactData.getAltDisplayName(); ContactsPreferences prefs = new ContactsPreferences(context); CharSequence styledName = ""; if (!TextUtils.isEmpty(displayName) && !TextUtils.isEmpty(altDisplayName)) { if (prefs.getDisplayOrder() == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) { styledName = displayName; } else { styledName = altDisplayName; } } else { styledName = context.getResources().getString(R.string.missing_name); } return styledName; } /** * Returns the phonetic name of the contact or null if there isn't one. */ public static String getPhoneticName(Context context, Contact contactData) { String phoneticName = contactData.getPhoneticName(); if (!TextUtils.isEmpty(phoneticName)) { return phoneticName; } return null; } /** * Returns the attribution string for the contact, which may specify the contact directory that * the contact came from. Returns null if there is none applicable. */ public static String getAttribution(Context context, Contact contactData) { if (contactData.isDirectoryEntry()) { String directoryDisplayName = contactData.getDirectoryDisplayName(); String directoryType = contactData.getDirectoryType(); String displayName = !TextUtils.isEmpty(directoryDisplayName) ? directoryDisplayName : directoryType; return context.getString(R.string.contact_directory_description, displayName); } return null; } /** * Returns the organization of the contact. If several organizations are given, * the first one is used. Returns null if not applicable. */ public static String getCompany(Context context, Contact contactData) { final boolean displayNameIsOrganization = contactData.getDisplayNameSource() == DisplayNameSources.ORGANIZATION; for (RawContact rawContact : contactData.getRawContacts()) { for (DataItem dataItem : Iterables.filter( rawContact.getDataItems(), OrganizationDataItem.class)) { OrganizationDataItem organization = (OrganizationDataItem) dataItem; final String company = organization.getCompany(); final String title = organization.getTitle(); final String combined; // We need to show company and title in a combined string. However, if the // DisplayName is already the organization, it mirrors company or (if company // is empty title). Make sure we don't show what's already shown as DisplayName if (TextUtils.isEmpty(company)) { combined = displayNameIsOrganization ? null : title; } else { if (TextUtils.isEmpty(title)) { combined = displayNameIsOrganization ? null : company; } else { if (displayNameIsOrganization) { combined = title; } else { combined = context.getString( R.string.organization_company_and_title, company, title); } } } if (!TextUtils.isEmpty(combined)) { return combined; } } } return null; } /** * Sets the starred state of this contact. */ public static void configureStarredImageView(ImageView starredView, boolean isDirectoryEntry, boolean isUserProfile, boolean isStarred) { // Check if the starred state should be visible if (!isDirectoryEntry && !isUserProfile) { starredView.setVisibility(View.VISIBLE); final int resId = isStarred ? R.drawable.btn_star_on_normal_holo_light : R.drawable.btn_star_off_normal_holo_light; starredView.setImageResource(resId); starredView.setTag(isStarred); starredView.setContentDescription(starredView.getResources().getString( isStarred ? R.string.menu_removeStar : R.string.menu_addStar)); } else { starredView.setVisibility(View.GONE); } } /** * Sets the starred state of this contact. */ public static void configureStarredMenuItem(MenuItem starredMenuItem, boolean isDirectoryEntry, boolean isUserProfile, boolean isStarred) { // Check if the starred state should be visible if (!isDirectoryEntry && !isUserProfile) { starredMenuItem.setVisible(true); final int resId = isStarred ? R.drawable.btn_star_on_normal_holo_dark : R.drawable.btn_star_off_normal_holo_dark; starredMenuItem.setIcon(resId); starredMenuItem.setChecked(isStarred); starredMenuItem.setTitle(isStarred ? R.string.menu_removeStar : R.string.menu_addStar); } else { starredMenuItem.setVisible(false); } } /** * Set the social snippet text. If there isn't one, then set the view to gone. */ public static void setSocialSnippet(Context context, Contact contactData, TextView statusView, ImageView statusPhotoView) { if (statusView == null) { return; } CharSequence snippet = null; String photoUri = null; if (!contactData.getStreamItems().isEmpty()) { StreamItemEntry firstEntry = contactData.getStreamItems().get(0); snippet = HtmlUtils.fromHtml(context, firstEntry.getText()); if (!firstEntry.getPhotos().isEmpty()) { StreamItemPhotoEntry firstPhoto = firstEntry.getPhotos().get(0); photoUri = firstPhoto.getPhotoUri(); // If displaying an image, hide the snippet text. snippet = null; } } setDataOrHideIfNone(snippet, statusView); if (photoUri != null) { ContactPhotoManager.getInstance(context).loadPhoto( statusPhotoView, Uri.parse(photoUri), -1, false, ContactPhotoManager.DEFAULT_BLANK); statusPhotoView.setVisibility(View.VISIBLE); } else { statusPhotoView.setVisibility(View.GONE); } } /** Creates the view that represents a stream item. */ public static View createStreamItemView(LayoutInflater inflater, Context context, View convertView, StreamItemEntry streamItem, View.OnClickListener photoClickListener) { // Try to recycle existing views. final View container; if (convertView != null) { container = convertView; } else { container = inflater.inflate(R.layout.stream_item_container, null, false); } final ContactPhotoManager contactPhotoManager = ContactPhotoManager.getInstance(context); final List photos = streamItem.getPhotos(); final int photoCount = photos.size(); // Add the text part. addStreamItemText(context, streamItem, container); // Add images. final ViewGroup imageRows = (ViewGroup) container.findViewById(R.id.stream_item_image_rows); if (photoCount == 0) { // This stream item only has text. imageRows.setVisibility(View.GONE); } else { // This stream item has text and photos. imageRows.setVisibility(View.VISIBLE); // Number of image rows needed, which is cailing(photoCount / 2) final int numImageRows = (photoCount + 1) / 2; // Actual image rows. final int numOldImageRows = imageRows.getChildCount(); // Make sure we have enough stream_item_row_images. if (numOldImageRows == numImageRows) { // Great, we have the just enough number of rows... } else if (numOldImageRows < numImageRows) { // Need to add more image rows. for (int i = numOldImageRows; i < numImageRows; i++) { View imageRow = inflater.inflate(R.layout.stream_item_row_images, imageRows, true); } } else { // We have exceeding image rows. Hide them. for (int i = numImageRows; i < numOldImageRows; i++) { imageRows.getChildAt(i).setVisibility(View.GONE); } } // Put images, two by two. for (int i = 0; i < photoCount; i += 2) { final View imageRow = imageRows.getChildAt(i / 2); // Reused image rows may not visible, so make sure they're shown. imageRow.setVisibility(View.VISIBLE); // Show first image. loadPhoto(contactPhotoManager, streamItem, photos.get(i), imageRow, R.id.stream_item_first_image, photoClickListener); final View secondContainer = imageRow.findViewById(R.id.second_image_container); if (i + 1 < photoCount) { // Show the second image too. loadPhoto(contactPhotoManager, streamItem, photos.get(i + 1), imageRow, R.id.stream_item_second_image, photoClickListener); secondContainer.setVisibility(View.VISIBLE); } else { // Hide the second image, but it still has to occupy the space. secondContainer.setVisibility(View.INVISIBLE); } } } return container; } /** Loads a photo into an image view. The image view is identified by the given id. */ private static void loadPhoto(ContactPhotoManager contactPhotoManager, final StreamItemEntry streamItem, final StreamItemPhotoEntry streamItemPhoto, View photoContainer, int imageViewId, View.OnClickListener photoClickListener) { final View frame = photoContainer.findViewById(imageViewId); final View pushLayerView = frame.findViewById(R.id.push_layer); final ImageView imageView = (ImageView) frame.findViewById(R.id.image); if (photoClickListener != null) { pushLayerView.setOnClickListener(photoClickListener); pushLayerView.setTag(new StreamPhotoTag(streamItem, streamItemPhoto)); pushLayerView.setFocusable(true); pushLayerView.setEnabled(true); } else { pushLayerView.setOnClickListener(null); pushLayerView.setTag(null); pushLayerView.setFocusable(false); // setOnClickListener makes it clickable, so we need to overwrite it pushLayerView.setClickable(false); pushLayerView.setEnabled(false); } contactPhotoManager.loadPhoto(imageView, Uri.parse(streamItemPhoto.getPhotoUri()), -1, false, ContactPhotoManager.DEFAULT_BLANK); } @VisibleForTesting static View addStreamItemText(Context context, StreamItemEntry streamItem, View rootView) { TextView htmlView = (TextView) rootView.findViewById(R.id.stream_item_html); TextView attributionView = (TextView) rootView.findViewById( R.id.stream_item_attribution); TextView commentsView = (TextView) rootView.findViewById(R.id.stream_item_comments); ImageGetter imageGetter = new DefaultImageGetter(context.getPackageManager()); // Stream item text setDataOrHideIfNone(streamItem.getDecodedText(), htmlView); // Attribution setDataOrHideIfNone(ContactBadgeUtil.getSocialDate(streamItem, context), attributionView); // Comments setDataOrHideIfNone(streamItem.getDecodedComments(), commentsView); return rootView; } /** * Sets the display name of this contact to the given {@link TextView}. If * there is none, then set the view to gone. */ public static void setDisplayName(Context context, Contact contactData, TextView textView) { if (textView == null) { return; } setDataOrHideIfNone(getDisplayName(context, contactData), textView); } /** * Sets the company and job title of this contact to the given {@link TextView}. If * there is none, then set the view to gone. */ public static void setCompanyName(Context context, Contact contactData, TextView textView) { if (textView == null) { return; } setDataOrHideIfNone(getCompany(context, contactData), textView); } /** * Sets the phonetic name of this contact to the given {@link TextView}. If * there is none, then set the view to gone. */ public static void setPhoneticName(Context context, Contact contactData, TextView textView) { if (textView == null) { return; } setDataOrHideIfNone(getPhoneticName(context, contactData), textView); } /** * Sets the attribution contact to the given {@link TextView}. If * there is none, then set the view to gone. */ public static void setAttribution(Context context, Contact contactData, TextView textView) { if (textView == null) { return; } setDataOrHideIfNone(getAttribution(context, contactData), textView); } /** * Helper function to display the given text in the {@link TextView} or * hides the {@link TextView} if the text is empty or null. */ private static void setDataOrHideIfNone(CharSequence textToDisplay, TextView textView) { if (!TextUtils.isEmpty(textToDisplay)) { textView.setText(textToDisplay); textView.setVisibility(View.VISIBLE); } else { textView.setText(null); textView.setVisibility(View.GONE); } } private static Html.ImageGetter sImageGetter; public static Html.ImageGetter getImageGetter(Context context) { if (sImageGetter == null) { sImageGetter = new DefaultImageGetter(context.getPackageManager()); } return sImageGetter; } /** Fetcher for images from resources to be included in HTML text. */ private static class DefaultImageGetter implements Html.ImageGetter { /** The scheme used to load resources. */ private static final String RES_SCHEME = "res"; private final PackageManager mPackageManager; public DefaultImageGetter(PackageManager packageManager) { mPackageManager = packageManager; } @Override public Drawable getDrawable(String source) { // Returning null means that a default image will be used. Uri uri; try { uri = Uri.parse(source); } catch (Throwable e) { Log.d(TAG, "Could not parse image source: " + source); return null; } if (!RES_SCHEME.equals(uri.getScheme())) { Log.d(TAG, "Image source does not correspond to a resource: " + source); return null; } // The URI authority represents the package name. String packageName = uri.getAuthority(); Resources resources = getResourcesForResourceName(packageName); if (resources == null) { Log.d(TAG, "Could not parse image source: " + source); return null; } List pathSegments = uri.getPathSegments(); if (pathSegments.size() != 1) { Log.d(TAG, "Could not parse image source: " + source); return null; } final String name = pathSegments.get(0); final int resId = resources.getIdentifier(name, "drawable", packageName); if (resId == 0) { // Use the default image icon in this case. Log.d(TAG, "Cannot resolve resource identifier: " + source); return null; } try { return getResourceDrawable(resources, resId); } catch (NotFoundException e) { Log.d(TAG, "Resource not found: " + source, e); return null; } } /** Returns the drawable associated with the given id. */ private Drawable getResourceDrawable(Resources resources, int resId) throws NotFoundException { Drawable drawable = resources.getDrawable(resId); drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); return drawable; } /** Returns the {@link Resources} of the package of the given resource name. */ private Resources getResourcesForResourceName(String packageName) { try { return mPackageManager.getResourcesForApplication(packageName); } catch (NameNotFoundException e) { Log.d(TAG, "Could not find package: " + packageName); return null; } } } /** * Sets an alpha value on the view. */ public static void setAlphaOnViewBackground(View view, float alpha) { if (view != null) { // Convert alpha layer to a black background HEX color with an alpha value for better // performance (i.e. use setBackgroundColor() instead of setAlpha()) view.setBackgroundColor((int) (MoreMath.clamp(alpha, 0.0f, 1.0f) * 255) << 24); } } /** * Returns the top coordinate of the first item in the {@link ListView}. If the first item * in the {@link ListView} is not visible or there are no children in the list, then return * Integer.MIN_VALUE. Note that the returned value will be <= 0 because the first item in the * list cannot have a positive offset. */ public static int getFirstListItemOffset(ListView listView) { if (listView == null || listView.getChildCount() == 0 || listView.getFirstVisiblePosition() != 0) { return Integer.MIN_VALUE; } return listView.getChildAt(0).getTop(); } /** * Tries to scroll the first item in the list to the given offset (this can be a no-op if the * list is already in the correct position). * @param listView that should be scrolled * @param offset which should be <= 0 */ public static void requestToMoveToOffset(ListView listView, int offset) { // We try to offset the list if the first item in the list is showing (which is presumed // to have a larger height than the desired offset). If the first item in the list is not // visible, then we simply do not scroll the list at all (since it can get complicated to // compute how many items in the list will equal the given offset). Potentially // some animation elsewhere will make the transition smoother for the user to compensate // for this simplification. if (listView == null || listView.getChildCount() == 0 || listView.getFirstVisiblePosition() != 0 || offset > 0) { return; } // As an optimization, check if the first item is already at the given offset. if (listView.getChildAt(0).getTop() == offset) { return; } listView.setSelectionFromTop(0, offset); } }