blob: 69e25ff97f8dde83526ec1ba37612dd929ed4cda [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 Erfanianccca3152017-02-22 16:32:36 -080027import com.android.dialer.logging.Logger;
28import com.android.dialer.logging.nano.DialerImpression;
29import com.android.incallui.AudioModeProvider.AudioModeListener;
30import com.android.incallui.InCallCameraManager.Listener;
31import com.android.incallui.InCallPresenter.CanAddCallListener;
32import com.android.incallui.InCallPresenter.InCallDetailsListener;
33import com.android.incallui.InCallPresenter.InCallState;
34import com.android.incallui.InCallPresenter.InCallStateListener;
35import com.android.incallui.InCallPresenter.IncomingCallListener;
36import com.android.incallui.call.CallList;
37import com.android.incallui.call.DialerCall;
Eric Erfaniand5e47f62017-03-15 14:41:07 -070038import com.android.incallui.call.DialerCall.CameraDirection;
Eric Erfanianccca3152017-02-22 16:32:36 -080039import com.android.incallui.call.TelecomAdapter;
Eric Erfanianccca3152017-02-22 16:32:36 -080040import com.android.incallui.incall.protocol.InCallButtonIds;
41import com.android.incallui.incall.protocol.InCallButtonUi;
42import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
Eric Erfanian90508232017-03-24 09:31:16 -070043import com.android.incallui.videotech.utils.VideoUtils;
Eric Erfanianccca3152017-02-22 16:32:36 -080044
45/** Logic for call buttons. */
46public class CallButtonPresenter
47 implements InCallStateListener,
48 AudioModeListener,
49 IncomingCallListener,
50 InCallDetailsListener,
51 CanAddCallListener,
52 Listener,
53 InCallButtonUiDelegate {
54
55 private static final String KEY_AUTOMATICALLY_MUTED = "incall_key_automatically_muted";
56 private static final String KEY_PREVIOUS_MUTE_STATE = "incall_key_previous_mute_state";
57
58 private final Context mContext;
59 private InCallButtonUi mInCallButtonUi;
60 private DialerCall mCall;
61 private boolean mAutomaticallyMuted = false;
62 private boolean mPreviousMuteState = false;
63 private boolean isInCallButtonUiReady;
64
65 public CallButtonPresenter(Context context) {
66 mContext = context.getApplicationContext();
67 }
68
69 @Override
70 public void onInCallButtonUiReady(InCallButtonUi ui) {
71 Assert.checkState(!isInCallButtonUiReady);
72 mInCallButtonUi = ui;
73 AudioModeProvider.getInstance().addListener(this);
74
75 // register for call state changes last
76 final InCallPresenter inCallPresenter = InCallPresenter.getInstance();
77 inCallPresenter.addListener(this);
78 inCallPresenter.addIncomingCallListener(this);
79 inCallPresenter.addDetailsListener(this);
80 inCallPresenter.addCanAddCallListener(this);
81 inCallPresenter.getInCallCameraManager().addCameraSelectionListener(this);
82
83 // Update the buttons state immediately for the current call
84 onStateChange(InCallState.NO_CALLS, inCallPresenter.getInCallState(), CallList.getInstance());
85 isInCallButtonUiReady = true;
86 }
87
88 @Override
89 public void onInCallButtonUiUnready() {
90 Assert.checkState(isInCallButtonUiReady);
91 mInCallButtonUi = null;
92 InCallPresenter.getInstance().removeListener(this);
93 AudioModeProvider.getInstance().removeListener(this);
94 InCallPresenter.getInstance().removeIncomingCallListener(this);
95 InCallPresenter.getInstance().removeDetailsListener(this);
96 InCallPresenter.getInstance().getInCallCameraManager().removeCameraSelectionListener(this);
97 InCallPresenter.getInstance().removeCanAddCallListener(this);
98 isInCallButtonUiReady = false;
99 }
100
101 @Override
102 public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
103 if (newState == InCallState.OUTGOING) {
104 mCall = callList.getOutgoingCall();
105 } else if (newState == InCallState.INCALL) {
106 mCall = callList.getActiveOrBackgroundCall();
107
108 // When connected to voice mail, automatically shows the dialpad.
109 // (On previous releases we showed it when in-call shows up, before waiting for
110 // OUTGOING. We may want to do that once we start showing "Voice mail" label on
111 // the dialpad too.)
112 if (oldState == InCallState.OUTGOING && mCall != null) {
113 if (CallerInfoUtils.isVoiceMailNumber(mContext, mCall) && getActivity() != null) {
114 getActivity().showDialpadFragment(true /* show */, true /* animate */);
115 }
116 }
117 } else if (newState == InCallState.INCOMING) {
118 if (getActivity() != null) {
119 getActivity().showDialpadFragment(false /* show */, true /* animate */);
120 }
121 mCall = callList.getIncomingCall();
122 } else {
123 mCall = null;
124 }
125 updateUi(newState, mCall);
126 }
127
128 /**
129 * Updates the user interface in response to a change in the details of a call. Currently handles
130 * changes to the call buttons in response to a change in the details for a call. This is
131 * important to ensure changes to the active call are reflected in the available buttons.
132 *
133 * @param call The active call.
134 * @param details The call details.
135 */
136 @Override
137 public void onDetailsChanged(DialerCall call, android.telecom.Call.Details details) {
138 // Only update if the changes are for the currently active call
139 if (mInCallButtonUi != null && call != null && call.equals(mCall)) {
140 updateButtonsState(call);
141 }
142 }
143
144 @Override
145 public void onIncomingCall(InCallState oldState, InCallState newState, DialerCall call) {
146 onStateChange(oldState, newState, CallList.getInstance());
147 }
148
149 @Override
150 public void onCanAddCallChanged(boolean canAddCall) {
151 if (mInCallButtonUi != null && mCall != null) {
152 updateButtonsState(mCall);
153 }
154 }
155
156 @Override
157 public void onAudioStateChanged(CallAudioState audioState) {
158 if (mInCallButtonUi != null) {
159 mInCallButtonUi.setAudioState(audioState);
160 }
161 }
162
163 @Override
164 public CallAudioState getCurrentAudioState() {
165 return AudioModeProvider.getInstance().getAudioState();
166 }
167
168 @Override
169 public void setAudioRoute(int route) {
170 LogUtil.i(
171 "CallButtonPresenter.setAudioRoute",
172 "sending new audio route: " + CallAudioState.audioRouteToString(route));
173 TelecomAdapter.getInstance().setAudioRoute(route);
174 }
175
176 /** Function assumes that bluetooth is not supported. */
177 @Override
178 public void toggleSpeakerphone() {
179 // This function should not be called if bluetooth is available.
180 CallAudioState audioState = getCurrentAudioState();
181 if (0 != (CallAudioState.ROUTE_BLUETOOTH & audioState.getSupportedRouteMask())) {
182 // It's clear the UI is wrong, so update the supported mode once again.
183 LogUtil.e(
184 "CallButtonPresenter", "toggling speakerphone not allowed when bluetooth supported.");
185 mInCallButtonUi.setAudioState(audioState);
186 return;
187 }
188
189 int newRoute;
190 if (audioState.getRoute() == CallAudioState.ROUTE_SPEAKER) {
191 newRoute = CallAudioState.ROUTE_WIRED_OR_EARPIECE;
192 Logger.get(mContext)
193 .logCallImpression(
194 DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_WIRED_OR_EARPIECE,
195 mCall.getUniqueCallId(),
196 mCall.getTimeAddedMs());
197 } else {
198 newRoute = CallAudioState.ROUTE_SPEAKER;
199 Logger.get(mContext)
200 .logCallImpression(
201 DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_SPEAKERPHONE,
202 mCall.getUniqueCallId(),
203 mCall.getTimeAddedMs());
204 }
205
206 setAudioRoute(newRoute);
207 }
208
209 @Override
Eric Erfanian9a090c82017-03-16 19:22:24 -0700210 public void muteClicked(boolean checked, boolean clickedByUser) {
211 LogUtil.i(
212 "CallButtonPresenter", "turning on mute: %s, clicked by user: %s", checked, clickedByUser);
213 if (clickedByUser) {
214 Logger.get(mContext)
215 .logCallImpression(
216 checked
217 ? DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_MUTE
218 : DialerImpression.Type.IN_CALL_SCREEN_TURN_OFF_MUTE,
219 mCall.getUniqueCallId(),
220 mCall.getTimeAddedMs());
221 }
Eric Erfanianccca3152017-02-22 16:32:36 -0800222 TelecomAdapter.getInstance().mute(checked);
223 }
224
225 @Override
226 public void holdClicked(boolean checked) {
227 if (mCall == null) {
228 return;
229 }
230 if (checked) {
231 LogUtil.i("CallButtonPresenter", "putting the call on hold: " + mCall);
232 mCall.hold();
233 } else {
234 LogUtil.i("CallButtonPresenter", "removing the call from hold: " + mCall);
235 mCall.unhold();
236 }
237 }
238
239 @Override
240 public void swapClicked() {
241 if (mCall == null) {
242 return;
243 }
244
245 LogUtil.i("CallButtonPresenter", "swapping the call: " + mCall);
246 TelecomAdapter.getInstance().swap(mCall.getId());
247 }
248
249 @Override
250 public void mergeClicked() {
251 TelecomAdapter.getInstance().merge(mCall.getId());
252 }
253
254 @Override
255 public void addCallClicked() {
256 // Automatically mute the current call
257 mAutomaticallyMuted = true;
258 mPreviousMuteState = AudioModeProvider.getInstance().getAudioState().isMuted();
259 // Simulate a click on the mute button
Eric Erfanian9a090c82017-03-16 19:22:24 -0700260 muteClicked(true /* checked */, false /* clickedByUser */);
Eric Erfanianccca3152017-02-22 16:32:36 -0800261 TelecomAdapter.getInstance().addCall();
262 }
263
264 @Override
265 public void showDialpadClicked(boolean checked) {
266 LogUtil.v("CallButtonPresenter", "show dialpad " + String.valueOf(checked));
267 getActivity().showDialpadFragment(checked /* show */, true /* animate */);
268 }
269
270 @Override
271 public void changeToVideoClicked() {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700272 LogUtil.enterBlock("CallButtonPresenter.changeToVideoClicked");
273 mCall.getVideoTech().upgradeToVideo();
Eric Erfanianccca3152017-02-22 16:32:36 -0800274 }
275
276 @Override
277 public void onEndCallClicked() {
278 LogUtil.i("CallButtonPresenter.onEndCallClicked", "call: " + mCall);
279 if (mCall != null) {
280 mCall.disconnect();
281 }
282 }
283
284 @Override
285 public void showAudioRouteSelector() {
286 mInCallButtonUi.showAudioRouteSelector();
287 }
288
289 /**
290 * Switches the camera between the front-facing and back-facing camera.
291 *
292 * @param useFrontFacingCamera True if we should switch to using the front-facing camera, or false
293 * if we should switch to using the back-facing camera.
294 */
295 @Override
296 public void switchCameraClicked(boolean useFrontFacingCamera) {
297 InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager();
298 cameraManager.setUseFrontFacingCamera(useFrontFacingCamera);
299
Eric Erfanianccca3152017-02-22 16:32:36 -0800300 String cameraId = cameraManager.getActiveCameraId();
301 if (cameraId != null) {
302 final int cameraDir =
303 cameraManager.isUsingFrontFacingCamera()
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700304 ? CameraDirection.CAMERA_DIRECTION_FRONT_FACING
305 : CameraDirection.CAMERA_DIRECTION_BACK_FACING;
306 mCall.setCameraDir(cameraDir);
307 mCall.getVideoTech().setCamera(cameraId);
Eric Erfanianccca3152017-02-22 16:32:36 -0800308 }
309 }
310
311 @Override
312 public void toggleCameraClicked() {
313 LogUtil.i("CallButtonPresenter.toggleCameraClicked", "");
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700314 Logger.get(mContext)
315 .logCallImpression(
316 DialerImpression.Type.IN_CALL_SCREEN_SWAP_CAMERA,
317 mCall.getUniqueCallId(),
318 mCall.getTimeAddedMs());
Eric Erfanianccca3152017-02-22 16:32:36 -0800319 switchCameraClicked(
320 !InCallPresenter.getInstance().getInCallCameraManager().isUsingFrontFacingCamera());
321 }
322
323 /**
324 * Stop or start client's video transmission.
325 *
326 * @param pause True if pausing the local user's video, or false if starting the local user's
327 * video.
328 */
329 @Override
330 public void pauseVideoClicked(boolean pause) {
331 LogUtil.i("CallButtonPresenter.pauseVideoClicked", "%s", pause ? "pause" : "unpause");
Eric Erfanianccca3152017-02-22 16:32:36 -0800332
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700333 Logger.get(mContext)
334 .logCallImpression(
335 pause
336 ? DialerImpression.Type.IN_CALL_SCREEN_TURN_OFF_VIDEO
337 : DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_VIDEO,
338 mCall.getUniqueCallId(),
339 mCall.getTimeAddedMs());
340
Eric Erfanianccca3152017-02-22 16:32:36 -0800341 if (pause) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700342 mCall.getVideoTech().stopTransmission();
Eric Erfanianccca3152017-02-22 16:32:36 -0800343 } else {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700344 mCall.getVideoTech().resumeTransmission();
Eric Erfanianccca3152017-02-22 16:32:36 -0800345 }
346
347 mInCallButtonUi.setVideoPaused(pause);
348 mInCallButtonUi.enableButton(InCallButtonIds.BUTTON_PAUSE_VIDEO, false);
349 }
350
351 private void updateUi(InCallState state, DialerCall call) {
352 LogUtil.v("CallButtonPresenter", "updating call UI for call: ", call);
353
354 if (mInCallButtonUi == null) {
355 return;
356 }
357
358 if (call != null) {
359 mInCallButtonUi.updateInCallButtonUiColors();
360 }
361
362 final boolean isEnabled =
363 state.isConnectingOrConnected() && !state.isIncoming() && call != null;
364 mInCallButtonUi.setEnabled(isEnabled);
365
366 if (call == null) {
367 return;
368 }
369
370 updateButtonsState(call);
371 }
372
373 /**
374 * Updates the buttons applicable for the UI.
375 *
376 * @param call The active call.
377 */
378 private void updateButtonsState(DialerCall call) {
379 LogUtil.v("CallButtonPresenter.updateButtonsState", "");
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700380 final boolean isVideo = call.isVideoCall();
Eric Erfanianccca3152017-02-22 16:32:36 -0800381
382 // Common functionality (audio, hold, etc).
383 // Show either HOLD or SWAP, but not both. If neither HOLD or SWAP is available:
384 // (1) If the device normally can hold, show HOLD in a disabled state.
385 // (2) If the device doesn't have the concept of hold/swap, remove the button.
386 final boolean showSwap = call.can(android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE);
387 final boolean showHold =
388 !showSwap
389 && call.can(android.telecom.Call.Details.CAPABILITY_SUPPORT_HOLD)
390 && call.can(android.telecom.Call.Details.CAPABILITY_HOLD);
391 final boolean isCallOnHold = call.getState() == DialerCall.State.ONHOLD;
392
393 final boolean showAddCall =
394 TelecomAdapter.getInstance().canAddCall() && UserManagerCompat.isUserUnlocked(mContext);
395 final boolean showMerge = call.can(android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE);
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700396 final boolean showUpgradeToVideo = !isVideo && (hasVideoCallCapabilities(call));
Eric Erfanianccca3152017-02-22 16:32:36 -0800397 final boolean showDowngradeToAudio = isVideo && isDowngradeToAudioSupported(call);
398 final boolean showMute = call.can(android.telecom.Call.Details.CAPABILITY_MUTE);
399
400 final boolean hasCameraPermission =
401 isVideo && VideoUtils.hasCameraPermissionAndAllowedByUser(mContext);
402 // Disabling local video doesn't seem to work when dialing. See b/30256571.
403 final boolean showPauseVideo =
404 isVideo
405 && call.getState() != DialerCall.State.DIALING
406 && call.getState() != DialerCall.State.CONNECTING;
407
408 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_AUDIO, true);
409 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_SWAP, showSwap);
410 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_HOLD, showHold);
411 mInCallButtonUi.setHold(isCallOnHold);
412 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_MUTE, showMute);
413 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_ADD_CALL, true);
414 mInCallButtonUi.enableButton(InCallButtonIds.BUTTON_ADD_CALL, showAddCall);
415 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO, showUpgradeToVideo);
416 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_DOWNGRADE_TO_AUDIO, showDowngradeToAudio);
417 mInCallButtonUi.showButton(
418 InCallButtonIds.BUTTON_SWITCH_CAMERA, isVideo && hasCameraPermission);
419 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_PAUSE_VIDEO, showPauseVideo);
420 if (isVideo) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700421 mInCallButtonUi.setVideoPaused(!call.getVideoTech().isTransmitting() || !hasCameraPermission);
Eric Erfanianccca3152017-02-22 16:32:36 -0800422 }
423 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_DIALPAD, true);
424 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_MERGE, showMerge);
425
426 mInCallButtonUi.updateButtonStates();
427 }
428
429 private boolean hasVideoCallCapabilities(DialerCall call) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700430 return call.getVideoTech().isAvailable();
Eric Erfanianccca3152017-02-22 16:32:36 -0800431 }
432
433 /**
434 * Determines if downgrading from a video call to an audio-only call is supported. In order to
435 * support downgrade to audio, the SDK version must be >= N and the call should NOT have the
436 * {@link android.telecom.Call.Details#CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO}.
437 *
438 * @param call The call.
439 * @return {@code true} if downgrading to an audio-only call from a video call is supported.
440 */
441 private boolean isDowngradeToAudioSupported(DialerCall call) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700442 // TODO(b/33676907): If there is an RCS video share session, return true here
Eric Erfanianccca3152017-02-22 16:32:36 -0800443 return !call.can(CallCompat.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO);
444 }
445
446 @Override
447 public void refreshMuteState() {
448 // Restore the previous mute state
449 if (mAutomaticallyMuted
450 && AudioModeProvider.getInstance().getAudioState().isMuted() != mPreviousMuteState) {
451 if (mInCallButtonUi == null) {
452 return;
453 }
Eric Erfanian9a090c82017-03-16 19:22:24 -0700454 muteClicked(mPreviousMuteState, false /* clickedByUser */);
Eric Erfanianccca3152017-02-22 16:32:36 -0800455 }
456 mAutomaticallyMuted = false;
457 }
458
459 @Override
460 public void onSaveInstanceState(Bundle outState) {
461 outState.putBoolean(KEY_AUTOMATICALLY_MUTED, mAutomaticallyMuted);
462 outState.putBoolean(KEY_PREVIOUS_MUTE_STATE, mPreviousMuteState);
463 }
464
465 @Override
466 public void onRestoreInstanceState(Bundle savedInstanceState) {
467 mAutomaticallyMuted =
468 savedInstanceState.getBoolean(KEY_AUTOMATICALLY_MUTED, mAutomaticallyMuted);
469 mPreviousMuteState = savedInstanceState.getBoolean(KEY_PREVIOUS_MUTE_STATE, mPreviousMuteState);
470 }
471
472 @Override
473 public void onCameraPermissionGranted() {
474 if (mCall != null) {
475 updateButtonsState(mCall);
476 }
477 }
478
479 @Override
480 public void onActiveCameraSelectionChanged(boolean isUsingFrontFacingCamera) {
481 if (mInCallButtonUi == null) {
482 return;
483 }
484 mInCallButtonUi.setCameraSwitched(!isUsingFrontFacingCamera);
485 }
486
487 @Override
488 public Context getContext() {
489 return mContext;
490 }
491
492 private InCallActivity getActivity() {
493 if (mInCallButtonUi != null) {
494 Fragment fragment = mInCallButtonUi.getInCallButtonUiFragment();
495 if (fragment != null) {
496 return (InCallActivity) fragment.getActivity();
497 }
498 }
499 return null;
500 }
501}