Add getThermalHeadroomThresholds API

Test: atest ThermalManagerServiceTest
Bug: 288119641
Change-Id: I06b1f389dfb6805c7aa07599b888438a2eb17993
diff --git a/core/api/current.txt b/core/api/current.txt
index d8ea721..d641fba 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -33039,6 +33039,7 @@
     method public int getCurrentThermalStatus();
     method public int getLocationPowerSaveMode();
     method public float getThermalHeadroom(@IntRange(from=0, to=60) int);
+    method @FlaggedApi("android.os.allow_thermal_headroom_thresholds") @NonNull public java.util.Map<java.lang.Integer,java.lang.Float> getThermalHeadroomThresholds();
     method public boolean isAllowedInLowPowerStandby(int);
     method public boolean isAllowedInLowPowerStandby(@NonNull String);
     method public boolean isBatteryDischargePredictionPersonalized();
diff --git a/core/java/android/os/IThermalService.aidl b/core/java/android/os/IThermalService.aidl
index c6c8adc..bcffa45 100644
--- a/core/java/android/os/IThermalService.aidl
+++ b/core/java/android/os/IThermalService.aidl
@@ -111,4 +111,9 @@
      *     occur; returns NaN if the headroom or forecast is unavailable
      */
     float getThermalHeadroom(int forecastSeconds);
+
+    /**
+     * @return thermal headroom for each thermal status
+     */
+    float[] getThermalHeadroomThresholds();
 }
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index fce715a..0511cfd 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -19,6 +19,7 @@
 import android.Manifest.permission;
 import android.annotation.CallbackExecutor;
 import android.annotation.CurrentTimeMillisLong;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
@@ -41,14 +42,17 @@
 
 import com.android.internal.util.Preconditions;
 
+import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.Executor;
@@ -1179,6 +1183,8 @@
 
     private final ArrayMap<OnThermalStatusChangedListener, IThermalStatusListener>
             mListenerMap = new ArrayMap<>();
+    private final Object mThermalHeadroomThresholdsLock = new Object();
+    private float[] mThermalHeadroomThresholds = null;
 
     /**
      * {@hide}
@@ -2634,6 +2640,7 @@
     public static final int THERMAL_STATUS_SHUTDOWN = Temperature.THROTTLING_SHUTDOWN;
 
     /** @hide */
