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
17package com.android.contacts.detail;
18
19import com.google.common.collect.Iterables;
20
21import com.android.contacts.R;
22import com.android.contacts.common.model.Contact;
23import com.android.contacts.common.model.RawContact;
24import com.android.contacts.common.model.dataitem.DataItem;
25import com.android.contacts.common.model.dataitem.OrganizationDataItem;
26import com.android.contacts.common.preference.ContactsPreferences;
27import com.android.contacts.util.MoreMath;
28
29import android.content.Context;
30import android.content.pm.PackageManager;
31import android.content.pm.PackageManager.NameNotFoundException;
32import android.content.res.Resources;
33import android.content.res.Resources.NotFoundException;
34import android.graphics.drawable.Drawable;
35import android.net.Uri;
36import android.provider.ContactsContract.DisplayNameSources;
37import android.text.BidiFormatter;
38import android.text.Html;
39import android.text.TextDirectionHeuristics;
40import android.text.TextUtils;
41import android.util.Log;
42import android.view.MenuItem;
43import android.view.View;
44import android.widget.ImageView;
45import android.widget.ListView;
46import android.widget.TextView;
47
48import java.util.List;
49
50/**
51 * This class contains utility methods to bind high-level contact details
52 * (meaning name, phonetic name, job, and attribution) from a
53 * {@link Contact} data object to appropriate {@link View}s.
54 */
55public class ContactDisplayUtils {
56    private static final String TAG = "ContactDisplayUtils";
57    private static BidiFormatter sBidiFormatter = BidiFormatter.getInstance();
58
59    /**
60     * Returns the display name of the contact, using the current display order setting.
61     * Returns res/string/missing_name if there is no display name.
62     */
63    public static CharSequence getDisplayName(Context context, Contact contactData) {
64        ContactsPreferences prefs = new ContactsPreferences(context);
65        final CharSequence displayName = contactData.getDisplayName();
66        if (prefs.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY) {
67            if (!TextUtils.isEmpty(displayName)) {
68                if (contactData.getDisplayNameSource() == DisplayNameSources.PHONE) {
69                    return sBidiFormatter.unicodeWrap(
70                            displayName.toString(), TextDirectionHeuristics.LTR);
71                }
72                return displayName;
73            }
74        } else {
75            final CharSequence altDisplayName = contactData.getAltDisplayName();
76            if (!TextUtils.isEmpty(altDisplayName)) {
77                return altDisplayName;
78            }
79        }
80        return context.getResources().getString(R.string.missing_name);
81    }
82
83    /**
84     * Returns the phonetic name of the contact or null if there isn't one.
85     */
86    public static String getPhoneticName(Context context, Contact contactData) {
87        String phoneticName = contactData.getPhoneticName();
88        if (!TextUtils.isEmpty(phoneticName)) {
89            return phoneticName;
90        }
91        return null;
92    }
93
94    /**
95     * Returns the attribution string for the contact, which may specify the contact directory that
96     * the contact came from. Returns null if there is none applicable.
97     */
98    public static String getAttribution(Context context, Contact contactData) {
99        if (contactData.isDirectoryEntry()) {
100            String directoryDisplayName = contactData.getDirectoryDisplayName();
101            String directoryType = contactData.getDirectoryType();
102            final String displayName;
103            if (!TextUtils.isEmpty(directoryDisplayName)) {
104                displayName = directoryDisplayName;
105            } else if (!TextUtils.isEmpty(directoryType)) {
106                displayName = directoryType;
107            } else {
108                return null;
109            }
110            return context.getString(R.string.contact_directory_description, displayName);
111        }
112        return null;
113    }
114
115    /**
116     * Returns the organization of the contact. If several organizations are given,
117     * the first one is used. Returns null if not applicable.
118     */
119    public static String getCompany(Context context, Contact contactData) {
120        final boolean displayNameIsOrganization = contactData.getDisplayNameSource()
121                == DisplayNameSources.ORGANIZATION;
122        for (RawContact rawContact : contactData.getRawContacts()) {
123            for (DataItem dataItem : Iterables.filter(
124                    rawContact.getDataItems(), OrganizationDataItem.class)) {
125                OrganizationDataItem organization = (OrganizationDataItem) dataItem;
126                final String company = organization.getCompany();
127                final String title = organization.getTitle();
128                final String combined;
129                // We need to show company and title in a combined string. However, if the
130                // DisplayName is already the organization, it mirrors company or (if company
131                // is empty title). Make sure we don't show what's already shown as DisplayName
132                if (TextUtils.isEmpty(company)) {
133                    combined = displayNameIsOrganization ? null : title;
134                } else {
135                    if (TextUtils.isEmpty(title)) {
136                        combined = displayNameIsOrganization ? null : company;
137                    } else {
138                        if (displayNameIsOrganization) {
139                            combined = title;
140                        } else {
141                            combined = context.getString(
142                                    R.string.organization_company_and_title,
143                                    company, title);
144                        }
145                    }
146                }
147
148                if (!TextUtils.isEmpty(combined)) {
149                    return combined;
150                }
151            }
152        }
153        return null;
154    }
155
156    /**
157     * Sets the starred state of this contact.
158     */
159    public static void configureStarredImageView(ImageView starredView, boolean isDirectoryEntry,
160            boolean isUserProfile, boolean isStarred) {
161        // Check if the starred state should be visible
162        if (!isDirectoryEntry && !isUserProfile) {
163            starredView.setVisibility(View.VISIBLE);
164            final int resId = isStarred
165                    ? R.drawable.btn_star_on_normal_holo_light
166                    : R.drawable.btn_star_off_normal_holo_light;
167            starredView.setImageResource(resId);
168            starredView.setTag(isStarred);
169            starredView.setContentDescription(starredView.getResources().getString(
170                    isStarred ? R.string.menu_removeStar : R.string.menu_addStar));
171        } else {
172            starredView.setVisibility(View.GONE);
173        }
174    }
175
176    /**
177     * Sets the starred state of this contact.
178     */
179    public static void configureStarredMenuItem(MenuItem starredMenuItem, boolean isDirectoryEntry,
180            boolean isUserProfile, boolean isStarred) {
181        // Check if the starred state should be visible
182        if (!isDirectoryEntry && !isUserProfile) {
183            starredMenuItem.setVisible(true);
184            final int resId = isStarred
185                    ? R.drawable.ic_star_24dp
186                    : R.drawable.ic_star_outline_24dp;
187            starredMenuItem.setIcon(resId);
188            starredMenuItem.setChecked(isStarred);
189            starredMenuItem.setTitle(isStarred ? R.string.menu_removeStar : R.string.menu_addStar);
190        } else {
191            starredMenuItem.setVisible(false);
192        }
193    }
194
195    /**
196     * Sets the display name of this contact to the given {@link TextView}. If
197     * there is none, then set the view to gone.
198     */
199    public static void setDisplayName(Context context, Contact contactData, TextView textView) {
200        if (textView == null) {
201            return;
202        }
203        setDataOrHideIfNone(getDisplayName(context, contactData), textView);
204    }
205
206    /**
207     * Sets the company and job title of this contact to the given {@link TextView}. If
208     * there is none, then set the view to gone.
209     */
210    public static void setCompanyName(Context context, Contact contactData, TextView textView) {
211        if (textView == null) {
212            return;
213        }
214        setDataOrHideIfNone(getCompany(context, contactData), textView);
215    }
216
217    /**
218     * Sets the phonetic name of this contact to the given {@link TextView}. If
219     * there is none, then set the view to gone.
220     */
221    public static void setPhoneticName(Context context, Contact contactData, TextView textView) {
222        if (textView == null) {
223            return;
224        }
225        setDataOrHideIfNone(getPhoneticName(context, contactData), textView);
226    }
227
228    /**
229     * Sets the attribution contact to the given {@link TextView}. If
230     * there is none, then set the view to gone.
231     */
232    public static void setAttribution(Context context, Contact contactData, TextView textView) {
233        if (textView == null) {
234            return;
235        }
236        setDataOrHideIfNone(getAttribution(context, contactData), textView);
237    }
238
239    /**
240     * Helper function to display the given text in the {@link TextView} or
241     * hides the {@link TextView} if the text is empty or null.
242     */
243    private static void setDataOrHideIfNone(CharSequence textToDisplay, TextView textView) {
244        if (!TextUtils.isEmpty(textToDisplay)) {
245            textView.setText(textToDisplay);
246            textView.setVisibility(View.VISIBLE);
247        } else {
248            textView.setText(null);
249            textView.setVisibility(View.GONE);
250        }
251    }
252
253    private static Html.ImageGetter sImageGetter;
254
255    public static Html.ImageGetter getImageGetter(Context context) {
256        if (sImageGetter == null) {
257            sImageGetter = new DefaultImageGetter(context.getPackageManager());
258        }
259        return sImageGetter;
260    }
261
262    /** Fetcher for images from resources to be included in HTML text. */
263    private static class DefaultImageGetter implements Html.ImageGetter {
264        /** The scheme used to load resources. */
265        private static final String RES_SCHEME = "res";
266
267        private final PackageManager mPackageManager;
268
269        public DefaultImageGetter(PackageManager packageManager) {
270            mPackageManager = packageManager;
271        }
272
273        @Override
274        public Drawable getDrawable(String source) {
275            // Returning null means that a default image will be used.
276            Uri uri;
277            try {
278                uri = Uri.parse(source);
279            } catch (Throwable e) {
280                Log.d(TAG, "Could not parse image source: " + source);
281                return null;
282            }
283            if (!RES_SCHEME.equals(uri.getScheme())) {
284                Log.d(TAG, "Image source does not correspond to a resource: " + source);
285                return null;
286            }
287            // The URI authority represents the package name.
288            String packageName = uri.getAuthority();
289
290            Resources resources = getResourcesForResourceName(packageName);
291            if (resources == null) {
292                Log.d(TAG, "Could not parse image source: " + source);
293                return null;
294            }
295
296            List<String> pathSegments = uri.getPathSegments();
297            if (pathSegments.size() != 1) {
298                Log.d(TAG, "Could not parse image source: " + source);
299                return null;
300            }
301
302            final String name = pathSegments.get(0);
303            final int resId = resources.getIdentifier(name, "drawable", packageName);
304
305            if (resId == 0) {
306                // Use the default image icon in this case.
307                Log.d(TAG, "Cannot resolve resource identifier: " + source);
308                return null;
309            }
310
311            try {
312                return getResourceDrawable(resources, resId);
313            } catch (NotFoundException e) {
314                Log.d(TAG, "Resource not found: " + source, e);
315                return null;
316            }
317        }
318
319        /** Returns the drawable associated with the given id. */
320        private Drawable getResourceDrawable(Resources resources, int resId)
321                throws NotFoundException {
322            Drawable drawable = resources.getDrawable(resId);
323            drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
324            return drawable;
325        }
326
327        /** Returns the {@link Resources} of the package of the given resource name. */
328        private Resources getResourcesForResourceName(String packageName) {
329            try {
330                return mPackageManager.getResourcesForApplication(packageName);
331            } catch (NameNotFoundException e) {
332                Log.d(TAG, "Could not find package: " + packageName);
333                return null;
334            }
335        }
336    }
337
338    /**
339     * Sets an alpha value on the view.
340     */
341    public static void setAlphaOnViewBackground(View view, float alpha) {
342        if (view != null) {
343            // Convert alpha layer to a black background HEX color with an alpha value for better
344            // performance (i.e. use setBackgroundColor() instead of setAlpha())
345            view.setBackgroundColor((int) (MoreMath.clamp(alpha, 0.0f, 1.0f) * 255) << 24);
346        }
347    }
348
349    /**
350     * Returns the top coordinate of the first item in the {@link ListView}. If the first item
351     * in the {@link ListView} is not visible or there are no children in the list, then return
352     * Integer.MIN_VALUE. Note that the returned value will be <= 0 because the first item in the
353     * list cannot have a positive offset.
354     */
355    public static int getFirstListItemOffset(ListView listView) {
356        if (listView == null || listView.getChildCount() == 0 ||
357                listView.getFirstVisiblePosition() != 0) {
358            return Integer.MIN_VALUE;
359        }
360        return listView.getChildAt(0).getTop();
361    }
362
363    /**
364     * Tries to scroll the first item in the list to the given offset (this can be a no-op if the
365     * list is already in the correct position).
366     * @param listView that should be scrolled
367     * @param offset which should be <= 0
368     */
369    public static void requestToMoveToOffset(ListView listView, int offset) {
370        // We try to offset the list if the first item in the list is showing (which is presumed
371        // to have a larger height than the desired offset). If the first item in the list is not
372        // visible, then we simply do not scroll the list at all (since it can get complicated to
373        // compute how many items in the list will equal the given offset). Potentially
374        // some animation elsewhere will make the transition smoother for the user to compensate
375        // for this simplification.
376        if (listView == null || listView.getChildCount() == 0 ||
377                listView.getFirstVisiblePosition() != 0 || offset > 0) {
378            return;
379        }
380
381        // As an optimization, check if the first item is already at the given offset.
382        if (listView.getChildAt(0).getTop() == offset) {
383            return;
384        }
385
386        listView.setSelectionFromTop(0, offset);
387    }
388}
389