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