1/*
2 * Copyright (C) 2012 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.common;
18
19import com.google.i18n.phonenumbers.NumberParseException;
20import com.google.i18n.phonenumbers.PhoneNumberUtil;
21
22import android.content.Context;
23import android.content.Intent;
24import android.graphics.Rect;
25import android.net.Uri;
26import android.provider.ContactsContract;
27import android.telephony.PhoneNumberUtils;
28import android.text.TextUtils;
29import android.view.View;
30import android.widget.TextView;
31
32import com.android.contacts.common.model.account.AccountType;
33
34/**
35 * Shared static contact utility methods.
36 */
37public class MoreContactUtils {
38
39    private static final String WAIT_SYMBOL_AS_STRING = String.valueOf(PhoneNumberUtils.WAIT);
40
41    /**
42     * Returns true if two data with mimetypes which represent values in contact entries are
43     * considered equal for collapsing in the GUI. For caller-id, use
44     * {@link android.telephony.PhoneNumberUtils#compare(android.content.Context, String, String)}
45     * instead
46     */
47    public static boolean shouldCollapse(CharSequence mimetype1, CharSequence data1,
48              CharSequence mimetype2, CharSequence data2) {
49        // different mimetypes? don't collapse
50        if (!TextUtils.equals(mimetype1, mimetype2)) return false;
51
52        // exact same string? good, bail out early
53        if (TextUtils.equals(data1, data2)) return true;
54
55        // so if either is null, these two must be different
56        if (data1 == null || data2 == null) return false;
57
58        // if this is not about phone numbers, we know this is not a match (of course, some
59        // mimetypes could have more sophisticated matching is the future, e.g. addresses)
60        if (!TextUtils.equals(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE,
61                mimetype1)) {
62            return false;
63        }
64
65        return shouldCollapsePhoneNumbers(data1.toString(), data2.toString());
66    }
67
68    // TODO: Move this to PhoneDataItem.shouldCollapse override
69    private static boolean shouldCollapsePhoneNumbers(String number1, String number2) {
70        // Now do the full phone number thing. split into parts, separated by waiting symbol
71        // and compare them individually
72        final String[] dataParts1 = number1.split(WAIT_SYMBOL_AS_STRING);
73        final String[] dataParts2 = number2.split(WAIT_SYMBOL_AS_STRING);
74        if (dataParts1.length != dataParts2.length) return false;
75        final PhoneNumberUtil util = PhoneNumberUtil.getInstance();
76        for (int i = 0; i < dataParts1.length; i++) {
77            // Match phone numbers represented by keypad letters, in which case prefer the
78            // phone number with letters.
79            final String dataPart1 = PhoneNumberUtils.convertKeypadLettersToDigits(dataParts1[i]);
80            final String dataPart2 = dataParts2[i];
81
82            // substrings equal? shortcut, don't parse
83            if (TextUtils.equals(dataPart1, dataPart2)) continue;
84
85            // do a full parse of the numbers
86            final PhoneNumberUtil.MatchType result = util.isNumberMatch(dataPart1, dataPart2);
87            switch (result) {
88                case NOT_A_NUMBER:
89                    // don't understand the numbers? let's play it safe
90                    return false;
91                case NO_MATCH:
92                    return false;
93                case EXACT_MATCH:
94                    break;
95                case NSN_MATCH:
96                    try {
97                        // For NANP phone numbers, match when one has +1 and the other does not.
98                        // In this case, prefer the +1 version.
99                        if (util.parse(dataPart1, null).getCountryCode() == 1) {
100                            // At this point, the numbers can be either case 1 or 2 below....
101                            //
102                            // case 1)
103                            // +14155551212    <--- country code 1
104                            //  14155551212    <--- 1 is trunk prefix, not country code
105                            //
106                            // and
107                            //
108                            // case 2)
109                            // +14155551212
110                            //   4155551212
111                            //
112                            // From b/7519057, case 2 needs to be equal.  But also that bug, case 3
113                            // below should not be equal.
114                            //
115                            // case 3)
116                            // 14155551212
117                            //  4155551212
118                            //
119                            // So in order to make sure transitive equality is valid, case 1 cannot
120                            // be equal.  Otherwise, transitive equality breaks and the following
121                            // would all be collapsed:
122                            //   4155551212  |
123                            //  14155551212  |---->   +14155551212
124                            // +14155551212  |
125                            //
126                            // With transitive equality, the collapsed values should be:
127                            //   4155551212  |         14155551212
128                            //  14155551212  |---->   +14155551212
129                            // +14155551212  |
130
131                            // Distinguish between case 1 and 2 by checking for trunk prefix '1'
132                            // at the start of number 2.
133                            if (dataPart2.trim().charAt(0) == '1') {
134                                // case 1
135                                return false;
136                            }
137                            break;
138                        }
139                    } catch (NumberParseException e) {
140                        // This is the case where the first number does not have a country code.
141                        // examples:
142                        // (123) 456-7890   &   123-456-7890  (collapse)
143                        // 0049 (8092) 1234   &   +49/80921234  (unit test says do not collapse)
144
145                        // Check the second number.  If it also does not have a country code, then
146                        // we should collapse.  If it has a country code, then it's a different
147                        // number and we should not collapse (this conclusion is based on an
148                        // existing unit test).
149                        try {
150                            util.parse(dataPart2, null);
151                        } catch (NumberParseException e2) {
152                            // Number 2 also does not have a country.  Collapse.
153                            break;
154                        }
155                    }
156                    return false;
157                case SHORT_NSN_MATCH:
158                    return false;
159                default:
160                    throw new IllegalStateException("Unknown result value from phone number " +
161                            "library");
162            }
163        }
164        return true;
165    }
166
167    /**
168     * Returns the {@link android.graphics.Rect} with left, top, right, and bottom coordinates
169     * that are equivalent to the given {@link android.view.View}'s bounds. This is equivalent to
170     * how the target {@link android.graphics.Rect} is calculated in
171     * {@link android.provider.ContactsContract.QuickContact#showQuickContact}.
172     */
173    public static Rect getTargetRectFromView(View view) {
174        final int[] pos = new int[2];
175        view.getLocationOnScreen(pos);
176
177        final Rect rect = new Rect();
178        rect.left = pos[0];
179        rect.top = pos[1];
180        rect.right = pos[0] + view.getWidth();
181        rect.bottom = pos[1] + view.getHeight();
182        return rect;
183    }
184
185    /**
186     * Returns a header view based on the R.layout.list_separator, where the
187     * containing {@link android.widget.TextView} is set using the given textResourceId.
188     */
189    public static TextView createHeaderView(Context context, int textResourceId) {
190        final TextView textView = (TextView) View.inflate(context, R.layout.list_separator, null);
191        textView.setText(context.getString(textResourceId));
192        return textView;
193    }
194
195    /**
196     * Set the top padding on the header view dynamically, based on whether the header is in
197     * the first row or not.
198     */
199    public static void setHeaderViewBottomPadding(Context context, TextView textView,
200            boolean isFirstRow) {
201        final int topPadding;
202        if (isFirstRow) {
203            topPadding = (int) context.getResources().getDimension(
204                    R.dimen.frequently_contacted_title_top_margin_when_first_row);
205        } else {
206            topPadding = (int) context.getResources().getDimension(
207                    R.dimen.frequently_contacted_title_top_margin);
208        }
209        textView.setPaddingRelative(textView.getPaddingStart(), topPadding,
210                textView.getPaddingEnd(), textView.getPaddingBottom());
211    }
212
213
214    /**
215     * Returns the intent to launch for the given invitable account type and contact lookup URI.
216     * This will return null if the account type is not invitable (i.e. there is no
217     * {@link AccountType#getInviteContactActivityClassName()} or
218     * {@link AccountType#syncAdapterPackageName}).
219     */
220    public static Intent getInvitableIntent(AccountType accountType, Uri lookupUri) {
221        String syncAdapterPackageName = accountType.syncAdapterPackageName;
222        String className = accountType.getInviteContactActivityClassName();
223        if (TextUtils.isEmpty(syncAdapterPackageName) || TextUtils.isEmpty(className)) {
224            return null;
225        }
226        Intent intent = new Intent();
227        intent.setClassName(syncAdapterPackageName, className);
228
229        intent.setAction(ContactsContract.Intents.INVITE_CONTACT);
230
231        // Data is the lookup URI.
232        intent.setData(lookupUri);
233        return intent;
234    }
235}
236