FM: Add backward compatiblity support for oreo FM configs
* Due to 43bf623710ab5376d98a143ad64f35c9f12d8d95 and some others CAF is
completely using audioManager get/setParameters to set DEVICE_OUT_FM
for FM audio path, but on older devices with oreo mixer paths and
kernel drivers this doesn’t work and breaks audio output although FM
recording works.
* “ro.vendor.fm.use_audio_session” prop is for using AudioTrack session
to set audio path.
Patch was ported to Q due to these commits
* https://github.com/LineageOS/android_vendor_qcom_opensource_fm-commonsys/commit/dfa459344f7af1e712e7570bf694f77340850772
* https://github.com/LineageOS/android_frameworks_base/commit/9fbc205fdc67b2dbc8ccfc4325fb60475f020983
Change-Id: I9acead5b810a0ec5df4322ddd3ea19930f81b42e
diff --git a/fmapp2/src/com/caf/fmradio/FMRadioService.java b/fmapp2/src/com/caf/fmradio/FMRadioService.java
index 2845023..3d90a8c 100644
--- a/fmapp2/src/com/caf/fmradio/FMRadioService.java
+++ b/fmapp2/src/com/caf/fmradio/FMRadioService.java
@@ -216,10 +216,13 @@
private boolean mIsSSRInProgressFromActivity = false;
private int mKeyActionDownCount = 0;
+ private Thread mRecordSinkThread = null;
private AudioTrack mAudioTrack = null;
+ private boolean mIsRecordSink = false;
private static final int AUDIO_FRAMES_COUNT_TO_IGNORE = 3;
private Object mEventWaitLock = new Object();
private boolean mIsFMDeviceLoopbackActive = false;
+ private Object mRecordSinkLock = new Object();
private File mStoragePath = null;
private static final int FM_OFF_FROM_APPLICATION = 1;
private static final int FM_OFF_FROM_ANTENNA = 2;
@@ -238,6 +241,20 @@
private AudioFocusRequest mGainFocusReq;
private PhoneStateCallback mPhoneStateCallback;
+ private AudioRoutingListener mRoutingListener = null;
+ private int mCurrentDevice = AudioDeviceInfo.TYPE_UNKNOWN; // current output device
+ private boolean mUseAudioSession = false;
+
+ private static final int AUDIO_SAMPLE_RATE = 44100;
+ private static final int AUDIO_CHANNEL_CONFIG =
+ AudioFormat.CHANNEL_CONFIGURATION_STEREO;
+ private static final int AUDIO_ENCODING_FORMAT =
+ AudioFormat.ENCODING_PCM_16BIT;
+ private static final int FM_RECORD_BUF_SIZE =
+ AudioRecord.getMinBufferSize(AUDIO_SAMPLE_RATE,
+ AUDIO_CHANNEL_CONFIG, AUDIO_ENCODING_FORMAT);
+ private AudioRecord mAudioRecord = null;
+
public FMRadioService() {
}
@@ -285,6 +302,10 @@
mA2dpDeviceSupportInHal = valueStr.contains("=true");
Log.d(LOGTAG, " is A2DP device Supported In HAL"+mA2dpDeviceSupportInHal);
+ mUseAudioSession = SystemProperties.getBoolean("ro.vendor.fm.use_audio_session", false);
+ if (mUseAudioSession) {
+ mRoutingListener = new AudioRoutingListener();
+ }
mGainFocusReq = requestAudioFocus();
AudioManager mAudioManager =
(AudioManager) getSystemService(Context.AUDIO_SERVICE);
@@ -376,6 +397,164 @@
super.onDestroy();
}
+ private synchronized void createRecordSessions() {
+ if (mAudioRecord != null) {
+ mAudioRecord.stop();
+ }
+ if (mAudioTrack != null) {
+ mAudioTrack.stop();
+ }
+
+ mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.RADIO_TUNER,
+ AUDIO_SAMPLE_RATE, AUDIO_CHANNEL_CONFIG,
+ AUDIO_ENCODING_FORMAT, FM_RECORD_BUF_SIZE);
+
+ mAudioTrack = new AudioTrack.Builder()
+ .setAudioFormat(new AudioFormat.Builder()
+ .setEncoding(AUDIO_ENCODING_FORMAT)
+ .setSampleRate(AUDIO_SAMPLE_RATE)
+ .setChannelIndexMask(AUDIO_CHANNEL_CONFIG)
+ .build())
+ .setBufferSizeInBytes(FM_RECORD_BUF_SIZE)
+ .build();
+ Log.d(LOGTAG, " adding RoutingChangedListener() ");
+ mAudioTrack.addOnRoutingChangedListener(mRoutingListener, null);
+
+ if (mMuted) {
+ mAudioTrack.setVolume(0.0f);
+ }
+ }
+
+ private synchronized void startRecordSink() {
+ Log.d(LOGTAG, "startRecordSink " + AudioSystem.getForceUse(AudioSystem.FOR_MEDIA));
+
+ mIsRecordSink = true;
+ createRecordSinkThread();
+ }
+
+ private synchronized void createRecordSinkThread() {
+ if (mRecordSinkThread == null) {
+ mRecordSinkThread = new RecordSinkThread();
+ mRecordSinkThread.start();
+ Log.d(LOGTAG, "mRecordSinkThread started");
+ try {
+ synchronized (mRecordSinkLock) {
+ Log.d(LOGTAG, "waiting for play to complete");
+ mRecordSinkLock.wait();
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ if (FmReceiver.isCherokeeChip() && mPref.getBoolean("SLIMBUS_SEQ", true)) {
+ enableSlimbus(ENABLE_SLIMBUS_DATA_PORT);
+ }
+ }
+ }
+
+ private synchronized void exitRecordSinkThread() {
+ if (isRecordSinking()) {
+ Log.d(LOGTAG, "stopRecordSink");
+ mAudioTrack.setPreferredDevice(null);
+ mIsRecordSink = false;
+ } else {
+ Log.d(LOGTAG, "exitRecordSinkThread called mRecordSinkThread not running");
+ return;
+ }
+ try {
+ Log.d(LOGTAG, "stopRecordSink waiting to join mRecordSinkThread");
+ mRecordSinkThread.join();
+ } catch (InterruptedException e) {
+ Log.d(LOGTAG, "Exceprion while mRecordSinkThread join");
+ }
+ mRecordSinkThread = null;
+ mAudioTrack = null;
+ mAudioRecord = null;
+ Log.d(LOGTAG, "exitRecordSinkThread completed");
+ }
+
+ private boolean isRecordSinking() {
+ return mIsRecordSink;
+ }
+
+ class RecordSinkThread extends Thread {
+ private int mCurrentFrame = 0;
+
+ private boolean isAudioFrameNeedIgnore() {
+ return mCurrentFrame < AUDIO_FRAMES_COUNT_TO_IGNORE;
+ }
+
+ @Override
+ public void run() {
+ try {
+ Log.d(LOGTAG, "RecordSinkThread: run started ");
+ byte[] buffer = new byte[FM_RECORD_BUF_SIZE];
+ while (isRecordSinking()) {
+ // Speaker mode or BT a2dp mode will come here and keep reading and writing.
+ // If we want FM sound output from speaker or BT a2dp, we must record data
+ // to AudioRecrd and write data to AudioTrack.
+ if (mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_STOPPED) {
+ mAudioRecord.startRecording();
+ Log.d(LOGTAG, "RecordSinkThread: mAudioRecord.startRecording started");
+ }
+
+ if (mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_STOPPED) {
+ Log.d(LOGTAG, "RecordSinkThread: mAudioTrack.play executed");
+ mAudioTrack.play();
+ Log.d(LOGTAG, "RecordSinkThread: mAudioTrack.play completed");
+ synchronized (mRecordSinkLock) {
+ mRecordSinkLock.notify();
+ }
+ }
+ int size = mAudioRecord.read(buffer, 0, FM_RECORD_BUF_SIZE);
+ // check whether need to ignore first 3 frames audio data from AudioRecord
+ // to avoid pop noise.
+ if (isAudioFrameNeedIgnore()) {
+ mCurrentFrame += 1;
+ continue;
+ }
+ if (size <= 0) {
+ Log.e(LOGTAG, "RecordSinkThread read data from AudioRecord "
+ + "error size: " + size);
+ continue;
+ }
+ byte[] tmpBuf = new byte[size];
+ System.arraycopy(buffer, 0, tmpBuf, 0, size);
+ // Check again to avoid noises, because RecordSink may be changed
+ // while AudioRecord is reading.
+ if (isRecordSinking()) {
+ mAudioTrack.write(tmpBuf, 0, tmpBuf.length);
+ } else {
+ mCurrentFrame = 0;
+ Log.d(LOGTAG,
+ "RecordSinkThread: stopRecordSink called stopping mAudioTrack and"
+ + " mAudioRecord ");
+ break;
+ }
+ }
+ } catch (Exception e) {
+ Log.d(LOGTAG, "RecordSinkThread.run, thread is interrupted, need exit thread");
+ } finally {
+ Log.d(LOGTAG,
+ "RecordSinkThread: stopRecordSink called stopping mAudioTrack and "
+ + "mAudioRecord ");
+ if (mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
+ Log.d(LOGTAG, "RecordSinkThread: mAudioRecord.stop()");
+ mAudioRecord.stop();
+ Log.d(LOGTAG, "RecordSinkThread: mAudioRecord.stop() completed");
+ mAudioRecord.release();
+ Log.d(LOGTAG, "RecordSinkThread: mAudioRecord.release() completed");
+ }
+ if (mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
+ Log.d(LOGTAG, "RecordSinkThread: mAudioTrack.stop();");
+ mAudioTrack.stop();
+ Log.d(LOGTAG, "RecordSinkThread:mAudioTrack.stop() completed");
+ mAudioTrack.release();
+ Log.d(LOGTAG, "RecordSinkThread: mAudioTrack.release() completed");
+ }
+ }
+ }
+ }
+
private void setFMVolume(int mCurrentVolumeIndex) {
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
float decibels = audioManager.getStreamVolumeDb(AudioManager.STREAM_MUSIC,
@@ -449,6 +628,77 @@
return true;
}
+ private boolean configureFMDeviceLoopback_O(boolean enable) {
+ boolean success = true;
+ int status = AudioSystem.SUCCESS;
+
+ Log.d(LOGTAG, "configureFMDeviceLoopback enable:" + enable +
+ " DeviceLoopbackActive:" + mIsFMDeviceLoopbackActive);
+ if (enable && !mIsFMDeviceLoopbackActive) {
+ status = AudioSystem.getDeviceConnectionState(AudioSystem.DEVICE_OUT_FM, "");
+ Log.d(LOGTAG, " FM hardwareLoopback Status = " + status);
+ if (status == AudioSystem.DEVICE_STATE_AVAILABLE) {
+ // This case usually happens, when FM is force killed through settings app
+ // and we don't get chance to disable Hardware LoopBack.
+ Log.d(LOGTAG, " FM HardwareLoopBack Active, disable it first");
+ AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_FM,
+ AudioSystem.DEVICE_STATE_UNAVAILABLE, "", "",
+ AudioSystem.AUDIO_FORMAT_DEFAULT);
+ mCurrentDevice = AudioDeviceInfo.TYPE_WIRED_HEADSET;
+ }
+ status = AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_FM,
+ AudioSystem.DEVICE_STATE_AVAILABLE, "", "",
+ AudioSystem.AUDIO_FORMAT_DEFAULT);
+ if (status != AudioSystem.SUCCESS) {
+ success = false;
+ Log.e(LOGTAG, "configureFMDeviceLoopback failed! status:" + status);
+ AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_FM,
+ AudioSystem.DEVICE_STATE_UNAVAILABLE, "", "",
+ AudioSystem.AUDIO_FORMAT_DEFAULT);
+ mCurrentDevice = AudioDeviceInfo.TYPE_UNKNOWN;
+ } else {
+ mIsFMDeviceLoopbackActive = true;
+ }
+ } else if (!enable && mIsFMDeviceLoopbackActive) {
+ AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_FM,
+ AudioSystem.DEVICE_STATE_UNAVAILABLE, "", "",
+ AudioSystem.AUDIO_FORMAT_DEFAULT);
+ mIsFMDeviceLoopbackActive = false;
+ mCurrentDevice = AudioDeviceInfo.TYPE_UNKNOWN;
+ }
+
+ return success;
+ }
+
+ private synchronized void configureAudioDataPath(boolean enable) {
+ Log.d(LOGTAG, "configureAudioDataPath:" + enable +
+ " mA2dpConnected:" + mA2dpConnected +
+ " isRecordSinking" + isRecordSinking() +
+ " mSpeakerPhoneOn:" + mSpeakerPhoneOn +
+ " mIsFMDeviceLoopbackActive:" + mIsFMDeviceLoopbackActive);
+
+ if (enable) {
+ Log.d(LOGTAG, "Start Hardware loop back for audio");
+ if (mStoppedOnFocusLoss) {
+ Log.d(LOGTAG, "FM does not have audio focus, not enabling " +
+ "audio path");
+ return;
+ }
+ if (!mIsFMDeviceLoopbackActive && !mA2dpConnected && !mSpeakerPhoneOn) {
+ // not on BT and device loop is also not active
+ if (FmReceiver.isCherokeeChip() && mPref.getBoolean("SLIMBUS_SEQ", true)) {
+ enableSlimbus(ENABLE_SLIMBUS_DATA_PORT);
+ }
+ exitRecordSinkThread();
+ configureFMDeviceLoopback_O(true);
+ }
+ } else {
+ //inform audio to disbale fm audio
+ configureFMDeviceLoopback_O(false);
+ exitRecordSinkThread();
+ }
+ }
+
private void setCurrentFMVolume() {
if(isFmOn()) {
AudioManager maudioManager =
@@ -1095,6 +1345,9 @@
if (mStoppedOnFactoryReset) {
mStoppedOnFactoryReset = false;
mSpeakerPhoneOn = false;
+ if (mUseAudioSession) {
+ configureAudioDataPath(true);
+ }
// In FM stop, the audio route is set to default audio device
}
String temp = mSpeakerPhoneOn ? "Speaker" : "WiredHeadset";
@@ -1104,7 +1357,11 @@
} else {
mAudioDevice = AudioDeviceInfo.TYPE_WIRED_HEADPHONES;
}
- configureFMDeviceLoopback(true);
+ if (mUseAudioSession) {
+ startApplicationLoopBack(mAudioDevice);
+ } else {
+ configureFMDeviceLoopback(true);
+ }
try {
if ((mServiceInUse) && (mCallbacks != null))
mCallbacks.onFmAudioPathStarted();
@@ -1115,7 +1372,11 @@
private void stopFM() {
Log.d(LOGTAG, "In stopFM");
- configureFMDeviceLoopback(false);
+ if (mUseAudioSession) {
+ configureAudioDataPath(false);
+ } else {
+ configureFMDeviceLoopback(false);
+ }
mPlaybackInProgress = false;
try {
if ((mServiceInUse) && (mCallbacks != null))
@@ -1128,7 +1389,11 @@
private void resetFM(){
Log.d(LOGTAG, "resetFM");
mPlaybackInProgress = false;
- configureFMDeviceLoopback(false);
+ if (mUseAudioSession) {
+ configureAudioDataPath(false);
+ } else {
+ configureFMDeviceLoopback(false);
+ }
}
private boolean getRecordServiceStatus() {
@@ -1557,6 +1822,20 @@
}
};
+ private class AudioRoutingListener implements AudioRouting.OnRoutingChangedListener {
+ public void onRoutingChanged(AudioRouting audioRouting) {
+ Log.d(LOGTAG, " onRoutingChanged + currdevice " + mCurrentDevice);
+ AudioDeviceInfo routedDevice = audioRouting.getRoutedDevice();
+ // if routing is nowhere, we get routedDevice as null
+ if (routedDevice != null) {
+ Log.d(LOGTAG, " Audio Routed to device id " + routedDevice.getType());
+ if (routedDevice.getType() != mCurrentDevice) {
+ startApplicationLoopBack(mCurrentDevice);
+ }
+ }
+ }
+ }
+
private Handler mDelayedStopHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
@@ -2636,11 +2915,15 @@
mAudioDevice = AudioDeviceInfo.TYPE_BUILTIN_SPEAKER;
outputDevice = "Speaker";
}
- mAudioDeviceType = mAudioDevice | AudioSystem.DEVICE_OUT_FM;
- AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
- String keyValPairs = new String("fm_routing="+mAudioDeviceType);
- Log.d(LOGTAG, "keyValPairs = "+keyValPairs);
- audioManager.setParameters(keyValPairs);
+ if (mUseAudioSession) {
+ startApplicationLoopBack(mAudioDevice);
+ } else {
+ mAudioDeviceType = mAudioDevice | AudioSystem.DEVICE_OUT_FM;
+ AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+ String keyValPairs = new String("fm_routing=" + mAudioDeviceType);
+ Log.d(LOGTAG, "keyValPairs = " + keyValPairs);
+ audioManager.setParameters(keyValPairs);
+ }
if (mReceiver.isCherokeeChip() && (mPref.getBoolean("SLIMBUS_SEQ", true))) {
enableSlimbus(ENABLE_SLIMBUS_DATA_PORT);
}
@@ -4097,4 +4380,50 @@
//TODO unregister the fm service here.
}
}
+
+ private void startApplicationLoopBack(int deviceType) {
+ // stop existing playback path before starting new one
+ Log.d(LOGTAG, "startApplicationLoopBack for device " + deviceType);
+
+ AudioDeviceInfo outputDevice = null;
+ AudioDeviceInfo[] deviceList = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
+ for (AudioDeviceInfo audioDeviceInfo : deviceList) {
+ Log.d(LOGTAG, "startApplicationLoopBack dev_type " + audioDeviceInfo.getType());
+ if (AudioDeviceInfo.TYPE_WIRED_HEADSET == deviceType
+ || AudioDeviceInfo.TYPE_WIRED_HEADPHONES == deviceType) {
+ if (audioDeviceInfo.getType() == AudioDeviceInfo.TYPE_WIRED_HEADSET ||
+ audioDeviceInfo.getType() == AudioDeviceInfo.TYPE_WIRED_HEADPHONES) {
+ outputDevice = audioDeviceInfo;
+ Log.d(LOGTAG, "startApplicationLoopBack found_dev "
+ + audioDeviceInfo.getType());
+ break;
+ }
+ } else if (audioDeviceInfo.getType() == deviceType) {
+ outputDevice = audioDeviceInfo;
+ Log.d(LOGTAG, "startApplicationLoopBack found_dev " + audioDeviceInfo.getType());
+ break;
+ }
+ }
+ if (outputDevice == null) {
+ Log.d(LOGTAG, "no output device" + deviceType + " found");
+ return;
+ }
+ if (mIsFMDeviceLoopbackActive) {
+ if (mReceiver != null && FmReceiver.isCherokeeChip() &&
+ mPref.getBoolean("SLIMBUS_SEQ", true)) {
+ enableSlimbus(DISABLE_SLIMBUS_DATA_PORT);
+ }
+ configureFMDeviceLoopback_O(false);
+ }
+ if (!isRecordSinking()) {
+ createRecordSessions();
+ Log.d(LOGTAG, "creating AudioTrack session");
+ }
+ mCurrentDevice = outputDevice.getType();
+ mAudioTrack.setPreferredDevice(outputDevice);
+ Log.d(LOGTAG, "PreferredDevice is set to " + outputDevice.getType());
+ if (!isRecordSinking()) {
+ startRecordSink();
+ }
+ }
}