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
17package android.view;
18
19import com.android.internal.R;
20
21import android.app.AlertDialog;
22import android.app.Dialog;
23import android.content.DialogInterface.OnDismissListener;
24import android.content.BroadcastReceiver;
25import android.content.Context;
26import android.content.DialogInterface;
27import android.content.Intent;
28import android.content.IntentFilter;
29import android.content.res.Resources;
30import android.media.AudioManager;
31import android.media.AudioService;
32import android.media.AudioSystem;
33import android.media.RingtoneManager;
34import android.media.ToneGenerator;
35import android.media.VolumeController;
36import android.net.Uri;
37import android.os.Handler;
38import android.os.Message;
39import android.os.Vibrator;
40import android.util.Log;
41import android.view.WindowManager.LayoutParams;
42import android.widget.ImageView;
43import android.widget.SeekBar;
44import android.widget.SeekBar.OnSeekBarChangeListener;
45
46import 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 */
59public 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