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