• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.contacts.common;
18 
19 import com.android.i18n.phonenumbers.NumberParseException;
20 import com.android.i18n.phonenumbers.PhoneNumberUtil;
21 
22 import android.content.Context;
23 import android.content.Intent;
24 import android.graphics.Rect;
25 import android.net.Uri;
26 import android.provider.ContactsContract;
27 import android.telephony.PhoneNumberUtils;
28 import android.text.TextUtils;
29 import android.view.View;
30 import android.widget.TextView;
31 
32 import com.android.contacts.common.model.account.AccountType;
33 
34 /**
35  * Shared static contact utility methods.
36  */
37 public 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      */
shouldCollapse(CharSequence mimetype1, CharSequence data1, CharSequence mimetype2, CharSequence data2)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 
shouldCollapsePhoneNumbers(String number1, String number2)68     private static boolean shouldCollapsePhoneNumbers(String number1, String number2) {
69         // Now do the full phone number thing. split into parts, separated by waiting symbol
70         // and compare them individually
71         final String[] dataParts1 = number1.split(WAIT_SYMBOL_AS_STRING);
72         final String[] dataParts2 = number2.split(WAIT_SYMBOL_AS_STRING);
73         if (dataParts1.length != dataParts2.length) return false;
74         final PhoneNumberUtil util = PhoneNumberUtil.getInstance();
75         for (int i = 0; i < dataParts1.length; i++) {
76             // Match phone numbers represented by keypad letters, in which case prefer the
77             // phone number with letters.
78             final String dataPart1 = PhoneNumberUtils.convertKeypadLettersToDigits(dataParts1[i]);
79             final String dataPart2 = dataParts2[i];
80 
81             // substrings equal? shortcut, don't parse
82             if (TextUtils.equals(dataPart1, dataPart2)) continue;
83 
84             // do a full parse of the numbers
85             final PhoneNumberUtil.MatchType result = util.isNumberMatch(dataPart1, dataPart2);
86             switch (result) {
87                 case NOT_A_NUMBER:
88                     // don't understand the numbers? let's play it safe
89                     return false;
90                 case NO_MATCH:
91                     return false;
92                 case EXACT_MATCH:
93                     break;
94                 case NSN_MATCH:
95                     try {
96                         // For NANP phone numbers, match when one has +1 and the other does not.
97                         // In this case, prefer the +1 version.
98                         if (util.parse(dataPart1, null).getCountryCode() == 1) {
99                             // At this point, the numbers can be either case 1 or 2 below....
100                             //
101                             // case 1)
102                             // +14155551212    <--- country code 1
103                             //  14155551212    <--- 1 is trunk prefix, not country code
104                             //
105                             // and
106                             //
107                             // case 2)
108                             // +14155551212
109                             //   4155551212
110                             //
111                             // From b/7519057, case 2 needs to be equal.  But also that bug, case 3
112                             // below should not be equal.
113                             //
114                             // case 3)
115                             // 14155551212
116                             //  4155551212
117                             //
118                             // So in order to make sure transitive equality is valid, case 1 cannot
119                             // be equal.  Otherwise, transitive equality breaks and the following
120                             // would all be collapsed:
121                             //   4155551212  |
122                             //  14155551212  |---->   +14155551212
123                             // +14155551212  |
124                             //
125                             // With transitive equality, the collapsed values should be:
126                             //   4155551212  |         14155551212
127                             //  14155551212  |---->   +14155551212
128                             // +14155551212  |
129 
130                             // Distinguish between case 1 and 2 by checking for trunk prefix '1'
131                             // at the start of number 2.
132                             if (dataPart2.trim().charAt(0) == '1') {
133                                 // case 1
134                                 return false;
135                             }
136                             break;
137                         }
138                     } catch (NumberParseException e) {
139                         // This is the case where the first number does not have a country code.
140                         // examples:
141                         // (123) 456-7890   &   123-456-7890  (collapse)
142                         // 0049 (8092) 1234   &   +49/80921234  (unit test says do not collapse)
143 
144                         // Check the second number.  If it also does not have a country code, then
145                         // we should collapse.  If it has a country code, then it's a different
146                         // number and we should not collapse (this conclusion is based on an
147                         // existing unit test).
148                         try {
149                             util.parse(dataPart2, null);
150                         } catch (NumberParseException e2) {
151                             // Number 2 also does not have a country.  Collapse.
152                             break;
153                         }
154                     }
155                     return false;
156                 case SHORT_NSN_MATCH:
157                     return false;
158                 default:
159                     throw new IllegalStateException("Unknown result value from phone number " +
160                             "library");
161             }
162         }
163         return true;
164     }
165 
166     /**
167      * Returns the {@link android.graphics.Rect} with left, top, right, and bottom coordinates
168      * that are equivalent to the given {@link android.view.View}'s bounds. This is equivalent to
169      * how the target {@link android.graphics.Rect} is calculated in
170      * {@link android.provider.ContactsContract.QuickContact#showQuickContact}.
171      */
getTargetRectFromView(Context context, View view)172     public static Rect getTargetRectFromView(Context context, View view) {
173         final float appScale = context.getResources().getCompatibilityInfo().applicationScale;
174         final int[] pos = new int[2];
175         view.getLocationOnScreen(pos);
176 
177         final Rect rect = new Rect();
178         rect.left = (int) (pos[0] * appScale + 0.5f);
179         rect.top = (int) (pos[1] * appScale + 0.5f);
180         rect.right = (int) ((pos[0] + view.getWidth()) * appScale + 0.5f);
181         rect.bottom = (int) ((pos[1] + view.getHeight()) * appScale + 0.5f);
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      */
createHeaderView(Context context, int textResourceId)189     public static View createHeaderView(Context context, int textResourceId) {
190         View view = View.inflate(context, R.layout.list_separator, null);
191         TextView textView = (TextView) view.findViewById(R.id.title);
192         textView.setText(context.getString(textResourceId));
193         textView.setAllCaps(true);
194         return view;
195     }
196 
197     /**
198      * Returns the intent to launch for the given invitable account type and contact lookup URI.
199      * This will return null if the account type is not invitable (i.e. there is no
200      * {@link AccountType#getInviteContactActivityClassName()} or
201      * {@link AccountType#syncAdapterPackageName}).
202      */
getInvitableIntent(AccountType accountType, Uri lookupUri)203     public static Intent getInvitableIntent(AccountType accountType, Uri lookupUri) {
204         String syncAdapterPackageName = accountType.syncAdapterPackageName;
205         String className = accountType.getInviteContactActivityClassName();
206         if (TextUtils.isEmpty(syncAdapterPackageName) || TextUtils.isEmpty(className)) {
207             return null;
208         }
209         Intent intent = new Intent();
210         intent.setClassName(syncAdapterPackageName, className);
211 
212         intent.setAction(ContactsContract.Intents.INVITE_CONTACT);
213 
214         // Data is the lookup URI.
215         intent.setData(lookupUri);
216         return intent;
217     }
218 }
219