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