• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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)
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 getTelecomManager(context).getCallCapablePhoneAccounts();
149     }
150     return new ArrayList<>();
151   }
152 
153   /** Return a list of phone accounts that are subscription/SIM accounts. */
getSubscriptionPhoneAccounts(Context context)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
composePhoneAccountHandle( @ullable String componentString, @Nullable String accountId)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    */
getSubscriptionInfo( @onNull Context context, @NonNull PhoneAccountHandle phoneAccountHandle)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    */
isInManagedCall(Context context)210   public static boolean isInManagedCall(Context context) {
211     return instance.isInManagedCall(context);
212   }
213 
isInCall(Context context)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    */
isVoicemailNumber( Context context, PhoneAccountHandle accountHandle, String number)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
getVoicemailNumber(Context context, PhoneAccountHandle accountHandle)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    */
placeCall(Context context, Intent intent)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 
getCallLogUri(Context context)265   public static Uri getCallLogUri(Context context) {
266     return hasReadWriteVoicemailPermissions(context)
267         ? Calls.CONTENT_URI_WITH_VOICEMAIL
268         : Calls.CONTENT_URI;
269   }
270 
hasReadWriteVoicemailPermissions(Context context)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 
hasModifyPhoneStatePermission(Context context)277   public static boolean hasModifyPhoneStatePermission(Context context) {
278     return isDefaultDialer(context)
279         || hasPermission(context, Manifest.permission.MODIFY_PHONE_STATE);
280   }
281 
hasReadPhoneStatePermission(Context context)282   public static boolean hasReadPhoneStatePermission(Context context) {
283     return isDefaultDialer(context) || hasPermission(context, Manifest.permission.READ_PHONE_STATE);
284   }
285 
hasCallPhonePermission(Context context)286   public static boolean hasCallPhonePermission(Context context) {
287     return isDefaultDialer(context) || hasPermission(context, Manifest.permission.CALL_PHONE);
288   }
289 
hasPermission(Context context, String permission)290   private static boolean hasPermission(Context context, String permission) {
291     return instance.hasPermission(context, permission);
292   }
293 
getTelecomManager(Context context)294   private static TelecomManager getTelecomManager(Context context) {
295     return (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
296   }
297 
isDefaultDialer(Context context)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")
getOtherAccount( @onNull Context context, @Nullable PhoneAccountHandle currentAccount)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 
isInManagedCall(Context context)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 
isInCall(Context context)345     public boolean isInCall(Context context) {
346       return hasReadPhoneStatePermission(context) && getTelecomManager(context).isInCall();
347     }
348 
hasPermission(Context context, String permission)349     public boolean hasPermission(Context context, String permission) {
350       return ContextCompat.checkSelfPermission(context, permission)
351           == PackageManager.PERMISSION_GRANTED;
352     }
353 
isDefaultDialer(Context context)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