summaryrefslogtreecommitdiff
path: root/java/com/android/incallui/ReturnToCallController.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/incallui/ReturnToCallController.java')
-rw-r--r--java/com/android/incallui/ReturnToCallController.java382
1 files changed, 382 insertions, 0 deletions
diff --git a/java/com/android/incallui/ReturnToCallController.java b/java/com/android/incallui/ReturnToCallController.java
new file mode 100644
index 000000000..6227c77bd
--- /dev/null
+++ b/java/com/android/incallui/ReturnToCallController.java
@@ -0,0 +1,382 @@
+/*
+ * 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.incallui;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.provider.Settings;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.telecom.CallAudioState;
+import android.text.TextUtils;
+import com.android.bubble.Bubble;
+import com.android.bubble.BubbleComponent;
+import com.android.bubble.BubbleInfo;
+import com.android.bubble.BubbleInfo.Action;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.configprovider.ConfigProviderBindings;
+import com.android.dialer.lettertile.LetterTileDrawable;
+import com.android.dialer.telecom.TelecomUtil;
+import com.android.incallui.ContactInfoCache.ContactCacheEntry;
+import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
+import com.android.incallui.InCallPresenter.InCallUiListener;
+import com.android.incallui.audiomode.AudioModeProvider;
+import com.android.incallui.audiomode.AudioModeProvider.AudioModeListener;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.CallList.Listener;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.speakerbuttonlogic.SpeakerButtonInfo;
+import com.android.incallui.speakerbuttonlogic.SpeakerButtonInfo.IconSize;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Listens for events relevant to the return-to-call bubble and updates the bubble's state as
+ * necessary.
+ *
+ * <p>Bubble shows when one of following happens: 1. a new outgoing/ongoing call appears 2. leave
+ * in-call UI with an outgoing/ongoing call
+ *
+ * <p>Bubble hides when one of following happens: 1. a call disconnect and there is no more
+ * outgoing/ongoing call 2. show in-call UI
+ */
+public class ReturnToCallController implements InCallUiListener, Listener, AudioModeListener {
+
+ private final Context context;
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ Bubble bubble;
+
+ private static Boolean canShowBubblesForTesting = null;
+
+ private CallAudioState audioState;
+
+ private final PendingIntent toggleSpeaker;
+ private final PendingIntent showSpeakerSelect;
+ private final PendingIntent toggleMute;
+ private final PendingIntent endCall;
+ private final PendingIntent fullScreen;
+
+ private final ContactInfoCache contactInfoCache;
+
+ public static boolean isEnabled(Context context) {
+ return ConfigProviderBindings.get(context).getBoolean("enable_return_to_call_bubble_v2", false);
+ }
+
+ public ReturnToCallController(Context context, ContactInfoCache contactInfoCache) {
+ this.context = context;
+ this.contactInfoCache = contactInfoCache;
+
+ toggleSpeaker = createActionIntent(ReturnToCallActionReceiver.ACTION_TOGGLE_SPEAKER);
+ showSpeakerSelect =
+ createActionIntent(ReturnToCallActionReceiver.ACTION_SHOW_AUDIO_ROUTE_SELECTOR);
+ toggleMute = createActionIntent(ReturnToCallActionReceiver.ACTION_TOGGLE_MUTE);
+ endCall = createActionIntent(ReturnToCallActionReceiver.ACTION_END_CALL);
+ fullScreen = createActionIntent(ReturnToCallActionReceiver.ACTION_RETURN_TO_CALL);
+
+ InCallPresenter.getInstance().addInCallUiListener(this);
+ CallList.getInstance().addListener(this);
+ AudioModeProvider.getInstance().addListener(this);
+ audioState = AudioModeProvider.getInstance().getAudioState();
+ }
+
+ public void tearDown() {
+ hide();
+ InCallPresenter.getInstance().removeInCallUiListener(this);
+ CallList.getInstance().removeListener(this);
+ AudioModeProvider.getInstance().removeListener(this);
+ }
+
+ @Override
+ public void onUiShowing(boolean showing) {
+ LogUtil.i("ReturnToCallController.onUiShowing", "showing: " + showing);
+ if (showing) {
+ LogUtil.i("ReturnToCallController.onUiShowing", "going to hide");
+ hide();
+ } else {
+ if (getCall() != null) {
+ LogUtil.i("ReturnToCallController.onUiShowing", "going to show");
+ show();
+ }
+ }
+ }
+
+ private void hide() {
+ if (bubble != null) {
+ bubble.hide();
+ } else {
+ LogUtil.i("ReturnToCallController.hide", "hide() called without calling show()");
+ }
+ }
+
+ private void show() {
+ if (bubble == null) {
+ bubble = startBubble();
+ } else {
+ bubble.show();
+ }
+ startContactInfoSearch();
+ }
+
+ /**
+ * Determines whether bubbles can be shown based on permissions obtained. This should be checked
+ * before attempting to create a Bubble.
+ *
+ * @return true iff bubbles are able to be shown.
+ * @see Settings#canDrawOverlays(Context)
+ */
+ private static boolean canShowBubbles(@NonNull Context context) {
+ return canShowBubblesForTesting != null
+ ? canShowBubblesForTesting
+ : Settings.canDrawOverlays(context);
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ static void setCanShowBubblesForTesting(boolean canShowBubbles) {
+ canShowBubblesForTesting = canShowBubbles;
+ }
+
+ @VisibleForTesting
+ public Bubble startBubble() {
+ if (!canShowBubbles(context)) {
+ LogUtil.i("ReturnToCallController.startBubble", "can't show bubble, no permission");
+ return null;
+ }
+ Bubble returnToCallBubble = BubbleComponent.get(context).getBubble();
+ returnToCallBubble.setBubbleInfo(generateBubbleInfo());
+ returnToCallBubble.show();
+ return returnToCallBubble;
+ }
+
+ @Override
+ public void onIncomingCall(DialerCall call) {}
+
+ @Override
+ public void onUpgradeToVideo(DialerCall call) {}
+
+ @Override
+ public void onSessionModificationStateChange(DialerCall call) {}
+
+ @Override
+ public void onCallListChange(CallList callList) {
+ if ((bubble == null || !bubble.isVisible())
+ && getCall() != null
+ && !InCallPresenter.getInstance().isShowingInCallUi()) {
+ LogUtil.i("ReturnToCallController.onCallListChange", "going to show bubble");
+ show();
+ }
+ }
+
+ @Override
+ public void onDisconnect(DialerCall call) {
+ LogUtil.enterBlock("ReturnToCallController.onDisconnect");
+ if (bubble != null && bubble.isVisible() && (getCall() == null)) {
+ // Show "Call ended" and hide bubble when there is no outgoing, active or background call
+ LogUtil.i("ReturnToCallController.onDisconnect", "show call ended and hide bubble");
+ // Don't show text if it's Duo upgrade
+ // It doesn't work for Duo fallback upgrade since we're not considered in call
+ if (!TelecomUtil.isInCall(context) || CallList.getInstance().getIncomingCall() != null) {
+ bubble.showText(context.getText(R.string.incall_call_ended));
+ }
+ hide();
+ } else {
+ startContactInfoSearch();
+ }
+ }
+
+ @Override
+ public void onWiFiToLteHandover(DialerCall call) {}
+
+ @Override
+ public void onHandoverToWifiFailed(DialerCall call) {}
+
+ @Override
+ public void onInternationalCallOnWifi(@NonNull DialerCall call) {}
+
+ @Override
+ public void onAudioStateChanged(CallAudioState audioState) {
+ this.audioState = audioState;
+ if (bubble != null) {
+ bubble.updateActions(generateActions());
+ }
+ }
+
+ private void startContactInfoSearch() {
+ DialerCall dialerCall = getCall();
+ if (dialerCall != null) {
+ contactInfoCache.findInfo(
+ dialerCall, false /* isIncoming */, new ReturnToCallContactInfoCacheCallback(this));
+ }
+ }
+
+ private DialerCall getCall() {
+ DialerCall dialerCall = CallList.getInstance().getOutgoingCall();
+ if (dialerCall == null) {
+ dialerCall = CallList.getInstance().getActiveOrBackgroundCall();
+ }
+ return dialerCall;
+ }
+
+ private void onPhotoAvatarReceived(@NonNull Drawable photo) {
+ if (bubble != null) {
+ bubble.updatePhotoAvatar(photo);
+ }
+ }
+
+ private void onLetterTileAvatarReceived(@NonNull Drawable photo) {
+ if (bubble != null) {
+ bubble.updateAvatar(photo);
+ }
+ }
+
+ private BubbleInfo generateBubbleInfo() {
+ return BubbleInfo.builder()
+ .setPrimaryColor(context.getResources().getColor(R.color.dialer_theme_color, null))
+ .setPrimaryIcon(Icon.createWithResource(context, R.drawable.on_going_call))
+ .setStartingYPosition(
+ context.getResources().getDimensionPixelOffset(R.dimen.return_to_call_initial_offset_y))
+ .setActions(generateActions())
+ .build();
+ }
+
+ @NonNull
+ private List<Action> generateActions() {
+ List<Action> actions = new ArrayList<>();
+ SpeakerButtonInfo speakerButtonInfo = new SpeakerButtonInfo(audioState, IconSize.SIZE_24_DP);
+
+ // Return to call
+ actions.add(
+ Action.builder()
+ .setIconDrawable(
+ context.getDrawable(R.drawable.quantum_ic_exit_to_app_flip_vd_theme_24))
+ .setIntent(fullScreen)
+ .setName(context.getText(R.string.bubble_return_to_call))
+ .setCheckable(false)
+ .build());
+ // Mute/unmute
+ actions.add(
+ Action.builder()
+ .setIconDrawable(context.getDrawable(R.drawable.quantum_ic_mic_off_white_24))
+ .setChecked(audioState.isMuted())
+ .setIntent(toggleMute)
+ .setName(context.getText(R.string.incall_label_mute))
+ .build());
+ // Speaker/audio selector
+ actions.add(
+ Action.builder()
+ .setIconDrawable(context.getDrawable(speakerButtonInfo.icon))
+ .setSecondaryIconDrawable(
+ speakerButtonInfo.checkable
+ ? null
+ : context.getDrawable(R.drawable.quantum_ic_arrow_drop_down_vd_theme_24))
+ .setName(context.getText(speakerButtonInfo.label))
+ .setCheckable(speakerButtonInfo.checkable)
+ .setChecked(speakerButtonInfo.isChecked)
+ .setIntent(speakerButtonInfo.checkable ? toggleSpeaker : showSpeakerSelect)
+ .build());
+ // End call
+ actions.add(
+ Action.builder()
+ .setIconDrawable(context.getDrawable(R.drawable.quantum_ic_call_end_vd_theme_24))
+ .setIntent(endCall)
+ .setName(context.getText(R.string.incall_label_end_call))
+ .setCheckable(false)
+ .build());
+ return actions;
+ }
+
+ @NonNull
+ private PendingIntent createActionIntent(String action) {
+ Intent intent = new Intent(context, ReturnToCallActionReceiver.class);
+ intent.setAction(action);
+ return PendingIntent.getBroadcast(context, 0, intent, 0);
+ }
+
+ @NonNull
+ private LetterTileDrawable createLettleTileDrawable(
+ DialerCall dialerCall, ContactCacheEntry entry) {
+ String preferredName =
+ ContactDisplayUtils.getPreferredDisplayName(
+ entry.namePrimary,
+ entry.nameAlternative,
+ ContactsPreferencesFactory.newContactsPreferences(context));
+ if (TextUtils.isEmpty(preferredName)) {
+ preferredName = entry.number;
+ }
+
+ LetterTileDrawable letterTile = new LetterTileDrawable(context.getResources());
+ letterTile.setCanonicalDialerLetterTileDetails(
+ dialerCall.updateNameIfRestricted(preferredName),
+ entry.lookupKey,
+ LetterTileDrawable.SHAPE_CIRCLE,
+ LetterTileDrawable.getContactTypeFromPrimitives(
+ dialerCall.isVoiceMailNumber(),
+ dialerCall.isSpam(),
+ entry.isBusiness,
+ dialerCall.getNumberPresentation(),
+ dialerCall.isConferenceCall()));
+ return letterTile;
+ }
+
+ private static class ReturnToCallContactInfoCacheCallback implements ContactInfoCacheCallback {
+
+ private final WeakReference<ReturnToCallController> returnToCallControllerWeakReference;
+
+ private ReturnToCallContactInfoCacheCallback(ReturnToCallController returnToCallController) {
+ returnToCallControllerWeakReference = new WeakReference<>(returnToCallController);
+ }
+
+ @Override
+ public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
+ ReturnToCallController returnToCallController = returnToCallControllerWeakReference.get();
+ if (returnToCallController == null) {
+ return;
+ }
+ if (entry.photo != null) {
+ returnToCallController.onPhotoAvatarReceived(entry.photo);
+ } else {
+ DialerCall dialerCall = CallList.getInstance().getCallById(callId);
+ if (dialerCall != null) {
+ returnToCallController.onLetterTileAvatarReceived(
+ returnToCallController.createLettleTileDrawable(dialerCall, entry));
+ }
+ }
+ }
+
+ @Override
+ public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
+ ReturnToCallController returnToCallController = returnToCallControllerWeakReference.get();
+ if (returnToCallController == null) {
+ return;
+ }
+ if (entry.photo != null) {
+ returnToCallController.onPhotoAvatarReceived(entry.photo);
+ } else {
+ DialerCall dialerCall = CallList.getInstance().getCallById(callId);
+ if (dialerCall != null) {
+ returnToCallController.onLetterTileAvatarReceived(
+ returnToCallController.createLettleTileDrawable(dialerCall, entry));
+ }
+ }
+ }
+ }
+}