CallButtonFragment.java revision 16a215c34b11744543f0bce8407a40a442a27f6b
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
17package com.android.incallui;
18
19import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_ADD_CALL;
20import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_AUDIO;
21import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_COUNT;
22import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_DIALPAD;
23import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_DOWNGRADE_TO_AUDIO;
24import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_HOLD;
25import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_MANAGE_VIDEO_CONFERENCE;
26import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_MERGE;
27import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_MUTE;
28import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_PAUSE_VIDEO;
29import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_SWAP;
30import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_SWITCH_CAMERA;
31import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_UPGRADE_TO_VIDEO;
32
33import android.content.Context;
34import android.content.res.ColorStateList;
35import android.content.res.Resources;
36import android.graphics.drawable.Drawable;
37import android.graphics.drawable.GradientDrawable;
38import android.graphics.drawable.LayerDrawable;
39import android.graphics.drawable.RippleDrawable;
40import android.graphics.drawable.StateListDrawable;
41import android.os.Bundle;
42import android.telecom.CallAudioState;
43import android.util.SparseIntArray;
44import android.view.ContextThemeWrapper;
45import android.view.HapticFeedbackConstants;
46import android.view.LayoutInflater;
47import android.view.Menu;
48import android.view.MenuItem;
49import android.view.View;
50import android.view.ViewGroup;
51import android.widget.CompoundButton;
52import android.widget.ImageButton;
53import android.widget.PopupMenu;
54import android.widget.PopupMenu.OnDismissListener;
55import android.widget.PopupMenu.OnMenuItemClickListener;
56
57import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette;
58import com.android.dialer.R;
59
60/**
61 * Fragment for call control buttons
62 */
63public 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
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
130    public CallButtonPresenter.CallButtonUi getUi() {
131        return this;
132    }
133
134    @Override
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
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
181    public void onActivityCreated(Bundle savedInstanceState) {
182        super.onActivityCreated(savedInstanceState);
183
184        // set the buttons
185        updateAudioButtons(getPresenter().getSupportedAudio());
186    }
187
188    @Override
189    public void onResume() {
190        if (getPresenter() != null) {
191            getPresenter().refreshMuteState();
192        }
193        super.onResume();
194
195        updateColors();
196    }
197
198    @Override
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
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     */
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     */
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
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
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
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
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
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
369    public void showButton(int buttonId, boolean show) {
370        mButtonVisibilityMap.put(buttonId, show ? BUTTON_VISIBLE : BUTTON_HIDDEN);
371    }
372
373    @Override
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
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
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
423    public void setCameraSwitched(boolean isBackFacingCamera) {
424        mSwitchCameraButton.setSelected(isBackFacingCamera);
425    }
426
427    @Override
428    public void setVideoPaused(boolean isPaused) {
429        mPauseVideoButton.setSelected(isPaused);
430    }
431
432    @Override
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
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
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
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
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
518    public void setSupportedAudio(int modeMask) {
519        updateAudioButtons(modeMask);
520        refreshAudioModePopup();
521    }
522
523    @Override
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
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     */
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
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     */
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     */
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     */
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
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
781    private boolean isSupported(int mode) {
782        return (mode == (getPresenter().getSupportedAudio() & mode));
783    }
784
785    private boolean isAudio(int mode) {
786        return (mode == getPresenter().getAudioMode());
787    }
788
789    @Override
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
803    public boolean isDialpadVisible() {
804        if (getActivity() != null && getActivity() instanceof InCallActivity) {
805            return ((InCallActivity) getActivity()).isDialpadVisible();
806        }
807        return false;
808    }
809
810    @Override
811    public Context getContext() {
812        return getActivity();
813    }
814}
815