• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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