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