Support SAR for OTT VOWifi Apps

Current SAR implementation only supports power backoff for cellular calls.
This commit extends the power backoff support to OTT VoIP apps as well.

Bug: 124163143
Test: atest com.android.server.wifi
Test: Manual

Change-Id: I3fa51852f9bd022749011135b4c1ec299204b64a
diff --git a/service/java/com/android/server/wifi/SarInfo.java b/service/java/com/android/server/wifi/SarInfo.java
index a62307e..7d58065 100644
--- a/service/java/com/android/server/wifi/SarInfo.java
+++ b/service/java/com/android/server/wifi/SarInfo.java
@@ -74,6 +74,7 @@
     public boolean isWifiSapEnabled = false;
     public boolean isWifiScanOnlyEnabled = false;
     public boolean isVoiceCall = false;
+    public boolean isEarPieceActive = false;
     public int attemptedSarScenario = RESET_SAR_SCENARIO;
 
     private boolean mAllWifiDisabled = true;
@@ -82,6 +83,7 @@
     private int mLastReportedSensorState = SAR_SENSOR_FREE_SPACE;
     private boolean mLastReportedIsWifiSapEnabled = false;
     private boolean mLastReportedIsVoiceCall = false;
+    private boolean mLastReportedIsEarPieceActive = false;
     private int mLastReportedScenario = INITIAL_SAR_SCENARIO;
     private long mLastReportedScenarioTs = 0;
 
