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();
+        }
+    }
 }