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