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