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 17 package com.android.incallui; 18 19 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_ADD_CALL; 20 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_AUDIO; 21 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_COUNT; 22 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_DIALPAD; 23 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_DOWNGRADE_TO_AUDIO; 24 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_HOLD; 25 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_MANAGE_VIDEO_CONFERENCE; 26 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_MERGE; 27 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_MUTE; 28 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_PAUSE_VIDEO; 29 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_SWAP; 30 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_SWITCH_CAMERA; 31 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_UPGRADE_TO_VIDEO; 32 33 import android.content.Context; 34 import android.content.res.ColorStateList; 35 import android.content.res.Resources; 36 import android.graphics.drawable.Drawable; 37 import android.graphics.drawable.GradientDrawable; 38 import android.graphics.drawable.LayerDrawable; 39 import android.graphics.drawable.RippleDrawable; 40 import android.graphics.drawable.StateListDrawable; 41 import android.os.Bundle; 42 import android.telecom.CallAudioState; 43 import android.util.SparseIntArray; 44 import android.view.ContextThemeWrapper; 45 import android.view.HapticFeedbackConstants; 46 import android.view.LayoutInflater; 47 import android.view.Menu; 48 import android.view.MenuItem; 49 import android.view.View; 50 import android.view.ViewGroup; 51 import android.widget.CompoundButton; 52 import android.widget.ImageButton; 53 import android.widget.PopupMenu; 54 import android.widget.PopupMenu.OnDismissListener; 55 import android.widget.PopupMenu.OnMenuItemClickListener; 56 57 import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette; 58 import com.android.dialer.R; 59 60 /** 61 * Fragment for call control buttons 62 */ 63 public class CallButtonFragment 64 extends BaseFragment<CallButtonPresenter, CallButtonPresenter.CallButtonUi> 65 implements CallButtonPresenter.CallButtonUi, OnMenuItemClickListener, OnDismissListener, 66 View.OnClickListener { 67 68 private static final int INVALID_INDEX = -1; 69 private int mButtonMaxVisible; 70 // The button is currently visible in the UI 71 private static final int BUTTON_VISIBLE = 1; 72 // The button is hidden in the UI 73 private static final int BUTTON_HIDDEN = 2; 74 // The button has been collapsed into the overflow menu 75 private static final int BUTTON_MENU = 3; 76 77 public interface Buttons { 78 79 public static final int BUTTON_AUDIO = 0; 80 public static final int BUTTON_MUTE = 1; 81 public static final int BUTTON_DIALPAD = 2; 82 public static final int BUTTON_HOLD = 3; 83 public static final int BUTTON_SWAP = 4; 84 public static final int BUTTON_UPGRADE_TO_VIDEO = 5; 85 public static final int BUTTON_SWITCH_CAMERA = 6; 86 public static final int BUTTON_DOWNGRADE_TO_AUDIO = 7; 87 public static final int BUTTON_ADD_CALL = 8; 88 public static final int BUTTON_MERGE = 9; 89 public static final int BUTTON_PAUSE_VIDEO = 10; 90 public static final int BUTTON_MANAGE_VIDEO_CONFERENCE = 11; 91 public static final int BUTTON_COUNT = 12; 92 } 93 94 private SparseIntArray mButtonVisibilityMap = new SparseIntArray(BUTTON_COUNT); 95 96 private CompoundButton mAudioButton; 97 private CompoundButton mMuteButton; 98 private CompoundButton mShowDialpadButton; 99 private CompoundButton mHoldButton; 100 private ImageButton mSwapButton; 101 private ImageButton mChangeToVideoButton; 102 private ImageButton mChangeToVoiceButton; 103 private CompoundButton mSwitchCameraButton; 104 private ImageButton mAddCallButton; 105 private ImageButton mMergeButton; 106 private CompoundButton mPauseVideoButton; 107 private ImageButton mOverflowButton; 108 private ImageButton mManageVideoCallConferenceButton; 109 110 private PopupMenu mAudioModePopup; 111 private boolean mAudioModePopupVisible; 112 private PopupMenu mOverflowPopup; 113 114 private int mPrevAudioMode = 0; 115 116 // Constants for Drawable.setAlpha() 117 private static final int HIDDEN = 0; 118 private static final int VISIBLE = 255; 119 120 private boolean mIsEnabled; 121 private MaterialPalette mCurrentThemeColors; 122 123 @Override createPresenter()124 public CallButtonPresenter createPresenter() { 125 // TODO: find a cleaner way to include audio mode provider than having a singleton instance. 126 return new CallButtonPresenter(); 127 } 128 129 @Override getUi()130 public CallButtonPresenter.CallButtonUi getUi() { 131 return this; 132 } 133 134 @Override onCreate(Bundle savedInstanceState)135 public void onCreate(Bundle savedInstanceState) { 136 super.onCreate(savedInstanceState); 137 138 for (int i = 0; i < BUTTON_COUNT; i++) { 139 mButtonVisibilityMap.put(i, BUTTON_HIDDEN); 140 } 141 142 mButtonMaxVisible = getResources().getInteger(R.integer.call_card_max_buttons); 143 } 144 145 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)146 public View onCreateView(LayoutInflater inflater, ViewGroup container, 147 Bundle savedInstanceState) { 148 final View parent = inflater.inflate(R.layout.call_button_fragment, container, false); 149 150 mAudioButton = (CompoundButton) parent.findViewById(R.id.audioButton); 151 mAudioButton.setOnClickListener(this); 152 mMuteButton = (CompoundButton) parent.findViewById(R.id.muteButton); 153 mMuteButton.setOnClickListener(this); 154 mShowDialpadButton = (CompoundButton) parent.findViewById(R.id.dialpadButton); 155 mShowDialpadButton.setOnClickListener(this); 156 mHoldButton = (CompoundButton) parent.findViewById(R.id.holdButton); 157 mHoldButton.setOnClickListener(this); 158 mSwapButton = (ImageButton) parent.findViewById(R.id.swapButton); 159 mSwapButton.setOnClickListener(this); 160 mChangeToVideoButton = (ImageButton) parent.findViewById(R.id.changeToVideoButton); 161 mChangeToVideoButton.setOnClickListener(this); 162 mChangeToVoiceButton = (ImageButton) parent.findViewById(R.id.changeToVoiceButton); 163 mChangeToVoiceButton.setOnClickListener(this); 164 mSwitchCameraButton = (CompoundButton) parent.findViewById(R.id.switchCameraButton); 165 mSwitchCameraButton.setOnClickListener(this); 166 mAddCallButton = (ImageButton) parent.findViewById(R.id.addButton); 167 mAddCallButton.setOnClickListener(this); 168 mMergeButton = (ImageButton) parent.findViewById(R.id.mergeButton); 169 mMergeButton.setOnClickListener(this); 170 mPauseVideoButton = (CompoundButton) parent.findViewById(R.id.pauseVideoButton); 171 mPauseVideoButton.setOnClickListener(this); 172 mOverflowButton = (ImageButton) parent.findViewById(R.id.overflowButton); 173 mOverflowButton.setOnClickListener(this); 174 mManageVideoCallConferenceButton = (ImageButton) parent.findViewById( 175 R.id.manageVideoCallConferenceButton); 176 mManageVideoCallConferenceButton.setOnClickListener(this); 177 return parent; 178 } 179 180 @Override onActivityCreated(Bundle savedInstanceState)181 public void onActivityCreated(Bundle savedInstanceState) { 182 super.onActivityCreated(savedInstanceState); 183 184 // set the buttons 185 updateAudioButtons(getPresenter().getSupportedAudio()); 186 } 187 188 @Override onResume()189 public void onResume() { 190 if (getPresenter() != null) { 191 getPresenter().refreshMuteState(); 192 } 193 super.onResume(); 194 195 updateColors(); 196 } 197 198 @Override onClick(View view)199 public void onClick(View view) { 200 int id = view.getId(); 201 Log.d(this, "onClick(View " + view + ", id " + id + ")..."); 202 203 if (id == R.id.audioButton) { 204 onAudioButtonClicked(); 205 } else if (id == R.id.addButton) { 206 getPresenter().addCallClicked(); 207 } else if (id == R.id.muteButton) { 208 getPresenter().muteClicked(!mMuteButton.isSelected()); 209 } else if (id == R.id.mergeButton) { 210 getPresenter().mergeClicked(); 211 mMergeButton.setEnabled(false); 212 } else if (id == R.id.holdButton) { 213 getPresenter().holdClicked(!mHoldButton.isSelected()); 214 } else if (id == R.id.swapButton) { 215 getPresenter().swapClicked(); 216 } else if (id == R.id.dialpadButton) { 217 getPresenter().showDialpadClicked(!mShowDialpadButton.isSelected()); 218 } else if (id == R.id.changeToVideoButton) { 219 getPresenter().changeToVideoClicked(); 220 } else if (id == R.id.changeToVoiceButton) { 221 getPresenter().changeToVoiceClicked(); 222 } else if (id == R.id.switchCameraButton) { 223 getPresenter().switchCameraClicked( 224 mSwitchCameraButton.isSelected() /* useFrontFacingCamera */); 225 } else if (id == R.id.pauseVideoButton) { 226 getPresenter().pauseVideoClicked( 227 !mPauseVideoButton.isSelected() /* pause */); 228 } else if (id == R.id.overflowButton) { 229 if (mOverflowPopup != null) { 230 mOverflowPopup.show(); 231 } 232 } else if (id == R.id.manageVideoCallConferenceButton) { 233 onManageVideoCallConferenceClicked(); 234 } else { 235 Log.wtf(this, "onClick: unexpected"); 236 return; 237 } 238 239 view.performHapticFeedback( 240 HapticFeedbackConstants.VIRTUAL_KEY, 241 HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); 242 } 243 updateColors()244 public void updateColors() { 245 MaterialPalette themeColors = InCallPresenter.getInstance().getThemeColors(); 246 247 if (mCurrentThemeColors != null && mCurrentThemeColors.equals(themeColors)) { 248 return; 249 } 250 251 View[] compoundButtons = { 252 mAudioButton, 253 mMuteButton, 254 mShowDialpadButton, 255 mHoldButton, 256 mSwitchCameraButton, 257 mPauseVideoButton 258 }; 259 260 for (View button : compoundButtons) { 261 final LayerDrawable layers = (LayerDrawable) button.getBackground(); 262 final RippleDrawable btnCompoundDrawable = compoundBackgroundDrawable(themeColors); 263 layers.setDrawableByLayerId(R.id.compoundBackgroundItem, btnCompoundDrawable); 264 } 265 266 ImageButton[] normalButtons = { 267 mSwapButton, 268 mChangeToVideoButton, 269 mChangeToVoiceButton, 270 mAddCallButton, 271 mMergeButton, 272 mOverflowButton 273 }; 274 275 for (ImageButton button : normalButtons) { 276 final LayerDrawable layers = (LayerDrawable) button.getBackground(); 277 final RippleDrawable btnDrawable = backgroundDrawable(themeColors); 278 layers.setDrawableByLayerId(R.id.backgroundItem, btnDrawable); 279 } 280 281 mCurrentThemeColors = themeColors; 282 } 283 284 /** 285 * Generate a RippleDrawable which will be the background for a compound button, i.e. 286 * a button with pressed and unpressed states. The unpressed state will be the same color 287 * as the rest of the call card, the pressed state will be the dark version of that color. 288 */ compoundBackgroundDrawable(MaterialPalette palette)289 private RippleDrawable compoundBackgroundDrawable(MaterialPalette palette) { 290 Resources res = getResources(); 291 ColorStateList rippleColor = 292 ColorStateList.valueOf(res.getColor(R.color.incall_accent_color)); 293 294 StateListDrawable stateListDrawable = new StateListDrawable(); 295 addSelectedAndFocused(res, stateListDrawable); 296 addFocused(res, stateListDrawable); 297 addSelected(res, stateListDrawable, palette); 298 addUnselected(res, stateListDrawable, palette); 299 300 return new RippleDrawable(rippleColor, stateListDrawable, null); 301 } 302 303 /** 304 * Generate a RippleDrawable which will be the background of a button to ensure it 305 * is the same color as the rest of the call card. 306 */ backgroundDrawable(MaterialPalette palette)307 private RippleDrawable backgroundDrawable(MaterialPalette palette) { 308 Resources res = getResources(); 309 ColorStateList rippleColor = 310 ColorStateList.valueOf(res.getColor(R.color.incall_accent_color)); 311 312 StateListDrawable stateListDrawable = new StateListDrawable(); 313 addFocused(res, stateListDrawable); 314 addUnselected(res, stateListDrawable, palette); 315 316 return new RippleDrawable(rippleColor, stateListDrawable, null); 317 } 318 319 // state_selected and state_focused addSelectedAndFocused(Resources res, StateListDrawable drawable)320 private void addSelectedAndFocused(Resources res, StateListDrawable drawable) { 321 int[] selectedAndFocused = {android.R.attr.state_selected, android.R.attr.state_focused}; 322 Drawable selectedAndFocusedDrawable = res.getDrawable(R.drawable.btn_selected_focused); 323 drawable.addState(selectedAndFocused, selectedAndFocusedDrawable); 324 } 325 326 // state_focused addFocused(Resources res, StateListDrawable drawable)327 private void addFocused(Resources res, StateListDrawable drawable) { 328 int[] focused = {android.R.attr.state_focused}; 329 Drawable focusedDrawable = res.getDrawable(R.drawable.btn_unselected_focused); 330 drawable.addState(focused, focusedDrawable); 331 } 332 333 // state_selected addSelected(Resources res, StateListDrawable drawable, MaterialPalette palette)334 private void addSelected(Resources res, StateListDrawable drawable, MaterialPalette palette) { 335 int[] selected = {android.R.attr.state_selected}; 336 LayerDrawable selectedDrawable = (LayerDrawable) res.getDrawable(R.drawable.btn_selected); 337 ((GradientDrawable) selectedDrawable.getDrawable(0)).setColor(palette.mSecondaryColor); 338 drawable.addState(selected, selectedDrawable); 339 } 340 341 // default addUnselected(Resources res, StateListDrawable drawable, MaterialPalette palette)342 private void addUnselected(Resources res, StateListDrawable drawable, MaterialPalette palette) { 343 LayerDrawable unselectedDrawable = 344 (LayerDrawable) res.getDrawable(R.drawable.btn_unselected); 345 ((GradientDrawable) unselectedDrawable.getDrawable(0)).setColor(palette.mPrimaryColor); 346 drawable.addState(new int[0], unselectedDrawable); 347 } 348 349 @Override setEnabled(boolean isEnabled)350 public void setEnabled(boolean isEnabled) { 351 mIsEnabled = isEnabled; 352 353 mAudioButton.setEnabled(isEnabled); 354 mMuteButton.setEnabled(isEnabled); 355 mShowDialpadButton.setEnabled(isEnabled); 356 mHoldButton.setEnabled(isEnabled); 357 mSwapButton.setEnabled(isEnabled); 358 mChangeToVideoButton.setEnabled(isEnabled); 359 mChangeToVoiceButton.setEnabled(isEnabled); 360 mSwitchCameraButton.setEnabled(isEnabled); 361 mAddCallButton.setEnabled(isEnabled); 362 mMergeButton.setEnabled(isEnabled); 363 mPauseVideoButton.setEnabled(isEnabled); 364 mOverflowButton.setEnabled(isEnabled); 365 mManageVideoCallConferenceButton.setEnabled(isEnabled); 366 } 367 368 @Override showButton(int buttonId, boolean show)369 public void showButton(int buttonId, boolean show) { 370 mButtonVisibilityMap.put(buttonId, show ? BUTTON_VISIBLE : BUTTON_HIDDEN); 371 } 372 373 @Override enableButton(int buttonId, boolean enable)374 public void enableButton(int buttonId, boolean enable) { 375 final View button = getButtonById(buttonId); 376 if (button != null) { 377 button.setEnabled(enable); 378 } 379 } 380 getButtonById(int id)381 private View getButtonById(int id) { 382 if (id == BUTTON_AUDIO) { 383 return mAudioButton; 384 } else if (id == BUTTON_MUTE) { 385 return mMuteButton; 386 } else if (id == BUTTON_DIALPAD) { 387 return mShowDialpadButton; 388 } else if (id == BUTTON_HOLD) { 389 return mHoldButton; 390 } else if (id == BUTTON_SWAP) { 391 return mSwapButton; 392 } else if (id == BUTTON_UPGRADE_TO_VIDEO) { 393 return mChangeToVideoButton; 394 } else if (id == BUTTON_DOWNGRADE_TO_AUDIO) { 395 return mChangeToVoiceButton; 396 } else if (id == BUTTON_SWITCH_CAMERA) { 397 return mSwitchCameraButton; 398 } else if (id == BUTTON_ADD_CALL) { 399 return mAddCallButton; 400 } else if (id == BUTTON_MERGE) { 401 return mMergeButton; 402 } else if (id == BUTTON_PAUSE_VIDEO) { 403 return mPauseVideoButton; 404 } else if (id == BUTTON_MANAGE_VIDEO_CONFERENCE) { 405 return mManageVideoCallConferenceButton; 406 } else { 407 Log.w(this, "Invalid button id"); 408 return null; 409 } 410 } 411 412 @Override setHold(boolean value)413 public void setHold(boolean value) { 414 if (mHoldButton.isSelected() != value) { 415 mHoldButton.setSelected(value); 416 mHoldButton.setContentDescription(getContext().getString( 417 value ? R.string.onscreenHoldText_selected 418 : R.string.onscreenHoldText_unselected)); 419 } 420 } 421 422 @Override setCameraSwitched(boolean isBackFacingCamera)423 public void setCameraSwitched(boolean isBackFacingCamera) { 424 mSwitchCameraButton.setSelected(isBackFacingCamera); 425 } 426 427 @Override setVideoPaused(boolean isPaused)428 public void setVideoPaused(boolean isPaused) { 429 mPauseVideoButton.setSelected(isPaused); 430 } 431 432 @Override setMute(boolean value)433 public void setMute(boolean value) { 434 if (mMuteButton.isSelected() != value) { 435 mMuteButton.setSelected(value); 436 mMuteButton.setContentDescription(getContext().getString( 437 value ? R.string.onscreenMuteText_selected 438 : R.string.onscreenMuteText_unselected)); 439 } 440 } 441 addToOverflowMenu(int id, View button, PopupMenu menu)442 private void addToOverflowMenu(int id, View button, PopupMenu menu) { 443 button.setVisibility(View.GONE); 444 menu.getMenu().add(Menu.NONE, id, Menu.NONE, button.getContentDescription()); 445 mButtonVisibilityMap.put(id, BUTTON_MENU); 446 } 447 getPopupMenu()448 private PopupMenu getPopupMenu() { 449 return new PopupMenu(new ContextThemeWrapper(getActivity(), R.style.InCallPopupMenuStyle), 450 mOverflowButton); 451 } 452 453 /** 454 * Iterates through the list of buttons and toggles their visibility depending on the 455 * setting configured by the CallButtonPresenter. If there are more visible buttons than 456 * the allowed maximum, the excess buttons are collapsed into a single overflow menu. 457 */ 458 @Override updateButtonStates()459 public void updateButtonStates() { 460 View prevVisibleButton = null; 461 int prevVisibleId = -1; 462 PopupMenu menu = null; 463 int visibleCount = 0; 464 for (int i = 0; i < BUTTON_COUNT; i++) { 465 final int visibility = mButtonVisibilityMap.get(i); 466 final View button = getButtonById(i); 467 if (visibility == BUTTON_VISIBLE) { 468 visibleCount++; 469 if (visibleCount <= mButtonMaxVisible) { 470 button.setVisibility(View.VISIBLE); 471 prevVisibleButton = button; 472 prevVisibleId = i; 473 } else { 474 if (menu == null) { 475 menu = getPopupMenu(); 476 } 477 // Collapse the current button into the overflow menu. If is the first visible 478 // button that exceeds the threshold, also collapse the previous visible button 479 // so that the total number of visible buttons will never exceed the threshold. 480 if (prevVisibleButton != null) { 481 addToOverflowMenu(prevVisibleId, prevVisibleButton, menu); 482 prevVisibleButton = null; 483 prevVisibleId = -1; 484 } 485 addToOverflowMenu(i, button, menu); 486 } 487 } else if (visibility == BUTTON_HIDDEN) { 488 button.setVisibility(View.GONE); 489 } 490 } 491 492 mOverflowButton.setVisibility(menu != null ? View.VISIBLE : View.GONE); 493 if (menu != null) { 494 mOverflowPopup = menu; 495 mOverflowPopup.setOnMenuItemClickListener(new OnMenuItemClickListener() { 496 @Override 497 public boolean onMenuItemClick(MenuItem item) { 498 final int id = item.getItemId(); 499 getButtonById(id).performClick(); 500 return true; 501 } 502 }); 503 } 504 } 505 506 @Override setAudio(int mode)507 public void setAudio(int mode) { 508 updateAudioButtons(getPresenter().getSupportedAudio()); 509 refreshAudioModePopup(); 510 511 if (mPrevAudioMode != mode) { 512 updateAudioButtonContentDescription(mode); 513 mPrevAudioMode = mode; 514 } 515 } 516 517 @Override setSupportedAudio(int modeMask)518 public void setSupportedAudio(int modeMask) { 519 updateAudioButtons(modeMask); 520 refreshAudioModePopup(); 521 } 522 523 @Override onMenuItemClick(MenuItem item)524 public boolean onMenuItemClick(MenuItem item) { 525 Log.d(this, "- onMenuItemClick: " + item); 526 Log.d(this, " id: " + item.getItemId()); 527 Log.d(this, " title: '" + item.getTitle() + "'"); 528 529 int mode = CallAudioState.ROUTE_WIRED_OR_EARPIECE; 530 int resId = item.getItemId(); 531 532 if (resId == R.id.audio_mode_speaker) { 533 mode = CallAudioState.ROUTE_SPEAKER; 534 } else if (resId == R.id.audio_mode_earpiece || resId == R.id.audio_mode_wired_headset) { 535 // InCallCallAudioState.ROUTE_EARPIECE means either the handset earpiece, 536 // or the wired headset (if connected.) 537 mode = CallAudioState.ROUTE_WIRED_OR_EARPIECE; 538 } else if (resId == R.id.audio_mode_bluetooth) { 539 mode = CallAudioState.ROUTE_BLUETOOTH; 540 } else { 541 Log.e(this, "onMenuItemClick: unexpected View ID " + item.getItemId() 542 + " (MenuItem = '" + item + "')"); 543 } 544 545 getPresenter().setAudioMode(mode); 546 547 return true; 548 } 549 550 // PopupMenu.OnDismissListener implementation; see showAudioModePopup(). 551 // This gets called when the PopupMenu gets dismissed for *any* reason, like 552 // the user tapping outside its bounds, or pressing Back, or selecting one 553 // of the menu items. 554 @Override onDismiss(PopupMenu menu)555 public void onDismiss(PopupMenu menu) { 556 Log.d(this, "- onDismiss: " + menu); 557 mAudioModePopupVisible = false; 558 updateAudioButtons(getPresenter().getSupportedAudio()); 559 } 560 561 /** 562 * Checks for supporting modes. If bluetooth is supported, it uses the audio 563 * pop up menu. Otherwise, it toggles the speakerphone. 564 */ onAudioButtonClicked()565 private void onAudioButtonClicked() { 566 Log.d(this, "onAudioButtonClicked: " + 567 CallAudioState.audioRouteToString(getPresenter().getSupportedAudio())); 568 569 if (isSupported(CallAudioState.ROUTE_BLUETOOTH)) { 570 showAudioModePopup(); 571 } else { 572 getPresenter().toggleSpeakerphone(); 573 } 574 } 575 onManageVideoCallConferenceClicked()576 private void onManageVideoCallConferenceClicked() { 577 Log.d(this, "onManageVideoCallConferenceClicked"); 578 InCallPresenter.getInstance().showConferenceCallManager(true); 579 } 580 581 /** 582 * Refreshes the "Audio mode" popup if it's visible. This is useful 583 * (for example) when a wired headset is plugged or unplugged, 584 * since we need to switch back and forth between the "earpiece" 585 * and "wired headset" items. 586 * 587 * This is safe to call even if the popup is already dismissed, or even if 588 * you never called showAudioModePopup() in the first place. 589 */ refreshAudioModePopup()590 public void refreshAudioModePopup() { 591 if (mAudioModePopup != null && mAudioModePopupVisible) { 592 // Dismiss the previous one 593 mAudioModePopup.dismiss(); // safe even if already dismissed 594 // And bring up a fresh PopupMenu 595 showAudioModePopup(); 596 } 597 } 598 599 /** 600 * Updates the audio button so that the appriopriate visual layers 601 * are visible based on the supported audio formats. 602 */ updateAudioButtons(int supportedModes)603 private void updateAudioButtons(int supportedModes) { 604 final boolean bluetoothSupported = isSupported(CallAudioState.ROUTE_BLUETOOTH); 605 final boolean speakerSupported = isSupported(CallAudioState.ROUTE_SPEAKER); 606 607 boolean audioButtonEnabled = false; 608 boolean audioButtonChecked = false; 609 boolean showMoreIndicator = false; 610 611 boolean showBluetoothIcon = false; 612 boolean showSpeakerphoneIcon = false; 613 boolean showHandsetIcon = false; 614 615 boolean showToggleIndicator = false; 616 617 if (bluetoothSupported) { 618 Log.d(this, "updateAudioButtons - popup menu mode"); 619 620 audioButtonEnabled = true; 621 audioButtonChecked = true; 622 showMoreIndicator = true; 623 624 // Update desired layers: 625 if (isAudio(CallAudioState.ROUTE_BLUETOOTH)) { 626 showBluetoothIcon = true; 627 } else if (isAudio(CallAudioState.ROUTE_SPEAKER)) { 628 showSpeakerphoneIcon = true; 629 } else { 630 showHandsetIcon = true; 631 // TODO: if a wired headset is plugged in, that takes precedence 632 // over the handset earpiece. If so, maybe we should show some 633 // sort of "wired headset" icon here instead of the "handset 634 // earpiece" icon. (Still need an asset for that, though.) 635 } 636 637 // The audio button is NOT a toggle in this state, so set selected to false. 638 mAudioButton.setSelected(false); 639 } else if (speakerSupported) { 640 Log.d(this, "updateAudioButtons - speaker toggle mode"); 641 642 audioButtonEnabled = true; 643 644 // The audio button *is* a toggle in this state, and indicated the 645 // current state of the speakerphone. 646 audioButtonChecked = isAudio(CallAudioState.ROUTE_SPEAKER); 647 mAudioButton.setSelected(audioButtonChecked); 648 649 // update desired layers: 650 showToggleIndicator = true; 651 showSpeakerphoneIcon = true; 652 } else { 653 Log.d(this, "updateAudioButtons - disabled..."); 654 655 // The audio button is a toggle in this state, but that's mostly 656 // irrelevant since it's always disabled and unchecked. 657 audioButtonEnabled = false; 658 audioButtonChecked = false; 659 mAudioButton.setSelected(false); 660 661 // update desired layers: 662 showToggleIndicator = true; 663 showSpeakerphoneIcon = true; 664 } 665 666 // Finally, update it all! 667 668 Log.v(this, "audioButtonEnabled: " + audioButtonEnabled); 669 Log.v(this, "audioButtonChecked: " + audioButtonChecked); 670 Log.v(this, "showMoreIndicator: " + showMoreIndicator); 671 Log.v(this, "showBluetoothIcon: " + showBluetoothIcon); 672 Log.v(this, "showSpeakerphoneIcon: " + showSpeakerphoneIcon); 673 Log.v(this, "showHandsetIcon: " + showHandsetIcon); 674 675 // Only enable the audio button if the fragment is enabled. 676 mAudioButton.setEnabled(audioButtonEnabled && mIsEnabled); 677 mAudioButton.setChecked(audioButtonChecked); 678 679 final LayerDrawable layers = (LayerDrawable) mAudioButton.getBackground(); 680 Log.d(this, "'layers' drawable: " + layers); 681 682 layers.findDrawableByLayerId(R.id.compoundBackgroundItem) 683 .setAlpha(showToggleIndicator ? VISIBLE : HIDDEN); 684 685 layers.findDrawableByLayerId(R.id.moreIndicatorItem) 686 .setAlpha(showMoreIndicator ? VISIBLE : HIDDEN); 687 688 layers.findDrawableByLayerId(R.id.bluetoothItem) 689 .setAlpha(showBluetoothIcon ? VISIBLE : HIDDEN); 690 691 layers.findDrawableByLayerId(R.id.handsetItem) 692 .setAlpha(showHandsetIcon ? VISIBLE : HIDDEN); 693 694 layers.findDrawableByLayerId(R.id.speakerphoneItem) 695 .setAlpha(showSpeakerphoneIcon ? VISIBLE : HIDDEN); 696 697 } 698 699 /** 700 * Update the content description of the audio button. 701 */ updateAudioButtonContentDescription(int mode)702 private void updateAudioButtonContentDescription(int mode) { 703 int stringId = 0; 704 705 // If bluetooth is not supported, the audio buttion will toggle, so use the label "speaker". 706 // Otherwise, use the label of the currently selected audio mode. 707 if (!isSupported(CallAudioState.ROUTE_BLUETOOTH)) { 708 stringId = R.string.audio_mode_speaker; 709 } else { 710 switch (mode) { 711 case CallAudioState.ROUTE_EARPIECE: 712 stringId = R.string.audio_mode_earpiece; 713 break; 714 case CallAudioState.ROUTE_BLUETOOTH: 715 stringId = R.string.audio_mode_bluetooth; 716 break; 717 case CallAudioState.ROUTE_WIRED_HEADSET: 718 stringId = R.string.audio_mode_wired_headset; 719 break; 720 case CallAudioState.ROUTE_SPEAKER: 721 stringId = R.string.audio_mode_speaker; 722 break; 723 } 724 } 725 726 if (stringId != 0) { 727 mAudioButton.setContentDescription(getResources().getString(stringId)); 728 } 729 } 730 showAudioModePopup()731 private void showAudioModePopup() { 732 Log.d(this, "showAudioPopup()..."); 733 734 final ContextThemeWrapper contextWrapper = new ContextThemeWrapper(getActivity(), 735 R.style.InCallPopupMenuStyle); 736 mAudioModePopup = new PopupMenu(contextWrapper, mAudioButton /* anchorView */); 737 mAudioModePopup.getMenuInflater().inflate(R.menu.incall_audio_mode_menu, 738 mAudioModePopup.getMenu()); 739 mAudioModePopup.setOnMenuItemClickListener(this); 740 mAudioModePopup.setOnDismissListener(this); 741 742 final Menu menu = mAudioModePopup.getMenu(); 743 744 // TODO: Still need to have the "currently active" audio mode come 745 // up pre-selected (or focused?) with a blue highlight. Still 746 // need exact visual design, and possibly framework support for this. 747 // See comments below for the exact logic. 748 749 final MenuItem speakerItem = menu.findItem(R.id.audio_mode_speaker); 750 speakerItem.setEnabled(isSupported(CallAudioState.ROUTE_SPEAKER)); 751 // TODO: Show speakerItem as initially "selected" if 752 // speaker is on. 753 754 // We display *either* "earpiece" or "wired headset", never both, 755 // depending on whether a wired headset is physically plugged in. 756 final MenuItem earpieceItem = menu.findItem(R.id.audio_mode_earpiece); 757 final MenuItem wiredHeadsetItem = menu.findItem(R.id.audio_mode_wired_headset); 758 759 final boolean usingHeadset = isSupported(CallAudioState.ROUTE_WIRED_HEADSET); 760 earpieceItem.setVisible(!usingHeadset); 761 earpieceItem.setEnabled(!usingHeadset); 762 wiredHeadsetItem.setVisible(usingHeadset); 763 wiredHeadsetItem.setEnabled(usingHeadset); 764 // TODO: Show the above item (either earpieceItem or wiredHeadsetItem) 765 // as initially "selected" if speakerOn and 766 // bluetoothIndicatorOn are both false. 767 768 final MenuItem bluetoothItem = menu.findItem(R.id.audio_mode_bluetooth); 769 bluetoothItem.setEnabled(isSupported(CallAudioState.ROUTE_BLUETOOTH)); 770 // TODO: Show bluetoothItem as initially "selected" if 771 // bluetoothIndicatorOn is true. 772 773 mAudioModePopup.show(); 774 775 // Unfortunately we need to manually keep track of the popup menu's 776 // visiblity, since PopupMenu doesn't have an isShowing() method like 777 // Dialogs do. 778 mAudioModePopupVisible = true; 779 } 780 isSupported(int mode)781 private boolean isSupported(int mode) { 782 return (mode == (getPresenter().getSupportedAudio() & mode)); 783 } 784 isAudio(int mode)785 private boolean isAudio(int mode) { 786 return (mode == getPresenter().getAudioMode()); 787 } 788 789 @Override displayDialpad(boolean value, boolean animate)790 public void displayDialpad(boolean value, boolean animate) { 791 if (getActivity() != null && getActivity() instanceof InCallActivity) { 792 boolean changed = ((InCallActivity) getActivity()).showDialpadFragment(value, animate); 793 if (changed) { 794 mShowDialpadButton.setSelected(value); 795 mShowDialpadButton.setContentDescription(getContext().getString( 796 value /* show */ ? R.string.onscreenShowDialpadText_unselected 797 : R.string.onscreenShowDialpadText_selected)); 798 } 799 } 800 } 801 802 @Override isDialpadVisible()803 public boolean isDialpadVisible() { 804 if (getActivity() != null && getActivity() instanceof InCallActivity) { 805 return ((InCallActivity) getActivity()).isDialpadVisible(); 806 } 807 return false; 808 } 809 810 @Override getContext()811 public Context getContext() { 812 return getActivity(); 813 } 814 } 815