Home | History | Annotate | Download | only in telecom
      1 /*
      2  * Copyright (C) 2015 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.dialer.telecom;
     18 
     19 import android.Manifest;
     20 import android.Manifest.permission;
     21 import android.content.ComponentName;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.content.pm.PackageManager;
     25 import android.net.Uri;
     26 import android.os.Build.VERSION;
     27 import android.os.Build.VERSION_CODES;
     28 import android.provider.CallLog.Calls;
     29 import android.support.annotation.NonNull;
     30 import android.support.annotation.Nullable;
     31 import android.support.annotation.RequiresPermission;
     32 import android.support.annotation.VisibleForTesting;
     33 import android.support.v4.content.ContextCompat;
     34 import android.telecom.PhoneAccount;
     35 import android.telecom.PhoneAccountHandle;
     36 import android.telecom.TelecomManager;
     37 import android.telephony.SubscriptionInfo;
     38 import android.telephony.SubscriptionManager;
     39 import android.text.TextUtils;
     40 import android.util.Pair;
     41 import com.android.dialer.common.LogUtil;
     42 import com.google.common.base.Optional;
     43 import java.util.ArrayList;
     44 import java.util.List;
     45 import java.util.Map;
     46 import java.util.concurrent.ConcurrentHashMap;
     47 
     48 /**
     49  * Performs permission checks before calling into TelecomManager. Each method is self-explanatory -
     50  * perform the required check and return the fallback default if the permission is missing,
     51  * otherwise return the value from TelecomManager.
     52  */
     53 @SuppressWarnings("MissingPermission")
     54 public abstract class TelecomUtil {
     55 
     56   private static final String TAG = "TelecomUtil";
     57   private static boolean warningLogged = false;
     58 
     59   private static TelecomUtilImpl instance = new TelecomUtilImpl();
     60 
     61   /**
     62    * Cache for {@link #isVoicemailNumber(Context, PhoneAccountHandle, String)}. Both
     63    * PhoneAccountHandle and number are cached because multiple numbers might be mapped to true, and
     64    * comparing with {@link #getVoicemailNumber(Context, PhoneAccountHandle)} will not suffice.
     65    */
     66   private static final Map<Pair<PhoneAccountHandle, String>, Boolean> isVoicemailNumberCache =
     67       new ConcurrentHashMap<>();
     68 
     69   @VisibleForTesting(otherwise = VisibleForTesting.NONE)
     70   public static void setInstanceForTesting(TelecomUtilImpl instanceForTesting) {
     71     instance = instanceForTesting;
     72   }
     73 
     74   public static void showInCallScreen(Context context, boolean showDialpad) {
     75     if (hasReadPhoneStatePermission(context)) {
     76       try {
     77         getTelecomManager(context).showInCallScreen(showDialpad);
     78       } catch (SecurityException e) {
     79         // Just in case
     80         LogUtil.w(TAG, "TelecomManager.showInCallScreen called without permission.");
     81       }
     82     }
     83   }
     84 
     85   public static void silenceRinger(Context context) {
     86     if (hasModifyPhoneStatePermission(context)) {
     87       try {
     88         getTelecomManager(context).silenceRinger();
     89       } catch (SecurityException e) {
     90         // Just in case
     91         LogUtil.w(TAG, "TelecomManager.silenceRinger called without permission.");
     92       }
     93     }
     94   }
     95 
     96   public static void cancelMissedCallsNotification(Context context) {
     97     if (hasModifyPhoneStatePermission(context)) {
     98       try {
     99         getTelecomManager(context).cancelMissedCallsNotification();
    100       } catch (SecurityException e) {
    101         LogUtil.w(TAG, "TelecomManager.cancelMissedCalls called without permission.");
    102       }
    103     }
    104   }
    105 
    106   public static Uri getAdnUriForPhoneAccount(Context context, PhoneAccountHandle handle) {
    107     if (hasModifyPhoneStatePermission(context)) {
    108       try {
    109         return getTelecomManager(context).getAdnUriForPhoneAccount(handle);
    110       } catch (SecurityException e) {
    111         LogUtil.w(TAG, "TelecomManager.getAdnUriForPhoneAccount called without permission.");
    112       }
    113     }
    114     return null;
    115   }
    116 
    117   public static boolean handleMmi(
    118       Context context, String dialString, @Nullable PhoneAccountHandle handle) {
    119     if (hasModifyPhoneStatePermission(context)) {
    120       try {
    121         if (handle == null) {
    122           return getTelecomManager(context).handleMmi(dialString);
    123         } else {
    124           return getTelecomManager(context).handleMmi(dialString, handle);
    125         }
    126       } catch (SecurityException e) {
    127         LogUtil.w(TAG, "TelecomManager.handleMmi called without permission.");
    128       }
    129     }
    130     return false;
    131   }
    132 
    133   @Nullable
    134   public static PhoneAccountHandle getDefaultOutgoingPhoneAccount(
    135       Context context, String uriScheme) {
    136     if (hasReadPhoneStatePermission(context)) {
    137       return getTelecomManager(context).getDefaultOutgoingPhoneAccount(uriScheme);
    138     }
    139     return null;
    140   }
    141 
    142   public static PhoneAccount getPhoneAccount(Context context, PhoneAccountHandle handle) {
    143     return getTelecomManager(context).getPhoneAccount(handle);
    144   }
    145 
    146   public static List<PhoneAccountHandle> getCallCapablePhoneAccounts(Context context) {
    147     if (hasReadPhoneStatePermission(context)) {
    148       return getTelecomManager(context).getCallCapablePhoneAccounts();
    149     }
    150     return new ArrayList<>();
    151   }
    152 
    153   /** Return a list of phone accounts that are subscription/SIM accounts. */
    154   public static List<PhoneAccountHandle> getSubscriptionPhoneAccounts(Context context) {
    155     List<PhoneAccountHandle> subscriptionAccountHandles = new ArrayList<>();
    156     final List<PhoneAccountHandle> accountHandles =
    157         TelecomUtil.getCallCapablePhoneAccounts(context);
    158     for (PhoneAccountHandle accountHandle : accountHandles) {
    159       PhoneAccount account = TelecomUtil.getPhoneAccount(context, accountHandle);
    160       if (account.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
    161         subscriptionAccountHandles.add(accountHandle);
    162       }
    163     }
    164     return subscriptionAccountHandles;
    165   }
    166 
    167   /** Compose {@link PhoneAccountHandle} object from component name and account id. */
    168   @Nullable
    169   public static PhoneAccountHandle composePhoneAccountHandle(
    170       @Nullable String componentString, @Nullable String accountId) {
    171     if (TextUtils.isEmpty(componentString) || TextUtils.isEmpty(accountId)) {
    172       return null;
    173     }
    174     final ComponentName componentName = ComponentName.unflattenFromString(componentString);
    175     if (componentName == null) {
    176       return null;
    177     }
    178     return new PhoneAccountHandle(componentName, accountId);
    179   }
    180 
    181   /**
    182    * @return the {@link SubscriptionInfo} of the SIM if {@code phoneAccountHandle} corresponds to a
    183    *     valid SIM. Absent otherwise.
    184    */
    185   public static Optional<SubscriptionInfo> getSubscriptionInfo(
    186       @NonNull Context context, @NonNull PhoneAccountHandle phoneAccountHandle) {
    187     if (TextUtils.isEmpty(phoneAccountHandle.getId())) {
    188       return Optional.absent();
    189     }
    190     if (!hasPermission(context, permission.READ_PHONE_STATE)) {
    191       return Optional.absent();
    192     }
    193     SubscriptionManager subscriptionManager = context.getSystemService(SubscriptionManager.class);
    194     List<SubscriptionInfo> subscriptionInfos = subscriptionManager.getActiveSubscriptionInfoList();
    195     if (subscriptionInfos == null) {
    196       return Optional.absent();
    197     }
    198     for (SubscriptionInfo info : subscriptionInfos) {
    199       if (phoneAccountHandle.getId().startsWith(info.getIccId())) {
    200         return Optional.of(info);
    201       }
    202     }
    203     return Optional.absent();
    204   }
    205 
    206   /**
    207    * Returns true if there is a dialer managed call in progress. Self managed calls starting from O
    208    * are not included.
    209    */
    210   public static boolean isInManagedCall(Context context) {
    211     return instance.isInManagedCall(context);
    212   }
    213 
    214   public static boolean isInCall(Context context) {
    215     return instance.isInCall(context);
    216   }
    217 
    218   /**
    219    * {@link TelecomManager#isVoiceMailNumber(PhoneAccountHandle, String)} takes about 10ms, which is
    220    * way too slow for regular purposes. This method will cache the result for the life time of the
    221    * process. The cache will not be invalidated, for example, if the voicemail number is changed by
    222    * setting up apps like Google Voicemail, the result will be wrong. These events are rare.
    223    */
    224   public static boolean isVoicemailNumber(
    225       Context context, PhoneAccountHandle accountHandle, String number) {
    226     if (TextUtils.isEmpty(number)) {
    227       return false;
    228     }
    229     Pair<PhoneAccountHandle, String> cacheKey = new Pair<>(accountHandle, number);
    230     if (isVoicemailNumberCache.containsKey(cacheKey)) {
    231       return isVoicemailNumberCache.get(cacheKey);
    232     }
    233     boolean result = false;
    234     if (hasReadPhoneStatePermission(context)) {
    235       result = getTelecomManager(context).isVoiceMailNumber(accountHandle, number);
    236     }
    237     isVoicemailNumberCache.put(cacheKey, result);
    238     return result;
    239   }
    240 
    241   @Nullable
    242   public static String getVoicemailNumber(Context context, PhoneAccountHandle accountHandle) {
    243     if (hasReadPhoneStatePermission(context)) {
    244       return getTelecomManager(context).getVoiceMailNumber(accountHandle);
    245     }
    246     return null;
    247   }
    248 
    249   /**
    250    * Tries to place a call using the {@link TelecomManager}.
    251    *
    252    * @param context context.
    253    * @param intent the call intent.
    254    * @return {@code true} if we successfully attempted to place the call, {@code false} if it failed
    255    *     due to a permission check.
    256    */
    257   public static boolean placeCall(Context context, Intent intent) {
    258     if (hasCallPhonePermission(context)) {
    259       getTelecomManager(context).placeCall(intent.getData(), intent.getExtras());
    260       return true;
    261     }
    262     return false;
    263   }
    264 
    265   public static Uri getCallLogUri(Context context) {
    266     return hasReadWriteVoicemailPermissions(context)
    267         ? Calls.CONTENT_URI_WITH_VOICEMAIL
    268         : Calls.CONTENT_URI;
    269   }
    270 
    271   public static boolean hasReadWriteVoicemailPermissions(Context context) {
    272     return isDefaultDialer(context)
    273         || (hasPermission(context, Manifest.permission.READ_VOICEMAIL)
    274             && hasPermission(context, Manifest.permission.WRITE_VOICEMAIL));
    275   }
    276 
    277   public static boolean hasModifyPhoneStatePermission(Context context) {
    278     return isDefaultDialer(context)
    279         || hasPermission(context, Manifest.permission.MODIFY_PHONE_STATE);
    280   }
    281 
    282   public static boolean hasReadPhoneStatePermission(Context context) {
    283     return isDefaultDialer(context) || hasPermission(context, Manifest.permission.READ_PHONE_STATE);
    284   }
    285 
    286   public static boolean hasCallPhonePermission(Context context) {
    287     return isDefaultDialer(context) || hasPermission(context, Manifest.permission.CALL_PHONE);
    288   }
    289 
    290   private static boolean hasPermission(Context context, String permission) {
    291     return instance.hasPermission(context, permission);
    292   }
    293 
    294   private static TelecomManager getTelecomManager(Context context) {
    295     return (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
    296   }
    297 
    298   public static boolean isDefaultDialer(Context context) {
    299     return instance.isDefaultDialer(context);
    300   }
    301 
    302   /** @return the other SIM based PhoneAccountHandle that is not {@code currentAccount} */
    303   @Nullable
    304   @RequiresPermission(permission.READ_PHONE_STATE)
    305   @SuppressWarnings("MissingPermission")
    306   public static PhoneAccountHandle getOtherAccount(
    307       @NonNull Context context, @Nullable PhoneAccountHandle currentAccount) {
    308     if (currentAccount == null) {
    309       return null;
    310     }
    311     TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
    312     for (PhoneAccountHandle phoneAccountHandle : telecomManager.getCallCapablePhoneAccounts()) {
    313       PhoneAccount phoneAccount = telecomManager.getPhoneAccount(phoneAccountHandle);
    314       if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)
    315           && !phoneAccountHandle.equals(currentAccount)) {
    316         return phoneAccountHandle;
    317       }
    318     }
    319     return null;
    320   }
    321 
    322   /** Contains an implementation for {@link TelecomUtil} methods */
    323   @VisibleForTesting()
    324   public static class TelecomUtilImpl {
    325 
    326     public boolean isInManagedCall(Context context) {
    327       if (hasReadPhoneStatePermission(context)) {
    328         // The TelecomManager#isInCall method returns true anytime the user is in a call.
    329         // Starting in O, the APIs include support for self-managed ConnectionServices so that other
    330         // apps like Duo can tell Telecom about its calls.  So, if the user is in a Duo call,
    331         // isInCall would return true.
    332         // Dialer uses this to determine whether to show the "return to call in progress" when
    333         // Dialer is launched.
    334         // Instead, Dialer should use TelecomManager#isInManagedCall, which only returns true if the
    335         // device is in a managed call which Dialer would know about.
    336         if (VERSION.SDK_INT >= VERSION_CODES.O) {
    337           return getTelecomManager(context).isInManagedCall();
    338         } else {
    339           return getTelecomManager(context).isInCall();
    340         }
    341       }
    342       return false;
    343     }
    344 
    345     public boolean isInCall(Context context) {
    346       return hasReadPhoneStatePermission(context) && getTelecomManager(context).isInCall();
    347     }
    348 
    349     public boolean hasPermission(Context context, String permission) {
    350       return ContextCompat.checkSelfPermission(context, permission)
    351           == PackageManager.PERMISSION_GRANTED;
    352     }
    353 
    354     public boolean isDefaultDialer(Context context) {
    355       final boolean result =
    356           TextUtils.equals(
    357               context.getPackageName(), getTelecomManager(context).getDefaultDialerPackage());
    358       if (result) {
    359         warningLogged = false;
    360       } else {
    361         if (!warningLogged) {
    362           // Log only once to prevent spam.
    363           LogUtil.w(TAG, "Dialer is not currently set to be default dialer");
    364           warningLogged = true;
    365         }
    366       }
    367       return result;
    368     }
    369   }
    370 }
    371