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