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