blob: 1908e966366573a7df0c25db9535261caf122d85 [file] [log] [blame]
/*
* 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.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.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<StreamItemPhotoEntry> 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<String> 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);
}
}