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