Limit music volume when headphones or headset are inserted. Display warning message when user wants to increase the volume above a platform specific volume and request user acknowledgement before proceeding. TODO: exact wording of the warning message must be defined by UX. Change-Id: I00f429f602534c6d8783126b929371c4d432e6e2
5594 lines
237 KiB
Java
5594 lines
237 KiB
Java
/*
|
|
* Copyright (C) 2006 The Android Open Source Project
|
|
*
|
|
* 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 android.media;
|
|
|
|
import static android.Manifest.permission.REMOTE_AUDIO_PLAYBACK;
|
|
import static android.media.AudioManager.RINGER_MODE_NORMAL;
|
|
import static android.media.AudioManager.RINGER_MODE_SILENT;
|
|
import static android.media.AudioManager.RINGER_MODE_VIBRATE;
|
|
|
|
import android.app.Activity;
|
|
import android.app.ActivityManagerNative;
|
|
import android.app.KeyguardManager;
|
|
import android.app.PendingIntent;
|
|
import android.app.PendingIntent.CanceledException;
|
|
import android.app.PendingIntent.OnFinished;
|
|
import android.bluetooth.BluetoothA2dp;
|
|
import android.bluetooth.BluetoothAdapter;
|
|
import android.bluetooth.BluetoothClass;
|
|
import android.bluetooth.BluetoothDevice;
|
|
import android.bluetooth.BluetoothHeadset;
|
|
import android.bluetooth.BluetoothProfile;
|
|
import android.content.ActivityNotFoundException;
|
|
import android.content.BroadcastReceiver;
|
|
import android.content.ComponentName;
|
|
import android.content.ContentResolver;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.IntentFilter;
|
|
import android.content.pm.PackageManager;
|
|
import android.content.res.Configuration;
|
|
import android.database.ContentObserver;
|
|
import android.media.MediaPlayer.OnCompletionListener;
|
|
import android.media.MediaPlayer.OnErrorListener;
|
|
import android.os.Binder;
|
|
import android.os.Bundle;
|
|
import android.os.Environment;
|
|
import android.os.Handler;
|
|
import android.os.IBinder;
|
|
import android.os.Looper;
|
|
import android.os.Message;
|
|
import android.os.PowerManager;
|
|
import android.os.RemoteCallbackList;
|
|
import android.os.RemoteException;
|
|
import android.os.ServiceManager;
|
|
import android.os.SystemProperties;
|
|
import android.os.UserHandle;
|
|
import android.os.Vibrator;
|
|
import android.provider.Settings;
|
|
import android.provider.Settings.System;
|
|
import android.speech.RecognizerIntent;
|
|
import android.telephony.PhoneStateListener;
|
|
import android.telephony.TelephonyManager;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
import android.view.KeyEvent;
|
|
import android.view.VolumePanel;
|
|
|
|
import com.android.internal.telephony.ITelephony;
|
|
|
|
import java.io.FileDescriptor;
|
|
import java.io.IOException;
|
|
import java.io.PrintWriter;
|
|
import java.util.ArrayList;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import java.util.HashMap;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.NoSuchElementException;
|
|
import java.util.Set;
|
|
import java.util.Stack;
|
|
|
|
/**
|
|
* The implementation of the volume manager service.
|
|
* <p>
|
|
* This implementation focuses on delivering a responsive UI. Most methods are
|
|
* asynchronous to external calls. For example, the task of setting a volume
|
|
* will update our internal state, but in a separate thread will set the system
|
|
* volume and later persist to the database. Similarly, setting the ringer mode
|
|
* will update the state and broadcast a change and in a separate thread later
|
|
* persist the ringer mode.
|
|
*
|
|
* @hide
|
|
*/
|
|
public class AudioService extends IAudioService.Stub implements OnFinished {
|
|
|
|
private static final String TAG = "AudioService";
|
|
|
|
/** Debug remote control client/display feature */
|
|
protected static final boolean DEBUG_RC = false;
|
|
/** Debug volumes */
|
|
protected static final boolean DEBUG_VOL = false;
|
|
|
|
/** How long to delay before persisting a change in volume/ringer mode. */
|
|
private static final int PERSIST_DELAY = 500;
|
|
|
|
private Context mContext;
|
|
private ContentResolver mContentResolver;
|
|
private boolean mVoiceCapable;
|
|
|
|
/** The UI */
|
|
private VolumePanel mVolumePanel;
|
|
|
|
// sendMsg() flags
|
|
/** If the msg is already queued, replace it with this one. */
|
|
private static final int SENDMSG_REPLACE = 0;
|
|
/** If the msg is already queued, ignore this one and leave the old. */
|
|
private static final int SENDMSG_NOOP = 1;
|
|
/** If the msg is already queued, queue this one and leave the old. */
|
|
private static final int SENDMSG_QUEUE = 2;
|
|
|
|
// AudioHandler messages
|
|
private static final int MSG_SET_DEVICE_VOLUME = 0;
|
|
private static final int MSG_PERSIST_VOLUME = 1;
|
|
private static final int MSG_PERSIST_MASTER_VOLUME = 2;
|
|
private static final int MSG_PERSIST_RINGER_MODE = 3;
|
|
private static final int MSG_MEDIA_SERVER_DIED = 4;
|
|
private static final int MSG_MEDIA_SERVER_STARTED = 5;
|
|
private static final int MSG_PLAY_SOUND_EFFECT = 6;
|
|
private static final int MSG_BTA2DP_DOCK_TIMEOUT = 7;
|
|
private static final int MSG_LOAD_SOUND_EFFECTS = 8;
|
|
private static final int MSG_SET_FORCE_USE = 9;
|
|
private static final int MSG_PERSIST_MEDIABUTTONRECEIVER = 10;
|
|
private static final int MSG_BT_HEADSET_CNCT_FAILED = 11;
|
|
private static final int MSG_RCDISPLAY_CLEAR = 12;
|
|
private static final int MSG_RCDISPLAY_UPDATE = 13;
|
|
private static final int MSG_SET_ALL_VOLUMES = 14;
|
|
private static final int MSG_PERSIST_MASTER_VOLUME_MUTE = 15;
|
|
private static final int MSG_REPORT_NEW_ROUTES = 16;
|
|
private static final int MSG_REEVALUATE_REMOTE = 17;
|
|
private static final int MSG_RCC_NEW_PLAYBACK_INFO = 18;
|
|
private static final int MSG_RCC_NEW_VOLUME_OBS = 19;
|
|
private static final int MSG_SET_FORCE_BT_A2DP_USE = 20;
|
|
// start of messages handled under wakelock
|
|
// these messages can only be queued, i.e. sent with queueMsgUnderWakeLock(),
|
|
// and not with sendMsg(..., ..., SENDMSG_QUEUE, ...)
|
|
private static final int MSG_SET_WIRED_DEVICE_CONNECTION_STATE = 21;
|
|
private static final int MSG_SET_A2DP_CONNECTION_STATE = 22;
|
|
// end of messages handled under wakelock
|
|
private static final int MSG_SET_RSX_CONNECTION_STATE = 23; // change remote submix connection
|
|
private static final int MSG_SET_FORCE_RSX_USE = 24; // force remote submix audio routing
|
|
private static final int MSG_CHECK_MUSIC_ACTIVE = 25;
|
|
|
|
// flags for MSG_PERSIST_VOLUME indicating if current and/or last audible volume should be
|
|
// persisted
|
|
private static final int PERSIST_CURRENT = 0x1;
|
|
private static final int PERSIST_LAST_AUDIBLE = 0x2;
|
|
|
|
private static final int BTA2DP_DOCK_TIMEOUT_MILLIS = 8000;
|
|
// Timeout for connection to bluetooth headset service
|
|
private static final int BT_HEADSET_CNCT_TIMEOUT_MS = 3000;
|
|
|
|
/** @see AudioSystemThread */
|
|
private AudioSystemThread mAudioSystemThread;
|
|
/** @see AudioHandler */
|
|
private AudioHandler mAudioHandler;
|
|
/** @see VolumeStreamState */
|
|
private VolumeStreamState[] mStreamStates;
|
|
private SettingsObserver mSettingsObserver;
|
|
|
|
private int mMode;
|
|
// protects mRingerMode
|
|
private final Object mSettingsLock = new Object();
|
|
|
|
private boolean mMediaServerOk;
|
|
|
|
private SoundPool mSoundPool;
|
|
private final Object mSoundEffectsLock = new Object();
|
|
private static final int NUM_SOUNDPOOL_CHANNELS = 4;
|
|
|
|
// Internally master volume is a float in the 0.0 - 1.0 range,
|
|
// but to support integer based AudioManager API we translate it to 0 - 100
|
|
private static final int MAX_MASTER_VOLUME = 100;
|
|
|
|
// Maximum volume adjust steps allowed in a single batch call.
|
|
private static final int MAX_BATCH_VOLUME_ADJUST_STEPS = 4;
|
|
|
|
/* Sound effect file names */
|
|
private static final String SOUND_EFFECTS_PATH = "/media/audio/ui/";
|
|
private static final String[] SOUND_EFFECT_FILES = new String[] {
|
|
"Effect_Tick.ogg",
|
|
"KeypressStandard.ogg",
|
|
"KeypressSpacebar.ogg",
|
|
"KeypressDelete.ogg",
|
|
"KeypressReturn.ogg"
|
|
};
|
|
|
|
/* Sound effect file name mapping sound effect id (AudioManager.FX_xxx) to
|
|
* file index in SOUND_EFFECT_FILES[] (first column) and indicating if effect
|
|
* uses soundpool (second column) */
|
|
private final int[][] SOUND_EFFECT_FILES_MAP = new int[][] {
|
|
{0, -1}, // FX_KEY_CLICK
|
|
{0, -1}, // FX_FOCUS_NAVIGATION_UP
|
|
{0, -1}, // FX_FOCUS_NAVIGATION_DOWN
|
|
{0, -1}, // FX_FOCUS_NAVIGATION_LEFT
|
|
{0, -1}, // FX_FOCUS_NAVIGATION_RIGHT
|
|
{1, -1}, // FX_KEYPRESS_STANDARD
|
|
{2, -1}, // FX_KEYPRESS_SPACEBAR
|
|
{3, -1}, // FX_FOCUS_DELETE
|
|
{4, -1} // FX_FOCUS_RETURN
|
|
};
|
|
|
|
/** @hide Maximum volume index values for audio streams */
|
|
private final int[] MAX_STREAM_VOLUME = new int[] {
|
|
5, // STREAM_VOICE_CALL
|
|
7, // STREAM_SYSTEM
|
|
7, // STREAM_RING
|
|
15, // STREAM_MUSIC
|
|
7, // STREAM_ALARM
|
|
7, // STREAM_NOTIFICATION
|
|
15, // STREAM_BLUETOOTH_SCO
|
|
7, // STREAM_SYSTEM_ENFORCED
|
|
15, // STREAM_DTMF
|
|
15 // STREAM_TTS
|
|
};
|
|
/* mStreamVolumeAlias[] indicates for each stream if it uses the volume settings
|
|
* of another stream: This avoids multiplying the volume settings for hidden
|
|
* stream types that follow other stream behavior for volume settings
|
|
* NOTE: do not create loops in aliases!
|
|
* Some streams alias to different streams according to device category (phone or tablet) or
|
|
* use case (in call s off call...).See updateStreamVolumeAlias() for more details
|
|
* mStreamVolumeAlias contains the default aliases for a voice capable device (phone) and
|
|
* STREAM_VOLUME_ALIAS_NON_VOICE for a non voice capable device (tablet).*/
|
|
private final int[] STREAM_VOLUME_ALIAS = new int[] {
|
|
AudioSystem.STREAM_VOICE_CALL, // STREAM_VOICE_CALL
|
|
AudioSystem.STREAM_RING, // STREAM_SYSTEM
|
|
AudioSystem.STREAM_RING, // STREAM_RING
|
|
AudioSystem.STREAM_MUSIC, // STREAM_MUSIC
|
|
AudioSystem.STREAM_ALARM, // STREAM_ALARM
|
|
AudioSystem.STREAM_RING, // STREAM_NOTIFICATION
|
|
AudioSystem.STREAM_BLUETOOTH_SCO, // STREAM_BLUETOOTH_SCO
|
|
AudioSystem.STREAM_RING, // STREAM_SYSTEM_ENFORCED
|
|
AudioSystem.STREAM_RING, // STREAM_DTMF
|
|
AudioSystem.STREAM_MUSIC // STREAM_TTS
|
|
};
|
|
private final int[] STREAM_VOLUME_ALIAS_NON_VOICE = new int[] {
|
|
AudioSystem.STREAM_VOICE_CALL, // STREAM_VOICE_CALL
|
|
AudioSystem.STREAM_MUSIC, // STREAM_SYSTEM
|
|
AudioSystem.STREAM_RING, // STREAM_RING
|
|
AudioSystem.STREAM_MUSIC, // STREAM_MUSIC
|
|
AudioSystem.STREAM_ALARM, // STREAM_ALARM
|
|
AudioSystem.STREAM_RING, // STREAM_NOTIFICATION
|
|
AudioSystem.STREAM_BLUETOOTH_SCO, // STREAM_BLUETOOTH_SCO
|
|
AudioSystem.STREAM_MUSIC, // STREAM_SYSTEM_ENFORCED
|
|
AudioSystem.STREAM_MUSIC, // STREAM_DTMF
|
|
AudioSystem.STREAM_MUSIC // STREAM_TTS
|
|
};
|
|
private int[] mStreamVolumeAlias;
|
|
|
|
// stream names used by dumpStreamStates()
|
|
private final String[] STREAM_NAMES = new String[] {
|
|
"STREAM_VOICE_CALL",
|
|
"STREAM_SYSTEM",
|
|
"STREAM_RING",
|
|
"STREAM_MUSIC",
|
|
"STREAM_ALARM",
|
|
"STREAM_NOTIFICATION",
|
|
"STREAM_BLUETOOTH_SCO",
|
|
"STREAM_SYSTEM_ENFORCED",
|
|
"STREAM_DTMF",
|
|
"STREAM_TTS"
|
|
};
|
|
|
|
private final AudioSystem.ErrorCallback mAudioSystemCallback = new AudioSystem.ErrorCallback() {
|
|
public void onError(int error) {
|
|
switch (error) {
|
|
case AudioSystem.AUDIO_STATUS_SERVER_DIED:
|
|
if (mMediaServerOk) {
|
|
sendMsg(mAudioHandler, MSG_MEDIA_SERVER_DIED, SENDMSG_NOOP, 0, 0,
|
|
null, 1500);
|
|
mMediaServerOk = false;
|
|
}
|
|
break;
|
|
case AudioSystem.AUDIO_STATUS_OK:
|
|
if (!mMediaServerOk) {
|
|
sendMsg(mAudioHandler, MSG_MEDIA_SERVER_STARTED, SENDMSG_NOOP, 0, 0,
|
|
null, 0);
|
|
mMediaServerOk = true;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Current ringer mode from one of {@link AudioManager#RINGER_MODE_NORMAL},
|
|
* {@link AudioManager#RINGER_MODE_SILENT}, or
|
|
* {@link AudioManager#RINGER_MODE_VIBRATE}.
|
|
*/
|
|
// protected by mSettingsLock
|
|
private int mRingerMode;
|
|
|
|
/** @see System#MODE_RINGER_STREAMS_AFFECTED */
|
|
private int mRingerModeAffectedStreams;
|
|
|
|
// Streams currently muted by ringer mode
|
|
private int mRingerModeMutedStreams;
|
|
|
|
/** @see System#MUTE_STREAMS_AFFECTED */
|
|
private int mMuteAffectedStreams;
|
|
|
|
/**
|
|
* NOTE: setVibrateSetting(), getVibrateSetting(), shouldVibrate() are deprecated.
|
|
* mVibrateSetting is just maintained during deprecation period but vibration policy is
|
|
* now only controlled by mHasVibrator and mRingerMode
|
|
*/
|
|
private int mVibrateSetting;
|
|
|
|
// Is there a vibrator
|
|
private final boolean mHasVibrator;
|
|
|
|
// Broadcast receiver for device connections intent broadcasts
|
|
private final BroadcastReceiver mReceiver = new AudioServiceBroadcastReceiver();
|
|
|
|
// Used to alter media button redirection when the phone is ringing.
|
|
private boolean mIsRinging = false;
|
|
|
|
// Devices currently connected
|
|
private final HashMap <Integer, String> mConnectedDevices = new HashMap <Integer, String>();
|
|
|
|
// Forced device usage for communications
|
|
private int mForcedUseForComm;
|
|
|
|
// True if we have master volume support
|
|
private final boolean mUseMasterVolume;
|
|
|
|
private final int[] mMasterVolumeRamp;
|
|
|
|
// List of binder death handlers for setMode() client processes.
|
|
// The last process to have called setMode() is at the top of the list.
|
|
private final ArrayList <SetModeDeathHandler> mSetModeDeathHandlers = new ArrayList <SetModeDeathHandler>();
|
|
|
|
// List of clients having issued a SCO start request
|
|
private final ArrayList <ScoClient> mScoClients = new ArrayList <ScoClient>();
|
|
|
|
// BluetoothHeadset API to control SCO connection
|
|
private BluetoothHeadset mBluetoothHeadset;
|
|
|
|
// Bluetooth headset device
|
|
private BluetoothDevice mBluetoothHeadsetDevice;
|
|
|
|
// Indicate if SCO audio connection is currently active and if the initiator is
|
|
// audio service (internal) or bluetooth headset (external)
|
|
private int mScoAudioState;
|
|
// SCO audio state is not active
|
|
private static final int SCO_STATE_INACTIVE = 0;
|
|
// SCO audio activation request waiting for headset service to connect
|
|
private static final int SCO_STATE_ACTIVATE_REQ = 1;
|
|
// SCO audio state is active or starting due to a local request to start a virtual call
|
|
private static final int SCO_STATE_ACTIVE_INTERNAL = 3;
|
|
// SCO audio deactivation request waiting for headset service to connect
|
|
private static final int SCO_STATE_DEACTIVATE_REQ = 5;
|
|
|
|
// SCO audio state is active due to an action in BT handsfree (either voice recognition or
|
|
// in call audio)
|
|
private static final int SCO_STATE_ACTIVE_EXTERNAL = 2;
|
|
// Deactivation request for all SCO connections (initiated by audio mode change)
|
|
// waiting for headset service to connect
|
|
private static final int SCO_STATE_DEACTIVATE_EXT_REQ = 4;
|
|
|
|
// Current connection state indicated by bluetooth headset
|
|
private int mScoConnectionState;
|
|
|
|
// true if boot sequence has been completed
|
|
private boolean mBootCompleted;
|
|
// listener for SoundPool sample load completion indication
|
|
private SoundPoolCallback mSoundPoolCallBack;
|
|
// thread for SoundPool listener
|
|
private SoundPoolListenerThread mSoundPoolListenerThread;
|
|
// message looper for SoundPool listener
|
|
private Looper mSoundPoolLooper = null;
|
|
// volume applied to sound played with playSoundEffect()
|
|
private static int sSoundEffectVolumeDb;
|
|
// getActiveStreamType() will return STREAM_NOTIFICATION during this period after a notification
|
|
// stopped
|
|
private static final int NOTIFICATION_VOLUME_DELAY_MS = 5000;
|
|
// previous volume adjustment direction received by checkForRingerModeChange()
|
|
private int mPrevVolDirection = AudioManager.ADJUST_SAME;
|
|
// Keyguard manager proxy
|
|
private KeyguardManager mKeyguardManager;
|
|
// mVolumeControlStream is set by VolumePanel to temporarily force the stream type which volume
|
|
// is controlled by Vol keys.
|
|
private int mVolumeControlStream = -1;
|
|
private final Object mForceControlStreamLock = new Object();
|
|
// VolumePanel is currently the only client of forceVolumeControlStream() and runs in system
|
|
// server process so in theory it is not necessary to monitor the client death.
|
|
// However it is good to be ready for future evolutions.
|
|
private ForceControlStreamClient mForceControlStreamClient = null;
|
|
// Used to play ringtones outside system_server
|
|
private volatile IRingtonePlayer mRingtonePlayer;
|
|
|
|
private int mDeviceOrientation = Configuration.ORIENTATION_UNDEFINED;
|
|
|
|
// Request to override default use of A2DP for media.
|
|
private boolean mBluetoothA2dpEnabled;
|
|
private final Object mBluetoothA2dpEnabledLock = new Object();
|
|
|
|
// Monitoring of audio routes. Protected by mCurAudioRoutes.
|
|
final AudioRoutesInfo mCurAudioRoutes = new AudioRoutesInfo();
|
|
final RemoteCallbackList<IAudioRoutesObserver> mRoutesObservers
|
|
= new RemoteCallbackList<IAudioRoutesObserver>();
|
|
|
|
/**
|
|
* A fake stream type to match the notion of remote media playback
|
|
*/
|
|
public final static int STREAM_REMOTE_MUSIC = -200;
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
// Construction
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
/** @hide */
|
|
public AudioService(Context context) {
|
|
mContext = context;
|
|
mContentResolver = context.getContentResolver();
|
|
mVoiceCapable = mContext.getResources().getBoolean(
|
|
com.android.internal.R.bool.config_voice_capable);
|
|
mSafeMediaVolumeIndex = mContext.getResources().getInteger(
|
|
com.android.internal.R.integer.config_safe_media_volume_index) * 10;
|
|
|
|
PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
|
|
mMediaEventWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "handleMediaEvent");
|
|
|
|
Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
|
|
mHasVibrator = vibrator == null ? false : vibrator.hasVibrator();
|
|
|
|
// Intialized volume
|
|
MAX_STREAM_VOLUME[AudioSystem.STREAM_VOICE_CALL] = SystemProperties.getInt(
|
|
"ro.config.vc_call_vol_steps",
|
|
MAX_STREAM_VOLUME[AudioSystem.STREAM_VOICE_CALL]);
|
|
|
|
sSoundEffectVolumeDb = context.getResources().getInteger(
|
|
com.android.internal.R.integer.config_soundEffectVolumeDb);
|
|
|
|
mVolumePanel = new VolumePanel(context, this);
|
|
mMode = AudioSystem.MODE_NORMAL;
|
|
mForcedUseForComm = AudioSystem.FORCE_NONE;
|
|
createAudioSystemThread();
|
|
readPersistedSettings();
|
|
mSettingsObserver = new SettingsObserver();
|
|
updateStreamVolumeAlias(false /*updateVolumes*/);
|
|
createStreamStates();
|
|
|
|
synchronized (mSafeMediaVolumeEnabled) {
|
|
enforceSafeMediaVolume();
|
|
}
|
|
|
|
mMediaServerOk = true;
|
|
|
|
// Call setRingerModeInt() to apply correct mute
|
|
// state on streams affected by ringer mode.
|
|
mRingerModeMutedStreams = 0;
|
|
setRingerModeInt(getRingerMode(), false);
|
|
|
|
AudioSystem.setErrorCallback(mAudioSystemCallback);
|
|
|
|
// Register for device connection intent broadcasts.
|
|
IntentFilter intentFilter =
|
|
new IntentFilter(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
|
|
intentFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
|
|
intentFilter.addAction(Intent.ACTION_DOCK_EVENT);
|
|
intentFilter.addAction(Intent.ACTION_USB_AUDIO_ACCESSORY_PLUG);
|
|
intentFilter.addAction(Intent.ACTION_USB_AUDIO_DEVICE_PLUG);
|
|
intentFilter.addAction(Intent.ACTION_BOOT_COMPLETED);
|
|
intentFilter.addAction(Intent.ACTION_SCREEN_ON);
|
|
intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
|
|
|
|
// Register a configuration change listener only if requested by system properties
|
|
// to monitor orientation changes (off by default)
|
|
if (SystemProperties.getBoolean("ro.audio.monitorOrientation", false)) {
|
|
Log.v(TAG, "monitoring device orientation");
|
|
intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
|
|
// initialize orientation in AudioSystem
|
|
setOrientationForAudioSystem();
|
|
}
|
|
|
|
context.registerReceiver(mReceiver, intentFilter);
|
|
|
|
// Register for package removal intent broadcasts for media button receiver persistence
|
|
IntentFilter pkgFilter = new IntentFilter();
|
|
pkgFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
|
|
pkgFilter.addDataScheme("package");
|
|
context.registerReceiver(mReceiver, pkgFilter);
|
|
|
|
// Register for phone state monitoring
|
|
TelephonyManager tmgr = (TelephonyManager)
|
|
context.getSystemService(Context.TELEPHONY_SERVICE);
|
|
tmgr.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
|
|
|
|
mUseMasterVolume = context.getResources().getBoolean(
|
|
com.android.internal.R.bool.config_useMasterVolume);
|
|
restoreMasterVolume();
|
|
|
|
mMasterVolumeRamp = context.getResources().getIntArray(
|
|
com.android.internal.R.array.config_masterVolumeRamp);
|
|
|
|
mMainRemote = new RemotePlaybackState(-1, MAX_STREAM_VOLUME[AudioManager.STREAM_MUSIC],
|
|
MAX_STREAM_VOLUME[AudioManager.STREAM_MUSIC]);
|
|
mHasRemotePlayback = false;
|
|
mMainRemoteIsActive = false;
|
|
postReevaluateRemote();
|
|
}
|
|
|
|
private void createAudioSystemThread() {
|
|
mAudioSystemThread = new AudioSystemThread();
|
|
mAudioSystemThread.start();
|
|
waitForAudioHandlerCreation();
|
|
}
|
|
|
|
/** Waits for the volume handler to be created by the other thread. */
|
|
private void waitForAudioHandlerCreation() {
|
|
synchronized(this) {
|
|
while (mAudioHandler == null) {
|
|
try {
|
|
// Wait for mAudioHandler to be set by the other thread
|
|
wait();
|
|
} catch (InterruptedException e) {
|
|
Log.e(TAG, "Interrupted while waiting on volume handler.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void checkAllAliasStreamVolumes() {
|
|
int numStreamTypes = AudioSystem.getNumStreamTypes();
|
|
for (int streamType = 0; streamType < numStreamTypes; streamType++) {
|
|
if (streamType != mStreamVolumeAlias[streamType]) {
|
|
mStreamStates[streamType].
|
|
setAllIndexes(mStreamStates[mStreamVolumeAlias[streamType]],
|
|
false /*lastAudible*/);
|
|
mStreamStates[streamType].
|
|
setAllIndexes(mStreamStates[mStreamVolumeAlias[streamType]],
|
|
true /*lastAudible*/);
|
|
}
|
|
// apply stream volume
|
|
if (mStreamStates[streamType].muteCount() == 0) {
|
|
mStreamStates[streamType].applyAllVolumes();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void createStreamStates() {
|
|
int numStreamTypes = AudioSystem.getNumStreamTypes();
|
|
VolumeStreamState[] streams = mStreamStates = new VolumeStreamState[numStreamTypes];
|
|
|
|
for (int i = 0; i < numStreamTypes; i++) {
|
|
streams[i] = new VolumeStreamState(System.VOLUME_SETTINGS[mStreamVolumeAlias[i]], i);
|
|
}
|
|
|
|
checkAllAliasStreamVolumes();
|
|
}
|
|
|
|
private void dumpStreamStates(PrintWriter pw) {
|
|
pw.println("\nStream volumes (device: index)");
|
|
int numStreamTypes = AudioSystem.getNumStreamTypes();
|
|
for (int i = 0; i < numStreamTypes; i++) {
|
|
pw.println("- "+STREAM_NAMES[i]+":");
|
|
mStreamStates[i].dump(pw);
|
|
pw.println("");
|
|
}
|
|
}
|
|
|
|
|
|
private void updateStreamVolumeAlias(boolean updateVolumes) {
|
|
int dtmfStreamAlias;
|
|
if (mVoiceCapable) {
|
|
mStreamVolumeAlias = STREAM_VOLUME_ALIAS;
|
|
dtmfStreamAlias = AudioSystem.STREAM_RING;
|
|
} else {
|
|
mStreamVolumeAlias = STREAM_VOLUME_ALIAS_NON_VOICE;
|
|
dtmfStreamAlias = AudioSystem.STREAM_MUSIC;
|
|
}
|
|
if (isInCommunication()) {
|
|
dtmfStreamAlias = AudioSystem.STREAM_VOICE_CALL;
|
|
}
|
|
mStreamVolumeAlias[AudioSystem.STREAM_DTMF] = dtmfStreamAlias;
|
|
if (updateVolumes) {
|
|
mStreamStates[AudioSystem.STREAM_DTMF].setAllIndexes(mStreamStates[dtmfStreamAlias],
|
|
false /*lastAudible*/);
|
|
mStreamStates[AudioSystem.STREAM_DTMF].setAllIndexes(mStreamStates[dtmfStreamAlias],
|
|
true /*lastAudible*/);
|
|
sendMsg(mAudioHandler,
|
|
MSG_SET_ALL_VOLUMES,
|
|
SENDMSG_QUEUE,
|
|
0,
|
|
0,
|
|
mStreamStates[AudioSystem.STREAM_DTMF], 0);
|
|
}
|
|
}
|
|
|
|
private void readPersistedSettings() {
|
|
final ContentResolver cr = mContentResolver;
|
|
|
|
int ringerModeFromSettings =
|
|
System.getInt(cr, System.MODE_RINGER, AudioManager.RINGER_MODE_NORMAL);
|
|
int ringerMode = ringerModeFromSettings;
|
|
// sanity check in case the settings are restored from a device with incompatible
|
|
// ringer modes
|
|
if (!AudioManager.isValidRingerMode(ringerMode)) {
|
|
ringerMode = AudioManager.RINGER_MODE_NORMAL;
|
|
}
|
|
if ((ringerMode == AudioManager.RINGER_MODE_VIBRATE) && !mHasVibrator) {
|
|
ringerMode = AudioManager.RINGER_MODE_SILENT;
|
|
}
|
|
if (ringerMode != ringerModeFromSettings) {
|
|
System.putInt(cr, System.MODE_RINGER, ringerMode);
|
|
}
|
|
synchronized(mSettingsLock) {
|
|
mRingerMode = ringerMode;
|
|
}
|
|
|
|
// System.VIBRATE_ON is not used any more but defaults for mVibrateSetting
|
|
// are still needed while setVibrateSetting() and getVibrateSetting() are being deprecated.
|
|
mVibrateSetting = getValueForVibrateSetting(0,
|
|
AudioManager.VIBRATE_TYPE_NOTIFICATION,
|
|
mHasVibrator ? AudioManager.VIBRATE_SETTING_ONLY_SILENT
|
|
: AudioManager.VIBRATE_SETTING_OFF);
|
|
mVibrateSetting = getValueForVibrateSetting(mVibrateSetting,
|
|
AudioManager.VIBRATE_TYPE_RINGER,
|
|
mHasVibrator ? AudioManager.VIBRATE_SETTING_ONLY_SILENT
|
|
: AudioManager.VIBRATE_SETTING_OFF);
|
|
|
|
// make sure settings for ringer mode are consistent with device type: non voice capable
|
|
// devices (tablets) include media stream in silent mode whereas phones don't.
|
|
mRingerModeAffectedStreams = Settings.System.getInt(cr,
|
|
Settings.System.MODE_RINGER_STREAMS_AFFECTED,
|
|
((1 << AudioSystem.STREAM_RING)|(1 << AudioSystem.STREAM_NOTIFICATION)|
|
|
(1 << AudioSystem.STREAM_SYSTEM)|(1 << AudioSystem.STREAM_SYSTEM_ENFORCED)));
|
|
if (mVoiceCapable) {
|
|
mRingerModeAffectedStreams &= ~(1 << AudioSystem.STREAM_MUSIC);
|
|
} else {
|
|
mRingerModeAffectedStreams |= (1 << AudioSystem.STREAM_MUSIC);
|
|
}
|
|
Settings.System.putInt(cr,
|
|
Settings.System.MODE_RINGER_STREAMS_AFFECTED, mRingerModeAffectedStreams);
|
|
|
|
mMuteAffectedStreams = System.getInt(cr,
|
|
System.MUTE_STREAMS_AFFECTED,
|
|
((1 << AudioSystem.STREAM_MUSIC)|(1 << AudioSystem.STREAM_RING)|(1 << AudioSystem.STREAM_SYSTEM)));
|
|
|
|
boolean masterMute = System.getInt(cr, System.VOLUME_MASTER_MUTE, 0) == 1;
|
|
AudioSystem.setMasterMute(masterMute);
|
|
broadcastMasterMuteStatus(masterMute);
|
|
|
|
// Each stream will read its own persisted settings
|
|
|
|
// Broadcast the sticky intent
|
|
broadcastRingerMode(ringerMode);
|
|
|
|
// Broadcast vibrate settings
|
|
broadcastVibrateSetting(AudioManager.VIBRATE_TYPE_RINGER);
|
|
broadcastVibrateSetting(AudioManager.VIBRATE_TYPE_NOTIFICATION);
|
|
|
|
// Restore the default media button receiver from the system settings
|
|
restoreMediaButtonReceiver();
|
|
}
|
|
|
|
private int rescaleIndex(int index, int srcStream, int dstStream) {
|
|
return (index * mStreamStates[dstStream].getMaxIndex() + mStreamStates[srcStream].getMaxIndex() / 2) / mStreamStates[srcStream].getMaxIndex();
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
// IPC methods
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
/** @see AudioManager#adjustVolume(int, int) */
|
|
public void adjustVolume(int direction, int flags) {
|
|
adjustSuggestedStreamVolume(direction, AudioManager.USE_DEFAULT_STREAM_TYPE, flags);
|
|
}
|
|
|
|
/** @see AudioManager#adjustLocalOrRemoteStreamVolume(int, int) with current assumption
|
|
* on streamType: fixed to STREAM_MUSIC */
|
|
public void adjustLocalOrRemoteStreamVolume(int streamType, int direction) {
|
|
if (DEBUG_VOL) Log.d(TAG, "adjustLocalOrRemoteStreamVolume(dir="+direction+")");
|
|
if (checkUpdateRemoteStateIfActive(AudioSystem.STREAM_MUSIC)) {
|
|
adjustRemoteVolume(AudioSystem.STREAM_MUSIC, direction, 0);
|
|
} else if (AudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0)) {
|
|
adjustStreamVolume(AudioSystem.STREAM_MUSIC, direction, 0);
|
|
}
|
|
}
|
|
|
|
/** @see AudioManager#adjustVolume(int, int, int) */
|
|
public void adjustSuggestedStreamVolume(int direction, int suggestedStreamType, int flags) {
|
|
if (DEBUG_VOL) Log.d(TAG, "adjustSuggestedStreamVolume() stream="+suggestedStreamType);
|
|
int streamType;
|
|
if (mVolumeControlStream != -1) {
|
|
streamType = mVolumeControlStream;
|
|
} else {
|
|
streamType = getActiveStreamType(suggestedStreamType);
|
|
}
|
|
|
|
// Play sounds on STREAM_RING only and if lock screen is not on.
|
|
if ((streamType != STREAM_REMOTE_MUSIC) &&
|
|
(flags & AudioManager.FLAG_PLAY_SOUND) != 0 &&
|
|
((mStreamVolumeAlias[streamType] != AudioSystem.STREAM_RING)
|
|
|| (mKeyguardManager != null && mKeyguardManager.isKeyguardLocked()))) {
|
|
flags &= ~AudioManager.FLAG_PLAY_SOUND;
|
|
}
|
|
|
|
if (streamType == STREAM_REMOTE_MUSIC) {
|
|
// don't play sounds for remote
|
|
flags &= ~AudioManager.FLAG_PLAY_SOUND;
|
|
//if (DEBUG_VOL) Log.i(TAG, "Need to adjust remote volume: calling adjustRemoteVolume()");
|
|
adjustRemoteVolume(AudioSystem.STREAM_MUSIC, direction, flags);
|
|
} else {
|
|
adjustStreamVolume(streamType, direction, flags);
|
|
}
|
|
}
|
|
|
|
/** @see AudioManager#adjustStreamVolume(int, int, int) */
|
|
public void adjustStreamVolume(int streamType, int direction, int flags) {
|
|
if (DEBUG_VOL) Log.d(TAG, "adjustStreamVolume() stream="+streamType+", dir="+direction);
|
|
|
|
ensureValidDirection(direction);
|
|
ensureValidStreamType(streamType);
|
|
|
|
// use stream type alias here so that streams with same alias have the same behavior,
|
|
// including with regard to silent mode control (e.g the use of STREAM_RING below and in
|
|
// checkForRingerModeChange() in place of STREAM_RING or STREAM_NOTIFICATION)
|
|
int streamTypeAlias = mStreamVolumeAlias[streamType];
|
|
VolumeStreamState streamState = mStreamStates[streamTypeAlias];
|
|
|
|
final int device = getDeviceForStream(streamTypeAlias);
|
|
// get last audible index if stream is muted, current index otherwise
|
|
final int aliasIndex = streamState.getIndex(device,
|
|
(streamState.muteCount() != 0) /* lastAudible */);
|
|
boolean adjustVolume = true;
|
|
|
|
// convert one UI step (+/-1) into a number of internal units on the stream alias
|
|
int step = rescaleIndex(10, streamType, streamTypeAlias);
|
|
|
|
if ((direction == AudioManager.ADJUST_RAISE) &&
|
|
!checkSafeMediaVolume(streamTypeAlias, aliasIndex + step, device)) {
|
|
return;
|
|
}
|
|
|
|
// If either the client forces allowing ringer modes for this adjustment,
|
|
// or the stream type is one that is affected by ringer modes
|
|
if (((flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0) ||
|
|
(streamTypeAlias == getMasterStreamType())) {
|
|
int ringerMode = getRingerMode();
|
|
// do not vibrate if already in vibrate mode
|
|
if (ringerMode == AudioManager.RINGER_MODE_VIBRATE) {
|
|
flags &= ~AudioManager.FLAG_VIBRATE;
|
|
}
|
|
// Check if the ringer mode changes with this volume adjustment. If
|
|
// it does, it will handle adjusting the volume, so we won't below
|
|
adjustVolume = checkForRingerModeChange(aliasIndex, direction, step);
|
|
if ((streamTypeAlias == getMasterStreamType()) &&
|
|
(mRingerMode == AudioManager.RINGER_MODE_SILENT)) {
|
|
streamState.setLastAudibleIndex(0, device);
|
|
}
|
|
}
|
|
|
|
// If stream is muted, adjust last audible index only
|
|
int index;
|
|
final int oldIndex = mStreamStates[streamType].getIndex(device,
|
|
(mStreamStates[streamType].muteCount() != 0) /* lastAudible */);
|
|
|
|
if (streamState.muteCount() != 0) {
|
|
if (adjustVolume) {
|
|
// Post a persist volume msg
|
|
// no need to persist volume on all streams sharing the same alias
|
|
streamState.adjustLastAudibleIndex(direction * step, device);
|
|
sendMsg(mAudioHandler,
|
|
MSG_PERSIST_VOLUME,
|
|
SENDMSG_QUEUE,
|
|
PERSIST_LAST_AUDIBLE,
|
|
device,
|
|
streamState,
|
|
PERSIST_DELAY);
|
|
}
|
|
index = mStreamStates[streamType].getIndex(device, true /* lastAudible */);
|
|
} else {
|
|
if (adjustVolume && streamState.adjustIndex(direction * step, device)) {
|
|
// Post message to set system volume (it in turn will post a message
|
|
// to persist). Do not change volume if stream is muted.
|
|
sendMsg(mAudioHandler,
|
|
MSG_SET_DEVICE_VOLUME,
|
|
SENDMSG_QUEUE,
|
|
device,
|
|
0,
|
|
streamState,
|
|
0);
|
|
}
|
|
index = mStreamStates[streamType].getIndex(device, false /* lastAudible */);
|
|
}
|
|
|
|
sendVolumeUpdate(streamType, oldIndex, index, flags);
|
|
}
|
|
|
|
/** @see AudioManager#adjustMasterVolume(int) */
|
|
public void adjustMasterVolume(int steps, int flags) {
|
|
ensureValidSteps(steps);
|
|
int volume = Math.round(AudioSystem.getMasterVolume() * MAX_MASTER_VOLUME);
|
|
int delta = 0;
|
|
int numSteps = Math.abs(steps);
|
|
int direction = steps > 0 ? AudioManager.ADJUST_RAISE : AudioManager.ADJUST_LOWER;
|
|
for (int i = 0; i < numSteps; ++i) {
|
|
delta = findVolumeDelta(direction, volume);
|
|
volume += delta;
|
|
}
|
|
|
|
//Log.d(TAG, "adjustMasterVolume volume: " + volume + " steps: " + steps);
|
|
setMasterVolume(volume, flags);
|
|
}
|
|
|
|
/** @see AudioManager#setStreamVolume(int, int, int) */
|
|
public void setStreamVolume(int streamType, int index, int flags) {
|
|
ensureValidStreamType(streamType);
|
|
VolumeStreamState streamState = mStreamStates[mStreamVolumeAlias[streamType]];
|
|
|
|
final int device = getDeviceForStream(streamType);
|
|
|
|
// get last audible index if stream is muted, current index otherwise
|
|
final int oldIndex = streamState.getIndex(device,
|
|
(streamState.muteCount() != 0) /* lastAudible */);
|
|
|
|
index = rescaleIndex(index * 10, streamType, mStreamVolumeAlias[streamType]);
|
|
|
|
if (!checkSafeMediaVolume(mStreamVolumeAlias[streamType], index, device)) {
|
|
return;
|
|
}
|
|
|
|
// setting volume on master stream type also controls silent mode
|
|
if (((flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0) ||
|
|
(mStreamVolumeAlias[streamType] == getMasterStreamType())) {
|
|
int newRingerMode;
|
|
if (index == 0) {
|
|
newRingerMode = mHasVibrator ? AudioManager.RINGER_MODE_VIBRATE
|
|
: AudioManager.RINGER_MODE_SILENT;
|
|
setStreamVolumeInt(mStreamVolumeAlias[streamType],
|
|
index,
|
|
device,
|
|
false,
|
|
true);
|
|
} else {
|
|
newRingerMode = AudioManager.RINGER_MODE_NORMAL;
|
|
}
|
|
setRingerMode(newRingerMode);
|
|
}
|
|
|
|
setStreamVolumeInt(mStreamVolumeAlias[streamType], index, device, false, true);
|
|
// get last audible index if stream is muted, current index otherwise
|
|
index = mStreamStates[streamType].getIndex(device,
|
|
(mStreamStates[streamType].muteCount() != 0) /* lastAudible */);
|
|
|
|
sendVolumeUpdate(streamType, oldIndex, index, flags);
|
|
}
|
|
|
|
/** @see AudioManager#forceVolumeControlStream(int) */
|
|
public void forceVolumeControlStream(int streamType, IBinder cb) {
|
|
synchronized(mForceControlStreamLock) {
|
|
mVolumeControlStream = streamType;
|
|
if (mVolumeControlStream == -1) {
|
|
if (mForceControlStreamClient != null) {
|
|
mForceControlStreamClient.release();
|
|
mForceControlStreamClient = null;
|
|
}
|
|
} else {
|
|
mForceControlStreamClient = new ForceControlStreamClient(cb);
|
|
}
|
|
}
|
|
}
|
|
|
|
private class ForceControlStreamClient implements IBinder.DeathRecipient {
|
|
private IBinder mCb; // To be notified of client's death
|
|
|
|
ForceControlStreamClient(IBinder cb) {
|
|
if (cb != null) {
|
|
try {
|
|
cb.linkToDeath(this, 0);
|
|
} catch (RemoteException e) {
|
|
// Client has died!
|
|
Log.w(TAG, "ForceControlStreamClient() could not link to "+cb+" binder death");
|
|
cb = null;
|
|
}
|
|
}
|
|
mCb = cb;
|
|
}
|
|
|
|
public void binderDied() {
|
|
synchronized(mForceControlStreamLock) {
|
|
Log.w(TAG, "SCO client died");
|
|
if (mForceControlStreamClient != this) {
|
|
Log.w(TAG, "unregistered control stream client died");
|
|
} else {
|
|
mForceControlStreamClient = null;
|
|
mVolumeControlStream = -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void release() {
|
|
if (mCb != null) {
|
|
mCb.unlinkToDeath(this, 0);
|
|
mCb = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
private int findVolumeDelta(int direction, int volume) {
|
|
int delta = 0;
|
|
if (direction == AudioManager.ADJUST_RAISE) {
|
|
if (volume == MAX_MASTER_VOLUME) {
|
|
return 0;
|
|
}
|
|
// This is the default value if we make it to the end
|
|
delta = mMasterVolumeRamp[1];
|
|
// If we're raising the volume move down the ramp array until we
|
|
// find the volume we're above and use that groups delta.
|
|
for (int i = mMasterVolumeRamp.length - 1; i > 1; i -= 2) {
|
|
if (volume >= mMasterVolumeRamp[i - 1]) {
|
|
delta = mMasterVolumeRamp[i];
|
|
break;
|
|
}
|
|
}
|
|
} else if (direction == AudioManager.ADJUST_LOWER){
|
|
if (volume == 0) {
|
|
return 0;
|
|
}
|
|
int length = mMasterVolumeRamp.length;
|
|
// This is the default value if we make it to the end
|
|
delta = -mMasterVolumeRamp[length - 1];
|
|
// If we're lowering the volume move up the ramp array until we
|
|
// find the volume we're below and use the group below it's delta
|
|
for (int i = 2; i < length; i += 2) {
|
|
if (volume <= mMasterVolumeRamp[i]) {
|
|
delta = -mMasterVolumeRamp[i - 1];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return delta;
|
|
}
|
|
|
|
private void sendBroadcastToAll(Intent intent) {
|
|
final long ident = Binder.clearCallingIdentity();
|
|
try {
|
|
mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
|
|
} finally {
|
|
Binder.restoreCallingIdentity(ident);
|
|
}
|
|
}
|
|
|
|
private void sendStickyBroadcastToAll(Intent intent) {
|
|
final long ident = Binder.clearCallingIdentity();
|
|
try {
|
|
mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
|
|
} finally {
|
|
Binder.restoreCallingIdentity(ident);
|
|
}
|
|
}
|
|
|
|
// UI update and Broadcast Intent
|
|
private void sendVolumeUpdate(int streamType, int oldIndex, int index, int flags) {
|
|
if (!mVoiceCapable && (streamType == AudioSystem.STREAM_RING)) {
|
|
streamType = AudioSystem.STREAM_NOTIFICATION;
|
|
}
|
|
|
|
mVolumePanel.postVolumeChanged(streamType, flags);
|
|
|
|
oldIndex = (oldIndex + 5) / 10;
|
|
index = (index + 5) / 10;
|
|
Intent intent = new Intent(AudioManager.VOLUME_CHANGED_ACTION);
|
|
intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, streamType);
|
|
intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, index);
|
|
intent.putExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, oldIndex);
|
|
sendBroadcastToAll(intent);
|
|
}
|
|
|
|
// UI update and Broadcast Intent
|
|
private void sendMasterVolumeUpdate(int flags, int oldVolume, int newVolume) {
|
|
mVolumePanel.postMasterVolumeChanged(flags);
|
|
|
|
Intent intent = new Intent(AudioManager.MASTER_VOLUME_CHANGED_ACTION);
|
|
intent.putExtra(AudioManager.EXTRA_PREV_MASTER_VOLUME_VALUE, oldVolume);
|
|
intent.putExtra(AudioManager.EXTRA_MASTER_VOLUME_VALUE, newVolume);
|
|
sendBroadcastToAll(intent);
|
|
}
|
|
|
|
// UI update and Broadcast Intent
|
|
private void sendMasterMuteUpdate(boolean muted, int flags) {
|
|
mVolumePanel.postMasterMuteChanged(flags);
|
|
broadcastMasterMuteStatus(muted);
|
|
}
|
|
|
|
private void broadcastMasterMuteStatus(boolean muted) {
|
|
Intent intent = new Intent(AudioManager.MASTER_MUTE_CHANGED_ACTION);
|
|
intent.putExtra(AudioManager.EXTRA_MASTER_VOLUME_MUTED, muted);
|
|
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
|
|
| Intent.FLAG_RECEIVER_REPLACE_PENDING);
|
|
sendStickyBroadcastToAll(intent);
|
|
}
|
|
|
|
/**
|
|
* Sets the stream state's index, and posts a message to set system volume.
|
|
* This will not call out to the UI. Assumes a valid stream type.
|
|
*
|
|
* @param streamType Type of the stream
|
|
* @param index Desired volume index of the stream
|
|
* @param device the device whose volume must be changed
|
|
* @param force If true, set the volume even if the desired volume is same
|
|
* as the current volume.
|
|
* @param lastAudible If true, stores new index as last audible one
|
|
*/
|
|
private void setStreamVolumeInt(int streamType,
|
|
int index,
|
|
int device,
|
|
boolean force,
|
|
boolean lastAudible) {
|
|
VolumeStreamState streamState = mStreamStates[streamType];
|
|
|
|
// If stream is muted, set last audible index only
|
|
if (streamState.muteCount() != 0) {
|
|
// Do not allow last audible index to be 0
|
|
if (index != 0) {
|
|
streamState.setLastAudibleIndex(index, device);
|
|
// Post a persist volume msg
|
|
sendMsg(mAudioHandler,
|
|
MSG_PERSIST_VOLUME,
|
|
SENDMSG_QUEUE,
|
|
PERSIST_LAST_AUDIBLE,
|
|
device,
|
|
streamState,
|
|
PERSIST_DELAY);
|
|
}
|
|
} else {
|
|
if (streamState.setIndex(index, device, lastAudible) || force) {
|
|
// Post message to set system volume (it in turn will post a message
|
|
// to persist).
|
|
sendMsg(mAudioHandler,
|
|
MSG_SET_DEVICE_VOLUME,
|
|
SENDMSG_QUEUE,
|
|
device,
|
|
0,
|
|
streamState,
|
|
0);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @see AudioManager#setStreamSolo(int, boolean) */
|
|
public void setStreamSolo(int streamType, boolean state, IBinder cb) {
|
|
for (int stream = 0; stream < mStreamStates.length; stream++) {
|
|
if (!isStreamAffectedByMute(stream) || stream == streamType) continue;
|
|
// Bring back last audible volume
|
|
mStreamStates[stream].mute(cb, state);
|
|
}
|
|
}
|
|
|
|
/** @see AudioManager#setStreamMute(int, boolean) */
|
|
public void setStreamMute(int streamType, boolean state, IBinder cb) {
|
|
if (isStreamAffectedByMute(streamType)) {
|
|
mStreamStates[streamType].mute(cb, state);
|
|
}
|
|
}
|
|
|
|
/** get stream mute state. */
|
|
public boolean isStreamMute(int streamType) {
|
|
return (mStreamStates[streamType].muteCount() != 0);
|
|
}
|
|
|
|
/** @see AudioManager#setMasterMute(boolean, IBinder) */
|
|
public void setMasterMute(boolean state, int flags, IBinder cb) {
|
|
if (state != AudioSystem.getMasterMute()) {
|
|
AudioSystem.setMasterMute(state);
|
|
// Post a persist master volume msg
|
|
sendMsg(mAudioHandler, MSG_PERSIST_MASTER_VOLUME_MUTE, SENDMSG_REPLACE, state ? 1
|
|
: 0, 0, null, PERSIST_DELAY);
|
|
sendMasterMuteUpdate(state, flags);
|
|
}
|
|
}
|
|
|
|
/** get master mute state. */
|
|
public boolean isMasterMute() {
|
|
return AudioSystem.getMasterMute();
|
|
}
|
|
|
|
/** @see AudioManager#getStreamVolume(int) */
|
|
public int getStreamVolume(int streamType) {
|
|
ensureValidStreamType(streamType);
|
|
int device = getDeviceForStream(streamType);
|
|
return (mStreamStates[streamType].getIndex(device, false /* lastAudible */) + 5) / 10;
|
|
}
|
|
|
|
public int getMasterVolume() {
|
|
if (isMasterMute()) return 0;
|
|
return getLastAudibleMasterVolume();
|
|
}
|
|
|
|
public void setMasterVolume(int volume, int flags) {
|
|
if (volume < 0) {
|
|
volume = 0;
|
|
} else if (volume > MAX_MASTER_VOLUME) {
|
|
volume = MAX_MASTER_VOLUME;
|
|
}
|
|
doSetMasterVolume((float)volume / MAX_MASTER_VOLUME, flags);
|
|
}
|
|
|
|
private void doSetMasterVolume(float volume, int flags) {
|
|
// don't allow changing master volume when muted
|
|
if (!AudioSystem.getMasterMute()) {
|
|
int oldVolume = getMasterVolume();
|
|
AudioSystem.setMasterVolume(volume);
|
|
|
|
int newVolume = getMasterVolume();
|
|
if (newVolume != oldVolume) {
|
|
// Post a persist master volume msg
|
|
sendMsg(mAudioHandler, MSG_PERSIST_MASTER_VOLUME, SENDMSG_REPLACE,
|
|
Math.round(volume * (float)1000.0), 0, null, PERSIST_DELAY);
|
|
}
|
|
// Send the volume update regardless whether there was a change.
|
|
sendMasterVolumeUpdate(flags, oldVolume, newVolume);
|
|
}
|
|
}
|
|
|
|
/** @see AudioManager#getStreamMaxVolume(int) */
|
|
public int getStreamMaxVolume(int streamType) {
|
|
ensureValidStreamType(streamType);
|
|
return (mStreamStates[streamType].getMaxIndex() + 5) / 10;
|
|
}
|
|
|
|
public int getMasterMaxVolume() {
|
|
return MAX_MASTER_VOLUME;
|
|
}
|
|
|
|
/** Get last audible volume before stream was muted. */
|
|
public int getLastAudibleStreamVolume(int streamType) {
|
|
ensureValidStreamType(streamType);
|
|
int device = getDeviceForStream(streamType);
|
|
return (mStreamStates[streamType].getIndex(device, true /* lastAudible */) + 5) / 10;
|
|
}
|
|
|
|
/** Get last audible master volume before it was muted. */
|
|
public int getLastAudibleMasterVolume() {
|
|
return Math.round(AudioSystem.getMasterVolume() * MAX_MASTER_VOLUME);
|
|
}
|
|
|
|
/** @see AudioManager#getMasterStreamType(int) */
|
|
public int getMasterStreamType() {
|
|
if (mVoiceCapable) {
|
|
return AudioSystem.STREAM_RING;
|
|
} else {
|
|
return AudioSystem.STREAM_MUSIC;
|
|
}
|
|
}
|
|
|
|
/** @see AudioManager#getRingerMode() */
|
|
public int getRingerMode() {
|
|
synchronized(mSettingsLock) {
|
|
return mRingerMode;
|
|
}
|
|
}
|
|
|
|
private void ensureValidRingerMode(int ringerMode) {
|
|
if (!AudioManager.isValidRingerMode(ringerMode)) {
|
|
throw new IllegalArgumentException("Bad ringer mode " + ringerMode);
|
|
}
|
|
}
|
|
|
|
/** @see AudioManager#setRingerMode(int) */
|
|
public void setRingerMode(int ringerMode) {
|
|
if ((ringerMode == AudioManager.RINGER_MODE_VIBRATE) && !mHasVibrator) {
|
|
ringerMode = AudioManager.RINGER_MODE_SILENT;
|
|
}
|
|
if (ringerMode != getRingerMode()) {
|
|
setRingerModeInt(ringerMode, true);
|
|
// Send sticky broadcast
|
|
broadcastRingerMode(ringerMode);
|
|
}
|
|
}
|
|
|
|
private void setRingerModeInt(int ringerMode, boolean persist) {
|
|
synchronized(mSettingsLock) {
|
|
mRingerMode = ringerMode;
|
|
}
|
|
|
|
// Mute stream if not previously muted by ringer mode and ringer mode
|
|
// is not RINGER_MODE_NORMAL and stream is affected by ringer mode.
|
|
// Unmute stream if previously muted by ringer mode and ringer mode
|
|
// is RINGER_MODE_NORMAL or stream is not affected by ringer mode.
|
|
int numStreamTypes = AudioSystem.getNumStreamTypes();
|
|
for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
|
|
if (isStreamMutedByRingerMode(streamType)) {
|
|
if (!isStreamAffectedByRingerMode(streamType) ||
|
|
ringerMode == AudioManager.RINGER_MODE_NORMAL) {
|
|
// ring and notifications volume should never be 0 when not silenced
|
|
// on voice capable devices
|
|
if (mVoiceCapable &&
|
|
mStreamVolumeAlias[streamType] == AudioSystem.STREAM_RING) {
|
|
synchronized (mStreamStates[streamType]) {
|
|
Set set = mStreamStates[streamType].mLastAudibleIndex.entrySet();
|
|
Iterator i = set.iterator();
|
|
while (i.hasNext()) {
|
|
Map.Entry entry = (Map.Entry)i.next();
|
|
if ((Integer)entry.getValue() == 0) {
|
|
entry.setValue(10);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
mStreamStates[streamType].mute(null, false);
|
|
mRingerModeMutedStreams &= ~(1 << streamType);
|
|
}
|
|
} else {
|
|
if (isStreamAffectedByRingerMode(streamType) &&
|
|
ringerMode != AudioManager.RINGER_MODE_NORMAL) {
|
|
mStreamStates[streamType].mute(null, true);
|
|
mRingerModeMutedStreams |= (1 << streamType);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Post a persist ringer mode msg
|
|
if (persist) {
|
|
sendMsg(mAudioHandler, MSG_PERSIST_RINGER_MODE,
|
|
SENDMSG_REPLACE, 0, 0, null, PERSIST_DELAY);
|
|
}
|
|
}
|
|
|
|
private void restoreMasterVolume() {
|
|
if (mUseMasterVolume) {
|
|
float volume = Settings.System.getFloat(mContentResolver,
|
|
Settings.System.VOLUME_MASTER, -1.0f);
|
|
if (volume >= 0.0f) {
|
|
AudioSystem.setMasterVolume(volume);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @see AudioManager#shouldVibrate(int) */
|
|
public boolean shouldVibrate(int vibrateType) {
|
|
if (!mHasVibrator) return false;
|
|
|
|
switch (getVibrateSetting(vibrateType)) {
|
|
|
|
case AudioManager.VIBRATE_SETTING_ON:
|
|
return getRingerMode() != AudioManager.RINGER_MODE_SILENT;
|
|
|
|
case AudioManager.VIBRATE_SETTING_ONLY_SILENT:
|
|
return getRingerMode() == AudioManager.RINGER_MODE_VIBRATE;
|
|
|
|
case AudioManager.VIBRATE_SETTING_OFF:
|
|
// return false, even for incoming calls
|
|
return false;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** @see AudioManager#getVibrateSetting(int) */
|
|
public int getVibrateSetting(int vibrateType) {
|
|
if (!mHasVibrator) return AudioManager.VIBRATE_SETTING_OFF;
|
|
return (mVibrateSetting >> (vibrateType * 2)) & 3;
|
|
}
|
|
|
|
/** @see AudioManager#setVibrateSetting(int, int) */
|
|
public void setVibrateSetting(int vibrateType, int vibrateSetting) {
|
|
|
|
if (!mHasVibrator) return;
|
|
|
|
mVibrateSetting = getValueForVibrateSetting(mVibrateSetting, vibrateType, vibrateSetting);
|
|
|
|
// Broadcast change
|
|
broadcastVibrateSetting(vibrateType);
|
|
|
|
}
|
|
|
|
/**
|
|
* @see #setVibrateSetting(int, int)
|
|
*/
|
|
public static int getValueForVibrateSetting(int existingValue, int vibrateType,
|
|
int vibrateSetting) {
|
|
|
|
// First clear the existing setting. Each vibrate type has two bits in
|
|
// the value. Note '3' is '11' in binary.
|
|
existingValue &= ~(3 << (vibrateType * 2));
|
|
|
|
// Set into the old value
|
|
existingValue |= (vibrateSetting & 3) << (vibrateType * 2);
|
|
|
|
return existingValue;
|
|
}
|
|
|
|
private class SetModeDeathHandler implements IBinder.DeathRecipient {
|
|
private IBinder mCb; // To be notified of client's death
|
|
private int mPid;
|
|
private int mMode = AudioSystem.MODE_NORMAL; // Current mode set by this client
|
|
|
|
SetModeDeathHandler(IBinder cb, int pid) {
|
|
mCb = cb;
|
|
mPid = pid;
|
|
}
|
|
|
|
public void binderDied() {
|
|
int newModeOwnerPid = 0;
|
|
synchronized(mSetModeDeathHandlers) {
|
|
Log.w(TAG, "setMode() client died");
|
|
int index = mSetModeDeathHandlers.indexOf(this);
|
|
if (index < 0) {
|
|
Log.w(TAG, "unregistered setMode() client died");
|
|
} else {
|
|
newModeOwnerPid = setModeInt(AudioSystem.MODE_NORMAL, mCb, mPid);
|
|
}
|
|
}
|
|
// when entering RINGTONE, IN_CALL or IN_COMMUNICATION mode, clear all
|
|
// SCO connections not started by the application changing the mode
|
|
if (newModeOwnerPid != 0) {
|
|
disconnectBluetoothSco(newModeOwnerPid);
|
|
}
|
|
}
|
|
|
|
public int getPid() {
|
|
return mPid;
|
|
}
|
|
|
|
public void setMode(int mode) {
|
|
mMode = mode;
|
|
}
|
|
|
|
public int getMode() {
|
|
return mMode;
|
|
}
|
|
|
|
public IBinder getBinder() {
|
|
return mCb;
|
|
}
|
|
}
|
|
|
|
/** @see AudioManager#setMode(int) */
|
|
public void setMode(int mode, IBinder cb) {
|
|
if (!checkAudioSettingsPermission("setMode()")) {
|
|
return;
|
|
}
|
|
|
|
if (mode < AudioSystem.MODE_CURRENT || mode >= AudioSystem.NUM_MODES) {
|
|
return;
|
|
}
|
|
|
|
int newModeOwnerPid = 0;
|
|
synchronized(mSetModeDeathHandlers) {
|
|
if (mode == AudioSystem.MODE_CURRENT) {
|
|
mode = mMode;
|
|
}
|
|
newModeOwnerPid = setModeInt(mode, cb, Binder.getCallingPid());
|
|
}
|
|
// when entering RINGTONE, IN_CALL or IN_COMMUNICATION mode, clear all
|
|
// SCO connections not started by the application changing the mode
|
|
if (newModeOwnerPid != 0) {
|
|
disconnectBluetoothSco(newModeOwnerPid);
|
|
}
|
|
}
|
|
|
|
// must be called synchronized on mSetModeDeathHandlers
|
|
// setModeInt() returns a valid PID if the audio mode was successfully set to
|
|
// any mode other than NORMAL.
|
|
int setModeInt(int mode, IBinder cb, int pid) {
|
|
int newModeOwnerPid = 0;
|
|
if (cb == null) {
|
|
Log.e(TAG, "setModeInt() called with null binder");
|
|
return newModeOwnerPid;
|
|
}
|
|
|
|
SetModeDeathHandler hdlr = null;
|
|
Iterator iter = mSetModeDeathHandlers.iterator();
|
|
while (iter.hasNext()) {
|
|
SetModeDeathHandler h = (SetModeDeathHandler)iter.next();
|
|
if (h.getPid() == pid) {
|
|
hdlr = h;
|
|
// Remove from client list so that it is re-inserted at top of list
|
|
iter.remove();
|
|
hdlr.getBinder().unlinkToDeath(hdlr, 0);
|
|
break;
|
|
}
|
|
}
|
|
int status = AudioSystem.AUDIO_STATUS_OK;
|
|
do {
|
|
if (mode == AudioSystem.MODE_NORMAL) {
|
|
// get new mode from client at top the list if any
|
|
if (!mSetModeDeathHandlers.isEmpty()) {
|
|
hdlr = mSetModeDeathHandlers.get(0);
|
|
cb = hdlr.getBinder();
|
|
mode = hdlr.getMode();
|
|
}
|
|
} else {
|
|
if (hdlr == null) {
|
|
hdlr = new SetModeDeathHandler(cb, pid);
|
|
}
|
|
// Register for client death notification
|
|
try {
|
|
cb.linkToDeath(hdlr, 0);
|
|
} catch (RemoteException e) {
|
|
// Client has died!
|
|
Log.w(TAG, "setMode() could not link to "+cb+" binder death");
|
|
}
|
|
|
|
// Last client to call setMode() is always at top of client list
|
|
// as required by SetModeDeathHandler.binderDied()
|
|
mSetModeDeathHandlers.add(0, hdlr);
|
|
hdlr.setMode(mode);
|
|
}
|
|
|
|
if (mode != mMode) {
|
|
status = AudioSystem.setPhoneState(mode);
|
|
if (status == AudioSystem.AUDIO_STATUS_OK) {
|
|
mMode = mode;
|
|
} else {
|
|
if (hdlr != null) {
|
|
mSetModeDeathHandlers.remove(hdlr);
|
|
cb.unlinkToDeath(hdlr, 0);
|
|
}
|
|
// force reading new top of mSetModeDeathHandlers stack
|
|
mode = AudioSystem.MODE_NORMAL;
|
|
}
|
|
} else {
|
|
status = AudioSystem.AUDIO_STATUS_OK;
|
|
}
|
|
} while (status != AudioSystem.AUDIO_STATUS_OK && !mSetModeDeathHandlers.isEmpty());
|
|
|
|
if (status == AudioSystem.AUDIO_STATUS_OK) {
|
|
if (mode != AudioSystem.MODE_NORMAL) {
|
|
if (mSetModeDeathHandlers.isEmpty()) {
|
|
Log.e(TAG, "setMode() different from MODE_NORMAL with empty mode client stack");
|
|
} else {
|
|
newModeOwnerPid = mSetModeDeathHandlers.get(0).getPid();
|
|
}
|
|
}
|
|
int streamType = getActiveStreamType(AudioManager.USE_DEFAULT_STREAM_TYPE);
|
|
if (streamType == STREAM_REMOTE_MUSIC) {
|
|
// here handle remote media playback the same way as local playback
|
|
streamType = AudioManager.STREAM_MUSIC;
|
|
}
|
|
int device = getDeviceForStream(streamType);
|
|
int index = mStreamStates[mStreamVolumeAlias[streamType]].getIndex(device, false);
|
|
setStreamVolumeInt(mStreamVolumeAlias[streamType], index, device, true, false);
|
|
|
|
updateStreamVolumeAlias(true /*updateVolumes*/);
|
|
}
|
|
return newModeOwnerPid;
|
|
}
|
|
|
|
/** @see AudioManager#getMode() */
|
|
public int getMode() {
|
|
return mMode;
|
|
}
|
|
|
|
/** @see AudioManager#playSoundEffect(int) */
|
|
public void playSoundEffect(int effectType) {
|
|
sendMsg(mAudioHandler, MSG_PLAY_SOUND_EFFECT, SENDMSG_NOOP,
|
|
effectType, -1, null, 0);
|
|
}
|
|
|
|
/** @see AudioManager#playSoundEffect(int, float) */
|
|
public void playSoundEffectVolume(int effectType, float volume) {
|
|
loadSoundEffects();
|
|
sendMsg(mAudioHandler, MSG_PLAY_SOUND_EFFECT, SENDMSG_NOOP,
|
|
effectType, (int) (volume * 1000), null, 0);
|
|
}
|
|
|
|
/**
|
|
* Loads samples into the soundpool.
|
|
* This method must be called at first when sound effects are enabled
|
|
*/
|
|
public boolean loadSoundEffects() {
|
|
int status;
|
|
|
|
synchronized (mSoundEffectsLock) {
|
|
if (!mBootCompleted) {
|
|
Log.w(TAG, "loadSoundEffects() called before boot complete");
|
|
return false;
|
|
}
|
|
|
|
if (mSoundPool != null) {
|
|
return true;
|
|
}
|
|
mSoundPool = new SoundPool(NUM_SOUNDPOOL_CHANNELS, AudioSystem.STREAM_SYSTEM, 0);
|
|
|
|
try {
|
|
mSoundPoolCallBack = null;
|
|
mSoundPoolListenerThread = new SoundPoolListenerThread();
|
|
mSoundPoolListenerThread.start();
|
|
// Wait for mSoundPoolCallBack to be set by the other thread
|
|
mSoundEffectsLock.wait();
|
|
} catch (InterruptedException e) {
|
|
Log.w(TAG, "Interrupted while waiting sound pool listener thread.");
|
|
}
|
|
|
|
if (mSoundPoolCallBack == null) {
|
|
Log.w(TAG, "loadSoundEffects() could not create SoundPool listener or thread");
|
|
if (mSoundPoolLooper != null) {
|
|
mSoundPoolLooper.quit();
|
|
mSoundPoolLooper = null;
|
|
}
|
|
mSoundPoolListenerThread = null;
|
|
mSoundPool.release();
|
|
mSoundPool = null;
|
|
return false;
|
|
}
|
|
/*
|
|
* poolId table: The value -1 in this table indicates that corresponding
|
|
* file (same index in SOUND_EFFECT_FILES[] has not been loaded.
|
|
* Once loaded, the value in poolId is the sample ID and the same
|
|
* sample can be reused for another effect using the same file.
|
|
*/
|
|
int[] poolId = new int[SOUND_EFFECT_FILES.length];
|
|
for (int fileIdx = 0; fileIdx < SOUND_EFFECT_FILES.length; fileIdx++) {
|
|
poolId[fileIdx] = -1;
|
|
}
|
|
/*
|
|
* Effects whose value in SOUND_EFFECT_FILES_MAP[effect][1] is -1 must be loaded.
|
|
* If load succeeds, value in SOUND_EFFECT_FILES_MAP[effect][1] is > 0:
|
|
* this indicates we have a valid sample loaded for this effect.
|
|
*/
|
|
|
|
int lastSample = 0;
|
|
for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) {
|
|
// Do not load sample if this effect uses the MediaPlayer
|
|
if (SOUND_EFFECT_FILES_MAP[effect][1] == 0) {
|
|
continue;
|
|
}
|
|
if (poolId[SOUND_EFFECT_FILES_MAP[effect][0]] == -1) {
|
|
String filePath = Environment.getRootDirectory()
|
|
+ SOUND_EFFECTS_PATH
|
|
+ SOUND_EFFECT_FILES[SOUND_EFFECT_FILES_MAP[effect][0]];
|
|
int sampleId = mSoundPool.load(filePath, 0);
|
|
if (sampleId <= 0) {
|
|
Log.w(TAG, "Soundpool could not load file: "+filePath);
|
|
} else {
|
|
SOUND_EFFECT_FILES_MAP[effect][1] = sampleId;
|
|
poolId[SOUND_EFFECT_FILES_MAP[effect][0]] = sampleId;
|
|
lastSample = sampleId;
|
|
}
|
|
} else {
|
|
SOUND_EFFECT_FILES_MAP[effect][1] = poolId[SOUND_EFFECT_FILES_MAP[effect][0]];
|
|
}
|
|
}
|
|
// wait for all samples to be loaded
|
|
if (lastSample != 0) {
|
|
mSoundPoolCallBack.setLastSample(lastSample);
|
|
|
|
try {
|
|
mSoundEffectsLock.wait();
|
|
status = mSoundPoolCallBack.status();
|
|
} catch (java.lang.InterruptedException e) {
|
|
Log.w(TAG, "Interrupted while waiting sound pool callback.");
|
|
status = -1;
|
|
}
|
|
} else {
|
|
status = -1;
|
|
}
|
|
|
|
if (mSoundPoolLooper != null) {
|
|
mSoundPoolLooper.quit();
|
|
mSoundPoolLooper = null;
|
|
}
|
|
mSoundPoolListenerThread = null;
|
|
if (status != 0) {
|
|
Log.w(TAG,
|
|
"loadSoundEffects(), Error "
|
|
+ ((lastSample != 0) ? mSoundPoolCallBack.status() : -1)
|
|
+ " while loading samples");
|
|
for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) {
|
|
if (SOUND_EFFECT_FILES_MAP[effect][1] > 0) {
|
|
SOUND_EFFECT_FILES_MAP[effect][1] = -1;
|
|
}
|
|
}
|
|
|
|
mSoundPool.release();
|
|
mSoundPool = null;
|
|
}
|
|
}
|
|
return (status == 0);
|
|
}
|
|
|
|
/**
|
|
* Unloads samples from the sound pool.
|
|
* This method can be called to free some memory when
|
|
* sound effects are disabled.
|
|
*/
|
|
public void unloadSoundEffects() {
|
|
synchronized (mSoundEffectsLock) {
|
|
if (mSoundPool == null) {
|
|
return;
|
|
}
|
|
|
|
mAudioHandler.removeMessages(MSG_LOAD_SOUND_EFFECTS);
|
|
mAudioHandler.removeMessages(MSG_PLAY_SOUND_EFFECT);
|
|
|
|
int[] poolId = new int[SOUND_EFFECT_FILES.length];
|
|
for (int fileIdx = 0; fileIdx < SOUND_EFFECT_FILES.length; fileIdx++) {
|
|
poolId[fileIdx] = 0;
|
|
}
|
|
|
|
for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) {
|
|
if (SOUND_EFFECT_FILES_MAP[effect][1] <= 0) {
|
|
continue;
|
|
}
|
|
if (poolId[SOUND_EFFECT_FILES_MAP[effect][0]] == 0) {
|
|
mSoundPool.unload(SOUND_EFFECT_FILES_MAP[effect][1]);
|
|
SOUND_EFFECT_FILES_MAP[effect][1] = -1;
|
|
poolId[SOUND_EFFECT_FILES_MAP[effect][0]] = -1;
|
|
}
|
|
}
|
|
mSoundPool.release();
|
|
mSoundPool = null;
|
|
}
|
|
}
|
|
|
|
class SoundPoolListenerThread extends Thread {
|
|
public SoundPoolListenerThread() {
|
|
super("SoundPoolListenerThread");
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
|
|
Looper.prepare();
|
|
mSoundPoolLooper = Looper.myLooper();
|
|
|
|
synchronized (mSoundEffectsLock) {
|
|
if (mSoundPool != null) {
|
|
mSoundPoolCallBack = new SoundPoolCallback();
|
|
mSoundPool.setOnLoadCompleteListener(mSoundPoolCallBack);
|
|
}
|
|
mSoundEffectsLock.notify();
|
|
}
|
|
Looper.loop();
|
|
}
|
|
}
|
|
|
|
private final class SoundPoolCallback implements
|
|
android.media.SoundPool.OnLoadCompleteListener {
|
|
|
|
int mStatus;
|
|
int mLastSample;
|
|
|
|
public int status() {
|
|
return mStatus;
|
|
}
|
|
|
|
public void setLastSample(int sample) {
|
|
mLastSample = sample;
|
|
}
|
|
|
|
public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
|
|
synchronized (mSoundEffectsLock) {
|
|
if (status != 0) {
|
|
mStatus = status;
|
|
}
|
|
if (sampleId == mLastSample) {
|
|
mSoundEffectsLock.notify();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @see AudioManager#reloadAudioSettings() */
|
|
public void reloadAudioSettings() {
|
|
// restore ringer mode, ringer mode affected streams, mute affected streams and vibrate settings
|
|
readPersistedSettings();
|
|
|
|
// restore volume settings
|
|
int numStreamTypes = AudioSystem.getNumStreamTypes();
|
|
for (int streamType = 0; streamType < numStreamTypes; streamType++) {
|
|
VolumeStreamState streamState = mStreamStates[streamType];
|
|
|
|
synchronized (streamState) {
|
|
streamState.readSettings();
|
|
|
|
// unmute stream that was muted but is not affect by mute anymore
|
|
if (streamState.muteCount() != 0 && !isStreamAffectedByMute(streamType)) {
|
|
int size = streamState.mDeathHandlers.size();
|
|
for (int i = 0; i < size; i++) {
|
|
streamState.mDeathHandlers.get(i).mMuteCount = 1;
|
|
streamState.mDeathHandlers.get(i).mute(false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
checkAllAliasStreamVolumes();
|
|
|
|
synchronized (mSafeMediaVolumeEnabled) {
|
|
enforceSafeMediaVolume();
|
|
}
|
|
|
|
// apply new ringer mode
|
|
setRingerModeInt(getRingerMode(), false);
|
|
}
|
|
|
|
/** @see AudioManager#setSpeakerphoneOn() */
|
|
public void setSpeakerphoneOn(boolean on){
|
|
if (!checkAudioSettingsPermission("setSpeakerphoneOn()")) {
|
|
return;
|
|
}
|
|
mForcedUseForComm = on ? AudioSystem.FORCE_SPEAKER : AudioSystem.FORCE_NONE;
|
|
|
|
sendMsg(mAudioHandler, MSG_SET_FORCE_USE, SENDMSG_QUEUE,
|
|
AudioSystem.FOR_COMMUNICATION, mForcedUseForComm, null, 0);
|
|
}
|
|
|
|
/** @see AudioManager#isSpeakerphoneOn() */
|
|
public boolean isSpeakerphoneOn() {
|
|
return (mForcedUseForComm == AudioSystem.FORCE_SPEAKER);
|
|
}
|
|
|
|
/** @see AudioManager#setBluetoothScoOn() */
|
|
public void setBluetoothScoOn(boolean on){
|
|
if (!checkAudioSettingsPermission("setBluetoothScoOn()")) {
|
|
return;
|
|
}
|
|
mForcedUseForComm = on ? AudioSystem.FORCE_BT_SCO : AudioSystem.FORCE_NONE;
|
|
|
|
sendMsg(mAudioHandler, MSG_SET_FORCE_USE, SENDMSG_QUEUE,
|
|
AudioSystem.FOR_COMMUNICATION, mForcedUseForComm, null, 0);
|
|
sendMsg(mAudioHandler, MSG_SET_FORCE_USE, SENDMSG_QUEUE,
|
|
AudioSystem.FOR_RECORD, mForcedUseForComm, null, 0);
|
|
}
|
|
|
|
/** @see AudioManager#isBluetoothScoOn() */
|
|
public boolean isBluetoothScoOn() {
|
|
return (mForcedUseForComm == AudioSystem.FORCE_BT_SCO);
|
|
}
|
|
|
|
/** @see AudioManager#setBluetoothA2dpOn() */
|
|
public void setBluetoothA2dpOn(boolean on) {
|
|
synchronized (mBluetoothA2dpEnabledLock) {
|
|
mBluetoothA2dpEnabled = on;
|
|
sendMsg(mAudioHandler, MSG_SET_FORCE_BT_A2DP_USE, SENDMSG_QUEUE,
|
|
AudioSystem.FOR_MEDIA,
|
|
mBluetoothA2dpEnabled ? AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP,
|
|
null, 0);
|
|
}
|
|
}
|
|
|
|
/** @see AudioManager#isBluetoothA2dpOn() */
|
|
public boolean isBluetoothA2dpOn() {
|
|
synchronized (mBluetoothA2dpEnabledLock) {
|
|
return mBluetoothA2dpEnabled;
|
|
}
|
|
}
|
|
|
|
/** @see AudioManager#startBluetoothSco() */
|
|
public void startBluetoothSco(IBinder cb){
|
|
if (!checkAudioSettingsPermission("startBluetoothSco()") ||
|
|
!mBootCompleted) {
|
|
return;
|
|
}
|
|
ScoClient client = getScoClient(cb, true);
|
|
client.incCount();
|
|
}
|
|
|
|
/** @see AudioManager#stopBluetoothSco() */
|
|
public void stopBluetoothSco(IBinder cb){
|
|
if (!checkAudioSettingsPermission("stopBluetoothSco()") ||
|
|
!mBootCompleted) {
|
|
return;
|
|
}
|
|
ScoClient client = getScoClient(cb, false);
|
|
if (client != null) {
|
|
client.decCount();
|
|
}
|
|
}
|
|
|
|
|
|
private class ScoClient implements IBinder.DeathRecipient {
|
|
private IBinder mCb; // To be notified of client's death
|
|
private int mCreatorPid;
|
|
private int mStartcount; // number of SCO connections started by this client
|
|
|
|
ScoClient(IBinder cb) {
|
|
mCb = cb;
|
|
mCreatorPid = Binder.getCallingPid();
|
|
mStartcount = 0;
|
|
}
|
|
|
|
public void binderDied() {
|
|
synchronized(mScoClients) {
|
|
Log.w(TAG, "SCO client died");
|
|
int index = mScoClients.indexOf(this);
|
|
if (index < 0) {
|
|
Log.w(TAG, "unregistered SCO client died");
|
|
} else {
|
|
clearCount(true);
|
|
mScoClients.remove(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void incCount() {
|
|
synchronized(mScoClients) {
|
|
requestScoState(BluetoothHeadset.STATE_AUDIO_CONNECTED);
|
|
if (mStartcount == 0) {
|
|
try {
|
|
mCb.linkToDeath(this, 0);
|
|
} catch (RemoteException e) {
|
|
// client has already died!
|
|
Log.w(TAG, "ScoClient incCount() could not link to "+mCb+" binder death");
|
|
}
|
|
}
|
|
mStartcount++;
|
|
}
|
|
}
|
|
|
|
public void decCount() {
|
|
synchronized(mScoClients) {
|
|
if (mStartcount == 0) {
|
|
Log.w(TAG, "ScoClient.decCount() already 0");
|
|
} else {
|
|
mStartcount--;
|
|
if (mStartcount == 0) {
|
|
try {
|
|
mCb.unlinkToDeath(this, 0);
|
|
} catch (NoSuchElementException e) {
|
|
Log.w(TAG, "decCount() going to 0 but not registered to binder");
|
|
}
|
|
}
|
|
requestScoState(BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void clearCount(boolean stopSco) {
|
|
synchronized(mScoClients) {
|
|
if (mStartcount != 0) {
|
|
try {
|
|
mCb.unlinkToDeath(this, 0);
|
|
} catch (NoSuchElementException e) {
|
|
Log.w(TAG, "clearCount() mStartcount: "+mStartcount+" != 0 but not registered to binder");
|
|
}
|
|
}
|
|
mStartcount = 0;
|
|
if (stopSco) {
|
|
requestScoState(BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
|
|
}
|
|
}
|
|
}
|
|
|
|
public int getCount() {
|
|
return mStartcount;
|
|
}
|
|
|
|
public IBinder getBinder() {
|
|
return mCb;
|
|
}
|
|
|
|
public int getPid() {
|
|
return mCreatorPid;
|
|
}
|
|
|
|
public int totalCount() {
|
|
synchronized(mScoClients) {
|
|
int count = 0;
|
|
int size = mScoClients.size();
|
|
for (int i = 0; i < size; i++) {
|
|
count += mScoClients.get(i).getCount();
|
|
}
|
|
return count;
|
|
}
|
|
}
|
|
|
|
private void requestScoState(int state) {
|
|
checkScoAudioState();
|
|
if (totalCount() == 0) {
|
|
if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) {
|
|
// Make sure that the state transitions to CONNECTING even if we cannot initiate
|
|
// the connection.
|
|
broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTING);
|
|
// Accept SCO audio activation only in NORMAL audio mode or if the mode is
|
|
// currently controlled by the same client process.
|
|
synchronized(mSetModeDeathHandlers) {
|
|
if ((mSetModeDeathHandlers.isEmpty() ||
|
|
mSetModeDeathHandlers.get(0).getPid() == mCreatorPid) &&
|
|
(mScoAudioState == SCO_STATE_INACTIVE ||
|
|
mScoAudioState == SCO_STATE_DEACTIVATE_REQ)) {
|
|
if (mScoAudioState == SCO_STATE_INACTIVE) {
|
|
if (mBluetoothHeadset != null && mBluetoothHeadsetDevice != null) {
|
|
if (mBluetoothHeadset.startScoUsingVirtualVoiceCall(
|
|
mBluetoothHeadsetDevice)) {
|
|
mScoAudioState = SCO_STATE_ACTIVE_INTERNAL;
|
|
} else {
|
|
broadcastScoConnectionState(
|
|
AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
|
|
}
|
|
} else if (getBluetoothHeadset()) {
|
|
mScoAudioState = SCO_STATE_ACTIVATE_REQ;
|
|
}
|
|
} else {
|
|
mScoAudioState = SCO_STATE_ACTIVE_INTERNAL;
|
|
broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTED);
|
|
}
|
|
} else {
|
|
broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
|
|
}
|
|
}
|
|
} else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED &&
|
|
(mScoAudioState == SCO_STATE_ACTIVE_INTERNAL ||
|
|
mScoAudioState == SCO_STATE_ACTIVATE_REQ)) {
|
|
if (mScoAudioState == SCO_STATE_ACTIVE_INTERNAL) {
|
|
if (mBluetoothHeadset != null && mBluetoothHeadsetDevice != null) {
|
|
if (!mBluetoothHeadset.stopScoUsingVirtualVoiceCall(
|
|
mBluetoothHeadsetDevice)) {
|
|
mScoAudioState = SCO_STATE_INACTIVE;
|
|
broadcastScoConnectionState(
|
|
AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
|
|
}
|
|
} else if (getBluetoothHeadset()) {
|
|
mScoAudioState = SCO_STATE_DEACTIVATE_REQ;
|
|
}
|
|
} else {
|
|
mScoAudioState = SCO_STATE_INACTIVE;
|
|
broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void checkScoAudioState() {
|
|
if (mBluetoothHeadset != null && mBluetoothHeadsetDevice != null &&
|
|
mScoAudioState == SCO_STATE_INACTIVE &&
|
|
mBluetoothHeadset.getAudioState(mBluetoothHeadsetDevice)
|
|
!= BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
|
|
mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;
|
|
}
|
|
}
|
|
|
|
private ScoClient getScoClient(IBinder cb, boolean create) {
|
|
synchronized(mScoClients) {
|
|
ScoClient client = null;
|
|
int size = mScoClients.size();
|
|
for (int i = 0; i < size; i++) {
|
|
client = mScoClients.get(i);
|
|
if (client.getBinder() == cb)
|
|
return client;
|
|
}
|
|
if (create) {
|
|
client = new ScoClient(cb);
|
|
mScoClients.add(client);
|
|
}
|
|
return client;
|
|
}
|
|
}
|
|
|
|
public void clearAllScoClients(int exceptPid, boolean stopSco) {
|
|
synchronized(mScoClients) {
|
|
ScoClient savedClient = null;
|
|
int size = mScoClients.size();
|
|
for (int i = 0; i < size; i++) {
|
|
ScoClient cl = mScoClients.get(i);
|
|
if (cl.getPid() != exceptPid) {
|
|
cl.clearCount(stopSco);
|
|
} else {
|
|
savedClient = cl;
|
|
}
|
|
}
|
|
mScoClients.clear();
|
|
if (savedClient != null) {
|
|
mScoClients.add(savedClient);
|
|
}
|
|
}
|
|
}
|
|
|
|
private boolean getBluetoothHeadset() {
|
|
boolean result = false;
|
|
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
|
|
if (adapter != null) {
|
|
result = adapter.getProfileProxy(mContext, mBluetoothProfileServiceListener,
|
|
BluetoothProfile.HEADSET);
|
|
}
|
|
// If we could not get a bluetooth headset proxy, send a failure message
|
|
// without delay to reset the SCO audio state and clear SCO clients.
|
|
// If we could get a proxy, send a delayed failure message that will reset our state
|
|
// in case we don't receive onServiceConnected().
|
|
sendMsg(mAudioHandler, MSG_BT_HEADSET_CNCT_FAILED,
|
|
SENDMSG_REPLACE, 0, 0, null, result ? BT_HEADSET_CNCT_TIMEOUT_MS : 0);
|
|
return result;
|
|
}
|
|
|
|
private void disconnectBluetoothSco(int exceptPid) {
|
|
synchronized(mScoClients) {
|
|
checkScoAudioState();
|
|
if (mScoAudioState == SCO_STATE_ACTIVE_EXTERNAL ||
|
|
mScoAudioState == SCO_STATE_DEACTIVATE_EXT_REQ) {
|
|
if (mBluetoothHeadsetDevice != null) {
|
|
if (mBluetoothHeadset != null) {
|
|
if (!mBluetoothHeadset.stopVoiceRecognition(
|
|
mBluetoothHeadsetDevice)) {
|
|
sendMsg(mAudioHandler, MSG_BT_HEADSET_CNCT_FAILED,
|
|
SENDMSG_REPLACE, 0, 0, null, 0);
|
|
}
|
|
} else if (mScoAudioState == SCO_STATE_ACTIVE_EXTERNAL &&
|
|
getBluetoothHeadset()) {
|
|
mScoAudioState = SCO_STATE_DEACTIVATE_EXT_REQ;
|
|
}
|
|
}
|
|
} else {
|
|
clearAllScoClients(exceptPid, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void resetBluetoothSco() {
|
|
synchronized(mScoClients) {
|
|
clearAllScoClients(0, false);
|
|
mScoAudioState = SCO_STATE_INACTIVE;
|
|
broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
|
|
}
|
|
}
|
|
|
|
private void broadcastScoConnectionState(int state) {
|
|
if (state != mScoConnectionState) {
|
|
Intent newIntent = new Intent(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED);
|
|
newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, state);
|
|
newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_PREVIOUS_STATE,
|
|
mScoConnectionState);
|
|
sendStickyBroadcastToAll(newIntent);
|
|
mScoConnectionState = state;
|
|
}
|
|
}
|
|
|
|
private BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
|
|
new BluetoothProfile.ServiceListener() {
|
|
public void onServiceConnected(int profile, BluetoothProfile proxy) {
|
|
BluetoothDevice btDevice;
|
|
List<BluetoothDevice> deviceList;
|
|
switch(profile) {
|
|
case BluetoothProfile.A2DP:
|
|
BluetoothA2dp a2dp = (BluetoothA2dp) proxy;
|
|
deviceList = a2dp.getConnectedDevices();
|
|
if (deviceList.size() > 0) {
|
|
btDevice = deviceList.get(0);
|
|
synchronized (mConnectedDevices) {
|
|
int state = a2dp.getConnectionState(btDevice);
|
|
int delay = checkSendBecomingNoisyIntent(
|
|
AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
|
|
(state == BluetoothA2dp.STATE_CONNECTED) ? 1 : 0);
|
|
queueMsgUnderWakeLock(mAudioHandler,
|
|
MSG_SET_A2DP_CONNECTION_STATE,
|
|
state,
|
|
0,
|
|
btDevice,
|
|
delay);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case BluetoothProfile.HEADSET:
|
|
synchronized (mScoClients) {
|
|
// Discard timeout message
|
|
mAudioHandler.removeMessages(MSG_BT_HEADSET_CNCT_FAILED);
|
|
mBluetoothHeadset = (BluetoothHeadset) proxy;
|
|
deviceList = mBluetoothHeadset.getConnectedDevices();
|
|
if (deviceList.size() > 0) {
|
|
mBluetoothHeadsetDevice = deviceList.get(0);
|
|
} else {
|
|
mBluetoothHeadsetDevice = null;
|
|
}
|
|
// Refresh SCO audio state
|
|
checkScoAudioState();
|
|
// Continue pending action if any
|
|
if (mScoAudioState == SCO_STATE_ACTIVATE_REQ ||
|
|
mScoAudioState == SCO_STATE_DEACTIVATE_REQ ||
|
|
mScoAudioState == SCO_STATE_DEACTIVATE_EXT_REQ) {
|
|
boolean status = false;
|
|
if (mBluetoothHeadsetDevice != null) {
|
|
switch (mScoAudioState) {
|
|
case SCO_STATE_ACTIVATE_REQ:
|
|
mScoAudioState = SCO_STATE_ACTIVE_INTERNAL;
|
|
status = mBluetoothHeadset.startScoUsingVirtualVoiceCall(
|
|
mBluetoothHeadsetDevice);
|
|
break;
|
|
case SCO_STATE_DEACTIVATE_REQ:
|
|
status = mBluetoothHeadset.stopScoUsingVirtualVoiceCall(
|
|
mBluetoothHeadsetDevice);
|
|
break;
|
|
case SCO_STATE_DEACTIVATE_EXT_REQ:
|
|
status = mBluetoothHeadset.stopVoiceRecognition(
|
|
mBluetoothHeadsetDevice);
|
|
}
|
|
}
|
|
if (!status) {
|
|
sendMsg(mAudioHandler, MSG_BT_HEADSET_CNCT_FAILED,
|
|
SENDMSG_REPLACE, 0, 0, null, 0);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
public void onServiceDisconnected(int profile) {
|
|
switch(profile) {
|
|
case BluetoothProfile.A2DP:
|
|
synchronized (mConnectedDevices) {
|
|
if (mConnectedDevices.containsKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP)) {
|
|
makeA2dpDeviceUnavailableNow(
|
|
mConnectedDevices.get(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP));
|
|
}
|
|
}
|
|
break;
|
|
|
|
case BluetoothProfile.HEADSET:
|
|
synchronized (mScoClients) {
|
|
mBluetoothHeadset = null;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
/** see AudioManager.setRemoteSubmixOn(boolean on) */
|
|
public void setRemoteSubmixOn(boolean on, int address) {
|
|
sendMsg(mAudioHandler, MSG_SET_RSX_CONNECTION_STATE,
|
|
SENDMSG_REPLACE /* replace with QUEUE when multiple addresses are supported */,
|
|
on ? 1 : 0 /*arg1*/,
|
|
address /*arg2*/,
|
|
null/*obj*/, 0/*delay*/);
|
|
|
|
// Note that we are currently forcing use of remote submix as soon as corresponding device
|
|
// is made available
|
|
sendMsg(mAudioHandler, MSG_SET_FORCE_RSX_USE, SENDMSG_REPLACE,
|
|
AudioSystem.FOR_MEDIA,
|
|
on ? AudioSystem.FORCE_REMOTE_SUBMIX : AudioSystem.FORCE_NONE,
|
|
null/*obj*/, 0/*delay*/);
|
|
}
|
|
|
|
private void onSetRsxConnectionState(int available, int address) {
|
|
AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_IN_REMOTE_SUBMIX,
|
|
available == 1 ?
|
|
AudioSystem.DEVICE_STATE_AVAILABLE : AudioSystem.DEVICE_STATE_UNAVAILABLE,
|
|
String.valueOf(address) /*device_address*/);
|
|
AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_REMOTE_SUBMIX,
|
|
available == 1 ?
|
|
AudioSystem.DEVICE_STATE_AVAILABLE : AudioSystem.DEVICE_STATE_UNAVAILABLE,
|
|
String.valueOf(address) /*device_address*/);
|
|
}
|
|
|
|
private void onCheckMusicActive() {
|
|
synchronized (mSafeMediaVolumeEnabled) {
|
|
if (!mSafeMediaVolumeEnabled) {
|
|
int device = getDeviceForStream(AudioSystem.STREAM_MUSIC);
|
|
|
|
if ((device & mSafeMediaVolumeDevices) != 0) {
|
|
sendMsg(mAudioHandler,
|
|
MSG_CHECK_MUSIC_ACTIVE,
|
|
SENDMSG_REPLACE,
|
|
device,
|
|
0,
|
|
null,
|
|
MUSIC_ACTIVE_POLL_PERIOD_MS);
|
|
if (AudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0)) {
|
|
// Approximate cumulative active music time
|
|
mMusicActiveMs += MUSIC_ACTIVE_POLL_PERIOD_MS;
|
|
if (mMusicActiveMs > UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX) {
|
|
setSafeMediaVolumeEnabled(true);
|
|
mMusicActiveMs = 0;
|
|
mVolumePanel.postDisplaySafeVolumeWarning();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
// Internal methods
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* Checks if the adjustment should change ringer mode instead of just
|
|
* adjusting volume. If so, this will set the proper ringer mode and volume
|
|
* indices on the stream states.
|
|
*/
|
|
private boolean checkForRingerModeChange(int oldIndex, int direction, int step) {
|
|
boolean adjustVolumeIndex = true;
|
|
int ringerMode = getRingerMode();
|
|
|
|
switch (ringerMode) {
|
|
case RINGER_MODE_NORMAL:
|
|
if (direction == AudioManager.ADJUST_LOWER) {
|
|
if (mHasVibrator) {
|
|
// "step" is the delta in internal index units corresponding to a
|
|
// change of 1 in UI index units.
|
|
// Because of rounding when rescaling from one stream index range to its alias
|
|
// index range, we cannot simply test oldIndex == step:
|
|
// (step <= oldIndex < 2 * step) is equivalent to: (old UI index == 1)
|
|
if (step <= oldIndex && oldIndex < 2 * step) {
|
|
ringerMode = RINGER_MODE_VIBRATE;
|
|
}
|
|
} else {
|
|
// (oldIndex < step) is equivalent to (old UI index == 0)
|
|
if ((oldIndex < step) && mPrevVolDirection != AudioManager.ADJUST_LOWER) {
|
|
ringerMode = RINGER_MODE_SILENT;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case RINGER_MODE_VIBRATE:
|
|
if (!mHasVibrator) {
|
|
Log.e(TAG, "checkForRingerModeChange() current ringer mode is vibrate" +
|
|
"but no vibrator is present");
|
|
break;
|
|
}
|
|
if ((direction == AudioManager.ADJUST_LOWER)) {
|
|
if (mPrevVolDirection != AudioManager.ADJUST_LOWER) {
|
|
ringerMode = RINGER_MODE_SILENT;
|
|
}
|
|
} else if (direction == AudioManager.ADJUST_RAISE) {
|
|
ringerMode = RINGER_MODE_NORMAL;
|
|
}
|
|
adjustVolumeIndex = false;
|
|
break;
|
|
case RINGER_MODE_SILENT:
|
|
if (direction == AudioManager.ADJUST_RAISE) {
|
|
if (mHasVibrator) {
|
|
ringerMode = RINGER_MODE_VIBRATE;
|
|
} else {
|
|
ringerMode = RINGER_MODE_NORMAL;
|
|
}
|
|
}
|
|
adjustVolumeIndex = false;
|
|
break;
|
|
default:
|
|
Log.e(TAG, "checkForRingerModeChange() wrong ringer mode: "+ringerMode);
|
|
break;
|
|
}
|
|
|
|
setRingerMode(ringerMode);
|
|
|
|
mPrevVolDirection = direction;
|
|
|
|
return adjustVolumeIndex;
|
|
}
|
|
|
|
public boolean isStreamAffectedByRingerMode(int streamType) {
|
|
return (mRingerModeAffectedStreams & (1 << streamType)) != 0;
|
|
}
|
|
|
|
private boolean isStreamMutedByRingerMode(int streamType) {
|
|
return (mRingerModeMutedStreams & (1 << streamType)) != 0;
|
|
}
|
|
|
|
public boolean isStreamAffectedByMute(int streamType) {
|
|
return (mMuteAffectedStreams & (1 << streamType)) != 0;
|
|
}
|
|
|
|
private void ensureValidDirection(int direction) {
|
|
if (direction < AudioManager.ADJUST_LOWER || direction > AudioManager.ADJUST_RAISE) {
|
|
throw new IllegalArgumentException("Bad direction " + direction);
|
|
}
|
|
}
|
|
|
|
private void ensureValidSteps(int steps) {
|
|
if (Math.abs(steps) > MAX_BATCH_VOLUME_ADJUST_STEPS) {
|
|
throw new IllegalArgumentException("Bad volume adjust steps " + steps);
|
|
}
|
|
}
|
|
|
|
private void ensureValidStreamType(int streamType) {
|
|
if (streamType < 0 || streamType >= mStreamStates.length) {
|
|
throw new IllegalArgumentException("Bad stream type " + streamType);
|
|
}
|
|
}
|
|
|
|
private boolean isInCommunication() {
|
|
boolean isOffhook = false;
|
|
|
|
if (mVoiceCapable) {
|
|
try {
|
|
ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone"));
|
|
if (phone != null) isOffhook = phone.isOffhook();
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "Couldn't connect to phone service", e);
|
|
}
|
|
}
|
|
return (isOffhook || getMode() == AudioManager.MODE_IN_COMMUNICATION);
|
|
}
|
|
|
|
private int getActiveStreamType(int suggestedStreamType) {
|
|
if (mVoiceCapable) {
|
|
if (isInCommunication()) {
|
|
if (AudioSystem.getForceUse(AudioSystem.FOR_COMMUNICATION)
|
|
== AudioSystem.FORCE_BT_SCO) {
|
|
// Log.v(TAG, "getActiveStreamType: Forcing STREAM_BLUETOOTH_SCO...");
|
|
return AudioSystem.STREAM_BLUETOOTH_SCO;
|
|
} else {
|
|
// Log.v(TAG, "getActiveStreamType: Forcing STREAM_VOICE_CALL...");
|
|
return AudioSystem.STREAM_VOICE_CALL;
|
|
}
|
|
} else if (suggestedStreamType == AudioManager.USE_DEFAULT_STREAM_TYPE) {
|
|
// Having the suggested stream be USE_DEFAULT_STREAM_TYPE is how remote control
|
|
// volume can have priority over STREAM_MUSIC
|
|
if (checkUpdateRemoteStateIfActive(AudioSystem.STREAM_MUSIC)) {
|
|
if (DEBUG_VOL)
|
|
Log.v(TAG, "getActiveStreamType: Forcing STREAM_REMOTE_MUSIC");
|
|
return STREAM_REMOTE_MUSIC;
|
|
} else if (AudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0)) {
|
|
if (DEBUG_VOL)
|
|
Log.v(TAG, "getActiveStreamType: Forcing STREAM_MUSIC stream active");
|
|
return AudioSystem.STREAM_MUSIC;
|
|
} else {
|
|
if (DEBUG_VOL)
|
|
Log.v(TAG, "getActiveStreamType: Forcing STREAM_RING b/c default");
|
|
return AudioSystem.STREAM_RING;
|
|
}
|
|
} else if (AudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0)) {
|
|
if (DEBUG_VOL)
|
|
Log.v(TAG, "getActiveStreamType: Forcing STREAM_MUSIC stream active");
|
|
return AudioSystem.STREAM_MUSIC;
|
|
} else {
|
|
if (DEBUG_VOL) Log.v(TAG, "getActiveStreamType: Returning suggested type "
|
|
+ suggestedStreamType);
|
|
return suggestedStreamType;
|
|
}
|
|
} else {
|
|
if (isInCommunication()) {
|
|
if (AudioSystem.getForceUse(AudioSystem.FOR_COMMUNICATION)
|
|
== AudioSystem.FORCE_BT_SCO) {
|
|
if (DEBUG_VOL) Log.v(TAG, "getActiveStreamType: Forcing STREAM_BLUETOOTH_SCO");
|
|
return AudioSystem.STREAM_BLUETOOTH_SCO;
|
|
} else {
|
|
if (DEBUG_VOL) Log.v(TAG, "getActiveStreamType: Forcing STREAM_VOICE_CALL");
|
|
return AudioSystem.STREAM_VOICE_CALL;
|
|
}
|
|
} else if (AudioSystem.isStreamActive(AudioSystem.STREAM_NOTIFICATION,
|
|
NOTIFICATION_VOLUME_DELAY_MS) ||
|
|
AudioSystem.isStreamActive(AudioSystem.STREAM_RING,
|
|
NOTIFICATION_VOLUME_DELAY_MS)) {
|
|
if (DEBUG_VOL) Log.v(TAG, "getActiveStreamType: Forcing STREAM_NOTIFICATION");
|
|
return AudioSystem.STREAM_NOTIFICATION;
|
|
} else if (suggestedStreamType == AudioManager.USE_DEFAULT_STREAM_TYPE) {
|
|
if (checkUpdateRemoteStateIfActive(AudioSystem.STREAM_MUSIC)) {
|
|
// Having the suggested stream be USE_DEFAULT_STREAM_TYPE is how remote control
|
|
// volume can have priority over STREAM_MUSIC
|
|
if (DEBUG_VOL) Log.v(TAG, "getActiveStreamType: Forcing STREAM_REMOTE_MUSIC");
|
|
return STREAM_REMOTE_MUSIC;
|
|
} else {
|
|
if (DEBUG_VOL)
|
|
Log.v(TAG, "getActiveStreamType: using STREAM_MUSIC as default");
|
|
return AudioSystem.STREAM_MUSIC;
|
|
}
|
|
} else {
|
|
if (DEBUG_VOL) Log.v(TAG, "getActiveStreamType: Returning suggested type "
|
|
+ suggestedStreamType);
|
|
return suggestedStreamType;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void broadcastRingerMode(int ringerMode) {
|
|
// Send sticky broadcast
|
|
Intent broadcast = new Intent(AudioManager.RINGER_MODE_CHANGED_ACTION);
|
|
broadcast.putExtra(AudioManager.EXTRA_RINGER_MODE, ringerMode);
|
|
broadcast.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
|
|
| Intent.FLAG_RECEIVER_REPLACE_PENDING);
|
|
sendStickyBroadcastToAll(broadcast);
|
|
}
|
|
|
|
private void broadcastVibrateSetting(int vibrateType) {
|
|
// Send broadcast
|
|
if (ActivityManagerNative.isSystemReady()) {
|
|
Intent broadcast = new Intent(AudioManager.VIBRATE_SETTING_CHANGED_ACTION);
|
|
broadcast.putExtra(AudioManager.EXTRA_VIBRATE_TYPE, vibrateType);
|
|
broadcast.putExtra(AudioManager.EXTRA_VIBRATE_SETTING, getVibrateSetting(vibrateType));
|
|
sendBroadcastToAll(broadcast);
|
|
}
|
|
}
|
|
|
|
// Message helper methods
|
|
/**
|
|
* Queue a message on the given handler's message queue, after acquiring the service wake lock.
|
|
* Note that the wake lock needs to be released after the message has been handled.
|
|
*/
|
|
private void queueMsgUnderWakeLock(Handler handler, int msg,
|
|
int arg1, int arg2, Object obj, int delay) {
|
|
mMediaEventWakeLock.acquire();
|
|
sendMsg(handler, msg, SENDMSG_QUEUE, arg1, arg2, obj, delay);
|
|
}
|
|
|
|
private static void sendMsg(Handler handler, int msg,
|
|
int existingMsgPolicy, int arg1, int arg2, Object obj, int delay) {
|
|
|
|
if (existingMsgPolicy == SENDMSG_REPLACE) {
|
|
handler.removeMessages(msg);
|
|
} else if (existingMsgPolicy == SENDMSG_NOOP && handler.hasMessages(msg)) {
|
|
return;
|
|
}
|
|
|
|
handler.sendMessageDelayed(handler.obtainMessage(msg, arg1, arg2, obj), delay);
|
|
}
|
|
|
|
boolean checkAudioSettingsPermission(String method) {
|
|
if (mContext.checkCallingOrSelfPermission("android.permission.MODIFY_AUDIO_SETTINGS")
|
|
== PackageManager.PERMISSION_GRANTED) {
|
|
return true;
|
|
}
|
|
String msg = "Audio Settings Permission Denial: " + method + " from pid="
|
|
+ Binder.getCallingPid()
|
|
+ ", uid=" + Binder.getCallingUid();
|
|
Log.w(TAG, msg);
|
|
return false;
|
|
}
|
|
|
|
private int getDeviceForStream(int stream) {
|
|
int device = AudioSystem.getDevicesForStream(stream);
|
|
if ((device & (device - 1)) != 0) {
|
|
// Multiple device selection is either:
|
|
// - speaker + one other device: give priority to speaker in this case.
|
|
// - one A2DP device + another device: happens with duplicated output. In this case
|
|
// retain the device on the A2DP output as the other must not correspond to an active
|
|
// selection if not the speaker.
|
|
if ((device & AudioSystem.DEVICE_OUT_SPEAKER) != 0) {
|
|
device = AudioSystem.DEVICE_OUT_SPEAKER;
|
|
} else {
|
|
device &= AudioSystem.DEVICE_OUT_ALL_A2DP;
|
|
}
|
|
}
|
|
return device;
|
|
}
|
|
|
|
public void setWiredDeviceConnectionState(int device, int state, String name) {
|
|
synchronized (mConnectedDevices) {
|
|
int delay = checkSendBecomingNoisyIntent(device, state);
|
|
if ((device & mSafeMediaVolumeDevices) != 0) {
|
|
setSafeMediaVolumeEnabled(state != 0);
|
|
// insert delay to allow new volume to apply before switching to headphones
|
|
if ((delay < SAFE_VOLUME_DELAY_MS) && (state != 0)) {
|
|
delay = SAFE_VOLUME_DELAY_MS;
|
|
}
|
|
}
|
|
|
|
queueMsgUnderWakeLock(mAudioHandler,
|
|
MSG_SET_WIRED_DEVICE_CONNECTION_STATE,
|
|
device,
|
|
state,
|
|
name,
|
|
delay);
|
|
}
|
|
}
|
|
|
|
public int setBluetoothA2dpDeviceConnectionState(BluetoothDevice device, int state)
|
|
{
|
|
int delay;
|
|
synchronized (mConnectedDevices) {
|
|
delay = checkSendBecomingNoisyIntent(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
|
|
(state == BluetoothA2dp.STATE_CONNECTED) ? 1 : 0);
|
|
queueMsgUnderWakeLock(mAudioHandler,
|
|
MSG_SET_A2DP_CONNECTION_STATE,
|
|
state,
|
|
0,
|
|
device,
|
|
delay);
|
|
}
|
|
return delay;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
// Inner classes
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
public class VolumeStreamState {
|
|
private final int mStreamType;
|
|
|
|
private String mVolumeIndexSettingName;
|
|
private String mLastAudibleVolumeIndexSettingName;
|
|
private int mIndexMax;
|
|
private final ConcurrentHashMap<Integer, Integer> mIndex =
|
|
new ConcurrentHashMap<Integer, Integer>(8, 0.75f, 4);
|
|
private final ConcurrentHashMap<Integer, Integer> mLastAudibleIndex =
|
|
new ConcurrentHashMap<Integer, Integer>(8, 0.75f, 4);
|
|
private ArrayList<VolumeDeathHandler> mDeathHandlers; //handles mute/solo clients death
|
|
|
|
private VolumeStreamState(String settingName, int streamType) {
|
|
|
|
mVolumeIndexSettingName = settingName;
|
|
mLastAudibleVolumeIndexSettingName = settingName + System.APPEND_FOR_LAST_AUDIBLE;
|
|
|
|
mStreamType = streamType;
|
|
mIndexMax = MAX_STREAM_VOLUME[streamType];
|
|
AudioSystem.initStreamVolume(streamType, 0, mIndexMax);
|
|
mIndexMax *= 10;
|
|
|
|
readSettings();
|
|
|
|
mDeathHandlers = new ArrayList<VolumeDeathHandler>();
|
|
}
|
|
|
|
public String getSettingNameForDevice(boolean lastAudible, int device) {
|
|
String name = lastAudible ?
|
|
mLastAudibleVolumeIndexSettingName :
|
|
mVolumeIndexSettingName;
|
|
String suffix = AudioSystem.getDeviceName(device);
|
|
if (suffix.isEmpty()) {
|
|
return name;
|
|
}
|
|
return name + "_" + suffix;
|
|
}
|
|
|
|
public synchronized void readSettings() {
|
|
int remainingDevices = AudioSystem.DEVICE_OUT_ALL;
|
|
|
|
for (int i = 0; remainingDevices != 0; i++) {
|
|
int device = (1 << i);
|
|
if ((device & remainingDevices) == 0) {
|
|
continue;
|
|
}
|
|
remainingDevices &= ~device;
|
|
|
|
// retrieve current volume for device
|
|
String name = getSettingNameForDevice(false /* lastAudible */, device);
|
|
// if no volume stored for current stream and device, use default volume if default
|
|
// device, continue otherwise
|
|
int defaultIndex = (device == AudioSystem.DEVICE_OUT_DEFAULT) ?
|
|
AudioManager.DEFAULT_STREAM_VOLUME[mStreamType] : -1;
|
|
int index = Settings.System.getInt(mContentResolver, name, defaultIndex);
|
|
if (index == -1) {
|
|
continue;
|
|
}
|
|
|
|
// retrieve last audible volume for device
|
|
name = getSettingNameForDevice(true /* lastAudible */, device);
|
|
// use stored last audible index if present, otherwise use current index if not 0
|
|
// or default index
|
|
defaultIndex = (index > 0) ?
|
|
index : AudioManager.DEFAULT_STREAM_VOLUME[mStreamType];
|
|
int lastAudibleIndex = Settings.System.getInt(mContentResolver, name, defaultIndex);
|
|
|
|
// a last audible index of 0 should never be stored for ring and notification
|
|
// streams on phones (voice capable devices).
|
|
// same for system stream on phones and tablets
|
|
if ((lastAudibleIndex == 0) &&
|
|
((mVoiceCapable &&
|
|
(mStreamVolumeAlias[mStreamType] == AudioSystem.STREAM_RING)) ||
|
|
(mStreamVolumeAlias[mStreamType] == AudioSystem.STREAM_SYSTEM))) {
|
|
lastAudibleIndex = AudioManager.DEFAULT_STREAM_VOLUME[mStreamType];
|
|
// Correct the data base
|
|
sendMsg(mAudioHandler,
|
|
MSG_PERSIST_VOLUME,
|
|
SENDMSG_QUEUE,
|
|
PERSIST_LAST_AUDIBLE,
|
|
device,
|
|
this,
|
|
PERSIST_DELAY);
|
|
}
|
|
mLastAudibleIndex.put(device, getValidIndex(10 * lastAudibleIndex));
|
|
// the initial index should never be 0 for ring and notification streams on phones
|
|
// (voice capable devices) if not in silent or vibrate mode.
|
|
// same for system stream on phones and tablets
|
|
if ((index == 0) && (mRingerMode == AudioManager.RINGER_MODE_NORMAL) &&
|
|
((mVoiceCapable &&
|
|
(mStreamVolumeAlias[mStreamType] == AudioSystem.STREAM_RING)) ||
|
|
(mStreamVolumeAlias[mStreamType] == AudioSystem.STREAM_SYSTEM))) {
|
|
index = lastAudibleIndex;
|
|
// Correct the data base
|
|
sendMsg(mAudioHandler,
|
|
MSG_PERSIST_VOLUME,
|
|
SENDMSG_QUEUE,
|
|
PERSIST_CURRENT,
|
|
device,
|
|
this,
|
|
PERSIST_DELAY);
|
|
}
|
|
mIndex.put(device, getValidIndex(10 * index));
|
|
}
|
|
}
|
|
|
|
public void applyDeviceVolume(int device) {
|
|
AudioSystem.setStreamVolumeIndex(mStreamType,
|
|
(getIndex(device, false /* lastAudible */) + 5)/10,
|
|
device);
|
|
}
|
|
|
|
public synchronized void applyAllVolumes() {
|
|
// apply default volume first: by convention this will reset all
|
|
// devices volumes in audio policy manager to the supplied value
|
|
AudioSystem.setStreamVolumeIndex(mStreamType,
|
|
(getIndex(AudioSystem.DEVICE_OUT_DEFAULT, false /* lastAudible */) + 5)/10,
|
|
AudioSystem.DEVICE_OUT_DEFAULT);
|
|
// then apply device specific volumes
|
|
Set set = mIndex.entrySet();
|
|
Iterator i = set.iterator();
|
|
while (i.hasNext()) {
|
|
Map.Entry entry = (Map.Entry)i.next();
|
|
int device = ((Integer)entry.getKey()).intValue();
|
|
if (device != AudioSystem.DEVICE_OUT_DEFAULT) {
|
|
AudioSystem.setStreamVolumeIndex(mStreamType,
|
|
((Integer)entry.getValue() + 5)/10,
|
|
device);
|
|
}
|
|
}
|
|
}
|
|
|
|
public boolean adjustIndex(int deltaIndex, int device) {
|
|
return setIndex(getIndex(device,
|
|
false /* lastAudible */) + deltaIndex,
|
|
device,
|
|
true /* lastAudible */);
|
|
}
|
|
|
|
public synchronized boolean setIndex(int index, int device, boolean lastAudible) {
|
|
int oldIndex = getIndex(device, false /* lastAudible */);
|
|
index = getValidIndex(index);
|
|
mIndex.put(device, getValidIndex(index));
|
|
|
|
if (oldIndex != index) {
|
|
if (lastAudible) {
|
|
mLastAudibleIndex.put(device, index);
|
|
}
|
|
// Apply change to all streams using this one as alias
|
|
// if changing volume of current device, also change volume of current
|
|
// device on aliased stream
|
|
boolean currentDevice = (device == getDeviceForStream(mStreamType));
|
|
int numStreamTypes = AudioSystem.getNumStreamTypes();
|
|
for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
|
|
if (streamType != mStreamType &&
|
|
mStreamVolumeAlias[streamType] == mStreamType) {
|
|
int scaledIndex = rescaleIndex(index, mStreamType, streamType);
|
|
mStreamStates[streamType].setIndex(scaledIndex,
|
|
device,
|
|
lastAudible);
|
|
if (currentDevice) {
|
|
mStreamStates[streamType].setIndex(scaledIndex,
|
|
getDeviceForStream(streamType),
|
|
lastAudible);
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public synchronized int getIndex(int device, boolean lastAudible) {
|
|
ConcurrentHashMap <Integer, Integer> indexes;
|
|
if (lastAudible) {
|
|
indexes = mLastAudibleIndex;
|
|
} else {
|
|
indexes = mIndex;
|
|
}
|
|
Integer index = indexes.get(device);
|
|
if (index == null) {
|
|
// there is always an entry for AudioSystem.DEVICE_OUT_DEFAULT
|
|
index = indexes.get(AudioSystem.DEVICE_OUT_DEFAULT);
|
|
}
|
|
return index.intValue();
|
|
}
|
|
|
|
public synchronized void setLastAudibleIndex(int index, int device) {
|
|
// Apply change to all streams using this one as alias
|
|
// if changing volume of current device, also change volume of current
|
|
// device on aliased stream
|
|
boolean currentDevice = (device == getDeviceForStream(mStreamType));
|
|
int numStreamTypes = AudioSystem.getNumStreamTypes();
|
|
for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
|
|
if (streamType != mStreamType &&
|
|
mStreamVolumeAlias[streamType] == mStreamType) {
|
|
int scaledIndex = rescaleIndex(index, mStreamType, streamType);
|
|
mStreamStates[streamType].setLastAudibleIndex(scaledIndex, device);
|
|
if (currentDevice) {
|
|
mStreamStates[streamType].setLastAudibleIndex(scaledIndex,
|
|
getDeviceForStream(streamType));
|
|
}
|
|
}
|
|
}
|
|
mLastAudibleIndex.put(device, getValidIndex(index));
|
|
}
|
|
|
|
public synchronized void adjustLastAudibleIndex(int deltaIndex, int device) {
|
|
setLastAudibleIndex(getIndex(device,
|
|
true /* lastAudible */) + deltaIndex,
|
|
device);
|
|
}
|
|
|
|
public int getMaxIndex() {
|
|
return mIndexMax;
|
|
}
|
|
|
|
// only called by setAllIndexes() which is already synchronized
|
|
public ConcurrentHashMap <Integer, Integer> getAllIndexes(boolean lastAudible) {
|
|
if (lastAudible) {
|
|
return mLastAudibleIndex;
|
|
} else {
|
|
return mIndex;
|
|
}
|
|
}
|
|
|
|
public synchronized void setAllIndexes(VolumeStreamState srcStream, boolean lastAudible) {
|
|
ConcurrentHashMap <Integer, Integer> indexes = srcStream.getAllIndexes(lastAudible);
|
|
Set set = indexes.entrySet();
|
|
Iterator i = set.iterator();
|
|
while (i.hasNext()) {
|
|
Map.Entry entry = (Map.Entry)i.next();
|
|
int device = ((Integer)entry.getKey()).intValue();
|
|
int index = ((Integer)entry.getValue()).intValue();
|
|
index = rescaleIndex(index, srcStream.getStreamType(), mStreamType);
|
|
setIndex(index, device, lastAudible);
|
|
}
|
|
}
|
|
|
|
public synchronized void mute(IBinder cb, boolean state) {
|
|
VolumeDeathHandler handler = getDeathHandler(cb, state);
|
|
if (handler == null) {
|
|
Log.e(TAG, "Could not get client death handler for stream: "+mStreamType);
|
|
return;
|
|
}
|
|
handler.mute(state);
|
|
}
|
|
|
|
public int getStreamType() {
|
|
return mStreamType;
|
|
}
|
|
|
|
private int getValidIndex(int index) {
|
|
if (index < 0) {
|
|
return 0;
|
|
} else if (index > mIndexMax) {
|
|
return mIndexMax;
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
private class VolumeDeathHandler implements IBinder.DeathRecipient {
|
|
private IBinder mICallback; // To be notified of client's death
|
|
private int mMuteCount; // Number of active mutes for this client
|
|
|
|
VolumeDeathHandler(IBinder cb) {
|
|
mICallback = cb;
|
|
}
|
|
|
|
// must be called while synchronized on parent VolumeStreamState
|
|
public void mute(boolean state) {
|
|
if (state) {
|
|
if (mMuteCount == 0) {
|
|
// Register for client death notification
|
|
try {
|
|
// mICallback can be 0 if muted by AudioService
|
|
if (mICallback != null) {
|
|
mICallback.linkToDeath(this, 0);
|
|
}
|
|
mDeathHandlers.add(this);
|
|
// If the stream is not yet muted by any client, set level to 0
|
|
if (muteCount() == 0) {
|
|
Set set = mIndex.entrySet();
|
|
Iterator i = set.iterator();
|
|
while (i.hasNext()) {
|
|
Map.Entry entry = (Map.Entry)i.next();
|
|
int device = ((Integer)entry.getKey()).intValue();
|
|
setIndex(0, device, false /* lastAudible */);
|
|
}
|
|
sendMsg(mAudioHandler,
|
|
MSG_SET_ALL_VOLUMES,
|
|
SENDMSG_QUEUE,
|
|
0,
|
|
0,
|
|
VolumeStreamState.this, 0);
|
|
}
|
|
} catch (RemoteException e) {
|
|
// Client has died!
|
|
binderDied();
|
|
return;
|
|
}
|
|
} else {
|
|
Log.w(TAG, "stream: "+mStreamType+" was already muted by this client");
|
|
}
|
|
mMuteCount++;
|
|
} else {
|
|
if (mMuteCount == 0) {
|
|
Log.e(TAG, "unexpected unmute for stream: "+mStreamType);
|
|
} else {
|
|
mMuteCount--;
|
|
if (mMuteCount == 0) {
|
|
// Unregister from client death notification
|
|
mDeathHandlers.remove(this);
|
|
// mICallback can be 0 if muted by AudioService
|
|
if (mICallback != null) {
|
|
mICallback.unlinkToDeath(this, 0);
|
|
}
|
|
if (muteCount() == 0) {
|
|
// If the stream is not muted any more, restore its volume if
|
|
// ringer mode allows it
|
|
if (!isStreamAffectedByRingerMode(mStreamType) ||
|
|
mRingerMode == AudioManager.RINGER_MODE_NORMAL) {
|
|
Set set = mIndex.entrySet();
|
|
Iterator i = set.iterator();
|
|
while (i.hasNext()) {
|
|
Map.Entry entry = (Map.Entry)i.next();
|
|
int device = ((Integer)entry.getKey()).intValue();
|
|
setIndex(getIndex(device,
|
|
true /* lastAudible */),
|
|
device,
|
|
false /* lastAudible */);
|
|
}
|
|
sendMsg(mAudioHandler,
|
|
MSG_SET_ALL_VOLUMES,
|
|
SENDMSG_QUEUE,
|
|
0,
|
|
0,
|
|
VolumeStreamState.this, 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void binderDied() {
|
|
Log.w(TAG, "Volume service client died for stream: "+mStreamType);
|
|
if (mMuteCount != 0) {
|
|
// Reset all active mute requests from this client.
|
|
mMuteCount = 1;
|
|
mute(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
private synchronized int muteCount() {
|
|
int count = 0;
|
|
int size = mDeathHandlers.size();
|
|
for (int i = 0; i < size; i++) {
|
|
count += mDeathHandlers.get(i).mMuteCount;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
// only called by mute() which is already synchronized
|
|
private VolumeDeathHandler getDeathHandler(IBinder cb, boolean state) {
|
|
VolumeDeathHandler handler;
|
|
int size = mDeathHandlers.size();
|
|
for (int i = 0; i < size; i++) {
|
|
handler = mDeathHandlers.get(i);
|
|
if (cb == handler.mICallback) {
|
|
return handler;
|
|
}
|
|
}
|
|
// If this is the first mute request for this client, create a new
|
|
// client death handler. Otherwise, it is an out of sequence unmute request.
|
|
if (state) {
|
|
handler = new VolumeDeathHandler(cb);
|
|
} else {
|
|
Log.w(TAG, "stream was not muted by this client");
|
|
handler = null;
|
|
}
|
|
return handler;
|
|
}
|
|
|
|
private void dump(PrintWriter pw) {
|
|
pw.print(" Current: ");
|
|
Set set = mIndex.entrySet();
|
|
Iterator i = set.iterator();
|
|
while (i.hasNext()) {
|
|
Map.Entry entry = (Map.Entry)i.next();
|
|
pw.print(Integer.toHexString(((Integer)entry.getKey()).intValue())
|
|
+ ": " + ((((Integer)entry.getValue()).intValue() + 5) / 10)+", ");
|
|
}
|
|
pw.print("\n Last audible: ");
|
|
set = mLastAudibleIndex.entrySet();
|
|
i = set.iterator();
|
|
while (i.hasNext()) {
|
|
Map.Entry entry = (Map.Entry)i.next();
|
|
pw.print(Integer.toHexString(((Integer)entry.getKey()).intValue())
|
|
+ ": " + ((((Integer)entry.getValue()).intValue() + 5) / 10)+", ");
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Thread that handles native AudioSystem control. */
|
|
private class AudioSystemThread extends Thread {
|
|
AudioSystemThread() {
|
|
super("AudioService");
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
// Set this thread up so the handler will work on it
|
|
Looper.prepare();
|
|
|
|
synchronized(AudioService.this) {
|
|
mAudioHandler = new AudioHandler();
|
|
|
|
// Notify that the handler has been created
|
|
AudioService.this.notify();
|
|
}
|
|
|
|
// Listen for volume change requests that are set by VolumePanel
|
|
Looper.loop();
|
|
}
|
|
}
|
|
|
|
/** Handles internal volume messages in separate volume thread. */
|
|
private class AudioHandler extends Handler {
|
|
|
|
private void setDeviceVolume(VolumeStreamState streamState, int device) {
|
|
|
|
// Apply volume
|
|
streamState.applyDeviceVolume(device);
|
|
|
|
// Apply change to all streams using this one as alias
|
|
int numStreamTypes = AudioSystem.getNumStreamTypes();
|
|
for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
|
|
if (streamType != streamState.mStreamType &&
|
|
mStreamVolumeAlias[streamType] == streamState.mStreamType) {
|
|
mStreamStates[streamType].applyDeviceVolume(getDeviceForStream(streamType));
|
|
}
|
|
}
|
|
|
|
// Post a persist volume msg
|
|
sendMsg(mAudioHandler,
|
|
MSG_PERSIST_VOLUME,
|
|
SENDMSG_QUEUE,
|
|
PERSIST_CURRENT|PERSIST_LAST_AUDIBLE,
|
|
device,
|
|
streamState,
|
|
PERSIST_DELAY);
|
|
|
|
}
|
|
|
|
private void setAllVolumes(VolumeStreamState streamState) {
|
|
|
|
// Apply volume
|
|
streamState.applyAllVolumes();
|
|
|
|
// Apply change to all streams using this one as alias
|
|
int numStreamTypes = AudioSystem.getNumStreamTypes();
|
|
for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
|
|
if (streamType != streamState.mStreamType &&
|
|
mStreamVolumeAlias[streamType] == streamState.mStreamType) {
|
|
mStreamStates[streamType].applyAllVolumes();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void persistVolume(VolumeStreamState streamState,
|
|
int persistType,
|
|
int device) {
|
|
if ((persistType & PERSIST_CURRENT) != 0) {
|
|
System.putInt(mContentResolver,
|
|
streamState.getSettingNameForDevice(false /* lastAudible */, device),
|
|
(streamState.getIndex(device, false /* lastAudible */) + 5)/ 10);
|
|
}
|
|
if ((persistType & PERSIST_LAST_AUDIBLE) != 0) {
|
|
System.putInt(mContentResolver,
|
|
streamState.getSettingNameForDevice(true /* lastAudible */, device),
|
|
(streamState.getIndex(device, true /* lastAudible */) + 5) / 10);
|
|
}
|
|
}
|
|
|
|
private void persistRingerMode(int ringerMode) {
|
|
System.putInt(mContentResolver, System.MODE_RINGER, ringerMode);
|
|
}
|
|
|
|
private void playSoundEffect(int effectType, int volume) {
|
|
synchronized (mSoundEffectsLock) {
|
|
if (mSoundPool == null) {
|
|
return;
|
|
}
|
|
float volFloat;
|
|
// use default if volume is not specified by caller
|
|
if (volume < 0) {
|
|
volFloat = (float)Math.pow(10, (float)sSoundEffectVolumeDb/20);
|
|
} else {
|
|
volFloat = (float) volume / 1000.0f;
|
|
}
|
|
|
|
if (SOUND_EFFECT_FILES_MAP[effectType][1] > 0) {
|
|
mSoundPool.play(SOUND_EFFECT_FILES_MAP[effectType][1], volFloat, volFloat, 0, 0, 1.0f);
|
|
} else {
|
|
MediaPlayer mediaPlayer = new MediaPlayer();
|
|
try {
|
|
String filePath = Environment.getRootDirectory() + SOUND_EFFECTS_PATH + SOUND_EFFECT_FILES[SOUND_EFFECT_FILES_MAP[effectType][0]];
|
|
mediaPlayer.setDataSource(filePath);
|
|
mediaPlayer.setAudioStreamType(AudioSystem.STREAM_SYSTEM);
|
|
mediaPlayer.prepare();
|
|
mediaPlayer.setVolume(volFloat, volFloat);
|
|
mediaPlayer.setOnCompletionListener(new OnCompletionListener() {
|
|
public void onCompletion(MediaPlayer mp) {
|
|
cleanupPlayer(mp);
|
|
}
|
|
});
|
|
mediaPlayer.setOnErrorListener(new OnErrorListener() {
|
|
public boolean onError(MediaPlayer mp, int what, int extra) {
|
|
cleanupPlayer(mp);
|
|
return true;
|
|
}
|
|
});
|
|
mediaPlayer.start();
|
|
} catch (IOException ex) {
|
|
Log.w(TAG, "MediaPlayer IOException: "+ex);
|
|
} catch (IllegalArgumentException ex) {
|
|
Log.w(TAG, "MediaPlayer IllegalArgumentException: "+ex);
|
|
} catch (IllegalStateException ex) {
|
|
Log.w(TAG, "MediaPlayer IllegalStateException: "+ex);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void onHandlePersistMediaButtonReceiver(ComponentName receiver) {
|
|
Settings.System.putString(mContentResolver, Settings.System.MEDIA_BUTTON_RECEIVER,
|
|
receiver == null ? "" : receiver.flattenToString());
|
|
}
|
|
|
|
private void cleanupPlayer(MediaPlayer mp) {
|
|
if (mp != null) {
|
|
try {
|
|
mp.stop();
|
|
mp.release();
|
|
} catch (IllegalStateException ex) {
|
|
Log.w(TAG, "MediaPlayer IllegalStateException: "+ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void setForceUse(int usage, int config) {
|
|
AudioSystem.setForceUse(usage, config);
|
|
}
|
|
|
|
@Override
|
|
public void handleMessage(Message msg) {
|
|
|
|
switch (msg.what) {
|
|
|
|
case MSG_SET_DEVICE_VOLUME:
|
|
setDeviceVolume((VolumeStreamState) msg.obj, msg.arg1);
|
|
break;
|
|
|
|
case MSG_SET_ALL_VOLUMES:
|
|
setAllVolumes((VolumeStreamState) msg.obj);
|
|
break;
|
|
|
|
case MSG_PERSIST_VOLUME:
|
|
persistVolume((VolumeStreamState) msg.obj, msg.arg1, msg.arg2);
|
|
break;
|
|
|
|
case MSG_PERSIST_MASTER_VOLUME:
|
|
Settings.System.putFloat(mContentResolver, Settings.System.VOLUME_MASTER,
|
|
(float)msg.arg1 / (float)1000.0);
|
|
break;
|
|
|
|
case MSG_PERSIST_MASTER_VOLUME_MUTE:
|
|
Settings.System.putInt(mContentResolver, Settings.System.VOLUME_MASTER_MUTE,
|
|
msg.arg1);
|
|
break;
|
|
|
|
case MSG_PERSIST_RINGER_MODE:
|
|
// note that the value persisted is the current ringer mode, not the
|
|
// value of ringer mode as of the time the request was made to persist
|
|
persistRingerMode(getRingerMode());
|
|
break;
|
|
|
|
case MSG_MEDIA_SERVER_DIED:
|
|
if (!mMediaServerOk) {
|
|
Log.e(TAG, "Media server died.");
|
|
// Force creation of new IAudioFlinger interface so that we are notified
|
|
// when new media_server process is back to life.
|
|
AudioSystem.setErrorCallback(mAudioSystemCallback);
|
|
sendMsg(mAudioHandler, MSG_MEDIA_SERVER_DIED, SENDMSG_NOOP, 0, 0,
|
|
null, 500);
|
|
}
|
|
break;
|
|
|
|
case MSG_MEDIA_SERVER_STARTED:
|
|
Log.e(TAG, "Media server started.");
|
|
// indicate to audio HAL that we start the reconfiguration phase after a media
|
|
// server crash
|
|
// Note that MSG_MEDIA_SERVER_STARTED message is only received when the media server
|
|
// process restarts after a crash, not the first time it is started.
|
|
AudioSystem.setParameters("restarting=true");
|
|
|
|
// Restore device connection states
|
|
synchronized (mConnectedDevices) {
|
|
Set set = mConnectedDevices.entrySet();
|
|
Iterator i = set.iterator();
|
|
while (i.hasNext()) {
|
|
Map.Entry device = (Map.Entry)i.next();
|
|
AudioSystem.setDeviceConnectionState(
|
|
((Integer)device.getKey()).intValue(),
|
|
AudioSystem.DEVICE_STATE_AVAILABLE,
|
|
(String)device.getValue());
|
|
}
|
|
}
|
|
// Restore call state
|
|
AudioSystem.setPhoneState(mMode);
|
|
|
|
// Restore forced usage for communcations and record
|
|
AudioSystem.setForceUse(AudioSystem.FOR_COMMUNICATION, mForcedUseForComm);
|
|
AudioSystem.setForceUse(AudioSystem.FOR_RECORD, mForcedUseForComm);
|
|
|
|
// Restore stream volumes
|
|
int numStreamTypes = AudioSystem.getNumStreamTypes();
|
|
for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
|
|
VolumeStreamState streamState = mStreamStates[streamType];
|
|
AudioSystem.initStreamVolume(streamType, 0, (streamState.mIndexMax + 5) / 10);
|
|
|
|
streamState.applyAllVolumes();
|
|
}
|
|
|
|
// Restore ringer mode
|
|
setRingerModeInt(getRingerMode(), false);
|
|
|
|
// Restore master volume
|
|
restoreMasterVolume();
|
|
|
|
// Reset device orientation (if monitored for this device)
|
|
if (SystemProperties.getBoolean("ro.audio.monitorOrientation", false)) {
|
|
setOrientationForAudioSystem();
|
|
}
|
|
|
|
synchronized (mBluetoothA2dpEnabledLock) {
|
|
AudioSystem.setForceUse(AudioSystem.FOR_MEDIA,
|
|
mBluetoothA2dpEnabled ?
|
|
AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP);
|
|
}
|
|
// indicate the end of reconfiguration phase to audio HAL
|
|
AudioSystem.setParameters("restarting=false");
|
|
break;
|
|
|
|
case MSG_LOAD_SOUND_EFFECTS:
|
|
loadSoundEffects();
|
|
break;
|
|
|
|
case MSG_PLAY_SOUND_EFFECT:
|
|
playSoundEffect(msg.arg1, msg.arg2);
|
|
break;
|
|
|
|
case MSG_BTA2DP_DOCK_TIMEOUT:
|
|
// msg.obj == address of BTA2DP device
|
|
synchronized (mConnectedDevices) {
|
|
makeA2dpDeviceUnavailableNow( (String) msg.obj );
|
|
}
|
|
break;
|
|
|
|
case MSG_SET_FORCE_USE:
|
|
case MSG_SET_FORCE_BT_A2DP_USE:
|
|
case MSG_SET_FORCE_RSX_USE:
|
|
setForceUse(msg.arg1, msg.arg2);
|
|
break;
|
|
|
|
case MSG_PERSIST_MEDIABUTTONRECEIVER:
|
|
onHandlePersistMediaButtonReceiver( (ComponentName) msg.obj );
|
|
break;
|
|
|
|
case MSG_RCDISPLAY_CLEAR:
|
|
onRcDisplayClear();
|
|
break;
|
|
|
|
case MSG_RCDISPLAY_UPDATE:
|
|
// msg.obj is guaranteed to be non null
|
|
onRcDisplayUpdate( (RemoteControlStackEntry) msg.obj, msg.arg1);
|
|
break;
|
|
|
|
case MSG_BT_HEADSET_CNCT_FAILED:
|
|
resetBluetoothSco();
|
|
break;
|
|
|
|
case MSG_SET_WIRED_DEVICE_CONNECTION_STATE:
|
|
onSetWiredDeviceConnectionState(msg.arg1, msg.arg2, (String)msg.obj);
|
|
mMediaEventWakeLock.release();
|
|
break;
|
|
|
|
case MSG_SET_A2DP_CONNECTION_STATE:
|
|
onSetA2dpConnectionState((BluetoothDevice)msg.obj, msg.arg1);
|
|
mMediaEventWakeLock.release();
|
|
break;
|
|
|
|
case MSG_REPORT_NEW_ROUTES: {
|
|
int N = mRoutesObservers.beginBroadcast();
|
|
if (N > 0) {
|
|
AudioRoutesInfo routes;
|
|
synchronized (mCurAudioRoutes) {
|
|
routes = new AudioRoutesInfo(mCurAudioRoutes);
|
|
}
|
|
while (N > 0) {
|
|
N--;
|
|
IAudioRoutesObserver obs = mRoutesObservers.getBroadcastItem(N);
|
|
try {
|
|
obs.dispatchAudioRoutesChanged(routes);
|
|
} catch (RemoteException e) {
|
|
}
|
|
}
|
|
}
|
|
mRoutesObservers.finishBroadcast();
|
|
break;
|
|
}
|
|
|
|
case MSG_REEVALUATE_REMOTE:
|
|
onReevaluateRemote();
|
|
break;
|
|
|
|
case MSG_RCC_NEW_PLAYBACK_INFO:
|
|
onNewPlaybackInfoForRcc(msg.arg1 /* rccId */, msg.arg2 /* key */,
|
|
((Integer)msg.obj).intValue() /* value */);
|
|
break;
|
|
case MSG_RCC_NEW_VOLUME_OBS:
|
|
onRegisterVolumeObserverForRcc(msg.arg1 /* rccId */,
|
|
(IRemoteVolumeObserver)msg.obj /* rvo */);
|
|
break;
|
|
|
|
case MSG_SET_RSX_CONNECTION_STATE:
|
|
onSetRsxConnectionState(msg.arg1/*available*/, msg.arg2/*address*/);
|
|
break;
|
|
|
|
case MSG_CHECK_MUSIC_ACTIVE:
|
|
onCheckMusicActive();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private class SettingsObserver extends ContentObserver {
|
|
|
|
SettingsObserver() {
|
|
super(new Handler());
|
|
mContentResolver.registerContentObserver(Settings.System.getUriFor(
|
|
Settings.System.MODE_RINGER_STREAMS_AFFECTED), false, this);
|
|
}
|
|
|
|
@Override
|
|
public void onChange(boolean selfChange) {
|
|
super.onChange(selfChange);
|
|
// FIXME This synchronized is not necessary if mSettingsLock only protects mRingerMode.
|
|
// However there appear to be some missing locks around mRingerModeMutedStreams
|
|
// and mRingerModeAffectedStreams, so will leave this synchronized for now.
|
|
// mRingerModeMutedStreams and mMuteAffectedStreams are safe (only accessed once).
|
|
synchronized (mSettingsLock) {
|
|
int ringerModeAffectedStreams = Settings.System.getInt(mContentResolver,
|
|
Settings.System.MODE_RINGER_STREAMS_AFFECTED,
|
|
((1 << AudioSystem.STREAM_RING)|(1 << AudioSystem.STREAM_NOTIFICATION)|
|
|
(1 << AudioSystem.STREAM_SYSTEM)|(1 << AudioSystem.STREAM_SYSTEM_ENFORCED)));
|
|
if (mVoiceCapable) {
|
|
ringerModeAffectedStreams &= ~(1 << AudioSystem.STREAM_MUSIC);
|
|
} else {
|
|
ringerModeAffectedStreams |= (1 << AudioSystem.STREAM_MUSIC);
|
|
}
|
|
if (ringerModeAffectedStreams != mRingerModeAffectedStreams) {
|
|
/*
|
|
* Ensure all stream types that should be affected by ringer mode
|
|
* are in the proper state.
|
|
*/
|
|
mRingerModeAffectedStreams = ringerModeAffectedStreams;
|
|
setRingerModeInt(getRingerMode(), false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// must be called synchronized on mConnectedDevices
|
|
private void makeA2dpDeviceAvailable(String address) {
|
|
// enable A2DP before notifying A2DP connection to avoid unecessary processing in
|
|
// audio policy manager
|
|
setBluetoothA2dpOnInt(true);
|
|
AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
|
|
AudioSystem.DEVICE_STATE_AVAILABLE,
|
|
address);
|
|
// Reset A2DP suspend state each time a new sink is connected
|
|
AudioSystem.setParameters("A2dpSuspended=false");
|
|
mConnectedDevices.put( new Integer(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP),
|
|
address);
|
|
}
|
|
|
|
private void sendBecomingNoisyIntent() {
|
|
sendBroadcastToAll(new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
|
|
}
|
|
|
|
// must be called synchronized on mConnectedDevices
|
|
private void makeA2dpDeviceUnavailableNow(String address) {
|
|
AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
|
|
AudioSystem.DEVICE_STATE_UNAVAILABLE,
|
|
address);
|
|
mConnectedDevices.remove(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP);
|
|
}
|
|
|
|
// must be called synchronized on mConnectedDevices
|
|
private void makeA2dpDeviceUnavailableLater(String address) {
|
|
// prevent any activity on the A2DP audio output to avoid unwanted
|
|
// reconnection of the sink.
|
|
AudioSystem.setParameters("A2dpSuspended=true");
|
|
// the device will be made unavailable later, so consider it disconnected right away
|
|
mConnectedDevices.remove(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP);
|
|
// send the delayed message to make the device unavailable later
|
|
Message msg = mAudioHandler.obtainMessage(MSG_BTA2DP_DOCK_TIMEOUT, address);
|
|
mAudioHandler.sendMessageDelayed(msg, BTA2DP_DOCK_TIMEOUT_MILLIS);
|
|
|
|
}
|
|
|
|
// must be called synchronized on mConnectedDevices
|
|
private void cancelA2dpDeviceTimeout() {
|
|
mAudioHandler.removeMessages(MSG_BTA2DP_DOCK_TIMEOUT);
|
|
}
|
|
|
|
// must be called synchronized on mConnectedDevices
|
|
private boolean hasScheduledA2dpDockTimeout() {
|
|
return mAudioHandler.hasMessages(MSG_BTA2DP_DOCK_TIMEOUT);
|
|
}
|
|
|
|
private void onSetA2dpConnectionState(BluetoothDevice btDevice, int state)
|
|
{
|
|
if (btDevice == null) {
|
|
return;
|
|
}
|
|
String address = btDevice.getAddress();
|
|
if (!BluetoothAdapter.checkBluetoothAddress(address)) {
|
|
address = "";
|
|
}
|
|
synchronized (mConnectedDevices) {
|
|
boolean isConnected =
|
|
(mConnectedDevices.containsKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP) &&
|
|
mConnectedDevices.get(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP).equals(address));
|
|
|
|
if (isConnected && state != BluetoothProfile.STATE_CONNECTED) {
|
|
if (btDevice.isBluetoothDock()) {
|
|
if (state == BluetoothProfile.STATE_DISCONNECTED) {
|
|
// introduction of a delay for transient disconnections of docks when
|
|
// power is rapidly turned off/on, this message will be canceled if
|
|
// we reconnect the dock under a preset delay
|
|
makeA2dpDeviceUnavailableLater(address);
|
|
// the next time isConnected is evaluated, it will be false for the dock
|
|
}
|
|
} else {
|
|
makeA2dpDeviceUnavailableNow(address);
|
|
}
|
|
synchronized (mCurAudioRoutes) {
|
|
if (mCurAudioRoutes.mBluetoothName != null) {
|
|
mCurAudioRoutes.mBluetoothName = null;
|
|
sendMsg(mAudioHandler, MSG_REPORT_NEW_ROUTES,
|
|
SENDMSG_NOOP, 0, 0, null, 0);
|
|
}
|
|
}
|
|
} else if (!isConnected && state == BluetoothProfile.STATE_CONNECTED) {
|
|
if (btDevice.isBluetoothDock()) {
|
|
// this could be a reconnection after a transient disconnection
|
|
cancelA2dpDeviceTimeout();
|
|
mDockAddress = address;
|
|
} else {
|
|
// this could be a connection of another A2DP device before the timeout of
|
|
// a dock: cancel the dock timeout, and make the dock unavailable now
|
|
if(hasScheduledA2dpDockTimeout()) {
|
|
cancelA2dpDeviceTimeout();
|
|
makeA2dpDeviceUnavailableNow(mDockAddress);
|
|
}
|
|
}
|
|
makeA2dpDeviceAvailable(address);
|
|
synchronized (mCurAudioRoutes) {
|
|
String name = btDevice.getAliasName();
|
|
if (!TextUtils.equals(mCurAudioRoutes.mBluetoothName, name)) {
|
|
mCurAudioRoutes.mBluetoothName = name;
|
|
sendMsg(mAudioHandler, MSG_REPORT_NEW_ROUTES,
|
|
SENDMSG_NOOP, 0, 0, null, 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private boolean handleDeviceConnection(boolean connected, int device, String params) {
|
|
synchronized (mConnectedDevices) {
|
|
boolean isConnected = (mConnectedDevices.containsKey(device) &&
|
|
(params.isEmpty() || mConnectedDevices.get(device).equals(params)));
|
|
|
|
if (isConnected && !connected) {
|
|
AudioSystem.setDeviceConnectionState(device,
|
|
AudioSystem.DEVICE_STATE_UNAVAILABLE,
|
|
mConnectedDevices.get(device));
|
|
mConnectedDevices.remove(device);
|
|
return true;
|
|
} else if (!isConnected && connected) {
|
|
AudioSystem.setDeviceConnectionState(device,
|
|
AudioSystem.DEVICE_STATE_AVAILABLE,
|
|
params);
|
|
mConnectedDevices.put(new Integer(device), params);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Devices which removal triggers intent ACTION_AUDIO_BECOMING_NOISY. The intent is only
|
|
// sent if none of these devices is connected.
|
|
int mBecomingNoisyIntentDevices =
|
|
AudioSystem.DEVICE_OUT_WIRED_HEADSET | AudioSystem.DEVICE_OUT_WIRED_HEADPHONE |
|
|
AudioSystem.DEVICE_OUT_ALL_A2DP | AudioSystem.DEVICE_OUT_AUX_DIGITAL |
|
|
AudioSystem.DEVICE_OUT_ANLG_DOCK_HEADSET | AudioSystem.DEVICE_OUT_DGTL_DOCK_HEADSET |
|
|
AudioSystem.DEVICE_OUT_ALL_USB;
|
|
|
|
// must be called before removing the device from mConnectedDevices
|
|
private int checkSendBecomingNoisyIntent(int device, int state) {
|
|
int delay = 0;
|
|
if ((state == 0) && ((device & mBecomingNoisyIntentDevices) != 0)) {
|
|
int devices = 0;
|
|
for (int dev : mConnectedDevices.keySet()) {
|
|
if ((dev & mBecomingNoisyIntentDevices) != 0) {
|
|
devices |= dev;
|
|
}
|
|
}
|
|
if (devices == device) {
|
|
delay = 1000;
|
|
sendBecomingNoisyIntent();
|
|
}
|
|
}
|
|
|
|
if (mAudioHandler.hasMessages(MSG_SET_A2DP_CONNECTION_STATE) ||
|
|
mAudioHandler.hasMessages(MSG_SET_WIRED_DEVICE_CONNECTION_STATE)) {
|
|
delay = 1000;
|
|
}
|
|
return delay;
|
|
}
|
|
|
|
private void sendDeviceConnectionIntent(int device, int state, String name)
|
|
{
|
|
Intent intent = new Intent();
|
|
|
|
intent.putExtra("state", state);
|
|
intent.putExtra("name", name);
|
|
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
|
|
|
|
int connType = 0;
|
|
|
|
if (device == AudioSystem.DEVICE_OUT_WIRED_HEADSET) {
|
|
connType = AudioRoutesInfo.MAIN_HEADSET;
|
|
intent.setAction(Intent.ACTION_HEADSET_PLUG);
|
|
intent.putExtra("microphone", 1);
|
|
} else if (device == AudioSystem.DEVICE_OUT_WIRED_HEADPHONE) {
|
|
connType = AudioRoutesInfo.MAIN_HEADPHONES;
|
|
intent.setAction(Intent.ACTION_HEADSET_PLUG);
|
|
intent.putExtra("microphone", 0);
|
|
} else if (device == AudioSystem.DEVICE_OUT_ANLG_DOCK_HEADSET) {
|
|
connType = AudioRoutesInfo.MAIN_DOCK_SPEAKERS;
|
|
intent.setAction(Intent.ACTION_ANALOG_AUDIO_DOCK_PLUG);
|
|
} else if (device == AudioSystem.DEVICE_OUT_DGTL_DOCK_HEADSET) {
|
|
connType = AudioRoutesInfo.MAIN_DOCK_SPEAKERS;
|
|
intent.setAction(Intent.ACTION_DIGITAL_AUDIO_DOCK_PLUG);
|
|
} else if (device == AudioSystem.DEVICE_OUT_AUX_DIGITAL) {
|
|
connType = AudioRoutesInfo.MAIN_HDMI;
|
|
intent.setAction(Intent.ACTION_HDMI_AUDIO_PLUG);
|
|
}
|
|
|
|
synchronized (mCurAudioRoutes) {
|
|
if (connType != 0) {
|
|
int newConn = mCurAudioRoutes.mMainType;
|
|
if (state != 0) {
|
|
newConn |= connType;
|
|
} else {
|
|
newConn &= ~connType;
|
|
}
|
|
if (newConn != mCurAudioRoutes.mMainType) {
|
|
mCurAudioRoutes.mMainType = newConn;
|
|
sendMsg(mAudioHandler, MSG_REPORT_NEW_ROUTES,
|
|
SENDMSG_NOOP, 0, 0, null, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
final long ident = Binder.clearCallingIdentity();
|
|
try {
|
|
ActivityManagerNative.broadcastStickyIntent(intent, null, UserHandle.USER_ALL);
|
|
} finally {
|
|
Binder.restoreCallingIdentity(ident);
|
|
}
|
|
}
|
|
|
|
private void onSetWiredDeviceConnectionState(int device, int state, String name)
|
|
{
|
|
synchronized (mConnectedDevices) {
|
|
if ((state == 0) && ((device == AudioSystem.DEVICE_OUT_WIRED_HEADSET) ||
|
|
(device == AudioSystem.DEVICE_OUT_WIRED_HEADPHONE))) {
|
|
setBluetoothA2dpOnInt(true);
|
|
}
|
|
handleDeviceConnection((state == 1), device, "");
|
|
if ((state != 0) && ((device == AudioSystem.DEVICE_OUT_WIRED_HEADSET) ||
|
|
(device == AudioSystem.DEVICE_OUT_WIRED_HEADPHONE))) {
|
|
setBluetoothA2dpOnInt(false);
|
|
}
|
|
sendDeviceConnectionIntent(device, state, name);
|
|
}
|
|
}
|
|
|
|
/* cache of the address of the last dock the device was connected to */
|
|
private String mDockAddress;
|
|
|
|
/**
|
|
* Receiver for misc intent broadcasts the Phone app cares about.
|
|
*/
|
|
private class AudioServiceBroadcastReceiver extends BroadcastReceiver {
|
|
@Override
|
|
public void onReceive(Context context, Intent intent) {
|
|
String action = intent.getAction();
|
|
int device;
|
|
int state;
|
|
|
|
if (action.equals(Intent.ACTION_DOCK_EVENT)) {
|
|
int dockState = intent.getIntExtra(Intent.EXTRA_DOCK_STATE,
|
|
Intent.EXTRA_DOCK_STATE_UNDOCKED);
|
|
int config;
|
|
switch (dockState) {
|
|
case Intent.EXTRA_DOCK_STATE_DESK:
|
|
config = AudioSystem.FORCE_BT_DESK_DOCK;
|
|
break;
|
|
case Intent.EXTRA_DOCK_STATE_CAR:
|
|
config = AudioSystem.FORCE_BT_CAR_DOCK;
|
|
break;
|
|
case Intent.EXTRA_DOCK_STATE_LE_DESK:
|
|
config = AudioSystem.FORCE_ANALOG_DOCK;
|
|
break;
|
|
case Intent.EXTRA_DOCK_STATE_HE_DESK:
|
|
config = AudioSystem.FORCE_DIGITAL_DOCK;
|
|
break;
|
|
case Intent.EXTRA_DOCK_STATE_UNDOCKED:
|
|
default:
|
|
config = AudioSystem.FORCE_NONE;
|
|
}
|
|
AudioSystem.setForceUse(AudioSystem.FOR_DOCK, config);
|
|
} else if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
|
|
state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE,
|
|
BluetoothProfile.STATE_DISCONNECTED);
|
|
device = AudioSystem.DEVICE_OUT_BLUETOOTH_SCO;
|
|
String address = null;
|
|
|
|
BluetoothDevice btDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
|
if (btDevice == null) {
|
|
return;
|
|
}
|
|
|
|
address = btDevice.getAddress();
|
|
BluetoothClass btClass = btDevice.getBluetoothClass();
|
|
if (btClass != null) {
|
|
switch (btClass.getDeviceClass()) {
|
|
case BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET:
|
|
case BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE:
|
|
device = AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_HEADSET;
|
|
break;
|
|
case BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO:
|
|
device = AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_CARKIT;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!BluetoothAdapter.checkBluetoothAddress(address)) {
|
|
address = "";
|
|
}
|
|
|
|
boolean connected = (state == BluetoothProfile.STATE_CONNECTED);
|
|
if (handleDeviceConnection(connected, device, address)) {
|
|
synchronized (mScoClients) {
|
|
if (connected) {
|
|
mBluetoothHeadsetDevice = btDevice;
|
|
} else {
|
|
mBluetoothHeadsetDevice = null;
|
|
resetBluetoothSco();
|
|
}
|
|
}
|
|
}
|
|
} else if (action.equals(Intent.ACTION_USB_AUDIO_ACCESSORY_PLUG) ||
|
|
action.equals(Intent.ACTION_USB_AUDIO_DEVICE_PLUG)) {
|
|
state = intent.getIntExtra("state", 0);
|
|
int alsaCard = intent.getIntExtra("card", -1);
|
|
int alsaDevice = intent.getIntExtra("device", -1);
|
|
String params = (alsaCard == -1 && alsaDevice == -1 ? ""
|
|
: "card=" + alsaCard + ";device=" + alsaDevice);
|
|
device = action.equals(Intent.ACTION_USB_AUDIO_ACCESSORY_PLUG) ?
|
|
AudioSystem.DEVICE_OUT_USB_ACCESSORY : AudioSystem.DEVICE_OUT_USB_DEVICE;
|
|
Log.v(TAG, "Broadcast Receiver: Got "
|
|
+ (action.equals(Intent.ACTION_USB_AUDIO_ACCESSORY_PLUG) ?
|
|
"ACTION_USB_AUDIO_ACCESSORY_PLUG" : "ACTION_USB_AUDIO_DEVICE_PLUG")
|
|
+ ", state = " + state + ", card: " + alsaCard + ", device: " + alsaDevice);
|
|
handleDeviceConnection((state == 1), device, params);
|
|
} else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
|
|
boolean broadcast = false;
|
|
int scoAudioState = AudioManager.SCO_AUDIO_STATE_ERROR;
|
|
synchronized (mScoClients) {
|
|
int btState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
|
|
// broadcast intent if the connection was initated by AudioService
|
|
if (!mScoClients.isEmpty() &&
|
|
(mScoAudioState == SCO_STATE_ACTIVE_INTERNAL ||
|
|
mScoAudioState == SCO_STATE_ACTIVATE_REQ ||
|
|
mScoAudioState == SCO_STATE_DEACTIVATE_REQ)) {
|
|
broadcast = true;
|
|
}
|
|
switch (btState) {
|
|
case BluetoothHeadset.STATE_AUDIO_CONNECTED:
|
|
scoAudioState = AudioManager.SCO_AUDIO_STATE_CONNECTED;
|
|
if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL &&
|
|
mScoAudioState != SCO_STATE_DEACTIVATE_REQ &&
|
|
mScoAudioState != SCO_STATE_DEACTIVATE_EXT_REQ) {
|
|
mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;
|
|
}
|
|
break;
|
|
case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
|
|
scoAudioState = AudioManager.SCO_AUDIO_STATE_DISCONNECTED;
|
|
mScoAudioState = SCO_STATE_INACTIVE;
|
|
clearAllScoClients(0, false);
|
|
break;
|
|
case BluetoothHeadset.STATE_AUDIO_CONNECTING:
|
|
if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL &&
|
|
mScoAudioState != SCO_STATE_DEACTIVATE_REQ &&
|
|
mScoAudioState != SCO_STATE_DEACTIVATE_EXT_REQ) {
|
|
mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;
|
|
}
|
|
default:
|
|
// do not broadcast CONNECTING or invalid state
|
|
broadcast = false;
|
|
break;
|
|
}
|
|
}
|
|
if (broadcast) {
|
|
broadcastScoConnectionState(scoAudioState);
|
|
//FIXME: this is to maintain compatibility with deprecated intent
|
|
// AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED. Remove when appropriate.
|
|
Intent newIntent = new Intent(AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED);
|
|
newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, scoAudioState);
|
|
sendStickyBroadcastToAll(newIntent);
|
|
}
|
|
} else if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {
|
|
mBootCompleted = true;
|
|
sendMsg(mAudioHandler, MSG_LOAD_SOUND_EFFECTS, SENDMSG_NOOP,
|
|
0, 0, null, 0);
|
|
|
|
mKeyguardManager =
|
|
(KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
|
|
mScoConnectionState = AudioManager.SCO_AUDIO_STATE_ERROR;
|
|
resetBluetoothSco();
|
|
getBluetoothHeadset();
|
|
//FIXME: this is to maintain compatibility with deprecated intent
|
|
// AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED. Remove when appropriate.
|
|
Intent newIntent = new Intent(AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED);
|
|
newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_STATE,
|
|
AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
|
|
sendStickyBroadcastToAll(newIntent);
|
|
|
|
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
|
|
if (adapter != null) {
|
|
adapter.getProfileProxy(mContext, mBluetoothProfileServiceListener,
|
|
BluetoothProfile.A2DP);
|
|
}
|
|
} else if (action.equals(Intent.ACTION_PACKAGE_REMOVED)) {
|
|
if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
|
|
// a package is being removed, not replaced
|
|
String packageName = intent.getData().getSchemeSpecificPart();
|
|
if (packageName != null) {
|
|
removeMediaButtonReceiverForPackage(packageName);
|
|
}
|
|
}
|
|
} else if (action.equals(Intent.ACTION_SCREEN_ON)) {
|
|
AudioSystem.setParameters("screen_state=on");
|
|
} else if (action.equals(Intent.ACTION_SCREEN_OFF)) {
|
|
AudioSystem.setParameters("screen_state=off");
|
|
} else if (action.equalsIgnoreCase(Intent.ACTION_CONFIGURATION_CHANGED)) {
|
|
handleConfigurationChanged(context);
|
|
}
|
|
}
|
|
}
|
|
|
|
//==========================================================================================
|
|
// AudioFocus
|
|
//==========================================================================================
|
|
|
|
/* constant to identify focus stack entry that is used to hold the focus while the phone
|
|
* is ringing or during a call. Used by com.android.internal.telephony.CallManager when
|
|
* entering and exiting calls.
|
|
*/
|
|
public final static String IN_VOICE_COMM_FOCUS_ID = "AudioFocus_For_Phone_Ring_And_Calls";
|
|
|
|
private final static Object mAudioFocusLock = new Object();
|
|
|
|
private final static Object mRingingLock = new Object();
|
|
|
|
private PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
|
|
@Override
|
|
public void onCallStateChanged(int state, String incomingNumber) {
|
|
if (state == TelephonyManager.CALL_STATE_RINGING) {
|
|
//Log.v(TAG, " CALL_STATE_RINGING");
|
|
synchronized(mRingingLock) {
|
|
mIsRinging = true;
|
|
}
|
|
} else if ((state == TelephonyManager.CALL_STATE_OFFHOOK)
|
|
|| (state == TelephonyManager.CALL_STATE_IDLE)) {
|
|
synchronized(mRingingLock) {
|
|
mIsRinging = false;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
private void notifyTopOfAudioFocusStack() {
|
|
// notify the top of the stack it gained focus
|
|
if (!mFocusStack.empty() && (mFocusStack.peek().mFocusDispatcher != null)) {
|
|
if (canReassignAudioFocus()) {
|
|
try {
|
|
mFocusStack.peek().mFocusDispatcher.dispatchAudioFocusChange(
|
|
AudioManager.AUDIOFOCUS_GAIN, mFocusStack.peek().mClientId);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "Failure to signal gain of audio control focus due to "+ e);
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static class FocusStackEntry {
|
|
public int mStreamType = -1;// no stream type
|
|
public IAudioFocusDispatcher mFocusDispatcher = null;
|
|
public IBinder mSourceRef = null;
|
|
public String mClientId;
|
|
public int mFocusChangeType;
|
|
public AudioFocusDeathHandler mHandler;
|
|
public String mPackageName;
|
|
public int mCallingUid;
|
|
|
|
public FocusStackEntry() {
|
|
}
|
|
|
|
public FocusStackEntry(int streamType, int duration,
|
|
IAudioFocusDispatcher afl, IBinder source, String id, AudioFocusDeathHandler hdlr,
|
|
String pn, int uid) {
|
|
mStreamType = streamType;
|
|
mFocusDispatcher = afl;
|
|
mSourceRef = source;
|
|
mClientId = id;
|
|
mFocusChangeType = duration;
|
|
mHandler = hdlr;
|
|
mPackageName = pn;
|
|
mCallingUid = uid;
|
|
}
|
|
|
|
public void unlinkToDeath() {
|
|
try {
|
|
if (mSourceRef != null && mHandler != null) {
|
|
mSourceRef.unlinkToDeath(mHandler, 0);
|
|
mHandler = null;
|
|
}
|
|
} catch (java.util.NoSuchElementException e) {
|
|
Log.e(TAG, "Encountered " + e + " in FocusStackEntry.unlinkToDeath()");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void finalize() throws Throwable {
|
|
unlinkToDeath(); // unlink exception handled inside method
|
|
super.finalize();
|
|
}
|
|
}
|
|
|
|
private final Stack<FocusStackEntry> mFocusStack = new Stack<FocusStackEntry>();
|
|
|
|
/**
|
|
* Helper function:
|
|
* Display in the log the current entries in the audio focus stack
|
|
*/
|
|
private void dumpFocusStack(PrintWriter pw) {
|
|
pw.println("\nAudio Focus stack entries:");
|
|
synchronized(mAudioFocusLock) {
|
|
Iterator<FocusStackEntry> stackIterator = mFocusStack.iterator();
|
|
while(stackIterator.hasNext()) {
|
|
FocusStackEntry fse = stackIterator.next();
|
|
pw.println(" source:" + fse.mSourceRef + " -- client: " + fse.mClientId
|
|
+ " -- duration: " + fse.mFocusChangeType
|
|
+ " -- uid: " + fse.mCallingUid
|
|
+ " -- stream: " + fse.mStreamType);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function:
|
|
* Called synchronized on mAudioFocusLock
|
|
* Remove a focus listener from the focus stack.
|
|
* @param focusListenerToRemove the focus listener
|
|
* @param signal if true and the listener was at the top of the focus stack, i.e. it was holding
|
|
* focus, notify the next item in the stack it gained focus.
|
|
*/
|
|
private void removeFocusStackEntry(String clientToRemove, boolean signal) {
|
|
// is the current top of the focus stack abandoning focus? (because of request, not death)
|
|
if (!mFocusStack.empty() && mFocusStack.peek().mClientId.equals(clientToRemove))
|
|
{
|
|
//Log.i(TAG, " removeFocusStackEntry() removing top of stack");
|
|
FocusStackEntry fse = mFocusStack.pop();
|
|
fse.unlinkToDeath();
|
|
if (signal) {
|
|
// notify the new top of the stack it gained focus
|
|
notifyTopOfAudioFocusStack();
|
|
// there's a new top of the stack, let the remote control know
|
|
synchronized(mRCStack) {
|
|
checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
|
|
}
|
|
}
|
|
} else {
|
|
// focus is abandoned by a client that's not at the top of the stack,
|
|
// no need to update focus.
|
|
Iterator<FocusStackEntry> stackIterator = mFocusStack.iterator();
|
|
while(stackIterator.hasNext()) {
|
|
FocusStackEntry fse = (FocusStackEntry)stackIterator.next();
|
|
if(fse.mClientId.equals(clientToRemove)) {
|
|
Log.i(TAG, " AudioFocus abandonAudioFocus(): removing entry for "
|
|
+ fse.mClientId);
|
|
stackIterator.remove();
|
|
fse.unlinkToDeath();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function:
|
|
* Called synchronized on mAudioFocusLock
|
|
* Remove focus listeners from the focus stack for a particular client when it has died.
|
|
*/
|
|
private void removeFocusStackEntryForClient(IBinder cb) {
|
|
// is the owner of the audio focus part of the client to remove?
|
|
boolean isTopOfStackForClientToRemove = !mFocusStack.isEmpty() &&
|
|
mFocusStack.peek().mSourceRef.equals(cb);
|
|
Iterator<FocusStackEntry> stackIterator = mFocusStack.iterator();
|
|
while(stackIterator.hasNext()) {
|
|
FocusStackEntry fse = (FocusStackEntry)stackIterator.next();
|
|
if(fse.mSourceRef.equals(cb)) {
|
|
Log.i(TAG, " AudioFocus abandonAudioFocus(): removing entry for "
|
|
+ fse.mClientId);
|
|
stackIterator.remove();
|
|
// the client just died, no need to unlink to its death
|
|
}
|
|
}
|
|
if (isTopOfStackForClientToRemove) {
|
|
// we removed an entry at the top of the stack:
|
|
// notify the new top of the stack it gained focus.
|
|
notifyTopOfAudioFocusStack();
|
|
// there's a new top of the stack, let the remote control know
|
|
synchronized(mRCStack) {
|
|
checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function:
|
|
* Returns true if the system is in a state where the focus can be reevaluated, false otherwise.
|
|
*/
|
|
private boolean canReassignAudioFocus() {
|
|
// focus requests are rejected during a phone call or when the phone is ringing
|
|
// this is equivalent to IN_VOICE_COMM_FOCUS_ID having the focus
|
|
if (!mFocusStack.isEmpty() && IN_VOICE_COMM_FOCUS_ID.equals(mFocusStack.peek().mClientId)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Inner class to monitor audio focus client deaths, and remove them from the audio focus
|
|
* stack if necessary.
|
|
*/
|
|
private class AudioFocusDeathHandler implements IBinder.DeathRecipient {
|
|
private IBinder mCb; // To be notified of client's death
|
|
|
|
AudioFocusDeathHandler(IBinder cb) {
|
|
mCb = cb;
|
|
}
|
|
|
|
public void binderDied() {
|
|
synchronized(mAudioFocusLock) {
|
|
Log.w(TAG, " AudioFocus audio focus client died");
|
|
removeFocusStackEntryForClient(mCb);
|
|
}
|
|
}
|
|
|
|
public IBinder getBinder() {
|
|
return mCb;
|
|
}
|
|
}
|
|
|
|
|
|
/** @see AudioManager#requestAudioFocus(IAudioFocusDispatcher, int, int) */
|
|
public int requestAudioFocus(int mainStreamType, int focusChangeHint, IBinder cb,
|
|
IAudioFocusDispatcher fd, String clientId, String callingPackageName) {
|
|
Log.i(TAG, " AudioFocus requestAudioFocus() from " + clientId);
|
|
// the main stream type for the audio focus request is currently not used. It may
|
|
// potentially be used to handle multiple stream type-dependent audio focuses.
|
|
|
|
// we need a valid binder callback for clients
|
|
if (!cb.pingBinder()) {
|
|
Log.e(TAG, " AudioFocus DOA client for requestAudioFocus(), aborting.");
|
|
return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
|
|
}
|
|
|
|
synchronized(mAudioFocusLock) {
|
|
if (!canReassignAudioFocus()) {
|
|
return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
|
|
}
|
|
|
|
// handle the potential premature death of the new holder of the focus
|
|
// (premature death == death before abandoning focus)
|
|
// Register for client death notification
|
|
AudioFocusDeathHandler afdh = new AudioFocusDeathHandler(cb);
|
|
try {
|
|
cb.linkToDeath(afdh, 0);
|
|
} catch (RemoteException e) {
|
|
// client has already died!
|
|
Log.w(TAG, "AudioFocus requestAudioFocus() could not link to "+cb+" binder death");
|
|
return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
|
|
}
|
|
|
|
if (!mFocusStack.empty() && mFocusStack.peek().mClientId.equals(clientId)) {
|
|
// if focus is already owned by this client and the reason for acquiring the focus
|
|
// hasn't changed, don't do anything
|
|
if (mFocusStack.peek().mFocusChangeType == focusChangeHint) {
|
|
// unlink death handler so it can be gc'ed.
|
|
// linkToDeath() creates a JNI global reference preventing collection.
|
|
cb.unlinkToDeath(afdh, 0);
|
|
return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
|
|
}
|
|
// the reason for the audio focus request has changed: remove the current top of
|
|
// stack and respond as if we had a new focus owner
|
|
FocusStackEntry fse = mFocusStack.pop();
|
|
fse.unlinkToDeath();
|
|
}
|
|
|
|
// notify current top of stack it is losing focus
|
|
if (!mFocusStack.empty() && (mFocusStack.peek().mFocusDispatcher != null)) {
|
|
try {
|
|
mFocusStack.peek().mFocusDispatcher.dispatchAudioFocusChange(
|
|
-1 * focusChangeHint, // loss and gain codes are inverse of each other
|
|
mFocusStack.peek().mClientId);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, " Failure to signal loss of focus due to "+ e);
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
|
|
// focus requester might already be somewhere below in the stack, remove it
|
|
removeFocusStackEntry(clientId, false /* signal */);
|
|
|
|
// push focus requester at the top of the audio focus stack
|
|
mFocusStack.push(new FocusStackEntry(mainStreamType, focusChangeHint, fd, cb,
|
|
clientId, afdh, callingPackageName, Binder.getCallingUid()));
|
|
|
|
// there's a new top of the stack, let the remote control know
|
|
synchronized(mRCStack) {
|
|
checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
|
|
}
|
|
}//synchronized(mAudioFocusLock)
|
|
|
|
return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
|
|
}
|
|
|
|
/** @see AudioManager#abandonAudioFocus(IAudioFocusDispatcher) */
|
|
public int abandonAudioFocus(IAudioFocusDispatcher fl, String clientId) {
|
|
Log.i(TAG, " AudioFocus abandonAudioFocus() from " + clientId);
|
|
try {
|
|
// this will take care of notifying the new focus owner if needed
|
|
synchronized(mAudioFocusLock) {
|
|
removeFocusStackEntry(clientId, true);
|
|
}
|
|
} catch (java.util.ConcurrentModificationException cme) {
|
|
// Catching this exception here is temporary. It is here just to prevent
|
|
// a crash seen when the "Silent" notification is played. This is believed to be fixed
|
|
// but this try catch block is left just to be safe.
|
|
Log.e(TAG, "FATAL EXCEPTION AudioFocus abandonAudioFocus() caused " + cme);
|
|
cme.printStackTrace();
|
|
}
|
|
|
|
return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
|
|
}
|
|
|
|
|
|
public void unregisterAudioFocusClient(String clientId) {
|
|
synchronized(mAudioFocusLock) {
|
|
removeFocusStackEntry(clientId, false);
|
|
}
|
|
}
|
|
|
|
|
|
//==========================================================================================
|
|
// RemoteControl
|
|
//==========================================================================================
|
|
public void dispatchMediaKeyEvent(KeyEvent keyEvent) {
|
|
filterMediaKeyEvent(keyEvent, false /*needWakeLock*/);
|
|
}
|
|
|
|
public void dispatchMediaKeyEventUnderWakelock(KeyEvent keyEvent) {
|
|
filterMediaKeyEvent(keyEvent, true /*needWakeLock*/);
|
|
}
|
|
|
|
private void filterMediaKeyEvent(KeyEvent keyEvent, boolean needWakeLock) {
|
|
// sanity check on the incoming key event
|
|
if (!isValidMediaKeyEvent(keyEvent)) {
|
|
Log.e(TAG, "not dispatching invalid media key event " + keyEvent);
|
|
return;
|
|
}
|
|
// event filtering for telephony
|
|
synchronized(mRingingLock) {
|
|
synchronized(mRCStack) {
|
|
if ((mMediaReceiverForCalls != null) &&
|
|
(mIsRinging || (getMode() == AudioSystem.MODE_IN_CALL))) {
|
|
dispatchMediaKeyEventForCalls(keyEvent, needWakeLock);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
// event filtering based on voice-based interactions
|
|
if (isValidVoiceInputKeyCode(keyEvent.getKeyCode())) {
|
|
filterVoiceInputKeyEvent(keyEvent, needWakeLock);
|
|
} else {
|
|
dispatchMediaKeyEvent(keyEvent, needWakeLock);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles the dispatching of the media button events to the telephony package.
|
|
* Precondition: mMediaReceiverForCalls != null
|
|
* @param keyEvent a non-null KeyEvent whose key code is one of the supported media buttons
|
|
* @param needWakeLock true if a PARTIAL_WAKE_LOCK needs to be held while this key event
|
|
* is dispatched.
|
|
*/
|
|
private void dispatchMediaKeyEventForCalls(KeyEvent keyEvent, boolean needWakeLock) {
|
|
Intent keyIntent = new Intent(Intent.ACTION_MEDIA_BUTTON, null);
|
|
keyIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
|
|
keyIntent.setPackage(mMediaReceiverForCalls.getPackageName());
|
|
if (needWakeLock) {
|
|
mMediaEventWakeLock.acquire();
|
|
keyIntent.putExtra(EXTRA_WAKELOCK_ACQUIRED, WAKELOCK_RELEASE_ON_FINISHED);
|
|
}
|
|
final long ident = Binder.clearCallingIdentity();
|
|
try {
|
|
mContext.sendOrderedBroadcastAsUser(keyIntent, UserHandle.ALL,
|
|
null, mKeyEventDone, mAudioHandler, Activity.RESULT_OK, null, null);
|
|
} finally {
|
|
Binder.restoreCallingIdentity(ident);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles the dispatching of the media button events to one of the registered listeners,
|
|
* or if there was none, broadcast an ACTION_MEDIA_BUTTON intent to the rest of the system.
|
|
* @param keyEvent a non-null KeyEvent whose key code is one of the supported media buttons
|
|
* @param needWakeLock true if a PARTIAL_WAKE_LOCK needs to be held while this key event
|
|
* is dispatched.
|
|
*/
|
|
private void dispatchMediaKeyEvent(KeyEvent keyEvent, boolean needWakeLock) {
|
|
if (needWakeLock) {
|
|
mMediaEventWakeLock.acquire();
|
|
}
|
|
Intent keyIntent = new Intent(Intent.ACTION_MEDIA_BUTTON, null);
|
|
keyIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
|
|
synchronized(mRCStack) {
|
|
if (!mRCStack.empty()) {
|
|
// send the intent that was registered by the client
|
|
try {
|
|
mRCStack.peek().mMediaIntent.send(mContext,
|
|
needWakeLock ? WAKELOCK_RELEASE_ON_FINISHED : 0 /*code*/,
|
|
keyIntent, AudioService.this, mAudioHandler);
|
|
} catch (CanceledException e) {
|
|
Log.e(TAG, "Error sending pending intent " + mRCStack.peek());
|
|
e.printStackTrace();
|
|
}
|
|
} else {
|
|
// legacy behavior when nobody registered their media button event receiver
|
|
// through AudioManager
|
|
if (needWakeLock) {
|
|
keyIntent.putExtra(EXTRA_WAKELOCK_ACQUIRED, WAKELOCK_RELEASE_ON_FINISHED);
|
|
}
|
|
final long ident = Binder.clearCallingIdentity();
|
|
try {
|
|
mContext.sendOrderedBroadcastAsUser(keyIntent, UserHandle.ALL,
|
|
null, mKeyEventDone,
|
|
mAudioHandler, Activity.RESULT_OK, null, null);
|
|
} finally {
|
|
Binder.restoreCallingIdentity(ident);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The different actions performed in response to a voice button key event.
|
|
*/
|
|
private final static int VOICEBUTTON_ACTION_DISCARD_CURRENT_KEY_PRESS = 1;
|
|
private final static int VOICEBUTTON_ACTION_START_VOICE_INPUT = 2;
|
|
private final static int VOICEBUTTON_ACTION_SIMULATE_KEY_PRESS = 3;
|
|
|
|
private final Object mVoiceEventLock = new Object();
|
|
private boolean mVoiceButtonDown;
|
|
private boolean mVoiceButtonHandled;
|
|
|
|
/**
|
|
* Filter key events that may be used for voice-based interactions
|
|
* @param keyEvent a non-null KeyEvent whose key code is that of one of the supported
|
|
* media buttons that can be used to trigger voice-based interactions.
|
|
* @param needWakeLock true if a PARTIAL_WAKE_LOCK needs to be held while this key event
|
|
* is dispatched.
|
|
*/
|
|
private void filterVoiceInputKeyEvent(KeyEvent keyEvent, boolean needWakeLock) {
|
|
if (DEBUG_RC) {
|
|
Log.v(TAG, "voice input key event: " + keyEvent + ", needWakeLock=" + needWakeLock);
|
|
}
|
|
|
|
int voiceButtonAction = VOICEBUTTON_ACTION_DISCARD_CURRENT_KEY_PRESS;
|
|
int keyAction = keyEvent.getAction();
|
|
synchronized (mVoiceEventLock) {
|
|
if (keyAction == KeyEvent.ACTION_DOWN) {
|
|
if (keyEvent.getRepeatCount() == 0) {
|
|
// initial down
|
|
mVoiceButtonDown = true;
|
|
mVoiceButtonHandled = false;
|
|
} else if (mVoiceButtonDown && !mVoiceButtonHandled
|
|
&& (keyEvent.getFlags() & KeyEvent.FLAG_LONG_PRESS) != 0) {
|
|
// long-press, start voice-based interactions
|
|
mVoiceButtonHandled = true;
|
|
voiceButtonAction = VOICEBUTTON_ACTION_START_VOICE_INPUT;
|
|
}
|
|
} else if (keyAction == KeyEvent.ACTION_UP) {
|
|
if (mVoiceButtonDown) {
|
|
// voice button up
|
|
mVoiceButtonDown = false;
|
|
if (!mVoiceButtonHandled && !keyEvent.isCanceled()) {
|
|
voiceButtonAction = VOICEBUTTON_ACTION_SIMULATE_KEY_PRESS;
|
|
}
|
|
}
|
|
}
|
|
}//synchronized (mVoiceEventLock)
|
|
|
|
// take action after media button event filtering for voice-based interactions
|
|
switch (voiceButtonAction) {
|
|
case VOICEBUTTON_ACTION_DISCARD_CURRENT_KEY_PRESS:
|
|
if (DEBUG_RC) Log.v(TAG, " ignore key event");
|
|
break;
|
|
case VOICEBUTTON_ACTION_START_VOICE_INPUT:
|
|
if (DEBUG_RC) Log.v(TAG, " start voice-based interactions");
|
|
// then start the voice-based interactions
|
|
startVoiceBasedInteractions(needWakeLock);
|
|
break;
|
|
case VOICEBUTTON_ACTION_SIMULATE_KEY_PRESS:
|
|
if (DEBUG_RC) Log.v(TAG, " send simulated key event, wakelock=" + needWakeLock);
|
|
sendSimulatedMediaButtonEvent(keyEvent, needWakeLock);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void sendSimulatedMediaButtonEvent(KeyEvent originalKeyEvent, boolean needWakeLock) {
|
|
// send DOWN event
|
|
KeyEvent keyEvent = KeyEvent.changeAction(originalKeyEvent, KeyEvent.ACTION_DOWN);
|
|
dispatchMediaKeyEvent(keyEvent, needWakeLock);
|
|
// send UP event
|
|
keyEvent = KeyEvent.changeAction(originalKeyEvent, KeyEvent.ACTION_UP);
|
|
dispatchMediaKeyEvent(keyEvent, needWakeLock);
|
|
|
|
}
|
|
|
|
|
|
private static boolean isValidMediaKeyEvent(KeyEvent keyEvent) {
|
|
if (keyEvent == null) {
|
|
return false;
|
|
}
|
|
final int keyCode = keyEvent.getKeyCode();
|
|
switch (keyCode) {
|
|
case KeyEvent.KEYCODE_MUTE:
|
|
case KeyEvent.KEYCODE_HEADSETHOOK:
|
|
case KeyEvent.KEYCODE_MEDIA_PLAY:
|
|
case KeyEvent.KEYCODE_MEDIA_PAUSE:
|
|
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
|
|
case KeyEvent.KEYCODE_MEDIA_STOP:
|
|
case KeyEvent.KEYCODE_MEDIA_NEXT:
|
|
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
|
|
case KeyEvent.KEYCODE_MEDIA_REWIND:
|
|
case KeyEvent.KEYCODE_MEDIA_RECORD:
|
|
case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
|
|
case KeyEvent.KEYCODE_MEDIA_CLOSE:
|
|
case KeyEvent.KEYCODE_MEDIA_EJECT:
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Checks whether the given key code is one that can trigger the launch of voice-based
|
|
* interactions.
|
|
* @param keyCode the key code associated with the key event
|
|
* @return true if the key is one of the supported voice-based interaction triggers
|
|
*/
|
|
private static boolean isValidVoiceInputKeyCode(int keyCode) {
|
|
if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tell the system to start voice-based interactions / voice commands
|
|
*/
|
|
private void startVoiceBasedInteractions(boolean needWakeLock) {
|
|
Intent voiceIntent = null;
|
|
// select which type of search to launch:
|
|
// - screen on and device unlocked: action is ACTION_WEB_SEARCH
|
|
// - device locked or screen off: action is ACTION_VOICE_SEARCH_HANDS_FREE
|
|
// with EXTRA_SECURE set to true if the device is securely locked
|
|
PowerManager pm = (PowerManager)mContext.getSystemService(Context.POWER_SERVICE);
|
|
boolean isLocked = mKeyguardManager != null && mKeyguardManager.isKeyguardLocked();
|
|
if (!isLocked && pm.isScreenOn()) {
|
|
voiceIntent = new Intent(android.speech.RecognizerIntent.ACTION_WEB_SEARCH);
|
|
} else {
|
|
voiceIntent = new Intent(RecognizerIntent.ACTION_VOICE_SEARCH_HANDS_FREE);
|
|
voiceIntent.putExtra(RecognizerIntent.EXTRA_SECURE,
|
|
isLocked && mKeyguardManager.isKeyguardSecure());
|
|
}
|
|
// start the search activity
|
|
if (needWakeLock) {
|
|
mMediaEventWakeLock.acquire();
|
|
}
|
|
try {
|
|
if (voiceIntent != null) {
|
|
voiceIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
|
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
|
|
mContext.startActivity(voiceIntent);
|
|
}
|
|
} catch (ActivityNotFoundException e) {
|
|
Log.w(TAG, "No activity for search: " + e);
|
|
} finally {
|
|
if (needWakeLock) {
|
|
mMediaEventWakeLock.release();
|
|
}
|
|
}
|
|
}
|
|
|
|
private PowerManager.WakeLock mMediaEventWakeLock;
|
|
|
|
private static final int WAKELOCK_RELEASE_ON_FINISHED = 1980; //magic number
|
|
|
|
// only set when wakelock was acquired, no need to check value when received
|
|
private static final String EXTRA_WAKELOCK_ACQUIRED =
|
|
"android.media.AudioService.WAKELOCK_ACQUIRED";
|
|
|
|
public void onSendFinished(PendingIntent pendingIntent, Intent intent,
|
|
int resultCode, String resultData, Bundle resultExtras) {
|
|
if (resultCode == WAKELOCK_RELEASE_ON_FINISHED) {
|
|
mMediaEventWakeLock.release();
|
|
}
|
|
}
|
|
|
|
BroadcastReceiver mKeyEventDone = new BroadcastReceiver() {
|
|
public void onReceive(Context context, Intent intent) {
|
|
if (intent == null) {
|
|
return;
|
|
}
|
|
Bundle extras = intent.getExtras();
|
|
if (extras == null) {
|
|
return;
|
|
}
|
|
if (extras.containsKey(EXTRA_WAKELOCK_ACQUIRED)) {
|
|
mMediaEventWakeLock.release();
|
|
}
|
|
}
|
|
};
|
|
|
|
private final Object mCurrentRcLock = new Object();
|
|
/**
|
|
* The one remote control client which will receive a request for display information.
|
|
* This object may be null.
|
|
* Access protected by mCurrentRcLock.
|
|
*/
|
|
private IRemoteControlClient mCurrentRcClient = null;
|
|
|
|
private final static int RC_INFO_NONE = 0;
|
|
private final static int RC_INFO_ALL =
|
|
RemoteControlClient.FLAG_INFORMATION_REQUEST_ALBUM_ART |
|
|
RemoteControlClient.FLAG_INFORMATION_REQUEST_KEY_MEDIA |
|
|
RemoteControlClient.FLAG_INFORMATION_REQUEST_METADATA |
|
|
RemoteControlClient.FLAG_INFORMATION_REQUEST_PLAYSTATE;
|
|
|
|
/**
|
|
* A monotonically increasing generation counter for mCurrentRcClient.
|
|
* Only accessed with a lock on mCurrentRcLock.
|
|
* No value wrap-around issues as we only act on equal values.
|
|
*/
|
|
private int mCurrentRcClientGen = 0;
|
|
|
|
/**
|
|
* Inner class to monitor remote control client deaths, and remove the client for the
|
|
* remote control stack if necessary.
|
|
*/
|
|
private class RcClientDeathHandler implements IBinder.DeathRecipient {
|
|
private IBinder mCb; // To be notified of client's death
|
|
private PendingIntent mMediaIntent;
|
|
|
|
RcClientDeathHandler(IBinder cb, PendingIntent pi) {
|
|
mCb = cb;
|
|
mMediaIntent = pi;
|
|
}
|
|
|
|
public void binderDied() {
|
|
Log.w(TAG, " RemoteControlClient died");
|
|
// remote control client died, make sure the displays don't use it anymore
|
|
// by setting its remote control client to null
|
|
registerRemoteControlClient(mMediaIntent, null/*rcClient*/, null/*ignored*/);
|
|
// the dead client was maybe handling remote playback, reevaluate
|
|
postReevaluateRemote();
|
|
}
|
|
|
|
public IBinder getBinder() {
|
|
return mCb;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A global counter for RemoteControlClient identifiers
|
|
*/
|
|
private static int sLastRccId = 0;
|
|
|
|
private class RemotePlaybackState {
|
|
int mRccId;
|
|
int mVolume;
|
|
int mVolumeMax;
|
|
int mVolumeHandling;
|
|
|
|
private RemotePlaybackState(int id, int vol, int volMax) {
|
|
mRccId = id;
|
|
mVolume = vol;
|
|
mVolumeMax = volMax;
|
|
mVolumeHandling = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME_HANDLING;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Internal cache for the playback information of the RemoteControlClient whose volume gets to
|
|
* be controlled by the volume keys ("main"), so we don't have to iterate over the RC stack
|
|
* every time we need this info.
|
|
*/
|
|
private RemotePlaybackState mMainRemote;
|
|
/**
|
|
* Indicates whether the "main" RemoteControlClient is considered active.
|
|
* Use synchronized on mMainRemote.
|
|
*/
|
|
private boolean mMainRemoteIsActive;
|
|
/**
|
|
* Indicates whether there is remote playback going on. True even if there is no "active"
|
|
* remote playback (mMainRemoteIsActive is false), but a RemoteControlClient has declared it
|
|
* handles remote playback.
|
|
* Use synchronized on mMainRemote.
|
|
*/
|
|
private boolean mHasRemotePlayback;
|
|
|
|
private static class RemoteControlStackEntry {
|
|
public int mRccId = RemoteControlClient.RCSE_ID_UNREGISTERED;
|
|
/**
|
|
* The target for the ACTION_MEDIA_BUTTON events.
|
|
* Always non null.
|
|
*/
|
|
public PendingIntent mMediaIntent;
|
|
/**
|
|
* The registered media button event receiver.
|
|
* Always non null.
|
|
*/
|
|
public ComponentName mReceiverComponent;
|
|
public String mCallingPackageName;
|
|
public int mCallingUid;
|
|
/**
|
|
* Provides access to the information to display on the remote control.
|
|
* May be null (when a media button event receiver is registered,
|
|
* but no remote control client has been registered) */
|
|
public IRemoteControlClient mRcClient;
|
|
public RcClientDeathHandler mRcClientDeathHandler;
|
|
/**
|
|
* Information only used for non-local playback
|
|
*/
|
|
public int mPlaybackType;
|
|
public int mPlaybackVolume;
|
|
public int mPlaybackVolumeMax;
|
|
public int mPlaybackVolumeHandling;
|
|
public int mPlaybackStream;
|
|
public int mPlaybackState;
|
|
public IRemoteVolumeObserver mRemoteVolumeObs;
|
|
|
|
public void resetPlaybackInfo() {
|
|
mPlaybackType = RemoteControlClient.PLAYBACK_TYPE_LOCAL;
|
|
mPlaybackVolume = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME;
|
|
mPlaybackVolumeMax = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME;
|
|
mPlaybackVolumeHandling = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME_HANDLING;
|
|
mPlaybackStream = AudioManager.STREAM_MUSIC;
|
|
mPlaybackState = RemoteControlClient.PLAYSTATE_STOPPED;
|
|
mRemoteVolumeObs = null;
|
|
}
|
|
|
|
/** precondition: mediaIntent != null, eventReceiver != null */
|
|
public RemoteControlStackEntry(PendingIntent mediaIntent, ComponentName eventReceiver) {
|
|
mMediaIntent = mediaIntent;
|
|
mReceiverComponent = eventReceiver;
|
|
mCallingUid = -1;
|
|
mRcClient = null;
|
|
mRccId = ++sLastRccId;
|
|
|
|
resetPlaybackInfo();
|
|
}
|
|
|
|
public void unlinkToRcClientDeath() {
|
|
if ((mRcClientDeathHandler != null) && (mRcClientDeathHandler.mCb != null)) {
|
|
try {
|
|
mRcClientDeathHandler.mCb.unlinkToDeath(mRcClientDeathHandler, 0);
|
|
mRcClientDeathHandler = null;
|
|
} catch (java.util.NoSuchElementException e) {
|
|
// not much we can do here
|
|
Log.e(TAG, "Encountered " + e + " in unlinkToRcClientDeath()");
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void finalize() throws Throwable {
|
|
unlinkToRcClientDeath();// unlink exception handled inside method
|
|
super.finalize();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The stack of remote control event receivers.
|
|
* Code sections and methods that modify the remote control event receiver stack are
|
|
* synchronized on mRCStack, but also BEFORE on mFocusLock as any change in either
|
|
* stack, audio focus or RC, can lead to a change in the remote control display
|
|
*/
|
|
private final Stack<RemoteControlStackEntry> mRCStack = new Stack<RemoteControlStackEntry>();
|
|
|
|
/**
|
|
* The component the telephony package can register so telephony calls have priority to
|
|
* handle media button events
|
|
*/
|
|
private ComponentName mMediaReceiverForCalls = null;
|
|
|
|
/**
|
|
* Helper function:
|
|
* Display in the log the current entries in the remote control focus stack
|
|
*/
|
|
private void dumpRCStack(PrintWriter pw) {
|
|
pw.println("\nRemote Control stack entries:");
|
|
synchronized(mRCStack) {
|
|
Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
|
|
while(stackIterator.hasNext()) {
|
|
RemoteControlStackEntry rcse = stackIterator.next();
|
|
pw.println(" pi: " + rcse.mMediaIntent +
|
|
" -- ercvr: " + rcse.mReceiverComponent +
|
|
" -- client: " + rcse.mRcClient +
|
|
" -- uid: " + rcse.mCallingUid +
|
|
" -- type: " + rcse.mPlaybackType +
|
|
" state: " + rcse.mPlaybackState);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function:
|
|
* Display in the log the current entries in the remote control stack, focusing
|
|
* on RemoteControlClient data
|
|
*/
|
|
private void dumpRCCStack(PrintWriter pw) {
|
|
pw.println("\nRemote Control Client stack entries:");
|
|
synchronized(mRCStack) {
|
|
Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
|
|
while(stackIterator.hasNext()) {
|
|
RemoteControlStackEntry rcse = stackIterator.next();
|
|
pw.println(" uid: " + rcse.mCallingUid +
|
|
" -- id: " + rcse.mRccId +
|
|
" -- type: " + rcse.mPlaybackType +
|
|
" -- state: " + rcse.mPlaybackState +
|
|
" -- vol handling: " + rcse.mPlaybackVolumeHandling +
|
|
" -- vol: " + rcse.mPlaybackVolume +
|
|
" -- volMax: " + rcse.mPlaybackVolumeMax +
|
|
" -- volObs: " + rcse.mRemoteVolumeObs);
|
|
|
|
}
|
|
}
|
|
synchronized (mMainRemote) {
|
|
pw.println("\nRemote Volume State:");
|
|
pw.println(" has remote: " + mHasRemotePlayback);
|
|
pw.println(" is remote active: " + mMainRemoteIsActive);
|
|
pw.println(" rccId: " + mMainRemote.mRccId);
|
|
pw.println(" volume handling: "
|
|
+ ((mMainRemote.mVolumeHandling == RemoteControlClient.PLAYBACK_VOLUME_FIXED) ?
|
|
"PLAYBACK_VOLUME_FIXED(0)" : "PLAYBACK_VOLUME_VARIABLE(1)"));
|
|
pw.println(" volume: " + mMainRemote.mVolume);
|
|
pw.println(" volume steps: " + mMainRemote.mVolumeMax);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function:
|
|
* Remove any entry in the remote control stack that has the same package name as packageName
|
|
* Pre-condition: packageName != null
|
|
*/
|
|
private void removeMediaButtonReceiverForPackage(String packageName) {
|
|
synchronized(mRCStack) {
|
|
if (mRCStack.empty()) {
|
|
return;
|
|
} else {
|
|
RemoteControlStackEntry oldTop = mRCStack.peek();
|
|
Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
|
|
// iterate over the stack entries
|
|
while(stackIterator.hasNext()) {
|
|
RemoteControlStackEntry rcse = (RemoteControlStackEntry)stackIterator.next();
|
|
if (packageName.equalsIgnoreCase(rcse.mReceiverComponent.getPackageName())) {
|
|
// a stack entry is from the package being removed, remove it from the stack
|
|
stackIterator.remove();
|
|
rcse.unlinkToRcClientDeath();
|
|
}
|
|
}
|
|
if (mRCStack.empty()) {
|
|
// no saved media button receiver
|
|
mAudioHandler.sendMessage(
|
|
mAudioHandler.obtainMessage(MSG_PERSIST_MEDIABUTTONRECEIVER, 0, 0,
|
|
null));
|
|
} else if (oldTop != mRCStack.peek()) {
|
|
// the top of the stack has changed, save it in the system settings
|
|
// by posting a message to persist it
|
|
mAudioHandler.sendMessage(
|
|
mAudioHandler.obtainMessage(MSG_PERSIST_MEDIABUTTONRECEIVER, 0, 0,
|
|
mRCStack.peek().mReceiverComponent));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function:
|
|
* Restore remote control receiver from the system settings.
|
|
*/
|
|
private void restoreMediaButtonReceiver() {
|
|
String receiverName = Settings.System.getString(mContentResolver,
|
|
Settings.System.MEDIA_BUTTON_RECEIVER);
|
|
if ((null != receiverName) && !receiverName.isEmpty()) {
|
|
ComponentName eventReceiver = ComponentName.unflattenFromString(receiverName);
|
|
// construct a PendingIntent targeted to the restored component name
|
|
// for the media button and register it
|
|
Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
|
|
// the associated intent will be handled by the component being registered
|
|
mediaButtonIntent.setComponent(eventReceiver);
|
|
PendingIntent pi = PendingIntent.getBroadcast(mContext,
|
|
0/*requestCode, ignored*/, mediaButtonIntent, 0/*flags*/);
|
|
registerMediaButtonIntent(pi, eventReceiver);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function:
|
|
* Set the new remote control receiver at the top of the RC focus stack.
|
|
* precondition: mediaIntent != null, target != null
|
|
*/
|
|
private void pushMediaButtonReceiver(PendingIntent mediaIntent, ComponentName target) {
|
|
// already at top of stack?
|
|
if (!mRCStack.empty() && mRCStack.peek().mMediaIntent.equals(mediaIntent)) {
|
|
return;
|
|
}
|
|
Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
|
|
RemoteControlStackEntry rcse = null;
|
|
boolean wasInsideStack = false;
|
|
while(stackIterator.hasNext()) {
|
|
rcse = (RemoteControlStackEntry)stackIterator.next();
|
|
if(rcse.mMediaIntent.equals(mediaIntent)) {
|
|
wasInsideStack = true;
|
|
stackIterator.remove();
|
|
break;
|
|
}
|
|
}
|
|
if (!wasInsideStack) {
|
|
rcse = new RemoteControlStackEntry(mediaIntent, target);
|
|
}
|
|
mRCStack.push(rcse);
|
|
|
|
// post message to persist the default media button receiver
|
|
mAudioHandler.sendMessage( mAudioHandler.obtainMessage(
|
|
MSG_PERSIST_MEDIABUTTONRECEIVER, 0, 0, target/*obj*/) );
|
|
}
|
|
|
|
/**
|
|
* Helper function:
|
|
* Remove the remote control receiver from the RC focus stack.
|
|
* precondition: pi != null
|
|
*/
|
|
private void removeMediaButtonReceiver(PendingIntent pi) {
|
|
Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
|
|
while(stackIterator.hasNext()) {
|
|
RemoteControlStackEntry rcse = (RemoteControlStackEntry)stackIterator.next();
|
|
if(rcse.mMediaIntent.equals(pi)) {
|
|
stackIterator.remove();
|
|
rcse.unlinkToRcClientDeath();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function:
|
|
* Called synchronized on mRCStack
|
|
*/
|
|
private boolean isCurrentRcController(PendingIntent pi) {
|
|
if (!mRCStack.empty() && mRCStack.peek().mMediaIntent.equals(pi)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//==========================================================================================
|
|
// Remote control display / client
|
|
//==========================================================================================
|
|
/**
|
|
* Update the remote control displays with the new "focused" client generation
|
|
*/
|
|
private void setNewRcClientOnDisplays_syncRcsCurrc(int newClientGeneration,
|
|
PendingIntent newMediaIntent, boolean clearing) {
|
|
// NOTE: Only one IRemoteControlDisplay supported in this implementation
|
|
if (mRcDisplay != null) {
|
|
try {
|
|
mRcDisplay.setCurrentClientId(
|
|
newClientGeneration, newMediaIntent, clearing);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "Dead display in setNewRcClientOnDisplays_syncRcsCurrc() "+e);
|
|
// if we had a display before, stop monitoring its death
|
|
rcDisplay_stopDeathMonitor_syncRcStack();
|
|
mRcDisplay = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the remote control clients with the new "focused" client generation
|
|
*/
|
|
private void setNewRcClientGenerationOnClients_syncRcsCurrc(int newClientGeneration) {
|
|
Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
|
|
while(stackIterator.hasNext()) {
|
|
RemoteControlStackEntry se = stackIterator.next();
|
|
if ((se != null) && (se.mRcClient != null)) {
|
|
try {
|
|
se.mRcClient.setCurrentClientGenerationId(newClientGeneration);
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "Dead client in setNewRcClientGenerationOnClients_syncRcsCurrc()"+e);
|
|
stackIterator.remove();
|
|
se.unlinkToRcClientDeath();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the displays and clients with the new "focused" client generation and name
|
|
* @param newClientGeneration the new generation value matching a client update
|
|
* @param newClientEventReceiver the media button event receiver associated with the client.
|
|
* May be null, which implies there is no registered media button event receiver.
|
|
* @param clearing true if the new client generation value maps to a remote control update
|
|
* where the display should be cleared.
|
|
*/
|
|
private void setNewRcClient_syncRcsCurrc(int newClientGeneration,
|
|
PendingIntent newMediaIntent, boolean clearing) {
|
|
// send the new valid client generation ID to all displays
|
|
setNewRcClientOnDisplays_syncRcsCurrc(newClientGeneration, newMediaIntent, clearing);
|
|
// send the new valid client generation ID to all clients
|
|
setNewRcClientGenerationOnClients_syncRcsCurrc(newClientGeneration);
|
|
}
|
|
|
|
/**
|
|
* Called when processing MSG_RCDISPLAY_CLEAR event
|
|
*/
|
|
private void onRcDisplayClear() {
|
|
if (DEBUG_RC) Log.i(TAG, "Clear remote control display");
|
|
|
|
synchronized(mRCStack) {
|
|
synchronized(mCurrentRcLock) {
|
|
mCurrentRcClientGen++;
|
|
// synchronously update the displays and clients with the new client generation
|
|
setNewRcClient_syncRcsCurrc(mCurrentRcClientGen,
|
|
null /*newMediaIntent*/, true /*clearing*/);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when processing MSG_RCDISPLAY_UPDATE event
|
|
*/
|
|
private void onRcDisplayUpdate(RemoteControlStackEntry rcse, int flags /* USED ?*/) {
|
|
synchronized(mRCStack) {
|
|
synchronized(mCurrentRcLock) {
|
|
if ((mCurrentRcClient != null) && (mCurrentRcClient.equals(rcse.mRcClient))) {
|
|
if (DEBUG_RC) Log.i(TAG, "Display/update remote control ");
|
|
|
|
mCurrentRcClientGen++;
|
|
// synchronously update the displays and clients with
|
|
// the new client generation
|
|
setNewRcClient_syncRcsCurrc(mCurrentRcClientGen,
|
|
rcse.mMediaIntent /*newMediaIntent*/,
|
|
false /*clearing*/);
|
|
|
|
// tell the current client that it needs to send info
|
|
try {
|
|
mCurrentRcClient.onInformationRequested(mCurrentRcClientGen,
|
|
flags, mArtworkExpectedWidth, mArtworkExpectedHeight);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "Current valid remote client is dead: "+e);
|
|
mCurrentRcClient = null;
|
|
}
|
|
} else {
|
|
// the remote control display owner has changed between the
|
|
// the message to update the display was sent, and the time it
|
|
// gets to be processed (now)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Helper function:
|
|
* Called synchronized on mRCStack
|
|
*/
|
|
private void clearRemoteControlDisplay_syncAfRcs() {
|
|
synchronized(mCurrentRcLock) {
|
|
mCurrentRcClient = null;
|
|
}
|
|
// will cause onRcDisplayClear() to be called in AudioService's handler thread
|
|
mAudioHandler.sendMessage( mAudioHandler.obtainMessage(MSG_RCDISPLAY_CLEAR) );
|
|
}
|
|
|
|
/**
|
|
* Helper function for code readability: only to be called from
|
|
* checkUpdateRemoteControlDisplay_syncAfRcs() which checks the preconditions for
|
|
* this method.
|
|
* Preconditions:
|
|
* - called synchronized mAudioFocusLock then on mRCStack
|
|
* - mRCStack.isEmpty() is false
|
|
*/
|
|
private void updateRemoteControlDisplay_syncAfRcs(int infoChangedFlags) {
|
|
RemoteControlStackEntry rcse = mRCStack.peek();
|
|
int infoFlagsAboutToBeUsed = infoChangedFlags;
|
|
// this is where we enforce opt-in for information display on the remote controls
|
|
// with the new AudioManager.registerRemoteControlClient() API
|
|
if (rcse.mRcClient == null) {
|
|
//Log.w(TAG, "Can't update remote control display with null remote control client");
|
|
clearRemoteControlDisplay_syncAfRcs();
|
|
return;
|
|
}
|
|
synchronized(mCurrentRcLock) {
|
|
if (!rcse.mRcClient.equals(mCurrentRcClient)) {
|
|
// new RC client, assume every type of information shall be queried
|
|
infoFlagsAboutToBeUsed = RC_INFO_ALL;
|
|
}
|
|
mCurrentRcClient = rcse.mRcClient;
|
|
}
|
|
// will cause onRcDisplayUpdate() to be called in AudioService's handler thread
|
|
mAudioHandler.sendMessage( mAudioHandler.obtainMessage(MSG_RCDISPLAY_UPDATE,
|
|
infoFlagsAboutToBeUsed /* arg1 */, 0, rcse /* obj, != null */) );
|
|
}
|
|
|
|
/**
|
|
* Helper function:
|
|
* Called synchronized on mAudioFocusLock, then mRCStack
|
|
* Check whether the remote control display should be updated, triggers the update if required
|
|
* @param infoChangedFlags the flags corresponding to the remote control client information
|
|
* that has changed, if applicable (checking for the update conditions might trigger a
|
|
* clear, rather than an update event).
|
|
*/
|
|
private void checkUpdateRemoteControlDisplay_syncAfRcs(int infoChangedFlags) {
|
|
// determine whether the remote control display should be refreshed
|
|
// if either stack is empty, there is a mismatch, so clear the RC display
|
|
if (mRCStack.isEmpty() || mFocusStack.isEmpty()) {
|
|
clearRemoteControlDisplay_syncAfRcs();
|
|
return;
|
|
}
|
|
|
|
// determine which entry in the AudioFocus stack to consider, and compare against the
|
|
// top of the stack for the media button event receivers : simply using the top of the
|
|
// stack would make the entry disappear from the RemoteControlDisplay in conditions such as
|
|
// notifications playing during music playback.
|
|
// crawl the AudioFocus stack until an entry is found with the following characteristics:
|
|
// - focus gain on STREAM_MUSIC stream
|
|
// - non-transient focus gain on a stream other than music
|
|
FocusStackEntry af = null;
|
|
Iterator<FocusStackEntry> stackIterator = mFocusStack.iterator();
|
|
while(stackIterator.hasNext()) {
|
|
FocusStackEntry fse = (FocusStackEntry)stackIterator.next();
|
|
if ((fse.mStreamType == AudioManager.STREAM_MUSIC)
|
|
|| (fse.mFocusChangeType == AudioManager.AUDIOFOCUS_GAIN)) {
|
|
af = fse;
|
|
break;
|
|
}
|
|
}
|
|
if (af == null) {
|
|
clearRemoteControlDisplay_syncAfRcs();
|
|
return;
|
|
}
|
|
|
|
// if the audio focus and RC owners belong to different packages, there is a mismatch, clear
|
|
if ((mRCStack.peek().mCallingPackageName != null)
|
|
&& (af.mPackageName != null)
|
|
&& !(mRCStack.peek().mCallingPackageName.compareTo(
|
|
af.mPackageName) == 0)) {
|
|
clearRemoteControlDisplay_syncAfRcs();
|
|
return;
|
|
}
|
|
// if the audio focus didn't originate from the same Uid as the one in which the remote
|
|
// control information will be retrieved, clear
|
|
if (mRCStack.peek().mCallingUid != af.mCallingUid) {
|
|
clearRemoteControlDisplay_syncAfRcs();
|
|
return;
|
|
}
|
|
// refresh conditions were verified: update the remote controls
|
|
// ok to call: synchronized mAudioFocusLock then on mRCStack, mRCStack is not empty
|
|
updateRemoteControlDisplay_syncAfRcs(infoChangedFlags);
|
|
}
|
|
|
|
/**
|
|
* see AudioManager.registerMediaButtonIntent(PendingIntent pi, ComponentName c)
|
|
* precondition: mediaIntent != null, target != null
|
|
*/
|
|
public void registerMediaButtonIntent(PendingIntent mediaIntent, ComponentName eventReceiver) {
|
|
Log.i(TAG, " Remote Control registerMediaButtonIntent() for " + mediaIntent);
|
|
|
|
synchronized(mAudioFocusLock) {
|
|
synchronized(mRCStack) {
|
|
pushMediaButtonReceiver(mediaIntent, eventReceiver);
|
|
// new RC client, assume every type of information shall be queried
|
|
checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* see AudioManager.unregisterMediaButtonIntent(PendingIntent mediaIntent)
|
|
* precondition: mediaIntent != null, eventReceiver != null
|
|
*/
|
|
public void unregisterMediaButtonIntent(PendingIntent mediaIntent, ComponentName eventReceiver)
|
|
{
|
|
Log.i(TAG, " Remote Control unregisterMediaButtonIntent() for " + mediaIntent);
|
|
|
|
synchronized(mAudioFocusLock) {
|
|
synchronized(mRCStack) {
|
|
boolean topOfStackWillChange = isCurrentRcController(mediaIntent);
|
|
removeMediaButtonReceiver(mediaIntent);
|
|
if (topOfStackWillChange) {
|
|
// current RC client will change, assume every type of info needs to be queried
|
|
checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* see AudioManager.registerMediaButtonEventReceiverForCalls(ComponentName c)
|
|
* precondition: c != null
|
|
*/
|
|
public void registerMediaButtonEventReceiverForCalls(ComponentName c) {
|
|
if (mContext.checkCallingPermission("android.permission.MODIFY_PHONE_STATE")
|
|
!= PackageManager.PERMISSION_GRANTED) {
|
|
Log.e(TAG, "Invalid permissions to register media button receiver for calls");
|
|
return;
|
|
}
|
|
synchronized(mRCStack) {
|
|
mMediaReceiverForCalls = c;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* see AudioManager.unregisterMediaButtonEventReceiverForCalls()
|
|
*/
|
|
public void unregisterMediaButtonEventReceiverForCalls() {
|
|
if (mContext.checkCallingPermission("android.permission.MODIFY_PHONE_STATE")
|
|
!= PackageManager.PERMISSION_GRANTED) {
|
|
Log.e(TAG, "Invalid permissions to unregister media button receiver for calls");
|
|
return;
|
|
}
|
|
synchronized(mRCStack) {
|
|
mMediaReceiverForCalls = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* see AudioManager.registerRemoteControlClient(ComponentName eventReceiver, ...)
|
|
* @return the unique ID of the RemoteControlStackEntry associated with the RemoteControlClient
|
|
* Note: using this method with rcClient == null is a way to "disable" the IRemoteControlClient
|
|
* without modifying the RC stack, but while still causing the display to refresh (will
|
|
* become blank as a result of this)
|
|
*/
|
|
public int registerRemoteControlClient(PendingIntent mediaIntent,
|
|
IRemoteControlClient rcClient, String callingPackageName) {
|
|
if (DEBUG_RC) Log.i(TAG, "Register remote control client rcClient="+rcClient);
|
|
int rccId = RemoteControlClient.RCSE_ID_UNREGISTERED;
|
|
synchronized(mAudioFocusLock) {
|
|
synchronized(mRCStack) {
|
|
// store the new display information
|
|
Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
|
|
while(stackIterator.hasNext()) {
|
|
RemoteControlStackEntry rcse = stackIterator.next();
|
|
if(rcse.mMediaIntent.equals(mediaIntent)) {
|
|
// already had a remote control client?
|
|
if (rcse.mRcClientDeathHandler != null) {
|
|
// stop monitoring the old client's death
|
|
rcse.unlinkToRcClientDeath();
|
|
}
|
|
// save the new remote control client
|
|
rcse.mRcClient = rcClient;
|
|
rcse.mCallingPackageName = callingPackageName;
|
|
rcse.mCallingUid = Binder.getCallingUid();
|
|
if (rcClient == null) {
|
|
// here rcse.mRcClientDeathHandler is null;
|
|
rcse.resetPlaybackInfo();
|
|
break;
|
|
}
|
|
rccId = rcse.mRccId;
|
|
|
|
// there is a new (non-null) client:
|
|
// 1/ give the new client the current display (if any)
|
|
if (mRcDisplay != null) {
|
|
try {
|
|
rcse.mRcClient.plugRemoteControlDisplay(mRcDisplay);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "Error connecting remote control display to client: "+e);
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
// 2/ monitor the new client's death
|
|
IBinder b = rcse.mRcClient.asBinder();
|
|
RcClientDeathHandler rcdh =
|
|
new RcClientDeathHandler(b, rcse.mMediaIntent);
|
|
try {
|
|
b.linkToDeath(rcdh, 0);
|
|
} catch (RemoteException e) {
|
|
// remote control client is DOA, disqualify it
|
|
Log.w(TAG, "registerRemoteControlClient() has a dead client " + b);
|
|
rcse.mRcClient = null;
|
|
}
|
|
rcse.mRcClientDeathHandler = rcdh;
|
|
break;
|
|
}
|
|
}
|
|
// if the eventReceiver is at the top of the stack
|
|
// then check for potential refresh of the remote controls
|
|
if (isCurrentRcController(mediaIntent)) {
|
|
checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
|
|
}
|
|
}
|
|
}
|
|
return rccId;
|
|
}
|
|
|
|
/**
|
|
* see AudioManager.unregisterRemoteControlClient(PendingIntent pi, ...)
|
|
* rcClient is guaranteed non-null
|
|
*/
|
|
public void unregisterRemoteControlClient(PendingIntent mediaIntent,
|
|
IRemoteControlClient rcClient) {
|
|
synchronized(mAudioFocusLock) {
|
|
synchronized(mRCStack) {
|
|
Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
|
|
while(stackIterator.hasNext()) {
|
|
RemoteControlStackEntry rcse = stackIterator.next();
|
|
if ((rcse.mMediaIntent.equals(mediaIntent))
|
|
&& rcClient.equals(rcse.mRcClient)) {
|
|
// we found the IRemoteControlClient to unregister
|
|
// stop monitoring its death
|
|
rcse.unlinkToRcClientDeath();
|
|
// reset the client-related fields
|
|
rcse.mRcClient = null;
|
|
rcse.mCallingPackageName = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The remote control displays.
|
|
* Access synchronized on mRCStack
|
|
* NOTE: Only one IRemoteControlDisplay supported in this implementation
|
|
*/
|
|
private IRemoteControlDisplay mRcDisplay;
|
|
private RcDisplayDeathHandler mRcDisplayDeathHandler;
|
|
private int mArtworkExpectedWidth = -1;
|
|
private int mArtworkExpectedHeight = -1;
|
|
/**
|
|
* Inner class to monitor remote control display deaths, and unregister them from the list
|
|
* of displays if necessary.
|
|
*/
|
|
private class RcDisplayDeathHandler implements IBinder.DeathRecipient {
|
|
private IBinder mCb; // To be notified of client's death
|
|
|
|
public RcDisplayDeathHandler(IBinder b) {
|
|
if (DEBUG_RC) Log.i(TAG, "new RcDisplayDeathHandler for "+b);
|
|
mCb = b;
|
|
}
|
|
|
|
public void binderDied() {
|
|
synchronized(mRCStack) {
|
|
Log.w(TAG, "RemoteControl: display died");
|
|
mRcDisplay = null;
|
|
}
|
|
}
|
|
|
|
public void unlinkToRcDisplayDeath() {
|
|
if (DEBUG_RC) Log.i(TAG, "unlinkToRcDisplayDeath for "+mCb);
|
|
try {
|
|
mCb.unlinkToDeath(this, 0);
|
|
} catch (java.util.NoSuchElementException e) {
|
|
// not much we can do here, the display was being unregistered anyway
|
|
Log.e(TAG, "Encountered " + e + " in unlinkToRcDisplayDeath()");
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private void rcDisplay_stopDeathMonitor_syncRcStack() {
|
|
if (mRcDisplay != null) { // implies (mRcDisplayDeathHandler != null)
|
|
// we had a display before, stop monitoring its death
|
|
mRcDisplayDeathHandler.unlinkToRcDisplayDeath();
|
|
}
|
|
}
|
|
|
|
private void rcDisplay_startDeathMonitor_syncRcStack() {
|
|
if (mRcDisplay != null) {
|
|
// new non-null display, monitor its death
|
|
IBinder b = mRcDisplay.asBinder();
|
|
mRcDisplayDeathHandler = new RcDisplayDeathHandler(b);
|
|
try {
|
|
b.linkToDeath(mRcDisplayDeathHandler, 0);
|
|
} catch (RemoteException e) {
|
|
// remote control display is DOA, disqualify it
|
|
Log.w(TAG, "registerRemoteControlDisplay() has a dead client " + b);
|
|
mRcDisplay = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register an IRemoteControlDisplay.
|
|
* Notify all IRemoteControlClient of the new display and cause the RemoteControlClient
|
|
* at the top of the stack to update the new display with its information.
|
|
* Since only one IRemoteControlDisplay is supported, this will unregister the previous display.
|
|
* @param rcd the IRemoteControlDisplay to register. No effect if null.
|
|
*/
|
|
public void registerRemoteControlDisplay(IRemoteControlDisplay rcd) {
|
|
if (DEBUG_RC) Log.d(TAG, ">>> registerRemoteControlDisplay("+rcd+")");
|
|
synchronized(mAudioFocusLock) {
|
|
synchronized(mRCStack) {
|
|
if ((mRcDisplay == rcd) || (rcd == null)) {
|
|
return;
|
|
}
|
|
// if we had a display before, stop monitoring its death
|
|
rcDisplay_stopDeathMonitor_syncRcStack();
|
|
mRcDisplay = rcd;
|
|
// new display, start monitoring its death
|
|
rcDisplay_startDeathMonitor_syncRcStack();
|
|
|
|
// let all the remote control clients there is a new display
|
|
// no need to unplug the previous because we only support one display
|
|
// and the clients don't track the death of the display
|
|
Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
|
|
while(stackIterator.hasNext()) {
|
|
RemoteControlStackEntry rcse = stackIterator.next();
|
|
if(rcse.mRcClient != null) {
|
|
try {
|
|
rcse.mRcClient.plugRemoteControlDisplay(mRcDisplay);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "Error connecting remote control display to client: " + e);
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
}
|
|
|
|
// we have a new display, of which all the clients are now aware: have it be updated
|
|
checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unregister an IRemoteControlDisplay.
|
|
* Since only one IRemoteControlDisplay is supported, this has no effect if the one to
|
|
* unregister is not the current one.
|
|
* @param rcd the IRemoteControlDisplay to unregister. No effect if null.
|
|
*/
|
|
public void unregisterRemoteControlDisplay(IRemoteControlDisplay rcd) {
|
|
if (DEBUG_RC) Log.d(TAG, "<<< unregisterRemoteControlDisplay("+rcd+")");
|
|
synchronized(mRCStack) {
|
|
// only one display here, so you can only unregister the current display
|
|
if ((rcd == null) || (rcd != mRcDisplay)) {
|
|
if (DEBUG_RC) Log.w(TAG, " trying to unregister unregistered RCD");
|
|
return;
|
|
}
|
|
// if we had a display before, stop monitoring its death
|
|
rcDisplay_stopDeathMonitor_syncRcStack();
|
|
mRcDisplay = null;
|
|
|
|
// disconnect this remote control display from all the clients
|
|
Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
|
|
while(stackIterator.hasNext()) {
|
|
RemoteControlStackEntry rcse = stackIterator.next();
|
|
if(rcse.mRcClient != null) {
|
|
try {
|
|
rcse.mRcClient.unplugRemoteControlDisplay(rcd);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "Error disconnecting remote control display to client: " + e);
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void remoteControlDisplayUsesBitmapSize(IRemoteControlDisplay rcd, int w, int h) {
|
|
synchronized(mRCStack) {
|
|
// NOTE: Only one IRemoteControlDisplay supported in this implementation
|
|
mArtworkExpectedWidth = w;
|
|
mArtworkExpectedHeight = h;
|
|
}
|
|
}
|
|
|
|
public void setPlaybackInfoForRcc(int rccId, int what, int value) {
|
|
sendMsg(mAudioHandler, MSG_RCC_NEW_PLAYBACK_INFO, SENDMSG_QUEUE,
|
|
rccId /* arg1 */, what /* arg2 */, Integer.valueOf(value) /* obj */, 0 /* delay */);
|
|
}
|
|
|
|
// handler for MSG_RCC_NEW_PLAYBACK_INFO
|
|
private void onNewPlaybackInfoForRcc(int rccId, int key, int value) {
|
|
if(DEBUG_RC) Log.d(TAG, "onNewPlaybackInfoForRcc(id=" + rccId +
|
|
", what=" + key + ",val=" + value + ")");
|
|
synchronized(mRCStack) {
|
|
Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
|
|
while(stackIterator.hasNext()) {
|
|
RemoteControlStackEntry rcse = stackIterator.next();
|
|
if (rcse.mRccId == rccId) {
|
|
switch (key) {
|
|
case RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE:
|
|
rcse.mPlaybackType = value;
|
|
postReevaluateRemote();
|
|
break;
|
|
case RemoteControlClient.PLAYBACKINFO_VOLUME:
|
|
rcse.mPlaybackVolume = value;
|
|
synchronized (mMainRemote) {
|
|
if (rccId == mMainRemote.mRccId) {
|
|
mMainRemote.mVolume = value;
|
|
mVolumePanel.postHasNewRemotePlaybackInfo();
|
|
}
|
|
}
|
|
break;
|
|
case RemoteControlClient.PLAYBACKINFO_VOLUME_MAX:
|
|
rcse.mPlaybackVolumeMax = value;
|
|
synchronized (mMainRemote) {
|
|
if (rccId == mMainRemote.mRccId) {
|
|
mMainRemote.mVolumeMax = value;
|
|
mVolumePanel.postHasNewRemotePlaybackInfo();
|
|
}
|
|
}
|
|
break;
|
|
case RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING:
|
|
rcse.mPlaybackVolumeHandling = value;
|
|
synchronized (mMainRemote) {
|
|
if (rccId == mMainRemote.mRccId) {
|
|
mMainRemote.mVolumeHandling = value;
|
|
mVolumePanel.postHasNewRemotePlaybackInfo();
|
|
}
|
|
}
|
|
break;
|
|
case RemoteControlClient.PLAYBACKINFO_USES_STREAM:
|
|
rcse.mPlaybackStream = value;
|
|
break;
|
|
case RemoteControlClient.PLAYBACKINFO_PLAYSTATE:
|
|
rcse.mPlaybackState = value;
|
|
synchronized (mMainRemote) {
|
|
if (rccId == mMainRemote.mRccId) {
|
|
mMainRemoteIsActive = isPlaystateActive(value);
|
|
postReevaluateRemote();
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
Log.e(TAG, "unhandled key " + key + " for RCC " + rccId);
|
|
break;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void registerRemoteVolumeObserverForRcc(int rccId, IRemoteVolumeObserver rvo) {
|
|
sendMsg(mAudioHandler, MSG_RCC_NEW_VOLUME_OBS, SENDMSG_QUEUE,
|
|
rccId /* arg1 */, 0, rvo /* obj */, 0 /* delay */);
|
|
}
|
|
|
|
// handler for MSG_RCC_NEW_VOLUME_OBS
|
|
private void onRegisterVolumeObserverForRcc(int rccId, IRemoteVolumeObserver rvo) {
|
|
synchronized(mRCStack) {
|
|
Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
|
|
while(stackIterator.hasNext()) {
|
|
RemoteControlStackEntry rcse = stackIterator.next();
|
|
if (rcse.mRccId == rccId) {
|
|
rcse.mRemoteVolumeObs = rvo;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if a remote client is active on the supplied stream type. Update the remote stream
|
|
* volume state if found and playing
|
|
* @param streamType
|
|
* @return false if no remote playing is currently playing
|
|
*/
|
|
private boolean checkUpdateRemoteStateIfActive(int streamType) {
|
|
synchronized(mRCStack) {
|
|
Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
|
|
while(stackIterator.hasNext()) {
|
|
RemoteControlStackEntry rcse = stackIterator.next();
|
|
if ((rcse.mPlaybackType == RemoteControlClient.PLAYBACK_TYPE_REMOTE)
|
|
&& isPlaystateActive(rcse.mPlaybackState)
|
|
&& (rcse.mPlaybackStream == streamType)) {
|
|
if (DEBUG_RC) Log.d(TAG, "remote playback active on stream " + streamType
|
|
+ ", vol =" + rcse.mPlaybackVolume);
|
|
synchronized (mMainRemote) {
|
|
mMainRemote.mRccId = rcse.mRccId;
|
|
mMainRemote.mVolume = rcse.mPlaybackVolume;
|
|
mMainRemote.mVolumeMax = rcse.mPlaybackVolumeMax;
|
|
mMainRemote.mVolumeHandling = rcse.mPlaybackVolumeHandling;
|
|
mMainRemoteIsActive = true;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
synchronized (mMainRemote) {
|
|
mMainRemoteIsActive = false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the given playback state is considered "active", i.e. it describes a state
|
|
* where playback is happening, or about to
|
|
* @param playState the playback state to evaluate
|
|
* @return true if active, false otherwise (inactive or unknown)
|
|
*/
|
|
private static boolean isPlaystateActive(int playState) {
|
|
switch (playState) {
|
|
case RemoteControlClient.PLAYSTATE_PLAYING:
|
|
case RemoteControlClient.PLAYSTATE_BUFFERING:
|
|
case RemoteControlClient.PLAYSTATE_FAST_FORWARDING:
|
|
case RemoteControlClient.PLAYSTATE_REWINDING:
|
|
case RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS:
|
|
case RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void adjustRemoteVolume(int streamType, int direction, int flags) {
|
|
int rccId = RemoteControlClient.RCSE_ID_UNREGISTERED;
|
|
boolean volFixed = false;
|
|
synchronized (mMainRemote) {
|
|
if (!mMainRemoteIsActive) {
|
|
if (DEBUG_VOL) Log.w(TAG, "adjustRemoteVolume didn't find an active client");
|
|
return;
|
|
}
|
|
rccId = mMainRemote.mRccId;
|
|
volFixed = (mMainRemote.mVolumeHandling ==
|
|
RemoteControlClient.PLAYBACK_VOLUME_FIXED);
|
|
}
|
|
// unlike "local" stream volumes, we can't compute the new volume based on the direction,
|
|
// we can only notify the remote that volume needs to be updated, and we'll get an async'
|
|
// update through setPlaybackInfoForRcc()
|
|
if (!volFixed) {
|
|
sendVolumeUpdateToRemote(rccId, direction);
|
|
}
|
|
|
|
// fire up the UI
|
|
mVolumePanel.postRemoteVolumeChanged(streamType, flags);
|
|
}
|
|
|
|
private void sendVolumeUpdateToRemote(int rccId, int direction) {
|
|
if (DEBUG_VOL) { Log.d(TAG, "sendVolumeUpdateToRemote(rccId="+rccId+" , dir="+direction); }
|
|
if (direction == 0) {
|
|
// only handling discrete events
|
|
return;
|
|
}
|
|
IRemoteVolumeObserver rvo = null;
|
|
synchronized (mRCStack) {
|
|
Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
|
|
while(stackIterator.hasNext()) {
|
|
RemoteControlStackEntry rcse = stackIterator.next();
|
|
//FIXME OPTIMIZE store this info in mMainRemote so we don't have to iterate?
|
|
if (rcse.mRccId == rccId) {
|
|
rvo = rcse.mRemoteVolumeObs;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (rvo != null) {
|
|
try {
|
|
rvo.dispatchRemoteVolumeUpdate(direction, -1);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "Error dispatching relative volume update", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
public int getRemoteStreamMaxVolume() {
|
|
synchronized (mMainRemote) {
|
|
if (mMainRemote.mRccId == RemoteControlClient.RCSE_ID_UNREGISTERED) {
|
|
return 0;
|
|
}
|
|
return mMainRemote.mVolumeMax;
|
|
}
|
|
}
|
|
|
|
public int getRemoteStreamVolume() {
|
|
synchronized (mMainRemote) {
|
|
if (mMainRemote.mRccId == RemoteControlClient.RCSE_ID_UNREGISTERED) {
|
|
return 0;
|
|
}
|
|
return mMainRemote.mVolume;
|
|
}
|
|
}
|
|
|
|
public void setRemoteStreamVolume(int vol) {
|
|
if (DEBUG_VOL) { Log.d(TAG, "setRemoteStreamVolume(vol="+vol+")"); }
|
|
int rccId = RemoteControlClient.RCSE_ID_UNREGISTERED;
|
|
synchronized (mMainRemote) {
|
|
if (mMainRemote.mRccId == RemoteControlClient.RCSE_ID_UNREGISTERED) {
|
|
return;
|
|
}
|
|
rccId = mMainRemote.mRccId;
|
|
}
|
|
IRemoteVolumeObserver rvo = null;
|
|
synchronized (mRCStack) {
|
|
Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
|
|
while(stackIterator.hasNext()) {
|
|
RemoteControlStackEntry rcse = stackIterator.next();
|
|
if (rcse.mRccId == rccId) {
|
|
//FIXME OPTIMIZE store this info in mMainRemote so we don't have to iterate?
|
|
rvo = rcse.mRemoteVolumeObs;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (rvo != null) {
|
|
try {
|
|
rvo.dispatchRemoteVolumeUpdate(0, vol);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "Error dispatching absolute volume update", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Call to make AudioService reevaluate whether it's in a mode where remote players should
|
|
* have their volume controlled. In this implementation this is only to reset whether
|
|
* VolumePanel should display remote volumes
|
|
*/
|
|
private void postReevaluateRemote() {
|
|
sendMsg(mAudioHandler, MSG_REEVALUATE_REMOTE, SENDMSG_QUEUE, 0, 0, null, 0);
|
|
}
|
|
|
|
private void onReevaluateRemote() {
|
|
if (DEBUG_VOL) { Log.w(TAG, "onReevaluateRemote()"); }
|
|
// is there a registered RemoteControlClient that is handling remote playback
|
|
boolean hasRemotePlayback = false;
|
|
synchronized (mRCStack) {
|
|
Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
|
|
while(stackIterator.hasNext()) {
|
|
RemoteControlStackEntry rcse = stackIterator.next();
|
|
if (rcse.mPlaybackType == RemoteControlClient.PLAYBACK_TYPE_REMOTE) {
|
|
hasRemotePlayback = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
synchronized (mMainRemote) {
|
|
if (mHasRemotePlayback != hasRemotePlayback) {
|
|
mHasRemotePlayback = hasRemotePlayback;
|
|
mVolumePanel.postRemoteSliderVisibility(hasRemotePlayback);
|
|
}
|
|
}
|
|
}
|
|
|
|
//==========================================================================================
|
|
// Device orientation
|
|
//==========================================================================================
|
|
/**
|
|
* Handles device configuration changes that may map to a change in the orientation.
|
|
* This feature is optional, and is defined by the definition and value of the
|
|
* "ro.audio.monitorOrientation" system property.
|
|
*/
|
|
private void handleConfigurationChanged(Context context) {
|
|
try {
|
|
// reading new orientation "safely" (i.e. under try catch) in case anything
|
|
// goes wrong when obtaining resources and configuration
|
|
int newOrientation = context.getResources().getConfiguration().orientation;
|
|
if (newOrientation != mDeviceOrientation) {
|
|
mDeviceOrientation = newOrientation;
|
|
setOrientationForAudioSystem();
|
|
}
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error retrieving device orientation: " + e);
|
|
}
|
|
}
|
|
|
|
private void setOrientationForAudioSystem() {
|
|
switch (mDeviceOrientation) {
|
|
case Configuration.ORIENTATION_LANDSCAPE:
|
|
//Log.i(TAG, "orientation is landscape");
|
|
AudioSystem.setParameters("orientation=landscape");
|
|
break;
|
|
case Configuration.ORIENTATION_PORTRAIT:
|
|
//Log.i(TAG, "orientation is portrait");
|
|
AudioSystem.setParameters("orientation=portrait");
|
|
break;
|
|
case Configuration.ORIENTATION_SQUARE:
|
|
//Log.i(TAG, "orientation is square");
|
|
AudioSystem.setParameters("orientation=square");
|
|
break;
|
|
case Configuration.ORIENTATION_UNDEFINED:
|
|
//Log.i(TAG, "orientation is undefined");
|
|
AudioSystem.setParameters("orientation=undefined");
|
|
break;
|
|
default:
|
|
Log.e(TAG, "Unknown orientation");
|
|
}
|
|
}
|
|
|
|
|
|
// Handles request to override default use of A2DP for media.
|
|
public void setBluetoothA2dpOnInt(boolean on) {
|
|
synchronized (mBluetoothA2dpEnabledLock) {
|
|
mBluetoothA2dpEnabled = on;
|
|
mAudioHandler.removeMessages(MSG_SET_FORCE_BT_A2DP_USE);
|
|
AudioSystem.setForceUse(AudioSystem.FOR_MEDIA,
|
|
mBluetoothA2dpEnabled ? AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setRingtonePlayer(IRingtonePlayer player) {
|
|
mContext.enforceCallingOrSelfPermission(REMOTE_AUDIO_PLAYBACK, null);
|
|
mRingtonePlayer = player;
|
|
}
|
|
|
|
@Override
|
|
public IRingtonePlayer getRingtonePlayer() {
|
|
return mRingtonePlayer;
|
|
}
|
|
|
|
@Override
|
|
public AudioRoutesInfo startWatchingRoutes(IAudioRoutesObserver observer) {
|
|
synchronized (mCurAudioRoutes) {
|
|
AudioRoutesInfo routes = new AudioRoutesInfo(mCurAudioRoutes);
|
|
mRoutesObservers.register(observer);
|
|
return routes;
|
|
}
|
|
}
|
|
|
|
|
|
//==========================================================================================
|
|
// Safe media volume management.
|
|
// MUSIC stream volume level is limited when headphones are connected according to safety
|
|
// regulation. When the user attempts to raise the volume above the limit, a warning is
|
|
// displayed and the user has to acknowlegde before the volume is actually changed.
|
|
// The volume index corresponding to the limit is stored in config_safe_media_volume_index
|
|
// property. Platforms with a different limit must set this property accordingly in their
|
|
// overlay.
|
|
//==========================================================================================
|
|
|
|
// mSafeMediaVolumeEnabled indicates whether the media volume is limited over headphones.
|
|
// It is true by default when headphones or a headset are inserted and can be overriden by
|
|
// calling AudioService.disableSafeMediaVolume() (when user opts out).
|
|
private Boolean mSafeMediaVolumeEnabled = new Boolean(false);
|
|
// mSafeMediaVolumeIndex is the cached value of config_safe_media_volume_index property
|
|
private final int mSafeMediaVolumeIndex;
|
|
// mSafeMediaVolumeDevices lists the devices for which safe media volume is enforced,
|
|
private final int mSafeMediaVolumeDevices = AudioSystem.DEVICE_OUT_WIRED_HEADSET |
|
|
AudioSystem.DEVICE_OUT_WIRED_HEADPHONE;
|
|
// mMusicActiveMs is the cumulative time of music activity since safe volume was disabled.
|
|
// When this time reaches UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX, the safe media volume is re-enabled
|
|
// automatically. mMusicActiveMs is rounded to a multiple of MUSIC_ACTIVE_POLL_PERIOD_MS.
|
|
private int mMusicActiveMs;
|
|
private static final int UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX = (20 * 3600 * 1000); // 20 hours
|
|
private static final int MUSIC_ACTIVE_POLL_PERIOD_MS = 60000; // 1 minute polling interval
|
|
private static final int SAFE_VOLUME_DELAY_MS = 500; // 500ms before switching to headphones
|
|
|
|
private void setSafeMediaVolumeEnabled(boolean on) {
|
|
synchronized (mSafeMediaVolumeEnabled) {
|
|
if (on && !mSafeMediaVolumeEnabled) {
|
|
enforceSafeMediaVolume();
|
|
} else if (!on && mSafeMediaVolumeEnabled) {
|
|
mMusicActiveMs = 0;
|
|
sendMsg(mAudioHandler,
|
|
MSG_CHECK_MUSIC_ACTIVE,
|
|
SENDMSG_REPLACE,
|
|
0,
|
|
0,
|
|
null,
|
|
MUSIC_ACTIVE_POLL_PERIOD_MS);
|
|
}
|
|
mSafeMediaVolumeEnabled = on;
|
|
}
|
|
}
|
|
|
|
private void enforceSafeMediaVolume() {
|
|
VolumeStreamState streamState = mStreamStates[AudioSystem.STREAM_MUSIC];
|
|
boolean lastAudible = (streamState.muteCount() != 0);
|
|
int devices = mSafeMediaVolumeDevices;
|
|
int i = 0;
|
|
|
|
while (devices != 0) {
|
|
int device = 1 << i++;
|
|
if ((device & devices) == 0) {
|
|
continue;
|
|
}
|
|
int index = streamState.getIndex(device, lastAudible);
|
|
if (index > mSafeMediaVolumeIndex) {
|
|
if (lastAudible) {
|
|
streamState.setLastAudibleIndex(mSafeMediaVolumeIndex, device);
|
|
sendMsg(mAudioHandler,
|
|
MSG_PERSIST_VOLUME,
|
|
SENDMSG_QUEUE,
|
|
PERSIST_LAST_AUDIBLE,
|
|
device,
|
|
streamState,
|
|
PERSIST_DELAY);
|
|
} else {
|
|
streamState.setIndex(mSafeMediaVolumeIndex, device, true);
|
|
sendMsg(mAudioHandler,
|
|
MSG_SET_DEVICE_VOLUME,
|
|
SENDMSG_QUEUE,
|
|
device,
|
|
0,
|
|
streamState,
|
|
0);
|
|
}
|
|
}
|
|
devices &= ~device;
|
|
}
|
|
}
|
|
|
|
private boolean checkSafeMediaVolume(int streamType, int index, int device) {
|
|
synchronized (mSafeMediaVolumeEnabled) {
|
|
if (mSafeMediaVolumeEnabled &&
|
|
(mStreamVolumeAlias[streamType] == AudioSystem.STREAM_MUSIC) &&
|
|
((device & mSafeMediaVolumeDevices) != 0) &&
|
|
(index > mSafeMediaVolumeIndex)) {
|
|
mVolumePanel.postDisplaySafeVolumeWarning();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public void disableSafeMediaVolume() {
|
|
synchronized (mSafeMediaVolumeEnabled) {
|
|
setSafeMediaVolumeEnabled(false);
|
|
}
|
|
}
|
|
|
|
|
|
@Override
|
|
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
|
|
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);
|
|
|
|
dumpFocusStack(pw);
|
|
dumpRCStack(pw);
|
|
dumpRCCStack(pw);
|
|
dumpStreamStates(pw);
|
|
pw.println("\nAudio routes:");
|
|
pw.print(" mMainType=0x"); pw.println(Integer.toHexString(mCurAudioRoutes.mMainType));
|
|
pw.print(" mBluetoothName="); pw.println(mCurAudioRoutes.mBluetoothName);
|
|
}
|
|
}
|