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 17 package android.preference; 18 19 import android.app.Dialog; 20 import android.content.Context; 21 import android.content.res.TypedArray; 22 import android.database.ContentObserver; 23 import android.media.AudioManager; 24 import android.media.Ringtone; 25 import android.media.RingtoneManager; 26 import android.net.Uri; 27 import android.os.Handler; 28 import android.os.HandlerThread; 29 import android.os.Message; 30 import android.os.Parcel; 31 import android.os.Parcelable; 32 import android.provider.Settings; 33 import android.provider.Settings.System; 34 import android.util.AttributeSet; 35 import android.util.Log; 36 import android.view.KeyEvent; 37 import android.view.View; 38 import android.widget.SeekBar; 39 import android.widget.SeekBar.OnSeekBarChangeListener; 40 41 /** 42 * @hide 43 */ 44 public 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