• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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.precall.impl;
18 
19 import android.app.Activity;
20 import android.content.ContentResolver;
21 import android.content.ContentUris;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.pm.PackageManager;
26 import android.content.pm.ResolveInfo;
27 import android.database.Cursor;
28 import android.net.Uri;
29 import android.os.Build.VERSION;
30 import android.os.Build.VERSION_CODES;
31 import android.provider.ContactsContract.Contacts;
32 import android.provider.ContactsContract.Data;
33 import android.provider.ContactsContract.PhoneLookup;
34 import android.provider.ContactsContract.QuickContact;
35 import android.provider.ContactsContract.RawContacts;
36 import android.support.annotation.MainThread;
37 import android.support.annotation.NonNull;
38 import android.support.annotation.Nullable;
39 import android.support.annotation.VisibleForTesting;
40 import android.support.annotation.WorkerThread;
41 import android.telecom.PhoneAccount;
42 import android.telecom.PhoneAccountHandle;
43 import android.telecom.TelecomManager;
44 import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment;
45 import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment.SelectPhoneAccountListener;
46 import com.android.dialer.callintent.CallIntentBuilder;
47 import com.android.dialer.common.Assert;
48 import com.android.dialer.common.LogUtil;
49 import com.android.dialer.common.concurrent.DialerExecutor.Worker;
50 import com.android.dialer.common.concurrent.DialerExecutorComponent;
51 import com.android.dialer.configprovider.ConfigProviderBindings;
52 import com.android.dialer.logging.DialerImpression;
53 import com.android.dialer.logging.DialerImpression.Type;
54 import com.android.dialer.logging.Logger;
55 import com.android.dialer.precall.PreCallAction;
56 import com.android.dialer.precall.PreCallCoordinator;
57 import com.android.dialer.precall.PreCallCoordinator.PendingAction;
58 import com.android.dialer.preferredsim.PreferredAccountUtil;
59 import com.android.dialer.preferredsim.PreferredSimFallbackContract;
60 import com.android.dialer.preferredsim.PreferredSimFallbackContract.PreferredSim;
61 import com.android.dialer.preferredsim.suggestion.SimSuggestionComponent;
62 import com.android.dialer.preferredsim.suggestion.SuggestionProvider.Suggestion;
63 import com.android.dialer.telecom.TelecomUtil;
64 import com.android.dialer.util.PermissionsUtil;
65 import com.google.common.base.Optional;
66 import com.google.common.collect.ImmutableSet;
67 import java.util.ArrayList;
68 import java.util.List;
69 
70 /** PreCallAction to select which phone account to call with. Ignored if there's only one account */
71 @SuppressWarnings("MissingPermission")
72 public class CallingAccountSelector implements PreCallAction {
73 
74   @VisibleForTesting static final String TAG_CALLING_ACCOUNT_SELECTOR = "CallingAccountSelector";
75 
76   @VisibleForTesting
77   static final String METADATA_SUPPORTS_PREFERRED_SIM = "supports_per_number_preferred_account";
78 
79   private SelectPhoneAccountDialogFragment selectPhoneAccountDialogFragment;
80 
81   private boolean isDiscarding;
82 
83   @Override
requiresUi(Context context, CallIntentBuilder builder)84   public boolean requiresUi(Context context, CallIntentBuilder builder) {
85     if (!ConfigProviderBindings.get(context)
86         .getBoolean("precall_calling_account_selector_enabled", true)) {
87       return false;
88     }
89 
90     if (builder.getPhoneAccountHandle() != null) {
91       return false;
92     }
93     TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
94     List<PhoneAccountHandle> accounts = telecomManager.getCallCapablePhoneAccounts();
95     if (accounts.size() <= 1) {
96       return false;
97     }
98 
99     if (TelecomUtil.isInManagedCall(context)) {
100       // Most devices are DSDS (dual SIM dual standby) which only one SIM can have active calls at
101       // a time. Telecom will ignore the phone account handle and use the current active SIM, thus
102       // there's no point of selecting SIMs
103       // TODO(a bug): let the user know selections are not available and preferred SIM is not
104       // used
105       // TODO(twyen): support other dual SIM modes when the API is exposed.
106       return false;
107     }
108 
109     return true;
110   }
111 
112   @Override
runWithoutUi(Context context, CallIntentBuilder builder)113   public void runWithoutUi(Context context, CallIntentBuilder builder) {
114     // do nothing.
115   }
116 
117   @Override
runWithUi(PreCallCoordinator coordinator)118   public void runWithUi(PreCallCoordinator coordinator) {
119     CallIntentBuilder builder = coordinator.getBuilder();
120     if (!requiresUi(coordinator.getActivity(), builder)) {
121       return;
122     }
123     switch (builder.getUri().getScheme()) {
124       case PhoneAccount.SCHEME_VOICEMAIL:
125         showDialog(coordinator, coordinator.startPendingAction(), null, null, null);
126         Logger.get(coordinator.getActivity()).logImpression(Type.DUAL_SIM_SELECTION_VOICEMAIL);
127         break;
128       case PhoneAccount.SCHEME_TEL:
129         processPreferredAccount(coordinator);
130         break;
131       default:
132         // might be PhoneAccount.SCHEME_SIP
133         LogUtil.e(
134             "CallingAccountSelector.run",
135             "unable to process scheme " + builder.getUri().getScheme());
136         break;
137     }
138   }
139 
140   /** Initiates a background worker to find if there's any preferred account. */
141   @MainThread
processPreferredAccount(PreCallCoordinator coordinator)142   private void processPreferredAccount(PreCallCoordinator coordinator) {
143     Assert.isMainThread();
144     CallIntentBuilder builder = coordinator.getBuilder();
145     Activity activity = coordinator.getActivity();
146     String phoneNumber = builder.getUri().getSchemeSpecificPart();
147     PendingAction pendingAction = coordinator.startPendingAction();
148     DialerExecutorComponent.get(coordinator.getActivity())
149         .dialerExecutorFactory()
150         .createNonUiTaskBuilder(new PreferredAccountWorker(phoneNumber))
151         .onSuccess(
152             (result -> {
153               if (isDiscarding) {
154                 return;
155               }
156               if (result.phoneAccountHandle.isPresent()) {
157                 Logger.get(coordinator.getActivity())
158                     .logImpression(DialerImpression.Type.DUAL_SIM_SELECTION_PREFERRED_USED);
159                 coordinator.getBuilder().setPhoneAccountHandle(result.phoneAccountHandle.get());
160                 pendingAction.finish();
161                 return;
162               }
163               PhoneAccountHandle defaultPhoneAccount =
164                   activity
165                       .getSystemService(TelecomManager.class)
166                       .getDefaultOutgoingPhoneAccount(builder.getUri().getScheme());
167               if (defaultPhoneAccount != null) {
168                 Logger.get(coordinator.getActivity())
169                     .logImpression(DialerImpression.Type.DUAL_SIM_SELECTION_GLOBAL_USED);
170                 builder.setPhoneAccountHandle(defaultPhoneAccount);
171                 pendingAction.finish();
172                 return;
173               }
174               if (result.suggestion.isPresent()) {
175                 LogUtil.i(
176                     "CallingAccountSelector.processPreferredAccount",
177                     "SIM suggested: " + result.suggestion.get().reason);
178                 if (result.suggestion.get().shouldAutoSelect) {
179                   Logger.get(coordinator.getActivity())
180                       .logImpression(
181                           DialerImpression.Type.DUAL_SIM_SELECTION_SUGGESTION_AUTO_SELECTED);
182                   LogUtil.i(
183                       "CallingAccountSelector.processPreferredAccount", "Auto selected suggestion");
184                   builder.setPhoneAccountHandle(result.suggestion.get().phoneAccountHandle);
185                   pendingAction.finish();
186                   return;
187                 }
188               }
189               showDialog(
190                   coordinator,
191                   pendingAction,
192                   result.dataId.orNull(),
193                   phoneNumber,
194                   result.suggestion.orNull());
195             }))
196         .build()
197         .executeParallel(activity);
198   }
199 
200   @MainThread
showDialog( PreCallCoordinator coordinator, PendingAction pendingAction, @Nullable String dataId, @Nullable String number, @Nullable Suggestion suggestion)201   private void showDialog(
202       PreCallCoordinator coordinator,
203       PendingAction pendingAction,
204       @Nullable String dataId,
205       @Nullable String number,
206       @Nullable Suggestion suggestion) {
207     Assert.isMainThread();
208     Logger.get(coordinator.getActivity()).logImpression(Type.DUAL_SIM_SELECTION_SHOWN);
209     if (dataId != null) {
210       Logger.get(coordinator.getActivity()).logImpression(Type.DUAL_SIM_SELECTION_IN_CONTACTS);
211     }
212     if (suggestion != null) {
213       Logger.get(coordinator.getActivity())
214           .logImpression(Type.DUAL_SIM_SELECTION_SUGGESTION_AVAILABLE);
215       switch (suggestion.reason) {
216         case INTRA_CARRIER:
217           Logger.get(coordinator.getActivity())
218               .logImpression(Type.DUAL_SIM_SELECTION_SUGGESTED_CARRIER);
219           break;
220         case FREQUENT:
221           Logger.get(coordinator.getActivity())
222               .logImpression(Type.DUAL_SIM_SELECTION_SUGGESTED_FREQUENCY);
223           break;
224         default:
225       }
226     }
227     List<PhoneAccountHandle> phoneAccountHandles =
228         coordinator
229             .getActivity()
230             .getSystemService(TelecomManager.class)
231             .getCallCapablePhoneAccounts();
232     selectPhoneAccountDialogFragment =
233         SelectPhoneAccountDialogFragment.newInstance(
234             R.string.pre_call_select_phone_account,
235             dataId != null /* canSetDefault */,
236             R.string.pre_call_select_phone_account_remember,
237             phoneAccountHandles,
238             new SelectedListener(coordinator, pendingAction, dataId, number, suggestion),
239             null /* call ID */,
240             buildHint(coordinator.getActivity(), phoneAccountHandles, suggestion));
241     selectPhoneAccountDialogFragment.show(
242         coordinator.getActivity().getFragmentManager(), TAG_CALLING_ACCOUNT_SELECTOR);
243   }
244 
245   @Nullable
buildHint( Context context, List<PhoneAccountHandle> phoneAccountHandles, @Nullable Suggestion suggestion)246   private static List<String> buildHint(
247       Context context,
248       List<PhoneAccountHandle> phoneAccountHandles,
249       @Nullable Suggestion suggestion) {
250     if (suggestion == null) {
251       return null;
252     }
253     List<String> hints = new ArrayList<>();
254     for (PhoneAccountHandle phoneAccountHandle : phoneAccountHandles) {
255       if (!phoneAccountHandle.equals(suggestion.phoneAccountHandle)) {
256         hints.add(null);
257         continue;
258       }
259       switch (suggestion.reason) {
260         case INTRA_CARRIER:
261           hints.add(context.getString(R.string.pre_call_select_phone_account_hint_intra_carrier));
262           break;
263         case FREQUENT:
264           hints.add(context.getString(R.string.pre_call_select_phone_account_hint_frequent));
265           break;
266         default:
267           LogUtil.w("CallingAccountSelector.buildHint", "unhandled reason " + suggestion.reason);
268       }
269     }
270     return hints;
271   }
272 
273   @MainThread
274   @Override
onDiscard()275   public void onDiscard() {
276     isDiscarding = true;
277     if (selectPhoneAccountDialogFragment != null) {
278       selectPhoneAccountDialogFragment.dismiss();
279     }
280   }
281 
282   private static class PreferredAccountWorkerResult {
283 
284     /** The preferred phone account for the number. Absent if not set or invalid. */
285     Optional<PhoneAccountHandle> phoneAccountHandle = Optional.absent();
286 
287     /**
288      * {@link android.provider.ContactsContract.Data#_ID} of the row matching the number. If the
289      * preferred account is to be set it should be stored in this row
290      */
291     Optional<String> dataId = Optional.absent();
292 
293     Optional<Suggestion> suggestion = Optional.absent();
294   }
295 
296   private static class PreferredAccountWorker
297       implements Worker<Context, PreferredAccountWorkerResult> {
298 
299     private final String phoneNumber;
300 
PreferredAccountWorker(String phoneNumber)301     public PreferredAccountWorker(String phoneNumber) {
302       this.phoneNumber = phoneNumber;
303     }
304 
305     @NonNull
306     @Override
307     @WorkerThread
doInBackground(Context context)308     public PreferredAccountWorkerResult doInBackground(Context context) throws Throwable {
309       PreferredAccountWorkerResult result = new PreferredAccountWorkerResult();
310       if (!isPreferredSimEnabled(context)) {
311         return result;
312       }
313       if (!PermissionsUtil.hasContactsReadPermissions(context)) {
314         LogUtil.i(
315             "CallingAccountSelector.PreferredAccountWorker.doInBackground",
316             "missing READ_CONTACTS permission");
317         return result;
318       }
319       result.dataId = getDataId(context, phoneNumber);
320       if (result.dataId.isPresent()) {
321         result.phoneAccountHandle = getPreferredAccount(context, result.dataId.get());
322       }
323       if (!result.phoneAccountHandle.isPresent()) {
324         result.suggestion =
325             SimSuggestionComponent.get(context)
326                 .getSuggestionProvider()
327                 .getSuggestion(context, phoneNumber);
328       }
329       return result;
330     }
331   }
332 
333   @WorkerThread
334   @NonNull
getDataId( @onNull Context context, @Nullable String phoneNumber)335   private static Optional<String> getDataId(
336       @NonNull Context context, @Nullable String phoneNumber) {
337     Assert.isWorkerThread();
338     if (VERSION.SDK_INT < VERSION_CODES.N) {
339       return Optional.absent();
340     }
341     try (Cursor cursor =
342         context
343             .getContentResolver()
344             .query(
345                 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber)),
346                 new String[] {PhoneLookup.DATA_ID},
347                 null,
348                 null,
349                 null)) {
350       if (cursor == null) {
351         return Optional.absent();
352       }
353       ImmutableSet<String> validAccountTypes = PreferredAccountUtil.getValidAccountTypes(context);
354       String result = null;
355       while (cursor.moveToNext()) {
356         Optional<String> accountType =
357             getAccountType(context.getContentResolver(), cursor.getLong(0));
358         if (!accountType.isPresent() || !validAccountTypes.contains(accountType.get())) {
359           LogUtil.i("CallingAccountSelector.getDataId", "ignoring non-writable " + accountType);
360           continue;
361         }
362         if (result != null && !result.equals(cursor.getString(0))) {
363           // TODO(twyen): if there are multiple entries attempt to grab from the contact that
364           // initiated the call.
365           LogUtil.i("CallingAccountSelector.getDataId", "lookup result not unique, ignoring");
366           return Optional.absent();
367         }
368         result = cursor.getString(0);
369       }
370       return Optional.fromNullable(result);
371     }
372   }
373 
374   @WorkerThread
getAccountType(ContentResolver contentResolver, long dataId)375   private static Optional<String> getAccountType(ContentResolver contentResolver, long dataId) {
376     Assert.isWorkerThread();
377     Optional<Long> rawContactId = getRawContactId(contentResolver, dataId);
378     if (!rawContactId.isPresent()) {
379       return Optional.absent();
380     }
381     try (Cursor cursor =
382         contentResolver.query(
383             ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId.get()),
384             new String[] {RawContacts.ACCOUNT_TYPE},
385             null,
386             null,
387             null)) {
388       if (cursor == null || !cursor.moveToFirst()) {
389         return Optional.absent();
390       }
391       return Optional.fromNullable(cursor.getString(0));
392     }
393   }
394 
395   @WorkerThread
getRawContactId(ContentResolver contentResolver, long dataId)396   private static Optional<Long> getRawContactId(ContentResolver contentResolver, long dataId) {
397     Assert.isWorkerThread();
398     try (Cursor cursor =
399         contentResolver.query(
400             ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
401             new String[] {Data.RAW_CONTACT_ID},
402             null,
403             null,
404             null)) {
405       if (cursor == null || !cursor.moveToFirst()) {
406         return Optional.absent();
407       }
408       return Optional.of(cursor.getLong(0));
409     }
410   }
411 
412   @WorkerThread
413   @NonNull
getPreferredAccount( @onNull Context context, @NonNull String dataId)414   private static Optional<PhoneAccountHandle> getPreferredAccount(
415       @NonNull Context context, @NonNull String dataId) {
416     Assert.isWorkerThread();
417     Assert.isNotNull(dataId);
418     try (Cursor cursor =
419         context
420             .getContentResolver()
421             .query(
422                 PreferredSimFallbackContract.CONTENT_URI,
423                 new String[] {
424                   PreferredSim.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME,
425                   PreferredSim.PREFERRED_PHONE_ACCOUNT_ID
426                 },
427                 PreferredSim.DATA_ID + " = ?",
428                 new String[] {dataId},
429                 null)) {
430       if (cursor == null) {
431         return Optional.absent();
432       }
433       if (!cursor.moveToFirst()) {
434         return Optional.absent();
435       }
436       return PreferredAccountUtil.getValidPhoneAccount(
437           context, cursor.getString(0), cursor.getString(1));
438     }
439   }
440 
441   private class SelectedListener extends SelectPhoneAccountListener {
442 
443     private final PreCallCoordinator coordinator;
444     private final PreCallCoordinator.PendingAction listener;
445     private final String dataId;
446     private final String number;
447     private final Suggestion suggestion;
448 
SelectedListener( @onNull PreCallCoordinator builder, @NonNull PreCallCoordinator.PendingAction listener, @Nullable String dataId, @Nullable String number, @Nullable Suggestion suggestion)449     public SelectedListener(
450         @NonNull PreCallCoordinator builder,
451         @NonNull PreCallCoordinator.PendingAction listener,
452         @Nullable String dataId,
453         @Nullable String number,
454         @Nullable Suggestion suggestion) {
455       this.coordinator = Assert.isNotNull(builder);
456       this.listener = Assert.isNotNull(listener);
457       this.dataId = dataId;
458       this.number = number;
459       this.suggestion = suggestion;
460     }
461 
462     @MainThread
463     @Override
onPhoneAccountSelected( PhoneAccountHandle selectedAccountHandle, boolean setDefault, @Nullable String callId)464     public void onPhoneAccountSelected(
465         PhoneAccountHandle selectedAccountHandle, boolean setDefault, @Nullable String callId) {
466       if (suggestion != null) {
467         if (suggestion.phoneAccountHandle.equals(selectedAccountHandle)) {
468           Logger.get(coordinator.getActivity())
469               .logImpression(DialerImpression.Type.DUAL_SIM_SELECTION_SUGGESTED_SIM_SELECTED);
470         } else {
471           Logger.get(coordinator.getActivity())
472               .logImpression(DialerImpression.Type.DUAL_SIM_SELECTION_NON_SUGGESTED_SIM_SELECTED);
473         }
474       }
475       coordinator.getBuilder().setPhoneAccountHandle(selectedAccountHandle);
476 
477       if (dataId != null && setDefault) {
478         Logger.get(coordinator.getActivity())
479             .logImpression(DialerImpression.Type.DUAL_SIM_SELECTION_PREFERRED_SET);
480         DialerExecutorComponent.get(coordinator.getActivity())
481             .dialerExecutorFactory()
482             .createNonUiTaskBuilder(new WritePreferredAccountWorker())
483             .build()
484             .executeParallel(
485                 new WritePreferredAccountWorkerInput(
486                     coordinator.getActivity(), dataId, selectedAccountHandle));
487       }
488       if (number != null) {
489         DialerExecutorComponent.get(coordinator.getActivity())
490             .dialerExecutorFactory()
491             .createNonUiTaskBuilder(
492                 new UserSelectionReporter(selectedAccountHandle, number, setDefault))
493             .build()
494             .executeParallel(coordinator.getActivity());
495       }
496       listener.finish();
497     }
498 
499     @MainThread
500     @Override
onDialogDismissed(@ullable String callId)501     public void onDialogDismissed(@Nullable String callId) {
502       if (isDiscarding) {
503         return;
504       }
505       coordinator.abortCall();
506       listener.finish();
507     }
508   }
509 
510   private static class UserSelectionReporter implements Worker<Context, Void> {
511 
512     private final String number;
513     private final PhoneAccountHandle phoneAccountHandle;
514     private final boolean remember;
515 
UserSelectionReporter( @onNull PhoneAccountHandle phoneAccountHandle, @Nullable String number, boolean remember)516     public UserSelectionReporter(
517         @NonNull PhoneAccountHandle phoneAccountHandle, @Nullable String number, boolean remember) {
518       this.phoneAccountHandle = Assert.isNotNull(phoneAccountHandle);
519       this.number = Assert.isNotNull(number);
520       this.remember = remember;
521     }
522 
523     @Nullable
524     @Override
doInBackground(@onNull Context context)525     public Void doInBackground(@NonNull Context context) throws Throwable {
526       SimSuggestionComponent.get(context)
527           .getSuggestionProvider()
528           .reportUserSelection(context, number, phoneAccountHandle, remember);
529       return null;
530     }
531   }
532 
533   private static class WritePreferredAccountWorkerInput {
534     private final Context context;
535     private final String dataId;
536     private final PhoneAccountHandle phoneAccountHandle;
537 
WritePreferredAccountWorkerInput( @onNull Context context, @NonNull String dataId, @NonNull PhoneAccountHandle phoneAccountHandle)538     WritePreferredAccountWorkerInput(
539         @NonNull Context context,
540         @NonNull String dataId,
541         @NonNull PhoneAccountHandle phoneAccountHandle) {
542       this.context = Assert.isNotNull(context);
543       this.dataId = Assert.isNotNull(dataId);
544       this.phoneAccountHandle = Assert.isNotNull(phoneAccountHandle);
545     }
546   }
547 
548   private static class WritePreferredAccountWorker
549       implements Worker<WritePreferredAccountWorkerInput, Void> {
550 
551     @Nullable
552     @Override
553     @WorkerThread
doInBackground(WritePreferredAccountWorkerInput input)554     public Void doInBackground(WritePreferredAccountWorkerInput input) throws Throwable {
555       ContentValues values = new ContentValues();
556       values.put(
557           PreferredSim.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME,
558           input.phoneAccountHandle.getComponentName().flattenToString());
559       values.put(PreferredSim.PREFERRED_PHONE_ACCOUNT_ID, input.phoneAccountHandle.getId());
560       input
561           .context
562           .getContentResolver()
563           .update(
564               PreferredSimFallbackContract.CONTENT_URI,
565               values,
566               PreferredSim.DATA_ID + " = ?",
567               new String[] {String.valueOf(input.dataId)});
568       return null;
569     }
570   }
571 
572   @WorkerThread
isPreferredSimEnabled(Context context)573   private static boolean isPreferredSimEnabled(Context context) {
574     Assert.isWorkerThread();
575     if (!ConfigProviderBindings.get(context).getBoolean("preferred_sim_enabled", true)) {
576       return false;
577     }
578 
579     Intent quickContactIntent = getQuickContactIntent();
580     ResolveInfo resolveInfo =
581         context
582             .getPackageManager()
583             .resolveActivity(quickContactIntent, PackageManager.GET_META_DATA);
584     if (resolveInfo == null
585         || resolveInfo.activityInfo == null
586         || resolveInfo.activityInfo.applicationInfo == null
587         || resolveInfo.activityInfo.applicationInfo.metaData == null) {
588       LogUtil.e("CallingAccountSelector.isPreferredSimEnabled", "cannot resolve quick contact app");
589       return false;
590     }
591     if (!resolveInfo.activityInfo.applicationInfo.metaData.getBoolean(
592         METADATA_SUPPORTS_PREFERRED_SIM, false)) {
593       LogUtil.i(
594           "CallingAccountSelector.isPreferredSimEnabled",
595           "system contacts does not support preferred SIM");
596       return false;
597     }
598     return true;
599   }
600 
601   @VisibleForTesting
getQuickContactIntent()602   static Intent getQuickContactIntent() {
603     Intent intent = new Intent(QuickContact.ACTION_QUICK_CONTACT);
604     intent.addCategory(Intent.CATEGORY_DEFAULT);
605     intent.setData(Contacts.CONTENT_URI.buildUpon().appendPath("1").build());
606     return intent;
607   }
608 }
609