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