summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/telecom/TelecomUtil.java
blob: c088146888e2798cb4b24d902e118b018e8491c8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dialer.telecom;

import android.Manifest;
import android.Manifest.permission;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.provider.CallLog.Calls;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresPermission;
import android.support.annotation.VisibleForTesting;
import android.support.v4.content.ContextCompat;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.text.TextUtils;
import android.util.Pair;
import com.android.dialer.common.LogUtil;
import com.google.common.base.Optional;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Performs permission checks before calling into TelecomManager. Each method is self-explanatory -
 * perform the required check and return the fallback default if the permission is missing,
 * otherwise return the value from TelecomManager.
 */
@SuppressWarnings({"MissingPermission", "Guava"})
public abstract class TelecomUtil {

  private static final String TAG = "TelecomUtil";
  private static boolean warningLogged = false;

  private static TelecomUtilImpl instance = new TelecomUtilImpl();

  /**
   * Cache for {@link #isVoicemailNumber(Context, PhoneAccountHandle, String)}. Both
   * PhoneAccountHandle and number are cached because multiple numbers might be mapped to true, and
   * comparing with {@link #getVoicemailNumber(Context, PhoneAccountHandle)} will not suffice.
   */
  private static final Map<Pair<PhoneAccountHandle, String>, Boolean> isVoicemailNumberCache =
      new ConcurrentHashMap<>();

  @VisibleForTesting(otherwise = VisibleForTesting.NONE)
  public static void setInstanceForTesting(TelecomUtilImpl instanceForTesting) {
    instance = instanceForTesting;
  }

  public static void showInCallScreen(Context context, boolean showDialpad) {
    if (hasReadPhoneStatePermission(context)) {
      try {
        getTelecomManager(context).showInCallScreen(showDialpad);
      } catch (SecurityException e) {
        // Just in case
        LogUtil.w(TAG, "TelecomManager.showInCallScreen called without permission.");
      }
    }
  }

  public static void silenceRinger(Context context) {
    if (hasModifyPhoneStatePermission(context)) {
      try {
        getTelecomManager(context).silenceRinger();
      } catch (SecurityException e) {
        // Just in case
        LogUtil.w(TAG, "TelecomManager.silenceRinger called without permission.");
      }
    }
  }

  public static void cancelMissedCallsNotification(Context context) {
    if (hasModifyPhoneStatePermission(context)) {
      try {
        getTelecomManager(context).cancelMissedCallsNotification();
      } catch (SecurityException e) {
        LogUtil.w(TAG, "TelecomManager.cancelMissedCalls called without permission.");
      }
    }
  }

  public static Uri getAdnUriForPhoneAccount(Context context, PhoneAccountHandle handle) {
    if (hasModifyPhoneStatePermission(context)) {
      try {
        return getTelecomManager(context).getAdnUriForPhoneAccount(handle);
      } catch (SecurityException e) {
        LogUtil.w(TAG, "TelecomManager.getAdnUriForPhoneAccount called without permission.");
      }
    }
    return null;
  }

  public static boolean handleMmi(
      Context context, String dialString, @Nullable PhoneAccountHandle handle) {
    if (hasModifyPhoneStatePermission(context)) {
      try {
        if (handle == null) {
          return getTelecomManager(context).handleMmi(dialString);
        } else {
          return getTelecomManager(context).handleMmi(dialString, handle);
        }
      } catch (SecurityException e) {
        LogUtil.w(TAG, "TelecomManager.handleMmi called without permission.");
      }
    }
    return false;
  }

  @Nullable
  public static PhoneAccountHandle getDefaultOutgoingPhoneAccount(
      Context context, String uriScheme) {
    if (hasReadPhoneStatePermission(context)) {
      return getTelecomManager(context).getDefaultOutgoingPhoneAccount(uriScheme);
    }
    return null;
  }

  public static PhoneAccount getPhoneAccount(Context context, PhoneAccountHandle handle) {
    return getTelecomManager(context).getPhoneAccount(handle);
  }

  public static List<PhoneAccountHandle> getCallCapablePhoneAccounts(Context context) {
    if (hasReadPhoneStatePermission(context)) {
      return Optional.fromNullable(getTelecomManager(context).getCallCapablePhoneAccounts())
          .or(new ArrayList<>());
    }
    return new ArrayList<>();
  }

