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