+    @Target(ElementType.TYPE_USE)
     @IntDef(prefix = { "THERMAL_STATUS_" }, value = {
             THERMAL_STATUS_NONE,
             THERMAL_STATUS_LIGHT,
@@ -2798,6 +2805,63 @@
     }
 
     /**
+     * Gets the thermal headroom thresholds for all available thermal throttling status above
+     * {@link #THERMAL_STATUS_NONE}.
+     * <p>
+     * A thermal status key in the returned map is only set if the device manufacturer has the
+     * corresponding threshold defined for at least one of its sensors. If it's set, one should
+     * expect to see that from {@link #getCurrentThermalStatus()} or
+     * {@link OnThermalStatusChangedListener#onThermalStatusChanged(int)}.
+     * <p>
+     * The headroom threshold is used to interpret the possible thermal throttling status based on
+     * the headroom prediction. For example, if the headroom threshold for
+     * {@link #THERMAL_STATUS_LIGHT} is 0.7, and a headroom prediction in 10s returns 0.75
+     * (or {@code getThermalHeadroom(10)=0.75}), one can expect that in 10 seconds the system could
+     * be in lightly throttled state if the workload remains the same. The app can consider
+     * taking actions according to the nearest throttling status the difference between the headroom
+     * and the threshold.
+     * <p>
+     * For new devices it's guaranteed to have a single sensor, but for older devices with multiple
+     * sensors reporting different threshold values, the minimum threshold is taken to be
+     * conservative on predictions. Thus, when reading real-time headroom, it's not guaranteed that
+     * a real-time value of 0.75 (or {@code getThermalHeadroom(0)}=0.75) exceeding the threshold of
+     * 0.7 above will always come with lightly throttled state
+     * (or {@code getCurrentThermalStatus()=THERMAL_STATUS_LIGHT}) but it can be lower
+     * (or {@code getCurrentThermalStatus()=THERMAL_STATUS_NONE}). While it's always guaranteed that
+     * the device won't be throttled heavier than the unmet threshold's state, so a real-time
+     * headroom of 0.75 will never come with {@link #THERMAL_STATUS_MODERATE} but lower, and 0.65
+     * will never come with {@link #THERMAL_STATUS_LIGHT} but {@link #THERMAL_STATUS_NONE}.
+     * <p>
+     * The returned map of thresholds will not change between calls to this function, so it's
+     * best to call this once on initialization. Modifying the result will not change the thresholds
+     * cached by the system, and a new call to the API will get a new copy.
+     *
+     * @return map from each thermal status to its thermal headroom
+     * @throws IllegalStateException if the thermal service is not ready
+     * @throws UnsupportedOperationException if the feature is not enabled
+     */
+    @FlaggedApi(Flags.FLAG_ALLOW_THERMAL_HEADROOM_THRESHOLDS)
+    public @NonNull Map<@ThermalStatus Integer, Float> getThermalHeadroomThresholds() {
+        try {
+            synchronized (mThermalHeadroomThresholdsLock) {
+                if (mThermalHeadroomThresholds == null) {
+                    mThermalHeadroomThresholds = mThermalService.getThermalHeadroomThresholds();
+                }
+                final ArrayMap<Integer, Float> ret = new ArrayMap<>(THERMAL_STATUS_SHUTDOWN);
+                for (int status = THERMAL_STATUS_LIGHT; status <= THERMAL_STATUS_SHUTDOWN;
+                        status++) {
+                    if (!Float.isNaN(mThermalHeadroomThresholds[status])) {
+                        ret.put(status, mThermalHeadroomThresholds[status]);
+                    }
+                }
+                return ret;
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * If true, the doze component is not started until after the screen has been
      * turned off and the screen off animation has been performed.
      * @hide
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index c4521c0..7aac9ef 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -22,6 +22,13 @@
 }
 
 flag {
+    name: "allow_thermal_headroom_thresholds"
+    namespace: "game"
+    description: "Enable thermal headroom thresholds API"
+    bug: "288119641"
+}
+
+flag {
     name: "allow_private_profile"
     namespace: "profile_experiences"
     description: "Guards a new Private Profile type in UserManager - everything from its setup to config to deletion."
diff --git a/services/core/java/com/android/server/power/ThermalManagerService.java b/services/core/java/com/android/server/power/ThermalManagerService.java
index 99064bc..d17207b 100644
--- a/services/core/java/com/android/server/power/ThermalManagerService.java
+++ b/services/core/java/com/android/server/power/ThermalManagerService.java
@@ -28,6 +28,7 @@
 import android.hardware.thermal.V1_1.IThermalCallback;
 import android.os.Binder;
 import android.os.CoolingDevice;
+import android.os.Flags;
 import android.os.Handler;
 import android.os.HwBinder;
 import android.os.IBinder;
@@ -181,7 +182,7 @@
                 onTemperatureChanged(temperatures.get(i), false);
             }
             onTemperatureMapChangedLocked();
-            mTemperatureWatcher.updateSevereThresholds();
+            mTemperatureWatcher.updateThresholds();
             mHalReady.set(true);
         }
     }
@@ -506,6 +507,20 @@
         }
 
         @Override
+        public float[] getThermalHeadroomThresholds() {
+            if (!mHalReady.get()) {
+                throw new IllegalStateException("Thermal HAL connection is not initialized");
+            }
+            if (!Flags.allowThermalHeadroomThresholds()) {
+                throw new UnsupportedOperationException("Thermal headroom thresholds not enabled");
+            }
+            synchronized (mTemperatureWatcher.mSamples) {
+                return Arrays.copyOf(mTemperatureWatcher.mHeadroomThresholds,
+                        mTemperatureWatcher.mHeadroomThresholds.length);
+            }
+        }
+
+        @Override
         protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
             dumpInternal(fd, pw, args);
         }
@@ -580,6 +595,12 @@
                             mHalWrapper.getTemperatureThresholds(false, 0));
                 }
             }
