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