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