From 00623aa60a7908b0709df38632cfa576cb15e33e Mon Sep 17 00:00:00 2001 From: twyen Date: Tue, 10 Oct 2017 12:15:08 -0700 Subject: Implement SIM swapping When the call is still ringing, a new button is added to allow to user to call with the other SIM. A background worker will be created to hang up the phone and redial with the other SIM. The in call UI will be prevented from ending during the process. Video: https://drive.google.com/a/google.com/file/d/0B2eYBUUznfyTSl9MdXQ0V1ZzQkE/view?usp=sharing UX has not been finalized, the icon and position are just placeholder. Bug: 64215256 Test: SwapSimWorkerTest PiperOrigin-RevId: 171715715 Change-Id: Idb3f486e9fc9a45d4c5e244af2d7d91b075bf0f2 --- java/com/android/dialer/telecom/TelecomUtil.java | 23 +++ java/com/android/incallui/CallButtonPresenter.java | 28 +++ .../incallui/callpending/CallPendingActivity.java | 3 + .../incallui/commontheme/res/values/strings.xml | 2 + .../incallui/incall/impl/ButtonChooserFactory.java | 1 + .../incallui/incall/impl/ButtonController.java | 17 ++ .../incallui/incall/impl/InCallFragment.java | 4 +- .../incallui/incall/impl/res/values/strings.xml | 4 + .../incallui/incall/protocol/InCallButtonIds.java | 4 +- .../incall/protocol/InCallButtonIdsExtension.java | 2 + .../incall/protocol/InCallButtonUiDelegate.java | 2 + .../android/incallui/multisim/SwapSimWorker.java | 202 +++++++++++++++++++++ 12 files changed, 290 insertions(+), 2 deletions(-) create mode 100644 java/com/android/incallui/multisim/SwapSimWorker.java diff --git a/java/com/android/dialer/telecom/TelecomUtil.java b/java/com/android/dialer/telecom/TelecomUtil.java index 8ff4b3967..3bf9b4666 100644 --- a/java/com/android/dialer/telecom/TelecomUtil.java +++ b/java/com/android/dialer/telecom/TelecomUtil.java @@ -17,6 +17,7 @@ package com.android.dialer.telecom; import android.Manifest; +import android.Manifest.permission; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -24,7 +25,9 @@ import android.net.Uri; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.provider.CallLog.Calls; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.RequiresPermission; import android.support.annotation.VisibleForTesting; import android.support.v4.content.ContextCompat; import android.telecom.PhoneAccount; @@ -234,6 +237,26 @@ public abstract class TelecomUtil { return instance.isDefaultDialer(context); } + /** @return the other SIM based PhoneAccountHandle that is not {@code currentAccount} */ + @Nullable + @RequiresPermission(permission.READ_PHONE_STATE) + @SuppressWarnings("MissingPermission") + public static PhoneAccountHandle getOtherAccount( + @NonNull Context context, @Nullable PhoneAccountHandle currentAccount) { + if (currentAccount == null) { + return null; + } + TelecomManager telecomManager = context.getSystemService(TelecomManager.class); + for (PhoneAccountHandle phoneAccountHandle : telecomManager.getCallCapablePhoneAccounts()) { + PhoneAccount phoneAccount = telecomManager.getPhoneAccount(phoneAccountHandle); + if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION) + && !phoneAccountHandle.equals(currentAccount)) { + return phoneAccountHandle; + } + } + return null; + } + /** Contains an implementation for {@link TelecomUtil} methods */ @VisibleForTesting() public static class TelecomUtilImpl { diff --git a/java/com/android/incallui/CallButtonPresenter.java b/java/com/android/incallui/CallButtonPresenter.java index bd5bb78c9..b3fb97fad 100644 --- a/java/com/android/incallui/CallButtonPresenter.java +++ b/java/com/android/incallui/CallButtonPresenter.java @@ -22,11 +22,14 @@ import android.os.Trace; import android.support.v4.app.Fragment; import android.support.v4.os.UserManagerCompat; import android.telecom.CallAudioState; +import android.telecom.PhoneAccountHandle; import com.android.contacts.common.compat.CallCompat; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.DialerExecutorComponent; import com.android.dialer.logging.DialerImpression; import com.android.dialer.logging.Logger; +import com.android.dialer.telecom.TelecomUtil; import com.android.incallui.InCallCameraManager.Listener; import com.android.incallui.InCallPresenter.CanAddCallListener; import com.android.incallui.InCallPresenter.InCallDetailsListener; @@ -42,6 +45,7 @@ import com.android.incallui.call.TelecomAdapter; import com.android.incallui.incall.protocol.InCallButtonIds; import com.android.incallui.incall.protocol.InCallButtonUi; import com.android.incallui.incall.protocol.InCallButtonUiDelegate; +import com.android.incallui.multisim.SwapSimWorker; import com.android.incallui.videotech.utils.VideoUtils; /** Logic for call buttons. */ @@ -63,6 +67,7 @@ public class CallButtonPresenter private boolean mAutomaticallyMuted = false; private boolean mPreviousMuteState = false; private boolean isInCallButtonUiReady; + private PhoneAccountHandle mOtherAccount; public CallButtonPresenter(Context context) { mContext = context.getApplicationContext(); @@ -310,6 +315,23 @@ public class CallButtonPresenter mInCallButtonUi.showAudioRouteSelector(); } + @Override + public void swapSimClicked() { + LogUtil.enterBlock("CallButtonPresenter.swapSimClicked"); + SwapSimWorker worker = + new SwapSimWorker( + getContext(), + mCall, + InCallPresenter.getInstance().getCallList(), + mOtherAccount, + InCallPresenter.getInstance().acquireInCallUiLock("swapSim")); + DialerExecutorComponent.get(getContext()) + .dialerExecutorFactory() + .createNonUiTaskBuilder(worker) + .build() + .executeParallel(null); + } + /** * Switches the camera between the front-facing and back-facing camera. * @@ -409,6 +431,7 @@ public class CallButtonPresenter * * @param call The active call. */ + @SuppressWarnings("MissingPermission") private void updateButtonsState(DialerCall call) { LogUtil.v("CallButtonPresenter.updateButtonsState", ""); final boolean isVideo = call.isVideoCall(); @@ -439,11 +462,15 @@ public class CallButtonPresenter && call.getState() != DialerCall.State.DIALING && call.getState() != DialerCall.State.CONNECTING; + mOtherAccount = TelecomUtil.getOtherAccount(getContext(), call.getAccountHandle()); + boolean showSwapSim = mOtherAccount != null && DialerCall.State.isDialing(call.getState()); + mInCallButtonUi.showButton(InCallButtonIds.BUTTON_AUDIO, true); mInCallButtonUi.showButton(InCallButtonIds.BUTTON_SWAP, showSwap); mInCallButtonUi.showButton(InCallButtonIds.BUTTON_HOLD, showHold); mInCallButtonUi.setHold(isCallOnHold); mInCallButtonUi.showButton(InCallButtonIds.BUTTON_MUTE, showMute); + mInCallButtonUi.showButton(InCallButtonIds.BUTTON_SWAP_SIM, showSwapSim); mInCallButtonUi.showButton(InCallButtonIds.BUTTON_ADD_CALL, true); mInCallButtonUi.enableButton(InCallButtonIds.BUTTON_ADD_CALL, showAddCall); mInCallButtonUi.showButton(InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO, showUpgradeToVideo); @@ -532,4 +559,5 @@ public class CallButtonPresenter } return null; } + } diff --git a/java/com/android/incallui/callpending/CallPendingActivity.java b/java/com/android/incallui/callpending/CallPendingActivity.java index 7fc4caf5a..47325d823 100644 --- a/java/com/android/incallui/callpending/CallPendingActivity.java +++ b/java/com/android/incallui/callpending/CallPendingActivity.java @@ -278,6 +278,9 @@ public class CallPendingActivity extends FragmentActivity @Override public void showAudioRouteSelector() {} + @Override + public void swapSimClicked() {} + @Override public Context getContext() { return CallPendingActivity.this; diff --git a/java/com/android/incallui/commontheme/res/values/strings.xml b/java/com/android/incallui/commontheme/res/values/strings.xml index 94a8c901b..f366a867f 100644 --- a/java/com/android/incallui/commontheme/res/values/strings.xml +++ b/java/com/android/incallui/commontheme/res/values/strings.xml @@ -24,6 +24,8 @@ Swap calls + Change SIM + Merge calls Handset earpiece diff --git a/java/com/android/incallui/incall/impl/ButtonChooserFactory.java b/java/com/android/incallui/incall/impl/ButtonChooserFactory.java index 0f4a95d38..0d0a93256 100644 --- a/java/com/android/incallui/incall/impl/ButtonChooserFactory.java +++ b/java/com/android/incallui/incall/impl/ButtonChooserFactory.java @@ -112,6 +112,7 @@ class ButtonChooserFactory { mapping.put(InCallButtonIds.BUTTON_AUDIO, MappingInfo.builder(2).build()); mapping.put(InCallButtonIds.BUTTON_MERGE, MappingInfo.builder(3).setSlotOrder(0).build()); mapping.put(InCallButtonIds.BUTTON_ADD_CALL, MappingInfo.builder(3).build()); + mapping.put(InCallButtonIds.BUTTON_SWAP_SIM, MappingInfo.builder(4).build()); return mapping; } } diff --git a/java/com/android/incallui/incall/impl/ButtonController.java b/java/com/android/incallui/incall/impl/ButtonController.java index b7a47f08e..cefbd723b 100644 --- a/java/com/android/incallui/incall/impl/ButtonController.java +++ b/java/com/android/incallui/incall/impl/ButtonController.java @@ -560,4 +560,21 @@ interface ButtonController { inCallScreenDelegate.onSecondaryInfoClicked(); } } + + class SwapSimButtonController extends SimpleNonCheckableButtonController { + + public SwapSimButtonController(InCallButtonUiDelegate delegate) { + super( + delegate, + InCallButtonIds.BUTTON_SWAP_SIM, + R.string.incall_content_description_swap_sim, + R.string.incall_label_swap_sim, + R.drawable.quantum_ic_swap_calls_white_36); + } + + @Override + public void onClick(View view) { + delegate.swapSimClicked(); + } + } } diff --git a/java/com/android/incallui/incall/impl/InCallFragment.java b/java/com/android/incallui/incall/impl/InCallFragment.java index 13175656d..f0504bc56 100644 --- a/java/com/android/incallui/incall/impl/InCallFragment.java +++ b/java/com/android/incallui/incall/impl/InCallFragment.java @@ -110,7 +110,8 @@ public class InCallFragment extends Fragment || id == InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO || id == InCallButtonIds.BUTTON_ADD_CALL || id == InCallButtonIds.BUTTON_MERGE - || id == InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE; + || id == InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE + || id == InCallButtonIds.BUTTON_SWAP_SIM; } @Override @@ -198,6 +199,7 @@ public class InCallFragment extends Fragment buttonControllers.add(new ButtonController.AddCallButtonController(inCallButtonUiDelegate)); buttonControllers.add(new ButtonController.SwapButtonController(inCallButtonUiDelegate)); buttonControllers.add(new ButtonController.MergeButtonController(inCallButtonUiDelegate)); + buttonControllers.add(new ButtonController.SwapSimButtonController(inCallButtonUiDelegate)); buttonControllers.add( new ButtonController.UpgradeToVideoButtonController(inCallButtonUiDelegate)); buttonControllers.add( diff --git a/java/com/android/incallui/incall/impl/res/values/strings.xml b/java/com/android/incallui/incall/impl/res/values/strings.xml index 2b30dfa53..d0217566a 100644 --- a/java/com/android/incallui/incall/impl/res/values/strings.xml +++ b/java/com/android/incallui/incall/impl/res/values/strings.xml @@ -61,6 +61,10 @@ Swap + + Change SIM + Note sent diff --git a/java/com/android/incallui/incall/protocol/InCallButtonIds.java b/java/com/android/incallui/incall/protocol/InCallButtonIds.java index 50ebc6413..3de533519 100644 --- a/java/com/android/incallui/incall/protocol/InCallButtonIds.java +++ b/java/com/android/incallui/incall/protocol/InCallButtonIds.java @@ -37,6 +37,7 @@ import java.lang.annotation.RetentionPolicy; InCallButtonIds.BUTTON_MANAGE_VIDEO_CONFERENCE, InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE, InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY, + InCallButtonIds.BUTTON_SWAP_SIM, InCallButtonIds.BUTTON_COUNT, }) public @interface InCallButtonIds { @@ -55,5 +56,6 @@ public @interface InCallButtonIds { int BUTTON_MANAGE_VIDEO_CONFERENCE = 11; int BUTTON_MANAGE_VOICE_CONFERENCE = 12; int BUTTON_SWITCH_TO_SECONDARY = 13; - int BUTTON_COUNT = 14; + int BUTTON_SWAP_SIM = 14; + int BUTTON_COUNT = 15; } diff --git a/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java b/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java index 6d802e346..db6e9009c 100644 --- a/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java +++ b/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java @@ -54,6 +54,8 @@ public class InCallButtonIdsExtension { return "MANAGE_VOICE_CONFERENCE"; } else if (id == InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) { return "SWITCH_TO_SECONDARY"; + } else if (id == InCallButtonIds.BUTTON_SWAP_SIM) { + return "SWAP_SIM"; } else { return "INVALID_BUTTON: " + id; } diff --git a/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java b/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java index e02ada96d..9f9c5fb03 100644 --- a/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java +++ b/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java @@ -63,5 +63,7 @@ public interface InCallButtonUiDelegate { void showAudioRouteSelector(); + void swapSimClicked(); + Context getContext(); } diff --git a/java/com/android/incallui/multisim/SwapSimWorker.java b/java/com/android/incallui/multisim/SwapSimWorker.java new file mode 100644 index 000000000..73c18c442 --- /dev/null +++ b/java/com/android/incallui/multisim/SwapSimWorker.java @@ -0,0 +1,202 @@ +/* + * 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.incallui.multisim; + +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +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.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; +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.ThreadUtil; +import com.android.dialer.util.PermissionsUtil; +import com.android.incallui.call.CallList; +import com.android.incallui.call.DialerCall; +import com.android.incallui.call.DialerCallListener; +import com.android.incallui.incalluilock.InCallUiLock; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Hangs up the current call and redial the call using the {@code otherAccount} instead. the in call + * ui will be prevented from closing until the process has finished. + */ +public class SwapSimWorker implements Worker, DialerCallListener, CallList.Listener { + + // Timeout waiting for the call to hangup or redial. + private static final int DEFAULT_TIMEOUT_MILLIS = 5_000; + + private final Context context; + private final DialerCall call; + private final CallList callList; + private final InCallUiLock inCallUiLock; + + private final CountDownLatch disconnectLatch = new CountDownLatch(1); + private final CountDownLatch dialingLatch = new CountDownLatch(1); + + private final PhoneAccountHandle otherAccount; + private final String number; + + private final int timeoutMillis; + + private CountDownLatch latchForTest; + + @MainThread + public SwapSimWorker( + Context context, + DialerCall call, + CallList callList, + PhoneAccountHandle otherAccount, + InCallUiLock lock) { + this(context, call, callList, otherAccount, lock, DEFAULT_TIMEOUT_MILLIS); + } + + @VisibleForTesting + SwapSimWorker( + Context context, + DialerCall call, + CallList callList, + PhoneAccountHandle otherAccount, + InCallUiLock lock, + int timeoutMillis) { + Assert.isMainThread(); + this.context = context; + this.call = call; + this.callList = callList; + this.otherAccount = otherAccount; + inCallUiLock = lock; + this.timeoutMillis = timeoutMillis; + number = call.getNumber(); + call.addListener(this); + call.disconnect(); + } + + @WorkerThread + @Nullable + @Override + @SuppressWarnings("MissingPermission") + public Void doInBackground(Void unused) { + try { + if (!PermissionsUtil.hasPhonePermissions(context)) { + LogUtil.e("SwapSimWorker.doInBackground", "missing phone permission"); + return null; + } + if (!disconnectLatch.await(timeoutMillis, TimeUnit.MILLISECONDS)) { + LogUtil.e("SwapSimWorker.doInBackground", "timeout waiting for call to disconnect"); + return null; + } + LogUtil.i("SwapSimWorker.doInBackground", "call disconnected, redialing"); + TelecomManager telecomManager = context.getSystemService(TelecomManager.class); + Bundle extras = new Bundle(); + extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, otherAccount); + callList.addListener(this); + telecomManager.placeCall(Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null), extras); + if (latchForTest != null) { + latchForTest.countDown(); + } + if (!dialingLatch.await(timeoutMillis, TimeUnit.MILLISECONDS)) { + LogUtil.e("SwapSimWorker.doInBackground", "timeout waiting for call to dial"); + } + return null; + } catch (InterruptedException e) { + LogUtil.e("SwapSimWorker.doInBackground", "interrupted", e); + Thread.currentThread().interrupt(); + return null; + } finally { + ThreadUtil.postOnUiThread( + () -> { + call.removeListener(this); + callList.removeListener(this); + inCallUiLock.release(); + }); + } + } + + @MainThread + @Override + public void onDialerCallDisconnect() { + disconnectLatch.countDown(); + } + + @Override + public void onCallListChange(CallList callList) { + if (callList.getOutgoingCall() != null) { + dialingLatch.countDown(); + } + } + + @VisibleForTesting + void setLatchForTest(CountDownLatch latch) { + latchForTest = latch; + } + + @Override + public void onDialerCallUpdate() {} + + @Override + public void onDialerCallChildNumberChange() {} + + @Override + public void onDialerCallLastForwardedNumberChange() {} + + @Override + public void onDialerCallUpgradeToVideo() {} + + @Override + public void onDialerCallSessionModificationStateChange() {} + + @Override + public void onWiFiToLteHandover() {} + + @Override + public void onHandoverToWifiFailure() {} + + @Override + public void onInternationalCallOnWifi() {} + + @Override + public void onEnrichedCallSessionUpdate() {} + + @Override + public void onIncomingCall(DialerCall call) {} + + @Override + public void onUpgradeToVideo(DialerCall call) {} + + @Override + public void onSessionModificationStateChange(DialerCall call) {} + + @Override + public void onDisconnect(DialerCall call) {} + + @Override + public void onWiFiToLteHandover(DialerCall call) {} + + @Override + public void onHandoverToWifiFailed(DialerCall call) {} + + @Override + public void onInternationalCallOnWifi(@NonNull DialerCall call) {} +} -- cgit v1.2.3