@@ -113,7 +115,8 @@
         /* Check if some change happened since last successful reporting */
         if ((sensorState != mLastReportedSensorState)
                 || (isWifiSapEnabled != mLastReportedIsWifiSapEnabled)
-                || (isVoiceCall != mLastReportedIsVoiceCall)) {
+                || (isVoiceCall != mLastReportedIsVoiceCall)
+                || (isEarPieceActive != mLastReportedIsEarPieceActive)) {
             return true;
         } else {
             return false;
@@ -129,6 +132,7 @@
         mLastReportedSensorState = sensorState;
         mLastReportedIsWifiSapEnabled = isWifiSapEnabled;
         mLastReportedIsVoiceCall = isVoiceCall;
+        mLastReportedIsEarPieceActive = isEarPieceActive;
         mLastReportedScenario = attemptedSarScenario;
         mLastReportedScenarioTs = System.currentTimeMillis();
 
@@ -169,10 +173,12 @@
         pw.println("    Wifi Client state is: " + isWifiClientEnabled);
         pw.println("    Wifi Soft AP state is: " + isWifiSapEnabled);
         pw.println("    Wifi ScanOnly state is: " + isWifiScanOnlyEnabled);
+        pw.println("    Earpiece state is : " + isEarPieceActive);
         pw.println("Last reported values:");
         pw.println("    Sensor state is: " + sensorStateToString(mLastReportedSensorState));
         pw.println("    Soft AP state is: " + mLastReportedIsWifiSapEnabled);
         pw.println("    Voice Call state is: " + mLastReportedIsVoiceCall);
+        pw.println("    Earpiece state is: " + mLastReportedIsEarPieceActive);
         pw.println("Last reported scenario: " + mLastReportedScenario);
         pw.println("Reported " +  (System.currentTimeMillis() - mLastReportedScenarioTs) / 1000
                 + " seconds ago");
diff --git a/service/java/com/android/server/wifi/SarManager.java b/service/java/com/android/server/wifi/SarManager.java
index 598e5c9..f0319fb 100644
--- a/service/java/com/android/server/wifi/SarManager.java
+++ b/service/java/com/android/server/wifi/SarManager.java
@@ -20,12 +20,18 @@
 import static android.telephony.TelephonyManager.CALL_STATE_OFFHOOK;
 import static android.telephony.TelephonyManager.CALL_STATE_RINGING;
 
+import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.hardware.Sensor;
 import android.hardware.SensorEvent;
 import android.hardware.SensorEventListener;
 import android.hardware.SensorManager;
+import android.media.AudioManager;
+import android.media.AudioSystem;
 import android.net.wifi.WifiManager;
+import android.os.Handler;
 import android.os.Looper;
 import android.telephony.PhoneStateListener;
 import android.telephony.TelephonyManager;
@@ -33,6 +39,7 @@
 import android.util.Log;
 
 import com.android.internal.R;
+import com.android.server.wifi.util.WifiHandler;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -49,6 +56,8 @@
  * - It constructs the sar info and send it towards the HAL
  */
 public class SarManager {
+    // Period for checking on voice steam active (in ms)
+    private static final int CHECK_VOICE_STREAM_INTERVAL_MS = 5000;
     /* For Logging */
     private static final String TAG = "WifiSarManager";
     private boolean mVerboseLoggingEnabled = true;
@@ -66,6 +75,10 @@
     private int mSarSensorEventNearHand;
     private int mSarSensorEventNearHead;
 
+    // Device starts with screen on
+    private boolean mScreenOn = false;
+    private boolean mIsVoiceStreamCheckEnabled = false;
+
     /**
      * Other parameters passed in or created in the constructor.
      */
@@ -75,6 +88,7 @@
     private final WifiNative mWifiNative;
     private final SarSensorEventListener mSensorListener;
     private final SensorManager mSensorManager;
+    private final Handler mHandler;
     private final Looper mLooper;
 
     /**
@@ -89,6 +103,7 @@
         mTelephonyManager = telephonyManager;
         mWifiNative = wifiNative;
         mLooper = looper;
+        mHandler = new WifiHandler(TAG, looper);
         mSensorManager = sensorManager;
         mPhoneStateListener = new WifiPhoneStateListener(looper);
         mSensorListener = new SarSensorEventListener();
@@ -101,6 +116,80 @@
         }
     }
 
+    /**
+     * Notify SarManager of screen status change
+     */
+    public void handleScreenStateChanged(boolean screenOn) {
+        if (!mSupportSarVoiceCall) {
+            return;
+        }
+
+        if (mScreenOn == screenOn) {
+            return;
+        }
+
+        if (mVerboseLoggingEnabled) {
+            Log.d(TAG, "handleScreenStateChanged: screenOn = " + screenOn);
+        }
+
+        mScreenOn = screenOn;
+
+        // Only schedule a voice stream check if screen is turning on, and it is currently not
+        // scheduled
+        if (mScreenOn && !mIsVoiceStreamCheckEnabled) {
+            mHandler.post(() -> {
+                checkAudioDevice();
+            });
+
+            mIsVoiceStreamCheckEnabled = true;
+        }
+    }
+
+    private boolean isVoiceCallOnEarpiece() {
+        AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+
+        return (audioManager.getDevicesForStream(AudioManager.STREAM_VOICE_CALL)
+                == AudioManager.DEVICE_OUT_EARPIECE);
+    }
+
+    private boolean isVoiceCallStreamActive() {
+        return AudioSystem.isStreamActive(AudioManager.STREAM_VOICE_CALL, 0);
+    }
+
+    private void checkAudioDevice() {
+        // First Check if audio stream is on
+        boolean voiceStreamActive = isVoiceCallStreamActive();
+        boolean earPieceActive;
+
+        if (voiceStreamActive) {
+            // Check on the audio route
+            earPieceActive = isVoiceCallOnEarpiece();
+
+            if (mVerboseLoggingEnabled) {
+                Log.d(TAG, "EarPiece active = " + earPieceActive);
+            }
+        } else {
+            earPieceActive = false;
+        }
+
+        // If audio route has changed, update SAR
+        if (earPieceActive != mSarInfo.isEarPieceActive) {
+            mSarInfo.isEarPieceActive = earPieceActive;
+            updateSarScenario();
+        }
+
+        // Now should we proceed with the checks
+        if (!mScreenOn && !voiceStreamActive) {
+            // No need to continue checking
+            mIsVoiceStreamCheckEnabled = false;
+        } else {
+            // Schedule another check
+            mHandler.postDelayed(() -> {
+                checkAudioDevice();
+            }, CHECK_VOICE_STREAM_INTERVAL_MS);
+        }
+    }
+
     private void readSarConfigs() {
         mSupportSarTxPowerLimit = mContext.getResources().getBoolean(
                 R.bool.config_wifi_framework_enable_sar_tx_power_limit);
@@ -145,6 +234,7 @@
         if (mSupportSarVoiceCall) {
             /* Listen for Phone State changes */
             registerPhoneStateListener();
+            registerVoiceStreamListener();
         }
 
         /* Only listen for SAR sensor if supported */
@@ -159,6 +249,56 @@
         }
     }
 
+    private void registerVoiceStreamListener() {
+        Log.i(TAG, "Registering for voice stream status");
+
+        // Register for listening to transitions of change of voice stream devices
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(AudioManager.STREAM_DEVICES_CHANGED_ACTION);
+
+        mContext.registerReceiver(
+                new BroadcastReceiver() {
+                    @Override
+                    public void onReceive(Context context, Intent intent) {
+                        boolean voiceStreamActive = isVoiceCallStreamActive();
+                        if (!voiceStreamActive) {
+                            // No need to proceed, there is no voice call ongoing
+                            return;
+                        }
+
+                        String action = intent.getAction();
+                        int streamType =
+                                intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
+                        int device = intent.getIntExtra(
+                                AudioManager.EXTRA_VOLUME_STREAM_DEVICES, -1);
+                        int oldDevice = intent.getIntExtra(
+                                AudioManager.EXTRA_PREV_VOLUME_STREAM_DEVICES, -1);
+
+                        if (streamType == AudioManager.STREAM_VOICE_CALL) {
+                            boolean earPieceActive = mSarInfo.isEarPieceActive;
+                            if (device == AudioManager.DEVICE_OUT_EARPIECE) {
+                                if (mVerboseLoggingEnabled) {
+                                    Log.d(TAG, "Switching to earpiece : HEAD ON");
+                                    Log.d(TAG, "Old device = " + oldDevice);
+                                }
+                                earPieceActive = true;
+                            } else if (oldDevice == AudioManager.DEVICE_OUT_EARPIECE) {
+                                if (mVerboseLoggingEnabled) {
+                                    Log.d(TAG, "Switching from earpiece : HEAD OFF");
+                                    Log.d(TAG, "New device = " + device);
+                                }
+                                earPieceActive = false;
+                            }
+
+                            if (earPieceActive != mSarInfo.isEarPieceActive) {
+                                mSarInfo.isEarPieceActive = earPieceActive;
+                                updateSarScenario();
+                            }
+                        }
+                    }
+                }, filter, null, mHandler);
+    }
+
     /**
      * Register the phone state listener.
      */
@@ -277,6 +417,10 @@
         /* Report change to HAL if needed */
         if (mSarInfo.isVoiceCall != newIsVoiceCall) {
             mSarInfo.isVoiceCall = newIsVoiceCall;
+
+            if (mVerboseLoggingEnabled) {
+                Log.d(TAG, "Voice Call = " + newIsVoiceCall);
+            }
             updateSarScenario();
         }
     }
