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