| /* |
| * 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; |
| |
| 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.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 com.android.newbubble.NewBubble; |
| import com.android.newbubble.NewBubbleComponent; |
| import com.android.newbubble.NewBubbleInfo; |
| import com.android.newbubble.NewBubbleInfo.Action; |
| 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 |
| */ |
| public class NewReturnToCallController implements InCallUiListener, Listener, AudioModeListener { |
| |
| private final Context context; |
| |
| @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) |
| NewBubble 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 NewReturnToCallController(Context context, ContactInfoCache contactInfoCache) { |
| this.context = context; |
| this.contactInfoCache = contactInfoCache; |
| |
| toggleSpeaker = createActionIntent(NewReturnToCallActionReceiver.ACTION_TOGGLE_SPEAKER); |
| showSpeakerSelect = |
| createActionIntent(NewReturnToCallActionReceiver.ACTION_SHOW_AUDIO_ROUTE_SELECTOR); |
| toggleMute = createActionIntent(NewReturnToCallActionReceiver.ACTION_TOGGLE_MUTE); |
| endCall = createActionIntent(NewReturnToCallActionReceiver.ACTION_END_CALL); |
| fullScreen = createActionIntent(NewReturnToCallActionReceiver.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) { |
| if (showing) { |
| hide(); |
| } else { |
| if (getCall() != null) { |
| 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 NewBubble startBubble() { |
| if (!canShowBubbles(context)) { |
| LogUtil.i("ReturnToCallController.startNewBubble", "can't show bubble, no permission"); |
| return null; |
| } |
| NewBubble returnToCallBubble = NewBubbleComponent.get(context).getNewBubble(); |
| 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) {} |
| |
| @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 NewBubbleInfo generateBubbleInfo() { |
| return NewBubbleInfo.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, NewReturnToCallActionReceiver.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<NewReturnToCallController> newReturnToCallControllerWeakReference; |
| |
| private ReturnToCallContactInfoCacheCallback( |
| NewReturnToCallController newReturnToCallController) { |
| newReturnToCallControllerWeakReference = new WeakReference<>(newReturnToCallController); |
| } |
| |
| @Override |
| public void onContactInfoComplete(String callId, ContactCacheEntry entry) { |
| NewReturnToCallController newReturnToCallController = |
| newReturnToCallControllerWeakReference.get(); |
| if (newReturnToCallController == null) { |
| return; |
| } |
| if (entry.photo != null) { |
| newReturnToCallController.onPhotoAvatarReceived(entry.photo); |
| } else { |
| DialerCall dialerCall = CallList.getInstance().getCallById(callId); |
| if (dialerCall != null) { |
| newReturnToCallController.onLetterTileAvatarReceived( |
| newReturnToCallController.createLettleTileDrawable(dialerCall, entry)); |
| } |
| } |
| } |
| |
| @Override |
| public void onImageLoadComplete(String callId, ContactCacheEntry entry) { |
| NewReturnToCallController newReturnToCallController = |
| newReturnToCallControllerWeakReference.get(); |
| if (newReturnToCallController == null) { |
| return; |
| } |
| if (entry.photo != null) { |
| newReturnToCallController.onPhotoAvatarReceived(entry.photo); |
| } else { |
| DialerCall dialerCall = CallList.getInstance().getCallById(callId); |
| if (dialerCall != null) { |
| newReturnToCallController.onLetterTileAvatarReceived( |
| newReturnToCallController.createLettleTileDrawable(dialerCall, entry)); |
| } |
| } |
| } |
| } |
| } |