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