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 android.content.Context;
20import android.content.Intent;
21import android.graphics.Rect;
22import android.net.Uri;
23import android.provider.ContactsContract;
24import android.telephony.PhoneNumberUtils;
25import android.text.TextUtils;
26import android.view.View;
27import android.widget.TextView;
28import com.android.contacts.common.model.account.AccountType;
29import com.google.i18n.phonenumbers.NumberParseException;
30import com.google.i18n.phonenumbers.PhoneNumberUtil;
31
32/** Shared static contact utility methods. */
33public 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   */
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
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   */
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   */
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   */
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   */
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