1 /* 2 * Copyright (C) 2014 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.content.BroadcastReceiver; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.IntentFilter; 23 import android.database.ContentObserver; 24 import android.media.AudioManager; 25 import android.media.Ringtone; 26 import android.media.RingtoneManager; 27 import android.net.Uri; 28 import android.os.Handler; 29 import android.os.HandlerThread; 30 import android.os.Message; 31 import android.preference.VolumePreference.VolumeStore; 32 import android.provider.Settings; 33 import android.provider.Settings.System; 34 import android.util.Log; 35 import android.widget.SeekBar; 36 import android.widget.SeekBar.OnSeekBarChangeListener; 37 38 /** 39 * Turns a {@link SeekBar} into a volume control. 40 * @hide 41 */ 42 public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callback { 43 private static final String TAG = "SeekBarVolumizer"; 44 45 public interface Callback { 46 void onSampleStarting(SeekBarVolumizer sbv); 47 void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch); 48 void onMuted(boolean muted); 49 } 50 51 private final Context mContext; 52 private final H mUiHandler = new H(); 53 private final Callback mCallback; 54 private final Uri mDefaultUri; 55 private final AudioManager mAudioManager; 56 private final int mStreamType; 57 private final int mMaxStreamVolume; 58 private boolean mAffectedByRingerMode; 59 private boolean mNotificationOrRing; 60 private final Receiver mReceiver = new Receiver(); 61 62 private Handler mHandler; 63 private Observer mVolumeObserver; 64 private int mOriginalStreamVolume; 65 private Ringtone mRingtone; 66 private int mLastProgress = -1; 67 private boolean mMuted; 68 private SeekBar mSeekBar; 69 private int mVolumeBeforeMute = -1; 70 private int mRingerMode; 71 72 private static final int MSG_SET_STREAM_VOLUME = 0; 73 private static final int MSG_START_SAMPLE = 1; 74 private static final int MSG_STOP_SAMPLE = 2; 75 private static final int MSG_INIT_SAMPLE = 3; 76 private static final int CHECK_RINGTONE_PLAYBACK_DELAY_MS = 1000; 77 78 public SeekBarVolumizer(Context context, int streamType, Uri defaultUri, Callback callback) { 79 mContext = context; 80 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 81 mStreamType = streamType; 82 mAffectedByRingerMode = mAudioManager.isStreamAffectedByRingerMode(mStreamType); 83 mNotificationOrRing = isNotificationOrRing(mStreamType); 84 if (mNotificationOrRing) { 85 mRingerMode = mAudioManager.getRingerModeInternal(); 86 } 87 mMaxStreamVolume = mAudioManager.getStreamMaxVolume(mStreamType); 88 mCallback = callback; 89 mOriginalStreamVolume = mAudioManager.getStreamVolume(mStreamType); 90 mMuted = mAudioManager.isStreamMute(mStreamType); 91 if (mCallback != null) { 92 mCallback.onMuted(mMuted); 93 } 94 if (defaultUri == null) { 95 if (mStreamType == AudioManager.STREAM_RING) { 96 defaultUri = Settings.System.DEFAULT_RINGTONE_URI; 97 } else if (mStreamType == AudioManager.STREAM_NOTIFICATION) { 98 defaultUri = Settings.System.DEFAULT_NOTIFICATION_URI; 99 } else { 100 defaultUri = Settings.System.DEFAULT_ALARM_ALERT_URI; 101 } 102 } 103 mDefaultUri = defaultUri; 104 } 105 106 private static boolean isNotificationOrRing(int stream) { 107 return stream == AudioManager.STREAM_RING || stream == AudioManager.STREAM_NOTIFICATION; 108 } 109 110 public void setSeekBar(SeekBar seekBar) { 111 if (mSeekBar != null) { 112 mSeekBar.setOnSeekBarChangeListener(null); 113 } 114 mSeekBar = seekBar; 115 mSeekBar.setOnSeekBarChangeListener(null); 116 mSeekBar.setMax(mMaxStreamVolume); 117 updateSeekBar(); 118 mSeekBar.setOnSeekBarChangeListener(this); 119 } 120 121 protected void updateSeekBar() { 122 if (mNotificationOrRing && mRingerMode == AudioManager.RINGER_MODE_VIBRATE) { 123 mSeekBar.setEnabled(true); 124 mSeekBar.setProgress(0); 125 } else if (mMuted) { 126 mSeekBar.setEnabled(false); 127 mSeekBar.setProgress(0); 128 } else { 129 mSeekBar.setEnabled(true); 130 mSeekBar.setProgress(mLastProgress > -1 ? mLastProgress : mOriginalStreamVolume); 131 } 132 } 133 134 @Override 135 public boolean handleMessage(Message msg) { 136 switch (msg.what) { 137 case MSG_SET_STREAM_VOLUME: 138 mAudioManager.setStreamVolume(mStreamType, mLastProgress, 139 AudioManager.FLAG_SHOW_UI_WARNINGS); 140 break; 141 case MSG_START_SAMPLE: 142 onStartSample(); 143 break; 144 case MSG_STOP_SAMPLE: 145 onStopSample(); 146 break; 147 case MSG_INIT_SAMPLE: 148 onInitSample(); 149 break; 150 default: 151 Log.e(TAG, "invalid SeekBarVolumizer message: "+msg.what); 152 } 153 return true; 154 } 155 156 private void onInitSample() { 157 mRingtone = RingtoneManager.getRingtone(mContext, mDefaultUri); 158 if (mRingtone != null) { 159 mRingtone.setStreamType(mStreamType); 160 } 161 } 162 163 private void postStartSample() { 164 if (mHandler == null) return; 165 mHandler.removeMessages(MSG_START_SAMPLE); 166 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_START_SAMPLE), 167 isSamplePlaying() ? CHECK_RINGTONE_PLAYBACK_DELAY_MS : 0); 168 } 169 170 private void onStartSample() { 171 if (!isSamplePlaying()) { 172 if (mCallback != null) { 173 mCallback.onSampleStarting(this); 174 } 175 if (mRingtone != null) { 176 try { 177 mRingtone.play(); 178 } catch (Throwable e) { 179 Log.w(TAG, "Error playing ringtone, stream " + mStreamType, e); 180 } 181 } 182 } 183 } 184 185 private void postStopSample() { 186 if (mHandler == null) return; 187 // remove pending delayed start messages 188 mHandler.removeMessages(MSG_START_SAMPLE); 189 mHandler.removeMessages(MSG_STOP_SAMPLE); 190 mHandler.sendMessage(mHandler.obtainMessage(MSG_STOP_SAMPLE)); 191 } 192 193 private void onStopSample() { 194 if (mRingtone != null) { 195 mRingtone.stop(); 196 } 197 } 198 199 public void stop() { 200 if (mHandler == null) return; // already stopped 201 postStopSample(); 202 mContext.getContentResolver().unregisterContentObserver(mVolumeObserver); 203 mReceiver.setListening(false); 204 mSeekBar.setOnSeekBarChangeListener(null); 205 mHandler.getLooper().quitSafely(); 206 mHandler = null; 207 mVolumeObserver = null; 208 } 209 210 public void start() { 211 if (mHandler != null) return; // already started 212 HandlerThread thread = new HandlerThread(TAG + ".CallbackHandler"); 213 thread.start(); 214 mHandler = new Handler(thread.getLooper(), this); 215 mHandler.sendEmptyMessage(MSG_INIT_SAMPLE); 216 mVolumeObserver = new Observer(mHandler); 217 mContext.getContentResolver().registerContentObserver( 218 System.getUriFor(System.VOLUME_SETTINGS[mStreamType]), 219 false, mVolumeObserver); 220 mReceiver.setListening(true); 221 } 222 223 public void revertVolume() { 224 mAudioManager.setStreamVolume(mStreamType, mOriginalStreamVolume, 0); 225 } 226 227 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) { 228 if (fromTouch) { 229 postSetVolume(progress); 230 } 231 if (mCallback != null) { 232 mCallback.onProgressChanged(seekBar, progress, fromTouch); 233 } 234 } 235 236 private void postSetVolume(int progress) { 237 if (mHandler == null) return; 238 // Do the volume changing separately to give responsive UI 239 mLastProgress = progress; 240 mHandler.removeMessages(MSG_SET_STREAM_VOLUME); 241 mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_STREAM_VOLUME)); 242 } 243 244 public void onStartTrackingTouch(SeekBar seekBar) { 245 } 246 247 public void onStopTrackingTouch(SeekBar seekBar) { 248 postStartSample(); 249 } 250 251 public boolean isSamplePlaying() { 252 return mRingtone != null && mRingtone.isPlaying(); 253 } 254 255 public void startSample() { 256 postStartSample(); 257 } 258 259 public void stopSample() { 260 postStopSample(); 261 } 262 263 public SeekBar getSeekBar() { 264 return mSeekBar; 265 } 266 267 public void changeVolumeBy(int amount) { 268 mSeekBar.incrementProgressBy(amount); 269 postSetVolume(mSeekBar.getProgress()); 270 postStartSample(); 271 mVolumeBeforeMute = -1; 272 } 273 274 public void muteVolume() { 275 if (mVolumeBeforeMute != -1) { 276 mSeekBar.setProgress(mVolumeBeforeMute); 277 postSetVolume(mVolumeBeforeMute); 278 postStartSample(); 279 mVolumeBeforeMute = -1; 280 } else { 281 mVolumeBeforeMute = mSeekBar.getProgress(); 282 mSeekBar.setProgress(0); 283 postStopSample(); 284 postSetVolume(0); 285 } 286 } 287 288 public void onSaveInstanceState(VolumeStore volumeStore) { 289 if (mLastProgress >= 0) { 290 volumeStore.volume = mLastProgress; 291 volumeStore.originalVolume = mOriginalStreamVolume; 292 } 293 } 294 295 public void onRestoreInstanceState(VolumeStore volumeStore) { 296 if (volumeStore.volume != -1) { 297 mOriginalStreamVolume = volumeStore.originalVolume; 298 mLastProgress = volumeStore.volume; 299 postSetVolume(mLastProgress); 300 } 301 } 302 303 private final class H extends Handler { 304 private static final int UPDATE_SLIDER = 1; 305 306 @Override 307 public void handleMessage(Message msg) { 308 if (msg.what == UPDATE_SLIDER) { 309 if (mSeekBar != null) { 310 mLastProgress = msg.arg1; 311 final boolean muted = msg.arg2 != 0; 312 if (muted != mMuted) { 313 mMuted = muted; 314 if (mCallback != null) { 315 mCallback.onMuted(mMuted); 316 } 317 } 318 updateSeekBar(); 319 } 320 } 321 } 322 323 public void postUpdateSlider(int volume, boolean mute) { 324 obtainMessage(UPDATE_SLIDER, volume, mute ? 1 : 0).sendToTarget(); 325 } 326 } 327 328 private void updateSlider() { 329 if (mSeekBar != null && mAudioManager != null) { 330 final int volume = mAudioManager.getStreamVolume(mStreamType); 331 final boolean mute = mAudioManager.isStreamMute(mStreamType); 332 mUiHandler.postUpdateSlider(volume, mute); 333 } 334 } 335 336 private final class Observer extends ContentObserver { 337 public Observer(Handler handler) { 338 super(handler); 339 } 340 341 @Override 342 public void onChange(boolean selfChange) { 343 super.onChange(selfChange); 344 updateSlider(); 345 } 346 } 347 348 private final class Receiver extends BroadcastReceiver { 349 private boolean mListening; 350 351 public void setListening(boolean listening) { 352 if (mListening == listening) return; 353 mListening = listening; 354 if (listening) { 355 final IntentFilter filter = new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION); 356 filter.addAction(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION); 357 mContext.registerReceiver(this, filter); 358 } else { 359 mContext.unregisterReceiver(this); 360 } 361 } 362 363 @Override 364 public void onReceive(Context context, Intent intent) { 365 final String action = intent.getAction(); 366 if (AudioManager.VOLUME_CHANGED_ACTION.equals(action)) { 367 int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1); 368 int streamValue = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, -1); 369 final boolean streamMatch = mNotificationOrRing ? isNotificationOrRing(streamType) 370 : (streamType == mStreamType); 371 if (mSeekBar != null && streamMatch && streamValue != -1) { 372 final boolean muted = mAudioManager.isStreamMute(mStreamType); 373 mUiHandler.postUpdateSlider(streamValue, muted); 374 } 375 } else if (AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION.equals(action)) { 376 if (mNotificationOrRing) { 377 mRingerMode = mAudioManager.getRingerModeInternal(); 378 } 379 if (mAffectedByRingerMode) { 380 updateSlider(); 381 } 382 } 383 } 384 } 385 } 386