• 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.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