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 android.graphics.drawable.LayerDrawable;
20import android.os.Bundle;
21import android.view.LayoutInflater;
22import android.view.Menu;
23import android.view.MenuItem;
24import android.view.View;
25import android.view.View.OnClickListener;
26import android.view.ViewGroup;
27import android.widget.CompoundButton;
28import android.widget.ImageButton;
29import android.widget.PopupMenu;
30import android.widget.PopupMenu.OnDismissListener;
31import android.widget.PopupMenu.OnMenuItemClickListener;
32import android.widget.ToggleButton;
33
34import com.android.services.telephony.common.AudioMode;
35
36/**
37 * Fragment for call control buttons
38 */
39public class CallButtonFragment
40        extends BaseFragment<CallButtonPresenter, CallButtonPresenter.CallButtonUi>
41        implements CallButtonPresenter.CallButtonUi, OnMenuItemClickListener, OnDismissListener,
42        View.OnClickListener, CompoundButton.OnCheckedChangeListener {
43
44    private ImageButton mMuteButton;
45    private ImageButton mAudioButton;
46    private ImageButton mHoldButton;
47    private ToggleButton mShowDialpadButton;
48    private ImageButton mMergeButton;
49    private ImageButton mAddCallButton;
50    private ImageButton mSwapButton;
51
52    private PopupMenu mAudioModePopup;
53    private boolean mAudioModePopupVisible;
54    private View mEndCallButton;
55    private View mExtraRowButton;
56    private View mManageConferenceButton;
57    private View mGenericMergeButton;
58
59    @Override
60    CallButtonPresenter createPresenter() {
61        // TODO: find a cleaner way to include audio mode provider than
62        // having a singleton instance.
63        return new CallButtonPresenter();
64    }
65
66    @Override
67    CallButtonPresenter.CallButtonUi getUi() {
68        return this;
69    }
70
71    @Override
72    public void onCreate(Bundle savedInstanceState) {
73        super.onCreate(savedInstanceState);
74    }
75
76    @Override
77    public View onCreateView(LayoutInflater inflater, ViewGroup container,
78            Bundle savedInstanceState) {
79        final View parent = inflater.inflate(R.layout.call_button_fragment, container, false);
80
81        mExtraRowButton = parent.findViewById(R.id.extraButtonRow);
82
83        mManageConferenceButton = parent.findViewById(R.id.manageConferenceButton);
84        mManageConferenceButton.setOnClickListener(new View.OnClickListener() {
85            @Override
86            public void onClick(View v) {
87                getPresenter().manageConferenceButtonClicked();
88            }
89        });
90        mGenericMergeButton = parent.findViewById(R.id.cdmaMergeButton);
91        mGenericMergeButton.setOnClickListener(new View.OnClickListener() {
92            @Override
93            public void onClick(View v) {
94                getPresenter().mergeClicked();
95            }
96        });
97
98        mEndCallButton = parent.findViewById(R.id.endButton);
99        mEndCallButton.setOnClickListener(new View.OnClickListener() {
100            @Override
101            public void onClick(View v) {
102                getPresenter().endCallClicked();
103            }
104        });
105
106        // make the hit target smaller for the end button so that is creates a deadzone
107        // along the inside perimeter of the button.
108        mEndCallButton.setOnTouchListener(new SmallerHitTargetTouchListener());
109
110        mMuteButton = (ImageButton) parent.findViewById(R.id.muteButton);
111        mMuteButton.setOnClickListener(new OnClickListener() {
112            @Override
113            public void onClick(View v) {
114                final ImageButton button = (ImageButton) v;
115                getPresenter().muteClicked(!button.isSelected());
116            }
117        });
118
119        mAudioButton = (ImageButton) parent.findViewById(R.id.audioButton);
120        mAudioButton.setOnClickListener(new View.OnClickListener() {
121            @Override
122            public void onClick(View view) {
123                onAudioButtonClicked();
124            }
125        });
126
127        mHoldButton = (ImageButton) parent.findViewById(R.id.holdButton);
128        mHoldButton.setOnClickListener(new OnClickListener() {
129            @Override
130            public void onClick(View v) {
131                final ImageButton button = (ImageButton) v;
132                getPresenter().holdClicked(!button.isSelected());
133            }
134        });
135
136        mShowDialpadButton = (ToggleButton) parent.findViewById(R.id.dialpadButton);
137        mShowDialpadButton.setOnClickListener(this);
138        mAddCallButton = (ImageButton) parent.findViewById(R.id.addButton);
139        mAddCallButton.setOnClickListener(this);
140        mMergeButton = (ImageButton) parent.findViewById(R.id.mergeButton);
141        mMergeButton.setOnClickListener(this);
142        mSwapButton = (ImageButton) parent.findViewById(R.id.swapButton);
143        mSwapButton.setOnClickListener(this);
144
145        return parent;
146    }
147
148    @Override
149    public void onActivityCreated(Bundle savedInstanceState) {
150        super.onActivityCreated(savedInstanceState);
151
152        // set the buttons
153        updateAudioButtons(getPresenter().getSupportedAudio());
154    }
155
156    @Override
157    public void onResume() {
158        if (getPresenter() != null) {
159            getPresenter().refreshMuteState();
160        }
161        super.onResume();
162    }
163
164    @Override
165    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
166    }
167
168    @Override
169    public void onClick(View view) {
170        int id = view.getId();
171        Log.d(this, "onClick(View " + view + ", id " + id + ")...");
172
173        switch(id) {
174            case R.id.addButton:
175                getPresenter().addCallClicked();
176                break;
177            case R.id.mergeButton:
178                getPresenter().mergeClicked();
179                break;
180            case R.id.swapButton:
181                getPresenter().swapClicked();
182                break;
183            case R.id.dialpadButton:
184                getPresenter().showDialpadClicked(mShowDialpadButton.isChecked());
185                break;
186            default:
187                Log.wtf(this, "onClick: unexpected");
188                break;
189        }
190    }
191
192    @Override
193    public void setEnabled(boolean isEnabled) {
194        View view = getView();
195        if (view.getVisibility() != View.VISIBLE) {
196            view.setVisibility(View.VISIBLE);
197        }
198
199        // The main end-call button spanning across the screen.
200        mEndCallButton.setEnabled(isEnabled);
201
202        // The smaller buttons laid out horizontally just below the end-call button.
203        mMuteButton.setEnabled(isEnabled);
204        mAudioButton.setEnabled(isEnabled);
205        mHoldButton.setEnabled(isEnabled);
206        mShowDialpadButton.setEnabled(isEnabled);
207        mMergeButton.setEnabled(isEnabled);
208        mAddCallButton.setEnabled(isEnabled);
209        mSwapButton.setEnabled(isEnabled);
210    }
211
212    @Override
213    public void setMute(boolean value) {
214        mMuteButton.setSelected(value);
215    }
216
217    @Override
218    public void enableMute(boolean enabled) {
219        mMuteButton.setEnabled(enabled);
220    }
221
222    @Override
223    public void setHold(boolean value) {
224        mHoldButton.setSelected(value);
225    }
226
227    @Override
228    public void showHold(boolean show) {
229        mHoldButton.setVisibility(show ? View.VISIBLE : View.GONE);
230    }
231
232    @Override
233    public void enableHold(boolean enabled) {
234        mHoldButton.setEnabled(enabled);
235    }
236
237    @Override
238    public void showMerge(boolean show) {
239        mMergeButton.setVisibility(show ? View.VISIBLE : View.GONE);
240    }
241
242    @Override
243    public void showSwap(boolean show) {
244        mSwapButton.setVisibility(show ? View.VISIBLE : View.GONE);
245    }
246
247    @Override
248    public void showAddCall(boolean show) {
249        mAddCallButton.setVisibility(show ? View.VISIBLE : View.GONE);
250    }
251
252    @Override
253    public void enableAddCall(boolean enabled) {
254        mAddCallButton.setEnabled(enabled);
255    }
256
257    @Override
258    public void setAudio(int mode) {
259        updateAudioButtons(getPresenter().getSupportedAudio());
260        refreshAudioModePopup();
261    }
262
263    @Override
264    public void setSupportedAudio(int modeMask) {
265        updateAudioButtons(modeMask);
266        refreshAudioModePopup();
267    }
268
269    @Override
270    public boolean onMenuItemClick(MenuItem item) {
271        Log.d(this, "- onMenuItemClick: " + item);
272        Log.d(this, "  id: " + item.getItemId());
273        Log.d(this, "  title: '" + item.getTitle() + "'");
274
275        int mode = AudioMode.WIRED_OR_EARPIECE;
276
277        switch (item.getItemId()) {
278            case R.id.audio_mode_speaker:
279                mode = AudioMode.SPEAKER;
280                break;
281            case R.id.audio_mode_earpiece:
282            case R.id.audio_mode_wired_headset:
283                // InCallAudioMode.EARPIECE means either the handset earpiece,
284                // or the wired headset (if connected.)
285                mode = AudioMode.WIRED_OR_EARPIECE;
286                break;
287            case R.id.audio_mode_bluetooth:
288                mode = AudioMode.BLUETOOTH;
289                break;
290            default:
291                Log.e(this, "onMenuItemClick:  unexpected View ID " + item.getItemId()
292                        + " (MenuItem = '" + item + "')");
293                break;
294        }
295
296        getPresenter().setAudioMode(mode);
297
298        return true;
299    }
300
301    // PopupMenu.OnDismissListener implementation; see showAudioModePopup().
302    // This gets called when the PopupMenu gets dismissed for *any* reason, like
303    // the user tapping outside its bounds, or pressing Back, or selecting one
304    // of the menu items.
305    @Override
306    public void onDismiss(PopupMenu menu) {
307        Log.d(this, "- onDismiss: " + menu);
308        mAudioModePopupVisible = false;
309    }
310
311    /**
312     * Checks for supporting modes.  If bluetooth is supported, it uses the audio
313     * pop up menu.  Otherwise, it toggles the speakerphone.
314     */
315    private void onAudioButtonClicked() {
316        Log.d(this, "onAudioButtonClicked: " +
317                AudioMode.toString(getPresenter().getSupportedAudio()));
318
319        if (isSupported(AudioMode.BLUETOOTH)) {
320            showAudioModePopup();
321        } else {
322            getPresenter().toggleSpeakerphone();
323        }
324    }
325
326    /**
327     * Refreshes the "Audio mode" popup if it's visible.  This is useful
328     * (for example) when a wired headset is plugged or unplugged,
329     * since we need to switch back and forth between the "earpiece"
330     * and "wired headset" items.
331     *
332     * This is safe to call even if the popup is already dismissed, or even if
333     * you never called showAudioModePopup() in the first place.
334     */
335    public void refreshAudioModePopup() {
336        if (mAudioModePopup != null && mAudioModePopupVisible) {
337            // Dismiss the previous one
338            mAudioModePopup.dismiss();  // safe even if already dismissed
339            // And bring up a fresh PopupMenu
340            showAudioModePopup();
341        }
342    }
343
344    /**
345     * Updates the audio button so that the appriopriate visual layers
346     * are visible based on the supported audio formats.
347     */
348    private void updateAudioButtons(int supportedModes) {
349        final boolean bluetoothSupported = isSupported(AudioMode.BLUETOOTH);
350        final boolean speakerSupported = isSupported(AudioMode.SPEAKER);
351
352        boolean audioButtonEnabled = false;
353        boolean audioButtonChecked = false;
354        boolean showMoreIndicator = false;
355
356        boolean showBluetoothIcon = false;
357        boolean showSpeakerphoneOnIcon = false;
358        boolean showSpeakerphoneOffIcon = false;
359        boolean showHandsetIcon = false;
360
361        boolean showToggleIndicator = false;
362
363        if (bluetoothSupported) {
364            Log.d(this, "updateAudioButtons - popup menu mode");
365
366            audioButtonEnabled = true;
367            showMoreIndicator = true;
368            // The audio button is NOT a toggle in this state.  (And its
369            // setChecked() state is irrelevant since we completely hide the
370            // btn_compound_background layer anyway.)
371
372            // Update desired layers:
373            if (isAudio(AudioMode.BLUETOOTH)) {
374                showBluetoothIcon = true;
375            } else if (isAudio(AudioMode.SPEAKER)) {
376                showSpeakerphoneOnIcon = true;
377            } else {
378                showHandsetIcon = true;
379                // TODO: if a wired headset is plugged in, that takes precedence
380                // over the handset earpiece.  If so, maybe we should show some
381                // sort of "wired headset" icon here instead of the "handset
382                // earpiece" icon.  (Still need an asset for that, though.)
383            }
384        } else if (speakerSupported) {
385            Log.d(this, "updateAudioButtons - speaker toggle mode");
386
387            audioButtonEnabled = true;
388
389            // The audio button *is* a toggle in this state, and indicated the
390            // current state of the speakerphone.
391            audioButtonChecked = isAudio(AudioMode.SPEAKER);
392
393            // update desired layers:
394            showToggleIndicator = true;
395
396            showSpeakerphoneOnIcon = isAudio(AudioMode.SPEAKER);
397            showSpeakerphoneOffIcon = !showSpeakerphoneOnIcon;
398        } else {
399            Log.d(this, "updateAudioButtons - disabled...");
400
401            // The audio button is a toggle in this state, but that's mostly
402            // irrelevant since it's always disabled and unchecked.
403            audioButtonEnabled = false;
404            audioButtonChecked = false;
405
406            // update desired layers:
407            showToggleIndicator = true;
408            showSpeakerphoneOffIcon = true;
409        }
410
411        // Finally, update it all!
412
413        Log.v(this, "audioButtonEnabled: " + audioButtonEnabled);
414        Log.v(this, "audioButtonChecked: " + audioButtonChecked);
415        Log.v(this, "showMoreIndicator: " + showMoreIndicator);
416        Log.v(this, "showBluetoothIcon: " + showBluetoothIcon);
417        Log.v(this, "showSpeakerphoneOnIcon: " + showSpeakerphoneOnIcon);
418        Log.v(this, "showSpeakerphoneOffIcon: " + showSpeakerphoneOffIcon);
419        Log.v(this, "showHandsetIcon: " + showHandsetIcon);
420
421        // Constants for Drawable.setAlpha()
422        final int HIDDEN = 0;
423        final int VISIBLE = 255;
424
425        mAudioButton.setEnabled(audioButtonEnabled);
426        mAudioButton.setSelected(audioButtonChecked);
427
428        final LayerDrawable layers = (LayerDrawable) mAudioButton.getBackground();
429        Log.d(this, "'layers' drawable: " + layers);
430
431        layers.findDrawableByLayerId(R.id.compoundBackgroundItem)
432                .setAlpha(showToggleIndicator ? VISIBLE : HIDDEN);
433
434        layers.findDrawableByLayerId(R.id.moreIndicatorItem)
435                .setAlpha(showMoreIndicator ? VISIBLE : HIDDEN);
436
437        layers.findDrawableByLayerId(R.id.bluetoothItem)
438                .setAlpha(showBluetoothIcon ? VISIBLE : HIDDEN);
439
440        layers.findDrawableByLayerId(R.id.handsetItem)
441                .setAlpha(showHandsetIcon ? VISIBLE : HIDDEN);
442
443        layers.findDrawableByLayerId(R.id.speakerphoneOnItem)
444                .setAlpha(showSpeakerphoneOnIcon ? VISIBLE : HIDDEN);
445
446        layers.findDrawableByLayerId(R.id.speakerphoneOffItem)
447                .setAlpha(showSpeakerphoneOffIcon ? VISIBLE : HIDDEN);
448    }
449
450    private void showAudioModePopup() {
451        Log.d(this, "showAudioPopup()...");
452
453        mAudioModePopup = new PopupMenu(getView().getContext(), mAudioButton /* anchorView */);
454        mAudioModePopup.getMenuInflater().inflate(R.menu.incall_audio_mode_menu,
455                mAudioModePopup.getMenu());
456        mAudioModePopup.setOnMenuItemClickListener(this);
457        mAudioModePopup.setOnDismissListener(this);
458
459        final Menu menu = mAudioModePopup.getMenu();
460
461        // TODO: Still need to have the "currently active" audio mode come
462        // up pre-selected (or focused?) with a blue highlight.  Still
463        // need exact visual design, and possibly framework support for this.
464        // See comments below for the exact logic.
465
466        final MenuItem speakerItem = menu.findItem(R.id.audio_mode_speaker);
467        speakerItem.setEnabled(isSupported(AudioMode.SPEAKER));
468        // TODO: Show speakerItem as initially "selected" if
469        // speaker is on.
470
471        // We display *either* "earpiece" or "wired headset", never both,
472        // depending on whether a wired headset is physically plugged in.
473        final MenuItem earpieceItem = menu.findItem(R.id.audio_mode_earpiece);
474        final MenuItem wiredHeadsetItem = menu.findItem(R.id.audio_mode_wired_headset);
475
476        final boolean usingHeadset = isSupported(AudioMode.WIRED_HEADSET);
477        earpieceItem.setVisible(!usingHeadset);
478        earpieceItem.setEnabled(!usingHeadset);
479        wiredHeadsetItem.setVisible(usingHeadset);
480        wiredHeadsetItem.setEnabled(usingHeadset);
481        // TODO: Show the above item (either earpieceItem or wiredHeadsetItem)
482        // as initially "selected" if speakerOn and
483        // bluetoothIndicatorOn are both false.
484
485        final MenuItem bluetoothItem = menu.findItem(R.id.audio_mode_bluetooth);
486        bluetoothItem.setEnabled(isSupported(AudioMode.BLUETOOTH));
487        // TODO: Show bluetoothItem as initially "selected" if
488        // bluetoothIndicatorOn is true.
489
490        mAudioModePopup.show();
491
492        // Unfortunately we need to manually keep track of the popup menu's
493        // visiblity, since PopupMenu doesn't have an isShowing() method like
494        // Dialogs do.
495        mAudioModePopupVisible = true;
496    }
497
498    private boolean isSupported(int mode) {
499        return (mode == (getPresenter().getSupportedAudio() & mode));
500    }
501
502    private boolean isAudio(int mode) {
503        return (mode == getPresenter().getAudioMode());
504    }
505
506    @Override
507    public void displayDialpad(boolean value) {
508        mShowDialpadButton.setChecked(value);
509        if (getActivity() != null && getActivity() instanceof InCallActivity) {
510            ((InCallActivity) getActivity()).displayDialpad(value);
511        }
512    }
513
514    @Override
515    public boolean isDialpadVisible() {
516        if (getActivity() != null && getActivity() instanceof InCallActivity) {
517            return ((InCallActivity) getActivity()).isDialpadVisible();
518        }
519        return false;
520    }
521
522    @Override
523    public void displayManageConferencePanel(boolean value) {
524        if (getActivity() != null && getActivity() instanceof InCallActivity) {
525            ((InCallActivity) getActivity()).displayManageConferencePanel(value);
526        }
527    }
528
529
530    @Override
531    public void showManageConferenceCallButton() {
532        mExtraRowButton.setVisibility(View.VISIBLE);
533        mManageConferenceButton.setVisibility(View.VISIBLE);
534        mGenericMergeButton.setVisibility(View.GONE);
535    }
536
537    @Override
538    public void showGenericMergeButton() {
539        mExtraRowButton.setVisibility(View.VISIBLE);
540        mManageConferenceButton.setVisibility(View.GONE);
541        mGenericMergeButton.setVisibility(View.VISIBLE);
542    }
543
544    @Override
545    public void hideExtraRow() {
546       mExtraRowButton.setVisibility(View.GONE);
547    }
548}
549