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
17package com.android.dialer.telecom;
18
19import android.Manifest;
20import android.content.Context;
21import android.content.Intent;
22import android.content.pm.PackageManager;
23import android.net.Uri;
24import android.provider.CallLog.Calls;
25import android.support.annotation.Nullable;
26import android.support.annotation.VisibleForTesting;
27import android.support.v4.content.ContextCompat;
28import android.telecom.PhoneAccount;
29import android.telecom.PhoneAccountHandle;
30import android.telecom.TelecomManager;
31import android.text.TextUtils;
32import android.util.Pair;
33import com.android.dialer.common.LogUtil;
34import java.util.ArrayList;
35import java.util.List;
36import java.util.Map;
37import java.util.concurrent.ConcurrentHashMap;
38
39/**
40 * Performs permission checks before calling into TelecomManager. Each method is self-explanatory -
41 * perform the required check and return the fallback default if the permission is missing,
42 * otherwise return the value from TelecomManager.
43 */
44public abstract class TelecomUtil {
45
46  private static final String TAG = "TelecomUtil";
47  private static boolean sWarningLogged = false;
48
49  private static TelecomUtilImpl instance = new TelecomUtilImpl();
50
51  /**
52   * Cache for {@link #isVoicemailNumber(Context, PhoneAccountHandle, String)}. Both
53   * PhoneAccountHandle and number are cached because multiple numbers might be mapped to true, and
54   * comparing with {@link #getVoicemailNumber(Context, PhoneAccountHandle)} will not suffice.
55   */
56  private static final Map<Pair<PhoneAccountHandle, String>, Boolean> isVoicemailNumberCache =
57      new ConcurrentHashMap<>();
58
59  @VisibleForTesting(otherwise = VisibleForTesting.NONE)
60  public static void setInstanceForTesting(TelecomUtilImpl instanceForTesting) {
61    instance = instanceForTesting;
62  }
63
64  public static void showInCallScreen(Context context, boolean showDialpad) {
65    if (hasReadPhoneStatePermission(context)) {
66      try {
67        getTelecomManager(context).showInCallScreen(showDialpad);
68      } catch (SecurityException e) {
69        // Just in case
70        LogUtil.w(TAG, "TelecomManager.showInCallScreen called without permission.");
71      }
72    }
73  }
74
75  public static void silenceRinger(Context context) {
76    if (hasModifyPhoneStatePermission(context)) {
77      try {
78        getTelecomManager(context).silenceRinger();
79      } catch (SecurityException e) {
80        // Just in case
81        LogUtil.w(TAG, "TelecomManager.silenceRinger called without permission.");
82      }
83    }
84  }
85
86  public static void cancelMissedCallsNotification(Context context) {
87    if (hasModifyPhoneStatePermission(context)) {
88      try {
89        getTelecomManager(context).cancelMissedCallsNotification();
90      } catch (SecurityException e) {
91        LogUtil.w(TAG, "TelecomManager.cancelMissedCalls called without permission.");
92      }
93    }
94  }
95
96  public static Uri getAdnUriForPhoneAccount(Context context, PhoneAccountHandle handle) {
97    if (hasModifyPhoneStatePermission(context)) {
98      try {
99        return getTelecomManager(context).getAdnUriForPhoneAccount(handle);
100      } catch (SecurityException e) {
101        LogUtil.w(TAG, "TelecomManager.getAdnUriForPhoneAccount called without permission.");
102      }
103    }
104    return null;
105  }
106
107  public static boolean handleMmi(
108      Context context, String dialString, @Nullable PhoneAccountHandle handle) {
109    if (hasModifyPhoneStatePermission(context)) {
110      try {
111        if (handle == null) {
112          return getTelecomManager(context).handleMmi(dialString);
113        } else {
114          return getTelecomManager(context).handleMmi(dialString, handle);
115        }
116      } catch (SecurityException e) {
117        LogUtil.w(TAG, "TelecomManager.handleMmi called without permission.");
118      }
119    }
120    return false;
121  }
122
123  @Nullable
124  public static PhoneAccountHandle getDefaultOutgoingPhoneAccount(
125      Context context, String uriScheme) {
126    if (hasReadPhoneStatePermission(context)) {
127      return getTelecomManager(context).getDefaultOutgoingPhoneAccount(uriScheme);
128    }
129    return null;
130  }
131
132  public static PhoneAccount getPhoneAccount(Context context, PhoneAccountHandle handle) {
133    return getTelecomManager(context).getPhoneAccount(handle);
134  }
135
136  public static List<PhoneAccountHandle> getCallCapablePhoneAccounts(Context context) {
137    if (hasReadPhoneStatePermission(context)) {
138      return getTelecomManager(context).getCallCapablePhoneAccounts();
139    }
140    return new ArrayList<>();
141  }
142
143  public static boolean isInCall(Context context) {
144    return instance.isInCall(context);
145  }
146
147  /**
148   * {@link TelecomManager#isVoiceMailNumber(PhoneAccountHandle, String)} takes about 10ms, which is
149   * way too slow for regular purposes. This method will cache the result for the life time of the
150   * process. The cache will not be invalidated, for example, if the voicemail number is changed by
151   * setting up apps like Google Voicemail, the result will be wrong. These events are rare.
152   */
153  public static boolean isVoicemailNumber(
154      Context context, PhoneAccountHandle accountHandle, String number) {
155    if (TextUtils.isEmpty(number)) {
156      return false;
157    }
158    Pair<PhoneAccountHandle, String> cacheKey = new Pair<>(accountHandle, number);
159    if (isVoicemailNumberCache.containsKey(cacheKey)) {
160      return isVoicemailNumberCache.get(cacheKey);
161    }
162    boolean result = false;
163    if (hasReadPhoneStatePermission(context)) {
164      result = getTelecomManager(context).isVoiceMailNumber(accountHandle, number);
165    }
166    isVoicemailNumberCache.put(cacheKey, result);
167    return result;
168  }
169
170  @Nullable
171  public static String getVoicemailNumber(Context context, PhoneAccountHandle accountHandle) {
172    if (hasReadPhoneStatePermission(context)) {
173      return getTelecomManager(context).getVoiceMailNumber(accountHandle);
174    }
175    return null;
176  }
177
178  /**
179   * Tries to place a call using the {@link TelecomManager}.
180   *
181   * @param context context.
182   * @param intent the call intent.
183   * @return {@code true} if we successfully attempted to place the call, {@code false} if it failed
184   *     due to a permission check.
185   */
186  public static boolean placeCall(Context context, Intent intent) {
187    if (hasCallPhonePermission(context)) {
188      getTelecomManager(context).placeCall(intent.getData(), intent.getExtras());
189      return true;
190    }
191    return false;
192  }
193
194  public static Uri getCallLogUri(Context context) {
195    return hasReadWriteVoicemailPermissions(context)
196        ? Calls.CONTENT_URI_WITH_VOICEMAIL
197        : Calls.CONTENT_URI;
198  }
199
200  public static boolean hasReadWriteVoicemailPermissions(Context context) {
201    return isDefaultDialer(context)
202        || (hasPermission(context, Manifest.permission.READ_VOICEMAIL)
203            && hasPermission(context, Manifest.permission.WRITE_VOICEMAIL));
204  }
205
206  public static boolean hasModifyPhoneStatePermission(Context context) {
207    return isDefaultDialer(context)
208        || hasPermission(context, Manifest.permission.MODIFY_PHONE_STATE);
209  }
210
211  public static boolean hasReadPhoneStatePermission(Context context) {
212    return isDefaultDialer(context) || hasPermission(context, Manifest.permission.READ_PHONE_STATE);
213  }
214
215  public static boolean hasCallPhonePermission(Context context) {
216    return isDefaultDialer(context) || hasPermission(context, Manifest.permission.CALL_PHONE);
217  }
218
219  private static boolean hasPermission(Context context, String permission) {
220    return instance.hasPermission(context, permission);
221  }
222
223  private static TelecomManager getTelecomManager(Context context) {
224    return (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
225  }
226
227  public static boolean isDefaultDialer(Context context) {
228    return instance.isDefaultDialer(context);
229  }
230
231  /** Contains an implementation for {@link TelecomUtil} methods */
232  @VisibleForTesting()
233  public static class TelecomUtilImpl {
234
235    public boolean isInCall(Context context) {
236      if (hasReadPhoneStatePermission(context)) {
237        return getTelecomManager(context).isInCall();
238      }
239      return false;
240    }
241
242    public boolean hasPermission(Context context, String permission) {
243      return ContextCompat.checkSelfPermission(context, permission)
244          == PackageManager.PERMISSION_GRANTED;
245    }
246
247    public boolean isDefaultDialer(Context context) {
248      final boolean result =
249          TextUtils.equals(
250              context.getPackageName(), getTelecomManager(context).getDefaultDialerPackage());
251      if (result) {
252        sWarningLogged = false;
253      } else {
254        if (!sWarningLogged) {
255          // Log only once to prevent spam.
256          LogUtil.w(TAG, "Dialer is not currently set to be default dialer");
257          sWarningLogged = true;
258        }
259      }
260      return result;
261    }
262  }
263}
264