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