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 window.setAttributes(lp); 315 window.addFlags(LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_NOT_TOUCH_MODAL 316 | LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH); 317 318 mToneGenerators = new ToneGenerator[AudioSystem.getNumStreamTypes()]; 319 mVibrator = (Vibrator)context.getSystemService(Context.VIBRATOR_SERVICE); 320 321 mVoiceCapable = context.getResources().getBoolean(R.bool.config_voice_capable); 322 mShowCombinedVolumes = !mVoiceCapable && !useMasterVolume; 323 // If we don't want to show multiple volumes, hide the settings button and divider 324 if (!mShowCombinedVolumes) { 325 mMoreButton.setVisibility(View.GONE); 326 mDivider.setVisibility(View.GONE); 327 } else { 328 mMoreButton.setOnClickListener(this); 329 } 330 331 boolean masterVolumeOnly = context.getResources().getBoolean( 332 com.android.internal.R.bool.config_useMasterVolume); 333 boolean masterVolumeKeySounds = mContext.getResources().getBoolean( 334 com.android.internal.R.bool.config_useVolumeKeySounds); 335 336 mPlayMasterStreamTones = masterVolumeOnly && masterVolumeKeySounds; 337 338 listenToRingerMode(); 339 } 340 341 public void setLayoutDirection(int layoutDirection) { 342 mPanel.setLayoutDirection(layoutDirection); 343 updateStates(); 344 } 345 346 private void listenToRingerMode() { 347 final IntentFilter filter = new IntentFilter(); 348 filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); 349 mContext.registerReceiver(new BroadcastReceiver() { 350 351 public void onReceive(Context context, Intent intent) { 352 final String action = intent.getAction(); 353 354 if (AudioManager.RINGER_MODE_CHANGED_ACTION.equals(action)) { 355 removeMessages(MSG_RINGER_MODE_CHANGED); 356 sendMessage(obtainMessage(MSG_RINGER_MODE_CHANGED)); 357 } 358 } 359 }, filter); 360 } 361 362 private boolean isMuted(int streamType) { 363 if (streamType == STREAM_MASTER) { 364 return mAudioManager.isMasterMute(); 365 } else if (streamType == AudioService.STREAM_REMOTE_MUSIC) { 366 return (mAudioService.getRemoteStreamVolume() <= 0); 367 } else { 368 return mAudioManager.isStreamMute(streamType); 369 } 370 } 371 372 private int getStreamMaxVolume(int streamType) { 373 if (streamType == STREAM_MASTER) { 374 return mAudioManager.getMasterMaxVolume(); 375 } else if (streamType == AudioService.STREAM_REMOTE_MUSIC) { 376 return mAudioService.getRemoteStreamMaxVolume(); 377 } else { 378 return mAudioManager.getStreamMaxVolume(streamType); 379 } 380 } 381 382 private int getStreamVolume(int streamType) { 383 if (streamType == STREAM_MASTER) { 384 return mAudioManager.getMasterVolume(); 385 } else if (streamType == AudioService.STREAM_REMOTE_MUSIC) { 386 return mAudioService.getRemoteStreamVolume(); 387 } else { 388 return mAudioManager.getStreamVolume(streamType); 389 } 390 } 391 392 private void setStreamVolume(int streamType, int index, int flags) { 393 if (streamType == STREAM_MASTER) { 394 mAudioManager.setMasterVolume(index, flags); 395 } else if (streamType == AudioService.STREAM_REMOTE_MUSIC) { 396 mAudioService.setRemoteStreamVolume(index); 397 } else { 398 mAudioManager.setStreamVolume(streamType, index, flags); 399 } 400 } 401 402 private void createSliders() { 403 LayoutInflater inflater = (LayoutInflater) mContext 404 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 405 mStreamControls = new HashMap<Integer, StreamControl>(STREAMS.length); 406 Resources res = mContext.getResources(); 407 for (int i = 0; i < STREAMS.length; i++) { 408 StreamResources streamRes = STREAMS[i]; 409 int streamType = streamRes.streamType; 410 if (mVoiceCapable && streamRes == StreamResources.NotificationStream) { 411 streamRes = StreamResources.RingerStream; 412 } 413 StreamControl sc = new StreamControl(); 414 sc.streamType = streamType; 415 sc.group = (ViewGroup) inflater.inflate(R.layout.volume_adjust_item, null); 416 sc.group.setTag(sc); 417 sc.icon = (ImageView) sc.group.findViewById(R.id.stream_icon); 418 sc.icon.setTag(sc); 419 sc.icon.setContentDescription(res.getString(streamRes.descRes)); 420 sc.iconRes = streamRes.iconRes; 421 sc.iconMuteRes = streamRes.iconMuteRes; 422 sc.icon.setImageResource(sc.iconRes); 423 sc.seekbarView = (SeekBar) sc.group.findViewById(R.id.seekbar); 424 int plusOne = (streamType == AudioSystem.STREAM_BLUETOOTH_SCO || 425 streamType == AudioSystem.STREAM_VOICE_CALL) ? 1 : 0; 426 sc.seekbarView.setMax(getStreamMaxVolume(streamType) + plusOne); 427 sc.seekbarView.setOnSeekBarChangeListener(this); 428 sc.seekbarView.setTag(sc); 429 mStreamControls.put(streamType, sc); 430 } 431 } 432 433 private void reorderSliders(int activeStreamType) { 434 mSliderGroup.removeAllViews(); 435 436 StreamControl active = mStreamControls.get(activeStreamType); 437 if (active == null) { 438 Log.e("VolumePanel", "Missing stream type! - " + activeStreamType); 439 mActiveStreamType = -1; 440 } else { 441 mSliderGroup.addView(active.group); 442 mActiveStreamType = activeStreamType; 443 active.group.setVisibility(View.VISIBLE); 444 updateSlider(active); 445 } 446 447 addOtherVolumes(); 448 } 449 450 private void addOtherVolumes() { 451 if (!mShowCombinedVolumes) return; 452 453 for (int i = 0; i < STREAMS.length; i++) { 454 // Skip the phone specific ones and the active one 455 final int streamType = STREAMS[i].streamType; 456 if (!STREAMS[i].show || streamType == mActiveStreamType) { 457 continue; 458 } 459 StreamControl sc = mStreamControls.get(streamType); 460 mSliderGroup.addView(sc.group); 461 updateSlider(sc); 462 } 463 } 464 465 /** Update the mute and progress state of a slider */ 466 private void updateSlider(StreamControl sc) { 467 sc.seekbarView.setProgress(getStreamVolume(sc.streamType)); 468 final boolean muted = isMuted(sc.streamType); 469 // Force reloading the image resource 470 sc.icon.setImageDrawable(null); 471 sc.icon.setImageResource(muted ? sc.iconMuteRes : sc.iconRes); 472 if (((sc.streamType == AudioManager.STREAM_RING) || 473 (sc.streamType == AudioManager.STREAM_NOTIFICATION)) && 474 mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE) { 475 sc.icon.setImageResource(R.drawable.ic_audio_ring_notif_vibrate); 476 } 477 if (sc.streamType == AudioService.STREAM_REMOTE_MUSIC) { 478 // never disable touch interactions for remote playback, the muting is not tied to 479 // the state of the phone. 480 sc.seekbarView.setEnabled(true); 481 } else if ((sc.streamType != mAudioManager.getMasterStreamType() && muted) || 482 (sConfirmSafeVolumeDialog != null)) { 483 sc.seekbarView.setEnabled(false); 484 } else { 485 sc.seekbarView.setEnabled(true); 486 } 487 } 488 489 private boolean isExpanded() { 490 return mMoreButton.getVisibility() != View.VISIBLE; 491 } 492 493 private void expand() { 494 final int count = mSliderGroup.getChildCount(); 495 for (int i = 0; i < count; i++) { 496 mSliderGroup.getChildAt(i).setVisibility(View.VISIBLE); 497 } 498 mMoreButton.setVisibility(View.INVISIBLE); 499 mDivider.setVisibility(View.INVISIBLE); 500 } 501 502 private void collapse() { 503 mMoreButton.setVisibility(View.VISIBLE); 504 mDivider.setVisibility(View.VISIBLE); 505 final int count = mSliderGroup.getChildCount(); 506 for (int i = 1; i < count; i++) { 507 mSliderGroup.getChildAt(i).setVisibility(View.GONE); 508 } 509 } 510 511 public void updateStates() { 512 final int count = mSliderGroup.getChildCount(); 513 for (int i = 0; i < count; i++) { 514 StreamControl sc = (StreamControl) mSliderGroup.getChildAt(i).getTag(); 515 updateSlider(sc); 516 } 517 } 518 519 public void postVolumeChanged(int streamType, int flags) { 520 if (hasMessages(MSG_VOLUME_CHANGED)) return; 521 synchronized (this) { 522 if (mStreamControls == null) { 523 createSliders(); 524 } 525 } 526 removeMessages(MSG_FREE_RESOURCES); 527 obtainMessage(MSG_VOLUME_CHANGED, streamType, flags).sendToTarget(); 528 } 529 530 public void postRemoteVolumeChanged(int streamType, int flags) { 531 if (hasMessages(MSG_REMOTE_VOLUME_CHANGED)) return; 532 synchronized (this) { 533 if (mStreamControls == null) { 534 createSliders(); 535 } 536 } 537 removeMessages(MSG_FREE_RESOURCES); 538 obtainMessage(MSG_REMOTE_VOLUME_CHANGED, streamType, flags).sendToTarget(); 539 } 540 541 public void postRemoteSliderVisibility(boolean visible) { 542 obtainMessage(MSG_SLIDER_VISIBILITY_CHANGED, 543 AudioService.STREAM_REMOTE_MUSIC, visible ? 1 : 0).sendToTarget(); 544 } 545 546 /** 547 * Called by AudioService when it has received new remote playback information that 548 * would affect the VolumePanel display (mainly volumes). The difference with 549 * {@link #postRemoteVolumeChanged(int, int)} is that the handling of the posted message 550 * (MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN) will only update the volume slider if it is being 551 * displayed. 552 * This special code path is due to the fact that remote volume updates arrive to AudioService 553 * asynchronously. So after AudioService has sent the volume update (which should be treated 554 * as a request to update the volume), the application will likely set a new volume. If the UI 555 * is still up, we need to refresh the display to show this new value. 556 */ 557 public void postHasNewRemotePlaybackInfo() { 558 if (hasMessages(MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN)) return; 559 // don't create or prevent resources to be freed, if they disappear, this update came too 560 // late and shouldn't warrant the panel to be displayed longer 561 obtainMessage(MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN).sendToTarget(); 562 } 563 564 public void postMasterVolumeChanged(int flags) { 565 postVolumeChanged(STREAM_MASTER, flags); 566 } 567 568 public void postMuteChanged(int streamType, int flags) { 569 if (hasMessages(MSG_VOLUME_CHANGED)) return; 570 synchronized (this) { 571 if (mStreamControls == null) { 572 createSliders(); 573 } 574 } 575 removeMessages(MSG_FREE_RESOURCES); 576 obtainMessage(MSG_MUTE_CHANGED, streamType, flags).sendToTarget(); 577 } 578 579 public void postMasterMuteChanged(int flags) { 580 postMuteChanged(STREAM_MASTER, flags); 581 } 582 583 public void postDisplaySafeVolumeWarning(int flags) { 584 if (hasMessages(MSG_DISPLAY_SAFE_VOLUME_WARNING)) return; 585 obtainMessage(MSG_DISPLAY_SAFE_VOLUME_WARNING, flags, 0).sendToTarget(); 586 } 587 588 /** 589 * Override this if you have other work to do when the volume changes (for 590 * example, vibrating, playing a sound, etc.). Make sure to call through to 591 * the superclass implementation. 592 */ 593 protected void onVolumeChanged(int streamType, int flags) { 594 595 if (LOGD) Log.d(TAG, "onVolumeChanged(streamType: " + streamType + ", flags: " + flags + ")"); 596 597 if ((flags & AudioManager.FLAG_SHOW_UI) != 0) { 598 synchronized (this) { 599 if (mActiveStreamType != streamType) { 600 reorderSliders(streamType); 601 } 602 onShowVolumeChanged(streamType, flags); 603 } 604 } 605 606 if ((flags & AudioManager.FLAG_PLAY_SOUND) != 0 && ! mRingIsSilent) { 607 removeMessages(MSG_PLAY_SOUND); 608 sendMessageDelayed(obtainMessage(MSG_PLAY_SOUND, streamType, flags), PLAY_SOUND_DELAY); 609 } 610 611 if ((flags & AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE) != 0) { 612 removeMessages(MSG_PLAY_SOUND); 613 removeMessages(MSG_VIBRATE); 614 onStopSounds(); 615 } 616 617 removeMessages(MSG_FREE_RESOURCES); 618 sendMessageDelayed(obtainMessage(MSG_FREE_RESOURCES), FREE_DELAY); 619 resetTimeout(); 620 } 621 622 protected void onMuteChanged(int streamType, int flags) { 623 624 if (LOGD) Log.d(TAG, "onMuteChanged(streamType: " + streamType + ", flags: " + flags + ")"); 625 626 StreamControl sc = mStreamControls.get(streamType); 627 if (sc != null) { 628 sc.icon.setImageResource(isMuted(sc.streamType) ? sc.iconMuteRes : sc.iconRes); 629 } 630 631 onVolumeChanged(streamType, flags); 632 } 633 634 protected void onShowVolumeChanged(int streamType, int flags) { 635 int index = getStreamVolume(streamType); 636 637 mRingIsSilent = false; 638 639 if (LOGD) { 640 Log.d(TAG, "onShowVolumeChanged(streamType: " + streamType 641 + ", flags: " + flags + "), index: " + index); 642 } 643 644 // get max volume for progress bar 645 646 int max = getStreamMaxVolume(streamType); 647 648 switch (streamType) { 649 650 case AudioManager.STREAM_RING: { 651 // setRingerIcon(); 652 Uri ringuri = RingtoneManager.getActualDefaultRingtoneUri( 653 mContext, RingtoneManager.TYPE_RINGTONE); 654 if (ringuri == null) { 655 mRingIsSilent = true; 656 } 657 break; 658 } 659 660 case AudioManager.STREAM_MUSIC: { 661 // Special case for when Bluetooth is active for music 662 if ((mAudioManager.getDevicesForStream(AudioManager.STREAM_MUSIC) & 663 (AudioManager.DEVICE_OUT_BLUETOOTH_A2DP | 664 AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES | 665 AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER)) != 0) { 666 setMusicIcon(R.drawable.ic_audio_bt, R.drawable.ic_audio_bt_mute); 667 } else { 668 setMusicIcon(R.drawable.ic_audio_vol, R.drawable.ic_audio_vol_mute); 669 } 670 break; 671 } 672 673 case AudioManager.STREAM_VOICE_CALL: { 674 /* 675 * For in-call voice call volume, there is no inaudible volume. 676 * Rescale the UI control so the progress bar doesn't go all 677 * the way to zero and don't show the mute icon. 678 */ 679 index++; 680 max++; 681 break; 682 } 683 684 case AudioManager.STREAM_ALARM: { 685 break; 686 } 687 688 case AudioManager.STREAM_NOTIFICATION: { 689 Uri ringuri = RingtoneManager.getActualDefaultRingtoneUri( 690 mContext, RingtoneManager.TYPE_NOTIFICATION); 691 if (ringuri == null) { 692 mRingIsSilent = true; 693 } 694 break; 695 } 696 697 case AudioManager.STREAM_BLUETOOTH_SCO: { 698 /* 699 * For in-call voice call volume, there is no inaudible volume. 700 * Rescale the UI control so the progress bar doesn't go all 701 * the way to zero and don't show the mute icon. 702 */ 703 index++; 704 max++; 705 break; 706 } 707 708 case AudioService.STREAM_REMOTE_MUSIC: { 709 if (LOGD) { Log.d(TAG, "showing remote volume "+index+" over "+ max); } 710 break; 711 } 712 } 713 714 StreamControl sc = mStreamControls.get(streamType); 715 if (sc != null) { 716 if (sc.seekbarView.getMax() != max) { 717 sc.seekbarView.setMax(max); 718 } 719 720 sc.seekbarView.setProgress(index); 721 if (((flags & AudioManager.FLAG_FIXED_VOLUME) != 0) || 722 (streamType != mAudioManager.getMasterStreamType() && 723 streamType != AudioService.STREAM_REMOTE_MUSIC && 724 isMuted(streamType)) || 725 sConfirmSafeVolumeDialog != null) { 726 sc.seekbarView.setEnabled(false); 727 } else { 728 sc.seekbarView.setEnabled(true); 729 } 730 } 731 732 if (!mDialog.isShowing()) { 733 int stream = (streamType == AudioService.STREAM_REMOTE_MUSIC) ? -1 : streamType; 734 // when the stream is for remote playback, use -1 to reset the stream type evaluation 735 mAudioManager.forceVolumeControlStream(stream); 736 mDialog.setContentView(mView); 737 // Showing dialog - use collapsed state 738 if (mShowCombinedVolumes) { 739 collapse(); 740 } 741 mDialog.show(); 742 } 743 744 // Do a little vibrate if applicable (only when going into vibrate mode) 745 if ((streamType != AudioService.STREAM_REMOTE_MUSIC) && 746 ((flags & AudioManager.FLAG_VIBRATE) != 0) && 747 mAudioService.isStreamAffectedByRingerMode(streamType) && 748 mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE) { 749 sendMessageDelayed(obtainMessage(MSG_VIBRATE), VIBRATE_DELAY); 750 } 751 } 752 753 protected void onPlaySound(int streamType, int flags) { 754 755 if (hasMessages(MSG_STOP_SOUNDS)) { 756 removeMessages(MSG_STOP_SOUNDS); 757 // Force stop right now 758 onStopSounds(); 759 } 760 761 synchronized (this) { 762 ToneGenerator toneGen = getOrCreateToneGenerator(streamType); 763 if (toneGen != null) { 764 toneGen.startTone(ToneGenerator.TONE_PROP_BEEP); 765 sendMessageDelayed(obtainMessage(MSG_STOP_SOUNDS), BEEP_DURATION); 766 } 767 } 768 } 769 770 protected void onStopSounds() { 771 772 synchronized (this) { 773 int numStreamTypes = AudioSystem.getNumStreamTypes(); 774 for (int i = numStreamTypes - 1; i >= 0; i--) { 775 ToneGenerator toneGen = mToneGenerators[i]; 776 if (toneGen != null) { 777 toneGen.stopTone(); 778 } 779 } 780 } 781 } 782 783 protected void onVibrate() { 784 785 // Make sure we ended up in vibrate ringer mode 786 if (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_VIBRATE) { 787 return; 788 } 789 790 mVibrator.vibrate(VIBRATE_DURATION); 791 } 792 793 protected void onRemoteVolumeChanged(int streamType, int flags) { 794 // streamType is the real stream type being affected, but for the UI sliders, we 795 // refer to AudioService.STREAM_REMOTE_MUSIC. We still play the beeps on the real 796 // stream type. 797 if (LOGD) Log.d(TAG, "onRemoteVolumeChanged(stream:"+streamType+", flags: " + flags + ")"); 798 799 if (((flags & AudioManager.FLAG_SHOW_UI) != 0) || mDialog.isShowing()) { 800 synchronized (this) { 801 if (mActiveStreamType != AudioService.STREAM_REMOTE_MUSIC) { 802 reorderSliders(AudioService.STREAM_REMOTE_MUSIC); 803 } 804 onShowVolumeChanged(AudioService.STREAM_REMOTE_MUSIC, flags); 805 } 806 } else { 807 if (LOGD) Log.d(TAG, "not calling onShowVolumeChanged(), no FLAG_SHOW_UI or no UI"); 808 } 809 810 if ((flags & AudioManager.FLAG_PLAY_SOUND) != 0 && ! mRingIsSilent) { 811 removeMessages(MSG_PLAY_SOUND); 812 sendMessageDelayed(obtainMessage(MSG_PLAY_SOUND, streamType, flags), PLAY_SOUND_DELAY); 813 } 814 815 if ((flags & AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE) != 0) { 816 removeMessages(MSG_PLAY_SOUND); 817 removeMessages(MSG_VIBRATE); 818 onStopSounds(); 819 } 820 821 removeMessages(MSG_FREE_RESOURCES); 822 sendMessageDelayed(obtainMessage(MSG_FREE_RESOURCES), FREE_DELAY); 823 resetTimeout(); 824 } 825 826 protected void onRemoteVolumeUpdateIfShown() { 827 if (LOGD) Log.d(TAG, "onRemoteVolumeUpdateIfShown()"); 828 if (mDialog.isShowing() 829 && (mActiveStreamType == AudioService.STREAM_REMOTE_MUSIC) 830 && (mStreamControls != null)) { 831 onShowVolumeChanged(AudioService.STREAM_REMOTE_MUSIC, 0); 832 } 833 } 834 835 836 /** 837 * Handler for MSG_SLIDER_VISIBILITY_CHANGED 838 * Hide or show a slider 839 * @param streamType can be a valid stream type value, or VolumePanel.STREAM_MASTER, 840 * or AudioService.STREAM_REMOTE_MUSIC 841 * @param visible 842 */ 843 synchronized protected void onSliderVisibilityChanged(int streamType, int visible) { 844 if (LOGD) Log.d(TAG, "onSliderVisibilityChanged(stream="+streamType+", visi="+visible+")"); 845 boolean isVisible = (visible == 1); 846 for (int i = STREAMS.length - 1 ; i >= 0 ; i--) { 847 StreamResources streamRes = STREAMS[i]; 848 if (streamRes.streamType == streamType) { 849 streamRes.show = isVisible; 850 if (!isVisible && (mActiveStreamType == streamType)) { 851 mActiveStreamType = -1; 852 } 853 break; 854 } 855 } 856 } 857 858 protected void onDisplaySafeVolumeWarning(int flags) { 859 if ((flags & AudioManager.FLAG_SHOW_UI) != 0 || mDialog.isShowing()) { 860 synchronized (sConfirmSafeVolumeLock) { 861 if (sConfirmSafeVolumeDialog != null) { 862 return; 863 } 864 sConfirmSafeVolumeDialog = new AlertDialog.Builder(mContext) 865 .setMessage(com.android.internal.R.string.safe_media_volume_warning) 866 .setPositiveButton(com.android.internal.R.string.yes, 867 new DialogInterface.OnClickListener() { 868 public void onClick(DialogInterface dialog, int which) { 869 mAudioService.disableSafeMediaVolume(); 870 } 871 }) 872 .setNegativeButton(com.android.internal.R.string.no, null) 873 .setIconAttribute(android.R.attr.alertDialogIcon) 874 .create(); 875 final WarningDialogReceiver warning = new WarningDialogReceiver(mContext, 876 sConfirmSafeVolumeDialog, this); 877 878 sConfirmSafeVolumeDialog.setOnDismissListener(warning); 879 sConfirmSafeVolumeDialog.getWindow().setType( 880 WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); 881 sConfirmSafeVolumeDialog.show(); 882 } 883 updateStates(); 884 } 885 resetTimeout(); 886 } 887 888 /** 889 * Lock on this VolumePanel instance as long as you use the returned ToneGenerator. 890 */ 891 private ToneGenerator getOrCreateToneGenerator(int streamType) { 892 if (streamType == STREAM_MASTER) { 893 // For devices that use the master volume setting only but still want to 894 // play a volume-changed tone, direct the master volume pseudostream to 895 // the system stream's tone generator. 896 if (mPlayMasterStreamTones) { 897 streamType = AudioManager.STREAM_SYSTEM; 898 } else { 899 return null; 900 } 901 } 902 synchronized (this) { 903 if (mToneGenerators[streamType] == null) { 904 try { 905 mToneGenerators[streamType] = new ToneGenerator(streamType, MAX_VOLUME); 906 } catch (RuntimeException e) { 907 if (LOGD) { 908 Log.d(TAG, "ToneGenerator constructor failed with " 909 + "RuntimeException: " + e); 910 } 911 } 912 } 913 return mToneGenerators[streamType]; 914 } 915 } 916 917 918 /** 919 * Switch between icons because Bluetooth music is same as music volume, but with 920 * different icons. 921 */ 922 private void setMusicIcon(int resId, int resMuteId) { 923 StreamControl sc = mStreamControls.get(AudioManager.STREAM_MUSIC); 924 if (sc != null) { 925 sc.iconRes = resId; 926 sc.iconMuteRes = resMuteId; 927 sc.icon.setImageResource(isMuted(sc.streamType) ? sc.iconMuteRes : sc.iconRes); 928 } 929 } 930 931 protected void onFreeResources() { 932 synchronized (this) { 933 for (int i = mToneGenerators.length - 1; i >= 0; i--) { 934 if (mToneGenerators[i] != null) { 935 mToneGenerators[i].release(); 936 } 937 mToneGenerators[i] = null; 938 } 939 } 940 } 941 942 @Override 943 public void handleMessage(Message msg) { 944 switch (msg.what) { 945 946 case MSG_VOLUME_CHANGED: { 947 onVolumeChanged(msg.arg1, msg.arg2); 948 break; 949 } 950 951 case MSG_MUTE_CHANGED: { 952 onMuteChanged(msg.arg1, msg.arg2); 953 break; 954 } 955 956 case MSG_FREE_RESOURCES: { 957 onFreeResources(); 958 break; 959 } 960 961 case MSG_STOP_SOUNDS: { 962 onStopSounds(); 963 break; 964 } 965 966 case MSG_PLAY_SOUND: { 967 onPlaySound(msg.arg1, msg.arg2); 968 break; 969 } 970 971 case MSG_VIBRATE: { 972 onVibrate(); 973 break; 974 } 975 976 case MSG_TIMEOUT: { 977 if (mDialog.isShowing()) { 978 mDialog.dismiss(); 979 mActiveStreamType = -1; 980 } 981 synchronized (sConfirmSafeVolumeLock) { 982 if (sConfirmSafeVolumeDialog != null) { 983 sConfirmSafeVolumeDialog.dismiss(); 984 } 985 } 986 break; 987 } 988 case MSG_RINGER_MODE_CHANGED: { 989 if (mDialog.isShowing()) { 990 updateStates(); 991 } 992 break; 993 } 994 995 case MSG_REMOTE_VOLUME_CHANGED: { 996 onRemoteVolumeChanged(msg.arg1, msg.arg2); 997 break; 998 } 999 1000 case MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN: 1001 onRemoteVolumeUpdateIfShown(); 1002 break; 1003 1004 case MSG_SLIDER_VISIBILITY_CHANGED: 1005 onSliderVisibilityChanged(msg.arg1, msg.arg2); 1006 break; 1007 1008 case MSG_DISPLAY_SAFE_VOLUME_WARNING: 1009 onDisplaySafeVolumeWarning(msg.arg1); 1010 break; 1011 } 1012 } 1013 1014 private void resetTimeout() { 1015 removeMessages(MSG_TIMEOUT); 1016 sendMessageDelayed(obtainMessage(MSG_TIMEOUT), TIMEOUT_DELAY); 1017 } 1018 1019 private void forceTimeout() { 1020 removeMessages(MSG_TIMEOUT); 1021 sendMessage(obtainMessage(MSG_TIMEOUT)); 1022 } 1023 1024 public void onProgressChanged(SeekBar seekBar, int progress, 1025 boolean fromUser) { 1026 final Object tag = seekBar.getTag(); 1027 if (fromUser && tag instanceof StreamControl) { 1028 StreamControl sc = (StreamControl) tag; 1029 if (getStreamVolume(sc.streamType) != progress) { 1030 setStreamVolume(sc.streamType, progress, 0); 1031 } 1032 } 1033 resetTimeout(); 1034 } 1035 1036 public void onStartTrackingTouch(SeekBar seekBar) { 1037 } 1038 1039 public void onStopTrackingTouch(SeekBar seekBar) { 1040 final Object tag = seekBar.getTag(); 1041 if (tag instanceof StreamControl) { 1042 StreamControl sc = (StreamControl) tag; 1043 // because remote volume updates are asynchronous, AudioService might have received 1044 // a new remote volume value since the finger adjusted the slider. So when the 1045 // progress of the slider isn't being tracked anymore, adjust the slider to the last 1046 // "published" remote volume value, so the UI reflects the actual volume. 1047 if (sc.streamType == AudioService.STREAM_REMOTE_MUSIC) { 1048 seekBar.setProgress(getStreamVolume(AudioService.STREAM_REMOTE_MUSIC)); 1049 } 1050 } 1051 } 1052 1053 public void onClick(View v) { 1054 if (v == mMoreButton) { 1055 expand(); 1056 } 1057 resetTimeout(); 1058 } 1059 } 1060