Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2b05463e authored by Xiang Wang's avatar Xiang Wang
Browse files

Add SDK support for thermal headroom callback API

* Add cache for headroom forecast which resets on temperature or threshold update
* Remove the cache for thermal headroom thresholds in PowerManager
  as it can change now
* Only trigger headroom callback on skin type throttling event or
  threshold update event that causes significant difference in headrooms

Bug: 360486877
Flag: android.os.allow_thermal_thresholds_callback
Test: atest ThermalManagerServiceTest ThermalManagerServiceMockingTest PowerManagerTest
Change-Id: Id5e311634f3b94fe041e51732496d182b2a78139
parent d4473d8f
Loading
Loading
Loading
Loading
+8 −1
Original line number Original line Diff line number Diff line
@@ -33862,12 +33862,14 @@ package android.os {
  }
  }
  public final class PowerManager {
  public final class PowerManager {
    method @FlaggedApi("android.os.allow_thermal_thresholds_callback") public void addThermalHeadroomListener(@NonNull android.os.PowerManager.OnThermalHeadroomChangedListener);
    method @FlaggedApi("android.os.allow_thermal_thresholds_callback") public void addThermalHeadroomListener(@NonNull java.util.concurrent.Executor, @NonNull android.os.PowerManager.OnThermalHeadroomChangedListener);
    method public void addThermalStatusListener(@NonNull android.os.PowerManager.OnThermalStatusChangedListener);
    method public void addThermalStatusListener(@NonNull android.os.PowerManager.OnThermalStatusChangedListener);
    method public void addThermalStatusListener(@NonNull java.util.concurrent.Executor, @NonNull android.os.PowerManager.OnThermalStatusChangedListener);
    method public void addThermalStatusListener(@NonNull java.util.concurrent.Executor, @NonNull android.os.PowerManager.OnThermalStatusChangedListener);
    method @Nullable public java.time.Duration getBatteryDischargePrediction();
    method @Nullable public java.time.Duration getBatteryDischargePrediction();
    method public int getCurrentThermalStatus();
    method public int getCurrentThermalStatus();
    method public int getLocationPowerSaveMode();
    method public int getLocationPowerSaveMode();
    method public float getThermalHeadroom(@IntRange(from=0, to=60) int);
    method @FloatRange(from=0.0f) 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 @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(int);
    method public boolean isAllowedInLowPowerStandby(@NonNull String);
    method public boolean isAllowedInLowPowerStandby(@NonNull String);
@@ -33885,6 +33887,7 @@ package android.os {
    method public boolean isWakeLockLevelSupported(int);
    method public boolean isWakeLockLevelSupported(int);
    method public android.os.PowerManager.WakeLock newWakeLock(int, String);
    method public android.os.PowerManager.WakeLock newWakeLock(int, String);
    method @RequiresPermission(android.Manifest.permission.REBOOT) public void reboot(@Nullable String);
    method @RequiresPermission(android.Manifest.permission.REBOOT) public void reboot(@Nullable String);
    method @FlaggedApi("android.os.allow_thermal_thresholds_callback") public void removeThermalHeadroomListener(@NonNull android.os.PowerManager.OnThermalHeadroomChangedListener);
    method public void removeThermalStatusListener(@NonNull android.os.PowerManager.OnThermalStatusChangedListener);
    method public void removeThermalStatusListener(@NonNull android.os.PowerManager.OnThermalStatusChangedListener);
    field @Deprecated @RequiresPermission(value=android.Manifest.permission.TURN_SCREEN_ON, conditional=true) public static final int ACQUIRE_CAUSES_WAKEUP = 268435456; // 0x10000000
    field @Deprecated @RequiresPermission(value=android.Manifest.permission.TURN_SCREEN_ON, conditional=true) public static final int ACQUIRE_CAUSES_WAKEUP = 268435456; // 0x10000000
    field public static final String ACTION_DEVICE_IDLE_MODE_CHANGED = "android.os.action.DEVICE_IDLE_MODE_CHANGED";
    field public static final String ACTION_DEVICE_IDLE_MODE_CHANGED = "android.os.action.DEVICE_IDLE_MODE_CHANGED";
@@ -33917,6 +33920,10 @@ package android.os {
    field public static final int THERMAL_STATUS_SHUTDOWN = 6; // 0x6
    field public static final int THERMAL_STATUS_SHUTDOWN = 6; // 0x6
  }
  }
  @FlaggedApi("android.os.allow_thermal_thresholds_callback") public static interface PowerManager.OnThermalHeadroomChangedListener {
    method public void onThermalHeadroomChanged(@FloatRange(from=0.0f) float, @FloatRange(from=0.0f) float, @IntRange(from=0) int, @NonNull java.util.Map<java.lang.Integer,java.lang.Float>);
  }
  public static interface PowerManager.OnThermalStatusChangedListener {
  public static interface PowerManager.OnThermalStatusChangedListener {
    method public void onThermalStatusChanged(int);
    method public void onThermalStatusChanged(int);
  }
  }
+1 −0
Original line number Original line Diff line number Diff line
@@ -206,6 +206,7 @@ filegroup {
        "android/os/Temperature.aidl",
        "android/os/Temperature.aidl",
        "android/os/CoolingDevice.aidl",
        "android/os/CoolingDevice.aidl",
        "android/os/IThermalEventListener.aidl",
        "android/os/IThermalEventListener.aidl",
        "android/os/IThermalHeadroomListener.aidl",
        "android/os/IThermalStatusListener.aidl",
        "android/os/IThermalStatusListener.aidl",
        "android/os/IThermalService.aidl",
        "android/os/IThermalService.aidl",
        "android/os/IPowerManager.aidl",
        "android/os/IPowerManager.aidl",
+31 −0
Original line number Original line Diff line number Diff line
/*
** Copyright 2024, 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.
*/

package android.os;

/**
 * Listener for thermal headroom and threshold changes.
 * This is mainly used by {@link android.os.PowerManager} to serve public thermal headoom related
 * APIs.
 * {@hide}
 */
oneway interface IThermalHeadroomListener {
    /**
     * Called when thermal headroom or thresholds changed.
     */
    void onHeadroomChange(in float headroom, in float forecastHeadroom,
                                 in int forecastSeconds, in float[] thresholds);
}
+17 −0
Original line number Original line Diff line number Diff line
@@ -18,6 +18,7 @@ package android.os;


import android.os.CoolingDevice;
import android.os.CoolingDevice;
import android.os.IThermalEventListener;
import android.os.IThermalEventListener;
import android.os.IThermalHeadroomListener;
import android.os.IThermalStatusListener;
import android.os.IThermalStatusListener;
import android.os.Temperature;
import android.os.Temperature;


@@ -116,4 +117,20 @@ interface IThermalService {
     * @return thermal headroom for each thermal status
     * @return thermal headroom for each thermal status
     */
     */
    float[] getThermalHeadroomThresholds();
    float[] getThermalHeadroomThresholds();

    /**
      * Register a listener for thermal headroom change.
      * @param listener the {@link android.os.IThermalHeadroomListener} to be notified.
      * @return true if registered successfully.
      * {@hide}
      */
    boolean registerThermalHeadroomListener(in IThermalHeadroomListener listener);

    /**
      * Unregister a previously-registered listener for thermal headroom.
      * @param listener the {@link android.os.IThermalHeadroomListener} to no longer be notified.
      * @return true if unregistered successfully.
      * {@hide}
      */
    boolean unregisterThermalHeadroomListener(in IThermalHeadroomListener listener);
}
}
+185 −53
Original line number Original line Diff line number Diff line
@@ -20,6 +20,7 @@ import android.Manifest.permission;
import android.annotation.CallbackExecutor;
import android.annotation.CallbackExecutor;
import android.annotation.CurrentTimeMillisLong;
import android.annotation.CurrentTimeMillisLong;
import android.annotation.FlaggedApi;
import android.annotation.FlaggedApi;
import android.annotation.FloatRange;
import android.annotation.IntDef;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.NonNull;
@@ -40,6 +41,7 @@ import android.util.Log;
import android.util.proto.ProtoOutputStream;
import android.util.proto.ProtoOutputStream;
import android.view.Display;
import android.view.Display;


import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
import com.android.internal.util.Preconditions;


import java.lang.annotation.ElementType;
import java.lang.annotation.ElementType;
@@ -1191,10 +1193,12 @@ public final class PowerManager {
    /** We lazily initialize it.*/
    /** We lazily initialize it.*/
    private PowerExemptionManager mPowerExemptionManager;
    private PowerExemptionManager mPowerExemptionManager;


    @GuardedBy("mThermalStatusListenerMap")
    private final ArrayMap<OnThermalStatusChangedListener, IThermalStatusListener>
    private final ArrayMap<OnThermalStatusChangedListener, IThermalStatusListener>
            mListenerMap = new ArrayMap<>();
            mThermalStatusListenerMap = new ArrayMap<>();
    private final Object mThermalHeadroomThresholdsLock = new Object();
    @GuardedBy("mThermalHeadroomListenerMap")
    private float[] mThermalHeadroomThresholds = null;
    private final ArrayMap<OnThermalHeadroomChangedListener, IThermalHeadroomListener>
            mThermalHeadroomListenerMap = new ArrayMap<>();


    /**
    /**
     * {@hide}
     * {@hide}
@@ -2681,15 +2685,59 @@ public final class PowerManager {
        void onThermalStatusChanged(@ThermalStatus int status);
        void onThermalStatusChanged(@ThermalStatus int status);
    }
    }


    /**
     * Listener passed to
     * {@link PowerManager#addThermalHeadroomListener} and
     * {@link PowerManager#removeThermalHeadroomListener}
     * to notify caller of Thermal headroom or thresholds changes.
     */
    @FlaggedApi(Flags.FLAG_ALLOW_THERMAL_THRESHOLDS_CALLBACK)
    public interface OnThermalHeadroomChangedListener {


        /**
        /**
     * This function adds a listener for thermal status change, listen call back will be
         * Called when overall thermal headroom or headroom thresholds have significantly
         * changed that requires action.
         * <p>
         * This may not be used to fully replace the {@link #getThermalHeadroom(int)} API as it will
         * only notify on one of the conditions below that will significantly change one or both
         * values of current headroom and headroom thresholds since previous callback:
         *   1. thermal throttling events: when the skin temperature has cross any of the thresholds
         *      and there isn't a previous callback in a short time ago with similar values.
         *   2. skin temperature threshold change events: note that if the absolute °C threshold
         *      values change in a way that does not significantly change the current headroom nor
         *      headroom thresholds, it will not trigger any callback. The client should not
         *      need to take action in such case since the difference from temperature vs threshold
         *      hasn't changed.
         * <p>
         * By API version 36, it provides a forecast in the same call for developer's convenience
         * based on a {@code forecastSeconds} defined by the device, which can be static or dynamic
         * varied by OEM. Be aware that it will not notify on forecast temperature change but the
         * events mentioned above. So periodically polling against {@link #getThermalHeadroom(int)}
         * API should still be used to actively monitor temperature forecast in advance.
         * <p>
         * This serves as a more advanced option compared to thermal status listener, where the
         * latter will only notify on thermal throttling events with status update.
         *
         * @param headroom current headroom
         * @param forecastHeadroom forecasted headroom in future
         * @param forecastSeconds how many seconds in the future used in forecast
         * @param thresholds new headroom thresholds, see {@link #getThermalHeadroomThresholds()}
         */
        void onThermalHeadroomChanged(
                @FloatRange(from = 0f) float headroom,
                @FloatRange(from = 0f) float forecastHeadroom,
                @IntRange(from = 0) int forecastSeconds,
                @NonNull Map<@ThermalStatus Integer, Float> thresholds);
    }

    /**
     * This function adds a listener for thermal status change, listener callback will be
     * enqueued tasks on the main thread
     * enqueued tasks on the main thread
     *
     *
     * @param listener listener to be added,
     * @param listener listener to be added,
     */
     */
    public void addThermalStatusListener(@NonNull OnThermalStatusChangedListener listener) {
    public void addThermalStatusListener(@NonNull OnThermalStatusChangedListener listener) {
        Objects.requireNonNull(listener, "listener cannot be null");
        Objects.requireNonNull(listener, "Thermal status listener cannot be null");
        addThermalStatusListener(mContext.getMainExecutor(), listener);
        addThermalStatusListener(mContext.getMainExecutor(), listener);
    }
    }


@@ -2701,10 +2749,11 @@ public final class PowerManager {
     */
     */
    public void addThermalStatusListener(@NonNull @CallbackExecutor Executor executor,
    public void addThermalStatusListener(@NonNull @CallbackExecutor Executor executor,
            @NonNull OnThermalStatusChangedListener listener) {
            @NonNull OnThermalStatusChangedListener listener) {
        Objects.requireNonNull(listener, "listener cannot be null");
        Objects.requireNonNull(listener, "Thermal status listener cannot be null");
        Objects.requireNonNull(executor, "executor cannot be null");
        Objects.requireNonNull(executor, "Executor cannot be null");
        Preconditions.checkArgument(!mListenerMap.containsKey(listener),
        synchronized (mThermalStatusListenerMap) {
                "Listener already registered: %s", listener);
            Preconditions.checkArgument(!mThermalStatusListenerMap.containsKey(listener),
                    "Thermal status listener already registered: %s", listener);
            IThermalStatusListener internalListener = new IThermalStatusListener.Stub() {
            IThermalStatusListener internalListener = new IThermalStatusListener.Stub() {
                @Override
                @Override
                public void onStatusChange(int status) {
                public void onStatusChange(int status) {
@@ -2718,14 +2767,15 @@ public final class PowerManager {
            };
            };
            try {
            try {
                if (mThermalService.registerThermalStatusListener(internalListener)) {
                if (mThermalService.registerThermalStatusListener(internalListener)) {
                mListenerMap.put(listener, internalListener);
                    mThermalStatusListenerMap.put(listener, internalListener);
                } else {
                } else {
                throw new RuntimeException("Listener failed to set");
                    throw new RuntimeException("Thermal status listener failed to set");
                }
                }
            } catch (RemoteException e) {
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
                throw e.rethrowFromSystemServer();
            }
            }
        }
        }
    }


    /**
    /**
     * This function removes a listener for thermal status change
     * This function removes a listener for thermal status change
@@ -2733,19 +2783,100 @@ public final class PowerManager {
     * @param listener listener to be removed
     * @param listener listener to be removed
     */
     */
    public void removeThermalStatusListener(@NonNull OnThermalStatusChangedListener listener) {
    public void removeThermalStatusListener(@NonNull OnThermalStatusChangedListener listener) {
        Objects.requireNonNull(listener, "listener cannot be null");
        Objects.requireNonNull(listener, "Thermal status listener cannot be null");
        IThermalStatusListener internalListener = mListenerMap.get(listener);
        synchronized (mThermalStatusListenerMap) {
        Preconditions.checkArgument(internalListener != null, "Listener was not added");
            IThermalStatusListener internalListener = mThermalStatusListenerMap.get(listener);
            Preconditions.checkArgument(internalListener != null,
                    "Thermal status listener was not added");
            try {
            try {
                if (mThermalService.unregisterThermalStatusListener(internalListener)) {
                if (mThermalService.unregisterThermalStatusListener(internalListener)) {
                mListenerMap.remove(listener);
                    mThermalStatusListenerMap.remove(listener);
                } else {
                } else {
                throw new RuntimeException("Listener failed to remove");
                    throw new RuntimeException("Failed to unregister thermal status listener");
                }
                }
            } catch (RemoteException e) {
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
                throw e.rethrowFromSystemServer();
            }
            }
        }
        }
    }

    /**
     * This function adds a listener for thermal headroom change, listener callback will be
     * enqueued tasks on the main thread
     *
     * @param listener listener to be added,
     */
    @FlaggedApi(Flags.FLAG_ALLOW_THERMAL_THRESHOLDS_CALLBACK)
    public void addThermalHeadroomListener(@NonNull OnThermalHeadroomChangedListener listener) {
        Objects.requireNonNull(listener, "Thermal headroom listener cannot be null");
        addThermalHeadroomListener(mContext.getMainExecutor(), listener);
    }

    /**
     * This function adds a listener for thermal headroom change.
     *
     * @param executor {@link Executor} to handle listener callback.
     * @param listener listener to be added.
     */
    @FlaggedApi(Flags.FLAG_ALLOW_THERMAL_THRESHOLDS_CALLBACK)
    public void addThermalHeadroomListener(@NonNull @CallbackExecutor Executor executor,
            @NonNull OnThermalHeadroomChangedListener listener) {
        Objects.requireNonNull(listener, "Thermal headroom listener cannot be null");
        Objects.requireNonNull(executor, "Executor cannot be null");
        synchronized (mThermalHeadroomListenerMap) {
            Preconditions.checkArgument(!mThermalHeadroomListenerMap.containsKey(listener),
                    "Thermal headroom listener already registered: %s", listener);
            IThermalHeadroomListener internalListener = new IThermalHeadroomListener.Stub() {
                @Override
                public void onHeadroomChange(float headroom, float forecastHeadroom,
                        int forecastSeconds, float[] thresholds)
                        throws RemoteException {
                    final Map<Integer, Float> map = convertThresholdsToMap(thresholds);
                    final long token = Binder.clearCallingIdentity();
                    try {
                        executor.execute(() -> listener.onThermalHeadroomChanged(headroom,
                                forecastHeadroom, forecastSeconds, map));
                    } finally {
                        Binder.restoreCallingIdentity(token);
                    }
                }
            };
            try {
                if (mThermalService.registerThermalHeadroomListener(internalListener)) {
                    mThermalHeadroomListenerMap.put(listener, internalListener);
                } else {
                    throw new RuntimeException("Thermal headroom listener failed to set");
                }
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }
    }

    /**
     * This function removes a listener for Thermal headroom change
     *
     * @param listener listener to be removed
     */
    @FlaggedApi(Flags.FLAG_ALLOW_THERMAL_THRESHOLDS_CALLBACK)
    public void removeThermalHeadroomListener(@NonNull OnThermalHeadroomChangedListener listener) {
        Objects.requireNonNull(listener, "Thermal headroom listener cannot be null");
        synchronized (mThermalHeadroomListenerMap) {
            IThermalHeadroomListener internalListener = mThermalHeadroomListenerMap.get(listener);
            Preconditions.checkArgument(internalListener != null,
                    "Thermal headroom listener was not added");
            try {
                if (mThermalService.unregisterThermalHeadroomListener(internalListener)) {
                    mThermalHeadroomListenerMap.remove(listener);
                } else {
                    throw new RuntimeException("Failed to unregister thermal status listener");
                }
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }
    }



    @CurrentTimeMillisLong
    @CurrentTimeMillisLong
    private final AtomicLong mLastHeadroomUpdate = new AtomicLong(0L);
    private final AtomicLong mLastHeadroomUpdate = new AtomicLong(0L);
@@ -2786,7 +2917,8 @@ public final class PowerManager {
     *         functionality or if this function is called significantly faster than once per
     *         functionality or if this function is called significantly faster than once per
     *         second.
     *         second.
     */
     */
    public float getThermalHeadroom(@IntRange(from = 0, to = 60) int forecastSeconds) {
    public @FloatRange(from = 0f) float getThermalHeadroom(
            @IntRange(from = 0, to = 60) int forecastSeconds) {
        // Rate-limit calls into the thermal service
        // Rate-limit calls into the thermal service
        long now = SystemClock.elapsedRealtime();
        long now = SystemClock.elapsedRealtime();
        long timeSinceLastUpdate = now - mLastHeadroomUpdate.get();
        long timeSinceLastUpdate = now - mLastHeadroomUpdate.get();
@@ -2831,9 +2963,11 @@ public final class PowerManager {
     * headroom of 0.75 will never come with {@link #THERMAL_STATUS_MODERATE} but lower, and 0.65
     * 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}.
     * will never come with {@link #THERMAL_STATUS_LIGHT} but {@link #THERMAL_STATUS_NONE}.
     * <p>
     * <p>
     * The returned map of thresholds will not change between calls to this function, so it's
     * Starting at {@link android.os.Build.VERSION_CODES#BAKLAVA} the returned map of thresholds can
     * best to call this once on initialization. Modifying the result will not change the thresholds
     * change between calls to this function, one could use the new
     * cached by the system, and a new call to the API will get a new copy.
     * {@link #addThermalHeadroomListener(Executor, OnThermalHeadroomChangedListener)} API to
     * register a listener and get callback for changes to thresholds.
     * <p>
     *
     *
     * @return map from each thermal status to its thermal headroom
     * @return map from each thermal status to its thermal headroom
     * @throws IllegalStateException if the thermal service is not ready
     * @throws IllegalStateException if the thermal service is not ready
@@ -2842,23 +2976,21 @@ public final class PowerManager {
    @FlaggedApi(Flags.FLAG_ALLOW_THERMAL_HEADROOM_THRESHOLDS)
    @FlaggedApi(Flags.FLAG_ALLOW_THERMAL_HEADROOM_THRESHOLDS)
    public @NonNull Map<@ThermalStatus Integer, Float> getThermalHeadroomThresholds() {
    public @NonNull Map<@ThermalStatus Integer, Float> getThermalHeadroomThresholds() {
        try {
        try {
            synchronized (mThermalHeadroomThresholdsLock) {
            return convertThresholdsToMap(mThermalService.getThermalHeadroomThresholds());
                if (mThermalHeadroomThresholds == null) {
        } catch (RemoteException e) {
                    mThermalHeadroomThresholds = mThermalService.getThermalHeadroomThresholds();
            throw e.rethrowFromSystemServer();
        }
        }
    }

    private Map<@ThermalStatus Integer, Float> convertThresholdsToMap(final float[] thresholds) {
        final ArrayMap<Integer, Float> ret = new ArrayMap<>(THERMAL_STATUS_SHUTDOWN);
        final ArrayMap<Integer, Float> ret = new ArrayMap<>(THERMAL_STATUS_SHUTDOWN);
                for (int status = THERMAL_STATUS_LIGHT; status <= THERMAL_STATUS_SHUTDOWN;
        for (int status = THERMAL_STATUS_LIGHT; status <= THERMAL_STATUS_SHUTDOWN; status++) {
                        status++) {
            if (!Float.isNaN(thresholds[status])) {
                    if (!Float.isNaN(mThermalHeadroomThresholds[status])) {
                ret.put(status, thresholds[status]);
                        ret.put(status, mThermalHeadroomThresholds[status]);
            }
            }
        }
        }
        return ret;
        return ret;
    }
    }
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }


    /**
    /**
     * If true, the doze component is not started until after the screen has been
     * If true, the doze component is not started until after the screen has been
Loading