blob: 30877ffa5746c500282b0dc8174acc6e7e384b0c [file] [log] [blame]
Eric Erfanianccca3152017-02-22 16:32:36 -08001/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.incallui;
18
19import android.content.Context;
Eric Erfanianccca3152017-02-22 16:32:36 -080020import android.os.Bundle;
21import android.support.v4.app.Fragment;
22import android.support.v4.os.UserManagerCompat;
23import android.telecom.CallAudioState;
Eric Erfanianccca3152017-02-22 16:32:36 -080024import com.android.contacts.common.compat.CallCompat;
25import com.android.dialer.common.Assert;
26import com.android.dialer.common.LogUtil;
Eric Erfanian8369df02017-05-03 10:27:13 -070027import com.android.dialer.logging.DialerImpression;
Eric Erfanianccca3152017-02-22 16:32:36 -080028import com.android.dialer.logging.Logger;
Eric Erfanianccca3152017-02-22 16:32:36 -080029import com.android.incallui.InCallCameraManager.Listener;
30import com.android.incallui.InCallPresenter.CanAddCallListener;
31import com.android.incallui.InCallPresenter.InCallDetailsListener;
32import com.android.incallui.InCallPresenter.InCallState;
33import com.android.incallui.InCallPresenter.InCallStateListener;
34import com.android.incallui.InCallPresenter.IncomingCallListener;
Eric Erfanian8369df02017-05-03 10:27:13 -070035import com.android.incallui.audiomode.AudioModeProvider;
36import com.android.incallui.audiomode.AudioModeProvider.AudioModeListener;
Eric Erfanianccca3152017-02-22 16:32:36 -080037import com.android.incallui.call.CallList;
38import com.android.incallui.call.DialerCall;
Eric Erfaniand5e47f62017-03-15 14:41:07 -070039import com.android.incallui.call.DialerCall.CameraDirection;
Eric Erfanianccca3152017-02-22 16:32:36 -080040import com.android.incallui.call.TelecomAdapter;
Eric Erfanianccca3152017-02-22 16:32:36 -080041import com.android.incallui.incall.protocol.InCallButtonIds;
42import com.android.incallui.incall.protocol.InCallButtonUi;
43import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
Eric Erfanian90508232017-03-24 09:31:16 -070044import com.android.incallui.videotech.utils.VideoUtils;
Eric Erfanianccca3152017-02-22 16:32:36 -080045
46/** Logic for call buttons. */
47public class CallButtonPresenter
48 implements InCallStateListener,
49 AudioModeListener,
50 IncomingCallListener,
51 InCallDetailsListener,
52 CanAddCallListener,
53 Listener,
54 InCallButtonUiDelegate {
55
56 private static final String KEY_AUTOMATICALLY_MUTED = "incall_key_automatically_muted";
57 private static final String KEY_PREVIOUS_MUTE_STATE = "incall_key_previous_mute_state";
58
59 private final Context mContext;
60 private InCallButtonUi mInCallButtonUi;
61 private DialerCall mCall;
62 private boolean mAutomaticallyMuted = false;
63 private boolean mPreviousMuteState = false;
64 private boolean isInCallButtonUiReady;
65
66 public CallButtonPresenter(Context context) {
67 mContext = context.getApplicationContext();
68 }
69
70 @Override
71 public void onInCallButtonUiReady(InCallButtonUi ui) {
72 Assert.checkState(!isInCallButtonUiReady);
73 mInCallButtonUi = ui;
74 AudioModeProvider.getInstance().addListener(this);
75
76 // register for call state changes last
77 final InCallPresenter inCallPresenter = InCallPresenter.getInstance();
78 inCallPresenter.addListener(this);
79 inCallPresenter.addIncomingCallListener(this);
80 inCallPresenter.addDetailsListener(this);
81 inCallPresenter.addCanAddCallListener(this);
82 inCallPresenter.getInCallCameraManager().addCameraSelectionListener(this);
83
84 // Update the buttons state immediately for the current call
85 onStateChange(InCallState.NO_CALLS, inCallPresenter.getInCallState(), CallList.getInstance());
86 isInCallButtonUiReady = true;
87 }
88
89 @Override
90 public void onInCallButtonUiUnready() {
91 Assert.checkState(isInCallButtonUiReady);
92 mInCallButtonUi = null;
93 InCallPresenter.getInstance().removeListener(this);
94 AudioModeProvider.getInstance().removeListener(this);
95 InCallPresenter.getInstance().removeIncomingCallListener(this);
96 InCallPresenter.getInstance().removeDetailsListener(this);
97 InCallPresenter.getInstance().getInCallCameraManager().removeCameraSelectionListener(this);
98 InCallPresenter.getInstance().removeCanAddCallListener(this);
99 isInCallButtonUiReady = false;
100 }
101
102 @Override
103 public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
104 if (newState == InCallState.OUTGOING) {
105 mCall = callList.getOutgoingCall();
106 } else if (newState == InCallState.INCALL) {
107 mCall = callList.getActiveOrBackgroundCall();
108
109 // When connected to voice mail, automatically shows the dialpad.
110 // (On previous releases we showed it when in-call shows up, before waiting for
111 // OUTGOING. We may want to do that once we start showing "Voice mail" label on
112 // the dialpad too.)
113 if (oldState == InCallState.OUTGOING && mCall != null) {
114 if (CallerInfoUtils.isVoiceMailNumber(mContext, mCall) && getActivity() != null) {
115 getActivity().showDialpadFragment(true /* show */, true /* animate */);
116 }
117 }
118 } else if (newState == InCallState.INCOMING) {
119 if (getActivity() != null) {
120 getActivity().showDialpadFragment(false /* show */, true /* animate */);
121 }
122 mCall = callList.getIncomingCall();
123 } else {
124 mCall = null;
125 }
126 updateUi(newState, mCall);
127 }
128
129 /**
130 * Updates the user interface in response to a change in the details of a call. Currently handles
131 * changes to the call buttons in response to a change in the details for a call. This is
132 * important to ensure changes to the active call are reflected in the available buttons.
133 *
134 * @param call The active call.
135 * @param details The call details.
136 */
137 @Override
138 public void onDetailsChanged(DialerCall call, android.telecom.Call.Details details) {
139 // Only update if the changes are for the currently active call
140 if (mInCallButtonUi != null && call != null && call.equals(mCall)) {
141 updateButtonsState(call);
142 }
143 }
144
145 @Override
146 public void onIncomingCall(InCallState oldState, InCallState newState, DialerCall call) {
147 onStateChange(oldState, newState, CallList.getInstance());
148 }
149
150 @Override
151 public void onCanAddCallChanged(boolean canAddCall) {
152 if (mInCallButtonUi != null && mCall != null) {
153 updateButtonsState(mCall);
154 }
155 }
156
157 @Override
158 public void onAudioStateChanged(CallAudioState audioState) {
159 if (mInCallButtonUi != null) {
160 mInCallButtonUi.setAudioState(audioState);
161 }
162 }
163
164 @Override
165 public CallAudioState getCurrentAudioState() {
166 return AudioModeProvider.getInstance().getAudioState();
167 }
168
169 @Override
170 public void setAudioRoute(int route) {
171 LogUtil.i(
172 "CallButtonPresenter.setAudioRoute",
173 "sending new audio route: " + CallAudioState.audioRouteToString(route));
174 TelecomAdapter.getInstance().setAudioRoute(route);
175 }
176
177 /** Function assumes that bluetooth is not supported. */
178 @Override
179 public void toggleSpeakerphone() {
180 // This function should not be called if bluetooth is available.
181 CallAudioState audioState = getCurrentAudioState();
182 if (0 != (CallAudioState.ROUTE_BLUETOOTH & audioState.getSupportedRouteMask())) {
183 // It's clear the UI is wrong, so update the supported mode once again.
184 LogUtil.e(
185 "CallButtonPresenter", "toggling speakerphone not allowed when bluetooth supported.");
186 mInCallButtonUi.setAudioState(audioState);
187 return;
188 }
189
190 int newRoute;
191 if (audioState.getRoute() == CallAudioState.ROUTE_SPEAKER) {
192 newRoute = CallAudioState.ROUTE_WIRED_OR_EARPIECE;
193 Logger.get(mContext)
194 .logCallImpression(
195 DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_WIRED_OR_EARPIECE,
196 mCall.getUniqueCallId(),
197 mCall.getTimeAddedMs());
198 } else {
199 newRoute = CallAudioState.ROUTE_SPEAKER;
200 Logger.get(mContext)
201 .logCallImpression(
202 DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_SPEAKERPHONE,
203 mCall.getUniqueCallId(),
204 mCall.getTimeAddedMs());
205 }
206
207 setAudioRoute(newRoute);
208 }
209
210 @Override
Eric Erfanian9a090c82017-03-16 19:22:24 -0700211 public void muteClicked(boolean checked, boolean clickedByUser) {
212 LogUtil.i(
213 "CallButtonPresenter", "turning on mute: %s, clicked by user: %s", checked, clickedByUser);
214 if (clickedByUser) {
215 Logger.get(mContext)
216 .logCallImpression(
217 checked
218 ? DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_MUTE
219 : DialerImpression.Type.IN_CALL_SCREEN_TURN_OFF_MUTE,
220 mCall.getUniqueCallId(),
221 mCall.getTimeAddedMs());
222 }
Eric Erfanianccca3152017-02-22 16:32:36 -0800223 TelecomAdapter.getInstance().mute(checked);
224 }
225
226 @Override
227 public void holdClicked(boolean checked) {
228 if (mCall == null) {
229 return;
230 }
231 if (checked) {
232 LogUtil.i("CallButtonPresenter", "putting the call on hold: " + mCall);
233 mCall.hold();
234 } else {
235 LogUtil.i("CallButtonPresenter", "removing the call from hold: " + mCall);
236 mCall.unhold();
237 }
238 }
239
240 @Override
241 public void swapClicked() {
242 if (mCall == null) {
243 return;
244 }
245
246 LogUtil.i("CallButtonPresenter", "swapping the call: " + mCall);
247 TelecomAdapter.getInstance().swap(mCall.getId());
248 }
249
250 @Override
251 public void mergeClicked() {
252 TelecomAdapter.getInstance().merge(mCall.getId());
253 }
254
255 @Override
256 public void addCallClicked() {
257 // Automatically mute the current call
258 mAutomaticallyMuted = true;
259 mPreviousMuteState = AudioModeProvider.getInstance().getAudioState().isMuted();
260 // Simulate a click on the mute button
Eric Erfanian9a090c82017-03-16 19:22:24 -0700261 muteClicked(true /* checked */, false /* clickedByUser */);
Eric Erfanianccca3152017-02-22 16:32:36 -0800262 TelecomAdapter.getInstance().addCall();
263 }
264
265 @Override
266 public void showDialpadClicked(boolean checked) {
267 LogUtil.v("CallButtonPresenter", "show dialpad " + String.valueOf(checked));
268 getActivity().showDialpadFragment(checked /* show */, true /* animate */);
269 }
270
271 @Override
272 public void changeToVideoClicked() {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700273 LogUtil.enterBlock("CallButtonPresenter.changeToVideoClicked");
Eric Erfanian8369df02017-05-03 10:27:13 -0700274 Logger.get(mContext)
275 .logCallImpression(
276 DialerImpression.Type.VIDEO_CALL_UPGRADE_REQUESTED,
277 mCall.getUniqueCallId(),
278 mCall.getTimeAddedMs());
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700279 mCall.getVideoTech().upgradeToVideo();
Eric Erfanianccca3152017-02-22 16:32:36 -0800280 }
281
282 @Override
283 public void onEndCallClicked() {
284 LogUtil.i("CallButtonPresenter.onEndCallClicked", "call: " + mCall);
285 if (mCall != null) {
286 mCall.disconnect();
287 }
288 }
289
290 @Override
291 public void showAudioRouteSelector() {
292 mInCallButtonUi.showAudioRouteSelector();
293 }
294
295 /**
296 * Switches the camera between the front-facing and back-facing camera.
297 *
298 * @param useFrontFacingCamera True if we should switch to using the front-facing camera, or false
299 * if we should switch to using the back-facing camera.
300 */
301 @Override
302 public void switchCameraClicked(boolean useFrontFacingCamera) {
Eric Erfanian91ce7d22017-06-05 13:35:02 -0700303 updateCamera(useFrontFacingCamera);
Eric Erfanianccca3152017-02-22 16:32:36 -0800304 }
305
306 @Override
307 public void toggleCameraClicked() {
308 LogUtil.i("CallButtonPresenter.toggleCameraClicked", "");
Eric Erfanianea7890c2017-06-19 12:40:59 -0700309 if (mCall == null) {
310 return;
311 }
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700312 Logger.get(mContext)
313 .logCallImpression(
314 DialerImpression.Type.IN_CALL_SCREEN_SWAP_CAMERA,
315 mCall.getUniqueCallId(),
316 mCall.getTimeAddedMs());
Eric Erfanianccca3152017-02-22 16:32:36 -0800317 switchCameraClicked(
318 !InCallPresenter.getInstance().getInCallCameraManager().isUsingFrontFacingCamera());
319 }
320
321 /**
322 * Stop or start client's video transmission.
323 *
324 * @param pause True if pausing the local user's video, or false if starting the local user's
325 * video.
326 */
327 @Override
328 public void pauseVideoClicked(boolean pause) {
329 LogUtil.i("CallButtonPresenter.pauseVideoClicked", "%s", pause ? "pause" : "unpause");
Eric Erfanianccca3152017-02-22 16:32:36 -0800330
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700331 Logger.get(mContext)
332 .logCallImpression(
333 pause
334 ? DialerImpression.Type.IN_CALL_SCREEN_TURN_OFF_VIDEO
335 : DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_VIDEO,
336 mCall.getUniqueCallId(),
337 mCall.getTimeAddedMs());
338
Eric Erfanianccca3152017-02-22 16:32:36 -0800339 if (pause) {
Eric Erfanian91ce7d22017-06-05 13:35:02 -0700340 mCall.getVideoTech().setCamera(null);
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700341 mCall.getVideoTech().stopTransmission();
Eric Erfanianccca3152017-02-22 16:32:36 -0800342 } else {
Eric Erfanian91ce7d22017-06-05 13:35:02 -0700343 updateCamera(
344 InCallPresenter.getInstance().getInCallCameraManager().isUsingFrontFacingCamera());
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700345 mCall.getVideoTech().resumeTransmission();
Eric Erfanianccca3152017-02-22 16:32:36 -0800346 }
347
348 mInCallButtonUi.setVideoPaused(pause);
349 mInCallButtonUi.enableButton(InCallButtonIds.BUTTON_PAUSE_VIDEO, false);
350 }
351
Eric Erfanian91ce7d22017-06-05 13:35:02 -0700352 private void updateCamera(boolean useFrontFacingCamera) {
353 InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager();
354 cameraManager.setUseFrontFacingCamera(useFrontFacingCamera);
355
356 String cameraId = cameraManager.getActiveCameraId();
357 if (cameraId != null) {
358 final int cameraDir =
359 cameraManager.isUsingFrontFacingCamera()
360 ? CameraDirection.CAMERA_DIRECTION_FRONT_FACING
361 : CameraDirection.CAMERA_DIRECTION_BACK_FACING;
362 mCall.setCameraDir(cameraDir);
363 mCall.getVideoTech().setCamera(cameraId);
364 }
365 }
366
Eric Erfanianccca3152017-02-22 16:32:36 -0800367 private void updateUi(InCallState state, DialerCall call) {
368 LogUtil.v("CallButtonPresenter", "updating call UI for call: ", call);
369
370 if (mInCallButtonUi == null) {
371 return;
372 }
373
374 if (call != null) {
375 mInCallButtonUi.updateInCallButtonUiColors();
376 }
377
378 final boolean isEnabled =
379 state.isConnectingOrConnected() && !state.isIncoming() && call != null;
380 mInCallButtonUi.setEnabled(isEnabled);
381
382 if (call == null) {
383 return;
384 }
385
386 updateButtonsState(call);
387 }
388
389 /**
390 * Updates the buttons applicable for the UI.
391 *
392 * @param call The active call.
393 */
394 private void updateButtonsState(DialerCall call) {
395 LogUtil.v("CallButtonPresenter.updateButtonsState", "");
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700396 final boolean isVideo = call.isVideoCall();
Eric Erfanianccca3152017-02-22 16:32:36 -0800397
398 // Common functionality (audio, hold, etc).
399 // Show either HOLD or SWAP, but not both. If neither HOLD or SWAP is available:
400 // (1) If the device normally can hold, show HOLD in a disabled state.
401 // (2) If the device doesn't have the concept of hold/swap, remove the button.
402 final boolean showSwap = call.can(android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE);
403 final boolean showHold =
404 !showSwap
405 && call.can(android.telecom.Call.Details.CAPABILITY_SUPPORT_HOLD)
406 && call.can(android.telecom.Call.Details.CAPABILITY_HOLD);
407 final boolean isCallOnHold = call.getState() == DialerCall.State.ONHOLD;
408
409 final boolean showAddCall =
410 TelecomAdapter.getInstance().canAddCall() && UserManagerCompat.isUserUnlocked(mContext);
411 final boolean showMerge = call.can(android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE);
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700412 final boolean showUpgradeToVideo = !isVideo && (hasVideoCallCapabilities(call));
Eric Erfanianccca3152017-02-22 16:32:36 -0800413 final boolean showDowngradeToAudio = isVideo && isDowngradeToAudioSupported(call);
414 final boolean showMute = call.can(android.telecom.Call.Details.CAPABILITY_MUTE);
415
416 final boolean hasCameraPermission =
Eric Erfanianff2ad7f2017-07-27 10:45:54 -0700417 isVideo && VideoUtils.hasCameraPermissionAndShownPrivacyToast(mContext);
Eric Erfanianccca3152017-02-22 16:32:36 -0800418 // Disabling local video doesn't seem to work when dialing. See b/30256571.
419 final boolean showPauseVideo =
420 isVideo
421 && call.getState() != DialerCall.State.DIALING
422 && call.getState() != DialerCall.State.CONNECTING;
423
424 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_AUDIO, true);
425 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_SWAP, showSwap);
426 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_HOLD, showHold);
427 mInCallButtonUi.setHold(isCallOnHold);
428 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_MUTE, showMute);
429 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_ADD_CALL, true);
430 mInCallButtonUi.enableButton(InCallButtonIds.BUTTON_ADD_CALL, showAddCall);
431 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO, showUpgradeToVideo);
432 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_DOWNGRADE_TO_AUDIO, showDowngradeToAudio);
433 mInCallButtonUi.showButton(
434 InCallButtonIds.BUTTON_SWITCH_CAMERA, isVideo && hasCameraPermission);
435 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_PAUSE_VIDEO, showPauseVideo);
436 if (isVideo) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700437 mInCallButtonUi.setVideoPaused(!call.getVideoTech().isTransmitting() || !hasCameraPermission);
Eric Erfanianccca3152017-02-22 16:32:36 -0800438 }
439 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_DIALPAD, true);
440 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_MERGE, showMerge);
441
442 mInCallButtonUi.updateButtonStates();
443 }
444
445 private boolean hasVideoCallCapabilities(DialerCall call) {
Eric Erfaniand8046e52017-04-06 09:41:50 -0700446 return call.getVideoTech().isAvailable(mContext);
Eric Erfanianccca3152017-02-22 16:32:36 -0800447 }
448
449 /**
450 * Determines if downgrading from a video call to an audio-only call is supported. In order to
451 * support downgrade to audio, the SDK version must be >= N and the call should NOT have the
452 * {@link android.telecom.Call.Details#CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO}.
453 *
454 * @param call The call.
455 * @return {@code true} if downgrading to an audio-only call from a video call is supported.
456 */
457 private boolean isDowngradeToAudioSupported(DialerCall call) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700458 // TODO(b/33676907): If there is an RCS video share session, return true here
Eric Erfanianccca3152017-02-22 16:32:36 -0800459 return !call.can(CallCompat.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO);
460 }
461
462 @Override
463 public void refreshMuteState() {
464 // Restore the previous mute state
465 if (mAutomaticallyMuted
466 && AudioModeProvider.getInstance().getAudioState().isMuted() != mPreviousMuteState) {
467 if (mInCallButtonUi == null) {
468 return;
469 }
Eric Erfanian9a090c82017-03-16 19:22:24 -0700470 muteClicked(mPreviousMuteState, false /* clickedByUser */);
Eric Erfanianccca3152017-02-22 16:32:36 -0800471 }
472 mAutomaticallyMuted = false;
473 }
474
475 @Override
476 public void onSaveInstanceState(Bundle outState) {
477 outState.putBoolean(KEY_AUTOMATICALLY_MUTED, mAutomaticallyMuted);
478 outState.putBoolean(KEY_PREVIOUS_MUTE_STATE, mPreviousMuteState);
479 }
480
481 @Override
482 public void onRestoreInstanceState(Bundle savedInstanceState) {
483 mAutomaticallyMuted =
484 savedInstanceState.getBoolean(KEY_AUTOMATICALLY_MUTED, mAutomaticallyMuted);
485 mPreviousMuteState = savedInstanceState.getBoolean(KEY_PREVIOUS_MUTE_STATE, mPreviousMuteState);
486 }
487
488 @Override
489 public void onCameraPermissionGranted() {
490 if (mCall != null) {
491 updateButtonsState(mCall);
492 }
493 }
494
495 @Override
496 public void onActiveCameraSelectionChanged(boolean isUsingFrontFacingCamera) {
497 if (mInCallButtonUi == null) {
498 return;
499 }
500 mInCallButtonUi.setCameraSwitched(!isUsingFrontFacingCamera);
501 }
502
503 @Override
504 public Context getContext() {
505 return mContext;
506 }
507
508 private InCallActivity getActivity() {
509 if (mInCallButtonUi != null) {
510 Fragment fragment = mInCallButtonUi.getInCallButtonUiFragment();
511 if (fragment != null) {
512 return (InCallActivity) fragment.getActivity();
513 }
514 }
515 return null;
516 }
517}