1 /* 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 17 package com.android.incallui; 18 19 import android.content.Context; 20 import android.os.Bundle; 21 import android.support.v4.app.Fragment; 22 import android.support.v4.os.UserManagerCompat; 23 import android.telecom.CallAudioState; 24 import com.android.contacts.common.compat.CallCompat; 25 import com.android.dialer.common.Assert; 26 import com.android.dialer.common.LogUtil; 27 import com.android.dialer.logging.DialerImpression; 28 import com.android.dialer.logging.Logger; 29 import com.android.incallui.InCallCameraManager.Listener; 30 import com.android.incallui.InCallPresenter.CanAddCallListener; 31 import com.android.incallui.InCallPresenter.InCallDetailsListener; 32 import com.android.incallui.InCallPresenter.InCallState; 33 import com.android.incallui.InCallPresenter.InCallStateListener; 34 import com.android.incallui.InCallPresenter.IncomingCallListener; 35 import com.android.incallui.audiomode.AudioModeProvider; 36 import com.android.incallui.audiomode.AudioModeProvider.AudioModeListener; 37 import com.android.incallui.call.CallList; 38 import com.android.incallui.call.DialerCall; 39 import com.android.incallui.call.DialerCall.CameraDirection; 40 import com.android.incallui.call.TelecomAdapter; 41 import com.android.incallui.incall.protocol.InCallButtonIds; 42 import com.android.incallui.incall.protocol.InCallButtonUi; 43 import com.android.incallui.incall.protocol.InCallButtonUiDelegate; 44 import com.android.incallui.videotech.utils.VideoUtils; 45 46 /** Logic for call buttons. */ 47 public 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 211 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 } 223 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 261 muteClicked(true /* checked */, false /* clickedByUser */); 262 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() { 273 LogUtil.enterBlock("CallButtonPresenter.changeToVideoClicked"); 274 Logger.get(mContext) 275 .logCallImpression( 276 DialerImpression.Type.VIDEO_CALL_UPGRADE_REQUESTED, 277 mCall.getUniqueCallId(), 278 mCall.getTimeAddedMs()); 279 mCall.getVideoTech().upgradeToVideo(); 280 } 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) { 303 InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager(); 304 cameraManager.setUseFrontFacingCamera(useFrontFacingCamera); 305 306 String cameraId = cameraManager.getActiveCameraId(); 307 if (cameraId != null) { 308 final int cameraDir = 309 cameraManager.isUsingFrontFacingCamera() 310 ? CameraDirection.CAMERA_DIRECTION_FRONT_FACING 311 : CameraDirection.CAMERA_DIRECTION_BACK_FACING; 312 mCall.setCameraDir(cameraDir); 313 mCall.getVideoTech().setCamera(cameraId); 314 } 315 } 316 317 @Override 318 public void toggleCameraClicked() { 319 LogUtil.i("CallButtonPresenter.toggleCameraClicked", ""); 320 Logger.get(mContext) 321 .logCallImpression( 322 DialerImpression.Type.IN_CALL_SCREEN_SWAP_CAMERA, 323 mCall.getUniqueCallId(), 324 mCall.getTimeAddedMs()); 325 switchCameraClicked( 326 !InCallPresenter.getInstance().getInCallCameraManager().isUsingFrontFacingCamera()); 327 } 328 329 /** 330 * Stop or start client's video transmission. 331 * 332 * @param pause True if pausing the local user's video, or false if starting the local user's 333 * video. 334 */ 335 @Override 336 public void pauseVideoClicked(boolean pause) { 337 LogUtil.i("CallButtonPresenter.pauseVideoClicked", "%s", pause ? "pause" : "unpause"); 338 339 Logger.get(mContext) 340 .logCallImpression( 341 pause 342 ? DialerImpression.Type.IN_CALL_SCREEN_TURN_OFF_VIDEO 343 : DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_VIDEO, 344 mCall.getUniqueCallId(), 345 mCall.getTimeAddedMs()); 346 347 if (pause) { 348 mCall.getVideoTech().stopTransmission(); 349 } else { 350 mCall.getVideoTech().resumeTransmission(); 351 } 352 353 mInCallButtonUi.setVideoPaused(pause); 354 mInCallButtonUi.enableButton(InCallButtonIds.BUTTON_PAUSE_VIDEO, false); 355 } 356 357 private void updateUi(InCallState state, DialerCall call) { 358 LogUtil.v("CallButtonPresenter", "updating call UI for call: ", call); 359 360 if (mInCallButtonUi == null) { 361 return; 362 } 363 364 if (call != null) { 365 mInCallButtonUi.updateInCallButtonUiColors(); 366 } 367 368 final boolean isEnabled = 369 state.isConnectingOrConnected() && !state.isIncoming() && call != null; 370 mInCallButtonUi.setEnabled(isEnabled); 371 372 if (call == null) { 373 return; 374 } 375 376 updateButtonsState(call); 377 } 378 379 /** 380 * Updates the buttons applicable for the UI. 381 * 382 * @param call The active call. 383 */ 384 private void updateButtonsState(DialerCall call) { 385 LogUtil.v("CallButtonPresenter.updateButtonsState", ""); 386 final boolean isVideo = call.isVideoCall(); 387 388 // Common functionality (audio, hold, etc). 389 // Show either HOLD or SWAP, but not both. If neither HOLD or SWAP is available: 390 // (1) If the device normally can hold, show HOLD in a disabled state. 391 // (2) If the device doesn't have the concept of hold/swap, remove the button. 392 final boolean showSwap = call.can(android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE); 393 final boolean showHold = 394 !showSwap 395 && call.can(android.telecom.Call.Details.CAPABILITY_SUPPORT_HOLD) 396 && call.can(android.telecom.Call.Details.CAPABILITY_HOLD); 397 final boolean isCallOnHold = call.getState() == DialerCall.State.ONHOLD; 398 399 final boolean showAddCall = 400 TelecomAdapter.getInstance().canAddCall() && UserManagerCompat.isUserUnlocked(mContext); 401 final boolean showMerge = call.can(android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE); 402 final boolean showUpgradeToVideo = !isVideo && (hasVideoCallCapabilities(call)); 403 final boolean showDowngradeToAudio = isVideo && isDowngradeToAudioSupported(call); 404 final boolean showMute = call.can(android.telecom.Call.Details.CAPABILITY_MUTE); 405 406 final boolean hasCameraPermission = 407 isVideo && VideoUtils.hasCameraPermissionAndAllowedByUser(mContext); 408 // Disabling local video doesn't seem to work when dialing. See b/30256571. 409 final boolean showPauseVideo = 410 isVideo 411 && call.getState() != DialerCall.State.DIALING 412 && call.getState() != DialerCall.State.CONNECTING; 413 414 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_AUDIO, true); 415 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_SWAP, showSwap); 416 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_HOLD, showHold); 417 mInCallButtonUi.setHold(isCallOnHold); 418 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_MUTE, showMute); 419 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_ADD_CALL, true); 420 mInCallButtonUi.enableButton(InCallButtonIds.BUTTON_ADD_CALL, showAddCall); 421 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO, showUpgradeToVideo); 422 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_DOWNGRADE_TO_AUDIO, showDowngradeToAudio); 423 mInCallButtonUi.showButton( 424 InCallButtonIds.BUTTON_SWITCH_CAMERA, isVideo && hasCameraPermission); 425 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_PAUSE_VIDEO, showPauseVideo); 426 if (isVideo) { 427 mInCallButtonUi.setVideoPaused(!call.getVideoTech().isTransmitting() || !hasCameraPermission); 428 } 429 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_DIALPAD, true); 430 mInCallButtonUi.showButton(InCallButtonIds.BUTTON_MERGE, showMerge); 431 432 mInCallButtonUi.updateButtonStates(); 433 } 434 435 private boolean hasVideoCallCapabilities(DialerCall call) { 436 return call.getVideoTech().isAvailable(mContext); 437 } 438 439 /** 440 * Determines if downgrading from a video call to an audio-only call is supported. In order to 441 * support downgrade to audio, the SDK version must be >= N and the call should NOT have the 442 * {@link android.telecom.Call.Details#CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO}. 443 * 444 * @param call The call. 445 * @return {@code true} if downgrading to an audio-only call from a video call is supported. 446 */ 447 private boolean isDowngradeToAudioSupported(DialerCall call) { 448 // TODO(b/33676907): If there is an RCS video share session, return true here 449 return !call.can(CallCompat.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO); 450 } 451 452 @Override 453 public void refreshMuteState() { 454 // Restore the previous mute state 455 if (mAutomaticallyMuted 456 && AudioModeProvider.getInstance().getAudioState().isMuted() != mPreviousMuteState) { 457 if (mInCallButtonUi == null) { 458 return; 459 } 460 muteClicked(mPreviousMuteState, false /* clickedByUser */); 461 } 462 mAutomaticallyMuted = false; 463 } 464 465 @Override 466 public void onSaveInstanceState(Bundle outState) { 467 outState.putBoolean(KEY_AUTOMATICALLY_MUTED, mAutomaticallyMuted); 468 outState.putBoolean(KEY_PREVIOUS_MUTE_STATE, mPreviousMuteState); 469 } 470 471 @Override 472 public void onRestoreInstanceState(Bundle savedInstanceState) { 473 mAutomaticallyMuted = 474 savedInstanceState.getBoolean(KEY_AUTOMATICALLY_MUTED, mAutomaticallyMuted); 475 mPreviousMuteState = savedInstanceState.getBoolean(KEY_PREVIOUS_MUTE_STATE, mPreviousMuteState); 476 } 477 478 @Override 479 public void onCameraPermissionGranted() { 480 if (mCall != null) { 481 updateButtonsState(mCall); 482 } 483 } 484 485 @Override 486 public void onActiveCameraSelectionChanged(boolean isUsingFrontFacingCamera) { 487 if (mInCallButtonUi == null) { 488 return; 489 } 490 mInCallButtonUi.setCameraSwitched(!isUsingFrontFacingCamera); 491 } 492 493 @Override 494 public Context getContext() { 495 return mContext; 496 } 497 498 private InCallActivity getActivity() { 499 if (mInCallButtonUi != null) { 500 Fragment fragment = mInCallButtonUi.getInCallButtonUiFragment(); 501 if (fragment != null) { 502 return (InCallActivity) fragment.getActivity(); 503 } 504 } 505 return null; 506 } 507 } 508