1 /* 2 * Copyright (C) 2007 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 android.view; 18 19 import com.android.internal.R; 20 21 import android.app.AlertDialog; 22 import android.app.Dialog; 23 import android.content.DialogInterface.OnDismissListener; 24 import android.content.BroadcastReceiver; 25 import android.content.Context; 26 import android.content.DialogInterface; 27 import android.content.Intent; 28 import android.content.IntentFilter; 29 import android.content.res.Resources; 30 import android.media.AudioManager; 31 import android.media.AudioService; 32 import android.media.AudioSystem; 33 import android.media.RingtoneManager; 34 import android.media.ToneGenerator; 35 import android.net.Uri; 36 import android.os.Handler; 37 import android.os.Message; 38 import android.os.Vibrator; 39 import android.util.Log; 40 import android.view.WindowManager.LayoutParams; 41 import android.widget.ImageView; 42 import android.widget.SeekBar; 43 import android.widget.SeekBar.OnSeekBarChangeListener; 44 45 import java.util.HashMap; 46 47 /** 48 * Handle the volume up and down keys. 49 * 50 * This code really should be moved elsewhere. 51 * 52 * Seriously, it really really should be moved elsewhere. This is used by 53 * android.media.AudioService, which actually runs in the system process, to 54 * show the volume dialog when the user changes the volume. What a mess. 55 * 56 * @hide 57 */ 58 public class VolumePanel extends Handler implements OnSeekBarChangeListener, View.OnClickListener 59 { 60 private static final String TAG = "VolumePanel"; 61 private static boolean LOGD = false; 62 63 /** 64 * The delay before playing a sound. This small period exists so the user 65 * can press another key (non-volume keys, too) to have it NOT be audible. 66 * <p> 67 * PhoneWindow will implement this part. 68 */ 69 public static final int PLAY_SOUND_DELAY = 300; 70 71 /** 72 * The delay before vibrating. This small period exists so if the user is 73 * moving to silent mode, it will not emit a short vibrate (it normally 74 * would since vibrate is between normal mode and silent mode using hardware 75 * keys). 76 */ 77 public static final int VIBRATE_DELAY = 300; 78 79 private static final int VIBRATE_DURATION = 300; 80 private static final int BEEP_DURATION = 150; 81 private static final int MAX_VOLUME = 100; 82 private static final int FREE_DELAY = 10000; 83 private static final int TIMEOUT_DELAY = 3000; 84 85 private static final int MSG_VOLUME_CHANGED = 0; 86 private static final int MSG_FREE_RESOURCES = 1; 87 private static final int MSG_PLAY_SOUND = 2; 88 private static final int MSG_STOP_SOUNDS = 3; 89 private static final int MSG_VIBRATE = 4; 90 private static final int MSG_TIMEOUT = 5; 91 private static final int MSG_RINGER_MODE_CHANGED = 6; 92 private static final int MSG_MUTE_CHANGED = 7; 93 private static final int MSG_REMOTE_VOLUME_CHANGED = 8; 94 private static final int MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN = 9; 95 private static final int MSG_SLIDER_VISIBILITY_CHANGED = 10; 96 private static final int MSG_DISPLAY_SAFE_VOLUME_WARNING = 11; 97 98 // Pseudo stream type for master volume 99 private static final int STREAM_MASTER = -100; 100 // Pseudo stream type for remote volume is defined in AudioService.STREAM_REMOTE_MUSIC 101 102 protected Context mContext; 103 private AudioManager mAudioManager; 104 protected AudioService mAudioService; 105 private boolean mRingIsSilent; 106 private boolean mShowCombinedVolumes; 107 private boolean mVoiceCapable; 108 109 // True if we want to play tones on the system stream when the master stream is specified. 110 private final boolean mPlayMasterStreamTones; 111 112 /** Dialog containing all the sliders */ 113 private final Dialog mDialog; 114 /** Dialog's content view */ 115 private final View mView; 116 117 /** The visible portion of the volume overlay */ 118 private final ViewGroup mPanel; 119 /** Contains the sliders and their touchable icons */ 120 private final ViewGroup mSliderGroup; 121 /** The button that expands the dialog to show all sliders */ 122 private final View mMoreButton; 123 /** Dummy divider icon that needs to vanish with the more button */ 124 private final View mDivider; 125 126 /** Currently active stream that shows up at the top of the list of sliders */ 127 private int mActiveStreamType = -1; 128 /** All the slider controls mapped by stream type */ 129 private HashMap<Integer,StreamControl> mStreamControls; 130 131 private enum StreamResources { 132 BluetoothSCOStream(AudioManager.STREAM_BLUETOOTH_SCO, 133 R.string.volume_icon_description_bluetooth, 134 R.drawable.ic_audio_bt, 135 R.drawable.ic_audio_bt, 136 false), 137 RingerStream(AudioManager.STREAM_RING, 138 R.string.volume_icon_description_ringer, 139 R.drawable.ic_audio_ring_notif, 140 R.drawable.ic_audio_ring_notif_mute, 141 false), 142 VoiceStream(AudioManager.STREAM_VOICE_CALL, 143 R.string.volume_icon_description_incall, 144 R.drawable.ic_audio_phone, 145 R.drawable.ic_audio_phone, 146 false), 147 AlarmStream(AudioManager.STREAM_ALARM, 148 R.string.volume_alarm, 149 R.drawable.ic_audio_alarm, 150 R.drawable.ic_audio_alarm_mute, 151 false), 152 MediaStream(AudioManager.STREAM_MUSIC, 153 R.string.volume_icon_description_media, 154 R.drawable.ic_audio_vol, 155 R.drawable.ic_audio_vol_mute, 156 true), 157 NotificationStream(AudioManager.STREAM_NOTIFICATION, 158 R.string.volume_icon_description_notification, 159 R.drawable.ic_audio_notification, 160 R.drawable.ic_audio_notification_mute, 161 true), 162 // for now, use media resources for master volume 163 MasterStream(STREAM_MASTER, 164 R.string.volume_icon_description_media, //FIXME should have its own description 165 R.drawable.ic_audio_vol, 166 R.drawable.ic_audio_vol_mute, 167 false), 168 RemoteStream(AudioService.STREAM_REMOTE_MUSIC, 169 R.string.volume_icon_description_media, //FIXME should have its own description 170 R.drawable.ic_media_route_on_holo_dark, 171 R.drawable.ic_media_route_disabled_holo_dark, 172 false);// will be dynamically updated 173 174 int streamType; 175 int descRes; 176 int iconRes; 177 int iconMuteRes; 178 // RING, VOICE_CALL & BLUETOOTH_SCO are hidden unless explicitly requested 179 boolean show; 180 181 StreamResources(int streamType, int descRes, int iconRes, int iconMuteRes, boolean show) { 182 this.streamType = streamType; 183 this.descRes = descRes; 184 this.iconRes = iconRes; 185 this.iconMuteRes = iconMuteRes; 186 this.show = show; 187 } 188 }; 189 190 // List of stream types and their order 191 private static final StreamResources[] STREAMS = { 192 StreamResources.BluetoothSCOStream, 193 StreamResources.RingerStream, 194 StreamResources.VoiceStream, 195 StreamResources.MediaStream, 196 StreamResources.NotificationStream, 197 StreamResources.AlarmStream, 198 StreamResources.MasterStream, 199 StreamResources.RemoteStream 200 }; 201 202 /** Object that contains data for each slider */ 203 private class StreamControl { 204 int streamType; 205 ViewGroup group; 206 ImageView icon; 207 SeekBar seekbarView; 208 int iconRes; 209 int iconMuteRes; 210 } 211 212 // Synchronize when accessing this 213 private ToneGenerator mToneGenerators[]; 214 private Vibrator mVibrator; 215 216 private static AlertDialog sConfirmSafeVolumeDialog; 217 private static Object sConfirmSafeVolumeLock = new Object(); 218 219 private static class WarningDialogReceiver extends BroadcastReceiver 220 implements DialogInterface.OnDismissListener { 221 private final Context mContext; 222 private final Dialog mDialog; 223 private final VolumePanel mVolumePanel; 224 225 WarningDialogReceiver(Context context, Dialog dialog, VolumePanel volumePanel) { 226 mContext = context; 227 mDialog = dialog; 228 mVolumePanel = volumePanel; 229 IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); 230 context.registerReceiver(this, filter); 231 } 232 233 @Override 234 public void onReceive(Context context, Intent intent) { 235 mDialog.cancel(); 236 cleanUp(); 237 } 238 239 public void onDismiss(DialogInterface unused) { 240 mContext.unregisterReceiver(this); 241 cleanUp(); 242 } 243 244 private void cleanUp() { 245 synchronized (sConfirmSafeVolumeLock) { 246 sConfirmSafeVolumeDialog = null; 247 } 248 mVolumePanel.forceTimeout(); 249 mVolumePanel.updateStates(); 250 } 251 } 252 253 254 public VolumePanel(final Context context, AudioService volumeService) { 255 mContext = context; 256 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 257 mAudioService = volumeService; 258 259 // For now, only show master volume if master volume is supported 260 boolean useMasterVolume = context.getResources().getBoolean( 261 com.android.internal.R.bool.config_useMasterVolume); 262 if (useMasterVolume) { 263 for (int i = 0; i < STREAMS.length; i++) { 264 StreamResources streamRes = STREAMS[i]; 265 streamRes.show = (streamRes.streamType == STREAM_MASTER); 266 } 267 } 268 269 LayoutInflater inflater = (LayoutInflater) context 270 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 271 View view = mView = inflater.inflate(R.layout.volume_adjust, null); 272 mView.setOnTouchListener(new View.OnTouchListener() { 273 public boolean onTouch(View v, MotionEvent event) { 274 resetTimeout(); 275 return false; 276 } 277 }); 278 mPanel = (ViewGroup) mView.findViewById(R.id.visible_panel); 279 mSliderGroup = (ViewGroup) mView.findViewById(R.id.slider_group); 280 mMoreButton = (ImageView) mView.findViewById(R.id.expand_button); 281 mDivider = (ImageView) mView.findViewById(R.id.expand_button_divider); 282 283 mDialog = new Dialog(context, R.style.Theme_Panel_Volume) { 284 public boolean onTouchEvent(MotionEvent event) { 285 if (isShowing() && event.getAction() == MotionEvent.ACTION_OUTSIDE && 286 sConfirmSafeVolumeDialog == null) { 287 forceTimeout(); 288 return true; 289 } 290 return false; 291 } 292 }; 293 mDialog.setTitle("Volume control"); // No need to localize 294 mDialog.setContentView(mView); 295 mDialog.setOnDismissListener(new OnDismissListener() { 296 public void onDismiss(DialogInterface dialog) { 297 mActiveStreamType = -1; 298 mAudioManager.forceVolumeControlStream(mActiveStreamType); 299 } 300 }); 301 // Change some window properties 302 Window window = mDialog.getWindow(); 303 window.setGravity(Gravity.TOP); 304 LayoutParams lp = window.getAttributes(); 305 lp.token = null; 306 // Offset from the top 307 lp.y = mContext.getResources().getDimensionPixelOffset( 308 com.android.internal.R.dimen.volume_panel_top); 309 lp.type = LayoutParams.TYPE_VOLUME_OVERLAY; 310 lp.width = LayoutParams.WRAP_CONTENT; 311 lp.height = LayoutParams.WRAP_CONTENT; 312 lp.privateFlags |= LayoutParams.PRIVATE_FLAG_FORCE_SHOW_NAV_BAR; 313 window.setAttributes(lp); 314 window.addFlags(LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_NOT_TOUCH_MODAL 315 | LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH); 316 317 mToneGenerators = new ToneGenerator[AudioSystem.getNumStreamTypes()]; 318 mVibrator = (Vibrator)context.getSystemService(Context.VIBRATOR_SERVICE); 319 320 mVoiceCapable = context.getResources().getBoolean(R.bool.config_voice_capable); 321 mShowCombinedVolumes = !mVoiceCapable && !useMasterVolume; 322 // If we don't want to show multiple volumes, hide the settings button and divider 323 if (!mShowCombinedVolumes) { 324 mMoreButton.setVisibility(View.GONE); 325 mDivider.setVisibility(View.GONE); 326 } else { 327 mMoreButton.setOnClickListener(this); 328 } 329 330 boolean masterVolumeOnly = context.getResources().getBoolean( 331 com.android.internal.R.bool.config_useMasterVolume); 332 boolean masterVolumeKeySounds = mContext.getResources().getBoolean( 333 com.android.internal.R.bool.config_useVolumeKeySounds); 334 335 mPlayMasterStreamTones = masterVolumeOnly && masterVolumeKeySounds; 336 337 listenToRingerMode(); 338 } 339 340 public void setLayoutDirection(int layoutDirection) { 341 mPanel.setLayoutDirection(layoutDirection); 342 updateStates(); 343 } 344 345 private void listenToRingerMode() { 346 final IntentFilter filter = new IntentFilter(); 347 filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); 348 mContext.registerReceiver(new BroadcastReceiver() { 349 350 public void onReceive(Context context, Intent intent) { 351 final String action = intent.getAction(); 352 353 if (AudioManager.RINGER_MODE_CHANGED_ACTION.equals(action)) { 354 removeMessages(MSG_RINGER_MODE_CHANGED); 355 sendMessage(obtainMessage(MSG_RINGER_MODE_CHANGED)); 356 } 357 } 358 }, filter); 359 } 360 361 private boolean isMuted(int streamType) { 362 if (streamType == STREAM_MASTER) { 363 return mAudioManager.isMasterMute(); 364 } else if (streamType == AudioService.STREAM_REMOTE_MUSIC) { 365 return (mAudioService.getRemoteStreamVolume() <= 0); 366 } else { 367 return mAudioManager.isStreamMute(streamType); 368 } 369 } 370 371 private int getStreamMaxVolume(int streamType) { 372 if (streamType == STREAM_MASTER) { 373 return mAudioManager.getMasterMaxVolume(); 374 } else if (streamType == AudioService.STREAM_REMOTE_MUSIC) { 375 return mAudioService.getRemoteStreamMaxVolume(); 376 } else { 377 return mAudioManager.getStreamMaxVolume(streamType); 378 } 379 } 380 381 private int getStreamVolume(int streamType) { 382 if (streamType == STREAM_MASTER) { 383 return mAudioManager.getMasterVolume(); 384 } else if (streamType == AudioService.STREAM_REMOTE_MUSIC) { 385 return mAudioService.getRemoteStreamVolume(); 386 } else { 387 return mAudioManager.getStreamVolume(streamType); 388 } 389 } 390 391 private void setStreamVolume(int streamType, int index, int flags) { 392 if (streamType == STREAM_MASTER) { 393 mAudioManager.setMasterVolume(index, flags); 394 } else if (streamType == AudioService.STREAM_REMOTE_MUSIC) { 395 mAudioService.setRemoteStreamVolume(index); 396 } else { 397 mAudioManager.setStreamVolume(streamType, index, flags); 398 } 399 } 400 401 private void createSliders() { 402 LayoutInflater inflater = (LayoutInflater) mContext 403 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 404 mStreamControls = new HashMap<Integer, StreamControl>(STREAMS.length); 405 Resources res = mContext.getResources(); 406 for (int i = 0; i < STREAMS.length; i++) { 407 StreamResources streamRes = STREAMS[i]; 408 int streamType = streamRes.streamType; 409 if (mVoiceCapable && streamRes == StreamResources.NotificationStream) { 410 streamRes = StreamResources.RingerStream; 411 } 412 StreamControl sc = new StreamControl(); 413 sc.streamType = streamType; 414 sc.group = (ViewGroup) inflater.inflate(R.layout.volume_adjust_item, null); 415 sc.group.setTag(sc); 416 sc.icon = (ImageView) sc.group.findViewById(R.id.stream_icon); 417 sc.icon.setTag(sc); 418 sc.icon.setContentDescription(res.getString(streamRes.descRes)); 419 sc.iconRes = streamRes.iconRes; 420 sc.iconMuteRes = streamRes.iconMuteRes; 421 sc.icon.setImageResource(sc.iconRes); 422 sc.seekbarView = (SeekBar) sc.group.findViewById(R.id.seekbar); 423 int plusOne = (streamType == AudioSystem.STREAM_BLUETOOTH_SCO || 424 streamType == AudioSystem.STREAM_VOICE_CALL) ? 1 : 0; 425 sc.seekbarView.setMax(getStreamMaxVolume(streamType) + plusOne); 426 sc.seekbarView.setOnSeekBarChangeListener(this); 427 sc.seekbarView.setTag(sc); 428 mStreamControls.put(streamType, sc); 429 } 430 } 431 432 private void reorderSliders(int activeStreamType) { 433 mSliderGroup.removeAllViews(); 434 435 StreamControl active = mStreamControls.get(activeStreamType); 436 if (active == null) { 437 Log.e("VolumePanel", "Missing stream type! - " + activeStreamType); 438 mActiveStreamType = -1; 439 } else { 440 mSliderGroup.addView(active.group); 441 mActiveStreamType = activeStreamType; 442 active.group.setVisibility(View.VISIBLE); 443 updateSlider(active); 444 } 445 446 addOtherVolumes(); 447 } 448 449 private void addOtherVolumes() { 450 if (!mShowCombinedVolumes) return; 451 452 for (int i = 0; i < STREAMS.length; i++) { 453 // Skip the phone specific ones and the active one 454 final int streamType = STREAMS[i].streamType; 455 if (!STREAMS[i].show || streamType == mActiveStreamType) { 456 continue; 457 } 458 StreamControl sc = mStreamControls.get(streamType); 459 mSliderGroup.addView(sc.group); 460 updateSlider(sc); 461 } 462 } 463 464 /** Update the mute and progress state of a slider */ 465 private void updateSlider(StreamControl sc) { 466 sc.seekbarView.setProgress(getStreamVolume(sc.streamType)); 467 final boolean muted = isMuted(sc.streamType); 468 // Force reloading the image resource 469 sc.icon.setImageDrawable(null); 470 sc.icon.setImageResource(muted ? sc.iconMuteRes : sc.iconRes); 471 if (((sc.streamType == AudioManager.STREAM_RING) || 472 (sc.streamType == AudioManager.STREAM_NOTIFICATION)) && 473 mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE) { 474 sc.icon.setImageResource(R.drawable.ic_audio_ring_notif_vibrate); 475 } 476 if (sc.streamType == AudioService.STREAM_REMOTE_MUSIC) { 477 // never disable touch interactions for remote playback, the muting is not tied to 478 // the state of the phone. 479 sc.seekbarView.setEnabled(true); 480 } else if ((sc.streamType != mAudioManager.getMasterStreamType() && muted) || 481 (sConfirmSafeVolumeDialog != null)) { 482 sc.seekbarView.setEnabled(false); 483 } else { 484 sc.seekbarView.setEnabled(true); 485 } 486 } 487 488 private boolean isExpanded() { 489 return mMoreButton.getVisibility() != View.VISIBLE; 490 } 491 492 private void expand() { 493 final int count = mSliderGroup.getChildCount(); 494 for (int i = 0; i < count; i++) { 495 mSliderGroup.getChildAt(i).setVisibility(View.VISIBLE); 496 } 497 mMoreButton.setVisibility(View.INVISIBLE); 498 mDivider.setVisibility(View.INVISIBLE); 499 } 500 501 private void collapse() { 502 mMoreButton.setVisibility(View.VISIBLE); 503 mDivider.setVisibility(View.VISIBLE); 504 final int count = mSliderGroup.getChildCount(); 505 for (int i = 1; i < count; i++) { 506 mSliderGroup.getChildAt(i).setVisibility(View.GONE); 507 } 508 } 509 510 public void updateStates() { 511 final int count = mSliderGroup.getChildCount(); 512 for (int i = 0; i < count; i++) { 513 StreamControl sc = (StreamControl) mSliderGroup.getChildAt(i).getTag(); 514 updateSlider(sc); 515 } 516 } 517 518 public void postVolumeChanged(int streamType, int flags) { 519 if (hasMessages(MSG_VOLUME_CHANGED)) return; 520 synchronized (this) { 521 if (mStreamControls == null) { 522 createSliders(); 523 } 524 } 525 removeMessages(MSG_FREE_RESOURCES); 526 obtainMessage(MSG_VOLUME_CHANGED, streamType, flags).sendToTarget(); 527 } 528 529 public void postRemoteVolumeChanged(int streamType, int flags) { 530 if (hasMessages(MSG_REMOTE_VOLUME_CHANGED)) return; 531 synchronized (this) { 532 if (mStreamControls == null) { 533 createSliders(); 534 } 535 } 536 removeMessages(MSG_FREE_RESOURCES); 537 obtainMessage(MSG_REMOTE_VOLUME_CHANGED, streamType, flags).sendToTarget(); 538 } 539 540 public void postRemoteSliderVisibility(boolean visible) { 541 obtainMessage(MSG_SLIDER_VISIBILITY_CHANGED, 542 AudioService.STREAM_REMOTE_MUSIC, visible ? 1 : 0).sendToTarget(); 543 } 544 545 /** 546 * Called by AudioService when it has received new remote playback information that 547 * would affect the VolumePanel display (mainly volumes). The difference with 548 * {@link #postRemoteVolumeChanged(int, int)} is that the handling of the posted message 549 * (MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN) will only update the volume slider if it is being 550 * displayed. 551 * This special code path is due to the fact that remote volume updates arrive to AudioService 552 * asynchronously. So after AudioService has sent the volume update (which should be treated 553 * as a request to update the volume), the application will likely set a new volume. If the UI 554 * is still up, we need to refresh the display to show this new value. 555 */ 556 public void postHasNewRemotePlaybackInfo() { 557 if (hasMessages(MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN)) return; 558 // don't create or prevent resources to be freed, if they disappear, this update came too 559 // late and shouldn't warrant the panel to be displayed longer 560 obtainMessage(MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN).sendToTarget(); 561 } 562 563 public void postMasterVolumeChanged(int flags) { 564 postVolumeChanged(STREAM_MASTER, flags); 565 } 566 567 public void postMuteChanged(int streamType, int flags) { 568 if (hasMessages(MSG_VOLUME_CHANGED)) return; 569 synchronized (this) { 570 if (mStreamControls == null) { 571 createSliders(); 572 } 573 } 574 removeMessages(MSG_FREE_RESOURCES); 575 obtainMessage(MSG_MUTE_CHANGED, streamType, flags).sendToTarget(); 576 } 577 578 public void postMasterMuteChanged(int flags) { 579 postMuteChanged(STREAM_MASTER, flags); 580 } 581 582 public void postDisplaySafeVolumeWarning(int flags) { 583 if (hasMessages(MSG_DISPLAY_SAFE_VOLUME_WARNING)) return; 584 obtainMessage(MSG_DISPLAY_SAFE_VOLUME_WARNING, flags, 0).sendToTarget(); 585 } 586 587 /** 588 * Override this if you have other work to do when the volume changes (for 589 * example, vibrating, playing a sound, etc.). Make sure to call through to 590 * the superclass implementation. 591 */ 592 protected void onVolumeChanged(int streamType, int flags) { 593 594 if (LOGD) Log.d(TAG, "onVolumeChanged(streamType: " + streamType + ", flags: " + flags + ")"); 595 596 if ((flags & AudioManager.FLAG_SHOW_UI) != 0) { 597 synchronized (this) { 598 if (mActiveStreamType != streamType) { 599 reorderSliders(streamType); 600 } 601 onShowVolumeChanged(streamType, flags); 602 } 603 } 604 605 if ((flags & AudioManager.FLAG_PLAY_SOUND) != 0 && ! mRingIsSilent) { 606 removeMessages(MSG_PLAY_SOUND); 607 sendMessageDelayed(obtainMessage(MSG_PLAY_SOUND, streamType, flags), PLAY_SOUND_DELAY); 608 } 609 610 if ((flags & AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE) != 0) { 611 removeMessages(MSG_PLAY_SOUND); 612 removeMessages(MSG_VIBRATE); 613 onStopSounds(); 614 } 615 616 removeMessages(MSG_FREE_RESOURCES); 617 sendMessageDelayed(obtainMessage(MSG_FREE_RESOURCES), FREE_DELAY); 618 resetTimeout(); 619 } 620 621 protected void onMuteChanged(int streamType, int flags) { 622 623 if (LOGD) Log.d(TAG, "onMuteChanged(streamType: " + streamType + ", flags: " + flags + ")"); 624 625 StreamControl sc = mStreamControls.get(streamType); 626 if (sc != null) { 627 sc.icon.setImageResource(isMuted(sc.streamType) ? sc.iconMuteRes : sc.iconRes); 628 } 629 630 onVolumeChanged(streamType, flags); 631 } 632 633 protected void onShowVolumeChanged(int streamType, int flags) { 634 int index = getStreamVolume(streamType); 635 636 mRingIsSilent = false; 637 638 if (LOGD) { 639 Log.d(TAG, "onShowVolumeChanged(streamType: " + streamType 640 + ", flags: " + flags + "), index: " + index); 641 } 642 643 // get max volume for progress bar 644 645 int max = getStreamMaxVolume(streamType); 646 647 switch (streamType) { 648 649 case AudioManager.STREAM_RING: { 650 // setRingerIcon(); 651 Uri ringuri = RingtoneManager.getActualDefaultRingtoneUri( 652 mContext, RingtoneManager.TYPE_RINGTONE); 653 if (ringuri == null) { 654 mRingIsSilent = true; 655 } 656 break; 657 } 658 659 case AudioManager.STREAM_MUSIC: { 660 // Special case for when Bluetooth is active for music 661 if ((mAudioManager.getDevicesForStream(AudioManager.STREAM_MUSIC) & 662 (AudioManager.DEVICE_OUT_BLUETOOTH_A2DP | 663 AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES | 664 AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER)) != 0) { 665 setMusicIcon(R.drawable.ic_audio_bt, R.drawable.ic_audio_bt_mute); 666 } else { 667 setMusicIcon(R.drawable.ic_audio_vol, R.drawable.ic_audio_vol_mute); 668 } 669 break; 670 } 671 672 case AudioManager.STREAM_VOICE_CALL: { 673 /* 674 * For in-call voice call volume, there is no inaudible volume. 675 * Rescale the UI control so the progress bar doesn't go all 676 * the way to zero and don't show the mute icon. 677 */ 678 index++; 679 max++; 680 break; 681 } 682 683 case AudioManager.STREAM_ALARM: { 684 break; 685 } 686 687 case AudioManager.STREAM_NOTIFICATION: { 688 Uri ringuri = RingtoneManager.getActualDefaultRingtoneUri( 689 mContext, RingtoneManager.TYPE_NOTIFICATION); 690 if (ringuri == null) { 691 mRingIsSilent = true; 692 } 693 break; 694 } 695 696 case AudioManager.STREAM_BLUETOOTH_SCO: { 697 /* 698 * For in-call voice call volume, there is no inaudible volume. 699 * Rescale the UI control so the progress bar doesn't go all 700 * the way to zero and don't show the mute icon. 701 */ 702 index++; 703 max++; 704 break; 705 } 706 707 case AudioService.STREAM_REMOTE_MUSIC: { 708 if (LOGD) { Log.d(TAG, "showing remote volume "+index+" over "+ max); } 709 break; 710 } 711 } 712 713 StreamControl sc = mStreamControls.get(streamType); 714 if (sc != null) { 715 if (sc.seekbarView.getMax() != max) { 716 sc.seekbarView.setMax(max); 717 } 718 719 sc.seekbarView.setProgress(index); 720 if (((flags & AudioManager.FLAG_FIXED_VOLUME) != 0) || 721 (streamType != mAudioManager.getMasterStreamType() && 722 streamType != AudioService.STREAM_REMOTE_MUSIC && 723 isMuted(streamType)) || 724 sConfirmSafeVolumeDialog != null) { 725 sc.seekbarView.setEnabled(false); 726 } else { 727 sc.seekbarView.setEnabled(true); 728 } 729 } 730 731 if (!mDialog.isShowing()) { 732 int stream = (streamType == AudioService.STREAM_REMOTE_MUSIC) ? -1 : streamType; 733 // when the stream is for remote playback, use -1 to reset the stream type evaluation 734 mAudioManager.forceVolumeControlStream(stream); 735 mDialog.setContentView(mView); 736 // Showing dialog - use collapsed state 737 if (mShowCombinedVolumes) { 738 collapse(); 739 } 740 mDialog.show(); 741 } 742 743 // Do a little vibrate if applicable (only when going into vibrate mode) 744 if ((streamType != AudioService.STREAM_REMOTE_MUSIC) && 745 ((flags & AudioManager.FLAG_VIBRATE) != 0) && 746 mAudioService.isStreamAffectedByRingerMode(streamType) && 747 mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE) { 748 sendMessageDelayed(obtainMessage(MSG_VIBRATE), VIBRATE_DELAY); 749 } 750 } 751 752 protected void onPlaySound(int streamType, int flags) { 753 754 if (hasMessages(MSG_STOP_SOUNDS)) { 755 removeMessages(MSG_STOP_SOUNDS); 756 // Force stop right now 757 onStopSounds(); 758 } 759 760 synchronized (this) { 761 ToneGenerator toneGen = getOrCreateToneGenerator(streamType); 762 if (toneGen != null) { 763 toneGen.startTone(ToneGenerator.TONE_PROP_BEEP); 764 sendMessageDelayed(obtainMessage(MSG_STOP_SOUNDS), BEEP_DURATION); 765 } 766 } 767 } 768 769 protected void onStopSounds() { 770 771 synchronized (this) { 772 int numStreamTypes = AudioSystem.getNumStreamTypes(); 773 for (int i = numStreamTypes - 1; i >= 0; i--) { 774 ToneGenerator toneGen = mToneGenerators[i]; 775 if (toneGen != null) { 776 toneGen.stopTone(); 777 } 778 } 779 } 780 } 781 782 protected void onVibrate() { 783 784 // Make sure we ended up in vibrate ringer mode 785 if (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_VIBRATE) { 786 return; 787 } 788 789 mVibrator.vibrate(VIBRATE_DURATION); 790 } 791 792 protected void onRemoteVolumeChanged(int streamType, int flags) { 793 // streamType is the real stream type being affected, but for the UI sliders, we 794 // refer to AudioService.STREAM_REMOTE_MUSIC. We still play the beeps on the real 795 // stream type. 796 if (LOGD) Log.d(TAG, "onRemoteVolumeChanged(stream:"+streamType+", flags: " + flags + ")"); 797 798 if (((flags & AudioManager.FLAG_SHOW_UI) != 0) || mDialog.isShowing()) { 799 synchronized (this) { 800 if (mActiveStreamType != AudioService.STREAM_REMOTE_MUSIC) { 801 reorderSliders(AudioService.STREAM_REMOTE_MUSIC); 802 } 803 onShowVolumeChanged(AudioService.STREAM_REMOTE_MUSIC, flags); 804 } 805 } else { 806 if (LOGD) Log.d(TAG, "not calling onShowVolumeChanged(), no FLAG_SHOW_UI or no UI"); 807 } 808 809 if ((flags & AudioManager.FLAG_PLAY_SOUND) != 0 && ! mRingIsSilent) { 810 removeMessages(MSG_PLAY_SOUND); 811 sendMessageDelayed(obtainMessage(MSG_PLAY_SOUND, streamType, flags), PLAY_SOUND_DELAY); 812 } 813 814 if ((flags & AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE) != 0) { 815 removeMessages(MSG_PLAY_SOUND); 816 removeMessages(MSG_VIBRATE); 817 onStopSounds(); 818 } 819 820 removeMessages(MSG_FREE_RESOURCES); 821 sendMessageDelayed(obtainMessage(MSG_FREE_RESOURCES), FREE_DELAY); 822 resetTimeout(); 823 } 824 825 protected void onRemoteVolumeUpdateIfShown() { 826 if (LOGD) Log.d(TAG, "onRemoteVolumeUpdateIfShown()"); 827 if (mDialog.isShowing() 828 && (mActiveStreamType == AudioService.STREAM_REMOTE_MUSIC) 829 && (mStreamControls != null)) { 830 onShowVolumeChanged(AudioService.STREAM_REMOTE_MUSIC, 0); 831 } 832 } 833 834 835 /** 836 * Handler for MSG_SLIDER_VISIBILITY_CHANGED 837 * Hide or show a slider 838 * @param streamType can be a valid stream type value, or VolumePanel.STREAM_MASTER, 839 * or AudioService.STREAM_REMOTE_MUSIC 840 * @param visible 841 */ 842 synchronized protected void onSliderVisibilityChanged(int streamType, int visible) { 843 if (LOGD) Log.d(TAG, "onSliderVisibilityChanged(stream="+streamType+", visi="+visible+")"); 844 boolean isVisible = (visible == 1); 845 for (int i = STREAMS.length - 1 ; i >= 0 ; i--) { 846 StreamResources streamRes = STREAMS[i]; 847 if (streamRes.streamType == streamType) { 848 streamRes.show = isVisible; 849 if (!isVisible && (mActiveStreamType == streamType)) { 850 mActiveStreamType = -1; 851 } 852 break; 853 } 854 } 855 } 856 857 protected void onDisplaySafeVolumeWarning(int flags) { 858 if ((flags & AudioManager.FLAG_SHOW_UI) != 0 || mDialog.isShowing()) { 859 synchronized (sConfirmSafeVolumeLock) { 860 if (sConfirmSafeVolumeDialog != null) { 861 return; 862 } 863 sConfirmSafeVolumeDialog = new AlertDialog.Builder(mContext) 864 .setMessage(com.android.internal.R.string.safe_media_volume_warning) 865 .setPositiveButton(com.android.internal.R.string.yes, 866 new DialogInterface.OnClickListener() { 867 public void onClick(DialogInterface dialog, int which) { 868 mAudioService.disableSafeMediaVolume(); 869 } 870 }) 871 .setNegativeButton(com.android.internal.R.string.no, null) 872 .setIconAttribute(android.R.attr.alertDialogIcon) 873 .create(); 874 final WarningDialogReceiver warning = new WarningDialogReceiver(mContext, 875 sConfirmSafeVolumeDialog, this); 876 877 sConfirmSafeVolumeDialog.setOnDismissListener(warning); 878 sConfirmSafeVolumeDialog.getWindow().setType( 879 WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); 880 sConfirmSafeVolumeDialog.show(); 881 } 882 updateStates(); 883 } 884 resetTimeout(); 885 } 886 887 /** 888 * Lock on this VolumePanel instance as long as you use the returned ToneGenerator. 889 */ 890 private ToneGenerator getOrCreateToneGenerator(int streamType) { 891 if (streamType == STREAM_MASTER) { 892 // For devices that use the master volume setting only but still want to 893 // play a volume-changed tone, direct the master volume pseudostream to 894 // the system stream's tone generator. 895 if (mPlayMasterStreamTones) { 896 streamType = AudioManager.STREAM_SYSTEM; 897 } else { 898 return null; 899 } 900 } 901 synchronized (this) { 902 if (mToneGenerators[streamType] == null) { 903 try { 904 mToneGenerators[streamType] = new ToneGenerator(streamType, MAX_VOLUME); 905 } catch (RuntimeException e) { 906 if (LOGD) { 907 Log.d(TAG, "ToneGenerator constructor failed with " 908 + "RuntimeException: " + e); 909 } 910 } 911 } 912 return mToneGenerators[streamType]; 913 } 914 } 915 916 917 /** 918 * Switch between icons because Bluetooth music is same as music volume, but with 919 * different icons. 920 */ 921 private void setMusicIcon(int resId, int resMuteId) { 922 StreamControl sc = mStreamControls.get(AudioManager.STREAM_MUSIC); 923 if (sc != null) { 924 sc.iconRes = resId; 925 sc.iconMuteRes = resMuteId; 926 sc.icon.setImageResource(isMuted(sc.streamType) ? sc.iconMuteRes : sc.iconRes); 927 } 928 } 929 930 protected void onFreeResources() { 931 synchronized (this) { 932 for (int i = mToneGenerators.length - 1; i >= 0; i--) { 933 if (mToneGenerators[i] != null) { 934 mToneGenerators[i].release(); 935 } 936 mToneGenerators[i] = null; 937 } 938 } 939 } 940 941 @Override 942 public void handleMessage(Message msg) { 943 switch (msg.what) { 944 945 case MSG_VOLUME_CHANGED: { 946 onVolumeChanged(msg.arg1, msg.arg2); 947 break; 948 } 949 950 case MSG_MUTE_CHANGED: { 951 onMuteChanged(msg.arg1, msg.arg2); 952 break; 953 } 954 955 case MSG_FREE_RESOURCES: { 956 onFreeResources(); 957 break; 958 } 959 960 case MSG_STOP_SOUNDS: { 961 onStopSounds(); 962 break; 963 } 964 965 case MSG_PLAY_SOUND: { 966 onPlaySound(msg.arg1, msg.arg2); 967 break; 968 } 969 970 case MSG_VIBRATE: { 971 onVibrate(); 972 break; 973 } 974 975 case MSG_TIMEOUT: { 976 if (mDialog.isShowing()) { 977 mDialog.dismiss(); 978 mActiveStreamType = -1; 979 } 980 synchronized (sConfirmSafeVolumeLock) { 981 if (sConfirmSafeVolumeDialog != null) { 982 sConfirmSafeVolumeDialog.dismiss(); 983 } 984 } 985 break; 986 } 987 case MSG_RINGER_MODE_CHANGED: { 988 if (mDialog.isShowing()) { 989 updateStates(); 990 } 991 break; 992 } 993 994 case MSG_REMOTE_VOLUME_CHANGED: { 995 onRemoteVolumeChanged(msg.arg1, msg.arg2); 996 break; 997 } 998 999 case MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN: 1000 onRemoteVolumeUpdateIfShown(); 1001 break; 1002 1003 case MSG_SLIDER_VISIBILITY_CHANGED: 1004 onSliderVisibilityChanged(msg.arg1, msg.arg2); 1005 break; 1006 1007 case MSG_DISPLAY_SAFE_VOLUME_WARNING: 1008 onDisplaySafeVolumeWarning(msg.arg1); 1009 break; 1010 } 1011 } 1012 1013 private void resetTimeout() { 1014 removeMessages(MSG_TIMEOUT); 1015 sendMessageDelayed(obtainMessage(MSG_TIMEOUT), TIMEOUT_DELAY); 1016 } 1017 1018 private void forceTimeout() { 1019 removeMessages(MSG_TIMEOUT); 1020 sendMessage(obtainMessage(MSG_TIMEOUT)); 1021 } 1022 1023 public void onProgressChanged(SeekBar seekBar, int progress, 1024 boolean fromUser) { 1025 final Object tag = seekBar.getTag(); 1026 if (fromUser && tag instanceof StreamControl) { 1027 StreamControl sc = (StreamControl) tag; 1028 if (getStreamVolume(sc.streamType) != progress) { 1029 setStreamVolume(sc.streamType, progress, 0); 1030 } 1031 } 1032 resetTimeout(); 1033 } 1034 1035 public void onStartTrackingTouch(SeekBar seekBar) { 1036 } 1037 1038 public void onStopTrackingTouch(SeekBar seekBar) { 1039 final Object tag = seekBar.getTag(); 1040 if (tag instanceof StreamControl) { 1041 StreamControl sc = (StreamControl) tag; 1042 // because remote volume updates are asynchronous, AudioService might have received 1043 // a new remote volume value since the finger adjusted the slider. So when the 1044 // progress of the slider isn't being tracked anymore, adjust the slider to the last 1045 // "published" remote volume value, so the UI reflects the actual volume. 1046 if (sc.streamType == AudioService.STREAM_REMOTE_MUSIC) { 1047 seekBar.setProgress(getStreamVolume(AudioService.STREAM_REMOTE_MUSIC)); 1048 } 1049 } 1050 } 1051 1052 public void onClick(View v) { 1053 if (v == mMoreButton) { 1054 expand(); 1055 } 1056 resetTimeout(); 1057 } 1058 } 1059