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 com.android.contacts.model.AccountType;
20import com.android.contacts.model.AccountTypeManager;
21import com.android.contacts.model.AccountWithDataSet;
22import com.android.contacts.test.NeededForTesting;
23import com.android.i18n.phonenumbers.PhoneNumberUtil;
24
25import android.content.Context;
26import android.content.Intent;
27import android.graphics.Rect;
28import android.location.CountryDetector;
29import android.net.Uri;
30import android.provider.ContactsContract;
31import android.provider.ContactsContract.CommonDataKinds.Im;
32import android.provider.ContactsContract.CommonDataKinds.Phone;
33import android.telephony.PhoneNumberUtils;
34import android.text.TextUtils;
35import android.view.View;
36import android.widget.TextView;
37
38import java.util.List;
39
40public class ContactsUtils {
41    private static final String TAG = "ContactsUtils";
42    private static final String WAIT_SYMBOL_AS_STRING = String.valueOf(PhoneNumberUtils.WAIT);
43
44
45    // TODO find a proper place for the canonical version of these
46    public interface ProviderNames {
47        String YAHOO = "Yahoo";
48        String GTALK = "GTalk";
49        String MSN = "MSN";
50        String ICQ = "ICQ";
51        String AIM = "AIM";
52        String XMPP = "XMPP";
53        String JABBER = "JABBER";
54        String SKYPE = "SKYPE";
55        String QQ = "QQ";
56    }
57
58    /**
59     * This looks up the provider name defined in
60     * ProviderNames from the predefined IM protocol id.
61     * This is used for interacting with the IM application.
62     *
63     * @param protocol the protocol ID
64     * @return the provider name the IM app uses for the given protocol, or null if no
65     * provider is defined for the given protocol
66     * @hide
67     */
68    public static String lookupProviderNameFromId(int protocol) {
69        switch (protocol) {
70            case Im.PROTOCOL_GOOGLE_TALK:
71                return ProviderNames.GTALK;
72            case Im.PROTOCOL_AIM:
73                return ProviderNames.AIM;
74            case Im.PROTOCOL_MSN:
75                return ProviderNames.MSN;
76            case Im.PROTOCOL_YAHOO:
77                return ProviderNames.YAHOO;
78            case Im.PROTOCOL_ICQ:
79                return ProviderNames.ICQ;
80            case Im.PROTOCOL_JABBER:
81                return ProviderNames.JABBER;
82            case Im.PROTOCOL_SKYPE:
83                return ProviderNames.SKYPE;
84            case Im.PROTOCOL_QQ:
85                return ProviderNames.QQ;
86        }
87        return null;
88    }
89
90    /**
91     * Test if the given {@link CharSequence} contains any graphic characters,
92     * first checking {@link TextUtils#isEmpty(CharSequence)} to handle null.
93     */
94    public static boolean isGraphic(CharSequence str) {
95        return !TextUtils.isEmpty(str) && TextUtils.isGraphic(str);
96    }
97
98    /**
99     * Returns true if two objects are considered equal.  Two null references are equal here.
100     */
101    @NeededForTesting
102    public static boolean areObjectsEqual(Object a, Object b) {
103        return a == b || (a != null && a.equals(b));
104    }
105
106    /**
107     * Returns true if two data with mimetypes which represent values in contact entries are
108     * considered equal for collapsing in the GUI. For caller-id, use
109     * {@link PhoneNumberUtils#compare(Context, String, String)} instead
110     */
111    public static final boolean shouldCollapse(CharSequence mimetype1, CharSequence data1,
112            CharSequence mimetype2, CharSequence data2) {
113        // different mimetypes? don't collapse
114        if (!TextUtils.equals(mimetype1, mimetype2)) return false;
115
116        // exact same string? good, bail out early
117        if (TextUtils.equals(data1, data2)) return true;
118
119        // so if either is null, these two must be different
120        if (data1 == null || data2 == null) return false;
121
122        // if this is not about phone numbers, we know this is not a match (of course, some
123        // mimetypes could have more sophisticated matching is the future, e.g. addresses)
124        if (!TextUtils.equals(Phone.CONTENT_ITEM_TYPE, mimetype1)) return false;
125
126        // Now do the full phone number thing. split into parts, seperated by waiting symbol
127        // and compare them individually
128        final String[] dataParts1 = data1.toString().split(WAIT_SYMBOL_AS_STRING);
129        final String[] dataParts2 = data2.toString().split(WAIT_SYMBOL_AS_STRING);
130        if (dataParts1.length != dataParts2.length) return false;
131        final PhoneNumberUtil util = PhoneNumberUtil.getInstance();
132        for (int i = 0; i < dataParts1.length; i++) {
133            final String dataPart1 = dataParts1[i];
134            final String dataPart2 = dataParts2[i];
135
136            // substrings equal? shortcut, don't parse
137            if (TextUtils.equals(dataPart1, dataPart2)) continue;
138
139            // do a full parse of the numbers
140            switch (util.isNumberMatch(dataPart1, dataPart2)) {
141                case NOT_A_NUMBER:
142                    // don't understand the numbers? let's play it safe
143                    return false;
144                case NO_MATCH:
145                    return false;
146                case EXACT_MATCH:
147                case SHORT_NSN_MATCH:
148                case NSN_MATCH:
149                    break;
150                default:
151                    throw new IllegalStateException("Unknown result value from phone number " +
152                            "library");
153            }
154        }
155        return true;
156    }
157
158    /**
159     * Returns true if two {@link Intent}s are both null, or have the same action.
160     */
161    public static final boolean areIntentActionEqual(Intent a, Intent b) {
162        if (a == b) {
163            return true;
164        }
165        if (a == null || b == null) {
166            return false;
167        }
168        return TextUtils.equals(a.getAction(), b.getAction());
169    }
170
171    /**
172     * @return The ISO 3166-1 two letters country code of the country the user
173     *         is in.
174     */
175    public static final String getCurrentCountryIso(Context context) {
176        CountryDetector detector =
177                (CountryDetector) context.getSystemService(Context.COUNTRY_DETECTOR);
178        return detector.detectCountry().getCountryIso();
179    }
180
181    public static boolean areContactWritableAccountsAvailable(Context context) {
182        final List<AccountWithDataSet> accounts =
183                AccountTypeManager.getInstance(context).getAccounts(true /* writeable */);
184        return !accounts.isEmpty();
185    }
186
187    public static boolean areGroupWritableAccountsAvailable(Context context) {
188        final List<AccountWithDataSet> accounts =
189                AccountTypeManager.getInstance(context).getGroupWritableAccounts();
190        return !accounts.isEmpty();
191    }
192
193    /**
194     * Returns the intent to launch for the given invitable account type and contact lookup URI.
195     * This will return null if the account type is not invitable (i.e. there is no
196     * {@link AccountType#getInviteContactActivityClassName()} or
197     * {@link AccountType#resPackageName}).
198     */
199    public static Intent getInvitableIntent(AccountType accountType, Uri lookupUri) {
200        String resPackageName = accountType.resPackageName;
201        String className = accountType.getInviteContactActivityClassName();
202        if (TextUtils.isEmpty(resPackageName) || TextUtils.isEmpty(className)) {
203            return null;
204        }
205        Intent intent = new Intent();
206        intent.setClassName(resPackageName, className);
207
208        intent.setAction(ContactsContract.Intents.INVITE_CONTACT);
209
210        // Data is the lookup URI.
211        intent.setData(lookupUri);
212        return intent;
213    }
214
215    /**
216     * Returns a header view based on the R.layout.list_separator, where the
217     * containing {@link TextView} is set using the given textResourceId.
218     */
219    public static View createHeaderView(Context context, int textResourceId) {
220        View view = View.inflate(context, R.layout.list_separator, null);
221        TextView textView = (TextView) view.findViewById(R.id.title);
222        textView.setText(context.getString(textResourceId));
223        return view;
224    }
225
226    /**
227     * Returns the {@link Rect} with left, top, right, and bottom coordinates
228     * that are equivalent to the given {@link View}'s bounds. This is equivalent to how the
229     * target {@link Rect} is calculated in {@link QuickContact#showQuickContact}.
230     */
231    public static Rect getTargetRectFromView(Context context, View view) {
232        final float appScale = context.getResources().getCompatibilityInfo().applicationScale;
233        final int[] pos = new int[2];
234        view.getLocationOnScreen(pos);
235
236        final Rect rect = new Rect();
237        rect.left = (int) (pos[0] * appScale + 0.5f);
238        rect.top = (int) (pos[1] * appScale + 0.5f);
239        rect.right = (int) ((pos[0] + view.getWidth()) * appScale + 0.5f);
240        rect.bottom = (int) ((pos[1] + view.getHeight()) * appScale + 0.5f);
241        return rect;
242    }
243}
244