summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/preferredsim
diff options
context:
space:
mode:
authortwyen <twyen@google.com>2018-04-30 14:25:46 -0700
committerCopybara-Service <copybara-piper@google.com>2018-04-30 18:18:34 -0700
commit56f79ba6c6608f7041f5e65866b7164499ca7676 (patch)
tree7699f1e6bd1b2180d70144327d68f6df94dd3265 /java/com/android/dialer/preferredsim
parent90a68377c36abccde8c4f1e5d3aa781944a1413c (diff)
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
Diffstat (limited to 'java/com/android/dialer/preferredsim')
-rw-r--r--java/com/android/dialer/preferredsim/PreferredAccountUtil.java5
-rw-r--r--java/com/android/dialer/preferredsim/PreferredAccountWorker.java269
-rw-r--r--java/com/android/dialer/preferredsim/PreferredSimComponent.java37
-rw-r--r--java/com/android/dialer/preferredsim/PreferredSimModule.java29
-rw-r--r--java/com/android/dialer/preferredsim/impl/AndroidManifest.xml4
-rw-r--r--java/com/android/dialer/preferredsim/impl/PreferredAccountWorkerImpl.java435
-rw-r--r--java/com/android/dialer/preferredsim/impl/res/values/strings.xml33
7 files changed, 584 insertions, 228 deletions
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<String> 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<Context, Result> {
+@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<PhoneAccountHandle> 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<PhoneAccountHandle> 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<SelectPhoneAccountDialogOptions.Builder> getDialogOptionsBuilder();
/**
* {@link android.provider.ContactsContract.Data#_ID} of the row matching the number. If the
@@ -66,207 +54,42 @@ public class PreferredAccountWorker implements Worker<Context, Result> {
public abstract Optional<Suggestion> 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> phoneAccountHandle);
-
- public abstract Builder setDataId(Optional<String> dataId);
-
- public abstract Builder setSuggestion(Optional<Suggestion> 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<String> dataId = getDataId(context, phoneNumber);
- Optional<PhoneAccountHandle> phoneAccountHandle = Optional.absent();
- if (dataId.isPresent()) {
- resultBuilder.setDataId(dataId);
- phoneAccountHandle = getPreferredAccount(context, dataId.get());
- }
- resultBuilder.setPhoneAccountHandle(phoneAccountHandle);
- Optional<Suggestion> 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<String> 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<String> validAccountTypes = PreferredAccountUtil.getValidAccountTypes(context);
- String result = null;
- while (cursor.moveToNext()) {
- Optional<String> 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<String> getAccountType(ContentResolver contentResolver, long dataId) {
- Assert.isWorkerThread();
- Optional<Long> 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<Long> 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<PhoneAccountHandle> 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<Result> selectAccount(String phoneNumber, List<PhoneAccountHandle> 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
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.dialer.preferredsim">
+ package="com.android.dialer.preferredsim.impl">
<application>
@@ -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<Result> selectAccount(
+ String phoneNumber, List<PhoneAccountHandle> candidates) {
+ return backgroundExecutor.submit(() -> doInBackground(phoneNumber, candidates));
+ }
+
+ private Result doInBackground(String phoneNumber, List<PhoneAccountHandle> candidates) {
+
+ Optional<String> dataId = getDataId(phoneNumber);
+ if (dataId.isPresent()) {
+ Optional<PhoneAccountHandle> 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> 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<PhoneAccountHandle> 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<PhoneAccountHandle> 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<PhoneAccountHandle> 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<PhoneAccountHandle> 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<String> hint =
+ SuggestionProvider.getHint(appContext, phoneAccountHandle, suggestion);
+ if (hint.isPresent()) {
+ entryBuilder.setHint(hint.get());
+ }
+ } else {
+ entryBuilder.setEnabled(false);
+ Optional<String> 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<String> 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<String> validAccountTypes =
+ PreferredAccountUtil.getValidAccountTypes(appContext);
+ String result = null;
+ while (cursor.moveToNext()) {
+ Optional<String> 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<String> getAccountType(ContentResolver contentResolver, long dataId) {
+ Assert.isWorkerThread();
+ Optional<Long> 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<Long> 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<PhoneAccountHandle> 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<ActiveCallInfo> 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<String> getActiveCallLabel() {
+ ImmutableList<ActiveCallInfo> 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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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
+ -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Toast when the user tried to place a call but has revoked phone permission [CHAR_LIMIT=none] -->
+ <string name="pre_call_permission_check_no_phone_permission">Cannot make call without phone permission</string>
+
+ <!-- Title of the dialog to select which SIM to call with before making a call, if the device has
+ multiple SIMs [CHAR LIMIT=40]-->
+ <string name="pre_call_select_phone_account">Choose SIM for this call</string>
+
+ <!-- Checkbox label when selecting a SIM when calling a contact, to use the selected SIM for the
+ same contact and never ask again [CHAR LIMIT=40]-->
+ <string name="pre_call_select_phone_account_remember">Remember this choice</string>
+
+ <!-- Hint to show under a SIM entry when selecting SIM for call on a multi-SIM device, when the
+ call cannot be placed with the SIM because there is already a call on the other SIM,
+ [CHAR LIMIT=NONE]-->
+ <string name="pre_call_select_phone_account_hint_other_sim_in_use">Not available while using <xliff:g example="SIM 1" id="other_sim">%1$s</xliff:g></string>
+</resources> \ No newline at end of file