+            if (Flags.allowThermalHeadroomThresholds()) {
+                synchronized (mTemperatureWatcher.mSamples) {
+                    pw.println("Temperature headroom thresholds:");
+                    pw.println(Arrays.toString(mTemperatureWatcher.mHeadroomThresholds));
+                }
+            }
         } finally {
             Binder.restoreCallingIdentity(token);
         }
@@ -964,7 +985,14 @@
                         connectToHal();
                     }
                     if (mInstance != null) {
-                        Slog.i(TAG, "Thermal HAL AIDL service connected.");
+                        try {
+                            Slog.i(TAG, "Thermal HAL AIDL service connected with version "
+                                    + mInstance.getInterfaceVersion());
+                        } catch (RemoteException e) {
+                            Slog.e(TAG, "Unable to read interface version from Thermal HAL", e);
+                            connectToHal();
+                            return;
+                        }
                         registerThermalChangedCallback();
                     }
                 }
@@ -1440,26 +1468,55 @@
         ArrayMap<String, Float> mSevereThresholds = new ArrayMap<>();
 
         @GuardedBy("mSamples")
+        float[] mHeadroomThresholds = new float[ThrottlingSeverity.SHUTDOWN + 1];
+        @GuardedBy("mSamples")
         private long mLastForecastCallTimeMillis = 0;
 
         private static final int INACTIVITY_THRESHOLD_MILLIS = 10000;
         @VisibleForTesting
         long mInactivityThresholdMillis = INACTIVITY_THRESHOLD_MILLIS;
 
