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