/*
* Copyright (C) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.android.talkback;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.os.Build;
import android.os.Message;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.util.SparseIntArray;
import com.android.utils.LogUtils;
import com.android.utils.WeakReferenceHandler;
import com.android.utils.compat.media.AudioManagerCompatUtils;
import com.google.android.marvin.talkback.TalkBackService;
/**
* Listens for and responds to volume changes.
*/
public class VolumeMonitor extends BroadcastReceiver {
/** Pseudo stream type for master volume. */
private static final int STREAM_MASTER = -100;
private static final SparseIntArray STREAM_NAMES = new SparseIntArray();
static {
STREAM_NAMES.put(STREAM_MASTER, R.string.value_stream_master);
STREAM_NAMES.put(AudioManager.STREAM_VOICE_CALL, R.string.value_stream_voice_call);
STREAM_NAMES.put(AudioManager.STREAM_SYSTEM, R.string.value_stream_system);
STREAM_NAMES.put(AudioManager.STREAM_RING, R.string.value_stream_ring);
STREAM_NAMES.put(AudioManager.STREAM_MUSIC, R.string.value_stream_music);
STREAM_NAMES.put(AudioManager.STREAM_ALARM, R.string.value_stream_alarm);
STREAM_NAMES.put(AudioManager.STREAM_NOTIFICATION, R.string.value_stream_notification);
STREAM_NAMES.put(AudioManager.STREAM_DTMF, R.string.value_stream_dtmf);
}
/** Keep track of adjustments made by this class. */
private final SparseIntArray mSelfAdjustments = new SparseIntArray(10);
private Context mContext;
private SpeechController mSpeechController;
private AudioManager mAudioManager;
private TelephonyManager mTelephonyManager;
/** The stream type currently being controlled. */
private int mCurrentStream = -1;
/**
* Creates and initializes a new volume monitor.
*
* @param context The parent service.
*/
public VolumeMonitor(SpeechController speechController, TalkBackService context) {
if (speechController == null) throw new IllegalStateException();
mContext = context;
mSpeechController = speechController;
// TODO: See if many objects use the same system services and get them once
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
}
public IntentFilter getFilter() {
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(AudioManagerCompatUtils.VOLUME_CHANGED_ACTION);
intentFilter.addAction(AudioManagerCompatUtils.MASTER_VOLUME_CHANGED_ACTION);
return intentFilter;
}
private boolean isSelfAdjusted(int streamType, int volume) {
if (mSelfAdjustments.indexOfKey(streamType) < 0) {
return false;
} else if (mSelfAdjustments.get(streamType) == volume) {
mSelfAdjustments.put(streamType, -1);
return true;
}
return false;
}
/**
* Called after volume changes. Handles acquiring control of the current
* stream and providing feedback.
*
* @param streamType The stream type constant.
* @param volume The current volume.
* @param prevVolume The previous volume.
*/
private void internalOnVolumeChanged(int streamType, int volume, int prevVolume) {
if (isSelfAdjusted(streamType, volume)) {
// Ignore self-adjustments.
return;
}
if (mCurrentStream < 0) {
// If the current stream hasn't been set, acquire control.
mCurrentStream = streamType;
AudioManagerCompatUtils.forceVolumeControlStream(mAudioManager, mCurrentStream);
mHandler.onControlAcquired(streamType);
return;
}
if (volume == prevVolume) {
// Ignore ADJUST_SAME if we've already acquired control.
return;
}
mHandler.releaseControlDelayed();
}
/**
* Called after control of a particular volume stream has been acquired and
* the audio stream has had a chance to quiet down.
*
* @param streamType The stream type over which control has been acquired.
*/
private void internalOnControlAcquired(int streamType) {
LogUtils.log(this, Log.VERBOSE, "Acquired control of stream %d", streamType);
mHandler.releaseControlDelayed();
}
/**
* Returns the volume announcement text for the specified stream.
*
* @param streamType The stream to announce.
* @return The volume announcement text for the stream.
*/
private String getAnnouncementForStreamType(int templateResId, int streamType) {
// The ringer has special cases for silent and vibrate.
if (streamType == AudioManager.STREAM_RING) {
switch (mAudioManager.getRingerMode()) {
case AudioManager.RINGER_MODE_VIBRATE:
return mContext.getString(R.string.value_ringer_vibrate);
case AudioManager.RINGER_MODE_SILENT:
return mContext.getString(R.string.value_ringer_silent);
}
}
final String streamName = getStreamName(streamType);
final int volume = getStreamVolume(streamType);
return mContext.getString(templateResId, streamName, volume);
}
/**
* Called after adjustments have been made and the user has not taken any
* action for a certain duration. Announces the current volume and releases
* control of the stream.
*/
private void internalOnReleaseControl() {
mHandler.clearReleaseControl();
final int streamType = mCurrentStream;
if (streamType < 0) {
// Already released!
return;
}
LogUtils.log(this, Log.VERBOSE, "Released control of stream %d", mCurrentStream);
if (!shouldAnnounceStream(streamType)) {
mHandler.post(new SpeechController.CompletionRunner(
mReleaseControl, SpeechController.STATUS_INTERRUPTED));
return;
}
final String text = getAnnouncementForStreamType(
R.string.template_stream_volume_set, streamType);
speakWithCompletion(text, mReleaseControl);
}
/**
* Releases control of the stream.
*/
public void releaseControl() {
mCurrentStream = -1;
AudioManagerCompatUtils.forceVolumeControlStream(mAudioManager, -1);
}
/**
* Returns whether a stream type should be announced.
*
* @param streamType The stream type.
* @return True if the stream should be announced.
*/
private boolean shouldAnnounceStream(int streamType) {
switch (streamType) {
case AudioManager.STREAM_MUSIC:
// Only announce music stream if it's not being used.
return !mAudioManager.isMusicActive();
case AudioManager.STREAM_VOICE_CALL:
// Never speak voice call volume. Since we only speak when
// telephony is idle, this check is only necessary for
// non-telephony voice calls (e.g. Google Talk).
return false;
default:
// Announce all other streams by default. The VOICE_CALL and
// RING streams are handled by checking the telephony state in
// speakWithCompletion().
return true;
}
}
/**
* Speaks text with a completion action, or just runs the completion action
* if the volume monitor should be quiet.
*
* @param text The text to speak.
* @param completedAction The action to run after speaking.
*/
private void speakWithCompletion(String text,
SpeechController.UtteranceCompleteRunnable completedAction) {
if ((mTelephonyManager != null)
&& (mTelephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE)) {
// If the phone is busy, don't speak anything.
mHandler.post(new SpeechController.CompletionRunner(
completedAction, SpeechController.STATUS_INTERRUPTED));
return;
}
mSpeechController.speak(
text, null, null, SpeechController.QUEUE_MODE_QUEUE, 0,
SpeechController.UTTERANCE_GROUP_DEFAULT, null, null, completedAction);
}
/**
* Returns the localized stream name for a given stream type constant.
*
* @param streamType A stream type constant.
* @return The localized stream name.
*/
private String getStreamName(int streamType) {
final int resId = STREAM_NAMES.get(streamType);
if (resId <= 0) {
return "";
}
return mContext.getString(resId);
}
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (AudioManagerCompatUtils.VOLUME_CHANGED_ACTION.equals(action)) {
final int type = intent.getIntExtra(
AudioManagerCompatUtils.EXTRA_VOLUME_STREAM_TYPE, -1);
// if there is a valid stream type alias, use it for the volume change, otherwise
// use the stream type.
final int typeAlias = intent.getIntExtra(
AudioManagerCompatUtils.EXTRA_VOLUME_STREAM_TYPE_ALIAS, type);
final int value = intent.getIntExtra(
AudioManagerCompatUtils.EXTRA_VOLUME_STREAM_VALUE, -1);
final int prevValue = intent.getIntExtra(
AudioManagerCompatUtils.EXTRA_PREV_VOLUME_STREAM_VALUE, -1);
if (typeAlias < 0 || value < 0 || prevValue < 0) {
return;
}
mHandler.onVolumeChanged(typeAlias, value, prevValue);
} else if (AudioManagerCompatUtils.MASTER_VOLUME_CHANGED_ACTION.equals(action)) {
final int value = intent.getIntExtra(
AudioManagerCompatUtils.EXTRA_MASTER_VOLUME_VALUE, -1);
final int prevValue = intent.getIntExtra(
AudioManagerCompatUtils.EXTRA_PREV_MASTER_VOLUME_VALUE, -1);
if (value < 0 || prevValue < 0) {
return;
}
mHandler.onVolumeChanged(STREAM_MASTER, value, prevValue);
}
}
/**
* Returns the stream volume as a percentage of maximum volume in increments
* of 5%, e.g. 73% is returned as 70.
*
* @param streamType A stream type constant.
* @return The stream volume as a percentage.
*/
private int getStreamVolume(int streamType) {
final int currentVolume = mAudioManager.getStreamVolume(streamType);
final int maxVolume = mAudioManager.getStreamMaxVolume(streamType);
return 5 * (int) (20 * currentVolume / maxVolume + 0.5);
}
private final VolumeHandler mHandler = new VolumeHandler(this);
/**
* Runnable that hides the volume overlay. Used as a completion action for
* the "volume set" utterance.
*/
private final SpeechController.UtteranceCompleteRunnable mReleaseControl =
new SpeechController.UtteranceCompleteRunnable() {
@Override
public void run(int status) {
releaseControl();
}
};
/**
* Handler class for the volume monitor. Transfers volume broadcasts to the
* service thread. Maintains timeout actions, including volume control
* acquisition and release.
*/
private static class VolumeHandler extends WeakReferenceHandler<VolumeMonitor> {
/** Timeout in milliseconds before the volume control disappears. */
private static final long RELEASE_CONTROL_TIMEOUT = 2000;
/** Timeout in milliseconds before the audio channel is available. */
private static final long ACQUIRED_CONTROL_TIMEOUT = 1000;
private static final int MSG_VOLUME_CHANGED = 1;
private static final int MSG_CONTROL = 2;
private static final int MSG_RELEASE_CONTROL = 3;
public VolumeHandler(VolumeMonitor parent) {
super(parent);
}
@Override
public void handleMessage(Message msg, VolumeMonitor parent) {
switch (msg.what) {
case MSG_VOLUME_CHANGED: {
final Integer type = (Integer) msg.obj;
final int value = msg.arg1;
final int prevValue = msg.arg2;
parent.internalOnVolumeChanged(type, value, prevValue);
break;
}
case MSG_CONTROL: {
final int streamType = msg.arg1;
parent.internalOnControlAcquired(streamType);
break;
}
case MSG_RELEASE_CONTROL: {
parent.internalOnReleaseControl();
break;
}
}
}
/**
* Starts the volume control release timeout.
*
* @see #internalOnReleaseControl
*/
public void releaseControlDelayed() {
clearReleaseControl();
final Message msg = obtainMessage(MSG_RELEASE_CONTROL);
sendMessageDelayed(msg, RELEASE_CONTROL_TIMEOUT);
}
/**
* Clears the volume control release timeout.
*/
public void clearReleaseControl() {
removeMessages(MSG_CONTROL);
removeMessages(MSG_RELEASE_CONTROL);
}
/**
* Starts the volume control acquisition timeout.
*
* @param type The stream type.
* @see #internalOnControlAcquired
*/
public void onControlAcquired(int type) {
removeMessages(MSG_CONTROL);
removeMessages(MSG_RELEASE_CONTROL);
// There is a small delay before we can speak.
final Message msg = obtainMessage(MSG_CONTROL, type, 0);
sendMessageDelayed(msg, ACQUIRED_CONTROL_TIMEOUT);
}
/**
* Transfers volume broadcasts to the handler thread.
*
* @param type The stream type.
* @param value The current volume index.
* @param prevValue The previous volume index.
*/
public void onVolumeChanged(int type, int value, int prevValue) {
obtainMessage(MSG_VOLUME_CHANGED, value, prevValue, type).sendToTarget();
}
}
}