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