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 { onSampleStarting(SeekBarVolumizer sbv)51 void onSampleStarting(SeekBarVolumizer sbv); onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch)52 void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch); onMuted(boolean muted, boolean zenMuted)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 SeekBarVolumizer(Context context, int streamType, Uri defaultUri, Callback callback)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 isNotificationOrRing(int stream)121 private static boolean isNotificationOrRing(int stream) { 122 return stream == AudioManager.STREAM_RING || stream == AudioManager.STREAM_NOTIFICATION; 123 } 124 setSeekBar(SeekBar seekBar)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 isZenMuted()136 private boolean isZenMuted() { 137 return mNotificationOrRing && mZenMode == Global.ZEN_MODE_ALARMS 138 || mZenMode == Global.ZEN_MODE_NO_INTERRUPTIONS; 139 } 140 updateSeekBar()141 protected void updateSeekBar() { 142 final boolean zenMuted = isZenMuted(); 143 mSeekBar.setEnabled(!zenMuted); 144 if (zenMuted) { 145 mSeekBar.setProgress(mLastAudibleStreamVolume); 146 } else if (mNotificationOrRing && mRingerMode == AudioManager.RINGER_MODE_VIBRATE) { 147 mSeekBar.setProgress(0); 148 } else if (mMuted) { 149 mSeekBar.setProgress(0); 150 } else { 151 mSeekBar.setProgress(mLastProgress > -1 ? mLastProgress : mOriginalStreamVolume); 152 } 153 } 154 155 @Override handleMessage(Message msg)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 onInitSample()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 postStartSample()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 onStartSample()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_INTERRUPTION_POLICY | 210 AudioAttributes.FLAG_BYPASS_MUTE) 211 .build()); 212 mRingtone.play(); 213 } catch (Throwable e) { 214 Log.w(TAG, "Error playing ringtone, stream " + mStreamType, e); 215 } 216 } 217 } 218 } 219 } 220 postStopSample()221 private void postStopSample() { 222 if (mHandler == null) return; 223 // remove pending delayed start messages 224 mHandler.removeMessages(MSG_START_SAMPLE); 225 mHandler.removeMessages(MSG_STOP_SAMPLE); 226 mHandler.sendMessage(mHandler.obtainMessage(MSG_STOP_SAMPLE)); 227 } 228 onStopSample()229 private void onStopSample() { 230 synchronized (this) { 231 if (mRingtone != null) { 232 mRingtone.stop(); 233 } 234 } 235 } 236 stop()237 public void stop() { 238 if (mHandler == null) return; // already stopped 239 postStopSample(); 240 mContext.getContentResolver().unregisterContentObserver(mVolumeObserver); 241 mReceiver.setListening(false); 242 mSeekBar.setOnSeekBarChangeListener(null); 243 mHandler.getLooper().quitSafely(); 244 mHandler = null; 245 mVolumeObserver = null; 246 } 247 start()248 public void start() { 249 if (mHandler != null) return; // already started 250 HandlerThread thread = new HandlerThread(TAG + ".CallbackHandler"); 251 thread.start(); 252 mHandler = new Handler(thread.getLooper(), this); 253 mHandler.sendEmptyMessage(MSG_INIT_SAMPLE); 254 mVolumeObserver = new Observer(mHandler); 255 mContext.getContentResolver().registerContentObserver( 256 System.getUriFor(System.VOLUME_SETTINGS[mStreamType]), 257 false, mVolumeObserver); 258 mReceiver.setListening(true); 259 } 260 revertVolume()261 public void revertVolume() { 262 mAudioManager.setStreamVolume(mStreamType, mOriginalStreamVolume, 0); 263 } 264 onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch)265 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) { 266 if (fromTouch) { 267 postSetVolume(progress); 268 } 269 if (mCallback != null) { 270 mCallback.onProgressChanged(seekBar, progress, fromTouch); 271 } 272 } 273 postSetVolume(int progress)274 private void postSetVolume(int progress) { 275 if (mHandler == null) return; 276 // Do the volume changing separately to give responsive UI 277 mLastProgress = progress; 278 mHandler.removeMessages(MSG_SET_STREAM_VOLUME); 279 mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_STREAM_VOLUME)); 280 } 281 onStartTrackingTouch(SeekBar seekBar)282 public void onStartTrackingTouch(SeekBar seekBar) { 283 } 284 onStopTrackingTouch(SeekBar seekBar)285 public void onStopTrackingTouch(SeekBar seekBar) { 286 postStartSample(); 287 } 288 isSamplePlaying()289 public boolean isSamplePlaying() { 290 synchronized (this) { 291 return mRingtone != null && mRingtone.isPlaying(); 292 } 293 } 294 startSample()295 public void startSample() { 296 postStartSample(); 297 } 298 stopSample()299 public void stopSample() { 300 postStopSample(); 301 } 302 getSeekBar()303 public SeekBar getSeekBar() { 304 return mSeekBar; 305 } 306 changeVolumeBy(int amount)307 public void changeVolumeBy(int amount) { 308 mSeekBar.incrementProgressBy(amount); 309 postSetVolume(mSeekBar.getProgress()); 310 postStartSample(); 311 mVolumeBeforeMute = -1; 312 } 313 muteVolume()314 public void muteVolume() { 315 if (mVolumeBeforeMute != -1) { 316 mSeekBar.setProgress(mVolumeBeforeMute); 317 postSetVolume(mVolumeBeforeMute); 318 postStartSample(); 319 mVolumeBeforeMute = -1; 320 } else { 321 mVolumeBeforeMute = mSeekBar.getProgress(); 322 mSeekBar.setProgress(0); 323 postStopSample(); 324 postSetVolume(0); 325 } 326 } 327 onSaveInstanceState(VolumeStore volumeStore)328 public void onSaveInstanceState(VolumeStore volumeStore) { 329 if (mLastProgress >= 0) { 330 volumeStore.volume = mLastProgress; 331 volumeStore.originalVolume = mOriginalStreamVolume; 332 } 333 } 334 onRestoreInstanceState(VolumeStore volumeStore)335 public void onRestoreInstanceState(VolumeStore volumeStore) { 336 if (volumeStore.volume != -1) { 337 mOriginalStreamVolume = volumeStore.originalVolume; 338 mLastProgress = volumeStore.volume; 339 postSetVolume(mLastProgress); 340 } 341 } 342 343 private final class H extends Handler { 344 private static final int UPDATE_SLIDER = 1; 345 346 @Override handleMessage(Message msg)347 public void handleMessage(Message msg) { 348 if (msg.what == UPDATE_SLIDER) { 349 if (mSeekBar != null) { 350 mLastProgress = msg.arg1; 351 mLastAudibleStreamVolume = Math.abs(msg.arg2); 352 final boolean muted = msg.arg2 < 0; 353 if (muted != mMuted) { 354 mMuted = muted; 355 if (mCallback != null) { 356 mCallback.onMuted(mMuted, isZenMuted()); 357 } 358 } 359 updateSeekBar(); 360 } 361 } 362 } 363 364 public void postUpdateSlider(int volume, int lastAudibleVolume, boolean mute) { 365 final int arg2 = lastAudibleVolume * (mute ? -1 : 1); 366 obtainMessage(UPDATE_SLIDER, volume, arg2).sendToTarget(); 367 } 368 } 369 370 private void updateSlider() { 371 if (mSeekBar != null && mAudioManager != null) { 372 final int volume = mAudioManager.getStreamVolume(mStreamType); 373 final int lastAudibleVolume = mAudioManager.getLastAudibleStreamVolume(mStreamType); 374 final boolean mute = mAudioManager.isStreamMute(mStreamType); 375 mUiHandler.postUpdateSlider(volume, lastAudibleVolume, mute); 376 } 377 } 378 379 private final class Observer extends ContentObserver { 380 public Observer(Handler handler) { 381 super(handler); 382 } 383 384 @Override 385 public void onChange(boolean selfChange) { 386 super.onChange(selfChange); 387 updateSlider(); 388 } 389 } 390 391 private final class Receiver extends BroadcastReceiver { 392 private boolean mListening; 393 394 public void setListening(boolean listening) { 395 if (mListening == listening) return; 396 mListening = listening; 397 if (listening) { 398 final IntentFilter filter = new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION); 399 filter.addAction(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION); 400 filter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED); 401 filter.addAction(AudioManager.STREAM_DEVICES_CHANGED_ACTION); 402 mContext.registerReceiver(this, filter); 403 } else { 404 mContext.unregisterReceiver(this); 405 } 406 } 407 408 @Override 409 public void onReceive(Context context, Intent intent) { 410 final String action = intent.getAction(); 411 if (AudioManager.VOLUME_CHANGED_ACTION.equals(action)) { 412 int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1); 413 int streamValue = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, -1); 414 updateVolumeSlider(streamType, streamValue); 415 } else if (AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION.equals(action)) { 416 if (mNotificationOrRing) { 417 mRingerMode = mAudioManager.getRingerModeInternal(); 418 } 419 if (mAffectedByRingerMode) { 420 updateSlider(); 421 } 422 } else if (AudioManager.STREAM_DEVICES_CHANGED_ACTION.equals(action)) { 423 int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1); 424 int streamVolume = mAudioManager.getStreamVolume(streamType); 425 updateVolumeSlider(streamType, streamVolume); 426 } else if (NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED.equals(action)) { 427 mZenMode = mNotificationManager.getZenMode(); 428 updateSlider(); 429 } 430 } 431 432 private void updateVolumeSlider(int streamType, int streamValue) { 433 final boolean streamMatch = mNotificationOrRing ? isNotificationOrRing(streamType) 434 : (streamType == mStreamType); 435 if (mSeekBar != null && streamMatch && streamValue != -1) { 436 final boolean muted = mAudioManager.isStreamMute(mStreamType) 437 || streamValue == 0; 438 mUiHandler.postUpdateSlider(streamValue, mLastAudibleStreamVolume, muted); 439 } 440 } 441 } 442 } 443