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 android.graphics.drawable.LayerDrawable; 20 import android.os.Bundle; 21 import android.view.LayoutInflater; 22 import android.view.Menu; 23 import android.view.MenuItem; 24 import android.view.View; 25 import android.view.View.OnClickListener; 26 import android.view.ViewGroup; 27 import android.widget.CompoundButton; 28 import android.widget.ImageButton; 29 import android.widget.PopupMenu; 30 import android.widget.PopupMenu.OnDismissListener; 31 import android.widget.PopupMenu.OnMenuItemClickListener; 32 import android.widget.ToggleButton; 33 34 import com.android.services.telephony.common.AudioMode; 35 36 /** 37 * Fragment for call control buttons 38 */ 39 public 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