VolumeZen: SystemUI now hosts the volume dialog.
- Allow SystemUI to set the volume controller interface using
a new binder call to audio service.
- Remove VolumePanel's dependency on AudioService.
- Host the base VolumePanel in the SystemUI process.
Change-Id: I095d5a1a579d42b68d0f81abb4087bd0c754b876
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index 217074f..d7ce255 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -47,6 +47,7 @@
com.android.systemui.power.PowerUI.class,
com.android.systemui.media.RingtonePlayer.class,
com.android.systemui.settings.SettingsUI.class,
+ com.android.systemui.volume.VolumeUI.class,
};
/**
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java b/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java
new file mode 100644
index 0000000..8657e07
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java
@@ -0,0 +1,1070 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume;
+
+import com.android.internal.R;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface.OnDismissListener;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.media.AudioManager;
+import android.media.AudioService;
+import android.media.AudioSystem;
+import android.media.RingtoneManager;
+import android.media.ToneGenerator;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Vibrator;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+import android.widget.ImageView;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+
+import java.util.HashMap;
+
+/**
+ * Handles the user interface for the volume keys.
+ *
+ * @hide
+ */
+public class VolumePanel extends Handler {
+ private static final String TAG = VolumePanel.class.getSimpleName();
+ private static boolean LOGD = false;
+
+ private static final int PLAY_SOUND_DELAY = AudioService.PLAY_SOUND_DELAY;
+
+ /**
+ * The delay before vibrating. This small period exists so if the user is
+ * moving to silent mode, it will not emit a short vibrate (it normally
+ * would since vibrate is between normal mode and silent mode using hardware
+ * keys).
+ */
+ public static final int VIBRATE_DELAY = 300;
+
+ private static final int VIBRATE_DURATION = 300;
+ private static final int BEEP_DURATION = 150;
+ private static final int MAX_VOLUME = 100;
+ private static final int FREE_DELAY = 10000;
+ private static final int TIMEOUT_DELAY = 3000;
+
+ private static final int MSG_VOLUME_CHANGED = 0;
+ private static final int MSG_FREE_RESOURCES = 1;
+ private static final int MSG_PLAY_SOUND = 2;
+ private static final int MSG_STOP_SOUNDS = 3;
+ private static final int MSG_VIBRATE = 4;
+ private static final int MSG_TIMEOUT = 5;
+ private static final int MSG_RINGER_MODE_CHANGED = 6;
+ private static final int MSG_MUTE_CHANGED = 7;
+ private static final int MSG_REMOTE_VOLUME_CHANGED = 8;
+ private static final int MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN = 9;
+ private static final int MSG_SLIDER_VISIBILITY_CHANGED = 10;
+ private static final int MSG_DISPLAY_SAFE_VOLUME_WARNING = 11;
+
+ // Pseudo stream type for master volume
+ private static final int STREAM_MASTER = -100;
+ // Pseudo stream type for remote volume is defined in AudioService.STREAM_REMOTE_MUSIC
+
+ protected final Context mContext;
+ private final AudioManager mAudioManager;
+ private boolean mRingIsSilent;
+ private boolean mShowCombinedVolumes;
+ private boolean mVoiceCapable;
+
+ // True if we want to play tones on the system stream when the master stream is specified.
+ private final boolean mPlayMasterStreamTones;
+
+ /** Dialog containing all the sliders */
+ private final Dialog mDialog;
+ /** Dialog's content view */
+ private final View mView;
+
+ /** The visible portion of the volume overlay */
+ private final ViewGroup mPanel;
+ /** Contains the sliders and their touchable icons */
+ private final ViewGroup mSliderGroup;
+ /** The button that expands the dialog to show all sliders */
+ private final View mMoreButton;
+ /** Dummy divider icon that needs to vanish with the more button */
+ private final View mDivider;
+
+ /** Currently active stream that shows up at the top of the list of sliders */
+ private int mActiveStreamType = -1;
+ /** All the slider controls mapped by stream type */
+ private HashMap<Integer,StreamControl> mStreamControls;
+
+ private enum StreamResources {
+ BluetoothSCOStream(AudioManager.STREAM_BLUETOOTH_SCO,
+ R.string.volume_icon_description_bluetooth,
+ R.drawable.ic_audio_bt,
+ R.drawable.ic_audio_bt,
+ false),
+ RingerStream(AudioManager.STREAM_RING,
+ R.string.volume_icon_description_ringer,
+ R.drawable.ic_audio_ring_notif,
+ R.drawable.ic_audio_ring_notif_mute,
+ false),
+ VoiceStream(AudioManager.STREAM_VOICE_CALL,
+ R.string.volume_icon_description_incall,
+ R.drawable.ic_audio_phone,
+ R.drawable.ic_audio_phone,
+ false),
+ AlarmStream(AudioManager.STREAM_ALARM,
+ R.string.volume_alarm,
+ R.drawable.ic_audio_alarm,
+ R.drawable.ic_audio_alarm_mute,
+ false),
+ MediaStream(AudioManager.STREAM_MUSIC,
+ R.string.volume_icon_description_media,
+ R.drawable.ic_audio_vol,
+ R.drawable.ic_audio_vol_mute,
+ true),
+ NotificationStream(AudioManager.STREAM_NOTIFICATION,
+ R.string.volume_icon_description_notification,
+ R.drawable.ic_audio_notification,
+ R.drawable.ic_audio_notification_mute,
+ true),
+ // for now, use media resources for master volume
+ MasterStream(STREAM_MASTER,
+ R.string.volume_icon_description_media, //FIXME should have its own description
+ R.drawable.ic_audio_vol,
+ R.drawable.ic_audio_vol_mute,
+ false),
+ RemoteStream(AudioService.STREAM_REMOTE_MUSIC,
+ R.string.volume_icon_description_media, //FIXME should have its own description
+ R.drawable.ic_media_route_on_holo_dark,
+ R.drawable.ic_media_route_disabled_holo_dark,
+ false);// will be dynamically updated
+
+ int streamType;
+ int descRes;
+ int iconRes;
+ int iconMuteRes;
+ // RING, VOICE_CALL & BLUETOOTH_SCO are hidden unless explicitly requested
+ boolean show;
+
+ StreamResources(int streamType, int descRes, int iconRes, int iconMuteRes, boolean show) {
+ this.streamType = streamType;
+ this.descRes = descRes;
+ this.iconRes = iconRes;
+ this.iconMuteRes = iconMuteRes;
+ this.show = show;
+ }
+ }
+
+ // List of stream types and their order
+ private static final StreamResources[] STREAMS = {
+ StreamResources.BluetoothSCOStream,
+ StreamResources.RingerStream,
+ StreamResources.VoiceStream,
+ StreamResources.MediaStream,
+ StreamResources.NotificationStream,
+ StreamResources.AlarmStream,
+ StreamResources.MasterStream,
+ StreamResources.RemoteStream
+ };
+
+ /** Object that contains data for each slider */
+ private class StreamControl {
+ int streamType;
+ ViewGroup group;
+ ImageView icon;
+ SeekBar seekbarView;
+ int iconRes;
+ int iconMuteRes;
+ }
+
+ // Synchronize when accessing this
+ private ToneGenerator mToneGenerators[];
+ private Vibrator mVibrator;
+
+ private static AlertDialog sConfirmSafeVolumeDialog;
+ private static Object sConfirmSafeVolumeLock = new Object();
+
+ private static class WarningDialogReceiver extends BroadcastReceiver
+ implements DialogInterface.OnDismissListener {
+ private final Context mContext;
+ private final Dialog mDialog;
+ private final VolumePanel mVolumePanel;
+
+ WarningDialogReceiver(Context context, Dialog dialog, VolumePanel volumePanel) {
+ mContext = context;
+ mDialog = dialog;
+ mVolumePanel = volumePanel;
+ IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+ context.registerReceiver(this, filter);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mDialog.cancel();
+ cleanUp();
+ }
+
+ @Override
+ public void onDismiss(DialogInterface unused) {
+ mContext.unregisterReceiver(this);
+ cleanUp();
+ }
+
+ private void cleanUp() {
+ synchronized (sConfirmSafeVolumeLock) {
+ sConfirmSafeVolumeDialog = null;
+ }
+ mVolumePanel.forceTimeout();
+ mVolumePanel.updateStates();
+ }
+ }
+
+
+ public VolumePanel(Context context) {
+ mContext = context;
+ mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+
+ // For now, only show master volume if master volume is supported
+ final Resources res = context.getResources();
+ final boolean useMasterVolume = res.getBoolean(R.bool.config_useMasterVolume);
+ if (useMasterVolume) {
+ for (int i = 0; i < STREAMS.length; i++) {
+ StreamResources streamRes = STREAMS[i];
+ streamRes.show = (streamRes.streamType == STREAM_MASTER);
+ }
+ }
+
+ mDialog = new Dialog(context) {
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (isShowing() && event.getAction() == MotionEvent.ACTION_OUTSIDE &&
+ sConfirmSafeVolumeDialog == null) {
+ forceTimeout();
+ return true;
+ }
+ return false;
+ }
+ };
+
+ // Change some window properties
+ final Window window = mDialog.getWindow();
+ final LayoutParams lp = window.getAttributes();
+ lp.token = null;
+ // Offset from the top
+ lp.y = res.getDimensionPixelOffset(R.dimen.volume_panel_top);
+ lp.type = LayoutParams.TYPE_VOLUME_OVERLAY;
+ lp.windowAnimations = R.style.Animation_VolumePanel;
+ window.setAttributes(lp);
+ window.setGravity(Gravity.TOP);
+ window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+ window.requestFeature(Window.FEATURE_NO_TITLE);
+ window.addFlags(LayoutParams.FLAG_NOT_FOCUSABLE
+ | LayoutParams.FLAG_NOT_TOUCH_MODAL
+ | LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH);
+
+ mDialog.setCanceledOnTouchOutside(true);
+ mDialog.setContentView(R.layout.volume_adjust);
+ mDialog.setOnDismissListener(new OnDismissListener() {
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ mActiveStreamType = -1;
+ mAudioManager.forceVolumeControlStream(mActiveStreamType);
+ }
+ });
+
+ mDialog.create();
+
+ mView = window.findViewById(R.id.content);
+ mView.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ resetTimeout();
+ return false;
+ }
+ });
+
+ mPanel = (ViewGroup) mView.findViewById(R.id.visible_panel);
+ mSliderGroup = (ViewGroup) mView.findViewById(R.id.slider_group);
+ mMoreButton = mView.findViewById(R.id.expand_button);
+ mDivider = mView.findViewById(R.id.expand_button_divider);
+
+ mToneGenerators = new ToneGenerator[AudioSystem.getNumStreamTypes()];
+ mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
+ mVoiceCapable = context.getResources().getBoolean(R.bool.config_voice_capable);
+
+ // If we don't want to show multiple volumes, hide the settings button
+ // and divider.
+ mShowCombinedVolumes = !mVoiceCapable && !useMasterVolume;
+ if (!mShowCombinedVolumes) {
+ mMoreButton.setVisibility(View.GONE);
+ mDivider.setVisibility(View.GONE);
+ } else {
+ mMoreButton.setOnClickListener(mClickListener);
+ }
+
+ final boolean masterVolumeOnly = res.getBoolean(R.bool.config_useMasterVolume);
+ final boolean masterVolumeKeySounds = res.getBoolean(R.bool.config_useVolumeKeySounds);
+ mPlayMasterStreamTones = masterVolumeOnly && masterVolumeKeySounds;
+
+ listenToRingerMode();
+ }
+
+ public void setLayoutDirection(int layoutDirection) {
+ mPanel.setLayoutDirection(layoutDirection);
+ updateStates();
+ }
+
+ private void listenToRingerMode() {
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
+ mContext.registerReceiver(new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+
+ if (AudioManager.RINGER_MODE_CHANGED_ACTION.equals(action)) {
+ removeMessages(MSG_RINGER_MODE_CHANGED);
+ sendMessage(obtainMessage(MSG_RINGER_MODE_CHANGED));
+ }
+ }
+ }, filter);
+ }
+
+ private boolean isMuted(int streamType) {
+ if (streamType == STREAM_MASTER) {
+ return mAudioManager.isMasterMute();
+ } else if (streamType == AudioService.STREAM_REMOTE_MUSIC) {
+ return (mAudioManager.getRemoteStreamVolume() <= 0);
+ } else {
+ return mAudioManager.isStreamMute(streamType);
+ }
+ }
+
+ private int getStreamMaxVolume(int streamType) {
+ if (streamType == STREAM_MASTER) {
+ return mAudioManager.getMasterMaxVolume();
+ } else if (streamType == AudioService.STREAM_REMOTE_MUSIC) {
+ return mAudioManager.getRemoteStreamMaxVolume();
+ } else {
+ return mAudioManager.getStreamMaxVolume(streamType);
+ }
+ }
+
+ private int getStreamVolume(int streamType) {
+ if (streamType == STREAM_MASTER) {
+ return mAudioManager.getMasterVolume();
+ } else if (streamType == AudioService.STREAM_REMOTE_MUSIC) {
+ return mAudioManager.getRemoteStreamVolume();
+ } else {
+ return mAudioManager.getStreamVolume(streamType);
+ }
+ }
+
+ private void setStreamVolume(int streamType, int index, int flags) {
+ if (streamType == STREAM_MASTER) {
+ mAudioManager.setMasterVolume(index, flags);
+ } else if (streamType == AudioService.STREAM_REMOTE_MUSIC) {
+ mAudioManager.setRemoteStreamVolume(index);
+ } else {
+ mAudioManager.setStreamVolume(streamType, index, flags);
+ }
+ }
+
+ private void createSliders() {
+ final Resources res = mContext.getResources();
+ final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+
+ mStreamControls = new HashMap<Integer, StreamControl>(STREAMS.length);
+
+ for (int i = 0; i < STREAMS.length; i++) {
+ StreamResources streamRes = STREAMS[i];
+
+ final int streamType = streamRes.streamType;
+ if (mVoiceCapable && streamRes == StreamResources.NotificationStream) {
+ streamRes = StreamResources.RingerStream;
+ }
+
+ final StreamControl sc = new StreamControl();
+ sc.streamType = streamType;
+ sc.group = (ViewGroup) inflater.inflate(R.layout.volume_adjust_item, null);
+ sc.group.setTag(sc);
+ sc.icon = (ImageView) sc.group.findViewById(R.id.stream_icon);
+ sc.icon.setTag(sc);
+ sc.icon.setContentDescription(res.getString(streamRes.descRes));
+ sc.iconRes = streamRes.iconRes;
+ sc.iconMuteRes = streamRes.iconMuteRes;
+ sc.icon.setImageResource(sc.iconRes);
+ sc.seekbarView = (SeekBar) sc.group.findViewById(R.id.seekbar);
+ final int plusOne = (streamType == AudioSystem.STREAM_BLUETOOTH_SCO ||
+ streamType == AudioSystem.STREAM_VOICE_CALL) ? 1 : 0;
+ sc.seekbarView.setMax(getStreamMaxVolume(streamType) + plusOne);
+ sc.seekbarView.setOnSeekBarChangeListener(mSeekListener);
+ sc.seekbarView.setTag(sc);
+ mStreamControls.put(streamType, sc);
+ }
+ }
+
+ private void reorderSliders(int activeStreamType) {
+ mSliderGroup.removeAllViews();
+
+ final StreamControl active = mStreamControls.get(activeStreamType);
+ if (active == null) {
+ Log.e("VolumePanel", "Missing stream type! - " + activeStreamType);
+ mActiveStreamType = -1;
+ } else {
+ mSliderGroup.addView(active.group);
+ mActiveStreamType = activeStreamType;
+ active.group.setVisibility(View.VISIBLE);
+ updateSlider(active);
+ }
+
+ addOtherVolumes();
+ }
+
+ private void addOtherVolumes() {
+ if (!mShowCombinedVolumes) return;
+
+ for (int i = 0; i < STREAMS.length; i++) {
+ // Skip the phone specific ones and the active one
+ final int streamType = STREAMS[i].streamType;
+ if (!STREAMS[i].show || streamType == mActiveStreamType) {
+ continue;
+ }
+ StreamControl sc = mStreamControls.get(streamType);
+ mSliderGroup.addView(sc.group);
+ updateSlider(sc);
+ }
+ }
+
+ /** Update the mute and progress state of a slider */
+ private void updateSlider(StreamControl sc) {
+ sc.seekbarView.setProgress(getStreamVolume(sc.streamType));
+ final boolean muted = isMuted(sc.streamType);
+ // Force reloading the image resource
+ sc.icon.setImageDrawable(null);
+ sc.icon.setImageResource(muted ? sc.iconMuteRes : sc.iconRes);
+ if (((sc.streamType == AudioManager.STREAM_RING) ||
+ (sc.streamType == AudioManager.STREAM_NOTIFICATION)) &&
+ mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE) {
+ sc.icon.setImageResource(R.drawable.ic_audio_ring_notif_vibrate);
+ }
+ if (sc.streamType == AudioService.STREAM_REMOTE_MUSIC) {
+ // never disable touch interactions for remote playback, the muting is not tied to
+ // the state of the phone.
+ sc.seekbarView.setEnabled(true);
+ } else if ((sc.streamType != mAudioManager.getMasterStreamType() && muted) ||
+ (sConfirmSafeVolumeDialog != null)) {
+ sc.seekbarView.setEnabled(false);
+ } else {
+ sc.seekbarView.setEnabled(true);
+ }
+ }
+
+ private void expand() {
+ final int count = mSliderGroup.getChildCount();
+ for (int i = 0; i < count; i++) {
+ mSliderGroup.getChildAt(i).setVisibility(View.VISIBLE);
+ }
+ mMoreButton.setVisibility(View.INVISIBLE);
+ mDivider.setVisibility(View.INVISIBLE);
+ }
+
+ private void collapse() {
+ mMoreButton.setVisibility(View.VISIBLE);
+ mDivider.setVisibility(View.VISIBLE);
+ final int count = mSliderGroup.getChildCount();
+ for (int i = 1; i < count; i++) {
+ mSliderGroup.getChildAt(i).setVisibility(View.GONE);
+ }
+ }
+
+ public void updateStates() {
+ final int count = mSliderGroup.getChildCount();
+ for (int i = 0; i < count; i++) {
+ StreamControl sc = (StreamControl) mSliderGroup.getChildAt(i).getTag();
+ updateSlider(sc);
+ }
+ }
+
+ public void postVolumeChanged(int streamType, int flags) {
+ if (hasMessages(MSG_VOLUME_CHANGED)) return;
+ synchronized (this) {
+ if (mStreamControls == null) {
+ createSliders();
+ }
+ }
+ removeMessages(MSG_FREE_RESOURCES);
+ obtainMessage(MSG_VOLUME_CHANGED, streamType, flags).sendToTarget();
+ }
+
+ public void postRemoteVolumeChanged(int streamType, int flags) {
+ if (hasMessages(MSG_REMOTE_VOLUME_CHANGED)) return;
+ synchronized (this) {
+ if (mStreamControls == null) {
+ createSliders();
+ }
+ }
+ removeMessages(MSG_FREE_RESOURCES);
+ obtainMessage(MSG_REMOTE_VOLUME_CHANGED, streamType, flags).sendToTarget();
+ }
+
+ public void postRemoteSliderVisibility(boolean visible) {
+ obtainMessage(MSG_SLIDER_VISIBILITY_CHANGED,
+ AudioService.STREAM_REMOTE_MUSIC, visible ? 1 : 0).sendToTarget();
+ }
+
+ /**
+ * Called by AudioService when it has received new remote playback information that
+ * would affect the VolumePanel display (mainly volumes). The difference with
+ * {@link #postRemoteVolumeChanged(int, int)} is that the handling of the posted message
+ * (MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN) will only update the volume slider if it is being
+ * displayed.
+ * This special code path is due to the fact that remote volume updates arrive to AudioService
+ * asynchronously. So after AudioService has sent the volume update (which should be treated
+ * as a request to update the volume), the application will likely set a new volume. If the UI
+ * is still up, we need to refresh the display to show this new value.
+ */
+ public void postHasNewRemotePlaybackInfo() {
+ if (hasMessages(MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN)) return;
+ // don't create or prevent resources to be freed, if they disappear, this update came too
+ // late and shouldn't warrant the panel to be displayed longer
+ obtainMessage(MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN).sendToTarget();
+ }
+
+ public void postMasterVolumeChanged(int flags) {
+ postVolumeChanged(STREAM_MASTER, flags);
+ }
+
+ public void postMuteChanged(int streamType, int flags) {
+ if (hasMessages(MSG_VOLUME_CHANGED)) return;
+ synchronized (this) {
+ if (mStreamControls == null) {
+ createSliders();
+ }
+ }
+ removeMessages(MSG_FREE_RESOURCES);
+ obtainMessage(MSG_MUTE_CHANGED, streamType, flags).sendToTarget();
+ }
+
+ public void postMasterMuteChanged(int flags) {
+ postMuteChanged(STREAM_MASTER, flags);
+ }
+
+ public void postDisplaySafeVolumeWarning(int flags) {
+ if (hasMessages(MSG_DISPLAY_SAFE_VOLUME_WARNING)) return;
+ obtainMessage(MSG_DISPLAY_SAFE_VOLUME_WARNING, flags, 0).sendToTarget();
+ }
+
+ public void postDismiss() {
+ removeMessages(MSG_TIMEOUT);
+ sendEmptyMessage(MSG_TIMEOUT);
+ }
+
+ /**
+ * Override this if you have other work to do when the volume changes (for
+ * example, vibrating, playing a sound, etc.). Make sure to call through to
+ * the superclass implementation.
+ */
+ protected void onVolumeChanged(int streamType, int flags) {
+
+ if (LOGD) Log.d(TAG, "onVolumeChanged(streamType: " + streamType + ", flags: " + flags + ")");
+
+ if ((flags & AudioManager.FLAG_SHOW_UI) != 0) {
+ synchronized (this) {
+ if (mActiveStreamType != streamType) {
+ reorderSliders(streamType);
+ }
+ onShowVolumeChanged(streamType, flags);
+ }
+ }
+
+ if ((flags & AudioManager.FLAG_PLAY_SOUND) != 0 && ! mRingIsSilent) {
+ removeMessages(MSG_PLAY_SOUND);
+ sendMessageDelayed(obtainMessage(MSG_PLAY_SOUND, streamType, flags), PLAY_SOUND_DELAY);
+ }
+
+ if ((flags & AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE) != 0) {
+ removeMessages(MSG_PLAY_SOUND);
+ removeMessages(MSG_VIBRATE);
+ onStopSounds();
+ }
+
+ removeMessages(MSG_FREE_RESOURCES);
+ sendMessageDelayed(obtainMessage(MSG_FREE_RESOURCES), FREE_DELAY);
+ resetTimeout();
+ }
+
+ protected void onMuteChanged(int streamType, int flags) {
+
+ if (LOGD) Log.d(TAG, "onMuteChanged(streamType: " + streamType + ", flags: " + flags + ")");
+
+ StreamControl sc = mStreamControls.get(streamType);
+ if (sc != null) {
+ sc.icon.setImageResource(isMuted(sc.streamType) ? sc.iconMuteRes : sc.iconRes);
+ }
+
+ onVolumeChanged(streamType, flags);
+ }
+
+ protected void onShowVolumeChanged(int streamType, int flags) {
+ int index = getStreamVolume(streamType);
+
+ mRingIsSilent = false;
+
+ if (LOGD) {
+ Log.d(TAG, "onShowVolumeChanged(streamType: " + streamType
+ + ", flags: " + flags + "), index: " + index);
+ }
+
+ // get max volume for progress bar
+
+ int max = getStreamMaxVolume(streamType);
+
+ switch (streamType) {
+
+ case AudioManager.STREAM_RING: {
+// setRingerIcon();
+ Uri ringuri = RingtoneManager.getActualDefaultRingtoneUri(
+ mContext, RingtoneManager.TYPE_RINGTONE);
+ if (ringuri == null) {
+ mRingIsSilent = true;
+ }
+ break;
+ }
+
+ case AudioManager.STREAM_MUSIC: {
+ // Special case for when Bluetooth is active for music
+ if ((mAudioManager.getDevicesForStream(AudioManager.STREAM_MUSIC) &
+ (AudioManager.DEVICE_OUT_BLUETOOTH_A2DP |
+ AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES |
+ AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER)) != 0) {
+ setMusicIcon(R.drawable.ic_audio_bt, R.drawable.ic_audio_bt_mute);
+ } else {
+ setMusicIcon(R.drawable.ic_audio_vol, R.drawable.ic_audio_vol_mute);
+ }
+ break;
+ }
+
+ case AudioManager.STREAM_VOICE_CALL: {
+ /*
+ * For in-call voice call volume, there is no inaudible volume.
+ * Rescale the UI control so the progress bar doesn't go all
+ * the way to zero and don't show the mute icon.
+ */
+ index++;
+ max++;
+ break;
+ }
+
+ case AudioManager.STREAM_ALARM: {
+ break;
+ }
+
+ case AudioManager.STREAM_NOTIFICATION: {
+ Uri ringuri = RingtoneManager.getActualDefaultRingtoneUri(
+ mContext, RingtoneManager.TYPE_NOTIFICATION);
+ if (ringuri == null) {
+ mRingIsSilent = true;
+ }
+ break;
+ }
+
+ case AudioManager.STREAM_BLUETOOTH_SCO: {
+ /*
+ * For in-call voice call volume, there is no inaudible volume.
+ * Rescale the UI control so the progress bar doesn't go all
+ * the way to zero and don't show the mute icon.
+ */
+ index++;
+ max++;
+ break;
+ }
+
+ case AudioService.STREAM_REMOTE_MUSIC: {
+ if (LOGD) { Log.d(TAG, "showing remote volume "+index+" over "+ max); }
+ break;
+ }
+ }
+
+ StreamControl sc = mStreamControls.get(streamType);
+ if (sc != null) {
+ if (sc.seekbarView.getMax() != max) {
+ sc.seekbarView.setMax(max);
+ }
+
+ sc.seekbarView.setProgress(index);
+ if (((flags & AudioManager.FLAG_FIXED_VOLUME) != 0) ||
+ (streamType != mAudioManager.getMasterStreamType() &&
+ streamType != AudioService.STREAM_REMOTE_MUSIC &&
+ isMuted(streamType)) ||
+ sConfirmSafeVolumeDialog != null) {
+ sc.seekbarView.setEnabled(false);
+ } else {
+ sc.seekbarView.setEnabled(true);
+ }
+ }
+
+ if (!mDialog.isShowing()) {
+ int stream = (streamType == AudioService.STREAM_REMOTE_MUSIC) ? -1 : streamType;
+ // when the stream is for remote playback, use -1 to reset the stream type evaluation
+ mAudioManager.forceVolumeControlStream(stream);
+
+ // Showing dialog - use collapsed state
+ if (mShowCombinedVolumes) {
+ collapse();
+ }
+ mDialog.show();
+ }
+
+ // Do a little vibrate if applicable (only when going into vibrate mode)
+ if ((streamType != AudioService.STREAM_REMOTE_MUSIC) &&
+ ((flags & AudioManager.FLAG_VIBRATE) != 0) &&
+ mAudioManager.isStreamAffectedByRingerMode(streamType) &&
+ mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE) {
+ sendMessageDelayed(obtainMessage(MSG_VIBRATE), VIBRATE_DELAY);
+ }
+ }
+
+ protected void onPlaySound(int streamType, int flags) {
+
+ if (hasMessages(MSG_STOP_SOUNDS)) {
+ removeMessages(MSG_STOP_SOUNDS);
+ // Force stop right now
+ onStopSounds();
+ }
+
+ synchronized (this) {
+ ToneGenerator toneGen = getOrCreateToneGenerator(streamType);
+ if (toneGen != null) {
+ toneGen.startTone(ToneGenerator.TONE_PROP_BEEP);
+ sendMessageDelayed(obtainMessage(MSG_STOP_SOUNDS), BEEP_DURATION);
+ }
+ }
+ }
+
+ protected void onStopSounds() {
+
+ synchronized (this) {
+ int numStreamTypes = AudioSystem.getNumStreamTypes();
+ for (int i = numStreamTypes - 1; i >= 0; i--) {
+ ToneGenerator toneGen = mToneGenerators[i];
+ if (toneGen != null) {
+ toneGen.stopTone();
+ }
+ }
+ }
+ }
+
+ protected void onVibrate() {
+
+ // Make sure we ended up in vibrate ringer mode
+ if (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_VIBRATE) {
+ return;
+ }
+
+ mVibrator.vibrate(VIBRATE_DURATION, AudioManager.STREAM_SYSTEM);
+ }
+
+ protected void onRemoteVolumeChanged(int streamType, int flags) {
+ // streamType is the real stream type being affected, but for the UI sliders, we
+ // refer to AudioService.STREAM_REMOTE_MUSIC. We still play the beeps on the real
+ // stream type.
+ if (LOGD) Log.d(TAG, "onRemoteVolumeChanged(stream:"+streamType+", flags: " + flags + ")");
+
+ if (((flags & AudioManager.FLAG_SHOW_UI) != 0) || mDialog.isShowing()) {
+ synchronized (this) {
+ if (mActiveStreamType != AudioService.STREAM_REMOTE_MUSIC) {
+ reorderSliders(AudioService.STREAM_REMOTE_MUSIC);
+ }
+ onShowVolumeChanged(AudioService.STREAM_REMOTE_MUSIC, flags);
+ }
+ } else {
+ if (LOGD) Log.d(TAG, "not calling onShowVolumeChanged(), no FLAG_SHOW_UI or no UI");
+ }
+
+ if ((flags & AudioManager.FLAG_PLAY_SOUND) != 0 && ! mRingIsSilent) {
+ removeMessages(MSG_PLAY_SOUND);
+ sendMessageDelayed(obtainMessage(MSG_PLAY_SOUND, streamType, flags), PLAY_SOUND_DELAY);
+ }
+
+ if ((flags & AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE) != 0) {
+ removeMessages(MSG_PLAY_SOUND);
+ removeMessages(MSG_VIBRATE);
+ onStopSounds();
+ }
+
+ removeMessages(MSG_FREE_RESOURCES);
+ sendMessageDelayed(obtainMessage(MSG_FREE_RESOURCES), FREE_DELAY);
+ resetTimeout();
+ }
+
+ protected void onRemoteVolumeUpdateIfShown() {
+ if (LOGD) Log.d(TAG, "onRemoteVolumeUpdateIfShown()");
+ if (mDialog.isShowing()
+ && (mActiveStreamType == AudioService.STREAM_REMOTE_MUSIC)
+ && (mStreamControls != null)) {
+ onShowVolumeChanged(AudioService.STREAM_REMOTE_MUSIC, 0);
+ }
+ }
+
+
+ /**
+ * Handler for MSG_SLIDER_VISIBILITY_CHANGED
+ * Hide or show a slider
+ * @param streamType can be a valid stream type value, or VolumePanel.STREAM_MASTER,
+ * or AudioService.STREAM_REMOTE_MUSIC
+ * @param visible
+ */
+ synchronized protected void onSliderVisibilityChanged(int streamType, int visible) {
+ if (LOGD) Log.d(TAG, "onSliderVisibilityChanged(stream="+streamType+", visi="+visible+")");
+ boolean isVisible = (visible == 1);
+ for (int i = STREAMS.length - 1 ; i >= 0 ; i--) {
+ StreamResources streamRes = STREAMS[i];
+ if (streamRes.streamType == streamType) {
+ streamRes.show = isVisible;
+ if (!isVisible && (mActiveStreamType == streamType)) {
+ mActiveStreamType = -1;
+ }
+ break;
+ }
+ }
+ }
+
+ protected void onDisplaySafeVolumeWarning(int flags) {
+ if ((flags & AudioManager.FLAG_SHOW_UI) != 0 || mDialog.isShowing()) {
+ synchronized (sConfirmSafeVolumeLock) {
+ if (sConfirmSafeVolumeDialog != null) {
+ return;
+ }
+ sConfirmSafeVolumeDialog = new AlertDialog.Builder(mContext)
+ .setMessage(com.android.internal.R.string.safe_media_volume_warning)
+ .setPositiveButton(com.android.internal.R.string.yes,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mAudioManager.disableSafeMediaVolume();
+ }
+ })
+ .setNegativeButton(com.android.internal.R.string.no, null)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .create();
+ final WarningDialogReceiver warning = new WarningDialogReceiver(mContext,
+ sConfirmSafeVolumeDialog, this);
+
+ sConfirmSafeVolumeDialog.setOnDismissListener(warning);
+ sConfirmSafeVolumeDialog.getWindow().setType(
+ WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
+ sConfirmSafeVolumeDialog.show();
+ }
+ updateStates();
+ }
+ resetTimeout();
+ }
+
+ /**
+ * Lock on this VolumePanel instance as long as you use the returned ToneGenerator.
+ */
+ private ToneGenerator getOrCreateToneGenerator(int streamType) {
+ if (streamType == STREAM_MASTER) {
+ // For devices that use the master volume setting only but still want to
+ // play a volume-changed tone, direct the master volume pseudostream to
+ // the system stream's tone generator.
+ if (mPlayMasterStreamTones) {
+ streamType = AudioManager.STREAM_SYSTEM;
+ } else {
+ return null;
+ }
+ }
+ synchronized (this) {
+ if (mToneGenerators[streamType] == null) {
+ try {
+ mToneGenerators[streamType] = new ToneGenerator(streamType, MAX_VOLUME);
+ } catch (RuntimeException e) {
+ if (LOGD) {
+ Log.d(TAG, "ToneGenerator constructor failed with "
+ + "RuntimeException: " + e);
+ }
+ }
+ }
+ return mToneGenerators[streamType];
+ }
+ }
+
+
+ /**
+ * Switch between icons because Bluetooth music is same as music volume, but with
+ * different icons.
+ */
+ private void setMusicIcon(int resId, int resMuteId) {
+ StreamControl sc = mStreamControls.get(AudioManager.STREAM_MUSIC);
+ if (sc != null) {
+ sc.iconRes = resId;
+ sc.iconMuteRes = resMuteId;
+ sc.icon.setImageResource(isMuted(sc.streamType) ? sc.iconMuteRes : sc.iconRes);
+ }
+ }
+
+ protected void onFreeResources() {
+ synchronized (this) {
+ for (int i = mToneGenerators.length - 1; i >= 0; i--) {
+ if (mToneGenerators[i] != null) {
+ mToneGenerators[i].release();
+ }
+ mToneGenerators[i] = null;
+ }
+ }
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+
+ case MSG_VOLUME_CHANGED: {
+ onVolumeChanged(msg.arg1, msg.arg2);
+ break;
+ }
+
+ case MSG_MUTE_CHANGED: {
+ onMuteChanged(msg.arg1, msg.arg2);
+ break;
+ }
+
+ case MSG_FREE_RESOURCES: {
+ onFreeResources();
+ break;
+ }
+
+ case MSG_STOP_SOUNDS: {
+ onStopSounds();
+ break;
+ }
+
+ case MSG_PLAY_SOUND: {
+ onPlaySound(msg.arg1, msg.arg2);
+ break;
+ }
+
+ case MSG_VIBRATE: {
+ onVibrate();
+ break;
+ }
+
+ case MSG_TIMEOUT: {
+ if (mDialog.isShowing()) {
+ mDialog.dismiss();
+ mActiveStreamType = -1;
+ }
+ synchronized (sConfirmSafeVolumeLock) {
+ if (sConfirmSafeVolumeDialog != null) {
+ sConfirmSafeVolumeDialog.dismiss();
+ }
+ }
+ break;
+ }
+ case MSG_RINGER_MODE_CHANGED: {
+ if (mDialog.isShowing()) {
+ updateStates();
+ }
+ break;
+ }
+
+ case MSG_REMOTE_VOLUME_CHANGED: {
+ onRemoteVolumeChanged(msg.arg1, msg.arg2);
+ break;
+ }
+
+ case MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN:
+ onRemoteVolumeUpdateIfShown();
+ break;
+
+ case MSG_SLIDER_VISIBILITY_CHANGED:
+ onSliderVisibilityChanged(msg.arg1, msg.arg2);
+ break;
+
+ case MSG_DISPLAY_SAFE_VOLUME_WARNING:
+ onDisplaySafeVolumeWarning(msg.arg1);
+ break;
+ }
+ }
+
+ private void resetTimeout() {
+ removeMessages(MSG_TIMEOUT);
+ sendMessageDelayed(obtainMessage(MSG_TIMEOUT), TIMEOUT_DELAY);
+ }
+
+ private void forceTimeout() {
+ removeMessages(MSG_TIMEOUT);
+ sendMessage(obtainMessage(MSG_TIMEOUT));
+ }
+
+ private final OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ final Object tag = seekBar.getTag();
+ if (fromUser && tag instanceof StreamControl) {
+ StreamControl sc = (StreamControl) tag;
+ if (getStreamVolume(sc.streamType) != progress) {
+ setStreamVolume(sc.streamType, progress, 0);
+ }
+ }
+ resetTimeout();
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ final Object tag = seekBar.getTag();
+ if (tag instanceof StreamControl) {
+ StreamControl sc = (StreamControl) tag;
+ // Because remote volume updates are asynchronous, AudioService
+ // might have received a new remote volume value since the
+ // finger adjusted the slider. So when the progress of the
+ // slider isn't being tracked anymore, adjust the slider to the
+ // last "published" remote volume value, so the UI reflects the
+ // actual volume.
+ if (sc.streamType == AudioService.STREAM_REMOTE_MUSIC) {
+ seekBar.setProgress(getStreamVolume(AudioService.STREAM_REMOTE_MUSIC));
+ }
+ }
+ }
+ };
+
+ private final View.OnClickListener mClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (v == mMoreButton) {
+ expand();
+ }
+ resetTimeout();
+ }
+ };
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
new file mode 100644
index 0000000..9bd75b7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
@@ -0,0 +1,125 @@
+package com.android.systemui.volume;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.media.AudioManager;
+import android.media.IVolumeController;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.systemui.SystemUI;
+
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+public class VolumeUI extends SystemUI {
+ private static final String TAG = "VolumeUI";
+ private static final String SETTING = "systemui_volume_controller"; // for testing
+ private static final Uri SETTING_URI = Settings.Global.getUriFor(SETTING);
+ private static final int DEFAULT = 1; // enabled by default
+
+ private AudioManager mAudioManager;
+ private VolumeController mVolumeController;
+
+ @Override
+ public void start() {
+ mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ updateController();
+ mContext.getContentResolver().registerContentObserver(SETTING_URI, false, mObserver);
+ }
+
+ private void updateController() {
+ if (Settings.Global.getInt(mContext.getContentResolver(), SETTING, DEFAULT) != 0) {
+ if (mVolumeController == null) {
+ mVolumeController = new VolumeController(mContext);
+ }
+ Log.d(TAG, "Registering volume controller");
+ mAudioManager.setVolumeController(mVolumeController);
+ } else {
+ Log.d(TAG, "Unregistering volume controller");
+ mAudioManager.setVolumeController(null);
+ }
+ }
+
+ private final ContentObserver mObserver = new ContentObserver(new Handler()) {
+ public void onChange(boolean selfChange, Uri uri) {
+ if (SETTING_URI.equals(uri)) {
+ updateController();
+ }
+ }
+ };
+
+ /** For now, simply host an unmodified base volume panel in this process. */
+ private final class VolumeController extends IVolumeController.Stub {
+ private final VolumePanel mPanel;
+
+ public VolumeController(Context context) {
+ mPanel = new VolumePanel(context);
+ }
+
+ @Override
+ public void hasNewRemotePlaybackInfo() throws RemoteException {
+ mPanel.postHasNewRemotePlaybackInfo();
+ }
+
+ @Override
+ public void remoteVolumeChanged(int streamType, int flags)
+ throws RemoteException {
+ mPanel.postRemoteVolumeChanged(streamType, flags);
+ }
+
+ @Override
+ public void remoteSliderVisibility(boolean visible)
+ throws RemoteException {
+ mPanel.postRemoteSliderVisibility(visible);
+ }
+
+ @Override
+ public void displaySafeVolumeWarning(int flags) throws RemoteException {
+ mPanel.postDisplaySafeVolumeWarning(flags);
+ }
+
+ @Override
+ public void volumeChanged(int streamType, int flags)
+ throws RemoteException {
+ mPanel.postVolumeChanged(streamType, flags);
+ }
+
+ @Override
+ public void masterVolumeChanged(int flags) throws RemoteException {
+ mPanel.postMasterVolumeChanged(flags);
+ }
+
+ @Override
+ public void masterMuteChanged(int flags) throws RemoteException {
+ mPanel.postMasterMuteChanged(flags);
+ }
+
+ @Override
+ public void setLayoutDirection(int layoutDirection)
+ throws RemoteException {
+ mPanel.setLayoutDirection(layoutDirection);
+ }
+
+ @Override
+ public void dismiss() throws RemoteException {
+ mPanel.postDismiss();
+ }
+ }
+}