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