/* Copyright © 2013-2014, Silent Circle, LLC. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Any redistribution, use, or modification is done solely for personal benefit and not for any commercial purpose or for monetary gain * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name Silent Circle nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SILENT CIRCLE, LLC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* * This implementation is edited version of original Android sources. */ /* * 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.silentcircle.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.text.Html; import android.text.Html.ImageGetter; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import com.silentcircle.contacts.ContactPhotoManager; import com.silentcircle.contacts.model.Contact; import com.silentcircle.contacts.model.RawContact; import com.silentcircle.contacts.model.dataitem.DataItem; import com.silentcircle.contacts.utils.ContactBadgeUtil; import com.silentcircle.contacts.utils.HtmlUtils; import com.silentcircle.contacts.utils.MoreMath; import com.silentcircle.contacts.utils.StreamItemPhotoEntry; import com.silentcircle.contacts.R; import com.silentcircle.silentcontacts.ScContactsContract; import com.silentcircle.silentcontacts.ScContactsContract.DisplayNameSources; import com.silentcircle.silentcontacts.ScContactsContract.StreamItems; import com.silentcircle.contacts.model.dataitem.OrganizationDataItem; import com.silentcircle.contacts.preference.ContactsPreferences; import com.silentcircle.contacts.utils.StreamItemEntry; import com.actionbarsherlock.view.MenuItem; 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 com.silentcircle.contacts.model.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() == ScContactsContract.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_dark : R.drawable.btn_star_off_normal_holo_dark; 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); } }