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 if (pause) { 326 videoCall.setCamera(null); 327 VideoProfile videoProfile = new VideoProfile( 328 mCall.getVideoState() & ~VideoProfile.STATE_TX_ENABLED); 329 videoCall.sendSessionModifyRequest(videoProfile); 330 } else { 331 InCallCameraManager cameraManager = InCallPresenter.getInstance(). 332 getInCallCameraManager(); 333 videoCall.setCamera(cameraManager.getActiveCameraId()); 334 VideoProfile videoProfile = new VideoProfile( 335 mCall.getVideoState() | VideoProfile.STATE_TX_ENABLED); 336 videoCall.sendSessionModifyRequest(videoProfile); 337 mCall.setSessionModificationState(Call.SessionModificationState.WAITING_FOR_RESPONSE); 338 } 339 getUi().setVideoPaused(pause); 340 } 341 updateUi(InCallState state, Call call)342 private void updateUi(InCallState state, Call call) { 343 Log.d(this, "Updating call UI for call: ", call); 344 345 final CallButtonUi ui = getUi(); 346 if (ui == null) { 347 return; 348 } 349 350 final boolean isEnabled = 351 state.isConnectingOrConnected() &&!state.isIncoming() && call != null; 352 ui.setEnabled(isEnabled); 353 354 if (call == null) { 355 return; 356 } 357 358 updateButtonsState(call); 359 } 360 361 /** 362 * Updates the buttons applicable for the UI. 363 * 364 * @param call The active call. 365 */ updateButtonsState(Call call)366 private void updateButtonsState(Call call) { 367 Log.v(this, "updateButtonsState"); 368 final CallButtonUi ui = getUi(); 369 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 ui.showButton(BUTTON_DIALPAD, true); 402 ui.showButton(BUTTON_MERGE, showMerge); 403 404 ui.updateButtonStates(); 405 } 406 hasVideoCallCapabilities(Call call)407 private boolean hasVideoCallCapabilities(Call call) { 408 if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) >= Build.VERSION_CODES.M) { 409 return call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX) 410 && call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_RX); 411 } 412 // In L, this single flag represents both video transmitting and receiving capabilities 413 return call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX); 414 } 415 416 /** 417 * Determines if downgrading from a video call to an audio-only call is supported. In order to 418 * support downgrade to audio, the SDK version must be >= N and the call should NOT have the 419 * {@link android.telecom.Call.Details#CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO}. 420 * @param call The call. 421 * @return {@code true} if downgrading to an audio-only call from a video call is supported. 422 */ isDowngradeToAudioSupported(Call call)423 private boolean isDowngradeToAudioSupported(Call call) { 424 return !call.can(CallSdkCompat.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO); 425 } 426 refreshMuteState()427 public void refreshMuteState() { 428 // Restore the previous mute state 429 if (mAutomaticallyMuted && 430 AudioModeProvider.getInstance().getMute() != mPreviousMuteState) { 431 if (getUi() == null) { 432 return; 433 } 434 muteClicked(mPreviousMuteState); 435 } 436 mAutomaticallyMuted = false; 437 } 438 439 @Override onSaveInstanceState(Bundle outState)440 public void onSaveInstanceState(Bundle outState) { 441 super.onSaveInstanceState(outState); 442 outState.putBoolean(KEY_AUTOMATICALLY_MUTED, mAutomaticallyMuted); 443 outState.putBoolean(KEY_PREVIOUS_MUTE_STATE, mPreviousMuteState); 444 } 445 446 @Override onRestoreInstanceState(Bundle savedInstanceState)447 public void onRestoreInstanceState(Bundle savedInstanceState) { 448 mAutomaticallyMuted = 449 savedInstanceState.getBoolean(KEY_AUTOMATICALLY_MUTED, mAutomaticallyMuted); 450 mPreviousMuteState = 451 savedInstanceState.getBoolean(KEY_PREVIOUS_MUTE_STATE, mPreviousMuteState); 452 super.onRestoreInstanceState(savedInstanceState); 453 } 454 455 public interface CallButtonUi extends Ui { showButton(int buttonId, boolean show)456 void showButton(int buttonId, boolean show); enableButton(int buttonId, boolean enable)457 void enableButton(int buttonId, boolean enable); setEnabled(boolean on)458 void setEnabled(boolean on); setMute(boolean on)459 void setMute(boolean on); setHold(boolean on)460 void setHold(boolean on); setCameraSwitched(boolean isBackFacingCamera)461 void setCameraSwitched(boolean isBackFacingCamera); setVideoPaused(boolean isPaused)462 void setVideoPaused(boolean isPaused); setAudio(int mode)463 void setAudio(int mode); setSupportedAudio(int mask)464 void setSupportedAudio(int mask); displayDialpad(boolean on, boolean animate)465 void displayDialpad(boolean on, boolean animate); isDialpadVisible()466 boolean isDialpadVisible(); 467 468 /** 469 * Once showButton() has been called on each of the individual buttons in the UI, call 470 * this to configure the overflow menu appropriately. 471 */ updateButtonStates()472 void updateButtonStates(); getContext()473 Context getContext(); 474 } 475 476 @Override onActiveCameraSelectionChanged(boolean isUsingFrontFacingCamera)477 public void onActiveCameraSelectionChanged(boolean isUsingFrontFacingCamera) { 478 if (getUi() == null) { 479 return; 480 } 481 getUi().setCameraSwitched(!isUsingFrontFacingCamera); 482 } 483 } 484