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