1 package com.android.incallui; 2 3 import android.content.Context; 4 import android.content.Loader; 5 import android.content.Loader.OnLoadCompleteListener; 6 import android.net.Uri; 7 import android.telecom.PhoneAccount; 8 import android.telecom.TelecomManager; 9 import android.text.TextUtils; 10 import android.util.Log; 11 12 import com.android.contacts.common.model.Contact; 13 import com.android.contacts.common.model.ContactLoader; 14 import com.android.dialer.R; 15 import com.android.dialer.calllog.ContactInfo; 16 import com.android.dialer.service.CachedNumberLookupService; 17 import com.android.dialer.service.CachedNumberLookupService.CachedContactInfo; 18 import com.android.dialer.util.TelecomUtil; 19 20 import java.util.Arrays; 21 22 /** 23 * Utility methods for contact and caller info related functionality 24 */ 25 public class CallerInfoUtils { 26 27 private static final String TAG = CallerInfoUtils.class.getSimpleName(); 28 29 /** Define for not a special CNAP string */ 30 private static final int CNAP_SPECIAL_CASE_NO = -1; 31 CallerInfoUtils()32 public CallerInfoUtils() { 33 } 34 35 private static final int QUERY_TOKEN = -1; 36 37 /** 38 * This is called to get caller info for a call. This will return a CallerInfo 39 * object immediately based off information in the call, but 40 * more information is returned to the OnQueryCompleteListener (which contains 41 * information about the phone number label, user's name, etc). 42 */ getCallerInfoForCall(Context context, Call call, CallerInfoAsyncQuery.OnQueryCompleteListener listener)43 public static CallerInfo getCallerInfoForCall(Context context, Call call, 44 CallerInfoAsyncQuery.OnQueryCompleteListener listener) { 45 CallerInfo info = buildCallerInfo(context, call); 46 47 // TODO: Have phoneapp send a Uri when it knows the contact that triggered this call. 48 49 if (info.numberPresentation == TelecomManager.PRESENTATION_ALLOWED) { 50 // Start the query with the number provided from the call. 51 Log.d(TAG, "==> Actually starting CallerInfoAsyncQuery.startQuery()..."); 52 CallerInfoAsyncQuery.startQuery(QUERY_TOKEN, context, info, listener, call); 53 } 54 return info; 55 } 56 buildCallerInfo(Context context, Call call)57 public static CallerInfo buildCallerInfo(Context context, Call call) { 58 CallerInfo info = new CallerInfo(); 59 60 // Store CNAP information retrieved from the Connection (we want to do this 61 // here regardless of whether the number is empty or not). 62 info.cnapName = call.getCnapName(); 63 info.name = info.cnapName; 64 info.numberPresentation = call.getNumberPresentation(); 65 info.namePresentation = call.getCnapNamePresentation(); 66 info.callSubject = call.getCallSubject(); 67 68 String number = call.getNumber(); 69 if (!TextUtils.isEmpty(number)) { 70 final String[] numbers = number.split("&"); 71 number = numbers[0]; 72 if (numbers.length > 1) { 73 info.forwardingNumber = numbers[1]; 74 } 75 76 number = modifyForSpecialCnapCases(context, info, number, info.numberPresentation); 77 info.phoneNumber = number; 78 } 79 80 // Because the InCallUI is immediately launched before the call is connected, occasionally 81 // a voicemail call will be passed to InCallUI as a "voicemail:" URI without a number. 82 // This call should still be handled as a voicemail call. 83 if ((call.getHandle() != null && 84 PhoneAccount.SCHEME_VOICEMAIL.equals(call.getHandle().getScheme())) || 85 isVoiceMailNumber(context, call)) { 86 info.markAsVoiceMail(context); 87 } 88 89 ContactInfoCache.getInstance(context).maybeInsertCnapInformationIntoCache(context, call, 90 info); 91 92 return info; 93 } 94 95 /** 96 * Creates a new {@link CachedContactInfo} from a {@link CallerInfo} 97 * 98 * @param lookupService the {@link CachedNumberLookupService} used to build a 99 * new {@link CachedContactInfo} 100 * @param {@link CallerInfo} object 101 * @return a CachedContactInfo object created from this CallerInfo 102 * @throws NullPointerException if lookupService or ci are null 103 */ buildCachedContactInfo(CachedNumberLookupService lookupService, CallerInfo ci)104 public static CachedContactInfo buildCachedContactInfo(CachedNumberLookupService lookupService, 105 CallerInfo ci) { 106 ContactInfo info = new ContactInfo(); 107 info.name = ci.name; 108 info.type = ci.numberType; 109 info.label = ci.phoneLabel; 110 info.number = ci.phoneNumber; 111 info.normalizedNumber = ci.normalizedNumber; 112 info.photoUri = ci.contactDisplayPhotoUri; 113 info.userType = ci.userType; 114 115 CachedContactInfo cacheInfo = lookupService.buildCachedContactInfo(info); 116 cacheInfo.setLookupKey(ci.lookupKeyOrNull); 117 return cacheInfo; 118 } 119 isVoiceMailNumber(Context context, Call call)120 public static boolean isVoiceMailNumber(Context context, Call call) { 121 return TelecomUtil.isVoicemailNumber(context, 122 call.getTelecomCall().getDetails().getAccountHandle(), 123 call.getNumber()); 124 } 125 126 /** 127 * Handles certain "corner cases" for CNAP. When we receive weird phone numbers 128 * from the network to indicate different number presentations, convert them to 129 * expected number and presentation values within the CallerInfo object. 130 * @param number number we use to verify if we are in a corner case 131 * @param presentation presentation value used to verify if we are in a corner case 132 * @return the new String that should be used for the phone number 133 */ modifyForSpecialCnapCases(Context context, CallerInfo ci, String number, int presentation)134 /* package */static String modifyForSpecialCnapCases(Context context, CallerInfo ci, 135 String number, int presentation) { 136 // Obviously we return number if ci == null, but still return number if 137 // number == null, because in these cases the correct string will still be 138 // displayed/logged after this function returns based on the presentation value. 139 if (ci == null || number == null) return number; 140 141 Log.d(TAG, "modifyForSpecialCnapCases: initially, number=" 142 + toLogSafePhoneNumber(number) 143 + ", presentation=" + presentation + " ci " + ci); 144 145 // "ABSENT NUMBER" is a possible value we could get from the network as the 146 // phone number, so if this happens, change it to "Unknown" in the CallerInfo 147 // and fix the presentation to be the same. 148 final String[] absentNumberValues = 149 context.getResources().getStringArray(R.array.absent_num); 150 if (Arrays.asList(absentNumberValues).contains(number) 151 && presentation == TelecomManager.PRESENTATION_ALLOWED) { 152 number = context.getString(R.string.unknown); 153 ci.numberPresentation = TelecomManager.PRESENTATION_UNKNOWN; 154 } 155 156 // Check for other special "corner cases" for CNAP and fix them similarly. Corner 157 // cases only apply if we received an allowed presentation from the network, so check 158 // if we think we have an allowed presentation, or if the CallerInfo presentation doesn't 159 // match the presentation passed in for verification (meaning we changed it previously 160 // because it's a corner case and we're being called from a different entry point). 161 if (ci.numberPresentation == TelecomManager.PRESENTATION_ALLOWED 162 || (ci.numberPresentation != presentation 163 && presentation == TelecomManager.PRESENTATION_ALLOWED)) { 164 // For all special strings, change number & numberPrentation. 165 if (isCnapSpecialCaseRestricted(number)) { 166 number = context.getString(R.string.private_num); 167 ci.numberPresentation = TelecomManager.PRESENTATION_RESTRICTED; 168 } else if (isCnapSpecialCaseUnknown(number)) { 169 number = context.getString(R.string.unknown); 170 ci.numberPresentation = TelecomManager.PRESENTATION_UNKNOWN; 171 } 172 Log.d(TAG, "SpecialCnap: number=" + toLogSafePhoneNumber(number) 173 + "; presentation now=" + ci.numberPresentation); 174 } 175 Log.d(TAG, "modifyForSpecialCnapCases: returning number string=" 176 + toLogSafePhoneNumber(number)); 177 return number; 178 } 179 isCnapSpecialCaseRestricted(String n)180 private static boolean isCnapSpecialCaseRestricted(String n) { 181 return n.equals("PRIVATE") || n.equals("P") || n.equals("RES"); 182 } 183 isCnapSpecialCaseUnknown(String n)184 private static boolean isCnapSpecialCaseUnknown(String n) { 185 return n.equals("UNAVAILABLE") || n.equals("UNKNOWN") || n.equals("UNA") || n.equals("U"); 186 } 187 toLogSafePhoneNumber(String number)188 /* package */static String toLogSafePhoneNumber(String number) { 189 // For unknown number, log empty string. 190 if (number == null) { 191 return ""; 192 } 193 194 // Todo: Figure out an equivalent for VDBG 195 if (false) { 196 // When VDBG is true we emit PII. 197 return number; 198 } 199 200 // Do exactly same thing as Uri#toSafeString() does, which will enable us to compare 201 // sanitized phone numbers. 202 StringBuilder builder = new StringBuilder(); 203 for (int i = 0; i < number.length(); i++) { 204 char c = number.charAt(i); 205 if (c == '-' || c == '@' || c == '.' || c == '&') { 206 builder.append(c); 207 } else { 208 builder.append('x'); 209 } 210 } 211 return builder.toString(); 212 } 213 214 /** 215 * Send a notification using a {@link ContactLoader} to inform the sync adapter that we are 216 * viewing a particular contact, so that it can download the high-res photo. 217 */ sendViewNotification(Context context, Uri contactUri)218 public static void sendViewNotification(Context context, Uri contactUri) { 219 final ContactLoader loader = new ContactLoader(context, contactUri, 220 true /* postViewNotification */); 221 loader.registerListener(0, new OnLoadCompleteListener<Contact>() { 222 @Override 223 public void onLoadComplete( 224 Loader<Contact> loader, Contact contact) { 225 try { 226 loader.reset(); 227 } catch (RuntimeException e) { 228 Log.e(TAG, "Error resetting loader", e); 229 } 230 } 231 }); 232 loader.startLoading(); 233 } 234 } 235