/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.incallui; import android.content.Context; import android.graphics.drawable.LayerDrawable; import android.os.Bundle; import android.telecom.AudioState; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.widget.CompoundButton; import android.widget.ImageButton; import android.widget.PopupMenu; import android.widget.PopupMenu.OnDismissListener; import android.widget.PopupMenu.OnMenuItemClickListener; /** * Fragment for call control buttons */ public class CallButtonFragment extends BaseFragment implements CallButtonPresenter.CallButtonUi, OnMenuItemClickListener, OnDismissListener, View.OnClickListener, CompoundButton.OnCheckedChangeListener { private ImageButton mAudioButton; private ImageButton mChangeToVoiceButton; private ImageButton mMuteButton; private ImageButton mShowDialpadButton; private ImageButton mHoldButton; private ImageButton mSwapButton; private ImageButton mChangeToVideoButton; private ImageButton mSwitchCameraButton; private ImageButton mAddCallButton; private ImageButton mMergeButton; private ImageButton mPauseVideoButton; private ImageButton mOverflowButton; private PopupMenu mAudioModePopup; private boolean mAudioModePopupVisible; private PopupMenu mOverflowPopup; private int mPrevAudioMode = 0; // Constants for Drawable.setAlpha() private static final int HIDDEN = 0; private static final int VISIBLE = 255; private boolean mIsEnabled; @Override CallButtonPresenter createPresenter() { // TODO: find a cleaner way to include audio mode provider than having a singleton instance. return new CallButtonPresenter(); } @Override CallButtonPresenter.CallButtonUi getUi() { return this; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View parent = inflater.inflate(R.layout.call_button_fragment, container, false); mAudioButton = (ImageButton) parent.findViewById(R.id.audioButton); mAudioButton.setOnClickListener(this); mChangeToVoiceButton = (ImageButton) parent.findViewById(R.id.changeToVoiceButton); mChangeToVoiceButton. setOnClickListener(this); mMuteButton = (ImageButton) parent.findViewById(R.id.muteButton); mMuteButton.setOnClickListener(this); mShowDialpadButton = (ImageButton) parent.findViewById(R.id.dialpadButton); mShowDialpadButton.setOnClickListener(this); mHoldButton = (ImageButton) parent.findViewById(R.id.holdButton); mHoldButton.setOnClickListener(this); mSwapButton = (ImageButton) parent.findViewById(R.id.swapButton); mSwapButton.setOnClickListener(this); mChangeToVideoButton = (ImageButton) parent.findViewById(R.id.changeToVideoButton); mChangeToVideoButton.setOnClickListener(this); mSwitchCameraButton = (ImageButton) parent.findViewById(R.id.switchCameraButton); mSwitchCameraButton.setOnClickListener(this); mAddCallButton = (ImageButton) parent.findViewById(R.id.addButton); mAddCallButton.setOnClickListener(this); mMergeButton = (ImageButton) parent.findViewById(R.id.mergeButton); mMergeButton.setOnClickListener(this); mPauseVideoButton = (ImageButton) parent.findViewById(R.id.pauseVideoButton); mPauseVideoButton.setOnClickListener(this); mOverflowButton = (ImageButton) parent.findViewById(R.id.overflowButton); mOverflowButton.setOnClickListener(this); return parent; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); // set the buttons updateAudioButtons(getPresenter().getSupportedAudio()); } @Override public void onResume() { if (getPresenter() != null) { getPresenter().refreshMuteState(); } super.onResume(); } @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { } @Override public void onClick(View view) { int id = view.getId(); Log.d(this, "onClick(View " + view + ", id " + id + ")..."); switch(id) { case R.id.audioButton: onAudioButtonClicked(); break; case R.id.addButton: getPresenter().addCallClicked(); break; case R.id.changeToVoiceButton: getPresenter().changeToVoiceClicked(); break; case R.id.muteButton: { final ImageButton button = (ImageButton) view; getPresenter().muteClicked(!button.isSelected()); break; } case R.id.mergeButton: getPresenter().mergeClicked(); break; case R.id.holdButton: { final ImageButton button = (ImageButton) view; getPresenter().holdClicked(!button.isSelected()); break; } case R.id.swapButton: getPresenter().swapClicked(); break; case R.id.dialpadButton: getPresenter().showDialpadClicked(!mShowDialpadButton.isSelected()); break; case R.id.changeToVideoButton: getPresenter().changeToVideoClicked(); break; case R.id.switchCameraButton: getPresenter().switchCameraClicked( mSwitchCameraButton.isSelected() /* useFrontFacingCamera */); break; case R.id.pauseVideoButton: getPresenter().pauseVideoClicked( !mPauseVideoButton.isSelected() /* pause */); break; case R.id.overflowButton: mOverflowPopup.show(); break; default: Log.wtf(this, "onClick: unexpected"); break; } } @Override public void setEnabled(boolean isEnabled) { mIsEnabled = isEnabled; View view = getView(); if (view.getVisibility() != View.VISIBLE) { view.setVisibility(View.VISIBLE); } mAudioButton.setEnabled(isEnabled); mChangeToVoiceButton.setEnabled(isEnabled); mMuteButton.setEnabled(isEnabled); mShowDialpadButton.setEnabled(isEnabled); mHoldButton.setEnabled(isEnabled); mSwapButton.setEnabled(isEnabled); mChangeToVideoButton.setEnabled(isEnabled); mSwitchCameraButton.setEnabled(isEnabled); mAddCallButton.setEnabled(isEnabled); mMergeButton.setEnabled(isEnabled); mPauseVideoButton.setEnabled(isEnabled); mOverflowButton.setEnabled(isEnabled); } @Override public void setMute(boolean value) { if (mMuteButton.isSelected() != value) { mMuteButton.setSelected(value); maybeSendAccessibilityEvent(mMuteButton, value ? R.string.accessibility_call_muted : R.string.accessibility_call_unmuted); } } @Override public void showAudioButton(boolean show) { mAudioButton.setVisibility(show ? View.VISIBLE : View.GONE); } @Override public void showChangeToVoiceButton(boolean show) { mChangeToVoiceButton.setVisibility(show ? View.VISIBLE : View.GONE); } @Override public void enableMute(boolean enabled) { mMuteButton.setEnabled(enabled); } @Override public void showDialpadButton(boolean show) { mShowDialpadButton.setVisibility(show ? View.VISIBLE : View.GONE); } @Override public void setHold(boolean value) { if (mHoldButton.isSelected() != value) { mHoldButton.setSelected(value); maybeSendAccessibilityEvent(mHoldButton, value ? R.string.accessibility_call_put_on_hold : R.string.accessibility_call_removed_from_hold); } } @Override public void showHoldButton(boolean show) { mHoldButton.setVisibility(show ? View.VISIBLE : View.GONE); } @Override public void enableHold(boolean enabled) { mHoldButton.setEnabled(enabled); } @Override public void showSwapButton(boolean show) { mSwapButton.setVisibility(show ? View.VISIBLE : View.GONE); } @Override public void showChangeToVideoButton(boolean show) { mChangeToVideoButton.setVisibility(show ? View.VISIBLE : View.GONE); } @Override public void showSwitchCameraButton(boolean show) { mSwitchCameraButton.setVisibility(show ? View.VISIBLE : View.GONE); } @Override public void setSwitchCameraButton(boolean isBackFacingCamera) { mSwitchCameraButton.setSelected(isBackFacingCamera); } @Override public void showAddCallButton(boolean show) { Log.d(this, "show Add call button: " + show); mAddCallButton.setVisibility(show ? View.VISIBLE : View.GONE); } @Override public void showMergeButton(boolean show) { mMergeButton.setVisibility(show ? View.VISIBLE : View.GONE); } @Override public void showPauseVideoButton(boolean show) { mPauseVideoButton.setVisibility(show ? View.VISIBLE : View.GONE); } @Override public void setPauseVideoButton(boolean isPaused) { mPauseVideoButton.setSelected(isPaused); } @Override public void showOverflowButton(boolean show) { mOverflowButton.setVisibility(show ? View.VISIBLE : View.GONE); } @Override public void configureOverflowMenu(boolean showMergeMenuOption, boolean showAddMenuOption, boolean showHoldMenuOption, boolean showSwapMenuOption) { if (mOverflowPopup == null) { final ContextThemeWrapper contextWrapper = new ContextThemeWrapper(getActivity(), R.style.InCallPopupMenuStyle); mOverflowPopup = new PopupMenu(contextWrapper, mOverflowButton); mOverflowPopup.getMenuInflater().inflate(R.menu.incall_overflow_menu, mOverflowPopup.getMenu()); mOverflowPopup.setOnMenuItemClickListener(new OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { case R.id.overflow_merge_menu_item: getPresenter().mergeClicked(); break; case R.id.overflow_add_menu_item: getPresenter().addCallClicked(); break; case R.id.overflow_hold_menu_item: getPresenter().holdClicked(true /* checked */); break; case R.id.overflow_resume_menu_item: getPresenter().holdClicked(false /* checked */); break; case R.id.overflow_swap_menu_item: getPresenter().addCallClicked(); break; default: Log.wtf(this, "onMenuItemClick: unexpected overflow menu click"); break; } return true; } }); mOverflowPopup.setOnDismissListener(new OnDismissListener() { @Override public void onDismiss(PopupMenu popupMenu) { popupMenu.dismiss(); } }); } final Menu menu = mOverflowPopup.getMenu(); menu.findItem(R.id.overflow_merge_menu_item).setVisible(showMergeMenuOption); menu.findItem(R.id.overflow_add_menu_item).setVisible(showAddMenuOption); menu.findItem(R.id.overflow_hold_menu_item).setVisible( showHoldMenuOption && !mHoldButton.isSelected()); menu.findItem(R.id.overflow_resume_menu_item).setVisible( showHoldMenuOption && mHoldButton.isSelected()); menu.findItem(R.id.overflow_swap_menu_item).setVisible(showSwapMenuOption); mOverflowButton.setEnabled(menu.hasVisibleItems()); } @Override public void setAudio(int mode) { updateAudioButtons(getPresenter().getSupportedAudio()); refreshAudioModePopup(); if (mPrevAudioMode != mode) { if (mPrevAudioMode != 0) { int stringId = 0; switch (mode) { case AudioState.ROUTE_EARPIECE: stringId = R.string.accessibility_earpiece_selected; break; case AudioState.ROUTE_BLUETOOTH: stringId = R.string.accessibility_bluetooth_headset_selected; break; case AudioState.ROUTE_WIRED_HEADSET: stringId = R.string.accessibility_wired_headset_selected; break; case AudioState.ROUTE_SPEAKER: stringId = R.string.accessibility_speakerphone_selected; break; } if (stringId != 0) { maybeSendAccessibilityEvent(mAudioButton, stringId); } } mPrevAudioMode = mode; } } @Override public void setSupportedAudio(int modeMask) { updateAudioButtons(modeMask); refreshAudioModePopup(); } @Override public boolean onMenuItemClick(MenuItem item) { Log.d(this, "- onMenuItemClick: " + item); Log.d(this, " id: " + item.getItemId()); Log.d(this, " title: '" + item.getTitle() + "'"); int mode = AudioState.ROUTE_WIRED_OR_EARPIECE; switch (item.getItemId()) { case R.id.audio_mode_speaker: mode = AudioState.ROUTE_SPEAKER; break; case R.id.audio_mode_earpiece: case R.id.audio_mode_wired_headset: // InCallAudioState.ROUTE_EARPIECE means either the handset earpiece, // or the wired headset (if connected.) mode = AudioState.ROUTE_WIRED_OR_EARPIECE; break; case R.id.audio_mode_bluetooth: mode = AudioState.ROUTE_BLUETOOTH; break; default: Log.e(this, "onMenuItemClick: unexpected View ID " + item.getItemId() + " (MenuItem = '" + item + "')"); break; } getPresenter().setAudioMode(mode); return true; } // PopupMenu.OnDismissListener implementation; see showAudioModePopup(). // This gets called when the PopupMenu gets dismissed for *any* reason, like // the user tapping outside its bounds, or pressing Back, or selecting one // of the menu items. @Override public void onDismiss(PopupMenu menu) { Log.d(this, "- onDismiss: " + menu); mAudioModePopupVisible = false; } /** * Checks for supporting modes. If bluetooth is supported, it uses the audio * pop up menu. Otherwise, it toggles the speakerphone. */ private void onAudioButtonClicked() { Log.d(this, "onAudioButtonClicked: " + AudioState.audioRouteToString(getPresenter().getSupportedAudio())); if (isSupported(AudioState.ROUTE_BLUETOOTH)) { showAudioModePopup(); } else { getPresenter().toggleSpeakerphone(); } } /** * Refreshes the "Audio mode" popup if it's visible. This is useful * (for example) when a wired headset is plugged or unplugged, * since we need to switch back and forth between the "earpiece" * and "wired headset" items. * * This is safe to call even if the popup is already dismissed, or even if * you never called showAudioModePopup() in the first place. */ public void refreshAudioModePopup() { if (mAudioModePopup != null && mAudioModePopupVisible) { // Dismiss the previous one mAudioModePopup.dismiss(); // safe even if already dismissed // And bring up a fresh PopupMenu showAudioModePopup(); } } /** * Updates the audio button so that the appriopriate visual layers * are visible based on the supported audio formats. */ private void updateAudioButtons(int supportedModes) { final boolean bluetoothSupported = isSupported(AudioState.ROUTE_BLUETOOTH); final boolean speakerSupported = isSupported(AudioState.ROUTE_SPEAKER); boolean audioButtonEnabled = false; boolean audioButtonChecked = false; boolean showMoreIndicator = false; boolean showBluetoothIcon = false; boolean showSpeakerphoneIcon = false; boolean showHandsetIcon = false; boolean showToggleIndicator = false; if (bluetoothSupported) { Log.d(this, "updateAudioButtons - popup menu mode"); audioButtonEnabled = true; showMoreIndicator = true; // The audio button is NOT a toggle in this state. (And its // setChecked() state is irrelevant since we completely hide the // btn_compound_background layer anyway.) // Update desired layers: if (isAudio(AudioState.ROUTE_BLUETOOTH)) { showBluetoothIcon = true; } else if (isAudio(AudioState.ROUTE_SPEAKER)) { showSpeakerphoneIcon = true; } else { showHandsetIcon = true; // TODO: if a wired headset is plugged in, that takes precedence // over the handset earpiece. If so, maybe we should show some // sort of "wired headset" icon here instead of the "handset // earpiece" icon. (Still need an asset for that, though.) } } else if (speakerSupported) { Log.d(this, "updateAudioButtons - speaker toggle mode"); audioButtonEnabled = true; // The audio button *is* a toggle in this state, and indicated the // current state of the speakerphone. audioButtonChecked = isAudio(AudioState.ROUTE_SPEAKER); // update desired layers: showToggleIndicator = true; showSpeakerphoneIcon = true; } else { Log.d(this, "updateAudioButtons - disabled..."); // The audio button is a toggle in this state, but that's mostly // irrelevant since it's always disabled and unchecked. audioButtonEnabled = false; audioButtonChecked = false; // update desired layers: showToggleIndicator = true; showSpeakerphoneIcon = true; } // Finally, update it all! Log.v(this, "audioButtonEnabled: " + audioButtonEnabled); Log.v(this, "audioButtonChecked: " + audioButtonChecked); Log.v(this, "showMoreIndicator: " + showMoreIndicator); Log.v(this, "showBluetoothIcon: " + showBluetoothIcon); Log.v(this, "showSpeakerphoneIcon: " + showSpeakerphoneIcon); Log.v(this, "showHandsetIcon: " + showHandsetIcon); // Only enable the audio button if the fragment is enabled. mAudioButton.setEnabled(audioButtonEnabled && mIsEnabled); mAudioButton.setSelected(audioButtonChecked); final LayerDrawable layers = (LayerDrawable) mAudioButton.getBackground(); Log.d(this, "'layers' drawable: " + layers); layers.findDrawableByLayerId(R.id.compoundBackgroundItem) .setAlpha(showToggleIndicator ? VISIBLE : HIDDEN); layers.findDrawableByLayerId(R.id.moreIndicatorItem) .setAlpha(showMoreIndicator ? VISIBLE : HIDDEN); layers.findDrawableByLayerId(R.id.bluetoothItem) .setAlpha(showBluetoothIcon ? VISIBLE : HIDDEN); layers.findDrawableByLayerId(R.id.handsetItem) .setAlpha(showHandsetIcon ? VISIBLE : HIDDEN); layers.findDrawableByLayerId(R.id.speakerphoneItem) .setAlpha(showSpeakerphoneIcon ? VISIBLE : HIDDEN); } private void showAudioModePopup() { Log.d(this, "showAudioPopup()..."); final ContextThemeWrapper contextWrapper = new ContextThemeWrapper(getActivity(), R.style.InCallPopupMenuStyle); mAudioModePopup = new PopupMenu(contextWrapper, mAudioButton /* anchorView */); mAudioModePopup.getMenuInflater().inflate(R.menu.incall_audio_mode_menu, mAudioModePopup.getMenu()); mAudioModePopup.setOnMenuItemClickListener(this); mAudioModePopup.setOnDismissListener(this); final Menu menu = mAudioModePopup.getMenu(); // TODO: Still need to have the "currently active" audio mode come // up pre-selected (or focused?) with a blue highlight. Still // need exact visual design, and possibly framework support for this. // See comments below for the exact logic. final MenuItem speakerItem = menu.findItem(R.id.audio_mode_speaker); speakerItem.setEnabled(isSupported(AudioState.ROUTE_SPEAKER)); // TODO: Show speakerItem as initially "selected" if // speaker is on. // We display *either* "earpiece" or "wired headset", never both, // depending on whether a wired headset is physically plugged in. final MenuItem earpieceItem = menu.findItem(R.id.audio_mode_earpiece); final MenuItem wiredHeadsetItem = menu.findItem(R.id.audio_mode_wired_headset); final boolean usingHeadset = isSupported(AudioState.ROUTE_WIRED_HEADSET); earpieceItem.setVisible(!usingHeadset); earpieceItem.setEnabled(!usingHeadset); wiredHeadsetItem.setVisible(usingHeadset); wiredHeadsetItem.setEnabled(usingHeadset); // TODO: Show the above item (either earpieceItem or wiredHeadsetItem) // as initially "selected" if speakerOn and // bluetoothIndicatorOn are both false. final MenuItem bluetoothItem = menu.findItem(R.id.audio_mode_bluetooth); bluetoothItem.setEnabled(isSupported(AudioState.ROUTE_BLUETOOTH)); // TODO: Show bluetoothItem as initially "selected" if // bluetoothIndicatorOn is true. mAudioModePopup.show(); // Unfortunately we need to manually keep track of the popup menu's // visiblity, since PopupMenu doesn't have an isShowing() method like // Dialogs do. mAudioModePopupVisible = true; } private boolean isSupported(int mode) { return (mode == (getPresenter().getSupportedAudio() & mode)); } private boolean isAudio(int mode) { return (mode == getPresenter().getAudioMode()); } @Override public void displayDialpad(boolean value, boolean animate) { mShowDialpadButton.setSelected(value); if (getActivity() != null && getActivity() instanceof InCallActivity) { ((InCallActivity) getActivity()).displayDialpad(value, animate); } } @Override public boolean isDialpadVisible() { if (getActivity() != null && getActivity() instanceof InCallActivity) { return ((InCallActivity) getActivity()).isDialpadVisible(); } return false; } @Override public Context getContext() { return getActivity(); } private void maybeSendAccessibilityEvent(View view, int stringId) { final Context context = getActivity(); AccessibilityManager manager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); if (manager != null && manager.isEnabled()) { AccessibilityEvent e = AccessibilityEvent.obtain(); e.setSource(view); e.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT); e.setClassName(getClass().getName()); e.setPackageName(context.getPackageName()); e.getText().add(context.getResources().getString(stringId)); manager.sendAccessibilityEvent(e); } } }