Home | History | Annotate | Download | only in incallui
      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 
     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    */
     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 
     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    */
    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 */
    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 
    210   private static boolean isCnapSpecialCaseRestricted(String n) {
    211     return n.equals("PRIVATE") || n.equals("P") || n.equals("RES") || n.equals("PRIVATENUMBER");
    212   }
    213 
    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 */
    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    */
    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. */
    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