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_COUNT;
22 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_DIALPAD;
23 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_DOWNGRADE_TO_AUDIO;
24 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_HOLD;
25 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_MANAGE_VIDEO_CONFERENCE;
26 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_MERGE;
27 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_MUTE;
28 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_PAUSE_VIDEO;
29 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_SWAP;
30 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_SWITCH_CAMERA;
31 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_UPGRADE_TO_VIDEO;
32 
33 import android.content.Context;
34 import android.content.res.ColorStateList;
35 import android.content.res.Resources;
36 import android.graphics.drawable.Drawable;
37 import android.graphics.drawable.GradientDrawable;
38 import android.graphics.drawable.LayerDrawable;
39 import android.graphics.drawable.RippleDrawable;
40 import android.graphics.drawable.StateListDrawable;
41 import android.os.Bundle;
42 import android.telecom.CallAudioState;
43 import android.util.SparseIntArray;
44 import android.view.ContextThemeWrapper;
45 import android.view.HapticFeedbackConstants;
46 import android.view.LayoutInflater;
47 import android.view.Menu;
48 import android.view.MenuItem;
49 import android.view.View;
50 import android.view.ViewGroup;
51 import android.widget.CompoundButton;
52 import android.widget.ImageButton;
53 import android.widget.PopupMenu;
54 import android.widget.PopupMenu.OnDismissListener;
55 import android.widget.PopupMenu.OnMenuItemClickListener;
56 
57 import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette;
58 import com.android.dialer.R;
59 
60 /**
61  * Fragment for call control buttons
62  */
63 public class CallButtonFragment
64         extends BaseFragment<CallButtonPresenter, CallButtonPresenter.CallButtonUi>
65         implements CallButtonPresenter.CallButtonUi, OnMenuItemClickListener, OnDismissListener,
66         View.OnClickListener {
67 
68     private static final int INVALID_INDEX = -1;
69     private int mButtonMaxVisible;
70     // The button is currently visible in the UI
71     private static final int BUTTON_VISIBLE = 1;
72     // The button is hidden in the UI
73     private static final int BUTTON_HIDDEN = 2;
74     // The button has been collapsed into the overflow menu
75     private static final int BUTTON_MENU = 3;
76 
77     public interface Buttons {
78 
79         public static final int BUTTON_AUDIO = 0;
80         public static final int BUTTON_MUTE = 1;
81         public static final int BUTTON_DIALPAD = 2;
82         public static final int BUTTON_HOLD = 3;
83         public static final int BUTTON_SWAP = 4;
84         public static final int BUTTON_UPGRADE_TO_VIDEO = 5;
85         public static final int BUTTON_SWITCH_CAMERA = 6;
86         public static final int BUTTON_DOWNGRADE_TO_AUDIO = 7;
87         public static final int BUTTON_ADD_CALL = 8;
88         public static final int BUTTON_MERGE = 9;
89         public static final int BUTTON_PAUSE_VIDEO = 10;
90         public static final int BUTTON_MANAGE_VIDEO_CONFERENCE = 11;
91         public static final int BUTTON_COUNT = 12;
92     }
93 
94     private SparseIntArray mButtonVisibilityMap = new SparseIntArray(BUTTON_COUNT);
95 
96     private CompoundButton mAudioButton;
97     private CompoundButton mMuteButton;
98     private CompoundButton mShowDialpadButton;
99     private CompoundButton mHoldButton;
100     private ImageButton mSwapButton;
101     private ImageButton mChangeToVideoButton;
102     private ImageButton mChangeToVoiceButton;
103     private CompoundButton mSwitchCameraButton;
104     private ImageButton mAddCallButton;
105     private ImageButton mMergeButton;
106     private CompoundButton mPauseVideoButton;
107     private ImageButton mOverflowButton;
108     private ImageButton mManageVideoCallConferenceButton;
109 
110     private PopupMenu mAudioModePopup;
111     private boolean mAudioModePopupVisible;
112     private PopupMenu mOverflowPopup;
113 
114     private int mPrevAudioMode = 0;
115 
116     // Constants for Drawable.setAlpha()
117     private static final int HIDDEN = 0;
118     private static final int VISIBLE = 255;
119 
120     private boolean mIsEnabled;
121     private MaterialPalette mCurrentThemeColors;
122 
123     @Override
createPresenter()124     public CallButtonPresenter createPresenter() {
125         // TODO: find a cleaner way to include audio mode provider than having a singleton instance.
126         return new CallButtonPresenter();
127     }
128 
129     @Override
getUi()130     public CallButtonPresenter.CallButtonUi getUi() {
131         return this;
132     }
133 
134     @Override
onCreate(Bundle savedInstanceState)135     public void onCreate(Bundle savedInstanceState) {
136         super.onCreate(savedInstanceState);
137 
138         for (int i = 0; i < BUTTON_COUNT; i++) {
139             mButtonVisibilityMap.put(i, BUTTON_HIDDEN);
140         }
141 
142         mButtonMaxVisible = getResources().getInteger(R.integer.call_card_max_buttons);
143     }
144 
145     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)146     public View onCreateView(LayoutInflater inflater, ViewGroup container,
147             Bundle savedInstanceState) {
148         final View parent = inflater.inflate(R.layout.call_button_fragment, container, false);
149 
150         mAudioButton = (CompoundButton) parent.findViewById(R.id.audioButton);
151         mAudioButton.setOnClickListener(this);
152         mMuteButton = (CompoundButton) parent.findViewById(R.id.muteButton);
153         mMuteButton.setOnClickListener(this);
154         mShowDialpadButton = (CompoundButton) parent.findViewById(R.id.dialpadButton);
155         mShowDialpadButton.setOnClickListener(this);
156         mHoldButton = (CompoundButton) parent.findViewById(R.id.holdButton);
157         mHoldButton.setOnClickListener(this);
158         mSwapButton = (ImageButton) parent.findViewById(R.id.swapButton);
159         mSwapButton.setOnClickListener(this);
160         mChangeToVideoButton = (ImageButton) parent.findViewById(R.id.changeToVideoButton);
161         mChangeToVideoButton.setOnClickListener(this);
162         mChangeToVoiceButton = (ImageButton) parent.findViewById(R.id.changeToVoiceButton);
163         mChangeToVoiceButton.setOnClickListener(this);
164         mSwitchCameraButton = (CompoundButton) parent.findViewById(R.id.switchCameraButton);
165         mSwitchCameraButton.setOnClickListener(this);
166         mAddCallButton = (ImageButton) parent.findViewById(R.id.addButton);
167         mAddCallButton.setOnClickListener(this);
168         mMergeButton = (ImageButton) parent.findViewById(R.id.mergeButton);
169         mMergeButton.setOnClickListener(this);
170         mPauseVideoButton = (CompoundButton) parent.findViewById(R.id.pauseVideoButton);
171         mPauseVideoButton.setOnClickListener(this);
172         mOverflowButton = (ImageButton) parent.findViewById(R.id.overflowButton);
173         mOverflowButton.setOnClickListener(this);
174         mManageVideoCallConferenceButton = (ImageButton) parent.findViewById(
175                 R.id.manageVideoCallConferenceButton);
176         mManageVideoCallConferenceButton.setOnClickListener(this);
177         return parent;
178     }
179 
180     @Override
onActivityCreated(Bundle savedInstanceState)181     public void onActivityCreated(Bundle savedInstanceState) {
182         super.onActivityCreated(savedInstanceState);
183 
184         // set the buttons
185         updateAudioButtons(getPresenter().getSupportedAudio());
186     }
187 
188     @Override
onResume()189     public void onResume() {
190         if (getPresenter() != null) {
191             getPresenter().refreshMuteState();
192         }
193         super.onResume();
194 
195         updateColors();
196     }
197 
198     @Override
onClick(View view)199     public void onClick(View view) {
200         int id = view.getId();
201         Log.d(this, "onClick(View " + view + ", id " + id + ")...");
202 
203         if (id == R.id.audioButton) {
204             onAudioButtonClicked();
205         } else if (id == R.id.addButton) {
206             getPresenter().addCallClicked();
207         } else if (id == R.id.muteButton) {
208             getPresenter().muteClicked(!mMuteButton.isSelected());
209         } else if (id == R.id.mergeButton) {
210             getPresenter().mergeClicked();
211             mMergeButton.setEnabled(false);
212         } else if (id == R.id.holdButton) {
213             getPresenter().holdClicked(!mHoldButton.isSelected());
214         } else if (id == R.id.swapButton) {
215             getPresenter().swapClicked();
216         } else if (id == R.id.dialpadButton) {
217             getPresenter().showDialpadClicked(!mShowDialpadButton.isSelected());
218         } else if (id == R.id.changeToVideoButton) {
219             getPresenter().changeToVideoClicked();
220         } else if (id == R.id.changeToVoiceButton) {
221             getPresenter().changeToVoiceClicked();
222         } else if (id == R.id.switchCameraButton) {
223             getPresenter().switchCameraClicked(
224                     mSwitchCameraButton.isSelected() /* useFrontFacingCamera */);
225         } else if (id == R.id.pauseVideoButton) {
226             getPresenter().pauseVideoClicked(
227                     !mPauseVideoButton.isSelected() /* pause */);
228         } else if (id == R.id.overflowButton) {
229             if (mOverflowPopup != null) {
230                 mOverflowPopup.show();
231             }
232         } else if (id == R.id.manageVideoCallConferenceButton) {
233             onManageVideoCallConferenceClicked();
234         } else {
235             Log.wtf(this, "onClick: unexpected");
236             return;
237         }
238 
239         view.performHapticFeedback(
240                 HapticFeedbackConstants.VIRTUAL_KEY,
241                 HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
242     }
243 
updateColors()244     public void updateColors() {
245         MaterialPalette themeColors = InCallPresenter.getInstance().getThemeColors();
246 
247         if (mCurrentThemeColors != null && mCurrentThemeColors.equals(themeColors)) {
248             return;
249         }
250 
251         View[] compoundButtons = {
252                 mAudioButton,
253                 mMuteButton,
254                 mShowDialpadButton,
255                 mHoldButton,
256                 mSwitchCameraButton,
257                 mPauseVideoButton
258         };
259 
260         for (View button : compoundButtons) {
261             final LayerDrawable layers = (LayerDrawable) button.getBackground();
262             final RippleDrawable btnCompoundDrawable = compoundBackgroundDrawable(themeColors);
263             layers.setDrawableByLayerId(R.id.compoundBackgroundItem, btnCompoundDrawable);
264         }
265 
266         ImageButton[] normalButtons = {
267                 mSwapButton,
268                 mChangeToVideoButton,
269                 mChangeToVoiceButton,
270                 mAddCallButton,
271                 mMergeButton,
272                 mOverflowButton
273         };
274 
275         for (ImageButton button : normalButtons) {
276             final LayerDrawable layers = (LayerDrawable) button.getBackground();
277             final RippleDrawable btnDrawable = backgroundDrawable(themeColors);
278             layers.setDrawableByLayerId(R.id.backgroundItem, btnDrawable);
279         }
280 
281         mCurrentThemeColors = themeColors;
282     }
283 
284     /**
285      * Generate a RippleDrawable which will be the background for a compound button, i.e.
286      * a button with pressed and unpressed states. The unpressed state will be the same color
287      * as the rest of the call card, the pressed state will be the dark version of that color.
288      */
compoundBackgroundDrawable(MaterialPalette palette)289     private RippleDrawable compoundBackgroundDrawable(MaterialPalette palette) {
290         Resources res = getResources();
291         ColorStateList rippleColor =
292                 ColorStateList.valueOf(res.getColor(R.color.incall_accent_color));
293 
294         StateListDrawable stateListDrawable = new StateListDrawable();
295         addSelectedAndFocused(res, stateListDrawable);
296         addFocused(res, stateListDrawable);
297         addSelected(res, stateListDrawable, palette);
298         addUnselected(res, stateListDrawable, palette);
299 
300         return new RippleDrawable(rippleColor, stateListDrawable, null);
301     }
302 
303     /**
304      * Generate a RippleDrawable which will be the background of a button to ensure it
305      * is the same color as the rest of the call card.
306      */
backgroundDrawable(MaterialPalette palette)307     private RippleDrawable backgroundDrawable(MaterialPalette palette) {
308         Resources res = getResources();
309         ColorStateList rippleColor =
310                 ColorStateList.valueOf(res.getColor(R.color.incall_accent_color));
311 
312         StateListDrawable stateListDrawable = new StateListDrawable();
313         addFocused(res, stateListDrawable);
314         addUnselected(res, stateListDrawable, palette);
315 
316         return new RippleDrawable(rippleColor, stateListDrawable, null);
317     }
318 
319     // state_selected and state_focused
addSelectedAndFocused(Resources res, StateListDrawable drawable)320     private void addSelectedAndFocused(Resources res, StateListDrawable drawable) {
321         int[] selectedAndFocused = {android.R.attr.state_selected, android.R.attr.state_focused};
322         Drawable selectedAndFocusedDrawable = res.getDrawable(R.drawable.btn_selected_focused);
323         drawable.addState(selectedAndFocused, selectedAndFocusedDrawable);
324     }
325 
326     // state_focused
addFocused(Resources res, StateListDrawable drawable)327     private void addFocused(Resources res, StateListDrawable drawable) {
328         int[] focused = {android.R.attr.state_focused};
329         Drawable focusedDrawable = res.getDrawable(R.drawable.btn_unselected_focused);
330         drawable.addState(focused, focusedDrawable);
331     }
332 
333     // state_selected
addSelected(Resources res, StateListDrawable drawable, MaterialPalette palette)334     private void addSelected(Resources res, StateListDrawable drawable, MaterialPalette palette) {
335         int[] selected = {android.R.attr.state_selected};
336         LayerDrawable selectedDrawable = (LayerDrawable) res.getDrawable(R.drawable.btn_selected);
337         ((GradientDrawable) selectedDrawable.getDrawable(0)).setColor(palette.mSecondaryColor);
338         drawable.addState(selected, selectedDrawable);
339     }
340 
341     // default
addUnselected(Resources res, StateListDrawable drawable, MaterialPalette palette)342     private void addUnselected(Resources res, StateListDrawable drawable, MaterialPalette palette) {
343         LayerDrawable unselectedDrawable =
344                 (LayerDrawable) res.getDrawable(R.drawable.btn_unselected);
345         ((GradientDrawable) unselectedDrawable.getDrawable(0)).setColor(palette.mPrimaryColor);
346         drawable.addState(new int[0], unselectedDrawable);
347     }
348 
349     @Override
setEnabled(boolean isEnabled)350     public void setEnabled(boolean isEnabled) {
351         mIsEnabled = isEnabled;
352 
353         mAudioButton.setEnabled(isEnabled);
354         mMuteButton.setEnabled(isEnabled);
355         mShowDialpadButton.setEnabled(isEnabled);
356         mHoldButton.setEnabled(isEnabled);
357         mSwapButton.setEnabled(isEnabled);
358         mChangeToVideoButton.setEnabled(isEnabled);
359         mChangeToVoiceButton.setEnabled(isEnabled);
360         mSwitchCameraButton.setEnabled(isEnabled);
361         mAddCallButton.setEnabled(isEnabled);
362         mMergeButton.setEnabled(isEnabled);
363         mPauseVideoButton.setEnabled(isEnabled);
364         mOverflowButton.setEnabled(isEnabled);
365         mManageVideoCallConferenceButton.setEnabled(isEnabled);
366     }
367 
368     @Override
showButton(int buttonId, boolean show)369     public void showButton(int buttonId, boolean show) {
370         mButtonVisibilityMap.put(buttonId, show ? BUTTON_VISIBLE : BUTTON_HIDDEN);
371     }
372 
373     @Override
enableButton(int buttonId, boolean enable)374     public void enableButton(int buttonId, boolean enable) {
375         final View button = getButtonById(buttonId);
376         if (button != null) {
377             button.setEnabled(enable);
378         }
379     }
380 
getButtonById(int id)381     private View getButtonById(int id) {
382         if (id == BUTTON_AUDIO) {
383             return mAudioButton;
384         } else if (id == BUTTON_MUTE) {
385             return mMuteButton;
386         } else if (id == BUTTON_DIALPAD) {
387             return mShowDialpadButton;
388         } else if (id == BUTTON_HOLD) {
389             return mHoldButton;
390         } else if (id == BUTTON_SWAP) {
391             return mSwapButton;
392         } else if (id == BUTTON_UPGRADE_TO_VIDEO) {
393             return mChangeToVideoButton;
394         } else if (id == BUTTON_DOWNGRADE_TO_AUDIO) {
395             return mChangeToVoiceButton;
396         } else if (id == BUTTON_SWITCH_CAMERA) {
397             return mSwitchCameraButton;
398         } else if (id == BUTTON_ADD_CALL) {
399             return mAddCallButton;
400         } else if (id == BUTTON_MERGE) {
401             return mMergeButton;
402         } else if (id == BUTTON_PAUSE_VIDEO) {
403             return mPauseVideoButton;
404         } else if (id == BUTTON_MANAGE_VIDEO_CONFERENCE) {
405             return mManageVideoCallConferenceButton;
406         } else {
407             Log.w(this, "Invalid button id");
408             return null;
409         }
410     }
411 
412     @Override
setHold(boolean value)413     public void setHold(boolean value) {
414         if (mHoldButton.isSelected() != value) {
415             mHoldButton.setSelected(value);
416             mHoldButton.setContentDescription(getContext().getString(
417                     value ? R.string.onscreenHoldText_selected
418                             : R.string.onscreenHoldText_unselected));
419         }
420     }
421 
422     @Override
setCameraSwitched(boolean isBackFacingCamera)423     public void setCameraSwitched(boolean isBackFacingCamera) {
424         mSwitchCameraButton.setSelected(isBackFacingCamera);
425     }
426 
427     @Override
setVideoPaused(boolean isPaused)428     public void setVideoPaused(boolean isPaused) {
429         mPauseVideoButton.setSelected(isPaused);
430     }
431 
432     @Override
setMute(boolean value)433     public void setMute(boolean value) {
434         if (mMuteButton.isSelected() != value) {
435             mMuteButton.setSelected(value);
436             mMuteButton.setContentDescription(getContext().getString(
437                     value ? R.string.onscreenMuteText_selected
438                             : R.string.onscreenMuteText_unselected));
439         }
440     }
441 
addToOverflowMenu(int id, View button, PopupMenu menu)442     private void addToOverflowMenu(int id, View button, PopupMenu menu) {
443         button.setVisibility(View.GONE);
444         menu.getMenu().add(Menu.NONE, id, Menu.NONE, button.getContentDescription());
445         mButtonVisibilityMap.put(id, BUTTON_MENU);
446     }
447 
getPopupMenu()448     private PopupMenu getPopupMenu() {
449         return new PopupMenu(new ContextThemeWrapper(getActivity(), R.style.InCallPopupMenuStyle),
450                 mOverflowButton);
451     }
452 
453     /**
454      * Iterates through the list of buttons and toggles their visibility depending on the
455      * setting configured by the CallButtonPresenter. If there are more visible buttons than
456      * the allowed maximum, the excess buttons are collapsed into a single overflow menu.
457      */
458     @Override
updateButtonStates()459     public void updateButtonStates() {
460         View prevVisibleButton = null;
461         int prevVisibleId = -1;
462         PopupMenu menu = null;
463         int visibleCount = 0;
464         for (int i = 0; i < BUTTON_COUNT; i++) {
465             final int visibility = mButtonVisibilityMap.get(i);
466             final View button = getButtonById(i);
467             if (visibility == BUTTON_VISIBLE) {
468                 visibleCount++;
469                 if (visibleCount <= mButtonMaxVisible) {
470                     button.setVisibility(View.VISIBLE);
471                     prevVisibleButton = button;
472                     prevVisibleId = i;
473                 } else {
474                     if (menu == null) {
475                         menu = getPopupMenu();
476                     }
477                     // Collapse the current button into the overflow menu. If is the first visible
478                     // button that exceeds the threshold, also collapse the previous visible button
479                     // so that the total number of visible buttons will never exceed the threshold.
480                     if (prevVisibleButton != null) {
481                         addToOverflowMenu(prevVisibleId, prevVisibleButton, menu);
482                         prevVisibleButton = null;
483                         prevVisibleId = -1;
484                     }
485                     addToOverflowMenu(i, button, menu);
486                 }
487             } else if (visibility == BUTTON_HIDDEN) {
488                 button.setVisibility(View.GONE);
489             }
490         }
491 
492         mOverflowButton.setVisibility(menu != null ? View.VISIBLE : View.GONE);
493         if (menu != null) {
494             mOverflowPopup = menu;
495             mOverflowPopup.setOnMenuItemClickListener(new OnMenuItemClickListener() {
496                 @Override
497                 public boolean onMenuItemClick(MenuItem item) {
498                     final int id = item.getItemId();
499                     getButtonById(id).performClick();
500                     return true;
501                 }
502             });
503         }
504     }
505 
506     @Override
setAudio(int mode)507     public void setAudio(int mode) {
508         updateAudioButtons(getPresenter().getSupportedAudio());
509         refreshAudioModePopup();
510 
511         if (mPrevAudioMode != mode) {
512             updateAudioButtonContentDescription(mode);
513             mPrevAudioMode = mode;
514         }
515     }
516 
517     @Override
setSupportedAudio(int modeMask)518     public void setSupportedAudio(int modeMask) {
519         updateAudioButtons(modeMask);
520         refreshAudioModePopup();
521     }
522 
523     @Override
onMenuItemClick(MenuItem item)524     public boolean onMenuItemClick(MenuItem item) {
525         Log.d(this, "- onMenuItemClick: " + item);
526         Log.d(this, "  id: " + item.getItemId());
527         Log.d(this, "  title: '" + item.getTitle() + "'");
528 
529         int mode = CallAudioState.ROUTE_WIRED_OR_EARPIECE;
530         int resId = item.getItemId();
531 
532         if (resId == R.id.audio_mode_speaker) {
533             mode = CallAudioState.ROUTE_SPEAKER;
534         } else if (resId == R.id.audio_mode_earpiece || resId == R.id.audio_mode_wired_headset) {
535             // InCallCallAudioState.ROUTE_EARPIECE means either the handset earpiece,
536             // or the wired headset (if connected.)
537             mode = CallAudioState.ROUTE_WIRED_OR_EARPIECE;
538         } else if (resId == R.id.audio_mode_bluetooth) {
539             mode = CallAudioState.ROUTE_BLUETOOTH;
540         } else {
541             Log.e(this, "onMenuItemClick:  unexpected View ID " + item.getItemId()
542                     + " (MenuItem = '" + item + "')");
543         }
544 
545         getPresenter().setAudioMode(mode);
546 
547         return true;
548     }
549 
550     // PopupMenu.OnDismissListener implementation; see showAudioModePopup().
551     // This gets called when the PopupMenu gets dismissed for *any* reason, like
552     // the user tapping outside its bounds, or pressing Back, or selecting one
553     // of the menu items.
554     @Override
onDismiss(PopupMenu menu)555     public void onDismiss(PopupMenu menu) {
556         Log.d(this, "- onDismiss: " + menu);
557         mAudioModePopupVisible = false;
558         updateAudioButtons(getPresenter().getSupportedAudio());
559     }
560 
561     /**
562      * Checks for supporting modes.  If bluetooth is supported, it uses the audio
563      * pop up menu.  Otherwise, it toggles the speakerphone.
564      */
onAudioButtonClicked()565     private void onAudioButtonClicked() {
566         Log.d(this, "onAudioButtonClicked: " +
567                 CallAudioState.audioRouteToString(getPresenter().getSupportedAudio()));
568 
569         if (isSupported(CallAudioState.ROUTE_BLUETOOTH)) {
570             showAudioModePopup();
571         } else {
572             getPresenter().toggleSpeakerphone();
573         }
574     }
575 
onManageVideoCallConferenceClicked()576     private void onManageVideoCallConferenceClicked() {
577         Log.d(this, "onManageVideoCallConferenceClicked");
578         InCallPresenter.getInstance().showConferenceCallManager(true);
579     }
580 
581     /**
582      * Refreshes the "Audio mode" popup if it's visible.  This is useful
583      * (for example) when a wired headset is plugged or unplugged,
584      * since we need to switch back and forth between the "earpiece"
585      * and "wired headset" items.
586      *
587      * This is safe to call even if the popup is already dismissed, or even if
588      * you never called showAudioModePopup() in the first place.
589      */
refreshAudioModePopup()590     public void refreshAudioModePopup() {
591         if (mAudioModePopup != null && mAudioModePopupVisible) {
592             // Dismiss the previous one
593             mAudioModePopup.dismiss();  // safe even if already dismissed
594             // And bring up a fresh PopupMenu
595             showAudioModePopup();
596         }
597     }
598 
599     /**
600      * Updates the audio button so that the appriopriate visual layers
601      * are visible based on the supported audio formats.
602      */
updateAudioButtons(int supportedModes)603     private void updateAudioButtons(int supportedModes) {
604         final boolean bluetoothSupported = isSupported(CallAudioState.ROUTE_BLUETOOTH);
605         final boolean speakerSupported = isSupported(CallAudioState.ROUTE_SPEAKER);
606 
607         boolean audioButtonEnabled = false;
608         boolean audioButtonChecked = false;
609         boolean showMoreIndicator = false;
610 
611         boolean showBluetoothIcon = false;
612         boolean showSpeakerphoneIcon = false;
613         boolean showHandsetIcon = false;
614 
615         boolean showToggleIndicator = false;
616 
617         if (bluetoothSupported) {
618             Log.d(this, "updateAudioButtons - popup menu mode");
619 
620             audioButtonEnabled = true;
621             audioButtonChecked = true;
622             showMoreIndicator = true;
623 
624             // Update desired layers:
625             if (isAudio(CallAudioState.ROUTE_BLUETOOTH)) {
626                 showBluetoothIcon = true;
627             } else if (isAudio(CallAudioState.ROUTE_SPEAKER)) {
628                 showSpeakerphoneIcon = true;
629             } else {
630                 showHandsetIcon = true;
631                 // TODO: if a wired headset is plugged in, that takes precedence
632                 // over the handset earpiece.  If so, maybe we should show some
633                 // sort of "wired headset" icon here instead of the "handset
634                 // earpiece" icon.  (Still need an asset for that, though.)
635             }
636 
637             // The audio button is NOT a toggle in this state, so set selected to false.
638             mAudioButton.setSelected(false);
639         } else if (speakerSupported) {
640             Log.d(this, "updateAudioButtons - speaker toggle mode");
641 
642             audioButtonEnabled = true;
643 
644             // The audio button *is* a toggle in this state, and indicated the
645             // current state of the speakerphone.
646             audioButtonChecked = isAudio(CallAudioState.ROUTE_SPEAKER);
647             mAudioButton.setSelected(audioButtonChecked);
648 
649             // update desired layers:
650             showToggleIndicator = true;
651             showSpeakerphoneIcon = true;
652         } else {
653             Log.d(this, "updateAudioButtons - disabled...");
654 
655             // The audio button is a toggle in this state, but that's mostly
656             // irrelevant since it's always disabled and unchecked.
657             audioButtonEnabled = false;
658             audioButtonChecked = false;
659             mAudioButton.setSelected(false);
660 
661             // update desired layers:
662             showToggleIndicator = true;
663             showSpeakerphoneIcon = true;
664         }
665 
666         // Finally, update it all!
667 
668         Log.v(this, "audioButtonEnabled: " + audioButtonEnabled);
669         Log.v(this, "audioButtonChecked: " + audioButtonChecked);
670         Log.v(this, "showMoreIndicator: " + showMoreIndicator);
671         Log.v(this, "showBluetoothIcon: " + showBluetoothIcon);
672         Log.v(this, "showSpeakerphoneIcon: " + showSpeakerphoneIcon);
673         Log.v(this, "showHandsetIcon: " + showHandsetIcon);
674 
675         // Only enable the audio button if the fragment is enabled.
676         mAudioButton.setEnabled(audioButtonEnabled && mIsEnabled);
677         mAudioButton.setChecked(audioButtonChecked);
678 
679         final LayerDrawable layers = (LayerDrawable) mAudioButton.getBackground();
680         Log.d(this, "'layers' drawable: " + layers);
681 
682         layers.findDrawableByLayerId(R.id.compoundBackgroundItem)
683                 .setAlpha(showToggleIndicator ? VISIBLE : HIDDEN);
684 
685         layers.findDrawableByLayerId(R.id.moreIndicatorItem)
686                 .setAlpha(showMoreIndicator ? VISIBLE : HIDDEN);
687 
688         layers.findDrawableByLayerId(R.id.bluetoothItem)
689                 .setAlpha(showBluetoothIcon ? VISIBLE : HIDDEN);
690 
691         layers.findDrawableByLayerId(R.id.handsetItem)
692                 .setAlpha(showHandsetIcon ? VISIBLE : HIDDEN);
693 
694         layers.findDrawableByLayerId(R.id.speakerphoneItem)
695                 .setAlpha(showSpeakerphoneIcon ? VISIBLE : HIDDEN);
696 
697     }
698 
699     /**
700      * Update the content description of the audio button.
701      */
updateAudioButtonContentDescription(int mode)702     private void updateAudioButtonContentDescription(int mode) {
703         int stringId = 0;
704 
705         // If bluetooth is not supported, the audio buttion will toggle, so use the label "speaker".
706         // Otherwise, use the label of the currently selected audio mode.
707         if (!isSupported(CallAudioState.ROUTE_BLUETOOTH)) {
708             stringId = R.string.audio_mode_speaker;
709         } else {
710             switch (mode) {
711                 case CallAudioState.ROUTE_EARPIECE:
712                     stringId = R.string.audio_mode_earpiece;
713                     break;
714                 case CallAudioState.ROUTE_BLUETOOTH:
715                     stringId = R.string.audio_mode_bluetooth;
716                     break;
717                 case CallAudioState.ROUTE_WIRED_HEADSET:
718                     stringId = R.string.audio_mode_wired_headset;
719                     break;
720                 case CallAudioState.ROUTE_SPEAKER:
721                     stringId = R.string.audio_mode_speaker;
722                     break;
723             }
724         }
725 
726         if (stringId != 0) {
727             mAudioButton.setContentDescription(getResources().getString(stringId));
728         }
729     }
730 
showAudioModePopup()731     private void showAudioModePopup() {
732         Log.d(this, "showAudioPopup()...");
733 
734         final ContextThemeWrapper contextWrapper = new ContextThemeWrapper(getActivity(),
735                 R.style.InCallPopupMenuStyle);
736         mAudioModePopup = new PopupMenu(contextWrapper, mAudioButton /* anchorView */);
737         mAudioModePopup.getMenuInflater().inflate(R.menu.incall_audio_mode_menu,
738                 mAudioModePopup.getMenu());
739         mAudioModePopup.setOnMenuItemClickListener(this);
740         mAudioModePopup.setOnDismissListener(this);
741 
742         final Menu menu = mAudioModePopup.getMenu();
743 
744         // TODO: Still need to have the "currently active" audio mode come
745         // up pre-selected (or focused?) with a blue highlight.  Still
746         // need exact visual design, and possibly framework support for this.
747         // See comments below for the exact logic.
748 
749         final MenuItem speakerItem = menu.findItem(R.id.audio_mode_speaker);
750         speakerItem.setEnabled(isSupported(CallAudioState.ROUTE_SPEAKER));
751         // TODO: Show speakerItem as initially "selected" if
752         // speaker is on.
753 
754         // We display *either* "earpiece" or "wired headset", never both,
755         // depending on whether a wired headset is physically plugged in.
756         final MenuItem earpieceItem = menu.findItem(R.id.audio_mode_earpiece);
757         final MenuItem wiredHeadsetItem = menu.findItem(R.id.audio_mode_wired_headset);
758 
759         final boolean usingHeadset = isSupported(CallAudioState.ROUTE_WIRED_HEADSET);
760         earpieceItem.setVisible(!usingHeadset);
761         earpieceItem.setEnabled(!usingHeadset);
762         wiredHeadsetItem.setVisible(usingHeadset);
763         wiredHeadsetItem.setEnabled(usingHeadset);
764         // TODO: Show the above item (either earpieceItem or wiredHeadsetItem)
765         // as initially "selected" if speakerOn and
766         // bluetoothIndicatorOn are both false.
767 
768         final MenuItem bluetoothItem = menu.findItem(R.id.audio_mode_bluetooth);
769         bluetoothItem.setEnabled(isSupported(CallAudioState.ROUTE_BLUETOOTH));
770         // TODO: Show bluetoothItem as initially "selected" if
771         // bluetoothIndicatorOn is true.
772 
773         mAudioModePopup.show();
774 
775         // Unfortunately we need to manually keep track of the popup menu's
776         // visiblity, since PopupMenu doesn't have an isShowing() method like
777         // Dialogs do.
778         mAudioModePopupVisible = true;
779     }
780 
isSupported(int mode)781     private boolean isSupported(int mode) {
782         return (mode == (getPresenter().getSupportedAudio() & mode));
783     }
784 
isAudio(int mode)785     private boolean isAudio(int mode) {
786         return (mode == getPresenter().getAudioMode());
787     }
788 
789     @Override
displayDialpad(boolean value, boolean animate)790     public void displayDialpad(boolean value, boolean animate) {
791         if (getActivity() != null && getActivity() instanceof InCallActivity) {
792             boolean changed = ((InCallActivity) getActivity()).showDialpadFragment(value, animate);
793             if (changed) {
794                 mShowDialpadButton.setSelected(value);
795                 mShowDialpadButton.setContentDescription(getContext().getString(
796                         value /* show */ ? R.string.onscreenShowDialpadText_unselected
797                                 : R.string.onscreenShowDialpadText_selected));
798             }
799         }
800     }
801 
802     @Override
isDialpadVisible()803     public boolean isDialpadVisible() {
804         if (getActivity() != null && getActivity() instanceof InCallActivity) {
805             return ((InCallActivity) getActivity()).isDialpadVisible();
806         }
807         return false;
808     }
809 
810     @Override
getContext()811     public Context getContext() {
812         return getActivity();
813     }
814 }
815