package me.barrasso.android.volume.utils; import android.media.AudioManager; import android.util.Pair; import android.util.SparseArray; import static me.barrasso.android.volume.LogUtils.LOGD; import static me.barrasso.android.volume.LogUtils.LOGI; import me.barrasso.android.volume.LogUtils; import me.barrasso.android.volume.popup.VolumePanel; /** * Simple container to keep track of stream volumes. Also contains helpful * methods for synchronizing volumes across all streams. */ public final class VolumeManager { public static final String TAG = LogUtils.makeLogTag(VolumeManager.class); public static SparseArray<String> STREAM_NAMES() { final SparseArray<String> STREAM_NAMES = new SparseArray<String>(); STREAM_NAMES.put(AudioManager.STREAM_VOICE_CALL, "STREAM_VOICE_CALL"); STREAM_NAMES.put(AudioManager.STREAM_SYSTEM, "STREAM_SYSTEM"); STREAM_NAMES.put(AudioManager.STREAM_NOTIFICATION, "STREAM_NOTIFICATION"); STREAM_NAMES.put(AudioManager.STREAM_RING, "STREAM_RING"); STREAM_NAMES.put(AudioManager.STREAM_DTMF, "STREAM_DTMF"); STREAM_NAMES.put(AudioManager.STREAM_MUSIC, "STREAM_MUSIC"); STREAM_NAMES.put(AudioManager.STREAM_ALARM, "STREAM_ALARM"); STREAM_NAMES.put(VolumePanel.STREAM_MASTER, "STREAM_MASTER"); STREAM_NAMES.put(VolumePanel.STREAM_BLUETOOTH_SCO, "STREAM_BLUETOOTH_SCO"); STREAM_NAMES.put(VolumePanel.STREAM_REMOTE_MUSIC, "STREAM_REMOTE_MUSIC"); STREAM_NAMES.put(AudioManager.USE_DEFAULT_STREAM_TYPE, "USE_DEFAULT_STREAM_TYPE"); return STREAM_NAMES; } public static String getStreamName(final int streamType) { return STREAM_NAMES().get(streamType); } final static int[] STREAMS = new int[] { AudioManager.STREAM_VOICE_CALL, AudioManager.STREAM_SYSTEM, AudioManager.STREAM_NOTIFICATION, AudioManager.STREAM_RING, AudioManager.STREAM_DTMF, AudioManager.STREAM_MUSIC, AudioManager.STREAM_ALARM }; // Map of stream types to a pair (first=volume, second=max) private final SparseArray<Pair<Integer, Integer>> STREAM_VOLUMES = new SparseArray<Pair<Integer, Integer>>(); private final SparseArray<Integer> STREAM_HIGHEST_VOLUMES = new SparseArray<Integer>(); private final AudioManager audioManager; private final int mSmallestMax; private final int mLargestMax; private Pair<Integer, Integer> mManagedVolume; public VolumeManager(AudioManager audioManager) { this.audioManager = audioManager; int[] extremes = syncStreams(); LOGI(TAG, "Volume streams [" + extremes[0] + ", " + extremes[1] + "]"); mSmallestMax = extremes[0]; mLargestMax = extremes[1]; } /** * Synchronize all streams based on AudioManager. * @return The smallest maximum volume. */ protected int[] syncStreams() { int[] extremes = new int[] { Integer.MAX_VALUE, Integer.MIN_VALUE }; // smallest, greatest for (final int stream : STREAMS) { Pair<Integer, Integer> volume = new Pair<Integer, Integer>( audioManager.getStreamVolume(stream), audioManager.getStreamMaxVolume(stream)); if (volume.second < extremes[0]) extremes[0] = volume.second; if (volume.second > extremes[1]) extremes[1] = volume.second; // Put ot set based on whether we already have an object. if (null != STREAM_VOLUMES.get(stream)) { STREAM_VOLUMES.setValueAt(stream, volume); } else { STREAM_VOLUMES.put(stream, volume); } Integer highest = STREAM_HIGHEST_VOLUMES.get(stream); if (null != highest && highest < volume.first) { STREAM_HIGHEST_VOLUMES.setValueAt(stream, volume.first); } else { STREAM_HIGHEST_VOLUMES.put(stream, volume.first); } } return extremes; } /** @return The smallest volume maximum of all streams. */ public int getSmallestMax() { return mSmallestMax; } /** @return The largest volume maximum of all streams. */ public int getLargestMax() { return mLargestMax; } /** Set the volume of all public AudioManager streams. */ public void adjustVolumeSync(final int direction) { // Find the stream with the smallest max volume. LOGD(TAG, "adjustVolumeSync(" + direction + ")"); Pair<Integer, Integer> lowestStream = null; for (final int stream : STREAMS) { Pair<Integer, Integer> streamPair = STREAM_VOLUMES.get(stream); if (null != streamPair) { if (null == lowestStream || streamPair.second < lowestStream.second) lowestStream = streamPair; if (streamPair.second == lowestStream.second && streamPair.first < lowestStream.first) lowestStream = streamPair; } } // Raise or lower all streams based on this one. if (null == lowestStream) return; LOGD(TAG, "Lowest stream (" + lowestStream.first + '/' + lowestStream.second + ')'); // There's an odd exception where volumes can be set higher than their maximum. To avoid // this strange issue we use the smallest of the volume and and maximum. int newVolume = Math.min(lowestStream.first, lowestStream.second); switch (direction) { case AudioManager.ADJUST_RAISE: // Lower the volume but bound to max. newVolume = Math.min(++newVolume, lowestStream.second); break; case AudioManager.ADJUST_LOWER: // Lower the volume but bound to zero. newVolume = Math.max(--newVolume, 0); break; } // Don't bother adjusting the volume if there's no change // (and this wasn't the intent). if (direction != AudioManager.ADJUST_SAME && lowestStream.first == newVolume) { LOGD(TAG, "adjustVolumeSync(" + direction + ") ignored, no volume change."); return; } setVolumeSync(newVolume, lowestStream.second); } /** * Synchronize all volumes based on the anomalous stream. This happens * when the screen is off and we cannot handle volume change directly. * {@link #syncToStream(int)} uses the difference in a given stream from * the average to determine the direction of the change. */ public void syncToStream(int stream) { LOGD(TAG, "syncToStream(" + stream + ")"); mManagedVolume = STREAM_VOLUMES.get(stream); syncToManaged(); } protected void syncToManaged() { int[] newVolumes = new int[STREAMS.length]; boolean[] volChange = new boolean[STREAMS.length]; // Determine and set the expected new volume for each stream. for (int i = 0; i < STREAMS.length; ++i) { Pair<Integer, Integer> streamPair = STREAM_VOLUMES.get(STREAMS[i]); if (null != streamPair) { final int newVolume = (mManagedVolume.first * streamPair.second) / mManagedVolume.second; volChange[i] = (newVolume != streamPair.first); newVolumes[i] = newVolume; setVolume(STREAMS[i], newVolume); } } // Batch all calls to change system volume AFTER we've updated our local // state. This prevents issues with synchronising and BroadcastReceiver. for (int i = 0; i < STREAMS.length; ++i) { if (volChange[i]) { audioManager.setStreamVolume(STREAMS[i], newVolumes[i], 0); } } } /** Set the volume of all public AudioManager streams. */ public void setVolumeSync(int volume, int max) { LOGD(TAG, "setVolumeSync(" + volume + ", " + max + ")"); mManagedVolume = new Pair<Integer, Integer>(volume, max); syncToManaged(); } public int getManagedVolume() { return (null == mManagedVolume) ? Integer.MIN_VALUE : mManagedVolume.first; } public int getManagedMaxVolume() { return (null == mManagedVolume) ? Integer.MIN_VALUE : mManagedVolume.second; } /** @see android.media.AudioManager#getStreamMaxVolume(int) */ public int getStreamMaxVolume(int streamType) { Pair<Integer, Integer> stream = STREAM_VOLUMES.get(streamType); return (null == stream) ? Integer.MIN_VALUE : stream.second; } /** @see android.media.AudioManager#getStreamVolume(int) (int) */ public int getStreamVolume(int streamType) { Pair<Integer, Integer> stream = STREAM_VOLUMES.get(streamType); return (null == stream) ? Integer.MIN_VALUE : stream.first; } /** @return The highest volume a given stream has achieved. */ public int getHighestVolume(int streamType) { return STREAM_HIGHEST_VOLUMES.get(streamType); } /** Set the highest volume a stream as achieved; used primarily to clear previous value. */ public void setHighestVolume(int streamType, int highestVolume) { STREAM_HIGHEST_VOLUMES.setValueAt(streamType, highestVolume); } /** Set the volume for an individual stream. */ public boolean setVolume(int streamType, int volume) { Pair<Integer, Integer> stream = STREAM_VOLUMES.get(streamType); if (null == stream) return false; STREAM_VOLUMES.setValueAt(streamType, new Pair<Integer, Integer>(volume, stream.second)); if (STREAM_HIGHEST_VOLUMES.get(streamType) < volume) { STREAM_HIGHEST_VOLUMES.setValueAt(streamType, volume); } return true; } public int size() { return STREAM_VOLUMES.size(); } @Override public String toString() { StringBuilder builder = new StringBuilder(getClass().getSimpleName()); builder.append("#{"); for (final int stream : STREAMS) { Pair<Integer, Integer> streamPair = STREAM_VOLUMES.get(stream); if (null != streamPair) { builder.append(getStreamName(stream)); builder.append('='); builder.append(streamPair.first); builder.append('/'); builder.append(streamPair.second); builder.append(' '); } } builder.append("}"); return builder.toString(); } }