-        void updateSevereThresholds() {
+        void updateThresholds() {
             synchronized (mSamples) {
                 List<TemperatureThreshold> thresholds =
                         mHalWrapper.getTemperatureThresholds(true, Temperature.TYPE_SKIN);
+                if (Flags.allowThermalHeadroomThresholds()) {
+                    Arrays.fill(mHeadroomThresholds, Float.NaN);
+                }
                 for (int t = 0; t < thresholds.size(); ++t) {
                     TemperatureThreshold threshold = thresholds.get(t);
                     if (threshold.hotThrottlingThresholds.length <= ThrottlingSeverity.SEVERE) {
                         continue;
                     }
-                    float temperature =
+                    float severeThreshold =
                             threshold.hotThrottlingThresholds[ThrottlingSeverity.SEVERE];
-                    if (!Float.isNaN(temperature)) {
-                        mSevereThresholds.put(threshold.name,
-                                threshold.hotThrottlingThresholds[ThrottlingSeverity.SEVERE]);
+                    if (!Float.isNaN(severeThreshold)) {
+                        mSevereThresholds.put(threshold.name, severeThreshold);
+                        for (int severity = ThrottlingSeverity.LIGHT;
+                                severity <= ThrottlingSeverity.SHUTDOWN; severity++) {
+                            if (Flags.allowThermalHeadroomThresholds()
+                                    && threshold.hotThrottlingThresholds.length > severity) {
+                                updateHeadroomThreshold(severity,
+                                        threshold.hotThrottlingThresholds[severity],
+                                        severeThreshold);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        // For a older device with multiple SKIN sensors, we will set a severity's headroom
+        // threshold based on the minimum value of all as a workaround.
+        void updateHeadroomThreshold(int severity, float threshold, float severeThreshold) {
+            if (!Float.isNaN(threshold)) {
+                synchronized (mSamples) {
+                    float headroom = normalizeTemperature(threshold, severeThreshold);
+                    if (Float.isNaN(mHeadroomThresholds[severity])) {
+                        mHeadroomThresholds[severity] = headroom;
+                    } else {
+                        float lastHeadroom = mHeadroomThresholds[severity];
+                        mHeadroomThresholds[severity] = Math.min(lastHeadroom, headroom);
                     }
                 }
             }
@@ -1541,15 +1598,13 @@
         private static final float DEGREES_BETWEEN_ZERO_AND_ONE = 30.0f;
 
         @VisibleForTesting
-        float normalizeTemperature(float temperature, float severeThreshold) {
-            synchronized (mSamples) {
-                float zeroNormalized = severeThreshold - DEGREES_BETWEEN_ZERO_AND_ONE;
-                if (temperature <= zeroNormalized) {
-                    return 0.0f;
-                }
-                float delta = temperature - zeroNormalized;
-                return delta / DEGREES_BETWEEN_ZERO_AND_ONE;
+        static float normalizeTemperature(float temperature, float severeThreshold) {
+            float zeroNormalized = severeThreshold - DEGREES_BETWEEN_ZERO_AND_ONE;
+            if (temperature <= zeroNormalized) {
+                return 0.0f;
             }
+            float delta = temperature - zeroNormalized;
+            return delta / DEGREES_BETWEEN_ZERO_AND_ONE;
         }
 
         private static final int MINIMUM_SAMPLE_COUNT = 3;
diff --git a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java
index 13c011a..44dad59 100644
--- a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java
@@ -18,9 +18,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -28,6 +30,7 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -49,6 +52,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.SystemService;
+import com.android.server.power.ThermalManagerService.TemperatureWatcher;
 import com.android.server.power.ThermalManagerService.ThermalHalWrapper;
 
 import org.junit.Before;
@@ -65,6 +69,7 @@
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 
 /**
  * atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server
@@ -415,9 +420,9 @@
 
     @Test
     public void testTemperatureWatcherUpdateSevereThresholds() throws RemoteException {
-        ThermalManagerService.TemperatureWatcher watcher = mService.mTemperatureWatcher;
+        TemperatureWatcher watcher = mService.mTemperatureWatcher;
         watcher.mSevereThresholds.erase();
-        watcher.updateSevereThresholds();
+        watcher.updateThresholds();
         assertEquals(1, watcher.mSevereThresholds.size());
         assertEquals("skin1", watcher.mSevereThresholds.keyAt(0));
         Float threshold = watcher.mSevereThresholds.get("skin1");
@@ -426,9 +431,60 @@
     }
 
     @Test
+    public void testTemperatureWatcherUpdateHeadroomThreshold() {
+        TemperatureWatcher watcher = mService.mTemperatureWatcher;
+        synchronized (watcher.mSamples) {
+            Arrays.fill(watcher.mHeadroomThresholds, Float.NaN);
+        }
+        watcher.updateHeadroomThreshold(ThrottlingSeverity.LIGHT, 40, 49);
+        watcher.updateHeadroomThreshold(ThrottlingSeverity.MODERATE, 46, 49);
+        watcher.updateHeadroomThreshold(ThrottlingSeverity.SEVERE, 49, 49);
+        watcher.updateHeadroomThreshold(ThrottlingSeverity.CRITICAL, 64, 49);
+        watcher.updateHeadroomThreshold(ThrottlingSeverity.EMERGENCY, 70, 49);
+        watcher.updateHeadroomThreshold(ThrottlingSeverity.SHUTDOWN, 79, 49);
+        synchronized (watcher.mSamples) {
+            assertArrayEquals(new float[]{Float.NaN, 0.7f, 0.9f, 1.0f, 1.5f, 1.7f, 2.0f},
+                    watcher.mHeadroomThresholds, 0.01f);
+        }
+
+        // when another sensor reports different threshold, we expect to see smaller one to be used
+        watcher.updateHeadroomThreshold(ThrottlingSeverity.LIGHT, 37, 52);
+        watcher.updateHeadroomThreshold(ThrottlingSeverity.MODERATE, 46, 52);
+        watcher.updateHeadroomThreshold(ThrottlingSeverity.SEVERE, 52, 52);
+        watcher.updateHeadroomThreshold(ThrottlingSeverity.CRITICAL, 64, 52);
+        watcher.updateHeadroomThreshold(ThrottlingSeverity.EMERGENCY, 100, 52);
+        watcher.updateHeadroomThreshold(ThrottlingSeverity.SHUTDOWN, 200, 52);
+        synchronized (watcher.mSamples) {
+            assertArrayEquals(new float[]{Float.NaN, 0.5f, 0.8f, 1.0f, 1.4f, 1.7f, 2.0f},
+                    watcher.mHeadroomThresholds, 0.01f);
+        }
+    }
+
+    @Test
+    public void testGetThermalHeadroomThresholdsOnlyReadOnce() throws Exception {
+        float[] expected = new float[]{Float.NaN, 0.1f, 0.2f, 0.3f, 0.4f, Float.NaN, 0.6f};
+        when(mIThermalServiceMock.getThermalHeadroomThresholds()).thenReturn(expected);
+        Map<Integer, Float> thresholds1 = mPowerManager.getThermalHeadroomThresholds();
+        verify(mIThermalServiceMock, times(1)).getThermalHeadroomThresholds();
+        for (int status = PowerManager.THERMAL_STATUS_LIGHT;
+                status <= PowerManager.THERMAL_STATUS_SHUTDOWN; status++) {
+            if (Float.isNaN(expected[status])) {
+                assertFalse(thresholds1.containsKey(status));
+            } else {
+                assertEquals(expected[status], thresholds1.get(status), 0.01f);
+            }
+        }
+        reset(mIThermalServiceMock);
+        Map<Integer, Float> thresholds2 = mPowerManager.getThermalHeadroomThresholds();
+        verify(mIThermalServiceMock, times(0)).getThermalHeadroomThresholds();
+        assertNotSame(thresholds1, thresholds2);
+        assertEquals(thresholds1, thresholds2);
+    }
+
+    @Test
     public void testTemperatureWatcherGetSlopeOf() throws RemoteException {
-        ThermalManagerService.TemperatureWatcher watcher = mService.mTemperatureWatcher;
-        List<ThermalManagerService.TemperatureWatcher.Sample> samples = new ArrayList<>();
+        TemperatureWatcher watcher = mService.mTemperatureWatcher;
+        List<TemperatureWatcher.Sample> samples = new ArrayList<>();
         for (int i = 0; i < 30; ++i) {
             samples.add(watcher.createSampleForTesting(i, (float) (i / 2 * 2)));
         }
@@ -437,21 +493,23 @@
 
     @Test
     public void testTemperatureWatcherNormalizeTemperature() throws RemoteException {
-        ThermalManagerService.TemperatureWatcher watcher = mService.mTemperatureWatcher;
-        assertEquals(0.5f, watcher.normalizeTemperature(25.0f, 40.0f), 0.0f);
+        assertEquals(0.5f,
+                TemperatureWatcher.normalizeTemperature(25.0f, 40.0f), 0.0f);
 
         // Temperatures more than 30 degrees below the SEVERE threshold should be clamped to 0.0f
-        assertEquals(0.0f, watcher.normalizeTemperature(0.0f, 40.0f), 0.0f);
+        assertEquals(0.0f,
+                TemperatureWatcher.normalizeTemperature(0.0f, 40.0f), 0.0f);
 
         // Temperatures above the SEVERE threshold should not be clamped
-        assertEquals(2.0f, watcher.normalizeTemperature(70.0f, 40.0f), 0.0f);
+        assertEquals(2.0f,
+                TemperatureWatcher.normalizeTemperature(70.0f, 40.0f), 0.0f);
     }
 
     @Test
     public void testTemperatureWatcherGetForecast() throws RemoteException {
-        ThermalManagerService.TemperatureWatcher watcher = mService.mTemperatureWatcher;
+        TemperatureWatcher watcher = mService.mTemperatureWatcher;
 
-        ArrayList<ThermalManagerService.TemperatureWatcher.Sample> samples = new ArrayList<>();
+        ArrayList<TemperatureWatcher.Sample> samples = new ArrayList<>();
 
         // Add a single sample
         samples.add(watcher.createSampleForTesting(0, 25.0f));
@@ -478,7 +536,7 @@
 
     @Test
     public void testTemperatureWatcherGetForecastUpdate() throws Exception {
-        ThermalManagerService.TemperatureWatcher watcher = mService.mTemperatureWatcher;
+        TemperatureWatcher watcher = mService.mTemperatureWatcher;
 
         // Reduce the inactivity threshold to speed up testing
         watcher.mInactivityThresholdMillis = 2000;
@@ -499,7 +557,7 @@
     }
 
     // Helper function to hold mSamples lock, avoid GuardedBy lint errors
-    private boolean isWatcherSamplesEmpty(ThermalManagerService.TemperatureWatcher watcher) {
+    private boolean isWatcherSamplesEmpty(TemperatureWatcher watcher) {
         synchronized (watcher.mSamples) {
             return watcher.mSamples.isEmpty();
         }