• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
CallButtonPresenter(Context context)66   public CallButtonPresenter(Context context) {
67     mContext = context.getApplicationContext();
68   }
69 
70   @Override
onInCallButtonUiReady(InCallButtonUi ui)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
onInCallButtonUiUnready()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
onStateChange(InCallState oldState, InCallState newState, CallList callList)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
onDetailsChanged(DialerCall call, android.telecom.Call.Details details)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
onIncomingCall(InCallState oldState, InCallState newState, DialerCall call)146   public void onIncomingCall(InCallState oldState, InCallState newState, DialerCall call) {
147     onStateChange(oldState, newState, CallList.getInstance());
148   }
149 
150   @Override
onCanAddCallChanged(boolean canAddCall)151   public void onCanAddCallChanged(boolean canAddCall) {
152     if (mInCallButtonUi != null && mCall != null) {
153       updateButtonsState(mCall);
154     }
155   }
156 
157   @Override
onAudioStateChanged(CallAudioState audioState)158   public void onAudioStateChanged(CallAudioState audioState) {
159     if (mInCallButtonUi != null) {
160       mInCallButtonUi.setAudioState(audioState);
161     }
162   }
163 
164   @Override
getCurrentAudioState()165   public CallAudioState getCurrentAudioState() {
166     return AudioModeProvider.getInstance().getAudioState();
167   }
168 
169   @Override
setAudioRoute(int route)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
toggleSpeakerphone()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
muteClicked(boolean checked, boolean clickedByUser)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
holdClicked(boolean checked)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
swapClicked()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
mergeClicked()251   public void mergeClicked() {
252     TelecomAdapter.getInstance().merge(mCall.getId());
253   }
254 
255   @Override
addCallClicked()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
showDialpadClicked(boolean checked)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
changeToVideoClicked()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
onEndCallClicked()283   public void onEndCallClicked() {
284     LogUtil.i("CallButtonPresenter.onEndCallClicked", "call: " + mCall);
285     if (mCall != null) {
286       mCall.disconnect();
287     }
288   }
289 
290   @Override
showAudioRouteSelector()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
switchCameraClicked(boolean useFrontFacingCamera)302   public void switchCameraClicked(boolean useFrontFacingCamera) {
303     updateCamera(useFrontFacingCamera);
304   }
305 
306   @Override
toggleCameraClicked()307   public void toggleCameraClicked() {
308     LogUtil.i("CallButtonPresenter.toggleCameraClicked", "");
309     if (mCall == null) {
310       return;
311     }
312     Logger.get(mContext)
313         .logCallImpression(
314             DialerImpression.Type.IN_CALL_SCREEN_SWAP_CAMERA,
315             mCall.getUniqueCallId(),
316             mCall.getTimeAddedMs());
317     switchCameraClicked(
318         !InCallPresenter.getInstance().getInCallCameraManager().isUsingFrontFacingCamera());
319   }
320 
321   /**
322    * Stop or start client's video transmission.
323    *
324    * @param pause True if pausing the local user's video, or false if starting the local user's
325    *     video.
326    */
327   @Override
pauseVideoClicked(boolean pause)328   public void pauseVideoClicked(boolean pause) {
329     LogUtil.i("CallButtonPresenter.pauseVideoClicked", "%s", pause ? "pause" : "unpause");
330 
331     Logger.get(mContext)
332         .logCallImpression(
333             pause
334                 ? DialerImpression.Type.IN_CALL_SCREEN_TURN_OFF_VIDEO
335                 : DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_VIDEO,
336             mCall.getUniqueCallId(),
337             mCall.getTimeAddedMs());
338 
339     if (pause) {
340       mCall.getVideoTech().setCamera(null);
341       mCall.getVideoTech().stopTransmission();
342     } else {
343       updateCamera(
344           InCallPresenter.getInstance().getInCallCameraManager().isUsingFrontFacingCamera());
345       mCall.getVideoTech().resumeTransmission();
346     }
347 
348     mInCallButtonUi.setVideoPaused(pause);
349     mInCallButtonUi.enableButton(InCallButtonIds.BUTTON_PAUSE_VIDEO, false);
350   }
351 
updateCamera(boolean useFrontFacingCamera)352   private void updateCamera(boolean useFrontFacingCamera) {
353     InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager();
354     cameraManager.setUseFrontFacingCamera(useFrontFacingCamera);
355 
356     String cameraId = cameraManager.getActiveCameraId();
357     if (cameraId != null) {
358       final int cameraDir =
359           cameraManager.isUsingFrontFacingCamera()
360               ? CameraDirection.CAMERA_DIRECTION_FRONT_FACING
361               : CameraDirection.CAMERA_DIRECTION_BACK_FACING;
362       mCall.setCameraDir(cameraDir);
363       mCall.getVideoTech().setCamera(cameraId);
364     }
365   }
366 
updateUi(InCallState state, DialerCall call)367   private void updateUi(InCallState state, DialerCall call) {
368     LogUtil.v("CallButtonPresenter", "updating call UI for call: ", call);
369 
370     if (mInCallButtonUi == null) {
371       return;
372     }
373 
374     if (call != null) {
375       mInCallButtonUi.updateInCallButtonUiColors();
376     }
377 
378     final boolean isEnabled =
379         state.isConnectingOrConnected() && !state.isIncoming() && call != null;
380     mInCallButtonUi.setEnabled(isEnabled);
381 
382     if (call == null) {
383       return;
384     }
385 
386     updateButtonsState(call);
387   }
388 
389   /**
390    * Updates the buttons applicable for the UI.
391    *
392    * @param call The active call.
393    */
updateButtonsState(DialerCall call)394   private void updateButtonsState(DialerCall call) {
395     LogUtil.v("CallButtonPresenter.updateButtonsState", "");
396     final boolean isVideo = call.isVideoCall();
397 
398     // Common functionality (audio, hold, etc).
399     // Show either HOLD or SWAP, but not both. If neither HOLD or SWAP is available:
400     //     (1) If the device normally can hold, show HOLD in a disabled state.
401     //     (2) If the device doesn't have the concept of hold/swap, remove the button.
402     final boolean showSwap = call.can(android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE);
403     final boolean showHold =
404         !showSwap
405             && call.can(android.telecom.Call.Details.CAPABILITY_SUPPORT_HOLD)
406             && call.can(android.telecom.Call.Details.CAPABILITY_HOLD);
407     final boolean isCallOnHold = call.getState() == DialerCall.State.ONHOLD;
408 
409     final boolean showAddCall =
410         TelecomAdapter.getInstance().canAddCall() && UserManagerCompat.isUserUnlocked(mContext);
411     final boolean showMerge = call.can(android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE);
412     final boolean showUpgradeToVideo = !isVideo && (hasVideoCallCapabilities(call));
413     final boolean showDowngradeToAudio = isVideo && isDowngradeToAudioSupported(call);
414     final boolean showMute = call.can(android.telecom.Call.Details.CAPABILITY_MUTE);
415 
416     final boolean hasCameraPermission =
417         isVideo && VideoUtils.hasCameraPermissionAndShownPrivacyToast(mContext);
418     // Disabling local video doesn't seem to work when dialing. See b/30256571.
419     final boolean showPauseVideo =
420         isVideo
421             && call.getState() != DialerCall.State.DIALING
422             && call.getState() != DialerCall.State.CONNECTING;
423 
424     mInCallButtonUi.showButton(InCallButtonIds.BUTTON_AUDIO, true);
425     mInCallButtonUi.showButton(InCallButtonIds.BUTTON_SWAP, showSwap);
426     mInCallButtonUi.showButton(InCallButtonIds.BUTTON_HOLD, showHold);
427     mInCallButtonUi.setHold(isCallOnHold);
428     mInCallButtonUi.showButton(InCallButtonIds.BUTTON_MUTE, showMute);
429     mInCallButtonUi.showButton(InCallButtonIds.BUTTON_ADD_CALL, true);
430     mInCallButtonUi.enableButton(InCallButtonIds.BUTTON_ADD_CALL, showAddCall);
431     mInCallButtonUi.showButton(InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO, showUpgradeToVideo);
432     mInCallButtonUi.showButton(InCallButtonIds.BUTTON_DOWNGRADE_TO_AUDIO, showDowngradeToAudio);
433     mInCallButtonUi.showButton(
434         InCallButtonIds.BUTTON_SWITCH_CAMERA, isVideo && hasCameraPermission);
435     mInCallButtonUi.showButton(InCallButtonIds.BUTTON_PAUSE_VIDEO, showPauseVideo);
436     if (isVideo) {
437       mInCallButtonUi.setVideoPaused(!call.getVideoTech().isTransmitting() || !hasCameraPermission);
438     }
439     mInCallButtonUi.showButton(InCallButtonIds.BUTTON_DIALPAD, true);
440     mInCallButtonUi.showButton(InCallButtonIds.BUTTON_MERGE, showMerge);
441 
442     mInCallButtonUi.updateButtonStates();
443   }
444 
hasVideoCallCapabilities(DialerCall call)445   private boolean hasVideoCallCapabilities(DialerCall call) {
446     return call.getVideoTech().isAvailable(mContext);
447   }
448 
449   /**
450    * Determines if downgrading from a video call to an audio-only call is supported. In order to
451    * support downgrade to audio, the SDK version must be >= N and the call should NOT have the
452    * {@link android.telecom.Call.Details#CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO}.
453    *
454    * @param call The call.
455    * @return {@code true} if downgrading to an audio-only call from a video call is supported.
456    */
isDowngradeToAudioSupported(DialerCall call)457   private boolean isDowngradeToAudioSupported(DialerCall call) {
458     // TODO(b/33676907): If there is an RCS video share session, return true here
459     return !call.can(CallCompat.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO);
460   }
461 
462   @Override
refreshMuteState()463   public void refreshMuteState() {
464     // Restore the previous mute state
465     if (mAutomaticallyMuted
466         && AudioModeProvider.getInstance().getAudioState().isMuted() != mPreviousMuteState) {
467       if (mInCallButtonUi == null) {
468         return;
469       }
470       muteClicked(mPreviousMuteState, false /* clickedByUser */);
471     }
472     mAutomaticallyMuted = false;
473   }
474 
475   @Override
onSaveInstanceState(Bundle outState)476   public void onSaveInstanceState(Bundle outState) {
477     outState.putBoolean(KEY_AUTOMATICALLY_MUTED, mAutomaticallyMuted);
478     outState.putBoolean(KEY_PREVIOUS_MUTE_STATE, mPreviousMuteState);
479   }
480 
481   @Override
onRestoreInstanceState(Bundle savedInstanceState)482   public void onRestoreInstanceState(Bundle savedInstanceState) {
483     mAutomaticallyMuted =
484         savedInstanceState.getBoolean(KEY_AUTOMATICALLY_MUTED, mAutomaticallyMuted);
485     mPreviousMuteState = savedInstanceState.getBoolean(KEY_PREVIOUS_MUTE_STATE, mPreviousMuteState);
486   }
487 
488   @Override
onCameraPermissionGranted()489   public void onCameraPermissionGranted() {
490     if (mCall != null) {
491       updateButtonsState(mCall);
492     }
493   }
494 
495   @Override
onActiveCameraSelectionChanged(boolean isUsingFrontFacingCamera)496   public void onActiveCameraSelectionChanged(boolean isUsingFrontFacingCamera) {
497     if (mInCallButtonUi == null) {
498       return;
499     }
500     mInCallButtonUi.setCameraSwitched(!isUsingFrontFacingCamera);
501   }
502 
503   @Override
getContext()504   public Context getContext() {
505     return mContext;
506   }
507 
getActivity()508   private InCallActivity getActivity() {
509     if (mInCallButtonUi != null) {
510       Fragment fragment = mInCallButtonUi.getInCallButtonUiFragment();
511       if (fragment != null) {
512         return (InCallActivity) fragment.getActivity();
513       }
514     }
515     return null;
516   }
517 }
518