diff options
18 files changed, 455 insertions, 35 deletions
diff --git a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java index adf7f1b4e..6067c4239 100644 --- a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java +++ b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java @@ -551,7 +551,7 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder if (mCallLogCache.isVoicemailNumber(accountHandle, number)) { // Call to generic voicemail number, in case there are multiple accounts primaryActionButtonView.setTag(IntentProvider.getReturnVoicemailCallIntentProvider()); - } else if (this.info != null && this.info.lookupKey != null) { + } else if (canSupportAssistedDialing()) { primaryActionButtonView.setTag( IntentProvider.getAssistedDialIntentProvider( number + postDialDigits, @@ -618,7 +618,7 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder ((TextView) callButtonView.findViewById(R.id.call_type_or_location_text)); if (canPlaceCallToNumber) { - if (this.info != null && this.info.lookupKey != null) { + if (canSupportAssistedDialing()) { callButtonView.setTag( IntentProvider.getAssistedDialIntentProvider( number, mContext, mContext.getSystemService(TelephonyManager.class))); @@ -685,7 +685,7 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder && mCachedNumberLookupService.canReportAsInvalid(info.sourceType, info.objectId); detailsButtonView.setTag( IntentProvider.getCallDetailIntentProvider( - callDetailsEntries, buildContact(), canReportCallerId)); + callDetailsEntries, buildContact(), canReportCallerId, canSupportAssistedDialing())); } boolean isBlockedOrSpam = blockId != null || (isSpamFeatureEnabled && isSpam); @@ -764,6 +764,10 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder return accountHandle.getComponentName().equals(mDefaultPhoneAccountHandle.getComponentName()); } + private boolean canSupportAssistedDialing() { + return info != null && info.lookupKey != null; + } + private boolean canSupportCarrierVideoCall() { return mCallLogCache.canRelyOnVideoPresence() && info != null diff --git a/java/com/android/dialer/app/calllog/IntentProvider.java b/java/com/android/dialer/app/calllog/IntentProvider.java index 52a7b0faf..5030de5f5 100644 --- a/java/com/android/dialer/app/calllog/IntentProvider.java +++ b/java/com/android/dialer/app/calllog/IntentProvider.java @@ -124,15 +124,20 @@ public abstract class IntentProvider { * * @param callDetailsEntries The call details of the other calls grouped together with the call. * @param contact The contact with which this call details intent pertains to. + * @param canReportCallerId Whether reporting a caller ID is supported. + * @param canSupportAssistedDialing Whether assisted dialing is supported. * @return The call details intent provider. */ public static IntentProvider getCallDetailIntentProvider( - CallDetailsEntries callDetailsEntries, DialerContact contact, boolean canReportCallerId) { + CallDetailsEntries callDetailsEntries, + DialerContact contact, + boolean canReportCallerId, + boolean canSupportAssistedDialing) { return new IntentProvider() { @Override public Intent getIntent(Context context) { return CallDetailsActivity.newInstance( - context, callDetailsEntries, contact, canReportCallerId); + context, callDetailsEntries, contact, canReportCallerId, canSupportAssistedDialing); } }; } diff --git a/java/com/android/dialer/calldetails/CallDetailsActivity.java b/java/com/android/dialer/calldetails/CallDetailsActivity.java index 569aaa5b5..d7f414bb2 100644 --- a/java/com/android/dialer/calldetails/CallDetailsActivity.java +++ b/java/com/android/dialer/calldetails/CallDetailsActivity.java @@ -16,6 +16,7 @@ package com.android.dialer.calldetails; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.os.AsyncTask; @@ -29,21 +30,30 @@ import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar.OnMenuItemClickListener; +import android.telephony.TelephonyManager; import android.view.MenuItem; +import android.widget.Toast; +import com.android.dialer.assisteddialing.ConcreteCreator; import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry; +import com.android.dialer.callintent.CallInitiationType; +import com.android.dialer.callintent.CallIntentBuilder; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.AsyncTaskExecutors; +import com.android.dialer.constants.ActivityRequestCodes; import com.android.dialer.dialercontact.DialerContact; import com.android.dialer.enrichedcall.EnrichedCallComponent; import com.android.dialer.enrichedcall.EnrichedCallManager.HistoricalDataChangedListener; import com.android.dialer.enrichedcall.historyquery.proto.HistoryResult; +import com.android.dialer.lightbringer.Lightbringer; +import com.android.dialer.lightbringer.LightbringerComponent; import com.android.dialer.logging.DialerImpression; import com.android.dialer.logging.Logger; import com.android.dialer.logging.UiAction; import com.android.dialer.performancereport.PerformanceReport; import com.android.dialer.postcall.PostCall; import com.android.dialer.protos.ProtoParsers; +import com.android.dialer.util.DialerUtils; import java.util.Collections; import java.util.List; import java.util.Map; @@ -51,6 +61,7 @@ import java.util.Map; /** Displays the details of a specific call log entry. */ public class CallDetailsActivity extends AppCompatActivity implements OnMenuItemClickListener, + CallDetailsHeaderViewHolder.CallbackActionListener, CallDetailsFooterViewHolder.ReportCallIdListener, HistoricalDataChangedListener { @@ -59,6 +70,7 @@ public class CallDetailsActivity extends AppCompatActivity private static final String EXTRA_CALL_DETAILS_ENTRIES = "call_details_entries"; private static final String EXTRA_CONTACT = "contact"; private static final String EXTRA_CAN_REPORT_CALLER_ID = "can_report_caller_id"; + private static final String EXTRA_CAN_SUPPORT_ASSISTED_DIALING = "can_support_assisted_dialing"; private static final String TASK_DELETE = "task_delete"; private CallDetailsEntries entries; @@ -74,7 +86,8 @@ public class CallDetailsActivity extends AppCompatActivity Context context, @NonNull CallDetailsEntries details, @NonNull DialerContact contact, - boolean canReportCallerId) { + boolean canReportCallerId, + boolean canSupportAssistedDialing) { Assert.isNotNull(details); Assert.isNotNull(contact); @@ -82,6 +95,7 @@ public class CallDetailsActivity extends AppCompatActivity ProtoParsers.put(intent, EXTRA_CONTACT, contact); ProtoParsers.put(intent, EXTRA_CALL_DETAILS_ENTRIES, details); intent.putExtra(EXTRA_CAN_REPORT_CALLER_ID, canReportCallerId); + intent.putExtra(EXTRA_CAN_SUPPORT_ASSISTED_DIALING, canSupportAssistedDialing); return intent; } @@ -142,7 +156,13 @@ public class CallDetailsActivity extends AppCompatActivity entries = ProtoParsers.getTrusted( intent, EXTRA_CALL_DETAILS_ENTRIES, CallDetailsEntries.getDefaultInstance()); - adapter = new CallDetailsAdapter(this, contact, entries.getEntriesList(), this); + adapter = + new CallDetailsAdapter( + this /* context */, + contact, + entries.getEntriesList(), + this /* callbackListener */, + this /* reportCallIdListener */); RecyclerView recyclerView = findViewById(R.id.recycler_view); recyclerView.setLayoutManager(new LinearLayoutManager(this)); @@ -188,6 +208,51 @@ public class CallDetailsActivity extends AppCompatActivity .getEntriesList()); } + @Override + public void placeImsVideoCall(String phoneNumber) { + Logger.get(this).logImpression(DialerImpression.Type.CALL_DETAILS_IMS_VIDEO_CALL_BACK); + DialerUtils.startActivityWithErrorToast( + this, + new CallIntentBuilder(phoneNumber, CallInitiationType.Type.CALL_DETAILS) + .setIsVideoCall(true) + .build()); + } + + @Override + public void placeLightbringerCall(String phoneNumber) { + Logger.get(this).logImpression(DialerImpression.Type.CALL_DETAILS_LIGHTBRINGER_CALL_BACK); + Lightbringer lightbringer = LightbringerComponent.get(this).getLightbringer(); + if (!lightbringer.isReachable(this, phoneNumber)) { + placeImsVideoCall(phoneNumber); + return; + } + + try { + startActivityForResult( + lightbringer.getIntent(this, phoneNumber), ActivityRequestCodes.DIALTACTS_LIGHTBRINGER); + } catch (ActivityNotFoundException e) { + Toast.makeText(this, R.string.activity_not_available, Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void placeVoiceCall(String phoneNumber, String postDialDigits) { + Logger.get(this).logImpression(DialerImpression.Type.CALL_DETAILS_VOICE_CALL_BACK); + + boolean canSupportedAssistedDialing = + getIntent().getExtras().getBoolean(EXTRA_CAN_SUPPORT_ASSISTED_DIALING, false); + CallIntentBuilder callIntentBuilder = + new CallIntentBuilder(phoneNumber + postDialDigits, CallInitiationType.Type.CALL_DETAILS); + if (canSupportedAssistedDialing) { + callIntentBuilder.setAllowAssistedDial( + true, + ConcreteCreator.createNewAssistedDialingMediator( + getSystemService(TelephonyManager.class), this)); + } + + DialerUtils.startActivityWithErrorToast(this, callIntentBuilder.build()); + } + @NonNull private Map<CallDetailsEntry, List<HistoryResult>> getAllHistoricalData( @Nullable String number, @NonNull CallDetailsEntries entries) { diff --git a/java/com/android/dialer/calldetails/CallDetailsAdapter.java b/java/com/android/dialer/calldetails/CallDetailsAdapter.java index 645587461..1f00d9d9a 100644 --- a/java/com/android/dialer/calldetails/CallDetailsAdapter.java +++ b/java/com/android/dialer/calldetails/CallDetailsAdapter.java @@ -23,7 +23,10 @@ import android.support.v7.widget.RecyclerView.ViewHolder; import android.view.LayoutInflater; import android.view.ViewGroup; import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry; +import com.android.dialer.calldetails.CallDetailsHeaderViewHolder.CallbackActionListener; import com.android.dialer.calllogutils.CallTypeHelper; +import com.android.dialer.calllogutils.CallbackActionHelper; +import com.android.dialer.calllogutils.CallbackActionHelper.CallbackAction; import com.android.dialer.common.Assert; import com.android.dialer.dialercontact.DialerContact; import com.android.dialer.lightbringer.LightbringerComponent; @@ -37,7 +40,8 @@ final class CallDetailsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol private static final int FOOTER_VIEW_TYPE = 3; private final DialerContact contact; - private final CallDetailsFooterViewHolder.ReportCallIdListener listener; + private final CallbackActionListener callbackActionListener; + private final CallDetailsFooterViewHolder.ReportCallIdListener reportCallIdListener; private final CallTypeHelper callTypeHelper; private List<CallDetailsEntry> callDetailsEntries; @@ -45,10 +49,12 @@ final class CallDetailsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol Context context, @NonNull DialerContact contact, @NonNull List<CallDetailsEntry> callDetailsEntries, - CallDetailsFooterViewHolder.ReportCallIdListener listener) { + CallbackActionListener callbackActionListener, + CallDetailsFooterViewHolder.ReportCallIdListener reportCallIdListener) { this.contact = Assert.isNotNull(contact); this.callDetailsEntries = callDetailsEntries; - this.listener = listener; + this.callbackActionListener = callbackActionListener; + this.reportCallIdListener = reportCallIdListener; callTypeHelper = new CallTypeHelper( context.getResources(), LightbringerComponent.get(context).getLightbringer()); @@ -60,13 +66,13 @@ final class CallDetailsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol switch (viewType) { case HEADER_VIEW_TYPE: return new CallDetailsHeaderViewHolder( - inflater.inflate(R.layout.contact_container, parent, false)); + inflater.inflate(R.layout.contact_container, parent, false), callbackActionListener); case CALL_ENTRY_VIEW_TYPE: return new CallDetailsEntryViewHolder( inflater.inflate(R.layout.call_details_entry, parent, false)); case FOOTER_VIEW_TYPE: return new CallDetailsFooterViewHolder( - inflater.inflate(R.layout.call_details_footer, parent, false), listener); + inflater.inflate(R.layout.call_details_footer, parent, false), reportCallIdListener); default: throw Assert.createIllegalStateFailException( "No ViewHolder available for viewType: " + viewType); @@ -76,7 +82,7 @@ final class CallDetailsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol @Override public void onBindViewHolder(ViewHolder holder, int position) { if (position == 0) { // Header - ((CallDetailsHeaderViewHolder) holder).updateContactInfo(contact); + ((CallDetailsHeaderViewHolder) holder).updateContactInfo(contact, getCallbackAction()); } else if (position == getItemCount() - 1) { ((CallDetailsFooterViewHolder) holder).setPhoneNumber(contact.getNumber()); } else { @@ -110,4 +116,12 @@ final class CallDetailsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol callDetailsEntries = entries; notifyDataSetChanged(); } + + private @CallbackAction int getCallbackAction() { + Assert.checkState(!callDetailsEntries.isEmpty()); + + CallDetailsEntry entry = callDetailsEntries.get(0); + return CallbackActionHelper.getCallbackAction( + contact.getNumber(), entry.getFeatures(), entry.getIsLightbringerCall()); + } } diff --git a/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java b/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java index dcd8e3537..7c3892a33 100644 --- a/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java +++ b/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java @@ -23,23 +23,22 @@ import android.telecom.PhoneAccount; import android.text.TextUtils; import android.view.View; import android.view.View.OnClickListener; +import android.widget.ImageView; import android.widget.QuickContactBadge; import android.widget.TextView; -import com.android.dialer.callintent.CallInitiationType; -import com.android.dialer.callintent.CallIntentBuilder; +import com.android.dialer.calllogutils.CallbackActionHelper.CallbackAction; import com.android.dialer.common.Assert; import com.android.dialer.contactphoto.ContactPhotoManager; import com.android.dialer.dialercontact.DialerContact; -import com.android.dialer.logging.DialerImpression; import com.android.dialer.logging.InteractionEvent; import com.android.dialer.logging.Logger; -import com.android.dialer.util.DialerUtils; /** ViewHolder for Header/Contact in {@link CallDetailsActivity}. */ public class CallDetailsHeaderViewHolder extends RecyclerView.ViewHolder implements OnClickListener { - private final View callBackButton; + private final CallbackActionListener callbackActionListener; + private final ImageView callbackButton; private final TextView nameView; private final TextView numberView; private final TextView networkView; @@ -47,24 +46,26 @@ public class CallDetailsHeaderViewHolder extends RecyclerView.ViewHolder private final Context context; private DialerContact contact; + private @CallbackAction int callbackAction; - CallDetailsHeaderViewHolder(View container) { + CallDetailsHeaderViewHolder(View container, CallbackActionListener callbackActionListener) { super(container); context = container.getContext(); - callBackButton = container.findViewById(R.id.call_back_button); + callbackButton = container.findViewById(R.id.call_back_button); nameView = container.findViewById(R.id.contact_name); numberView = container.findViewById(R.id.phone_number); networkView = container.findViewById(R.id.network); contactPhoto = container.findViewById(R.id.quick_contact_photo); - callBackButton.setOnClickListener(this); + callbackButton.setOnClickListener(this); + this.callbackActionListener = callbackActionListener; Logger.get(context) .logQuickContactOnTouch( contactPhoto, InteractionEvent.Type.OPEN_QUICK_CONTACT_FROM_CALL_DETAILS, true); } /** Populates the contact info fields based on the current contact information. */ - void updateContactInfo(DialerContact contact) { + void updateContactInfo(DialerContact contact, @CallbackAction int callbackAction) { this.contact = contact; ContactPhotoManager.getInstance(context) .loadDialerThumbnailOrPhoto( @@ -99,23 +100,61 @@ public class CallDetailsHeaderViewHolder extends RecyclerView.ViewHolder } } - if (TextUtils.isEmpty(contact.getNumber())) { - callBackButton.setVisibility(View.GONE); + setCallbackAction(callbackAction); + } + + private void setCallbackAction(@CallbackAction int callbackAction) { + this.callbackAction = callbackAction; + switch (callbackAction) { + case CallbackAction.LIGHTBRINGER: + case CallbackAction.IMS_VIDEO: + callbackButton.setVisibility(View.VISIBLE); + callbackButton.setImageResource(R.drawable.quantum_ic_videocam_vd_theme_24); + break; + case CallbackAction.VOICE: + callbackButton.setVisibility(View.VISIBLE); + callbackButton.setImageResource(R.drawable.quantum_ic_call_vd_theme_24); + break; + case CallbackAction.NONE: + callbackButton.setVisibility(View.GONE); + break; + default: + throw Assert.createIllegalStateFailException("Invalid action: " + callbackAction); } } @Override public void onClick(View view) { - if (view == callBackButton) { - Logger.get(view.getContext()).logImpression(DialerImpression.Type.CALL_DETAILS_CALL_BACK); - DialerUtils.startActivityWithErrorToast( - view.getContext(), - new CallIntentBuilder( - contact.getNumber() + contact.getPostDialDigits(), - CallInitiationType.Type.CALL_DETAILS) - .build()); + if (view == callbackButton) { + switch (callbackAction) { + case CallbackAction.IMS_VIDEO: + callbackActionListener.placeImsVideoCall(contact.getNumber()); + break; + case CallbackAction.LIGHTBRINGER: + callbackActionListener.placeLightbringerCall(contact.getNumber()); + break; + case CallbackAction.VOICE: + callbackActionListener.placeVoiceCall(contact.getNumber(), contact.getPostDialDigits()); + break; + case CallbackAction.NONE: + default: + throw Assert.createIllegalStateFailException("Invalid action: " + callbackAction); + } } else { throw Assert.createIllegalStateFailException("View OnClickListener not implemented: " + view); } } + + /** Listener for making a callback */ + interface CallbackActionListener { + + /** Places an IMS video call. */ + void placeImsVideoCall(String phoneNumber); + + /** Places a Lightbringer call. */ + void placeLightbringerCall(String phoneNumber); + + /** Place a traditional voice call. */ + void placeVoiceCall(String phoneNumber, String postDialDigits); + } } diff --git a/java/com/android/dialer/logging/dialer_impression.proto b/java/com/android/dialer/logging/dialer_impression.proto index fe1c5e9ba..098391877 100644 --- a/java/com/android/dialer/logging/dialer_impression.proto +++ b/java/com/android/dialer/logging/dialer_impression.proto @@ -323,7 +323,7 @@ message DialerImpression { CALL_DETAILS_COPY_NUMBER = 1121; CALL_DETAILS_EDIT_BEFORE_CALL = 1122; - CALL_DETAILS_CALL_BACK = 1123; + CALL_DETAILS_CALL_BACK = 1123 [deprecated = true]; VVM_USER_DISMISSED_VM_ALMOST_FULL_PROMO = 1124; VVM_USER_DISMISSED_VM_FULL_PROMO = 1125; @@ -554,5 +554,10 @@ message DialerImpression { // More voicemail transcription impressions VVM_TRANSCRIPTION_POLL_REQUEST = 1283; + + // Callback impressions (with more granularity) from the call details UI + CALL_DETAILS_IMS_VIDEO_CALL_BACK = 1284; + CALL_DETAILS_LIGHTBRINGER_CALL_BACK = 1285; + CALL_DETAILS_VOICE_CALL_BACK = 1286; } } 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 @@ -279,6 +279,9 @@ public class CallPendingActivity extends FragmentActivity 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 @@ <string name="incall_content_description_swap_calls">Swap calls</string> + <string name="incall_content_description_swap_sim">Change SIM</string> + <string name="incall_content_description_merge_calls">Merge calls</string> <string name="incall_content_description_earpiece">Handset earpiece</string> 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 @@ <string name="incall_label_swap">Swap</string> + <!-- Button shown during a call with a multi-SIM device to hang up and call with the other SIM + instead. [CHAR LIMIT=12] --> + <string name="incall_label_swap_sim">Change SIM</string> + <!-- Used to inform the user that the note associated with an outgoing call has been sent. [CHAR LIMIT=32] --> <string name="incall_note_sent">Note sent</string> 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<Void, Void>, 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) {} +} |