Home | History | Annotate | Download | only in common
      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.google.i18n.phonenumbers.NumberParseException;
     20 import com.google.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      */
     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         // 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) return false;
     83         final PhoneNumberUtil util = PhoneNumberUtil.getInstance();
     84         for (int i = 0; i < dataParts1.length; i++) {
     85             // Match phone numbers represented by keypad letters, in which case prefer the
     86             // phone number with letters.
     87             final String dataPart1 = PhoneNumberUtils.convertKeypadLettersToDigits(dataParts1[i]);
     88             final String dataPart2 = dataParts2[i];
     89 
     90             // substrings equal? shortcut, don't parse
     91             if (TextUtils.equals(dataPart1, dataPart2)) continue;
     92 
     93             // do a full parse of the numbers
     94             final PhoneNumberUtil.MatchType result = util.isNumberMatch(dataPart1, dataPart2);
     95             switch (result) {
     96                 case NOT_A_NUMBER:
     97                     // don't understand the numbers? let's play it safe
     98                     return false;
     99                 case NO_MATCH:
    100                     return false;
    101                 case EXACT_MATCH:
    102                     break;
    103                 case NSN_MATCH:
    104                     try {
    105                         // For NANP phone numbers, match when one has +1 and the other does not.
    106                         // In this case, prefer the +1 version.
    107                         if (util.parse(dataPart1, null).getCountryCode() == 1) {
    108                             // At this point, the numbers can be either case 1 or 2 below....
    109                             //
    110                             // case 1)
    111                             // +14155551212    <--- country code 1
    112                             //  14155551212    <--- 1 is trunk prefix, not country code
    113                             //
    114                             // and
    115                             //
    116                             // case 2)
    117                             // +14155551212
    118                             //   4155551212
    119                             //
    120                             // From b/7519057, case 2 needs to be equal.  But also that bug, case 3
    121                             // below should not be equal.
    122                             //
    123                             // case 3)
    124                             // 14155551212
    125                             //  4155551212
    126                             //
    127                             // So in order to make sure transitive equality is valid, case 1 cannot
    128                             // be equal.  Otherwise, transitive equality breaks and the following
    129                             // would all be collapsed:
    130                             //   4155551212  |
    131                             //  14155551212  |---->   +14155551212
    132                             // +14155551212  |
    133                             //
    134                             // With transitive equality, the collapsed values should be:
    135                             //   4155551212  |         14155551212
    136                             //  14155551212  |---->   +14155551212
    137                             // +14155551212  |
    138 
    139                             // Distinguish between case 1 and 2 by checking for trunk prefix '1'
    140                             // at the start of number 2.
    141                             if (dataPart2.trim().charAt(0) == '1') {
    142                                 // case 1
    143                                 return false;
    144                             }
    145                             break;
    146                         }
    147                     } catch (NumberParseException e) {
    148                         // This is the case where the first number does not have a country code.
    149                         // examples:
    150                         // (123) 456-7890   &   123-456-7890  (collapse)
    151                         // 0049 (8092) 1234   &   +49/80921234  (unit test says do not collapse)
    152 
    153                         // Check the second number.  If it also does not have a country code, then
    154                         // we should collapse.  If it has a country code, then it's a different
    155                         // number and we should not collapse (this conclusion is based on an
    156                         // existing unit test).
    157                         try {
    158                             util.parse(dataPart2, null);
    159                         } catch (NumberParseException e2) {
    160                             // Number 2 also does not have a country.  Collapse.
    161                             break;
    162                         }
    163                     }
    164                     return false;
    165                 case SHORT_NSN_MATCH:
    166                     return false;
    167                 default:
    168                     throw new IllegalStateException("Unknown result value from phone number " +
    169                             "library");
    170             }
    171         }
    172         return true;
    173     }
    174 
    175     /**
    176      * Returns the {@link android.graphics.Rect} with left, top, right, and bottom coordinates
    177      * that are equivalent to the given {@link android.view.View}'s bounds. This is equivalent to
    178      * how the target {@link android.graphics.Rect} is calculated in
    179      * {@link android.provider.ContactsContract.QuickContact#showQuickContact}.
    180      */
    181     public static Rect getTargetRectFromView(View view) {
    182         final int[] pos = new int[2];
    183         view.getLocationOnScreen(pos);
    184 
    185         final Rect rect = new Rect();
    186         rect.left = pos[0];
    187         rect.top = pos[1];
    188         rect.right = pos[0] + view.getWidth();
    189         rect.bottom = pos[1] + view.getHeight();
    190         return rect;
    191     }
    192 
    193     /**
    194      * Returns a header view based on the R.layout.list_separator, where the
    195      * containing {@link android.widget.TextView} is set using the given textResourceId.
    196      */
    197     public static TextView createHeaderView(Context context, int textResourceId) {
    198         final TextView textView = (TextView) View.inflate(context, R.layout.list_separator, null);
    199         textView.setText(context.getString(textResourceId));
    200         return textView;
    201     }
    202 
    203     /**
    204      * Set the top padding on the header view dynamically, based on whether the header is in
    205      * the first row or not.
    206      */
    207     public static void setHeaderViewBottomPadding(Context context, TextView textView,
    208             boolean isFirstRow) {
    209         final int topPadding;
    210         if (isFirstRow) {
    211             topPadding = (int) context.getResources().getDimension(
    212                     R.dimen.frequently_contacted_title_top_margin_when_first_row);
    213         } else {
    214             topPadding = (int) context.getResources().getDimension(
    215                     R.dimen.frequently_contacted_title_top_margin);
    216         }
    217         textView.setPaddingRelative(textView.getPaddingStart(), topPadding,
    218                 textView.getPaddingEnd(), textView.getPaddingBottom());
    219     }
    220 
    221 
    222     /**
    223      * Returns the intent to launch for the given invitable account type and contact lookup URI.
    224      * This will return null if the account type is not invitable (i.e. there is no
    225      * {@link AccountType#getInviteContactActivityClassName()} or
    226      * {@link AccountType#syncAdapterPackageName}).
    227      */
    228     public static Intent getInvitableIntent(AccountType accountType, Uri lookupUri) {
    229         String syncAdapterPackageName = accountType.syncAdapterPackageName;
    230         String className = accountType.getInviteContactActivityClassName();
    231         if (TextUtils.isEmpty(syncAdapterPackageName) || TextUtils.isEmpty(className)) {
    232             return null;
    233         }
    234         Intent intent = new Intent();
    235         intent.setClassName(syncAdapterPackageName, className);
    236 
    237         intent.setAction(ContactsContract.Intents.INVITE_CONTACT);
    238 
    239         // Data is the lookup URI.
    240         intent.setData(lookupUri);
    241         return intent;
    242     }
    243 }
    244