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