diff --git a/service/java/com/android/server/wifi/WifiStateMachine.java b/service/java/com/android/server/wifi/WifiStateMachine.java
index 67b1ad6..390a102 100644
--- a/service/java/com/android/server/wifi/WifiStateMachine.java
+++ b/service/java/com/android/server/wifi/WifiStateMachine.java
@@ -2392,6 +2392,8 @@
             mWifiConnectivityManager.handleScreenStateChanged(screenOn);
         }
 
+        mSarManager.handleScreenStateChanged(screenOn);
+
         if (mVerboseLoggingEnabled) log("handleScreenStateChanged Exit: " + screenOn);
     }
 
diff --git a/service/java/com/android/server/wifi/WifiVendorHal.java b/service/java/com/android/server/wifi/WifiVendorHal.java
index fad90d1..cff9a91 100644
--- a/service/java/com/android/server/wifi/WifiVendorHal.java
+++ b/service/java/com/android/server/wifi/WifiVendorHal.java
@@ -2664,7 +2664,7 @@
         /* As long as no voice call is active (in case voice call is supported),
          * no backoff is needed */
         if (sarInfo.sarVoiceCallSupported) {
-            return sarInfo.isVoiceCall;
+            return (sarInfo.isVoiceCall || sarInfo.isEarPieceActive);
         } else {
             return false;
         }
