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