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.preference;
18
19import android.app.Dialog;
20import android.content.Context;
21import android.content.res.TypedArray;
22import android.database.ContentObserver;
23import android.media.AudioManager;
24import android.media.Ringtone;
25import android.media.RingtoneManager;
26import android.net.Uri;
27import android.os.Handler;
28import android.os.HandlerThread;
29import android.os.Message;
30import android.os.Parcel;
31import android.os.Parcelable;
32import android.provider.Settings;
33import android.provider.Settings.System;
34import android.util.AttributeSet;
35import android.util.Log;
36import android.view.KeyEvent;
37import android.view.View;
38import android.widget.SeekBar;
39import android.widget.SeekBar.OnSeekBarChangeListener;
40
41/**
42 * @hide
43 */
44public class VolumePreference extends SeekBarDialogPreference implements
45        PreferenceManager.OnActivityStopListener, View.OnKeyListener {
46
47    private static final String TAG = "VolumePreference";
48
49    private int mStreamType;
50
51    /** May be null if the dialog isn't visible. */
52    private SeekBarVolumizer mSeekBarVolumizer;
53
54    public VolumePreference(Context context, AttributeSet attrs) {
55        super(context, attrs);
56
57        TypedArray a = context.obtainStyledAttributes(attrs,
58                com.android.internal.R.styleable.VolumePreference, 0, 0);
59        mStreamType = a.getInt(android.R.styleable.VolumePreference_streamType, 0);
60        a.recycle();
61    }
62
63    public void setStreamType(int streamType) {
64        mStreamType = streamType;
65    }
66
67    @Override
68    protected void onBindDialogView(View view) {
69        super.onBindDialogView(view);
70
71        final SeekBar seekBar = (SeekBar) view.findViewById(com.android.internal.R.id.seekbar);
72        mSeekBarVolumizer = new SeekBarVolumizer(getContext(), seekBar, mStreamType);
73
74        getPreferenceManager().registerOnActivityStopListener(this);
75
76        // grab focus and key events so that pressing the volume buttons in the
77        // dialog doesn't also show the normal volume adjust toast.
78        view.setOnKeyListener(this);
79        view.setFocusableInTouchMode(true);
80        view.requestFocus();
81    }
82
83    public boolean onKey(View v, int keyCode, KeyEvent event) {
84        // If key arrives immediately after the activity has been cleaned up.
85        if (mSeekBarVolumizer == null) return true;
86        boolean isdown = (event.getAction() == KeyEvent.ACTION_DOWN);
87        switch (keyCode) {
88            case KeyEvent.KEYCODE_VOLUME_DOWN:
89                if (isdown) {
90                    mSeekBarVolumizer.changeVolumeBy(-1);
91                }
92                return true;
93            case KeyEvent.KEYCODE_VOLUME_UP:
94                if (isdown) {
95                    mSeekBarVolumizer.changeVolumeBy(1);
96                }
97                return true;
98            case KeyEvent.KEYCODE_VOLUME_MUTE:
99                if (isdown) {
100                    mSeekBarVolumizer.muteVolume();
101                }
102                return true;
103            default:
104                return false;
105        }
106    }
107
108    @Override
109    protected void onDialogClosed(boolean positiveResult) {
110        super.onDialogClosed(positiveResult);
111
112        if (!positiveResult && mSeekBarVolumizer != null) {
113            mSeekBarVolumizer.revertVolume();
114        }
115
116        cleanup();
117    }
118
119    public void onActivityStop() {
120        if (mSeekBarVolumizer != null) {
121            mSeekBarVolumizer.postStopSample();
122        }
123    }
124
125    /**
126     * Do clean up.  This can be called multiple times!
127     */
128    private void cleanup() {
129       getPreferenceManager().unregisterOnActivityStopListener(this);
130
131       if (mSeekBarVolumizer != null) {
132           Dialog dialog = getDialog();
133           if (dialog != null && dialog.isShowing()) {
134               View view = dialog.getWindow().getDecorView()
135                       .findViewById(com.android.internal.R.id.seekbar);
136               if (view != null) view.setOnKeyListener(null);
137               // Stopped while dialog was showing, revert changes
138               mSeekBarVolumizer.revertVolume();
139           }
140           mSeekBarVolumizer.stop();
141           mSeekBarVolumizer = null;
142       }
143
144    }
145
146    protected void onSampleStarting(SeekBarVolumizer volumizer) {
147        if (mSeekBarVolumizer != null && volumizer != mSeekBarVolumizer) {
148            mSeekBarVolumizer.stopSample();
149        }
150    }
151
152    @Override
153    protected Parcelable onSaveInstanceState() {
154        final Parcelable superState = super.onSaveInstanceState();
155        if (isPersistent()) {
156            // No need to save instance state since it's persistent
157            return superState;
158        }
159
160        final SavedState myState = new SavedState(superState);
161        if (mSeekBarVolumizer != null) {
162            mSeekBarVolumizer.onSaveInstanceState(myState.getVolumeStore());
163        }
164        return myState;
165    }
166
167    @Override
168    protected void onRestoreInstanceState(Parcelable state) {
169        if (state == null || !state.getClass().equals(SavedState.class)) {
170            // Didn't save state for us in onSaveInstanceState
171            super.onRestoreInstanceState(state);
172            return;
173        }
174
175        SavedState myState = (SavedState) state;
176        super.onRestoreInstanceState(myState.getSuperState());
177        if (mSeekBarVolumizer != null) {
178            mSeekBarVolumizer.onRestoreInstanceState(myState.getVolumeStore());
179        }
180    }
181
182    public static class VolumeStore {
183        public int volume = -1;
184        public int originalVolume = -1;
185    }
186
187    private static class SavedState extends BaseSavedState {
188        VolumeStore mVolumeStore = new VolumeStore();
189
190        public SavedState(Parcel source) {
191            super(source);
192            mVolumeStore.volume = source.readInt();
193            mVolumeStore.originalVolume = source.readInt();
194        }
195
196        @Override
197        public void writeToParcel(Parcel dest, int flags) {
198            super.writeToParcel(dest, flags);
199            dest.writeInt(mVolumeStore.volume);
200            dest.writeInt(mVolumeStore.originalVolume);
201        }
202
203        VolumeStore getVolumeStore() {
204            return mVolumeStore;
205        }
206
207        public SavedState(Parcelable superState) {
208            super(superState);
209        }
210
211        public static final Parcelable.Creator<SavedState> CREATOR =
212                new Parcelable.Creator<SavedState>() {
213            public SavedState createFromParcel(Parcel in) {
214                return new SavedState(in);
215            }
216
217            public SavedState[] newArray(int size) {
218                return new SavedState[size];
219            }
220        };
221    }
222
223    /**
224     * Turns a {@link SeekBar} into a volume control.
225     */
226    public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callback {
227
228        private Context mContext;
229        private Handler mHandler;
230
231        private AudioManager mAudioManager;
232        private int mStreamType;
233        private int mOriginalStreamVolume;
234        private Ringtone mRingtone;
235
236        private int mLastProgress = -1;
237        private SeekBar mSeekBar;
238        private int mVolumeBeforeMute = -1;
239
240        private static final int MSG_SET_STREAM_VOLUME = 0;
241        private static final int MSG_START_SAMPLE = 1;
242        private static final int MSG_STOP_SAMPLE = 2;
243        private static final int CHECK_RINGTONE_PLAYBACK_DELAY_MS = 1000;
244
245        private ContentObserver mVolumeObserver = new ContentObserver(mHandler) {
246            @Override
247            public void onChange(boolean selfChange) {
248                super.onChange(selfChange);
249                if (mSeekBar != null && mAudioManager != null) {
250                    int volume = mAudioManager.getStreamVolume(mStreamType);
251                    mSeekBar.setProgress(volume);
252                }
253            }
254        };
255
256        public SeekBarVolumizer(Context context, SeekBar seekBar, int streamType) {
257            this(context, seekBar, streamType, null);
258        }
259
260        public SeekBarVolumizer(Context context, SeekBar seekBar, int streamType, Uri defaultUri) {
261            mContext = context;
262            mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
263            mStreamType = streamType;
264            mSeekBar = seekBar;
265
266            HandlerThread thread = new HandlerThread(TAG + ".CallbackHandler");
267            thread.start();
268            mHandler = new Handler(thread.getLooper(), this);
269
270            initSeekBar(seekBar, defaultUri);
271        }
272
273        private void initSeekBar(SeekBar seekBar, Uri defaultUri) {
274            seekBar.setMax(mAudioManager.getStreamMaxVolume(mStreamType));
275            mOriginalStreamVolume = mAudioManager.getStreamVolume(mStreamType);
276            seekBar.setProgress(mOriginalStreamVolume);
277            seekBar.setOnSeekBarChangeListener(this);
278
279            mContext.getContentResolver().registerContentObserver(
280                    System.getUriFor(System.VOLUME_SETTINGS[mStreamType]),
281                    false, mVolumeObserver);
282
283            if (defaultUri == null) {
284                if (mStreamType == AudioManager.STREAM_RING) {
285                    defaultUri = Settings.System.DEFAULT_RINGTONE_URI;
286                } else if (mStreamType == AudioManager.STREAM_NOTIFICATION) {
287                    defaultUri = Settings.System.DEFAULT_NOTIFICATION_URI;
288                } else {
289                    defaultUri = Settings.System.DEFAULT_ALARM_ALERT_URI;
290                }
291            }
292
293            mRingtone = RingtoneManager.getRingtone(mContext, defaultUri);
294
295            if (mRingtone != null) {
296                mRingtone.setStreamType(mStreamType);
297            }
298        }
299
300        @Override
301        public boolean handleMessage(Message msg) {
302            switch (msg.what) {
303                case MSG_SET_STREAM_VOLUME:
304                    mAudioManager.setStreamVolume(mStreamType, mLastProgress, 0);
305                    break;
306                case MSG_START_SAMPLE:
307                    onStartSample();
308                    break;
309                case MSG_STOP_SAMPLE:
310                    onStopSample();
311                    break;
312                default:
313                    Log.e(TAG, "invalid SeekBarVolumizer message: "+msg.what);
314            }
315            return true;
316        }
317
318        private void postStartSample() {
319            mHandler.removeMessages(MSG_START_SAMPLE);
320            mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_START_SAMPLE),
321                    isSamplePlaying() ? CHECK_RINGTONE_PLAYBACK_DELAY_MS : 0);
322        }
323
324        private void onStartSample() {
325            if (!isSamplePlaying()) {
326                onSampleStarting(this);
327                if (mRingtone != null) {
328                    mRingtone.play();
329                }
330            }
331        }
332
333        private void postStopSample() {
334            // remove pending delayed start messages
335            mHandler.removeMessages(MSG_START_SAMPLE);
336            mHandler.removeMessages(MSG_STOP_SAMPLE);
337            mHandler.sendMessage(mHandler.obtainMessage(MSG_STOP_SAMPLE));
338        }
339
340        private void onStopSample() {
341            if (mRingtone != null) {
342                mRingtone.stop();
343            }
344        }
345
346        public void stop() {
347            postStopSample();
348            mContext.getContentResolver().unregisterContentObserver(mVolumeObserver);
349            mSeekBar.setOnSeekBarChangeListener(null);
350        }
351
352        public void revertVolume() {
353            mAudioManager.setStreamVolume(mStreamType, mOriginalStreamVolume, 0);
354        }
355
356        public void onProgressChanged(SeekBar seekBar, int progress,
357                boolean fromTouch) {
358            if (!fromTouch) {
359                return;
360            }
361
362            postSetVolume(progress);
363        }
364
365        void postSetVolume(int progress) {
366            // Do the volume changing separately to give responsive UI
367            mLastProgress = progress;
368            mHandler.removeMessages(MSG_SET_STREAM_VOLUME);
369            mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_STREAM_VOLUME));
370        }
371
372        public void onStartTrackingTouch(SeekBar seekBar) {
373        }
374
375        public void onStopTrackingTouch(SeekBar seekBar) {
376            postStartSample();
377        }
378
379        public boolean isSamplePlaying() {
380            return mRingtone != null && mRingtone.isPlaying();
381        }
382
383        public void startSample() {
384            postStartSample();
385        }
386
387        public void stopSample() {
388            postStopSample();
389        }
390
391        public SeekBar getSeekBar() {
392            return mSeekBar;
393        }
394
395        public void changeVolumeBy(int amount) {
396            mSeekBar.incrementProgressBy(amount);
397            postSetVolume(mSeekBar.getProgress());
398            postStartSample();
399            mVolumeBeforeMute = -1;
400        }
401
402        public void muteVolume() {
403            if (mVolumeBeforeMute != -1) {
404                mSeekBar.setProgress(mVolumeBeforeMute);
405                postSetVolume(mVolumeBeforeMute);
406                postStartSample();
407                mVolumeBeforeMute = -1;
408            } else {
409                mVolumeBeforeMute = mSeekBar.getProgress();
410                mSeekBar.setProgress(0);
411                postStopSample();
412                postSetVolume(0);
413            }
414        }
415
416        public void onSaveInstanceState(VolumeStore volumeStore) {
417            if (mLastProgress >= 0) {
418                volumeStore.volume = mLastProgress;
419                volumeStore.originalVolume = mOriginalStreamVolume;
420            }
421        }
422
423        public void onRestoreInstanceState(VolumeStore volumeStore) {
424            if (volumeStore.volume != -1) {
425                mOriginalStreamVolume = volumeStore.originalVolume;
426                mLastProgress = volumeStore.volume;
427                postSetVolume(mLastProgress);
428            }
429        }
430    }
431}
432