From a1c046f99a353985f41d2b0d9cecf31d53364ee6 Mon Sep 17 00:00:00 2001 From: twyen Date: Mon, 13 Nov 2017 14:57:58 -0800 Subject: Implement preferred SIM Before prompting the user to select the SIM, CallingAccountSelector will lookup the fallback preferred SIM database to see if a preferred SIM is already set and bypass the selection. If the number is in contacts the user will also have the option to store the selected SIM as preferred. Bug: 64213352 Test: CallingAccountSelectorTest PiperOrigin-RevId: 175592732 Change-Id: I6a5a8ad8772eccfb4a119c529dcd3945b9dc0b1e --- .../widget/SelectPhoneAccountDialogFragment.java | 5 + .../precall/impl/CallingAccountSelector.java | 241 +++++++++++++++++++-- .../dialer/precall/impl/PreferredAccountUtil.java | 94 ++++++++ .../dialer/simulator/impl/SimulatorMainMenu.java | 19 ++ 4 files changed, 345 insertions(+), 14 deletions(-) create mode 100644 java/com/android/dialer/precall/impl/PreferredAccountUtil.java (limited to 'java') diff --git a/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java b/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java index 6c6aebc0b..e21fded97 100644 --- a/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java +++ b/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java @@ -123,6 +123,11 @@ public class SelectPhoneAccountDialogFragment extends DialogFragment { return mListener; } + @VisibleForTesting + public boolean canSetDefault() { + return getArguments().getBoolean(ARG_CAN_SET_DEFAULT); + } + @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); diff --git a/java/com/android/dialer/precall/impl/CallingAccountSelector.java b/java/com/android/dialer/precall/impl/CallingAccountSelector.java index ca8798c5d..ca74bef08 100644 --- a/java/com/android/dialer/precall/impl/CallingAccountSelector.java +++ b/java/com/android/dialer/precall/impl/CallingAccountSelector.java @@ -17,10 +17,18 @@ package com.android.dialer.precall.impl; import android.app.Activity; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract.PhoneLookup; import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; +import android.support.annotation.WorkerThread; +import android.support.v4.util.ArraySet; import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; import android.telecom.TelecomManager; @@ -28,9 +36,17 @@ import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment; import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment.SelectPhoneAccountListener; import com.android.dialer.callintent.CallIntentBuilder; import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.DialerExecutor.Worker; +import com.android.dialer.common.concurrent.DialerExecutorComponent; import com.android.dialer.precall.PreCallAction; import com.android.dialer.precall.PreCallCoordinator; +import com.android.dialer.precall.PreCallCoordinator.PendingAction; +import com.android.dialer.preferredsim.PreferredSimFallbackContract; +import com.android.dialer.preferredsim.PreferredSimFallbackContract.PreferredSim; +import com.google.common.base.Optional; import java.util.List; +import java.util.Set; /** PreCallAction to select which phone account to call with. Ignored if there's only one account */ @SuppressWarnings("MissingPermission") @@ -43,6 +59,7 @@ public class CallingAccountSelector implements PreCallAction { private boolean isDiscarding; @Override + @MainThread public void run(PreCallCoordinator coordinator) { CallIntentBuilder builder = coordinator.getBuilder(); if (builder.getPhoneAccountHandle() != null) { @@ -54,43 +71,189 @@ public class CallingAccountSelector implements PreCallAction { if (accounts.size() <= 1) { return; } - boolean isVoicemail = builder.getUri().getScheme().equals(PhoneAccount.SCHEME_VOICEMAIL); - - if (!isVoicemail) { - PhoneAccountHandle defaultPhoneAccount = - telecomManager.getDefaultOutgoingPhoneAccount(builder.getUri().getScheme()); - if (defaultPhoneAccount != null) { - builder.setPhoneAccountHandle(defaultPhoneAccount); - return; - } + switch (builder.getUri().getScheme()) { + case PhoneAccount.SCHEME_VOICEMAIL: + showDialog(coordinator, coordinator.startPendingAction(), null); + break; + case PhoneAccount.SCHEME_TEL: + processPreferredAccount(coordinator); + break; + default: + // might be PhoneAccount.SCHEME_SIP + LogUtil.e( + "CallingAccountSelector.run", + "unable to process scheme " + builder.getUri().getScheme()); + break; } + } + /** Initiates a background worker to find if there's any preferred account. */ + @MainThread + private void processPreferredAccount(PreCallCoordinator coordinator) { + Assert.isMainThread(); + CallIntentBuilder builder = coordinator.getBuilder(); + Activity activity = coordinator.getActivity(); + String phoneNumber = builder.getUri().getSchemeSpecificPart(); + PendingAction pendingAction = coordinator.startPendingAction(); + DialerExecutorComponent.get(coordinator.getActivity()) + .dialerExecutorFactory() + .createUiTaskBuilder( + activity.getFragmentManager(), + "PreferredAccountWorker", + new PreferredAccountWorker(phoneNumber)) + .onSuccess( + (result -> { + if (result.phoneAccountHandle.isPresent()) { + coordinator.getBuilder().setPhoneAccountHandle(result.phoneAccountHandle.get()); + pendingAction.finish(); + return; + } + PhoneAccountHandle defaultPhoneAccount = + activity + .getSystemService(TelecomManager.class) + .getDefaultOutgoingPhoneAccount(builder.getUri().getScheme()); + if (defaultPhoneAccount != null) { + builder.setPhoneAccountHandle(defaultPhoneAccount); + pendingAction.finish(); + return; + } + showDialog(coordinator, pendingAction, result.dataId.orNull()); + })) + .build() + .executeParallel(activity); + } + + @MainThread + private void showDialog( + PreCallCoordinator coordinator, PendingAction pendingAction, @Nullable String dataId) { + Assert.isMainThread(); selectPhoneAccountDialogFragment = SelectPhoneAccountDialogFragment.newInstance( R.string.pre_call_select_phone_account, - false /* canSetDefault */, // TODO(twyen): per contact defaults - accounts, - new SelectedListener(coordinator, coordinator.startPendingAction()), + dataId != null /* canSetDefault */, + coordinator + .getActivity() + .getSystemService(TelecomManager.class) + .getCallCapablePhoneAccounts(), + new SelectedListener(coordinator, pendingAction, dataId), null /* call ID */); selectPhoneAccountDialogFragment.show( - activity.getFragmentManager(), TAG_CALLING_ACCOUNT_SELECTOR); + coordinator.getActivity().getFragmentManager(), TAG_CALLING_ACCOUNT_SELECTOR); } + @MainThread @Override public void onDiscard() { isDiscarding = true; selectPhoneAccountDialogFragment.dismiss(); } + private static class PreferredAccountWorkerResult { + + /** The preferred phone account for the number. Absent if not set or invalid. */ + Optional phoneAccountHandle = Optional.absent(); + + /** + * {@link android.provider.ContactsContract.Data#_ID} of the row matching the number. If the + * preferred account is to be set it should be stored in this row + */ + Optional dataId = Optional.absent(); + } + + private static class PreferredAccountWorker + implements Worker { + + private final String phoneNumber; + + public PreferredAccountWorker(String phoneNumber) { + this.phoneNumber = phoneNumber; + } + + @NonNull + @Override + @WorkerThread + public PreferredAccountWorkerResult doInBackground(Context context) throws Throwable { + PreferredAccountWorkerResult result = new PreferredAccountWorkerResult(); + result.dataId = getDataId(context.getContentResolver(), phoneNumber); + if (result.dataId.isPresent()) { + result.phoneAccountHandle = getPreferredAccount(context, result.dataId.get()); + } + return result; + } + } + + @WorkerThread + @NonNull + private static Optional getDataId( + @NonNull ContentResolver contentResolver, @Nullable String phoneNumber) { + Assert.isWorkerThread(); + try (Cursor cursor = + contentResolver.query( + Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber)), + new String[] {PhoneLookup.DATA_ID}, + null, + null, + null)) { + if (cursor == null) { + return Optional.absent(); + } + Set result = new ArraySet<>(); + while (cursor.moveToNext()) { + result.add(cursor.getString(0)); + } + // TODO(twyen): if there are multiples attempt to grab from the contact that initiated the + // call. + if (result.size() == 1) { + return Optional.of(result.iterator().next()); + } else { + LogUtil.i("CallingAccountSelector.getDataId", "lookup result not unique, ignoring"); + return Optional.absent(); + } + } + } + + @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)); + } + } + private class SelectedListener extends SelectPhoneAccountListener { private final PreCallCoordinator coordinator; private final PreCallCoordinator.PendingAction listener; + private final String dataId; public SelectedListener( - @NonNull PreCallCoordinator builder, @NonNull PreCallCoordinator.PendingAction listener) { + @NonNull PreCallCoordinator builder, + @NonNull PreCallCoordinator.PendingAction listener, + @Nullable String dataId) { this.coordinator = Assert.isNotNull(builder); this.listener = Assert.isNotNull(listener); + this.dataId = dataId; } @MainThread @@ -98,6 +261,17 @@ public class CallingAccountSelector implements PreCallAction { public void onPhoneAccountSelected( PhoneAccountHandle selectedAccountHandle, boolean setDefault, @Nullable String callId) { coordinator.getBuilder().setPhoneAccountHandle(selectedAccountHandle); + + if (dataId != null && setDefault) { + DialerExecutorComponent.get(coordinator.getActivity()) + .dialerExecutorFactory() + .createNonUiTaskBuilder(new WritePreferredAccountWorker()) + .build() + .executeParallel( + new WritePreferredAccountWorkerInput( + coordinator.getActivity(), dataId, selectedAccountHandle)); + } + listener.finish(); } @@ -111,4 +285,43 @@ public class CallingAccountSelector implements PreCallAction { listener.finish(); } } + + private static class WritePreferredAccountWorkerInput { + private final Context context; + private final String dataId; + private final PhoneAccountHandle phoneAccountHandle; + + WritePreferredAccountWorkerInput( + @NonNull Context context, + @NonNull String dataId, + @NonNull PhoneAccountHandle phoneAccountHandle) { + this.context = Assert.isNotNull(context); + this.dataId = Assert.isNotNull(dataId); + this.phoneAccountHandle = Assert.isNotNull(phoneAccountHandle); + } + } + + private static class WritePreferredAccountWorker + implements Worker { + + @Nullable + @Override + @WorkerThread + public Void doInBackground(WritePreferredAccountWorkerInput input) throws Throwable { + ContentValues values = new ContentValues(); + values.put( + PreferredSim.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME, + input.phoneAccountHandle.getComponentName().flattenToString()); + values.put(PreferredSim.PREFERRED_PHONE_ACCOUNT_ID, input.phoneAccountHandle.getId()); + input + .context + .getContentResolver() + .update( + PreferredSimFallbackContract.CONTENT_URI, + values, + PreferredSim.DATA_ID + " = ?", + new String[] {String.valueOf(input.dataId)}); + return null; + } + } } diff --git a/java/com/android/dialer/precall/impl/PreferredAccountUtil.java b/java/com/android/dialer/precall/impl/PreferredAccountUtil.java new file mode 100644 index 000000000..a41cb6e78 --- /dev/null +++ b/java/com/android/dialer/precall/impl/PreferredAccountUtil.java @@ -0,0 +1,94 @@ +/* + * 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 + */ + +package com.android.dialer.precall.impl; + +import android.content.ComponentName; +import android.content.Context; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import com.android.dialer.common.LogUtil; +import com.google.common.base.Optional; + +/** + * Utilities for looking up and validating preferred {@link PhoneAccountHandle}. Contacts should + * follow the same logic. + */ +public class PreferredAccountUtil { + + /** + * Validates {@code componentNameString} and {@code idString} maps to SIM that is present on the + * device. + */ + @NonNull + public static Optional getValidPhoneAccount( + @NonNull Context context, @Nullable String componentNameString, @Nullable String idString) { + if (TextUtils.isEmpty(componentNameString) || TextUtils.isEmpty(idString)) { + LogUtil.i("PreferredAccountUtil.getValidPhoneAccount", "empty componentName or id"); + return Optional.absent(); + } + ComponentName componentName = ComponentName.unflattenFromString(componentNameString); + if (componentName == null) { + LogUtil.e("PreferredAccountUtil.getValidPhoneAccount", "cannot parse component name"); + return Optional.absent(); + } + PhoneAccountHandle phoneAccountHandle = new PhoneAccountHandle(componentName, idString); + + if (isPhoneAccountValid(context, phoneAccountHandle)) { + return Optional.of(phoneAccountHandle); + } + return Optional.absent(); + } + + private static boolean isPhoneAccountValid( + Context context, PhoneAccountHandle phoneAccountHandle) { + if (VERSION.SDK_INT >= VERSION_CODES.O) { + return context + .getSystemService(TelephonyManager.class) + .createForPhoneAccountHandle(phoneAccountHandle) + != null; + } + + PhoneAccount phoneAccount = + context.getSystemService(TelecomManager.class).getPhoneAccount(phoneAccountHandle); + if (phoneAccount == null) { + LogUtil.e("PreferredAccountUtil.isPhoneAccountValid", "invalid phone account"); + return false; + } + + if (!phoneAccount.isEnabled()) { + LogUtil.e("PreferredAccountUtil.isPhoneAccountValid", "disabled phone account"); + return false; + } + for (SubscriptionInfo info : + SubscriptionManager.from(context).getActiveSubscriptionInfoList()) { + if (phoneAccountHandle.getId().startsWith(info.getIccId())) { + LogUtil.i("PreferredAccountUtil.isPhoneAccountValid", "sim found"); + return true; + } + } + return false; + } +} diff --git a/java/com/android/dialer/simulator/impl/SimulatorMainMenu.java b/java/com/android/dialer/simulator/impl/SimulatorMainMenu.java index f4b1916c5..e2082105b 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorMainMenu.java +++ b/java/com/android/dialer/simulator/impl/SimulatorMainMenu.java @@ -29,6 +29,7 @@ import com.android.dialer.databasepopulator.ContactsPopulator; import com.android.dialer.databasepopulator.VoicemailPopulator; import com.android.dialer.enrichedcall.simulator.EnrichedCallSimulatorActivity; import com.android.dialer.persistentlog.PersistentLogger; +import com.android.dialer.preferredsim.PreferredSimFallbackContract; /** Implements the top level simulator menu. */ final class SimulatorMainMenu { @@ -40,6 +41,7 @@ final class SimulatorMainMenu { .addItem("Notifications", SimulatorNotifications.getActionProvider(context)) .addItem("Populate database", () -> populateDatabase(context)) .addItem("Clean database", () -> cleanDatabase(context)) + .addItem("clear preferred SIM", () -> clearPreferredSim(context)) .addItem("Sync voicemail", () -> syncVoicemail(context)) .addItem("Share persistent log", () -> sharePersistentLog(context)) .addItem( @@ -63,6 +65,14 @@ final class SimulatorMainMenu { .executeSerial(context); } + private static void clearPreferredSim(Context context) { + DialerExecutorComponent.get(context) + .dialerExecutorFactory() + .createNonUiTaskBuilder(new ClearPreferredSimWorker()) + .build() + .executeSerial(context); + } + private static void syncVoicemail(@NonNull Context context) { Intent intent = new Intent(VoicemailContract.ACTION_SYNC_VOICEMAIL); context.sendBroadcast(intent); @@ -109,6 +119,15 @@ final class SimulatorMainMenu { } } + private static class ClearPreferredSimWorker implements Worker { + @Nullable + @Override + public Void doInBackground(Context context) { + context.getContentResolver().delete(PreferredSimFallbackContract.CONTENT_URI, null, null); + return null; + } + } + private static class ShareLogWorker implements Worker { @Nullable @Override -- cgit v1.2.3