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