1/*
2 * Copyright (C) 2009 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;
18
19import android.content.Context;
20import android.content.Intent;
21import android.content.res.Configuration;
22import android.database.Cursor;
23import android.graphics.Rect;
24import android.location.CountryDetector;
25import android.net.Uri;
26import android.provider.ContactsContract;
27import android.provider.ContactsContract.CommonDataKinds.Im;
28import android.provider.ContactsContract.CommonDataKinds.Phone;
29import android.provider.ContactsContract.DisplayPhoto;
30import android.provider.ContactsContract.QuickContact;
31import android.telephony.PhoneNumberUtils;
32import android.text.TextUtils;
33import android.view.View;
34import android.widget.TextView;
35
36import com.android.contacts.activities.DialtactsActivity;
37import com.android.contacts.model.AccountTypeManager;
38import com.android.contacts.model.account.AccountType;
39import com.android.contacts.model.account.AccountWithDataSet;
40import com.android.contacts.test.NeededForTesting;
41import com.android.contacts.util.Constants;
42
43import java.util.List;
44
45public class ContactsUtils {
46    private static final String TAG = "ContactsUtils";
47    private static final String WAIT_SYMBOL_AS_STRING = String.valueOf(PhoneNumberUtils.WAIT);
48
49    private static int sThumbnailSize = -1;
50
51    // TODO find a proper place for the canonical version of these
52    public interface ProviderNames {
53        String YAHOO = "Yahoo";
54        String GTALK = "GTalk";
55        String MSN = "MSN";
56        String ICQ = "ICQ";
57        String AIM = "AIM";
58        String XMPP = "XMPP";
59        String JABBER = "JABBER";
60        String SKYPE = "SKYPE";
61        String QQ = "QQ";
62    }
63
64    /**
65     * This looks up the provider name defined in
66     * ProviderNames from the predefined IM protocol id.
67     * This is used for interacting with the IM application.
68     *
69     * @param protocol the protocol ID
70     * @return the provider name the IM app uses for the given protocol, or null if no
71     * provider is defined for the given protocol
72     * @hide
73     */
74    public static String lookupProviderNameFromId(int protocol) {
75        switch (protocol) {
76            case Im.PROTOCOL_GOOGLE_TALK:
77                return ProviderNames.GTALK;
78            case Im.PROTOCOL_AIM:
79                return ProviderNames.AIM;
80            case Im.PROTOCOL_MSN:
81                return ProviderNames.MSN;
82            case Im.PROTOCOL_YAHOO:
83                return ProviderNames.YAHOO;
84            case Im.PROTOCOL_ICQ:
85                return ProviderNames.ICQ;
86            case Im.PROTOCOL_JABBER:
87                return ProviderNames.JABBER;
88            case Im.PROTOCOL_SKYPE:
89                return ProviderNames.SKYPE;
90            case Im.PROTOCOL_QQ:
91                return ProviderNames.QQ;
92        }
93        return null;
94    }
95
96    /**
97     * Test if the given {@link CharSequence} contains any graphic characters,
98     * first checking {@link TextUtils#isEmpty(CharSequence)} to handle null.
99     */
100    public static boolean isGraphic(CharSequence str) {
101        return !TextUtils.isEmpty(str) && TextUtils.isGraphic(str);
102    }
103
104    /**
105     * Returns true if two objects are considered equal.  Two null references are equal here.
106     */
107    @NeededForTesting
108    public static boolean areObjectsEqual(Object a, Object b) {
109        return a == b || (a != null && a.equals(b));
110    }
111
112    /**
113     * Returns true if two data with mimetypes which represent values in contact entries are
114     * considered equal for collapsing in the GUI. For caller-id, use
115     * {@link PhoneNumberUtils#compare(Context, String, String)} instead
116     */
117    public static final boolean shouldCollapse(CharSequence mimetype1, CharSequence data1,
118            CharSequence mimetype2, CharSequence data2) {
119        // different mimetypes? don't collapse
120        if (!TextUtils.equals(mimetype1, mimetype2)) return false;
121
122        // exact same string? good, bail out early
123        if (TextUtils.equals(data1, data2)) return true;
124
125        // so if either is null, these two must be different
126        if (data1 == null || data2 == null) return false;
127
128        // if this is not about phone numbers, we know this is not a match (of course, some
129        // mimetypes could have more sophisticated matching is the future, e.g. addresses)
130        if (!TextUtils.equals(Phone.CONTENT_ITEM_TYPE, mimetype1)) return false;
131
132        return shouldCollapsePhoneNumbers(data1.toString(), data2.toString());
133    }
134
135    private static final boolean shouldCollapsePhoneNumbers(
136            String number1WithLetters, String number2WithLetters) {
137        final String number1 = PhoneNumberUtils.convertKeypadLettersToDigits(number1WithLetters);
138        final String number2 = PhoneNumberUtils.convertKeypadLettersToDigits(number2WithLetters);
139
140        int index1 = 0;
141        int index2 = 0;
142        for (;;) {
143            // Skip formatting characters.
144            while (index1 < number1.length() &&
145                    !PhoneNumberUtils.isNonSeparator(number1.charAt(index1))) {
146                index1++;
147            }
148            while (index2 < number2.length() &&
149                    !PhoneNumberUtils.isNonSeparator(number2.charAt(index2))) {
150                index2++;
151            }
152            // If both have finished, match.  If only one has finished, not match.
153            final boolean number1End = (index1 == number1.length());
154            final boolean number2End = (index2 == number2.length());
155            if (number1End) {
156                return number2End;
157            }
158            if (number2End) return false;
159
160            // If the non-formatting characters are different, not match.
161            if (number1.charAt(index1) != number2.charAt(index2)) return false;
162
163            // Go to the next characters.
164            index1++;
165            index2++;
166        }
167    }
168
169    /**
170     * Returns true if two {@link Intent}s are both null, or have the same action.
171     */
172    public static final boolean areIntentActionEqual(Intent a, Intent b) {
173        if (a == b) {
174            return true;
175        }
176        if (a == null || b == null) {
177            return false;
178        }
179        return TextUtils.equals(a.getAction(), b.getAction());
180    }
181
182    /**
183     * @return The ISO 3166-1 two letters country code of the country the user
184     *         is in.
185     */
186    public static final String getCurrentCountryIso(Context context) {
187        CountryDetector detector =
188                (CountryDetector) context.getSystemService(Context.COUNTRY_DETECTOR);
189        return detector.detectCountry().getCountryIso();
190    }
191
192    public static boolean areContactWritableAccountsAvailable(Context context) {
193        final List<AccountWithDataSet> accounts =
194                AccountTypeManager.getInstance(context).getAccounts(true /* writeable */);
195        return !accounts.isEmpty();
196    }
197
198    public static boolean areGroupWritableAccountsAvailable(Context context) {
199        final List<AccountWithDataSet> accounts =
200                AccountTypeManager.getInstance(context).getGroupWritableAccounts();
201        return !accounts.isEmpty();
202    }
203
204    /**
205     * Returns the intent to launch for the given invitable account type and contact lookup URI.
206     * This will return null if the account type is not invitable (i.e. there is no
207     * {@link AccountType#getInviteContactActivityClassName()} or
208     * {@link AccountType#syncAdapterPackageName}).
209     */
210    public static Intent getInvitableIntent(AccountType accountType, Uri lookupUri) {
211        String syncAdapterPackageName = accountType.syncAdapterPackageName;
212        String className = accountType.getInviteContactActivityClassName();
213        if (TextUtils.isEmpty(syncAdapterPackageName) || TextUtils.isEmpty(className)) {
214            return null;
215        }
216        Intent intent = new Intent();
217        intent.setClassName(syncAdapterPackageName, className);
218
219        intent.setAction(ContactsContract.Intents.INVITE_CONTACT);
220
221        // Data is the lookup URI.
222        intent.setData(lookupUri);
223        return intent;
224    }
225
226    /**
227     * Return Uri with an appropriate scheme, accepting Voicemail, SIP, and usual phone call
228     * numbers.
229     */
230    public static Uri getCallUri(String number) {
231        if (PhoneNumberUtils.isUriNumber(number)) {
232             return Uri.fromParts(Constants.SCHEME_SIP, number, null);
233        }
234        return Uri.fromParts(Constants.SCHEME_TEL, number, null);
235     }
236
237    /**
238     * Return an Intent for making a phone call. Scheme (e.g. tel, sip) will be determined
239     * automatically.
240     */
241    public static Intent getCallIntent(String number) {
242        return getCallIntent(number, null);
243    }
244
245    /**
246     * Return an Intent for making a phone call. A given Uri will be used as is (without any
247     * sanity check).
248     */
249    public static Intent getCallIntent(Uri uri) {
250        return getCallIntent(uri, null);
251    }
252
253    /**
254     * A variant of {@link #getCallIntent(String)} but also accept a call origin. For more
255     * information about call origin, see comments in Phone package (PhoneApp).
256     */
257    public static Intent getCallIntent(String number, String callOrigin) {
258        return getCallIntent(getCallUri(number), callOrigin);
259    }
260
261    /**
262     * A variant of {@link #getCallIntent(Uri)} but also accept a call origin. For more
263     * information about call origin, see comments in Phone package (PhoneApp).
264     */
265    public static Intent getCallIntent(Uri uri, String callOrigin) {
266        final Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, uri);
267        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
268        if (callOrigin != null) {
269            intent.putExtra(DialtactsActivity.EXTRA_CALL_ORIGIN, callOrigin);
270        }
271        return intent;
272    }
273
274    /**
275     * Return an Intent for launching voicemail screen.
276     */
277    public static Intent getVoicemailIntent() {
278        final Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
279                Uri.fromParts("voicemail", "", null));
280        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
281        return intent;
282    }
283
284    /**
285     * Returns a header view based on the R.layout.list_separator, where the
286     * containing {@link TextView} is set using the given textResourceId.
287     */
288    public static View createHeaderView(Context context, int textResourceId) {
289        View view = View.inflate(context, R.layout.list_separator, null);
290        TextView textView = (TextView) view.findViewById(R.id.title);
291        textView.setText(context.getString(textResourceId));
292        return view;
293    }
294
295    /**
296     * Returns the {@link Rect} with left, top, right, and bottom coordinates
297     * that are equivalent to the given {@link View}'s bounds. This is equivalent to how the
298     * target {@link Rect} is calculated in {@link QuickContact#showQuickContact}.
299     */
300    public static Rect getTargetRectFromView(Context context, View view) {
301        final float appScale = context.getResources().getCompatibilityInfo().applicationScale;
302        final int[] pos = new int[2];
303        view.getLocationOnScreen(pos);
304
305        final Rect rect = new Rect();
306        rect.left = (int) (pos[0] * appScale + 0.5f);
307        rect.top = (int) (pos[1] * appScale + 0.5f);
308        rect.right = (int) ((pos[0] + view.getWidth()) * appScale + 0.5f);
309        rect.bottom = (int) ((pos[1] + view.getHeight()) * appScale + 0.5f);
310        return rect;
311    }
312
313    /**
314     * Returns the size (width and height) of thumbnail pictures as configured in the provider. This
315     * can safely be called from the UI thread, as the provider can serve this without performing
316     * a database access
317     */
318    public static int getThumbnailSize(Context context) {
319        if (sThumbnailSize == -1) {
320            final Cursor c = context.getContentResolver().query(
321                    DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI,
322                    new String[] { DisplayPhoto.THUMBNAIL_MAX_DIM }, null, null, null);
323            try {
324                c.moveToFirst();
325                sThumbnailSize = c.getInt(0);
326            } finally {
327                c.close();
328            }
329        }
330        return sThumbnailSize;
331    }
332
333    /**
334     * @return if the context is in landscape orientation.
335     */
336    public static boolean isLandscape(Context context) {
337        return context.getResources().getConfiguration().orientation
338                == Configuration.ORIENTATION_LANDSCAPE;
339    }
340}
341