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