@@ -2679,7 +2679,7 @@
      * Otherwise, an exception is thrown.
      */
     private int frameworkToHalTxPowerScenario_1_1(SarInfo sarInfo) {
-        if (sarInfo.sarVoiceCallSupported && sarInfo.isVoiceCall) {
+        if (sarInfo.sarVoiceCallSupported && (sarInfo.isVoiceCall || sarInfo.isEarPieceActive)) {
             return android.hardware.wifi.V1_1.IWifiChip.TxPowerScenario.VOICE_CALL;
         } else {
             throw new IllegalArgumentException("bad scenario: voice call not active/supported");
@@ -2703,7 +2703,7 @@
         if (sarInfo.sarSapSupported && sarInfo.isWifiSapEnabled) {
             return true;
         }
-        if (sarInfo.sarVoiceCallSupported && sarInfo.isVoiceCall) {
+        if (sarInfo.sarVoiceCallSupported && (sarInfo.isVoiceCall || sarInfo.isEarPieceActive)) {
             return true;
         }
         return false;
@@ -2752,7 +2752,7 @@
                     throw new IllegalArgumentException("bad scenario: Invalid sensor state");
             }
         } else if (sarInfo.sarSapSupported && sarInfo.sarVoiceCallSupported) {
-            if (sarInfo.isVoiceCall) {
+            if (sarInfo.isVoiceCall || sarInfo.isEarPieceActive) {
                 return android.hardware.wifi.V1_2.IWifiChip
                         .TxPowerScenario.ON_HEAD_CELL_ON;
             } else if (sarInfo.isWifiSapEnabled) {
@@ -2763,7 +2763,7 @@
             }
         } else if (sarInfo.sarVoiceCallSupported) {
             /* SAR Sensors and SoftAP not supported, act like V1_1 */
-            if (sarInfo.isVoiceCall) {
+            if (sarInfo.isVoiceCall || sarInfo.isEarPieceActive) {
                 return android.hardware.wifi.V1_1.IWifiChip.TxPowerScenario.VOICE_CALL;
             } else {
                 throw new IllegalArgumentException("bad scenario: voice call not active");
diff --git a/tests/wifitests/src/com/android/server/wifi/SarInfoTest.java b/tests/wifitests/src/com/android/server/wifi/SarInfoTest.java
index 2bf1f3e..50aec59 100644
--- a/tests/wifitests/src/com/android/server/wifi/SarInfoTest.java
+++ b/tests/wifitests/src/com/android/server/wifi/SarInfoTest.java
@@ -231,6 +231,21 @@
     }
 
     /**
+     * Test a change in earpiece status, shouldReport should return true
+     * Note: will need to report once before making the change to remove
+     * the effect of sensor state change.
+     */
+    @Test
+    public void testSarInfo_earpiece_wifi_enabled() throws Exception {
+        mSarInfo.isWifiClientEnabled = true;
+        assertTrue(mSarInfo.shouldReport());
+        mSarInfo.reportingSuccessful();
+
+        mSarInfo.isEarPieceActive = true;
+        assertTrue(mSarInfo.shouldReport());
+    }
+
+    /**
      * Test starting SAP, shouldReport should return true
      * Note: will need to report once before starting SAP to remove
      * the effect of sensor state change.
diff --git a/tests/wifitests/src/com/android/server/wifi/WifiVendorHalTest.java b/tests/wifitests/src/com/android/server/wifi/WifiVendorHalTest.java
index f089ebb..ebeecbd 100644
--- a/tests/wifitests/src/com/android/server/wifi/WifiVendorHalTest.java
+++ b/tests/wifitests/src/com/android/server/wifi/WifiVendorHalTest.java
@@ -2259,6 +2259,35 @@
     }
 
     /**
+     * Test the selectTxPowerScenario HIDL method invocation with no sensor support, but with
+     * SAP and voice call support.
+     * When earpiece is active, should result in cell with near head scenario
+     * Using IWifiChip 1.2 interface
+     */
+    @Test
+    public void testEarPieceScenarios_SelectTxPowerV1_2() throws RemoteException {
+        // Create a SAR info record (with sensor and SAP support)
+        SarInfo sarInfo = new SarInfo();
+        sarInfo.sarVoiceCallSupported = true;
+        sarInfo.sarSapSupported = true;
+        sarInfo.sarSensorSupported = false;
+
+        sarInfo.isEarPieceActive = true;
+
+        // Expose the 1.2 IWifiChip.
+        mWifiVendorHal = new WifiVendorHalSpyV1_2(mHalDeviceManager, mLooper.getLooper());
+        when(mIWifiChipV12.selectTxPowerScenario_1_2(anyInt())).thenReturn(mWifiStatusSuccess);
+
+        // ON_HEAD_CELL_ON
+        assertTrue(mWifiVendorHal.startVendorHalSta());
+        assertTrue(mWifiVendorHal.selectTxPowerScenario(sarInfo));
+        verify(mIWifiChipV12).selectTxPowerScenario_1_2(
+                eq(android.hardware.wifi.V1_2.IWifiChip.TxPowerScenario.ON_HEAD_CELL_ON));
+        verify(mIWifiChipV12, never()).resetTxPowerScenario();
+        mWifiVendorHal.stopVendorHal();
+    }
+
+    /**
      * Test the selectTxPowerScenario HIDL method invocation with sensor related scenarios
      * to IWifiChip 1.2 interface
      */