  /** Return a list of phone accounts that are subscription/SIM accounts. */
  public static List<PhoneAccountHandle> getSubscriptionPhoneAccounts(Context context) {
    List<PhoneAccountHandle> subscriptionAccountHandles = new ArrayList<>();
    final List<PhoneAccountHandle> accountHandles =
        TelecomUtil.getCallCapablePhoneAccounts(context);
    for (PhoneAccountHandle accountHandle : accountHandles) {
      PhoneAccount account = TelecomUtil.getPhoneAccount(context, accountHandle);
      if (account.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
        subscriptionAccountHandles.add(accountHandle);
      }
    }
    return subscriptionAccountHandles;
  }

  /** Compose {@link PhoneAccountHandle} object from component name and account id. */
  @Nullable
  public static PhoneAccountHandle composePhoneAccountHandle(
      @Nullable String componentString, @Nullable String accountId) {
    if (TextUtils.isEmpty(componentString) || TextUtils.isEmpty(accountId)) {
      return null;
    }
    final ComponentName componentName = ComponentName.unflattenFromString(componentString);
    if (componentName == null) {
      return null;
    }
    return new PhoneAccountHandle(componentName, accountId);
  }

  /**
   * @return the {@link SubscriptionInfo} of the SIM if {@code phoneAccountHandle} corresponds to a
   *     valid SIM. Absent otherwise.
   */
  public static Optional<SubscriptionInfo> getSubscriptionInfo(
      @NonNull Context context, @NonNull PhoneAccountHandle phoneAccountHandle) {
    if (TextUtils.isEmpty(phoneAccountHandle.getId())) {
      return Optional.absent();
    }
    if (!hasPermission(context, permission.READ_PHONE_STATE)) {
      return Optional.absent();
    }
    SubscriptionManager subscriptionManager = context.getSystemService(SubscriptionManager.class);
    List<SubscriptionInfo> subscriptionInfos = subscriptionManager.getActiveSubscriptionInfoList();
    if (subscriptionInfos == null) {
      return Optional.absent();
    }
    for (SubscriptionInfo info : subscriptionInfos) {
      if (phoneAccountHandle.getId().startsWith(info.getIccId())) {
        return Optional.of(info);
      }
    }
    return Optional.absent();
  }

  /**
   * Returns true if there is a dialer managed call in progress. Self managed calls starting from O
   * are not included.
   */
  public static boolean isInManagedCall(Context context) {
    return instance.isInManagedCall(context);
  }

  public static boolean isInCall(Context context) {
    return instance.isInCall(context);
  }

  /**
   * {@link TelecomManager#isVoiceMailNumber(PhoneAccountHandle, String)} takes about 10ms, which is
   * way too slow for regular purposes. This method will cache the result for the life time of the
   * process. The cache will not be invalidated, for example, if the voicemail number is changed by
   * setting up apps like Google Voicemail, the result will be wrong. These events are rare.
   */
  public static boolean isVoicemailNumber(
      Context context, PhoneAccountHandle accountHandle, String number) {
    if (TextUtils.isEmpty(number)) {
      return false;
    }
    Pair<PhoneAccountHandle, String> cacheKey = new Pair<>(accountHandle, number);
    if (isVoicemailNumberCache.containsKey(cacheKey)) {
      return isVoicemailNumberCache.get(cacheKey);
    }
    boolean result = false;
    if (hasReadPhoneStatePermission(context)) {
      result = getTelecomManager(context).isVoiceMailNumber(accountHandle, number);
    }
    isVoicemailNumberCache.put(cacheKey, result);
    return result;
  }

  @Nullable
  public static String getVoicemailNumber(Context context, PhoneAccountHandle accountHandle) {
    if (hasReadPhoneStatePermission(context)) {
      return getTelecomManager(context).getVoiceMailNumber(accountHandle);
    }
    return null;
  }

  /**
   * Tries to place a call using the {@link TelecomManager}.
   *
   * @param context context.
   * @param intent the call intent.
   * @return {@code true} if we successfully attempted to place the call, {@code false} if it failed
   *     due to a permission check.
   */
  public static boolean placeCall(Context context, Intent intent) {
    if (hasCallPhonePermission(context)) {
      getTelecomManager(context).placeCall(intent.getData(), intent.getExtras());
      return true;
    }
    return false;
  }

