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