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", "Guava"}) 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) setInstanceForTesting(TelecomUtilImpl instanceForTesting)70 public static void setInstanceForTesting(TelecomUtilImpl instanceForTesting) { 71 instance = instanceForTesting; 72 } 73 showInCallScreen(Context context, boolean showDialpad)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 silenceRinger(Context context)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 cancelMissedCallsNotification(Context context)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 getAdnUriForPhoneAccount(Context context, PhoneAccountHandle handle)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 handleMmi( Context context, String dialString, @Nullable PhoneAccountHandle handle)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 getDefaultOutgoingPhoneAccount( Context context, String uriScheme)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 getPhoneAccount(Context context, PhoneAccountHandle handle)142 public static PhoneAccount getPhoneAccount(Context context, PhoneAccountHandle handle) { 143 return getTelecomManager(context).getPhoneAccount(handle); 144 } 145 getCallCapablePhoneAccounts(Context context)146 public static List<PhoneAccountHandle> getCallCapablePhoneAccounts(Context context) { 147 if (hasReadPhoneStatePermission(context)) { 148 return Optional.fromNullable(getTelecomManager(context).getCallCapablePhoneAccounts()) 149 .or(new ArrayList<>()); 150 } 151 return new ArrayList<>(); 152 } 153 154 /** Return a list of phone accounts that are subscription/SIM accounts. */ getSubscriptionPhoneAccounts(Context context)155 public static List<PhoneAccountHandle> getSubscriptionPhoneAccounts(Context context) { 156 List<PhoneAccountHandle> subscriptionAccountHandles = new ArrayList<>(); 157 final List<PhoneAccountHandle> accountHandles = 158 TelecomUtil.getCallCapablePhoneAccounts(context); 159 for (PhoneAccountHandle accountHandle : accountHandles) { 160 PhoneAccount account = TelecomUtil.getPhoneAccount(context, accountHandle); 161 if (account.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) { 162 subscriptionAccountHandles.add(accountHandle); 163 } 164 } 165 return subscriptionAccountHandles; 166 } 167 168 /** Compose {@link PhoneAccountHandle} object from component name and account id. */ 169 @Nullable composePhoneAccountHandle( @ullable String componentString, @Nullable String accountId)170 public static PhoneAccountHandle composePhoneAccountHandle( 171 @Nullable String componentString, @Nullable String accountId) { 172 if (TextUtils.isEmpty(componentString) || TextUtils.isEmpty(accountId)) { 173 return null; 174 } 175 final ComponentName componentName = ComponentName.unflattenFromString(componentString); 176 if (componentName == null) { 177 return null; 178 } 179 return new PhoneAccountHandle(componentName, accountId); 180 } 181 182 /** 183 * @return the {@link SubscriptionInfo} of the SIM if {@code phoneAccountHandle} corresponds to a 184 * valid SIM. Absent otherwise. 185 */ getSubscriptionInfo( @onNull Context context, @NonNull PhoneAccountHandle phoneAccountHandle)186 public static Optional<SubscriptionInfo> getSubscriptionInfo( 187 @NonNull Context context, @NonNull PhoneAccountHandle phoneAccountHandle) { 188 if (TextUtils.isEmpty(phoneAccountHandle.getId())) { 189 return Optional.absent(); 190 } 191 if (!hasPermission(context, permission.READ_PHONE_STATE)) { 192 return Optional.absent(); 193 } 194 SubscriptionManager subscriptionManager = context.getSystemService(SubscriptionManager.class); 195 List<SubscriptionInfo> subscriptionInfos = subscriptionManager.getActiveSubscriptionInfoList(); 196 if (subscriptionInfos == null) { 197 return Optional.absent(); 198 } 199 for (SubscriptionInfo info : subscriptionInfos) { 200 if (phoneAccountHandle.getId().startsWith(info.getIccId())) { 201 return Optional.of(info); 202 } 203 } 204 return Optional.absent(); 205 } 206 207 /** 208 * Returns true if there is a dialer managed call in progress. Self managed calls starting from O 209 * are not included. 210 */ isInManagedCall(Context context)211 public static boolean isInManagedCall(Context context) { 212 return instance.isInManagedCall(context); 213 } 214 isInCall(Context context)215 public static boolean isInCall(Context context) { 216 return instance.isInCall(context); 217 } 218 219 /** 220 * {@link TelecomManager#isVoiceMailNumber(PhoneAccountHandle, String)} takes about 10ms, which is 221 * way too slow for regular purposes. This method will cache the result for the life time of the 222 * process. The cache will not be invalidated, for example, if the voicemail number is changed by 223 * setting up apps like Google Voicemail, the result will be wrong. These events are rare. 224 */ isVoicemailNumber( Context context, PhoneAccountHandle accountHandle, String number)225 public static boolean isVoicemailNumber( 226 Context context, PhoneAccountHandle accountHandle, String number) { 227 if (TextUtils.isEmpty(number)) { 228 return false; 229 } 230 Pair<PhoneAccountHandle, String> cacheKey = new Pair<>(accountHandle, number); 231 if (isVoicemailNumberCache.containsKey(cacheKey)) { 232 return isVoicemailNumberCache.get(cacheKey); 233 } 234 boolean result = false; 235 if (hasReadPhoneStatePermission(context)) { 236 result = getTelecomManager(context).isVoiceMailNumber(accountHandle, number); 237 } 238 isVoicemailNumberCache.put(cacheKey, result); 239 return result; 240 } 241 242 @Nullable getVoicemailNumber(Context context, PhoneAccountHandle accountHandle)243 public static String getVoicemailNumber(Context context, PhoneAccountHandle accountHandle) { 244 if (hasReadPhoneStatePermission(context)) { 245 return getTelecomManager(context).getVoiceMailNumber(accountHandle); 246 } 247 return null; 248 } 249 250 /** 251 * Tries to place a call using the {@link TelecomManager}. 252 * 253 * @param context context. 254 * @param intent the call intent. 255 * @return {@code true} if we successfully attempted to place the call, {@code false} if it failed 256 * due to a permission check. 257 */ placeCall(Context context, Intent intent)258 public static boolean placeCall(Context context, Intent intent) { 259 if (hasCallPhonePermission(context)) { 260 getTelecomManager(context).placeCall(intent.getData(), intent.getExtras()); 261 return true; 262 } 263 return false; 264 } 265 getCallLogUri(Context context)266 public static Uri getCallLogUri(Context context) { 267 return hasReadWriteVoicemailPermissions(context) 268 ? Calls.CONTENT_URI_WITH_VOICEMAIL 269 : Calls.CONTENT_URI; 270 } 271 hasReadWriteVoicemailPermissions(Context context)272 public static boolean hasReadWriteVoicemailPermissions(Context context) { 273 return isDefaultDialer(context) 274 || (hasPermission(context, Manifest.permission.READ_VOICEMAIL) 275 && hasPermission(context, Manifest.permission.WRITE_VOICEMAIL)); 276 } 277 278 /** @deprecated use {@link com.android.dialer.util.PermissionsUtil} */ 279 @Deprecated hasModifyPhoneStatePermission(Context context)280 public static boolean hasModifyPhoneStatePermission(Context context) { 281 return isDefaultDialer(context) 282 || hasPermission(context, Manifest.permission.MODIFY_PHONE_STATE); 283 } 284 285 /** @deprecated use {@link com.android.dialer.util.PermissionsUtil} */ 286 @Deprecated hasReadPhoneStatePermission(Context context)287 public static boolean hasReadPhoneStatePermission(Context context) { 288 return isDefaultDialer(context) || hasPermission(context, Manifest.permission.READ_PHONE_STATE); 289 } 290 291 /** @deprecated use {@link com.android.dialer.util.PermissionsUtil} */ 292 @Deprecated hasCallPhonePermission(Context context)293 public static boolean hasCallPhonePermission(Context context) { 294 return isDefaultDialer(context) || hasPermission(context, Manifest.permission.CALL_PHONE); 295 } 296 hasPermission(Context context, String permission)297 private static boolean hasPermission(Context context, String permission) { 298 return instance.hasPermission(context, permission); 299 } 300 getTelecomManager(Context context)301 private static TelecomManager getTelecomManager(Context context) { 302 return (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); 303 } 304 isDefaultDialer(Context context)305 public static boolean isDefaultDialer(Context context) { 306 return instance.isDefaultDialer(context); 307 } 308 309 /** @return the other SIM based PhoneAccountHandle that is not {@code currentAccount} */ 310 @Nullable 311 @RequiresPermission(permission.READ_PHONE_STATE) 312 @SuppressWarnings("MissingPermission") getOtherAccount( @onNull Context context, @Nullable PhoneAccountHandle currentAccount)313 public static PhoneAccountHandle getOtherAccount( 314 @NonNull Context context, @Nullable PhoneAccountHandle currentAccount) { 315 if (currentAccount == null) { 316 return null; 317 } 318 TelecomManager telecomManager = context.getSystemService(TelecomManager.class); 319 for (PhoneAccountHandle phoneAccountHandle : telecomManager.getCallCapablePhoneAccounts()) { 320 PhoneAccount phoneAccount = telecomManager.getPhoneAccount(phoneAccountHandle); 321 if (phoneAccount == null) { 322 continue; 323 } 324 if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION) 325 && !phoneAccountHandle.equals(currentAccount)) { 326 return phoneAccountHandle; 327 } 328 } 329 return null; 330 } 331 332 /** Contains an implementation for {@link TelecomUtil} methods */ 333 @VisibleForTesting() 334 public static class TelecomUtilImpl { 335 isInManagedCall(Context context)336 public boolean isInManagedCall(Context context) { 337 if (hasReadPhoneStatePermission(context)) { 338 // The TelecomManager#isInCall method returns true anytime the user is in a call. 339 // Starting in O, the APIs include support for self-managed ConnectionServices so that other 340 // apps like Duo can tell Telecom about its calls. So, if the user is in a Duo call, 341 // isInCall would return true. 342 // Dialer uses this to determine whether to show the "return to call in progress" when 343 // Dialer is launched. 344 // Instead, Dialer should use TelecomManager#isInManagedCall, which only returns true if the 345 // device is in a managed call which Dialer would know about. 346 if (VERSION.SDK_INT >= VERSION_CODES.O) { 347 return getTelecomManager(context).isInManagedCall(); 348 } else { 349 return getTelecomManager(context).isInCall(); 350 } 351 } 352 return false; 353 } 354 isInCall(Context context)355 public boolean isInCall(Context context) { 356 return hasReadPhoneStatePermission(context) && getTelecomManager(context).isInCall(); 357 } 358 hasPermission(Context context, String permission)359 public boolean hasPermission(Context context, String permission) { 360 return ContextCompat.checkSelfPermission(context, permission) 361 == PackageManager.PERMISSION_GRANTED; 362 } 363 isDefaultDialer(Context context)364 public boolean isDefaultDialer(Context context) { 365 final boolean result = 366 TextUtils.equals( 367 context.getPackageName(), getTelecomManager(context).getDefaultDialerPackage()); 368 if (result) { 369 warningLogged = false; 370 } else { 371 if (!warningLogged) { 372 // Log only once to prevent spam. 373 LogUtil.w(TAG, "Dialer is not currently set to be default dialer"); 374 warningLogged = true; 375 } 376 } 377 return result; 378 } 379 } 380 } 381