  public static Uri getCallLogUri(Context context) {
    return hasReadWriteVoicemailPermissions(context)
        ? Calls.CONTENT_URI_WITH_VOICEMAIL
        : Calls.CONTENT_URI;
  }

  public static boolean hasReadWriteVoicemailPermissions(Context context) {
    return isDefaultDialer(context)
        || (hasPermission(context, Manifest.permission.READ_VOICEMAIL)
            && hasPermission(context, Manifest.permission.WRITE_VOICEMAIL));
  }

  /** @deprecated use {@link com.android.dialer.util.PermissionsUtil} */
  @Deprecated
  public static boolean hasModifyPhoneStatePermission(Context context) {
    return isDefaultDialer(context)
        || hasPermission(context, Manifest.permission.MODIFY_PHONE_STATE);
  }

  /** @deprecated use {@link com.android.dialer.util.PermissionsUtil} */
  @Deprecated
  public static boolean hasReadPhoneStatePermission(Context context) {
    return isDefaultDialer(context) || hasPermission(context, Manifest.permission.READ_PHONE_STATE);
  }

  /** @deprecated use {@link com.android.dialer.util.PermissionsUtil} */
  @Deprecated
  public static boolean hasCallPhonePermission(Context context) {
    return isDefaultDialer(context) || hasPermission(context, Manifest.permission.CALL_PHONE);
  }

  private static boolean hasPermission(Context context, String permission) {
    return instance.hasPermission(context, permission);
  }

  private static TelecomManager getTelecomManager(Context context) {
    return (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
  }

  public static boolean isDefaultDialer(Context context) {
    return instance.isDefaultDialer(context);
  }

  /** @return the other SIM based PhoneAccountHandle that is not {@code currentAccount} */
  @Nullable
  @RequiresPermission(permission.READ_PHONE_STATE)
  @SuppressWarnings("MissingPermission")
  public static PhoneAccountHandle getOtherAccount(
      @NonNull Context context, @Nullable PhoneAccountHandle currentAccount) {
    if (currentAccount == null) {
      return null;
    }
    TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
    for (PhoneAccountHandle phoneAccountHandle : telecomManager.getCallCapablePhoneAccounts()) {
      PhoneAccount phoneAccount = telecomManager.getPhoneAccount(phoneAccountHandle);
      if (phoneAccount == null) {
        continue;
      }
      if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)
          && !phoneAccountHandle.equals(currentAccount)) {
        return phoneAccountHandle;
      }
    }
    return null;
  }

  /** Contains an implementation for {@link TelecomUtil} methods */
  @VisibleForTesting()
  public static class TelecomUtilImpl {

    public boolean isInManagedCall(Context context) {
      if (hasReadPhoneStatePermission(context)) {
        // The TelecomManager#isInCall method returns true anytime the user is in a call.
        // Starting in O, the APIs include support for self-managed ConnectionServices so that other
        // apps like Duo can tell Telecom about its calls.  So, if the user is in a Duo call,
        // isInCall would return true.
        // Dialer uses this to determine whether to show the "return to call in progress" when
        // Dialer is launched.
        // Instead, Dialer should use TelecomManager#isInManagedCall, which only returns true if the
        // device is in a managed call which Dialer would know about.
        if (VERSION.SDK_INT >= VERSION_CODES.O) {
          return getTelecomManager(context).isInManagedCall();
        } else {
          return getTelecomManager(context).isInCall();
        }
      }
      return false;
    }

    public boolean isInCall(Context context) {
      return hasReadPhoneStatePermission(context) && getTelecomManager(context).isInCall();
    }

    public boolean hasPermission(Context context, String permission) {
      return ContextCompat.checkSelfPermission(context, permission)
          == PackageManager.PERMISSION_GRANTED;
    }

    public boolean isDefaultDialer(Context context) {
      final boolean result =
          TextUtils.equals(
              context.getPackageName(), getTelecomManager(context).getDefaultDialerPackage());
      if (result) {
        warningLogged = false;
      } else {
        if (!warningLogged) {
          // Log only once to prevent spam.
          LogUtil.w(TAG, "Dialer is not currently set to be default dialer");
          warningLogged = true;
        }
      }
      return result;
    }
  }
}