CallerInfoUtils.java revision cded3beaf28a703e1ef8f71bbc6836e6806c3736
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
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.annotation.NonNull;
26import 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   */
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
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   */
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, @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 */
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
229  private static boolean isCnapSpecialCaseRestricted(String n) {
230    return n.equals("PRIVATE") || n.equals("P") || n.equals("RES") || n.equals("PRIVATENUMBER");
231  }
232
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 */
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   */
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