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