From 3b2c7814d05cef19a7e4797d5a756621cc0eeb77 Mon Sep 17 00:00:00 2001 From: twyen Date: Mon, 30 Apr 2018 12:13:06 -0700 Subject: Don't show dialog if in call activity is not visible. UiListener nulls out the callbacks onDetach(), which is after onSaveInstanceState(). TEST=N/A InCallActivity is not testable. Bug: 78517857 Test: N/A InCallActivity is not testable. PiperOrigin-RevId: 194824783 Change-Id: I2c4f41cf58a498a841f69ef078e58d07ec5b3c5a --- java/com/android/incallui/InCallActivity.java | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'java/com/android/incallui/InCallActivity.java') diff --git a/java/com/android/incallui/InCallActivity.java b/java/com/android/incallui/InCallActivity.java index 0f0e9d9f2..0ee98c25c 100644 --- a/java/com/android/incallui/InCallActivity.java +++ b/java/com/android/incallui/InCallActivity.java @@ -382,6 +382,12 @@ public class InCallActivity extends TransactionSafeFragmentActivity this, preferredAccountFuture, result -> { + if (!isVisible()) { + LogUtil.i( + "CallingAccountSelector.showPhoneAccountSelectionDialog", + "activity ended before result returned"); + return; + } if (result.getPhoneAccountHandle().isPresent()) { Logger.get(this).logImpression(Type.DUAL_SIM_SELECTION_PREFERRED_USED); selectPhoneAccountListener.onPhoneAccountSelected( -- cgit v1.2.3 From 56f79ba6c6608f7041f5e65866b7164499ca7676 Mon Sep 17 00:00:00 2001 From: twyen Date: Mon, 30 Apr 2018 14:25:46 -0700 Subject: Refactor PreferredAccountWorker to provide the dialog to be shown. When dual SIM selection support is added to In Call UI it was a rush order and codes are duplicated. This CL moves the duplicated logic into PreferredAccountWorker so the same dialog can be shown for both. TEST=manual Bug: 69675796,72618783 Test: manual PiperOrigin-RevId: 194845320 Change-Id: Id283ca7616580b0efd4e8f02e63691c70ee7f93c --- .../binary/aosp/AospDialerRootComponent.java | 2 + .../basecomponent/BaseDialerRootComponent.java | 2 + .../google/GoogleStubDialerRootComponent.java | 2 + .../android/dialer/precall/PreCallCoordinator.java | 7 + .../precall/impl/CallingAccountSelector.java | 266 +++---------- .../precall/impl/PreCallCoordinatorImpl.java | 25 ++ .../android/dialer/precall/impl/PreCallModule.java | 5 +- .../dialer/precall/impl/res/values/strings.xml | 13 - .../dialer/preferredsim/PreferredAccountUtil.java | 5 +- .../preferredsim/PreferredAccountWorker.java | 269 +++---------- .../dialer/preferredsim/PreferredSimComponent.java | 37 ++ .../dialer/preferredsim/PreferredSimModule.java | 29 ++ .../dialer/preferredsim/impl/AndroidManifest.xml | 4 +- .../impl/PreferredAccountWorkerImpl.java | 435 +++++++++++++++++++++ .../preferredsim/impl/res/values/strings.xml | 33 ++ .../incallui/ActiveCallsCallListListener.java | 4 +- java/com/android/incallui/InCallActivity.java | 78 +--- 17 files changed, 697 insertions(+), 519 deletions(-) create mode 100644 java/com/android/dialer/preferredsim/PreferredSimComponent.java create mode 100644 java/com/android/dialer/preferredsim/PreferredSimModule.java create mode 100644 java/com/android/dialer/preferredsim/impl/PreferredAccountWorkerImpl.java create mode 100644 java/com/android/dialer/preferredsim/impl/res/values/strings.xml (limited to 'java/com/android/incallui/InCallActivity.java') diff --git a/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java b/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java index e1021894f..6cbaf4fe7 100644 --- a/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java +++ b/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java @@ -33,6 +33,7 @@ import com.android.dialer.metrics.StubMetricsModule; import com.android.dialer.phonelookup.PhoneLookupModule; import com.android.dialer.phonenumbergeoutil.impl.PhoneNumberGeoUtilModule; import com.android.dialer.precall.impl.PreCallModule; +import com.android.dialer.preferredsim.PreferredSimModule; import com.android.dialer.preferredsim.suggestion.stub.StubSimSuggestionModule; import com.android.dialer.simulator.impl.SimulatorModule; import com.android.dialer.simulator.stub.StubSimulatorEnrichedCallModule; @@ -60,6 +61,7 @@ import javax.inject.Singleton; PhoneLookupModule.class, PhoneNumberGeoUtilModule.class, PreCallModule.class, + PreferredSimModule.class, SharedPrefConfigProviderModule.class, SimulatorModule.class, StubSimulatorEnrichedCallModule.class, diff --git a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java index 75ddaf7f0..5fed683f6 100644 --- a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java +++ b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java @@ -34,6 +34,7 @@ import com.android.dialer.phonelookup.PhoneLookupComponent; import com.android.dialer.phonelookup.database.PhoneLookupDatabaseComponent; import com.android.dialer.phonenumbergeoutil.PhoneNumberGeoUtilComponent; import com.android.dialer.precall.PreCallComponent; +import com.android.dialer.preferredsim.PreferredSimComponent; import com.android.dialer.preferredsim.suggestion.SimSuggestionComponent; import com.android.dialer.simulator.SimulatorComponent; import com.android.dialer.spam.SpamComponent; @@ -72,6 +73,7 @@ public interface BaseDialerRootComponent PhoneLookupDatabaseComponent.HasComponent, PhoneNumberGeoUtilComponent.HasComponent, PreCallComponent.HasComponent, + PreferredSimComponent.HasComponent, UiItemLoaderComponent.HasComponent, SimSuggestionComponent.HasComponent, SimulatorComponent.HasComponent, diff --git a/java/com/android/dialer/binary/google/GoogleStubDialerRootComponent.java b/java/com/android/dialer/binary/google/GoogleStubDialerRootComponent.java index bdbdeb9dd..f4f7a0a3b 100644 --- a/java/com/android/dialer/binary/google/GoogleStubDialerRootComponent.java +++ b/java/com/android/dialer/binary/google/GoogleStubDialerRootComponent.java @@ -33,6 +33,7 @@ import com.android.dialer.metrics.StubMetricsModule; import com.android.dialer.phonelookup.PhoneLookupModule; import com.android.dialer.phonenumbergeoutil.impl.PhoneNumberGeoUtilModule; import com.android.dialer.precall.impl.PreCallModule; +import com.android.dialer.preferredsim.PreferredSimModule; import com.android.dialer.preferredsim.suggestion.stub.StubSimSuggestionModule; import com.android.dialer.simulator.impl.SimulatorModule; import com.android.dialer.simulator.stub.StubSimulatorEnrichedCallModule; @@ -65,6 +66,7 @@ import javax.inject.Singleton; PhoneLookupModule.class, // TODO(zachh): Module which uses APDL? PhoneNumberGeoUtilModule.class, PreCallModule.class, + PreferredSimModule.class, SharedPrefConfigProviderModule.class, SimulatorModule.class, StorageModule.class, diff --git a/java/com/android/dialer/precall/PreCallCoordinator.java b/java/com/android/dialer/precall/PreCallCoordinator.java index cb3221afd..9c24e0d69 100644 --- a/java/com/android/dialer/precall/PreCallCoordinator.java +++ b/java/com/android/dialer/precall/PreCallCoordinator.java @@ -21,6 +21,8 @@ import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import com.android.dialer.callintent.CallIntentBuilder; +import com.android.dialer.function.Consumer; +import com.google.common.util.concurrent.ListenableFuture; /** * Runs {@link PreCallAction} one by one to prepare a {@link @@ -65,4 +67,9 @@ public interface PreCallCoordinator { @MainThread @NonNull PendingAction startPendingAction(); + + void listen( + ListenableFuture future, + Consumer successListener, + Consumer failureListener); } diff --git a/java/com/android/dialer/precall/impl/CallingAccountSelector.java b/java/com/android/dialer/precall/impl/CallingAccountSelector.java index 8f63fa0db..b22e4d973 100644 --- a/java/com/android/dialer/precall/impl/CallingAccountSelector.java +++ b/java/com/android/dialer/precall/impl/CallingAccountSelector.java @@ -29,13 +29,9 @@ import android.telephony.PhoneNumberUtils; import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment; import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment.SelectPhoneAccountListener; import com.android.contacts.common.widget.SelectPhoneAccountDialogOptions; -import com.android.contacts.common.widget.SelectPhoneAccountDialogOptionsUtil; -import com.android.dialer.activecalls.ActiveCallInfo; -import com.android.dialer.activecalls.ActiveCallsComponent; import com.android.dialer.callintent.CallIntentBuilder; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; -import com.android.dialer.common.concurrent.DialerExecutorComponent; import com.android.dialer.configprovider.ConfigProviderBindings; import com.android.dialer.logging.DialerImpression.Type; import com.android.dialer.logging.Logger; @@ -44,13 +40,10 @@ import com.android.dialer.precall.PreCallCoordinator; import com.android.dialer.precall.PreCallCoordinator.PendingAction; import com.android.dialer.preferredsim.PreferredAccountRecorder; import com.android.dialer.preferredsim.PreferredAccountWorker; -import com.android.dialer.preferredsim.PreferredAccountWorker.Result; import com.android.dialer.preferredsim.suggestion.SuggestionProvider; import com.android.dialer.preferredsim.suggestion.SuggestionProvider.Suggestion; -import com.google.common.base.Optional; -import com.google.common.collect.ImmutableList; import java.util.List; -import java.util.Objects; +import javax.inject.Inject; /** PreCallAction to select which phone account to call with. Ignored if there's only one account */ @SuppressWarnings("MissingPermission") @@ -62,6 +55,13 @@ public class CallingAccountSelector implements PreCallAction { private boolean isDiscarding; + private final PreferredAccountWorker preferredAccountWorker; + + @Inject + CallingAccountSelector(PreferredAccountWorker preferredAccountWorker) { + this.preferredAccountWorker = preferredAccountWorker; + } + @Override public boolean requiresUi(Context context, CallIntentBuilder builder) { if (!ConfigProviderBindings.get(context) @@ -97,7 +97,13 @@ public class CallingAccountSelector implements PreCallAction { } switch (builder.getUri().getScheme()) { case PhoneAccount.SCHEME_VOICEMAIL: - showDialog(coordinator, coordinator.startPendingAction(), null, null, null); + showDialog( + coordinator, + coordinator.startPendingAction(), + preferredAccountWorker.getVoicemailDialogOptions(), + null, + null, + null); Logger.get(coordinator.getActivity()).logImpression(Type.DUAL_SIM_SELECTION_VOICEMAIL); break; case PhoneAccount.SCHEME_TEL: @@ -120,224 +126,58 @@ public class CallingAccountSelector implements PreCallAction { Activity activity = coordinator.getActivity(); String phoneNumber = builder.getUri().getSchemeSpecificPart(); PendingAction pendingAction = coordinator.startPendingAction(); - DialerExecutorComponent.get(coordinator.getActivity()) - .dialerExecutorFactory() - .createNonUiTaskBuilder(new PreferredAccountWorker(phoneNumber)) - .onSuccess( - (result -> { - if (isDiscarding) { - return; - } - if (result.getPhoneAccountHandle().isPresent()) { - usePreferredAccount(coordinator, pendingAction, result); - return; - } - PhoneAccountHandle defaultPhoneAccount = - activity - .getSystemService(TelecomManager.class) - .getDefaultOutgoingPhoneAccount(builder.getUri().getScheme()); - if (defaultPhoneAccount != null) { - useDefaultAccount(coordinator, pendingAction, result, defaultPhoneAccount); - return; - } - if (result.getSuggestion().isPresent()) { - LogUtil.i( - "CallingAccountSelector.processPreferredAccount", - "SIM suggested: " + result.getSuggestion().get().reason); - if (result.getSuggestion().get().shouldAutoSelect) { - useSuggestedAccount(coordinator, pendingAction, result); - return; - } - } - showDialog( - coordinator, - pendingAction, - result.getDataId().orNull(), - phoneNumber, - result.getSuggestion().orNull()); - })) - .build() - .executeParallel(activity); - } - private void usePreferredAccount( - PreCallCoordinator coordinator, PendingAction pendingAction, Result result) { - String phoneNumber = coordinator.getBuilder().getUri().getSchemeSpecificPart(); - if (isSelectable(coordinator.getActivity(), result.getPhoneAccountHandle().get())) { - Logger.get(coordinator.getActivity()).logImpression(Type.DUAL_SIM_SELECTION_PREFERRED_USED); - coordinator.getBuilder().setPhoneAccountHandle(result.getPhoneAccountHandle().get()); - pendingAction.finish(); - } else { - Logger.get(coordinator.getActivity()) - .logImpression(Type.DUAL_SIM_SELECTION_PREFERRED_NOT_SELECTABLE); - LogUtil.i("CallingAccountSelector.usePreferredAccount", "preferred account not selectable"); - showDialog( - coordinator, - pendingAction, - result.getDataId().orNull(), - phoneNumber, - result.getSuggestion().orNull()); - } - } - - private void useDefaultAccount( - PreCallCoordinator coordinator, - PendingAction pendingAction, - Result result, - PhoneAccountHandle defaultPhoneAccount) { - CallIntentBuilder builder = coordinator.getBuilder(); - String phoneNumber = builder.getUri().getSchemeSpecificPart(); - if (isSelectable(coordinator.getActivity(), defaultPhoneAccount)) { - Logger.get(coordinator.getActivity()).logImpression(Type.DUAL_SIM_SELECTION_GLOBAL_USED); - builder.setPhoneAccountHandle(defaultPhoneAccount); - pendingAction.finish(); - } else { - Logger.get(coordinator.getActivity()) - .logImpression(Type.DUAL_SIM_SELECTION_GLOBAL_NOT_SELECTABLE); - LogUtil.i("CallingAccountSelector.useDefaultAccount", "default account not selectable"); - showDialog( - coordinator, - pendingAction, - result.getDataId().orNull(), - phoneNumber, - result.getSuggestion().orNull()); - } - } - - private void useSuggestedAccount( - PreCallCoordinator coordinator, PendingAction pendingAction, Result result) { - CallIntentBuilder builder = coordinator.getBuilder(); - String phoneNumber = builder.getUri().getSchemeSpecificPart(); - if (isSelectable(coordinator.getActivity(), result.getSuggestion().get().phoneAccountHandle)) { - Logger.get(coordinator.getActivity()) - .logImpression(Type.DUAL_SIM_SELECTION_SUGGESTION_AUTO_SELECTED); - LogUtil.i("CallingAccountSelector.processPreferredAccount", "Auto selected suggestion"); - builder.setPhoneAccountHandle(result.getSuggestion().get().phoneAccountHandle); - builder - .getInCallUiIntentExtras() - .putString( - SuggestionProvider.EXTRA_SIM_SUGGESTION_REASON, - result.getSuggestion().get().reason.name()); - pendingAction.finish(); - } else { - LogUtil.i("CallingAccountSelector.useSuggestedAccount", "suggested account not selectable"); - Logger.get(coordinator.getActivity()) - .logImpression(Type.DUAL_SIM_SELECTION_SUGGESTION_AUTO_NOT_SELECTABLE); - showDialog( - coordinator, - pendingAction, - result.getDataId().orNull(), - phoneNumber, - result.getSuggestion().orNull()); - } - } - - /** - * Most devices are DSDS (dual SIM dual standby) which only one SIM can have active calls at a - * time. TODO(twyen): support other dual SIM modes when the API is exposed. - */ - private static boolean isSelectable(Context context, PhoneAccountHandle phoneAccountHandle) { - ImmutableList activeCalls = - ActiveCallsComponent.get(context).activeCalls().getActiveCalls(); - if (activeCalls.isEmpty()) { - return true; - } - for (ActiveCallInfo activeCall : activeCalls) { - if (Objects.equals(phoneAccountHandle, activeCall.phoneAccountHandle().orNull())) { - return true; - } - } - return false; - } - - private static Optional getActiveCallLabel(Context context) { - ImmutableList activeCalls = - ActiveCallsComponent.get(context).activeCalls().getActiveCalls(); - - if (activeCalls.isEmpty()) { - LogUtil.e("CallingAccountSelector.getActiveCallLabel", "active calls no longer exist"); - return Optional.absent(); - } - ActiveCallInfo activeCall = activeCalls.get(0); - if (!activeCall.phoneAccountHandle().isPresent()) { - LogUtil.e("CallingAccountSelector.getActiveCallLabel", "active call has no phone account"); - return Optional.absent(); - } - PhoneAccount phoneAccount = - context - .getSystemService(TelecomManager.class) - .getPhoneAccount(activeCall.phoneAccountHandle().get()); - if (phoneAccount == null) { - LogUtil.e("CallingAccountSelector.getActiveCallLabel", "phone account not found"); - return Optional.absent(); - } - return Optional.of(phoneAccount.getLabel().toString()); + coordinator.listen( + preferredAccountWorker.selectAccount( + phoneNumber, + activity.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts()), + result -> { + if (result.getSelectedPhoneAccountHandle().isPresent()) { + + if (result.getSuggestion().isPresent() + && result + .getSelectedPhoneAccountHandle() + .get() + .equals(result.getSuggestion().get().phoneAccountHandle)) { + builder + .getInCallUiIntentExtras() + .putString( + SuggestionProvider.EXTRA_SIM_SUGGESTION_REASON, + result.getSuggestion().get().reason.name()); + } + + coordinator + .getBuilder() + .setPhoneAccountHandle(result.getSelectedPhoneAccountHandle().get()); + pendingAction.finish(); + return; + } + showDialog( + coordinator, + pendingAction, + result.getDialogOptionsBuilder().get().build(), + result.getDataId().orNull(), + phoneNumber, + result.getSuggestion().orNull()); + }, + (throwable) -> { + throw new RuntimeException(throwable); + }); } @MainThread private void showDialog( PreCallCoordinator coordinator, PendingAction pendingAction, + SelectPhoneAccountDialogOptions dialogOptions, @Nullable String dataId, @Nullable String number, @Nullable Suggestion suggestion) { Assert.isMainThread(); - Logger.get(coordinator.getActivity()).logImpression(Type.DUAL_SIM_SELECTION_SHOWN); - if (dataId != null) { - Logger.get(coordinator.getActivity()).logImpression(Type.DUAL_SIM_SELECTION_IN_CONTACTS); - } - if (suggestion != null) { - Logger.get(coordinator.getActivity()) - .logImpression(Type.DUAL_SIM_SELECTION_SUGGESTION_AVAILABLE); - switch (suggestion.reason) { - case INTRA_CARRIER: - Logger.get(coordinator.getActivity()) - .logImpression(Type.DUAL_SIM_SELECTION_SUGGESTED_CARRIER); - break; - case FREQUENT: - Logger.get(coordinator.getActivity()) - .logImpression(Type.DUAL_SIM_SELECTION_SUGGESTED_FREQUENCY); - break; - default: - } - } - SelectPhoneAccountDialogOptions.Builder optionsBuilder = - SelectPhoneAccountDialogOptions.newBuilder() - .setTitle(R.string.pre_call_select_phone_account) - .setCanSetDefault(dataId != null) - .setSetDefaultLabel(R.string.pre_call_select_phone_account_remember); - for (PhoneAccountHandle phoneAccountHandle : - coordinator - .getActivity() - .getSystemService(TelecomManager.class) - .getCallCapablePhoneAccounts()) { - SelectPhoneAccountDialogOptions.Entry.Builder entryBuilder = - SelectPhoneAccountDialogOptions.Entry.newBuilder(); - SelectPhoneAccountDialogOptionsUtil.setPhoneAccountHandle(entryBuilder, phoneAccountHandle); - if (isSelectable(coordinator.getActivity(), phoneAccountHandle)) { - Optional hint = - SuggestionProvider.getHint(coordinator.getActivity(), phoneAccountHandle, suggestion); - if (hint.isPresent()) { - entryBuilder.setHint(hint.get()); - } - } else { - entryBuilder.setEnabled(false); - Optional activeCallLabel = getActiveCallLabel(coordinator.getActivity()); - if (activeCallLabel.isPresent()) { - entryBuilder.setHint( - coordinator - .getActivity() - .getString( - R.string.pre_call_select_phone_account_hint_other_sim_in_use, - activeCallLabel.get())); - } - } - optionsBuilder.addEntries(entryBuilder); - } selectPhoneAccountDialogFragment = SelectPhoneAccountDialogFragment.newInstance( - optionsBuilder.build(), + dialogOptions, new SelectedListener( coordinator, pendingAction, diff --git a/java/com/android/dialer/precall/impl/PreCallCoordinatorImpl.java b/java/com/android/dialer/precall/impl/PreCallCoordinatorImpl.java index 36af62245..f2ff0e3e3 100644 --- a/java/com/android/dialer/precall/impl/PreCallCoordinatorImpl.java +++ b/java/com/android/dialer/precall/impl/PreCallCoordinatorImpl.java @@ -24,6 +24,9 @@ import android.support.annotation.Nullable; import com.android.dialer.callintent.CallIntentBuilder; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.DialerExecutorComponent; +import com.android.dialer.common.concurrent.UiListener; +import com.android.dialer.function.Consumer; import com.android.dialer.logging.DialerImpression.Type; import com.android.dialer.logging.Logger; import com.android.dialer.precall.PreCallAction; @@ -31,6 +34,9 @@ import com.android.dialer.precall.PreCallComponent; import com.android.dialer.precall.PreCallCoordinator; import com.android.dialer.telecom.TelecomUtil; import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; /** * Implements {@link PreCallCoordinator}. Listens to the life cycle of {@link PreCallActivity} to @@ -49,6 +55,8 @@ public class PreCallCoordinatorImpl implements PreCallCoordinator { private PendingAction pendingAction; private boolean aborted = false; + private UiListener uiListener; + PreCallCoordinatorImpl(@NonNull Activity activity) { this.activity = Assert.isNotNull(activity); } @@ -61,6 +69,9 @@ public class PreCallCoordinatorImpl implements PreCallCoordinator { } else { builder = Assert.isNotNull(intent.getParcelableExtra(EXTRA_CALL_INTENT_BUILDER)); } + uiListener = + DialerExecutorComponent.get(activity) + .createUiListener(activity.getFragmentManager(), "PreCallCoordinatorImpl.uiListener"); } void onRestoreInstanceState(Bundle savedInstanceState) { @@ -152,4 +163,18 @@ public class PreCallCoordinatorImpl implements PreCallCoordinator { onActionFinished(); } } + + @SuppressWarnings("unchecked") + @Override + public void listen( + ListenableFuture future, + Consumer successListener, + Consumer failureListener) { + + uiListener.listen( + activity, + Futures.transform(future, (output) -> (Object) output, MoreExecutors.directExecutor()), + output -> successListener.accept((OutputT) output), + failureListener::accept); + } } diff --git a/java/com/android/dialer/precall/impl/PreCallModule.java b/java/com/android/dialer/precall/impl/PreCallModule.java index 4643b1976..9820e2b66 100644 --- a/java/com/android/dialer/precall/impl/PreCallModule.java +++ b/java/com/android/dialer/precall/impl/PreCallModule.java @@ -36,12 +36,13 @@ public abstract class PreCallModule { @Provides @Singleton - public static ImmutableList provideActions() { + public static ImmutableList provideActions( + CallingAccountSelector callingAccountSelector) { return ImmutableList.of( new PermissionCheckAction(), new MalformedNumberRectifier( ImmutableList.of(new UkRegionPrefixInInternationalFormatHandler())), - new CallingAccountSelector(), + callingAccountSelector, new AssistedDialAction()); } } diff --git a/java/com/android/dialer/precall/impl/res/values/strings.xml b/java/com/android/dialer/precall/impl/res/values/strings.xml index 5e7ddd36c..a920f0720 100644 --- a/java/com/android/dialer/precall/impl/res/values/strings.xml +++ b/java/com/android/dialer/precall/impl/res/values/strings.xml @@ -17,17 +17,4 @@ Cannot make call without phone permission - - - Choose SIM for this call - - - Remember this choice - - - Not available while using %1$s \ No newline at end of file diff --git a/java/com/android/dialer/preferredsim/PreferredAccountUtil.java b/java/com/android/dialer/preferredsim/PreferredAccountUtil.java index 1cfdbb161..b546dc0b8 100644 --- a/java/com/android/dialer/preferredsim/PreferredAccountUtil.java +++ b/java/com/android/dialer/preferredsim/PreferredAccountUtil.java @@ -30,7 +30,7 @@ import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.text.TextUtils; import com.android.dialer.common.LogUtil; -import com.android.dialer.configprovider.ConfigProviderComponent; +import com.android.dialer.configprovider.ConfigProviderBindings; import com.google.common.base.Optional; import com.google.common.collect.ImmutableSet; @@ -102,8 +102,7 @@ public class PreferredAccountUtil { */ public static ImmutableSet getValidAccountTypes(Context context) { return ImmutableSet.copyOf( - ConfigProviderComponent.get(context) - .getConfigProvider() + ConfigProviderBindings.get(context) .getString( "preferred_sim_valid_account_types", "com.google;" diff --git a/java/com/android/dialer/preferredsim/PreferredAccountWorker.java b/java/com/android/dialer/preferredsim/PreferredAccountWorker.java index df743c342..965d08843 100644 --- a/java/com/android/dialer/preferredsim/PreferredAccountWorker.java +++ b/java/com/android/dialer/preferredsim/PreferredAccountWorker.java @@ -16,47 +16,35 @@ package com.android.dialer.preferredsim; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.database.Cursor; -import android.net.Uri; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.Data; -import android.provider.ContactsContract.PhoneLookup; -import android.provider.ContactsContract.QuickContact; -import android.provider.ContactsContract.RawContacts; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.VisibleForTesting; -import android.support.annotation.WorkerThread; import android.telecom.PhoneAccountHandle; -import android.text.TextUtils; -import com.android.dialer.common.Assert; -import com.android.dialer.common.LogUtil; -import com.android.dialer.common.concurrent.DialerExecutor.Worker; -import com.android.dialer.configprovider.ConfigProviderBindings; -import com.android.dialer.preferredsim.PreferredAccountWorker.Result; -import com.android.dialer.preferredsim.PreferredSimFallbackContract.PreferredSim; -import com.android.dialer.preferredsim.suggestion.SimSuggestionComponent; +import com.android.contacts.common.widget.SelectPhoneAccountDialogOptions; import com.android.dialer.preferredsim.suggestion.SuggestionProvider.Suggestion; -import com.android.dialer.util.PermissionsUtil; import com.google.auto.value.AutoValue; import com.google.common.base.Optional; -import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.List; /** Query a preferred SIM to make a call with. */ -public class PreferredAccountWorker implements Worker { +@SuppressWarnings({"missingPermission", "Guava"}) +public interface PreferredAccountWorker { - /** The result of the worker. */ + /** Result of the query. */ @AutoValue - public abstract static class Result { + abstract class Result { - /** The preferred phone account for the number. Absent if not set or invalid. */ - public abstract Optional getPhoneAccountHandle(); + /** + * The phone account to dial with for the number. Absent if no account can be auto selected. If + * absent, {@link #getSelectedPhoneAccountHandle()} will be present to show a dialog for the + * user to manually select. + */ + public abstract Optional getSelectedPhoneAccountHandle(); + + /** + * The {@link SelectPhoneAccountDialogOptions} that should be used to show the selection dialog. + * Absent if {@link #getSelectedPhoneAccountHandle()} is present, which should be used directly + * instead of asking the user. + */ + public abstract Optional getDialogOptionsBuilder(); /** * {@link android.provider.ContactsContract.Data#_ID} of the row matching the number. If the @@ -66,207 +54,42 @@ public class PreferredAccountWorker implements Worker { public abstract Optional getSuggestion(); - static Builder builder() { - return new AutoValue_PreferredAccountWorker_Result.Builder(); + public static Builder builder(PhoneAccountHandle selectedPhoneAccountHandle) { + return new AutoValue_PreferredAccountWorker_Result.Builder() + .setSelectedPhoneAccountHandle(selectedPhoneAccountHandle); } - @AutoValue.Builder - abstract static class Builder { - - public abstract Builder setPhoneAccountHandle( - Optional phoneAccountHandle); - - public abstract Builder setDataId(Optional dataId); - - public abstract Builder setSuggestion(Optional suggestion); - - public abstract Result build(); + public static Builder builder(SelectPhoneAccountDialogOptions.Builder optionsBuilder) { + return new AutoValue_PreferredAccountWorker_Result.Builder() + .setDialogOptionsBuilder(optionsBuilder); } - } - - @VisibleForTesting - public static final String METADATA_SUPPORTS_PREFERRED_SIM = - "supports_per_number_preferred_account"; - private final String phoneNumber; - - public PreferredAccountWorker(String phoneNumber) { - this.phoneNumber = phoneNumber; - } - - @NonNull - @Override - @WorkerThread - public Result doInBackground(Context context) throws Throwable { - Result.Builder resultBuilder = Result.builder(); - if (!isPreferredSimEnabled(context)) { - return resultBuilder.build(); - } - if (!PermissionsUtil.hasContactsReadPermissions(context)) { - LogUtil.i("PreferredAccountWorker.doInBackground", "missing READ_CONTACTS permission"); - return resultBuilder.build(); - } - Optional dataId = getDataId(context, phoneNumber); - Optional phoneAccountHandle = Optional.absent(); - if (dataId.isPresent()) { - resultBuilder.setDataId(dataId); - phoneAccountHandle = getPreferredAccount(context, dataId.get()); - } - resultBuilder.setPhoneAccountHandle(phoneAccountHandle); - Optional suggestion = Optional.absent(); - if (!phoneAccountHandle.isPresent()) { - suggestion = - SimSuggestionComponent.get(context) - .getSuggestionProvider() - .getSuggestion(context, phoneNumber); - resultBuilder.setSuggestion(suggestion); - } - return resultBuilder.build(); - } - - @WorkerThread - @NonNull - private static Optional getDataId( - @NonNull Context context, @Nullable String phoneNumber) { - Assert.isWorkerThread(); - if (TextUtils.isEmpty(phoneNumber)) { - return Optional.absent(); - } - try (Cursor cursor = - context - .getContentResolver() - .query( - Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber)), - new String[] {PhoneLookup.DATA_ID}, - null, - null, - null)) { - if (cursor == null) { - return Optional.absent(); - } - ImmutableSet validAccountTypes = PreferredAccountUtil.getValidAccountTypes(context); - String result = null; - while (cursor.moveToNext()) { - Optional accountType = - getAccountType(context.getContentResolver(), cursor.getLong(0)); - if (accountType.isPresent() && !validAccountTypes.contains(accountType.get())) { - // Empty accountType is treated as writable - LogUtil.i("CallingAccountSelector.getDataId", "ignoring non-writable " + accountType); - continue; - } - if (result != null && !result.equals(cursor.getString(0))) { - // TODO(twyen): if there are multiple entries attempt to grab from the contact that - // initiated the call. - LogUtil.i("CallingAccountSelector.getDataId", "lookup result not unique, ignoring"); - return Optional.absent(); - } - result = cursor.getString(0); - } - return Optional.fromNullable(result); - } - } + /** For implementations of {@link PreferredAccountWorker} only. */ + @AutoValue.Builder + public abstract static class Builder { - @WorkerThread - private static Optional getAccountType(ContentResolver contentResolver, long dataId) { - Assert.isWorkerThread(); - Optional rawContactId = getRawContactId(contentResolver, dataId); - if (!rawContactId.isPresent()) { - return Optional.absent(); - } - try (Cursor cursor = - contentResolver.query( - ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId.get()), - new String[] {RawContacts.ACCOUNT_TYPE}, - null, - null, - null)) { - if (cursor == null || !cursor.moveToFirst()) { - return Optional.absent(); - } - return Optional.fromNullable(cursor.getString(0)); - } - } + abstract Builder setSelectedPhoneAccountHandle(PhoneAccountHandle phoneAccountHandle); - @WorkerThread - private static Optional getRawContactId(ContentResolver contentResolver, long dataId) { - Assert.isWorkerThread(); - try (Cursor cursor = - contentResolver.query( - ContentUris.withAppendedId(Data.CONTENT_URI, dataId), - new String[] {Data.RAW_CONTACT_ID}, - null, - null, - null)) { - if (cursor == null || !cursor.moveToFirst()) { - return Optional.absent(); - } - return Optional.of(cursor.getLong(0)); - } - } + public abstract Builder setDataId(String dataId); - @WorkerThread - @NonNull - private static Optional getPreferredAccount( - @NonNull Context context, @NonNull String dataId) { - Assert.isWorkerThread(); - Assert.isNotNull(dataId); - try (Cursor cursor = - context - .getContentResolver() - .query( - PreferredSimFallbackContract.CONTENT_URI, - new String[] { - PreferredSim.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME, - PreferredSim.PREFERRED_PHONE_ACCOUNT_ID - }, - PreferredSim.DATA_ID + " = ?", - new String[] {dataId}, - null)) { - if (cursor == null) { - return Optional.absent(); - } - if (!cursor.moveToFirst()) { - return Optional.absent(); - } - return PreferredAccountUtil.getValidPhoneAccount( - context, cursor.getString(0), cursor.getString(1)); - } - } + abstract Builder setDialogOptionsBuilder( + SelectPhoneAccountDialogOptions.Builder optionsBuilder); - @WorkerThread - private static boolean isPreferredSimEnabled(Context context) { - Assert.isWorkerThread(); - if (!ConfigProviderBindings.get(context).getBoolean("preferred_sim_enabled", true)) { - return false; - } + public abstract Builder setSuggestion(Suggestion suggestion); - Intent quickContactIntent = getQuickContactIntent(); - ResolveInfo resolveInfo = - context - .getPackageManager() - .resolveActivity(quickContactIntent, PackageManager.GET_META_DATA); - if (resolveInfo == null - || resolveInfo.activityInfo == null - || resolveInfo.activityInfo.applicationInfo == null - || resolveInfo.activityInfo.applicationInfo.metaData == null) { - LogUtil.e("CallingAccountSelector.isPreferredSimEnabled", "cannot resolve quick contact app"); - return false; - } - if (!resolveInfo.activityInfo.applicationInfo.metaData.getBoolean( - METADATA_SUPPORTS_PREFERRED_SIM, false)) { - LogUtil.i( - "CallingAccountSelector.isPreferredSimEnabled", - "system contacts does not support preferred SIM"); - return false; + public abstract Result build(); } - return true; } - @VisibleForTesting - public static Intent getQuickContactIntent() { - Intent intent = new Intent(QuickContact.ACTION_QUICK_CONTACT); - intent.addCategory(Intent.CATEGORY_DEFAULT); - intent.setData(Contacts.CONTENT_URI.buildUpon().appendPath("1").build()); - return intent; - } + /** + * @return a {@link SelectPhoneAccountDialogOptions} for a dialog to select SIM for voicemail call + */ + SelectPhoneAccountDialogOptions getVoicemailDialogOptions(); + + /** + * Return {@link Result} for the best {@link PhoneAccountHandle} among {@code candidates} to call + * the number with. If none are eligible, a {@link SelectPhoneAccountDialogOptions} will be + * provided to show a dialog for the user to manually select. + */ + ListenableFuture selectAccount(String phoneNumber, List candidates); } diff --git a/java/com/android/dialer/preferredsim/PreferredSimComponent.java b/java/com/android/dialer/preferredsim/PreferredSimComponent.java new file mode 100644 index 000000000..b8d7fa199 --- /dev/null +++ b/java/com/android/dialer/preferredsim/PreferredSimComponent.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018 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.preferredsim; + +import android.content.Context; +import com.android.dialer.inject.HasRootComponent; +import dagger.Subcomponent; + +/** Component for preferred SIM */ +@Subcomponent +public abstract class PreferredSimComponent { + public abstract PreferredAccountWorker preferredAccountWorker(); + + public static PreferredSimComponent get(Context context) { + return ((HasComponent) ((HasRootComponent) context.getApplicationContext()).component()) + .preferredSimComponent(); + } + + /** Used to refer to the root application component. */ + public interface HasComponent { + PreferredSimComponent preferredSimComponent(); + } +} diff --git a/java/com/android/dialer/preferredsim/PreferredSimModule.java b/java/com/android/dialer/preferredsim/PreferredSimModule.java new file mode 100644 index 000000000..bdc6a5a13 --- /dev/null +++ b/java/com/android/dialer/preferredsim/PreferredSimModule.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2018 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.preferredsim; + +import com.android.dialer.preferredsim.impl.PreferredAccountWorkerImpl; +import dagger.Binds; +import dagger.Module; + +/** Module for preferred SIM */ +@Module +public abstract class PreferredSimModule { + + @Binds + public abstract PreferredAccountWorker to(PreferredAccountWorkerImpl impl); +} diff --git a/java/com/android/dialer/preferredsim/impl/AndroidManifest.xml b/java/com/android/dialer/preferredsim/impl/AndroidManifest.xml index e21598fc3..e6f932ef0 100644 --- a/java/com/android/dialer/preferredsim/impl/AndroidManifest.xml +++ b/java/com/android/dialer/preferredsim/impl/AndroidManifest.xml @@ -14,7 +14,7 @@ ~ limitations under the License --> + package="com.android.dialer.preferredsim.impl"> @@ -22,7 +22,7 @@ android:authorities="com.android.dialer.preferredsimfallback" android:exported="true" android:multiprocess="false" - android:name=".impl.PreferredSimFallbackProvider" + android:name=".PreferredSimFallbackProvider" android:readPermission="android.permission.READ_CONTACTS" android:writePermission="android.permission.WRITE_CONTACTS"/> diff --git a/java/com/android/dialer/preferredsim/impl/PreferredAccountWorkerImpl.java b/java/com/android/dialer/preferredsim/impl/PreferredAccountWorkerImpl.java new file mode 100644 index 000000000..086cd7a10 --- /dev/null +++ b/java/com/android/dialer/preferredsim/impl/PreferredAccountWorkerImpl.java @@ -0,0 +1,435 @@ +/* + * Copyright (C) 2018 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.preferredsim.impl; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.PhoneLookup; +import android.provider.ContactsContract.QuickContact; +import android.provider.ContactsContract.RawContacts; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.annotation.WorkerThread; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; +import android.text.TextUtils; +import com.android.contacts.common.widget.SelectPhoneAccountDialogOptions; +import com.android.contacts.common.widget.SelectPhoneAccountDialogOptionsUtil; +import com.android.dialer.activecalls.ActiveCallInfo; +import com.android.dialer.activecalls.ActiveCallsComponent; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; +import com.android.dialer.configprovider.ConfigProviderBindings; +import com.android.dialer.inject.ApplicationContext; +import com.android.dialer.logging.DialerImpression.Type; +import com.android.dialer.logging.Logger; +import com.android.dialer.preferredsim.PreferredAccountUtil; +import com.android.dialer.preferredsim.PreferredAccountWorker; +import com.android.dialer.preferredsim.PreferredAccountWorker.Result.Builder; +import com.android.dialer.preferredsim.PreferredSimFallbackContract; +import com.android.dialer.preferredsim.PreferredSimFallbackContract.PreferredSim; +import com.android.dialer.preferredsim.suggestion.SimSuggestionComponent; +import com.android.dialer.preferredsim.suggestion.SuggestionProvider; +import com.android.dialer.preferredsim.suggestion.SuggestionProvider.Suggestion; +import com.android.dialer.util.PermissionsUtil; +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import java.util.List; +import java.util.Objects; +import javax.inject.Inject; + +/** Implements {@link PreferredAccountWorker}. */ +@SuppressWarnings({"missingPermission", "Guava"}) +public class PreferredAccountWorkerImpl implements PreferredAccountWorker { + + private final Context appContext; + private final ListeningExecutorService backgroundExecutor; + + @VisibleForTesting + public static final String METADATA_SUPPORTS_PREFERRED_SIM = + "supports_per_number_preferred_account"; + + @Inject + public PreferredAccountWorkerImpl( + @ApplicationContext Context appContext, + @BackgroundExecutor ListeningExecutorService backgroundExecutor) { + this.appContext = appContext; + this.backgroundExecutor = backgroundExecutor; + } + + @Override + public SelectPhoneAccountDialogOptions getVoicemailDialogOptions() { + return SelectPhoneAccountDialogOptionsUtil.builderWithAccounts( + appContext.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts()) + .setTitle(R.string.pre_call_select_phone_account) + .setCanSetDefault(false) + .build(); + } + + @Override + public ListenableFuture selectAccount( + String phoneNumber, List candidates) { + return backgroundExecutor.submit(() -> doInBackground(phoneNumber, candidates)); + } + + private Result doInBackground(String phoneNumber, List candidates) { + + Optional dataId = getDataId(phoneNumber); + if (dataId.isPresent()) { + Optional preferred = getPreferredAccount(appContext, dataId.get()); + if (preferred.isPresent()) { + return usePreferredSim(preferred.get(), candidates, dataId.get()); + } + } + + PhoneAccountHandle defaultPhoneAccount = + appContext + .getSystemService(TelecomManager.class) + .getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL); + if (defaultPhoneAccount != null) { + return useDefaultSim(defaultPhoneAccount, candidates, dataId.orNull()); + } + + Optional suggestion = + SimSuggestionComponent.get(appContext) + .getSuggestionProvider() + .getSuggestion(appContext, phoneNumber); + if (suggestion.isPresent() && suggestion.get().shouldAutoSelect) { + return useSuggestedSim(suggestion.get(), candidates, dataId.orNull()); + } + + Builder resultBuilder = + Result.builder( + createDialogOptionsBuilder(candidates, dataId.orNull(), suggestion.orNull())); + if (suggestion.isPresent()) { + resultBuilder.setSuggestion(suggestion.get()); + } + if (dataId.isPresent()) { + resultBuilder.setDataId(dataId.get()); + } + return resultBuilder.build(); + } + + private Result usePreferredSim( + PhoneAccountHandle preferred, List candidates, String dataId) { + Builder resultBuilder; + if (isSelectable(preferred)) { + Logger.get(appContext).logImpression(Type.DUAL_SIM_SELECTION_PREFERRED_USED); + resultBuilder = Result.builder(preferred); + } else { + Logger.get(appContext).logImpression(Type.DUAL_SIM_SELECTION_PREFERRED_NOT_SELECTABLE); + LogUtil.i("CallingAccountSelector.usePreferredAccount", "preferred account not selectable"); + resultBuilder = Result.builder(createDialogOptionsBuilder(candidates, dataId, null)); + } + resultBuilder.setDataId(dataId); + return resultBuilder.build(); + } + + private Result useDefaultSim( + PhoneAccountHandle defaultPhoneAccount, + List candidates, + @Nullable String dataId) { + if (isSelectable(defaultPhoneAccount)) { + Logger.get(appContext).logImpression(Type.DUAL_SIM_SELECTION_GLOBAL_USED); + return Result.builder(defaultPhoneAccount).build(); + } else { + Logger.get(appContext).logImpression(Type.DUAL_SIM_SELECTION_GLOBAL_NOT_SELECTABLE); + LogUtil.i("CallingAccountSelector.usePreferredAccount", "global account not selectable"); + return Result.builder(createDialogOptionsBuilder(candidates, dataId, null)).build(); + } + } + + private Result useSuggestedSim( + Suggestion suggestion, List candidates, @Nullable String dataId) { + Builder resultBuilder; + PhoneAccountHandle suggestedPhoneAccount = suggestion.phoneAccountHandle; + if (isSelectable(suggestedPhoneAccount)) { + resultBuilder = Result.builder(suggestedPhoneAccount); + Logger.get(appContext).logImpression(Type.DUAL_SIM_SELECTION_SUGGESTION_AUTO_SELECTED); + } else { + Logger.get(appContext).logImpression(Type.DUAL_SIM_SELECTION_SUGGESTION_AUTO_NOT_SELECTABLE); + LogUtil.i("CallingAccountSelector.usePreferredAccount", "global account not selectable"); + resultBuilder = Result.builder(createDialogOptionsBuilder(candidates, dataId, suggestion)); + return resultBuilder.build(); + } + resultBuilder.setSuggestion(suggestion); + return resultBuilder.build(); + } + + SelectPhoneAccountDialogOptions.Builder createDialogOptionsBuilder( + List candidates, + @Nullable String dataId, + @Nullable Suggestion suggestion) { + Logger.get(appContext).logImpression(Type.DUAL_SIM_SELECTION_SHOWN); + if (dataId != null) { + Logger.get(appContext).logImpression(Type.DUAL_SIM_SELECTION_IN_CONTACTS); + } + if (suggestion != null) { + Logger.get(appContext).logImpression(Type.DUAL_SIM_SELECTION_SUGGESTION_AVAILABLE); + switch (suggestion.reason) { + case INTRA_CARRIER: + Logger.get(appContext).logImpression(Type.DUAL_SIM_SELECTION_SUGGESTED_CARRIER); + break; + case FREQUENT: + Logger.get(appContext).logImpression(Type.DUAL_SIM_SELECTION_SUGGESTED_FREQUENCY); + break; + default: + } + } + SelectPhoneAccountDialogOptions.Builder optionsBuilder = + SelectPhoneAccountDialogOptions.newBuilder() + .setTitle(R.string.pre_call_select_phone_account) + .setCanSetDefault(dataId != null) + .setSetDefaultLabel(R.string.pre_call_select_phone_account_remember); + + for (PhoneAccountHandle phoneAccountHandle : candidates) { + SelectPhoneAccountDialogOptions.Entry.Builder entryBuilder = + SelectPhoneAccountDialogOptions.Entry.newBuilder(); + SelectPhoneAccountDialogOptionsUtil.setPhoneAccountHandle(entryBuilder, phoneAccountHandle); + if (isSelectable(phoneAccountHandle)) { + Optional hint = + SuggestionProvider.getHint(appContext, phoneAccountHandle, suggestion); + if (hint.isPresent()) { + entryBuilder.setHint(hint.get()); + } + } else { + entryBuilder.setEnabled(false); + Optional activeCallLabel = getActiveCallLabel(); + if (activeCallLabel.isPresent()) { + entryBuilder.setHint( + appContext.getString( + R.string.pre_call_select_phone_account_hint_other_sim_in_use, + activeCallLabel.get())); + } + } + optionsBuilder.addEntries(entryBuilder); + } + + return optionsBuilder; + } + + @WorkerThread + @NonNull + private Optional getDataId(@Nullable String phoneNumber) { + Assert.isWorkerThread(); + + if (!isPreferredSimEnabled(appContext)) { + return Optional.absent(); + } + if (!PermissionsUtil.hasContactsReadPermissions(appContext)) { + LogUtil.i("PreferredAccountWorker.doInBackground", "missing READ_CONTACTS permission"); + return Optional.absent(); + } + + if (TextUtils.isEmpty(phoneNumber)) { + return Optional.absent(); + } + try (Cursor cursor = + appContext + .getContentResolver() + .query( + Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber)), + new String[] {PhoneLookup.DATA_ID}, + null, + null, + null)) { + if (cursor == null) { + return Optional.absent(); + } + ImmutableSet validAccountTypes = + PreferredAccountUtil.getValidAccountTypes(appContext); + String result = null; + while (cursor.moveToNext()) { + Optional accountType = + getAccountType(appContext.getContentResolver(), cursor.getLong(0)); + if (accountType.isPresent() && !validAccountTypes.contains(accountType.get())) { + // Empty accountType is treated as writable + LogUtil.i("CallingAccountSelector.getDataId", "ignoring non-writable " + accountType); + continue; + } + if (result != null && !result.equals(cursor.getString(0))) { + // TODO(twyen): if there are multiple entries attempt to grab from the contact that + // initiated the call. + LogUtil.i("CallingAccountSelector.getDataId", "lookup result not unique, ignoring"); + return Optional.absent(); + } + result = cursor.getString(0); + } + return Optional.fromNullable(result); + } + } + + @WorkerThread + private static Optional getAccountType(ContentResolver contentResolver, long dataId) { + Assert.isWorkerThread(); + Optional rawContactId = getRawContactId(contentResolver, dataId); + if (!rawContactId.isPresent()) { + return Optional.absent(); + } + try (Cursor cursor = + contentResolver.query( + ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId.get()), + new String[] {RawContacts.ACCOUNT_TYPE}, + null, + null, + null)) { + if (cursor == null || !cursor.moveToFirst()) { + return Optional.absent(); + } + return Optional.fromNullable(cursor.getString(0)); + } + } + + @WorkerThread + private static Optional getRawContactId(ContentResolver contentResolver, long dataId) { + Assert.isWorkerThread(); + try (Cursor cursor = + contentResolver.query( + ContentUris.withAppendedId(Data.CONTENT_URI, dataId), + new String[] {Data.RAW_CONTACT_ID}, + null, + null, + null)) { + if (cursor == null || !cursor.moveToFirst()) { + return Optional.absent(); + } + return Optional.of(cursor.getLong(0)); + } + } + + @WorkerThread + @NonNull + private static Optional getPreferredAccount( + @NonNull Context context, @NonNull String dataId) { + Assert.isWorkerThread(); + Assert.isNotNull(dataId); + try (Cursor cursor = + context + .getContentResolver() + .query( + PreferredSimFallbackContract.CONTENT_URI, + new String[] { + PreferredSim.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME, + PreferredSim.PREFERRED_PHONE_ACCOUNT_ID + }, + PreferredSim.DATA_ID + " = ?", + new String[] {dataId}, + null)) { + if (cursor == null) { + return Optional.absent(); + } + if (!cursor.moveToFirst()) { + return Optional.absent(); + } + return PreferredAccountUtil.getValidPhoneAccount( + context, cursor.getString(0), cursor.getString(1)); + } + } + + @WorkerThread + private static boolean isPreferredSimEnabled(Context context) { + Assert.isWorkerThread(); + if (!ConfigProviderBindings.get(context).getBoolean("preferred_sim_enabled", true)) { + return false; + } + + Intent quickContactIntent = getQuickContactIntent(); + ResolveInfo resolveInfo = + context + .getPackageManager() + .resolveActivity(quickContactIntent, PackageManager.GET_META_DATA); + if (resolveInfo == null + || resolveInfo.activityInfo == null + || resolveInfo.activityInfo.applicationInfo == null + || resolveInfo.activityInfo.applicationInfo.metaData == null) { + LogUtil.e("CallingAccountSelector.isPreferredSimEnabled", "cannot resolve quick contact app"); + return false; + } + if (!resolveInfo.activityInfo.applicationInfo.metaData.getBoolean( + METADATA_SUPPORTS_PREFERRED_SIM, false)) { + LogUtil.i( + "CallingAccountSelector.isPreferredSimEnabled", + "system contacts does not support preferred SIM"); + return false; + } + return true; + } + + @VisibleForTesting + public static Intent getQuickContactIntent() { + Intent intent = new Intent(QuickContact.ACTION_QUICK_CONTACT); + intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.setData(Contacts.CONTENT_URI.buildUpon().appendPath("1").build()); + return intent; + } + + /** + * Most devices are DSDS (dual SIM dual standby) which only one SIM can have active calls at a + * time. TODO(twyen): support other dual SIM modes when the API is exposed. + */ + private boolean isSelectable(PhoneAccountHandle phoneAccountHandle) { + ImmutableList activeCalls = + ActiveCallsComponent.get(appContext).activeCalls().getActiveCalls(); + if (activeCalls.isEmpty()) { + return true; + } + for (ActiveCallInfo activeCall : activeCalls) { + if (Objects.equals(phoneAccountHandle, activeCall.phoneAccountHandle().orNull())) { + return true; + } + } + return false; + } + + private Optional getActiveCallLabel() { + ImmutableList activeCalls = + ActiveCallsComponent.get(appContext).activeCalls().getActiveCalls(); + + if (activeCalls.isEmpty()) { + LogUtil.e("CallingAccountSelector.getActiveCallLabel", "active calls no longer exist"); + return Optional.absent(); + } + ActiveCallInfo activeCall = activeCalls.get(0); + if (!activeCall.phoneAccountHandle().isPresent()) { + LogUtil.e("CallingAccountSelector.getActiveCallLabel", "active call has no phone account"); + return Optional.absent(); + } + PhoneAccount phoneAccount = + appContext + .getSystemService(TelecomManager.class) + .getPhoneAccount(activeCall.phoneAccountHandle().get()); + if (phoneAccount == null) { + LogUtil.e("CallingAccountSelector.getActiveCallLabel", "phone account not found"); + return Optional.absent(); + } + return Optional.of(phoneAccount.getLabel().toString()); + } +} diff --git a/java/com/android/dialer/preferredsim/impl/res/values/strings.xml b/java/com/android/dialer/preferredsim/impl/res/values/strings.xml new file mode 100644 index 000000000..5e7ddd36c --- /dev/null +++ b/java/com/android/dialer/preferredsim/impl/res/values/strings.xml @@ -0,0 +1,33 @@ + + + + + Cannot make call without phone permission + + + Choose SIM for this call + + + Remember this choice + + + Not available while using %1$s + \ No newline at end of file diff --git a/java/com/android/incallui/ActiveCallsCallListListener.java b/java/com/android/incallui/ActiveCallsCallListListener.java index 3e4cb9375..d94db8836 100644 --- a/java/com/android/incallui/ActiveCallsCallListListener.java +++ b/java/com/android/incallui/ActiveCallsCallListListener.java @@ -49,10 +49,10 @@ public class ActiveCallsCallListListener implements CallList.Listener { public void onCallListChange(CallList callList) { ImmutableList.Builder activeCalls = ImmutableList.builder(); for (DialerCall call : callList.getAllCalls()) { - if (call.getState() != DialerCallState.DISCONNECTED) { + if (call.getState() != DialerCallState.DISCONNECTED && call.getAccountHandle() != null) { activeCalls.add( ActiveCallInfo.builder() - .setPhoneAccountHandle(Optional.fromNullable(call.getAccountHandle())) + .setPhoneAccountHandle(Optional.of(call.getAccountHandle())) .build()); } } diff --git a/java/com/android/incallui/InCallActivity.java b/java/com/android/incallui/InCallActivity.java index 0ee98c25c..5ac6b5029 100644 --- a/java/com/android/incallui/InCallActivity.java +++ b/java/com/android/incallui/InCallActivity.java @@ -55,8 +55,6 @@ import android.view.animation.AnimationUtils; import android.widget.CheckBox; import android.widget.Toast; import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment; -import com.android.contacts.common.widget.SelectPhoneAccountDialogOptions; -import com.android.contacts.common.widget.SelectPhoneAccountDialogOptionsUtil; import com.android.dialer.animation.AnimUtils; import com.android.dialer.animation.AnimationListenerAdapter; import com.android.dialer.common.Assert; @@ -65,7 +63,6 @@ import com.android.dialer.common.concurrent.DialerExecutorComponent; import com.android.dialer.common.concurrent.ThreadUtil; import com.android.dialer.common.concurrent.UiListener; import com.android.dialer.configprovider.ConfigProviderBindings; -import com.android.dialer.logging.DialerImpression.Type; import com.android.dialer.logging.Logger; import com.android.dialer.logging.ScreenEvent; import com.android.dialer.metrics.Metrics; @@ -73,7 +70,7 @@ import com.android.dialer.metrics.MetricsComponent; import com.android.dialer.preferredsim.PreferredAccountRecorder; import com.android.dialer.preferredsim.PreferredAccountWorker; import com.android.dialer.preferredsim.PreferredAccountWorker.Result; -import com.android.dialer.preferredsim.suggestion.SuggestionProvider; +import com.android.dialer.preferredsim.PreferredSimComponent; import com.android.dialer.util.ViewUtil; import com.android.incallui.answer.bindings.AnswerBindings; import com.android.incallui.answer.protocol.AnswerScreen; @@ -365,19 +362,18 @@ public class InCallActivity extends TransactionSafeFragmentActivity return false; } - ListenableFuture preferredAccountFuture = - DialerExecutorComponent.get(this) - .backgroundExecutor() - .submit( - () -> { - try { - return new PreferredAccountWorker(waitingForAccountCall.getNumber()) - .doInBackground(getApplicationContext()); - } catch (Throwable throwable) { - throw new Exception(throwable); - } - }); + PreferredAccountWorker preferredAccountWorker = + PreferredSimComponent.get(this).preferredAccountWorker(); + Bundle extras = waitingForAccountCall.getIntentExtras(); + List phoneAccountHandles = + extras == null + ? new ArrayList<>() + : extras.getParcelableArrayList(Call.AVAILABLE_PHONE_ACCOUNTS); + + ListenableFuture preferredAccountFuture = + preferredAccountWorker.selectAccount( + waitingForAccountCall.getNumber(), phoneAccountHandles); preferredAccountWorkerResultListener.listen( this, preferredAccountFuture, @@ -388,62 +384,22 @@ public class InCallActivity extends TransactionSafeFragmentActivity "activity ended before result returned"); return; } - if (result.getPhoneAccountHandle().isPresent()) { - Logger.get(this).logImpression(Type.DUAL_SIM_SELECTION_PREFERRED_USED); + String callId = waitingForAccountCall.getId(); + if (result.getSelectedPhoneAccountHandle().isPresent()) { selectPhoneAccountListener.onPhoneAccountSelected( - result.getPhoneAccountHandle().get(), false, waitingForAccountCall.getId()); + result.getSelectedPhoneAccountHandle().get(), false, callId); return; } - if (result.getSuggestion().isPresent()) { - LogUtil.i( - "CallingAccountSelector.processPreferredAccount", - "SIM suggested: " + result.getSuggestion().get().reason); - if (result.getSuggestion().get().shouldAutoSelect) { - Logger.get(this).logImpression(Type.DUAL_SIM_SELECTION_SUGGESTION_AUTO_SELECTED); - LogUtil.i( - "CallingAccountSelector.processPreferredAccount", "Auto selected suggestion"); - selectPhoneAccountListener.onPhoneAccountSelected( - result.getSuggestion().get().phoneAccountHandle, - false, - waitingForAccountCall.getId()); - return; - } - } - Bundle extras = waitingForAccountCall.getIntentExtras(); - List phoneAccountHandles = - extras == null - ? new ArrayList<>() - : extras.getParcelableArrayList(Call.AVAILABLE_PHONE_ACCOUNTS); waitingForAccountCall.setPreferredAccountRecorder( new PreferredAccountRecorder( waitingForAccountCall.getNumber(), result.getSuggestion().orNull(), result.getDataId().orNull())); - SelectPhoneAccountDialogOptions.Builder optionsBuilder = - SelectPhoneAccountDialogOptions.newBuilder() - .setTitle(R.string.select_phone_account_for_calls) - .setCanSetDefault(result.getDataId().isPresent()) - .setSetDefaultLabel(R.string.select_phone_account_for_calls_remember) - .setCallId(waitingForAccountCall.getId()); - - for (PhoneAccountHandle phoneAccountHandle : phoneAccountHandles) { - SelectPhoneAccountDialogOptions.Entry.Builder entryBuilder = - SelectPhoneAccountDialogOptions.Entry.newBuilder(); - SelectPhoneAccountDialogOptionsUtil.setPhoneAccountHandle( - entryBuilder, phoneAccountHandle); - Optional hint = - SuggestionProvider.getHint( - this, phoneAccountHandle, result.getSuggestion().orNull()); - if (hint.isPresent()) { - entryBuilder.setHint(hint.get()); - } - optionsBuilder.addEntries(entryBuilder); - } - selectPhoneAccountDialogFragment = SelectPhoneAccountDialogFragment.newInstance( - optionsBuilder.build(), selectPhoneAccountListener); + result.getDialogOptionsBuilder().get().setCallId(callId).build(), + selectPhoneAccountListener); selectPhoneAccountDialogFragment.show(getFragmentManager(), Tags.SELECT_ACCOUNT_FRAGMENT); }, throwable -> { -- cgit v1.2.3