blob: dfcabf14f1ebc1d62f7b79b44c4d6281083d6b14 [file] [log] [blame]
/******************************************************************************
*
* Copyright 2021 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.
*
******************************************************************************/
/**
* Bluetooth ActiveDeviceManagerService. There is one instance each for
* voice and media profile management..
* - "Idle" and "Active" are steady states.
* - "Activating" and "Deactivating" are transient states until the
* SHO / Deactivation is completed.
*
*
* (Idle)
* | ^
* SetActive | | Removed
* V |
* (Activating) (Deactivating)
* | ^
* Activated | | setActive(NULL) / removeDevice
* V |
* (Active / Broadcasting)
*
*
*/
package com.android.bluetooth.apm;
import static android.Manifest.permission.BLUETOOTH_CONNECT;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothHeadset;
import com.android.bluetooth.Utils;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.ActiveDeviceManager;
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.a2dp.A2dpService;
import com.android.bluetooth.hearingaid.HearingAidService;
import com.android.bluetooth.hfp.HeadsetService;
import com.android.bluetooth.mcp.McpService;
import com.android.bluetooth.broadcast.BroadcastService;
import com.android.bluetooth.cc.CCService;
import android.content.Intent;
import android.content.Context;
import android.os.Looper;
import android.os.Message;
import android.os.HandlerThread;
import android.os.UserHandle;
import android.os.SystemProperties;
import android.util.Log;
import android.media.AudioManager;
import com.android.bluetooth.BluetoothStatsLog;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.Boolean;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.lang.Integer;
import java.util.Scanner;
import java.util.Objects;
public class ActiveDeviceManagerService {
private static final boolean DBG = true;
private static final String TAG = "APM: ActiveDeviceManagerService";
private static ActiveDeviceManagerService sActiveDeviceManager = null;
private AudioManager mAudioManager;
private HandlerThread[] thread = new HandlerThread[AudioType.SIZE];
private ShoStateMachine[] sm = new ShoStateMachine[AudioType.SIZE];
private ApmNativeInterface apmNative;
private Context mContext;
private boolean txStreamSuspended = false;
private final Lock lock = new ReentrantLock();
private final Condition mediaHandoffComplete = lock.newCondition();
private final Condition voiceHandoffComplete = lock.newCondition();
static class Event {
static final int SET_ACTIVE = 1;
static final int ACTIVE_DEVICE_CHANGE = 2;
static final int REMOVE_DEVICE = 3;
static final int DEVICE_REMOVED = 4;
static final int ACTIVATE_TIMEOUT = 5;
static final int DEACTIVATE_TIMEOUT = 6;
static final int SUSPEND_RECORDING = 7;
static final int RESUME_RECORDING = 8;
static final int RETRY_DEACTIVATE = 9;
static final int STOP_SM = 0;
}
public static final int SHO_SUCCESS = 0;
public static final int SHO_PENDING = 1;
public static final int SHO_FAILED = 2;
public static final int ALREADY_ACTIVE = 3;
static final int RETRY_LIMIT = 4;
static final int ACTIVATE_TIMEOUT_DELAY = 3000;
static final int DEACTIVATE_TIMEOUT_DELAY = 2000;
static final int DEACTIVATE_TRY_DELAY = 500;
private ActiveDeviceManagerService (Context context) {
thread[AudioType.MEDIA] = new HandlerThread("ActiveDeviceManager.MediaThread");
thread[AudioType.MEDIA].start();
Looper mediaLooper = thread[AudioType.MEDIA].getLooper();
sm[AudioType.MEDIA] = new ShoStateMachine(AudioType.MEDIA, mediaLooper);
thread[AudioType.VOICE] = new HandlerThread("ActiveDeviceManager.VoiceThread");
thread[AudioType.VOICE].start();
Looper voiceLooper = thread[AudioType.VOICE].getLooper();
sm[AudioType.VOICE] = new ShoStateMachine(AudioType.VOICE, voiceLooper);
mContext = context;
apmNative = ApmNativeInterface.getInstance();
apmNative.init();
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
Objects.requireNonNull(mAudioManager,
"AudioManager cannot be null when ActiveDeviceManagerService starts");
}
public static ActiveDeviceManagerService get(Context context) {
if(sActiveDeviceManager == null) {
sActiveDeviceManager = new ActiveDeviceManagerService(context);
ActiveDeviceManagerServiceIntf.init(sActiveDeviceManager);
}
return sActiveDeviceManager;
}
public static ActiveDeviceManagerService get() {
return sActiveDeviceManager;
}
public boolean setActiveDevice(BluetoothDevice device, Integer mAudioType, Boolean isUIReq, Boolean playReq) {
Log.d(TAG, "setActiveDevice(" + device + ") audioType: " + mAudioType);
boolean isCallActive = false;
if(ApmConst.AudioFeatures.CALL_AUDIO == mAudioType) {
CallAudio mCallAudio = CallAudio.get();
isCallActive = mCallAudio.isAudioOn();
}
synchronized(sm[mAudioType]) {
sm[mAudioType].mSHOQueue.device = device;
sm[mAudioType].mSHOQueue.isUIReq = isUIReq;
sm[mAudioType].mSHOQueue.PlayReq = (playReq || isCallActive);
sm[mAudioType].mSHOQueue.isBroadcast = false;
sm[mAudioType].mSHOQueue.isRecordingMode = false;
sm[mAudioType].mSHOQueue.isGamingMode = false;
}
if(device != null) {
sm[mAudioType].sendMessage(Event.SET_ACTIVE);
}
else {
if (mAudioType == AudioType.MEDIA && sm[mAudioType].mState == sm[mAudioType].mBroadcasting) {
Log.d(TAG, "LE Broadcast is active, ignore REMOVE_DEVICE");
} else {
sm[mAudioType].sendMessage(Event.REMOVE_DEVICE);
}
}
return isUIReq;
}
public boolean setActiveDevice(BluetoothDevice device, Integer mAudioType, Boolean isUIReq) {
return setActiveDevice(device, mAudioType, isUIReq, false);
}
public boolean setActiveDevice(BluetoothDevice device, Integer mAudioType) {
return setActiveDevice(device, mAudioType, false, false);
}
public boolean setActiveDeviceBlocking(BluetoothDevice device, Integer mAudioType) {
Log.d(TAG, "setActiveDeviceBlocking: Enter");
if(ApmConst.AudioFeatures.CALL_AUDIO == mAudioType) {
setActiveDevice(device, mAudioType, false, false);
try {
lock.lock();
voiceHandoffComplete.await();
} catch (InterruptedException e) {
Log.d(TAG, "setActiveDeviceBlocking: Unblocked because of exception: " + e);
} finally {
Log.d(TAG, "setActiveDeviceBlocking: unlock");
lock.unlock();
}
}
Log.d(TAG, "setActiveDeviceBlocking: Exit");
return true;
}
public BluetoothDevice getQueuedDevice(Integer mAudioType) {
return sm[mAudioType].mSHOQueue.device;
}
public boolean removeActiveDevice(Integer mAudioType, Boolean forceStopAudio) {
sm[mAudioType].mSHOQueue.forceStopAudio = forceStopAudio;
setActiveDevice(null, mAudioType, false);
return true;
}
public BluetoothDevice getActiveDevice(Integer mAudioType) {
return sm[mAudioType].Current.Device;
}
public int getActiveProfile(Integer mAudioType) {
if(sm[mAudioType].mState == sm[mAudioType].mBroadcasting) {
// Use Current.Profile here
return ApmConst.AudioProfiles.BROADCAST_LE;
} else if(sm[mAudioType].mState == sm[mAudioType].mActive) {
return sm[mAudioType].Current.Profile;
}
return ApmConst.AudioProfiles.NONE;
}
public boolean onActiveDeviceChange(BluetoothDevice device, Integer mAudioType) {
return onActiveDeviceChange(device, mAudioType, ApmConst.AudioProfiles.NONE);
}
public boolean onActiveDeviceChange(BluetoothDevice device, Integer mAudioType, Integer mProfile) {
if (device != null || mProfile == ApmConst.AudioProfiles.BROADCAST_LE) {
DeviceProfileCombo mDeviceProfileCombo = new DeviceProfileCombo(device, mProfile);
sm[mAudioType].sendMessage(Event.ACTIVE_DEVICE_CHANGE, mDeviceProfileCombo);
}
else
sm[mAudioType].sendMessage(Event.DEVICE_REMOVED);
return true;
}
public boolean enableBroadcast(BluetoothDevice device) {
synchronized(sm[AudioType.MEDIA]) {
sm[AudioType.MEDIA].mSHOQueue.device = device;
sm[AudioType.MEDIA].mSHOQueue.isBroadcast = true;
sm[AudioType.MEDIA].mSHOQueue.PlayReq = false;
}
sm[AudioType.MEDIA].sendMessage(Event.SET_ACTIVE);
return true;
}
public boolean disableBroadcast() {
Log.d(TAG, "disableBroadcast");
synchronized(sm[AudioType.MEDIA]) {
sm[AudioType.MEDIA].mSHOQueue.isBroadcast = false;
sm[AudioType.MEDIA].mSHOQueue.PlayReq = false;
}
if (sm[AudioType.MEDIA].mState == sm[AudioType.MEDIA].mBroadcasting) {
sm[AudioType.MEDIA].sendMessage(Event.REMOVE_DEVICE);
}
return true;
}
public boolean enableGaming(BluetoothDevice device) {
if (sm[AudioType.MEDIA].mState == sm[AudioType.MEDIA].mGamingMode &&
device.equals(getActiveDevice(AudioType.MEDIA))) {
Log.d(TAG, "Device already in Gaming Mode");
return true;
}
Log.d(TAG, "enableGaming");
synchronized(sm[AudioType.MEDIA]) {
sm[AudioType.MEDIA].mSHOQueue.device = device;
sm[AudioType.MEDIA].mSHOQueue.isBroadcast = false;
sm[AudioType.MEDIA].mSHOQueue.isGamingMode = true;
sm[AudioType.MEDIA].mSHOQueue.PlayReq = false;
}
sm[AudioType.MEDIA].sendMessage(Event.SET_ACTIVE);
return true;
}
public boolean disableGaming(BluetoothDevice device) {
if (sm[AudioType.MEDIA].mState != sm[AudioType.MEDIA].mGamingMode) {
Log.e(TAG, "Gaming Mode not active");
return true;
}
/*MediaAudio mMediaAudio = MediaAudio.get();
if(mMediaAudio != null && mMediaAudio.isA2dpPlaying(device)) {
Log.w(TAG, "Gaming Stream is Active");
return false;
}*/
Log.d(TAG, "disableGaming");
synchronized(sm[AudioType.MEDIA]) {
sm[AudioType.MEDIA].mSHOQueue.device = device;
sm[AudioType.MEDIA].mSHOQueue.isGamingMode = false;
sm[AudioType.MEDIA].mSHOQueue.PlayReq = false;
sm[AudioType.MEDIA].mSHOQueue.isUIReq = true;
}
//sm[AudioType.MEDIA].sendMessageDelayed(Event.SET_ACTIVE, DEACTIVATE_TRY_DELAY);
sm[AudioType.MEDIA].sendMessage(Event.SET_ACTIVE);
return true;
}
public boolean enableRecording(BluetoothDevice device) {
Log.d(TAG, "enableRecording: " + device);
MediaAudio mMediaAudio = MediaAudio.get();
if(txStreamSuspended == false) {
Log.d(TAG, "Set A2dpSuspended=true");
mAudioManager.setParameters("A2dpSuspended=true");
txStreamSuspended = true;
}
synchronized(sm[AudioType.MEDIA]) {
sm[AudioType.MEDIA].mSHOQueue.device = device;
sm[AudioType.MEDIA].mSHOQueue.isBroadcast = false;
sm[AudioType.MEDIA].mSHOQueue.isGamingMode = false;
sm[AudioType.MEDIA].mSHOQueue.isRecordingMode = true;
sm[AudioType.MEDIA].mSHOQueue.PlayReq = false;
sm[AudioType.MEDIA].mSHOQueue.isUIReq = true;
}
sm[AudioType.MEDIA].sendMessage(Event.SET_ACTIVE);
return true;
}
public boolean disableRecording(BluetoothDevice device) {
Log.d(TAG, "disableRecording: " + device);
synchronized(sm[AudioType.MEDIA]) {
sm[AudioType.MEDIA].mSHOQueue.device = device;
sm[AudioType.MEDIA].mSHOQueue.isRecordingMode = false;
sm[AudioType.MEDIA].mSHOQueue.PlayReq = false;
}
if (sm[AudioType.MEDIA].mState == sm[AudioType.MEDIA].mRecordingMode) {
sm[AudioType.MEDIA].sendMessage(Event.SET_ACTIVE);
}
return true;
}
public boolean suspendRecording(Boolean suspend) {
Log.d(TAG, "suspendRecording: " + suspend);
if(sm[AudioType.MEDIA].mState == sm[AudioType.MEDIA].mRecordingMode) {
if(suspend) {
sm[AudioType.MEDIA].sendMessage(Event.SUSPEND_RECORDING);
} else {
sm[AudioType.MEDIA].sendMessage(Event.RESUME_RECORDING);
}
}
return true;
}
public boolean isRecordingActive(BluetoothDevice device) {
Log.d(TAG, "isRecordingActive");
return sm[AudioType.MEDIA].mState == sm[AudioType.MEDIA].mRecordingMode;
}
public boolean isStableState(int mAudioType) {
State state = sm[mAudioType].mState;
return !(sm[mAudioType].mActivating == state || sm[mAudioType].mDeactivating == state);
}
private void broadcastActiveDeviceChange(BluetoothDevice device, int mAudioType) {
if (DBG) {
Log.d(TAG, "broadcastActiveDeviceChange(" + device + ")");
}
Intent intent;
/*if (mAdapterService != null)
BluetoothStatsLog.write(BluetoothStatsLog.BLUETOOTH_ACTIVE_DEVICE_CHANGED, BluetoothProfile.A2DP,
mAdapterService.obfuscateAddress(device), 0);*/
if(mAudioType == AudioType.MEDIA)
intent = new Intent(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED);
else
intent = new Intent(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
| Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
if(mAudioType == AudioType.MEDIA) {
A2dpService mA2dpService = A2dpService.getA2dpService();
if(mA2dpService == null) {
Log.e(TAG, "A2dp Service not ready");
return;
}
mA2dpService.sendBroadcast(intent, BLUETOOTH_CONNECT);
} else {
HeadsetService mHeadsetService = HeadsetService.getHeadsetService();
if(mHeadsetService == null) {
Log.e(TAG, "Headset Service not ready");
return;
}
mHeadsetService.sendBroadcastAsUser(intent, UserHandle.ALL, BLUETOOTH_CONNECT,
Utils.getTempAllowlistBroadcastOptions());
}
}
public void disable() {
Log.d(TAG, "disable() called");
sm[AudioType.MEDIA].sendMessage(Event.STOP_SM);
sm[AudioType.VOICE].sendMessage(Event.STOP_SM);
}
public void cleanup() {
sm[AudioType.VOICE].doQuit();
sm[AudioType.MEDIA].doQuit();
thread[AudioType.VOICE].quitSafely();
thread[AudioType.MEDIA].quitSafely();
thread[AudioType.VOICE] = null;
thread[AudioType.MEDIA] = null;
sActiveDeviceManager = null;
}
private class DeviceProfileCombo {
BluetoothDevice Device;
BluetoothDevice absoluteDevice;
int Profile;
DeviceProfileCombo(BluetoothDevice mDevice, int mProfile) {
Device = mDevice;
Profile = mProfile;
}
DeviceProfileCombo() {
Device = null;
Profile = ApmConst.AudioProfiles.NONE;
}
}
private final class ShoStateMachine extends StateMachine {
private static final boolean DBG = true;
private static final String TAG = "APM: ActiveDeviceManagerService";
static final int IDLE = 0;
static final int ACTIVATING = 1;
static final int ACTIVE = 2;
static final int DEACTIVATING = 3;
static final int BROADCAST_ACTIVE = 4;
static final int GAMING_ACTIVE = 5;
static final int RECORDING_ACTIVE = 6;
private Idle mIdle;
private Activating mActivating;
private Active mActive;
private Deactivating mDeactivating;
private Broadcasting mBroadcasting;
private Gaming mGamingMode;
private Recording mRecordingMode;
private DeviceProfileCombo Current;
private DeviceProfileCombo Target;
private SHOReq mTargetSHO;
private SHOReq mSHOQueue;
private DeviceProfileMap dpm;
private int mAudioType;
private State mState;
private State mPrevState = null;
private BluetoothDevice mPrevActiveDevice;
private int mPrevActiveProfile = ApmConst.AudioProfiles.NONE;
boolean enabled;
boolean updatePending = false;
boolean mRecordingSuspended = false;
private String sAudioType;
ShoStateMachine (int audioType, Looper looper) {
super(TAG, looper);
setDbg(DBG);
mIdle = new Idle();
mActivating = new Activating();
mActive = new Active();
mDeactivating = new Deactivating();
mBroadcasting = new Broadcasting();
mGamingMode = new Gaming();
mRecordingMode = new Recording();
Current = new DeviceProfileCombo();
Target = new DeviceProfileCombo();
mSHOQueue = new SHOReq();
mTargetSHO = new SHOReq();
addState(mIdle);
addState(mActivating);
addState(mActive);
addState(mDeactivating);
addState(mBroadcasting);
addState(mGamingMode);
addState(mRecordingMode);
mAudioType = audioType;
if(mAudioType == AudioType.MEDIA)
sAudioType = new String("MEDIA");
else if(mAudioType == AudioType.VOICE)
sAudioType = new String("VOICE");
enabled = true;
setInitialState(mIdle);
start();
}
public void doQuit () {
Log.i(TAG, "Stopping SHO StateMachine for " + mAudioType);
sAudioType = null;
quitNow();
}
/* public void cleanUp {
}*/
private String messageWhatToString(int msg) {
switch (msg) {
case Event.SET_ACTIVE:
return "SET ACTIVE";
case Event.ACTIVE_DEVICE_CHANGE:
return "ACTIVE DEVICE CHANGED";
case Event.REMOVE_DEVICE:
return "REMOVE DEVICE";
case Event.DEVICE_REMOVED:
return "REMOVED";
case Event.ACTIVATE_TIMEOUT:
return "SET ACTIVE TIMEOUT";
case Event.DEACTIVATE_TIMEOUT:
return "REMOVE DEVICE TIMEOUT";
case Event.STOP_SM:
return "STOP STATE MACHINE";
default:
break;
}
return Integer.toString(msg);
}
int startSho(BluetoothDevice device, int profile) {
MediaAudio mMediaAudio = MediaAudio.get();
int ret = SHO_FAILED;
StreamAudioService streamAudioService;
Log.e(TAG, ": startSho() for device: " + device + ", for profile: " + profile);
switch (profile) {
case ApmConst.AudioProfiles.A2DP:
A2dpService a2dpService = A2dpService.getA2dpService();
if(a2dpService != null)
// pass play status here
ret = a2dpService.setActiveDevice(device, false);
break;
case ApmConst.AudioProfiles.HFP:
HeadsetService headsetService = HeadsetService.getHeadsetService();
if(headsetService != null)
ret = headsetService.setActiveDeviceHF(device);
break;
case ApmConst.AudioProfiles.TMAP_MEDIA:
case ApmConst.AudioProfiles.BAP_MEDIA:
streamAudioService = StreamAudioService.getStreamAudioService();
ret = streamAudioService.setActiveDevice(device, ApmConst.AudioProfiles.BAP_MEDIA, false);
if (ret == ActiveDeviceManagerService.ALREADY_ACTIVE) {
ret = SHO_SUCCESS;
}
break;
case ApmConst.AudioProfiles.BAP_RECORDING:
streamAudioService = StreamAudioService.getStreamAudioService();
ret = streamAudioService.setActiveDevice(device, ApmConst.AudioProfiles.BAP_RECORDING, false);
if (ret == ActiveDeviceManagerService.ALREADY_ACTIVE) {
ret = SHO_SUCCESS;
}
break;
case ApmConst.AudioProfiles.TMAP_CALL:
case ApmConst.AudioProfiles.BAP_CALL:
streamAudioService = StreamAudioService.getStreamAudioService();
ret = streamAudioService.setActiveDevice(device, profile, false);
break;
case ApmConst.AudioProfiles.BAP_GCP:
streamAudioService = StreamAudioService.getStreamAudioService();
ret = streamAudioService.setActiveDevice(device, ApmConst.AudioProfiles.BAP_GCP, false);
if (ret == ActiveDeviceManagerService.ALREADY_ACTIVE) {
ret = SHO_SUCCESS;
}
break;
case ApmConst.AudioProfiles.BROADCAST_LE:
//ret = SHO_SUCCESS;//broadcastService.setActiveDevice();
BroadcastService mBroadcastService = BroadcastService.getBroadcastService();
if (mBroadcastService != null)
ret = mBroadcastService.setActiveDevice(device);
break;
case ApmConst.AudioProfiles.HAP_BREDR:
HearingAidService hearingAidService = HearingAidService.getHearingAidService();
ret = hearingAidService.setActiveDevice(device) ? SHO_SUCCESS : SHO_FAILED;
break;
}
return ret;
}
class Idle extends State {
@Override
public void enter() {
synchronized (this) {
mState = mIdle;
}
Current.Device = null;
//2 Update dependent profiles
if(mPrevState != null && mPrevActiveDevice != null) {
broadcastActiveDeviceChange (null, mAudioType);
if (mAudioType == AudioType.MEDIA &&
mPrevActiveProfile != ApmConst.AudioProfiles.HAP_BREDR) {
mPrevActiveProfile = ApmConst.AudioProfiles.NONE;
MediaAudio mMediaAudio = MediaAudio.get();
boolean suppressNoisyIntent = !mTargetSHO.forceStopAudio
&& (mMediaAudio.getConnectionState(mPrevActiveDevice)
== BluetoothProfile.STATE_CONNECTED);
/*TODO: Add profile check here*/
if(mAudioManager != null) {
log("De-Activate Device " + mPrevActiveDevice + " Noisy Intent: " + suppressNoisyIntent);
mAudioManager.handleBluetoothA2dpActiveDeviceChange(
mPrevActiveDevice, BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.A2DP, suppressNoisyIntent, -1);
}
}
VolumeManager mVolumeManager = VolumeManager.get();
if(mVolumeManager != null) {
mVolumeManager.onActiveDeviceChange(Current.Device, mAudioType);
}
}
if(txStreamSuspended && mAudioType == AudioType.MEDIA) {
mAudioManager.setParameters("A2dpSuspended=false");
txStreamSuspended = false;
}
if(!enabled)
log("state machine stopped");
}
@Override
public void exit() {
mPrevState = mIdle;
mPrevActiveDevice = null;
}
@Override
public boolean processMessage(Message message) {
log("Idle: Process Message (" + mAudioType + "): "
+ messageWhatToString(message.what));
if(!enabled) {
log("State Machine not running. Returning");
return NOT_HANDLED;
}
switch(message.what) {
case Event.SET_ACTIVE:
transitionTo(mActivating);
break;
case Event.ACTIVE_DEVICE_CHANGE:
/* Might move to active here*/
case Event.REMOVE_DEVICE:
log("Idle: Process Message Ignored");
break;
case Event.STOP_SM:
enabled = false;
log("state machine stopped");
break;
default:
return NOT_HANDLED;
}
return HANDLED;
}
}
class Activating extends State {
int ret;
@Override
public void enter() {
synchronized (this) {
mState = mActivating;
updatePending = true;
Target.Device = mSHOQueue.device;
Target.absoluteDevice = Target.Device;
mTargetSHO.copy(mSHOQueue);
mSHOQueue.reset();
Log.w(TAG, "Activating " + sAudioType + " Device: " + Target.Device);
}
dpm = DeviceProfileMap.getDeviceProfileMapInstance();
if (mTargetSHO.isBroadcast) {
Target.Profile = dpm.getProfile(Target.Device, ApmConst.AudioFeatures.BROADCAST_AUDIO);
mSHOQueue.device = Current.Device;
mSHOQueue.isUIReq = false;
} else if (mTargetSHO.isRecordingMode) {
mSHOQueue.device = Current.Device;
Target.Profile = ApmConst.AudioProfiles.BAP_RECORDING;
mSHOQueue.isUIReq = false;
} else if (mTargetSHO.isGamingMode) {
/*Only single profile supports gaming Mode*/
Target.Profile = ApmConst.AudioProfiles.BAP_GCP;
} else {
Target.Profile = dpm.getProfile(Target.Device, mAudioType);
}
if(Target.Profile == ApmConst.AudioProfiles.BAP_CALL ||
Target.Profile == ApmConst.AudioProfiles.BAP_MEDIA ||
Target.Profile == ApmConst.AudioProfiles.BAP_RECORDING ||
Target.Profile == ApmConst.AudioProfiles.BAP_GCP) {
StreamAudioService streamAudioService = StreamAudioService.getStreamAudioService();
Target.Device = streamAudioService.getDeviceGroup(Target.Device);
}
if(Target.Device == null) {
Log.e(TAG, "Target Device is null, Returning");
transitionTo(mPrevState);
updatePending = false;
return;
}
if(Target.Device.equals(Current.Device) && Target.Profile == Current.Profile){
Log.d(TAG,"Target Device: " + Target.Device + " and Profile: " + Target.Profile +
" already active");
transitionTo(mPrevState);
updatePending = false;
return;
}
if(Current.Device == null || isSameProfile(Current.Profile, Target.Profile, mAudioType)) {
/* Single Step SHO*/
ActivateDevice(Target, mTargetSHO);
} else {
/*Multi Step SHO*/
ret = startSho(null, Current.Profile);
if(SHO_PENDING == ret) {
sendMessageDelayed(Event.DEACTIVATE_TIMEOUT, DEACTIVATE_TIMEOUT_DELAY);
} else if(ret == SHO_FAILED) {
mTargetSHO.retryCount = 1;
sendMessageDelayed(Event.RETRY_DEACTIVATE, DEACTIVATE_TRY_DELAY);
} else if(SHO_SUCCESS == ret) {
mPrevState = mIdle;
Current.Device = null;
ActivateDevice(Target, mTargetSHO);
}
}
}
@Override
public void exit() {
removeMessages(Event.ACTIVATE_TIMEOUT);
mPrevState = mActivating;
}
@Override
public boolean processMessage(Message message) {
log("Activating: Process Message (" + mAudioType + "): "
+ messageWhatToString(message.what));
switch(message.what) {
case Event.SET_ACTIVE:
log("New SHO request while handling previous. Add to queue");
removeDeferredMessages(Event.REMOVE_DEVICE);
removeDeferredMessages(Event.SET_ACTIVE);
deferMessage(message);
break;
case Event.ACTIVE_DEVICE_CHANGE:
DeviceProfileCombo mDeviceProfileCombo = (DeviceProfileCombo)message.obj;
removeMessages(Event.ACTIVATE_TIMEOUT);
if (Target.Profile == ApmConst.AudioProfiles.BAP_GCP
&& Target.Profile == mDeviceProfileCombo.Profile) {
Current.Device = Target.Device;
Current.Profile = Target.Profile;
transitionTo(mGamingMode);
} else if (Target.Profile == ApmConst.AudioProfiles.BROADCAST_LE
&& Target.Profile == mDeviceProfileCombo.Profile) {
Current.Device = Target.Device;
Current.Profile = Target.Profile;
transitionTo(mBroadcasting);
} else if(Target.Device != null && Target.Device.equals(mDeviceProfileCombo.Device)) {
Current.Device = mDeviceProfileCombo.Device;
Current.Profile = Target.Profile;
Current.absoluteDevice = Target.absoluteDevice;
transitionTo(mActive);
}
break;
case Event.REMOVE_DEVICE:
removeDeferredMessages(Event.REMOVE_DEVICE);
deferMessage(message);
break;
case Event.DEVICE_REMOVED:
mPrevState = mIdle;
Current.Device = null;
removeMessages(Event.DEACTIVATE_TIMEOUT);
ActivateDevice(Target, mTargetSHO);
break;
case Event.RETRY_DEACTIVATE:
ret = startSho(null, Current.Profile);
if(SHO_PENDING == ret) {
mTargetSHO.retryCount = 0;
sendMessageDelayed(Event.DEACTIVATE_TIMEOUT, DEACTIVATE_TIMEOUT_DELAY);
} else if(ret == SHO_FAILED) {
if(mTargetSHO.retryCount >= RETRY_LIMIT) {
updatePending = false;
transitionTo(mPrevState);
} else {
mTargetSHO.retryCount++;
sendMessageDelayed(Event.RETRY_DEACTIVATE, DEACTIVATE_TRY_DELAY);
}
} else if(SHO_SUCCESS == ret) {
mTargetSHO.retryCount = 0;
mPrevState = mIdle;
Current.Device = null;
ActivateDevice(Target, mTargetSHO);
}
break;
case Event.ACTIVATE_TIMEOUT:
case Event.DEACTIVATE_TIMEOUT:
transitionTo(mPrevState);
break;
case Event.STOP_SM:
deferMessage(message);
break;
default:
return NOT_HANDLED;
}
return HANDLED;
}
void ActivateDevice(DeviceProfileCombo mTarget, SHOReq mTargetSHO) {
ret = startSho(mTarget.Device, mTarget.Profile);
if(SHO_PENDING == ret) {
Current.Device = mTarget.Device;
Current.absoluteDevice = mTarget.absoluteDevice;
Current.Profile = mTarget.Profile;
if(mAudioType == AudioType.MEDIA) {
sendActiveDeviceMediaUpdate(Current);
}
sendMessageDelayed(Event.ACTIVATE_TIMEOUT, ACTIVATE_TIMEOUT_DELAY);
} else if(ret == SHO_FAILED) {
if (mState == mBroadcasting) {
mTargetSHO.forceStopAudio = true;
Log.d(TAG,"Previous state was broadcasting, moving to idle");
}
updatePending = false;
transitionTo(mPrevState);
} else if(SHO_SUCCESS == ret) {
Current.Device = mTarget.Device;
Current.absoluteDevice = mTarget.absoluteDevice;
Current.Profile = mTarget.Profile;
if(mTargetSHO.isBroadcast) {
transitionTo(mBroadcasting);
} else if (mTargetSHO.isGamingMode) {
transitionTo(mGamingMode);
} else if (mTargetSHO.isRecordingMode) {
transitionTo(mRecordingMode);
} else {
transitionTo(mActive);
}
} else if(ALREADY_ACTIVE == ret) {
transitionTo(mActive);
}
}
}
class Deactivating extends State {
int ret;
@Override
public void enter() {
synchronized (this) {
mState = mDeactivating;
}
if (mPrevState == mBroadcasting) {
mPrevState = mIdle;
}
Target.Device = null;
Target.Profile = Current.Profile;
mTargetSHO.copy(mSHOQueue);
mSHOQueue.reset();
ret = startSho(Target.Device, Target.Profile);
Log.d(TAG, "ret: " + ret);
if (SHO_SUCCESS == ret) {
transitionTo(mIdle);
} else if (SHO_PENDING == ret) {
sendMessageDelayed(Event.DEACTIVATE_TIMEOUT, DEACTIVATE_TIMEOUT_DELAY);
} else {
transitionTo(mPrevState);
}
}
@Override
public void exit() {
removeMessages(Event.DEACTIVATE_TIMEOUT);
mPrevState = mDeactivating;
mPrevActiveDevice = Current.Device;
Current.Device = null;
mPrevActiveProfile = Current.Profile;
Current.Profile = ApmConst.AudioProfiles.NONE; // Add profile value here
}
@Override
public boolean processMessage(Message message) {
log("Deactivating: Process Message (" + mAudioType + "): "
+ messageWhatToString(message.what));
switch(message.what) {
case Event.SET_ACTIVE:
log("New SHO request while handling previous. Add to queue");
removeDeferredMessages(Event.SET_ACTIVE);
deferMessage(message);
break;
case Event.ACTIVE_DEVICE_CHANGE:
break;
case Event.REMOVE_DEVICE:
break;
case Event.DEVICE_REMOVED:
removeMessages(Event.DEACTIVATE_TIMEOUT);
transitionTo(mIdle);
break;
case Event.DEACTIVATE_TIMEOUT:
transitionTo(mPrevState);
case Event.STOP_SM:
deferMessage(message);
break;
default:
return NOT_HANDLED;
}
return HANDLED;
}
}
class Active extends State {
int ret;
@Override
public void enter() {
synchronized (this) {
mState = mActive;
}
if(updatePending) {
if(mAudioType == AudioType.MEDIA)
sendActiveDeviceMediaUpdate(Current);
else if(mAudioType == AudioType.VOICE)
sendActiveDeviceVoiceUpdate(Current);
}
if(txStreamSuspended && mAudioType == AudioType.MEDIA) {
mAudioManager.setParameters("A2dpSuspended=false");
txStreamSuspended = false;
} else if (mAudioType == AudioType.VOICE) {
lock.lock();
voiceHandoffComplete.signal();
lock.unlock();
Log.d(TAG, "Voice Active: unlock by signal");
}
}
@Override
public void exit() {
//2 update dependent profiles
mPrevState = mActive;
mPrevActiveDevice = Current.Device;
VolumeManager mVolumeManager = VolumeManager.get();
if(mVolumeManager != null) {
mVolumeManager.saveVolume(mAudioType);
}
}
@Override
public boolean processMessage(Message message) {
log("Active: Process Message (" + mAudioType + "): "
+ messageWhatToString(message.what));
switch(message.what) {
case Event.SET_ACTIVE:
if(mSHOQueue.device == null) {
Log.w(TAG, "Invalid request");
break;
}
transitionTo(mActivating);
break;
case Event.ACTIVE_DEVICE_CHANGE:
// might have to handle
break;
case Event.REMOVE_DEVICE:
transitionTo(mDeactivating);
break;
case Event.DEVICE_REMOVED:
//might have to handle
break;
case Event.STOP_SM:
transitionTo(mDeactivating);
enabled = false;
break;
default:
return NOT_HANDLED;
}
return HANDLED;
}
}
class Gaming extends State {
int ret;
@Override
public void enter() {
synchronized (this) {
mState = mGamingMode;
}
if(updatePending) {
sendActiveDeviceGamingUpdate(Current);
}
if(txStreamSuspended) {
mAudioManager.setParameters("A2dpSuspended=false");
txStreamSuspended = false;
}
}
@Override
public void exit() {
//2 update dependent profiles
mPrevState = mGamingMode;
mPrevActiveDevice = Current.Device;
VolumeManager mVolumeManager = VolumeManager.get();
if(mVolumeManager != null) {
mVolumeManager.saveVolume(mAudioType);
}
}
@Override
public boolean processMessage(Message message) {
log("Gaming: Process Message (" + mAudioType + "): "
+ messageWhatToString(message.what));
switch(message.what) {
case Event.SET_ACTIVE:
if(mSHOQueue.device == null) {
Log.w(TAG, "Invalid request");
break;
}
if(!mSHOQueue.isUIReq && Current.Device.equals(mSHOQueue.device)) {
Log.w(TAG, "Spurious request for same device. Ignore");
mSHOQueue.reset();
break;
}
/*MediaAudio mMediaAudio = MediaAudio.get();
if(mMediaAudio != null && mMediaAudio.isA2dpPlaying(mSHOQueue.device)) {
if(!(mSHOQueue.isBroadcast || mSHOQueue.isRecordingMode)) {
Log.w(TAG, "Gaming streaming is on");
break;
}
}*/
transitionTo(mActivating);
break;
case Event.ACTIVE_DEVICE_CHANGE:
// might have to handle
break;
case Event.REMOVE_DEVICE:
transitionTo(mDeactivating);
break;
case Event.DEVICE_REMOVED:
//might have to handle
break;
case Event.STOP_SM:
transitionTo(mDeactivating);
enabled = false;
break;
default:
return NOT_HANDLED;
}
return HANDLED;
}
}
class Broadcasting extends State {
int ret;
@Override
public void enter() {
synchronized (this) {
mState = mBroadcasting;
}
if (updatePending) {
apmNative.activeDeviceUpdate(Current.Device, Current.Profile, mAudioType);
broadcastActiveDeviceChange(Current.Device, AudioType.MEDIA);
mAudioManager.avrcpSupportsAbsoluteVolume(Current.Device.getAddress(), false);
// Update active device to null in VolumeManager while enter broadcasting state
VolumeManager mVolumeManager = VolumeManager.get();
if(mVolumeManager != null) {
mVolumeManager.onActiveDeviceChange(null,
ApmConst.AudioFeatures.MEDIA_AUDIO);
}
int rememberedVolume = 15;
mAudioManager.handleBluetoothA2dpActiveDeviceChange(
Current.Device, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.A2DP,
true, rememberedVolume);
updatePending = false;
}
if(txStreamSuspended) {
mAudioManager.setParameters("A2dpSuspended=false");
txStreamSuspended = false;
}
}
@Override
public void exit() {
mPrevState = mBroadcasting;
mPrevActiveDevice = Current.Device;
}
@Override
public boolean processMessage(Message message) {
log("Broadcasting: Process Message (" + mAudioType + "): "
+ messageWhatToString(message.what));
switch(message.what) {
case Event.SET_ACTIVE:
if(mSHOQueue.isUIReq)
transitionTo(mActivating);
break;
case Event.ACTIVE_DEVICE_CHANGE:
break;
case Event.REMOVE_DEVICE:
if(mSHOQueue.device == null) {
transitionTo(mDeactivating);
} else {
transitionTo(mActivating);
}
break;
case Event.DEVICE_REMOVED:
break;
case Event.STOP_SM:
transitionTo(mDeactivating);
enabled = false;
break;
default:
return NOT_HANDLED;
}
return HANDLED;
}
}
class Recording extends State {
int ret;
@Override
public void enter() {
synchronized (this) {
mState = mRecordingMode;
}
if(updatePending) {
sendActiveDeviceRecordingUpdate(Current);
}
mRecordingSuspended = false;
}
@Override
public void exit() {
mPrevState = mRecordingMode;
mPrevActiveDevice = Current.Device;
HeadsetService hfpService = HeadsetService.getHeadsetService();
mAudioManager.handleBluetoothA2dpActiveDeviceChange(
mPrevActiveDevice,
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.A2DP_SINK,
true, -1);
if(mRecordingSuspended) {
mAudioManager.setParameters("A2dpCaptureSuspend=false");
mRecordingSuspended = false;
}
CallAudio mCallAudio = CallAudio.get();
boolean isInCall = mCallAudio != null &&
mCallAudio.isVoiceOrCallActive();
if(isInCall) {
Log.d(TAG, " reset txStreamSuspended as call is active" );
txStreamSuspended = false;
}
VolumeManager mVolumeManager = VolumeManager.get();
if(mVolumeManager != null) {
mVolumeManager.saveVolume(mAudioType);
}
}
@Override
public boolean processMessage(Message message) {
log("Recording: Process Message (" + mAudioType + "): "
+ messageWhatToString(message.what));
switch(message.what) {
case Event.SET_ACTIVE:
if (mSHOQueue.device == null) {
transitionTo(mDeactivating);
} else {
transitionTo(mActivating);
}
break;
case Event.ACTIVE_DEVICE_CHANGE:
break;
case Event.REMOVE_DEVICE:
transitionTo(mDeactivating);
break;
case Event.DEVICE_REMOVED:
//might have to handle
break;
case Event.SUSPEND_RECORDING: {
if(mRecordingSuspended) break;
mAudioManager.setParameters("A2dpCaptureSuspend=true");
mRecordingSuspended = true;
} break;
case Event.RESUME_RECORDING: {
if(!mRecordingSuspended) break;
mAudioManager.setParameters("A2dpCaptureSuspend=false");
mRecordingSuspended = false;
} break;
case Event.STOP_SM:
transitionTo(mDeactivating);
enabled = false;
break;
default:
return NOT_HANDLED;
}
return HANDLED;
}
}
void sendActiveDeviceMediaUpdate(DeviceProfileCombo Current) {
if(Current.Profile == ApmConst.AudioProfiles.HAP_BREDR) {
if(mPrevActiveDevice != null) {
broadcastActiveDeviceChange (null, AudioType.MEDIA );
ActiveDeviceManager mDeviceManager = AdapterService.getAdapterService().getActiveDeviceManager();
mDeviceManager.onActiveDeviceChange(null, ApmConst.AudioFeatures.MEDIA_AUDIO);
mAudioManager.handleBluetoothA2dpActiveDeviceChange(
mPrevActiveDevice, BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.A2DP, true, -1);
}
return;
}
apmNative.activeDeviceUpdate(Current.Device, Current.Profile, AudioType.MEDIA);
Log.d(TAG, "sendActiveDeviceMediaUpdate: mPrevActiveDevice: "
+ mPrevActiveDevice + ", Current.Device: " + Current.Device);
MediaAudio mMediaAudio = MediaAudio.get();
mMediaAudio.refreshCurrentCodec(Current.Device);
broadcastActiveDeviceChange (Current.absoluteDevice, AudioType.MEDIA);
ActiveDeviceManager mDeviceManager = AdapterService.getAdapterService().getActiveDeviceManager();
mDeviceManager.onActiveDeviceChange(Current.Device, ApmConst.AudioFeatures.MEDIA_AUDIO);
if(Current.Profile == ApmConst.AudioProfiles.A2DP) {
A2dpService mA2dpService = A2dpService.getA2dpService();
mA2dpService.broadcastActiveCodecConfig();
}
//2 Update dependent profiles
VolumeManager mVolumeManager = VolumeManager.get();
if(mVolumeManager != null) {
mVolumeManager.onActiveDeviceChange(Current.Device,
ApmConst.AudioFeatures.MEDIA_AUDIO);
}
McpService mMcpService = McpService.getMcpService();
if (mMcpService != null) {
mMcpService.SetActiveDevices(Current.absoluteDevice, Current.Profile);
}
int deviceVolume = 7;
if(mVolumeManager != null) {
deviceVolume = mVolumeManager.getActiveVolume(ApmConst.AudioFeatures.MEDIA_AUDIO);
}
if(mAudioManager != null) {
mAudioManager.handleBluetoothA2dpActiveDeviceChange(
Current.Device, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.A2DP,
true, deviceVolume);
}
updatePending = false;
}
void sendActiveDeviceGamingUpdate(DeviceProfileCombo Current) {
apmNative.activeDeviceUpdate(Current.Device, Current.Profile, AudioType.MEDIA);
MediaAudio mMediaAudio = MediaAudio.get();
mMediaAudio.refreshCurrentCodec(Current.Device);
broadcastActiveDeviceChange (Current.absoluteDevice, AudioType.MEDIA);
ActiveDeviceManager mDeviceManager = AdapterService.getAdapterService().getActiveDeviceManager();
mDeviceManager.onActiveDeviceChange(Current.Device, ApmConst.AudioFeatures.MEDIA_AUDIO);
//2 Update dependent profiles
VolumeManager mVolumeManager = VolumeManager.get();
int deviceVolume = 7;
if(mVolumeManager != null) {
mVolumeManager.onActiveDeviceChange(Current.Device,
ApmConst.AudioFeatures.MEDIA_AUDIO);
deviceVolume = mVolumeManager.getActiveVolume(ApmConst.AudioFeatures.MEDIA_AUDIO);
}
if(mAudioManager != null) {
mAudioManager.handleBluetoothA2dpActiveDeviceChange(
Current.Device, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.A2DP,
true, deviceVolume);
/*Add back channel call here*/
}
updatePending = false;
}
void sendActiveDeviceVoiceUpdate(DeviceProfileCombo Current) {
Log.d(TAG, "sendActiveDeviceVoiceUpdate");
if(Current.Profile == ApmConst.AudioProfiles.HAP_BREDR) {
if(mPrevActiveDevice != null) {
broadcastActiveDeviceChange (null, AudioType.VOICE);
ActiveDeviceManager mDeviceManager = AdapterService.getAdapterService().getActiveDeviceManager();
mDeviceManager.onActiveDeviceChange(Current.Device, ApmConst.AudioFeatures.CALL_AUDIO);
}
return;
}
broadcastActiveDeviceChange (Current.absoluteDevice, AudioType.VOICE);
ActiveDeviceManager mDeviceManager = AdapterService.getAdapterService().getActiveDeviceManager();
mDeviceManager.onActiveDeviceChange(Current.Device, ApmConst.AudioFeatures.CALL_AUDIO);
VolumeManager mVolumeManager = VolumeManager.get();
if(mVolumeManager != null) {
mVolumeManager.onActiveDeviceChange(Current.Device,
ApmConst.AudioFeatures.CALL_AUDIO);
}
CCService ccService = CCService.getCCService();
if (ccService != null) {
ccService.setActiveDevice(Current.absoluteDevice);
}
if(mTargetSHO.PlayReq) {
CallAudio mCallAudio = CallAudio.get();
mCallAudio.connectAudio();
}
updatePending = false;
}
void sendActiveDeviceRecordingUpdate(DeviceProfileCombo Current) {
apmNative.activeDeviceUpdate(Current.Device, Current.Profile, AudioType.MEDIA);
Log.d(TAG, "sendActiveDeviceRecordingUpdate: mPrevActiveDevice: "
+ mPrevActiveDevice + ", Current.Device: " + Current.Device);
MediaAudio mMediaAudio = MediaAudio.get();
mMediaAudio.refreshCurrentCodec(Current.Device);
if (mAudioManager != null) {
mAudioManager.handleBluetoothA2dpActiveDeviceChange(Current.Device,
BluetoothProfile.STATE_CONNECTED, BluetoothProfile.A2DP_SINK, false, -1); // TO-Check
}
updatePending = false;
}
boolean isSameProfile (int p1, int p2, int audioType) {
if(p1 == p2) {
return true;
}
if(audioType == AudioType.MEDIA) {
int leMediaMask = ApmConst.AudioProfiles.TMAP_MEDIA |
ApmConst.AudioProfiles.BAP_MEDIA |
ApmConst.AudioProfiles.BAP_RECORDING |
ApmConst.AudioProfiles.BAP_GCP;
if((leMediaMask & p1) > 0 && (leMediaMask & p2) > 0) {
return true;
}
} else if(audioType == AudioType.VOICE) {
int leVoiceMask = ApmConst.AudioProfiles.TMAP_CALL | ApmConst.AudioProfiles.BAP_CALL;
if((leVoiceMask & p1) > 0 && (leVoiceMask & p2) > 0) {
return true;
}
}
return false;
}
}
static class AudioType {
public static int VOICE = ApmConst.AudioFeatures.CALL_AUDIO;
public static int MEDIA = ApmConst.AudioFeatures.MEDIA_AUDIO;
public static int SIZE = 2;
}
private class SHOReq {
BluetoothDevice device;
boolean PlayReq;
int retryCount;
boolean isBroadcast;
boolean isGamingMode;
boolean isRecordingMode;
boolean isUIReq;
boolean forceStopAudio;
void copy(SHOReq src) {
device = src.device;
PlayReq = src.PlayReq;
retryCount = src.retryCount;
isBroadcast = src.isBroadcast;
isGamingMode = src.isGamingMode;
isRecordingMode = src.isRecordingMode;
isUIReq = src.isUIReq;
forceStopAudio = src.forceStopAudio;
}
void reset() {
device = null;
PlayReq = false;
retryCount = 0;
isBroadcast = false;
isGamingMode = false;
isRecordingMode = false;
isUIReq = false;
forceStopAudio = false;
}
}
}