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