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 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
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
129    public CallButtonPresenter.CallButtonUi getUi() {
130        return this;
131    }
132
133    @Override
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
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
180    public void onActivityCreated(Bundle savedInstanceState) {
181        super.onActivityCreated(savedInstanceState);
182
183        // set the buttons
184        updateAudioButtons();
185    }
186
187    @Override
188    public void onResume() {
189        if (getPresenter() != null) {
190            getPresenter().refreshMuteState();
191        }
192        super.onResume();
193
194        updateColors();
195    }
196
197    @Override
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
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     */
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     */
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
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
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
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
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
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
368    public void showButton(int buttonId, boolean show) {
369        mButtonVisibilityMap.put(buttonId, show ? BUTTON_VISIBLE : BUTTON_HIDDEN);
370    }
371
372    @Override
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
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
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
422    public void setCameraSwitched(boolean isBackFacingCamera) {
423        mSwitchCameraButton.setSelected(isBackFacingCamera);
424    }
425
426    @Override
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
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
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
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
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
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
523    public void setSupportedAudio(int modeMask) {
524        updateAudioButtons();
525        refreshAudioModePopup();
526    }
527
528    @Override
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
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     */
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
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     */
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     */
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     */
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
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
786    private boolean isSupported(int mode) {
787        return (mode == (getPresenter().getSupportedAudio() & mode));
788    }
789
790    private boolean isAudio(int mode) {
791        return (mode == getPresenter().getAudioMode());
792    }
793
794    @Override
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
808    public boolean isDialpadVisible() {
809        if (getActivity() != null && getActivity() instanceof InCallActivity) {
810            return ((InCallActivity) getActivity()).isDialpadVisible();
811        }
812        return false;
813    }
814
815    @Override
816    public Context getContext() {
817        return getActivity();
818    }
819}
820