Merge changes from topic "rate limit battery changed broadcast" into main

* changes:
  Don't trigger batterychanged broadcast when ChargeCounter is updated.
  Rate limit the battery changed broadcast.
diff --git a/services/core/java/com/android/server/BatteryService.java b/services/core/java/com/android/server/BatteryService.java
index 78bc658..3dcca14 100644
--- a/services/core/java/com/android/server/BatteryService.java
+++ b/services/core/java/com/android/server/BatteryService.java
@@ -22,6 +22,9 @@
 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import static com.android.server.health.Utils.copyV1Battery;
 
+import static java.lang.Math.abs;
+
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.ActivityManager;
@@ -48,6 +51,7 @@
 import android.os.Handler;
 import android.os.IBatteryPropertiesRegistrar;
 import android.os.IBinder;
+import android.os.Looper;
 import android.os.OsProtoEnums;
 import android.os.PowerManager;
 import android.os.RemoteException;
@@ -67,6 +71,7 @@
 import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IBatteryStats;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.os.SomeArgs;
@@ -84,6 +89,7 @@
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.NoSuchElementException;
+import java.util.Objects;
 import java.util.concurrent.CopyOnWriteArraySet;
 
 /**
@@ -149,19 +155,112 @@
     private HealthInfo mHealthInfo;
     private final HealthInfo mLastHealthInfo = new HealthInfo();
     private boolean mBatteryLevelCritical;
-    private int mLastBatteryStatus;
-    private int mLastBatteryHealth;
-    private boolean mLastBatteryPresent;
-    private int mLastBatteryLevel;
-    private int mLastBatteryVoltage;
-    private int mLastBatteryTemperature;
-    private boolean mLastBatteryLevelCritical;
-    private int mLastMaxChargingCurrent;
-    private int mLastMaxChargingVoltage;
-    private int mLastChargeCounter;
-    private int mLastBatteryCycleCount;
-    private int mLastChargingState;
-    private int mLastBatteryCapacityLevel;
+
+    /**
+     * {@link HealthInfo#batteryStatus} value when {@link Intent#ACTION_BATTERY_CHANGED}
+     * broadcast was sent last.
+     * Note: This value may be used for internal operations and/or to determine whether to trigger
+     * the {@link Intent#ACTION_BATTERY_CHANGED} broadcast or not.
+     */
+    private int mLastBroadcastBatteryStatus;
+    /**
+     * {@link HealthInfo#batteryHealth} value when {@link Intent#ACTION_BATTERY_CHANGED}
+     * broadcast was sent last.
+     * Note: This value may be used for internal operations and/or to determine whether to trigger
+     * the {@link Intent#ACTION_BATTERY_CHANGED} broadcast or not.
+     */
+    private int mLastBroadcastBatteryHealth;
+    /**
+     * {@link HealthInfo#batteryPresent} value when {@link Intent#ACTION_BATTERY_CHANGED}
+     * broadcast was sent last.
+     * Note: This value may be used for internal operations and/or to determine whether to trigger
+     * the {@link Intent#ACTION_BATTERY_CHANGED} broadcast or not.
+     */
+    private boolean mLastBroadcastBatteryPresent;
+    /**
+     * {@link HealthInfo#batteryLevel} value when {@link Intent#ACTION_BATTERY_CHANGED}
+     * broadcast was sent last.
+     * Note: This value may be used for internal operations and/or to determine whether to trigger
+     * the {@link Intent#ACTION_BATTERY_CHANGED} broadcast or not.
+     */
+    private int mLastBroadcastBatteryLevel;
+    /**
+     * {@link HealthInfo#batteryVoltageMillivolts} value when {@link Intent#ACTION_BATTERY_CHANGED}
+     * broadcast was sent last.
+     * Note: This value may be used for internal operations and/or to determine whether to trigger
+     * the {@link Intent#ACTION_BATTERY_CHANGED} broadcast or not.
+     */
+    private int mLastBroadcastBatteryVoltage;
+    /**
+     * {@link HealthInfo#batteryTemperatureTenthsCelsius} value when
+     * {@link Intent#ACTION_BATTERY_CHANGED} broadcast was sent last.
+     * Note: This value may be used for internal operations and/or to determine whether to trigger
+     * the {@link Intent#ACTION_BATTERY_CHANGED} broadcast or not.
+     */
+    private int mLastBroadcastBatteryTemperature;
+    /**
+     * {@link #mBatteryLevelCritical} value when {@link Intent#ACTION_BATTERY_CHANGED}
+     * broadcast was sent last.
+     * Note: These values may be used for internal operations and/or to determine whether to trigger
+     * the broadcast or not.
+     */
+    private boolean mLastBroadcastBatteryLevelCritical;
+    /**
+     * {@link HealthInfo#maxChargingCurrentMicroamps} value when
+     * {@link Intent#ACTION_BATTERY_CHANGED} broadcast was sent last.
+     * Note: This value may be used for internal operations and/or to determine whether to trigger
+     * the {@link Intent#ACTION_BATTERY_CHANGED} broadcast or not.
+     */
+    private int mLastBroadcastMaxChargingCurrent;
+    /**
+     * {@link HealthInfo#maxChargingVoltageMicrovolts} value when
+     * {@link Intent#ACTION_BATTERY_CHANGED} broadcast was sent last.
+     * Note: This value may be used for internal operations and/or to determine whether to trigger
+     * the {@link Intent#ACTION_BATTERY_CHANGED} broadcast or not.
+     */
+    private int mLastBroadcastMaxChargingVoltage;
+    /**
+     * {@link HealthInfo#batteryChargeCounterUah} value when {@link Intent#ACTION_BATTERY_CHANGED}
+     * broadcast was sent last.
+     * Note: This value may be used for internal operations and/or to determine whether to trigger
+     * the {@link Intent#ACTION_BATTERY_CHANGED} broadcast or not.
+     */
+    private int mLastBroadcastChargeCounter;
+    /**
+     * {@link HealthInfo#batteryCycleCount} value when {@link Intent#ACTION_BATTERY_CHANGED}
+     * broadcast was sent last.
+     * Note: This value may be used for internal operations and/or to determine whether to trigger
+     * the {@link Intent#ACTION_BATTERY_CHANGED} broadcast or not.
+     */
+    private int mLastBroadcastBatteryCycleCount;
+    /**
+     * {@link HealthInfo#chargingState} value when {@link Intent#ACTION_BATTERY_CHANGED}
+     * broadcast was sent last.
+     * Note: This value may be used for internal operations and/or to determine whether to trigger
+     * the {@link Intent#ACTION_BATTERY_CHANGED} broadcast or not.
+     */
+    private int mLastBroadcastChargingState;
+    /**
+     * {@link HealthInfo#batteryCapacityLevel} value when {@link Intent#ACTION_BATTERY_CHANGED}
+     * broadcast was sent last.
+     * Note: This value may be used for internal operations and/or to determine whether to trigger
+     * the {@link Intent#ACTION_BATTERY_CHANGED} broadcast or not.
+     */
+    private int mLastBroadcastBatteryCapacityLevel;
+    /**
+     * {@link #mPlugType} value when {@link Intent#ACTION_BATTERY_CHANGED}
+     * broadcast was sent last.
+     * Note: These values may be used for internal operations and/or to determine whether to trigger
+     * the broadcast or not.
+     */
+    private int mLastBroadcastPlugType = -1; // Extra state so we can detect first run
+    /**
+     * {@link #mInvalidCharger} value when {@link Intent#ACTION_BATTERY_CHANGED}
+     * broadcast was sent last.
+     * Note: These values may be used for internal operations and/or to determine whether to trigger
+     * the broadcast or not.
+     */
+    private int mLastBroadcastInvalidCharger;
     /**
      * The last seen charging policy. This requires the
      * {@link android.Manifest.permission#BATTERY_STATS} permission and should therefore not be
@@ -172,7 +271,6 @@
     private int mSequence = 1;
 
     private int mInvalidCharger;
-    private int mLastInvalidCharger;
 
     private int mLowBatteryWarningLevel;
     private int mLastLowBatteryWarningLevel;
@@ -184,7 +282,6 @@
     private static String sSystemUiPackage;
 
     private int mPlugType;
-    private int mLastPlugType = -1; // Extra state so we can detect first run
 
     private boolean mBatteryLevelLow;
 
@@ -197,6 +294,16 @@
     private boolean mUpdatesStopped;
     private boolean mBatteryInputSuspended;
 
+    /**
+     * Time when the voltage was updated last by HAL and we sent the
+     * {@link Intent#ACTION_BATTERY_CHANGED} broadcast.
+     * Note: This value is used to rate limit the {@link Intent#ACTION_BATTERY_CHANGED} broadcast
+     * so it is possible that voltage was updated but we did not send the broadcast so in that
+     * case we do not update the time.
+     */
+    @VisibleForTesting
+    public long mLastBroadcastVoltageUpdateTime;
+
     private Led mLed;
 
     private boolean mSentLowBatteryBroadcast = false;
@@ -211,7 +318,8 @@
     private final CopyOnWriteArraySet<BatteryManagerInternal.ChargingPolicyChangeListener>
             mChargingPolicyChangeListeners = new CopyOnWriteArraySet<>();
 
-    private static final Bundle BATTERY_CHANGED_OPTIONS = BroadcastOptions.makeBasic()
+    @VisibleForTesting
+    public static final Bundle BATTERY_CHANGED_OPTIONS = BroadcastOptions.makeBasic()
             .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT)
             .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE)
             .toBundle();
@@ -234,6 +342,25 @@
     private static final int MSG_BROADCAST_POWER_CONNECTION_CHANGED = 2;
     private static final int MSG_BROADCAST_BATTERY_LOW_OKAY = 3;
 
+    /**
+     * This value is used to rate limit the {@link Intent#ACTION_BATTERY_CHANGED} broadcast. We
+     * only send the broadcast and update the temperature value when the temp change is greater or
+     * equals to 1 degree celsius.
+     */
+    private static final int ABSOLUTE_DECI_CELSIUS_DIFF_FOR_TEMP_UPDATE = 10;
+    /**
+     * This value is used to rate limit the {@link Intent#ACTION_BATTERY_CHANGED} broadcast. We
+     * only send the broadcast if the last voltage was updated at least 20s seconds back and has a
+     * fluctuation of at least 1%.
+     */
+    private static final int TIME_DIFF_FOR_VOLTAGE_UPDATE_MS = 20000;
+    /**
+     * The value is used to rate limit the {@link Intent#ACTION_BATTERY_CHANGED} broadcast. We
+     * only send the broadcast if the last voltage was updated at least 20s seconds back and has a
+     * fluctuation of at least 1%.
+     */
+    private static final float BASE_POINT_DIFF_FOR_VOLTAGE_UPDATE = 0.01f;
+
     private final Handler.Callback mLocalCallback = msg -> {
         switch (msg.what) {
             case MSG_BROADCAST_BATTERY_CHANGED: {
@@ -283,10 +410,19 @@
     };
 
     public BatteryService(Context context) {
+        this(context, Objects.requireNonNull(Looper.myLooper(),
+                "BatteryService uses handler!! Can't create handler inside thread that has not "
+                        + "called Looper.prepare()"));
+    }
+
+    @VisibleForTesting
+    public BatteryService(Context context, @NonNull Looper looper) {
         super(context);
 
+        Objects.requireNonNull(looper);
+
         mContext = context;
-        mHandler = new Handler(mLocalCallback, true /*async*/);
+        mHandler = new Handler(looper, mLocalCallback, true /*async*/);
         mLed = new Led(context, getLocalService(LightsManager.class));
         mBatteryStats = BatteryStatsService.getService();
         mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
@@ -436,7 +572,7 @@
 
     private boolean shouldSendBatteryLowLocked() {
         final boolean plugged = mPlugType != BATTERY_PLUGGED_NONE;
-        final boolean oldPlugged = mLastPlugType != BATTERY_PLUGGED_NONE;
+        final boolean oldPlugged = mLastBroadcastPlugType != BATTERY_PLUGGED_NONE;
 
         /* The ACTION_BATTERY_LOW broadcast is sent in these situations:
          * - is just un-plugged (previously was plugged) and battery level is
@@ -447,7 +583,7 @@
         return !plugged
                 && mHealthInfo.batteryStatus != BatteryManager.BATTERY_STATUS_UNKNOWN
                 && mHealthInfo.batteryLevel <= mLowBatteryWarningLevel
-                && (oldPlugged || mLastBatteryLevel > mLowBatteryWarningLevel
+                && (oldPlugged || mLastBroadcastBatteryLevel > mLowBatteryWarningLevel
                     || mHealthInfo.batteryLevel > mLastLowBatteryWarningLevel);
     }
 
@@ -515,7 +651,13 @@
         }
     }
 
-    private void update(android.hardware.health.HealthInfo info) {
+    /**
+     * Updates the healthInfo and triggers the broadcast.
+     *
+     * @param info the new health info
+     */
+    @VisibleForTesting
+    public void update(android.hardware.health.HealthInfo info) {
         traceBegin("HealthInfoUpdate");
 
         Trace.traceCounter(
@@ -556,8 +698,8 @@
         long dischargeDuration = 0;
 
         mBatteryLevelCritical =
-            mHealthInfo.batteryStatus != BatteryManager.BATTERY_STATUS_UNKNOWN
-            && mHealthInfo.batteryLevel <= mCriticalBatteryLevel;
+                mHealthInfo.batteryStatus != BatteryManager.BATTERY_STATUS_UNKNOWN
+                        && mHealthInfo.batteryLevel <= mCriticalBatteryLevel;
         mPlugType = plugType(mHealthInfo);
 
         if (DEBUG) {
@@ -591,24 +733,28 @@
             mHandler.post(this::notifyChargingPolicyChanged);
         }
 
-        if (force
-                || (mHealthInfo.batteryStatus != mLastBatteryStatus
-                        || mHealthInfo.batteryHealth != mLastBatteryHealth
-                        || mHealthInfo.batteryPresent != mLastBatteryPresent
-                        || mHealthInfo.batteryLevel != mLastBatteryLevel
-                        || mPlugType != mLastPlugType
-                        || mHealthInfo.batteryVoltageMillivolts != mLastBatteryVoltage
-                        || mHealthInfo.batteryTemperatureTenthsCelsius != mLastBatteryTemperature
-                        || mHealthInfo.maxChargingCurrentMicroamps != mLastMaxChargingCurrent
-                        || mHealthInfo.maxChargingVoltageMicrovolts != mLastMaxChargingVoltage
-                        || mHealthInfo.batteryChargeCounterUah != mLastChargeCounter
-                        || mInvalidCharger != mLastInvalidCharger
-                        || mHealthInfo.batteryCycleCount != mLastBatteryCycleCount
-                        || mHealthInfo.chargingState != mLastChargingState
-                        || mHealthInfo.batteryCapacityLevel != mLastBatteryCapacityLevel)) {
+        final boolean includeChargeCounter =
+                !com.android.server.flags.Flags.rateLimitBatteryChangedBroadcast()
+                        && mHealthInfo.batteryChargeCounterUah != mLastBroadcastChargeCounter;
 
-            if (mPlugType != mLastPlugType) {
-                if (mLastPlugType == BATTERY_PLUGGED_NONE) {
+        if (force
+                || (mHealthInfo.batteryStatus != mLastBroadcastBatteryStatus
+                || mHealthInfo.batteryHealth != mLastBroadcastBatteryHealth
+                || mHealthInfo.batteryPresent != mLastBroadcastBatteryPresent
+                || mHealthInfo.batteryLevel != mLastBroadcastBatteryLevel
+                || mPlugType != mLastBroadcastPlugType
+                || mHealthInfo.batteryVoltageMillivolts != mLastBroadcastBatteryVoltage
+                || mHealthInfo.batteryTemperatureTenthsCelsius != mLastBroadcastBatteryTemperature
+                || mHealthInfo.maxChargingCurrentMicroamps != mLastBroadcastMaxChargingCurrent
+                || mHealthInfo.maxChargingVoltageMicrovolts != mLastBroadcastMaxChargingVoltage
+                || includeChargeCounter
+                || mInvalidCharger != mLastBroadcastInvalidCharger
+                || mHealthInfo.batteryCycleCount != mLastBroadcastBatteryCycleCount
+                || mHealthInfo.chargingState != mLastBroadcastChargingState
+                || mHealthInfo.batteryCapacityLevel != mLastBroadcastBatteryCapacityLevel)) {
+
+            if (mPlugType != mLastBroadcastPlugType) {
+                if (mLastBroadcastPlugType == BATTERY_PLUGGED_NONE) {
                     // discharging -> charging
                     mChargeStartLevel = mHealthInfo.batteryLevel;
                     mChargeStartTime = SystemClock.elapsedRealtime();
@@ -622,7 +768,8 @@
 
                     // There's no value in this data unless we've discharged at least once and the
                     // battery level has changed; so don't log until it does.
-                    if (mDischargeStartTime != 0 && mDischargeStartLevel != mHealthInfo.batteryLevel) {
+                    if (mDischargeStartTime != 0
+                            && mDischargeStartLevel != mHealthInfo.batteryLevel) {
                         dischargeDuration = SystemClock.elapsedRealtime() - mDischargeStartTime;
                         logOutlier = true;
                         EventLog.writeEvent(EventLogTags.BATTERY_DISCHARGE, dischargeDuration,
@@ -639,7 +786,7 @@
                     if (mChargeStartTime != 0 && chargeDuration != 0) {
                         final LogMaker builder = new LogMaker(MetricsEvent.ACTION_CHARGE);
                         builder.setType(MetricsEvent.TYPE_DISMISS);
-                        builder.addTaggedData(MetricsEvent.FIELD_PLUG_TYPE, mLastPlugType);
+                        builder.addTaggedData(MetricsEvent.FIELD_PLUG_TYPE, mLastBroadcastPlugType);
                         builder.addTaggedData(MetricsEvent.FIELD_CHARGING_DURATION_MILLIS,
                                 chargeDuration);
                         builder.addTaggedData(MetricsEvent.FIELD_BATTERY_LEVEL_START,
@@ -651,19 +798,20 @@
                     mChargeStartTime = 0;
                 }
             }
-            if (mHealthInfo.batteryStatus != mLastBatteryStatus ||
-                    mHealthInfo.batteryHealth != mLastBatteryHealth ||
-                    mHealthInfo.batteryPresent != mLastBatteryPresent ||
-                    mPlugType != mLastPlugType) {
+            if (mHealthInfo.batteryStatus != mLastBroadcastBatteryStatus
+                    || mHealthInfo.batteryHealth != mLastBroadcastBatteryHealth
+                    || mHealthInfo.batteryPresent != mLastBroadcastBatteryPresent
+                    || mPlugType != mLastBroadcastPlugType) {
                 EventLog.writeEvent(EventLogTags.BATTERY_STATUS,
-                        mHealthInfo.batteryStatus, mHealthInfo.batteryHealth, mHealthInfo.batteryPresent ? 1 : 0,
+                        mHealthInfo.batteryStatus, mHealthInfo.batteryHealth,
+                        mHealthInfo.batteryPresent ? 1 : 0,
                         mPlugType, mHealthInfo.batteryTechnology);
                 SystemProperties.set(
                         "debug.tracing.battery_status",
                         Integer.toString(mHealthInfo.batteryStatus));
                 SystemProperties.set("debug.tracing.plug_type", Integer.toString(mPlugType));
             }
-            if (mHealthInfo.batteryLevel != mLastBatteryLevel) {
+            if (mHealthInfo.batteryLevel != mLastBroadcastBatteryLevel) {
                 // Don't do this just from voltage or temperature changes, that is
                 // too noisy.
                 EventLog.writeEvent(
@@ -672,8 +820,8 @@
                         mHealthInfo.batteryVoltageMillivolts,
                         mHealthInfo.batteryTemperatureTenthsCelsius);
             }
-            if (mBatteryLevelCritical && !mLastBatteryLevelCritical &&
-                    mPlugType == BATTERY_PLUGGED_NONE) {
+            if (mBatteryLevelCritical && !mLastBroadcastBatteryLevelCritical
+                    && mPlugType == BATTERY_PLUGGED_NONE) {
                 // We want to make sure we log discharge cycle outliers
                 // if the battery is about to die.
                 dischargeDuration = SystemClock.elapsedRealtime() - mDischargeStartTime;
@@ -684,7 +832,7 @@
                 // Should we now switch in to low battery mode?
                 if (mPlugType == BATTERY_PLUGGED_NONE
                         && mHealthInfo.batteryStatus !=
-                           BatteryManager.BATTERY_STATUS_UNKNOWN
+                        BatteryManager.BATTERY_STATUS_UNKNOWN
                         && mHealthInfo.batteryLevel <= mLowBatteryWarningLevel) {
                     mBatteryLevelLow = true;
                 }
@@ -692,7 +840,7 @@
                 // Should we now switch out of low battery mode?
                 if (mPlugType != BATTERY_PLUGGED_NONE) {
                     mBatteryLevelLow = false;
-                } else if (mHealthInfo.batteryLevel >= mLowBatteryCloseWarningLevel)  {
+                } else if (mHealthInfo.batteryLevel >= mLowBatteryCloseWarningLevel) {
                     mBatteryLevelLow = false;
                 } else if (force && mHealthInfo.batteryLevel >= mLowBatteryWarningLevel) {
                     // If being forced, the previous state doesn't matter, we will just
@@ -706,7 +854,7 @@
             // Separate broadcast is sent for power connected / not connected
             // since the standard intent will not wake any applications and some
             // applications may want to have smart behavior based on this.
-            if (mPlugType != 0 && mLastPlugType == 0) {
+            if (mPlugType != 0 && mLastBroadcastPlugType == 0) {
                 final Intent statusIntent = new Intent(Intent.ACTION_POWER_CONNECTED);
                 statusIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
                 statusIntent.putExtra(BatteryManager.EXTRA_SEQUENCE, mSequence);
@@ -726,8 +874,7 @@
                         }
                     });
                 }
-            }
-            else if (mPlugType == 0 && mLastPlugType != 0) {
+            } else if (mPlugType == 0 && mLastBroadcastPlugType != 0) {
                 final Intent statusIntent = new Intent(Intent.ACTION_POWER_DISCONNECTED);
                 statusIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
                 statusIntent.putExtra(BatteryManager.EXTRA_SEQUENCE, mSequence);
@@ -797,8 +944,14 @@
             // We are doing this after sending the above broadcasts, so anything processing
             // them will get the new sequence number at that point.  (See for example how testing
             // of JobScheduler's BatteryController works.)
-            sendBatteryChangedIntentLocked(force);
-            if (mLastBatteryLevel != mHealthInfo.batteryLevel || mLastPlugType != mPlugType) {
+
+            boolean rateLimitBatteryChangedBroadcast = rateLimitBatteryChangedBroadcast(force);
+
+            if (!rateLimitBatteryChangedBroadcast) {
+                sendBatteryChangedIntentLocked(force);
+            }
+            if (mLastBroadcastBatteryLevel != mHealthInfo.batteryLevel
+                    || mLastBroadcastPlugType != mPlugType) {
                 sendBatteryLevelChangedIntentLocked();
             }
 
@@ -811,21 +964,24 @@
                 logOutlierLocked(dischargeDuration);
             }
 
-            mLastBatteryStatus = mHealthInfo.batteryStatus;
-            mLastBatteryHealth = mHealthInfo.batteryHealth;
-            mLastBatteryPresent = mHealthInfo.batteryPresent;
-            mLastBatteryLevel = mHealthInfo.batteryLevel;
-            mLastPlugType = mPlugType;
-            mLastBatteryVoltage = mHealthInfo.batteryVoltageMillivolts;
-            mLastBatteryTemperature = mHealthInfo.batteryTemperatureTenthsCelsius;
-            mLastMaxChargingCurrent = mHealthInfo.maxChargingCurrentMicroamps;
-            mLastMaxChargingVoltage = mHealthInfo.maxChargingVoltageMicrovolts;
-            mLastChargeCounter = mHealthInfo.batteryChargeCounterUah;
-            mLastBatteryLevelCritical = mBatteryLevelCritical;
-            mLastInvalidCharger = mInvalidCharger;
-            mLastBatteryCycleCount = mHealthInfo.batteryCycleCount;
-            mLastChargingState = mHealthInfo.chargingState;
-            mLastBatteryCapacityLevel = mHealthInfo.batteryCapacityLevel;
+            // Only update the values when we send the broadcast
+            if (!rateLimitBatteryChangedBroadcast) {
+                mLastBroadcastBatteryStatus = mHealthInfo.batteryStatus;
+                mLastBroadcastBatteryHealth = mHealthInfo.batteryHealth;
+                mLastBroadcastBatteryPresent = mHealthInfo.batteryPresent;
+                mLastBroadcastBatteryLevel = mHealthInfo.batteryLevel;
+                mLastBroadcastPlugType = mPlugType;
+                mLastBroadcastBatteryVoltage = mHealthInfo.batteryVoltageMillivolts;
+                mLastBroadcastBatteryTemperature = mHealthInfo.batteryTemperatureTenthsCelsius;
+                mLastBroadcastMaxChargingCurrent = mHealthInfo.maxChargingCurrentMicroamps;
+                mLastBroadcastMaxChargingVoltage = mHealthInfo.maxChargingVoltageMicrovolts;
+                mLastBroadcastChargeCounter = mHealthInfo.batteryChargeCounterUah;
+                mLastBroadcastBatteryLevelCritical = mBatteryLevelCritical;
+                mLastBroadcastInvalidCharger = mInvalidCharger;
+                mLastBroadcastBatteryCycleCount = mHealthInfo.batteryCycleCount;
+                mLastBroadcastChargingState = mHealthInfo.chargingState;
+                mLastBroadcastBatteryCapacityLevel = mHealthInfo.batteryCapacityLevel;
+            }
         }
     }
 
@@ -1089,6 +1245,74 @@
         }
     }
 
+    /**
+     * Rate limit's the broadcast based on the changes in temp, voltage and chargeCounter.
+     */
+    private boolean rateLimitBatteryChangedBroadcast(boolean forceUpdate) {
+        if (!com.android.server.flags.Flags.rateLimitBatteryChangedBroadcast()) {
+            return false;
+        }
+        if (mLastBroadcastBatteryVoltage == 0 || mLastBroadcastBatteryTemperature == 0) {
+            mLastBroadcastVoltageUpdateTime = SystemClock.elapsedRealtime();
+            return false;
+        }
+
+        final boolean voltageUpdated =
+                mLastBroadcastBatteryVoltage != mHealthInfo.batteryVoltageMillivolts;
+        final boolean temperatureUpdated =
+                mLastBroadcastBatteryTemperature != mHealthInfo.batteryTemperatureTenthsCelsius;
+        final boolean otherStatesUpdated = forceUpdate
+                || mHealthInfo.batteryStatus != mLastBroadcastBatteryStatus
+                || mHealthInfo.batteryHealth != mLastBroadcastBatteryHealth
+                || mHealthInfo.batteryPresent != mLastBroadcastBatteryPresent
+                || mHealthInfo.batteryLevel != mLastBroadcastBatteryLevel
+                || mPlugType != mLastBroadcastPlugType
+                || mHealthInfo.maxChargingCurrentMicroamps != mLastBroadcastMaxChargingCurrent
+                || mHealthInfo.maxChargingVoltageMicrovolts != mLastBroadcastMaxChargingVoltage
+                || mInvalidCharger != mLastBroadcastInvalidCharger
+                || mHealthInfo.batteryCycleCount != mLastBroadcastBatteryCycleCount
+                || mHealthInfo.chargingState != mLastBroadcastChargingState
+                || mHealthInfo.batteryCapacityLevel != mLastBroadcastBatteryCapacityLevel;
+
+        // We only rate limit based on changes in the temp, voltage.
+        if (otherStatesUpdated) {
+
+            if (voltageUpdated) {
+                mLastBroadcastVoltageUpdateTime = SystemClock.elapsedRealtime();
+            }
+            return false;
+        }
+
+        final float basePointDiff =
+                (float) (mLastBroadcastBatteryVoltage - mHealthInfo.batteryVoltageMillivolts)
+                        / mLastBroadcastBatteryVoltage;
+
+        // We only send the broadcast if voltage change is greater than 1% and last voltage
+        // update was sent at least 20 seconds back.
+        if (voltageUpdated
+                && abs(basePointDiff) >= BASE_POINT_DIFF_FOR_VOLTAGE_UPDATE
+                && SystemClock.elapsedRealtime() - mLastBroadcastVoltageUpdateTime
+                        >= TIME_DIFF_FOR_VOLTAGE_UPDATE_MS) {
+            mLastBroadcastVoltageUpdateTime = SystemClock.elapsedRealtime();
+
+            return false;
+        }
+
+        // Only send the broadcast if the temperature update is greater than 1 degree celsius.
+        if (temperatureUpdated
+                && abs(
+                mLastBroadcastBatteryTemperature - mHealthInfo.batteryTemperatureTenthsCelsius)
+                        >= ABSOLUTE_DECI_CELSIUS_DIFF_FOR_TEMP_UPDATE) {
+
+            if (voltageUpdated) {
+                mLastBroadcastVoltageUpdateTime = SystemClock.elapsedRealtime();
+            }
+            return false;
+        }
+
+        return true;
+    }
+
     class Shell extends ShellCommand {
         @Override
         public int onCommand(String cmd) {
@@ -1399,6 +1623,10 @@
                 pw.println("  level: " + mHealthInfo.batteryLevel);
                 pw.println("  scale: " + BATTERY_SCALE);
                 pw.println("  voltage: " + mHealthInfo.batteryVoltageMillivolts);
+                pw.println(" Time when the latest updated value of the voltage was sent via "
+                        + "battery changed broadcast: " + mLastBroadcastVoltageUpdateTime);
+                pw.println(" The last voltage value sent via the battery changed broadcast: "
+                        + mLastBroadcastBatteryVoltage);
                 pw.println("  temperature: " + mHealthInfo.batteryTemperatureTenthsCelsius);
                 pw.println("  technology: " + mHealthInfo.batteryTechnology);
                 pw.println("  Charging state: " + mHealthInfo.chargingState);
@@ -1457,6 +1685,11 @@
         Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
     }
 
+    @VisibleForTesting
+    public Handler getHandlerForTest() {
+        return mHandler;
+    }
+
     @SuppressLint("AndroidFrameworkRequiresPermission")
     private static void sendBroadcastToAllUsers(Context context, Intent intent,
             Bundle options) {
diff --git a/services/core/java/com/android/server/flags/services.aconfig b/services/core/java/com/android/server/flags/services.aconfig
index 69ba785..eea5c98 100644
--- a/services/core/java/com/android/server/flags/services.aconfig
+++ b/services/core/java/com/android/server/flags/services.aconfig
@@ -67,3 +67,14 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    namespace: "backstage_power"
+    name: "rate_limit_battery_changed_broadcast"
+    description: "Optimize the delivery of the battery changed broadcast by rate limiting the frequency of the updates"
+    bug: "362337621"
+    is_fixed_read_only: true
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/BatteryServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/BatteryServiceTest.java
new file mode 100644
index 0000000..5e2f80b
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/BatteryServiceTest.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright (C) 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 com.android.server;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.hardware.health.HealthInfo;
+import android.os.HandlerThread;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.internal.R;
+import com.android.internal.app.IBatteryStats;
+import com.android.modules.utils.testing.ExtendedMockitoRule;
+import com.android.server.am.BatteryStatsService;
+import com.android.server.flags.Flags;
+import com.android.server.lights.LightsManager;
+import com.android.server.lights.LogicalLight;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class BatteryServiceTest {
+
+    private static final int CURRENT_BATTERY_VOLTAGE = 3000;
+    private static final int VOLTAGE_LESS_THEN_ONE_PERCENT = 3029;
+    private static final int VOLTAGE_MORE_THEN_ONE_PERCENT = 3030;
+    private static final int CURRENT_BATTERY_TEMP = 300;
+    private static final int TEMP_LESS_THEN_ONE_DEGREE_CELSIUS = 305;
+    private static final int TEMP_MORE_THEN_ONE_DEGREE_CELSIUS = 310;
+    private static final int CURRENT_BATTERY_HEALTH = 2;
+    private static final int UPDATED_BATTERY_HEALTH = 3;
+    private static final int CURRENT_CHARGE_COUNTER = 4680000;
+    private static final int UPDATED_CHARGE_COUNTER = 4218000;
+    private static final int HANDLER_IDLE_TIME_MS = 5000;
+    @Rule
+    public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
+            .mockStatic(SystemProperties.class)
+            .mockStatic(ActivityManager.class)
+            .mockStatic(BatteryStatsService.class)
+            .build();
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+    @Mock
+    private Context mContextMock;
+    @Mock
+    private LightsManager mLightsManagerMock;
+    @Mock
+    private ActivityManagerInternal mActivityManagerInternalMock;
+    @Mock
+    private IBatteryStats mIBatteryStatsMock;
+
+    private BatteryService mBatteryService;
+    private String mSystemUiPackage;
+
+    /**
+     * Creates a mock and registers it to {@link LocalServices}.
+     */
+    private static <T> void addLocalServiceMock(Class<T> clazz, T mock) {
+        LocalServices.removeServiceForTest(clazz);
+        LocalServices.addService(clazz, mock);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mSystemUiPackage = InstrumentationRegistry.getInstrumentation().getTargetContext()
+                .getResources().getString(R.string.config_systemUi);
+
+        when(mLightsManagerMock.getLight(anyInt())).thenReturn(mock(LogicalLight.class));
+        when(mActivityManagerInternalMock.isSystemReady()).thenReturn(true);
+        when(mContextMock.getResources()).thenReturn(
+                InstrumentationRegistry.getInstrumentation().getTargetContext().getResources());
+        ExtendedMockito.when(BatteryStatsService.getService()).thenReturn(mIBatteryStatsMock);
+
+        doNothing().when(mIBatteryStatsMock).setBatteryState(anyInt(), anyInt(), anyInt(), anyInt(),
+                anyInt(), anyInt(), anyInt(), anyInt(), anyLong());
+        doNothing().when(() -> SystemProperties.set(anyString(), anyString()));
+        doNothing().when(() -> ActivityManager.broadcastStickyIntent(any(),
+                eq(new String[]{mSystemUiPackage}), eq(AppOpsManager.OP_NONE),
+                eq(BatteryService.BATTERY_CHANGED_OPTIONS), eq(UserHandle.USER_ALL)));
+
+        addLocalServiceMock(LightsManager.class, mLightsManagerMock);
+        addLocalServiceMock(ActivityManagerInternal.class, mActivityManagerInternalMock);
+
+        createBatteryService();
+    }
+
+    @Test
+    public void createBatteryService_withNullLooper_throwsNullPointerException() {
+        assertThrows(NullPointerException.class, () -> new BatteryService(mContextMock));
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_RATE_LIMIT_BATTERY_CHANGED_BROADCAST)
+    public void onlyVoltageUpdated_lessThenOnePercent_broadcastNotSent() {
+        mBatteryService.update(createHealthInfo(VOLTAGE_LESS_THEN_ONE_PERCENT, CURRENT_BATTERY_TEMP,
+                CURRENT_CHARGE_COUNTER, CURRENT_BATTERY_HEALTH));
+
+        waitForHandlerToExecute();
+
+        verifyNumberOfTimesBroadcastSent(0);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_RATE_LIMIT_BATTERY_CHANGED_BROADCAST)
+    public void onlyVoltageUpdated_beforeTwentySeconds_broadcastNotSent() {
+        mBatteryService.update(
+                createHealthInfo(VOLTAGE_MORE_THEN_ONE_PERCENT, CURRENT_BATTERY_TEMP,
+                        CURRENT_CHARGE_COUNTER,
+                        CURRENT_BATTERY_HEALTH));
+
+        waitForHandlerToExecute();
+
+        verifyNumberOfTimesBroadcastSent(0);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_RATE_LIMIT_BATTERY_CHANGED_BROADCAST)
+    public void onlyVoltageUpdated_broadcastSent() {
+        mBatteryService.mLastBroadcastVoltageUpdateTime = SystemClock.elapsedRealtime() - 20000;
+        mBatteryService.update(createHealthInfo(VOLTAGE_MORE_THEN_ONE_PERCENT, CURRENT_BATTERY_TEMP,
+                CURRENT_CHARGE_COUNTER, CURRENT_BATTERY_HEALTH));
+
+        waitForHandlerToExecute();
+
+        verifyNumberOfTimesBroadcastSent(1);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_RATE_LIMIT_BATTERY_CHANGED_BROADCAST)
+    public void onlyTempUpdated_lessThenOneDegreeCelsius_broadcastNotSent() {
+        mBatteryService.update(
+                createHealthInfo(CURRENT_BATTERY_VOLTAGE, TEMP_LESS_THEN_ONE_DEGREE_CELSIUS,
+                        CURRENT_CHARGE_COUNTER, CURRENT_BATTERY_HEALTH));
+
+        waitForHandlerToExecute();
+
+        verifyNumberOfTimesBroadcastSent(0);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_RATE_LIMIT_BATTERY_CHANGED_BROADCAST)
+    public void tempUpdated_broadcastSent() {
+        long lastVoltageUpdateTime = mBatteryService.mLastBroadcastVoltageUpdateTime;
+        mBatteryService.update(
+                createHealthInfo(VOLTAGE_LESS_THEN_ONE_PERCENT, TEMP_MORE_THEN_ONE_DEGREE_CELSIUS,
+                        UPDATED_CHARGE_COUNTER, CURRENT_BATTERY_HEALTH));
+
+        waitForHandlerToExecute();
+
+        assertTrue(lastVoltageUpdateTime < mBatteryService.mLastBroadcastVoltageUpdateTime);
+        verifyNumberOfTimesBroadcastSent(1);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_RATE_LIMIT_BATTERY_CHANGED_BROADCAST)
+    public void batteryHealthUpdated_voltageAndTempConst_broadcastSent() {
+        mBatteryService.update(
+                createHealthInfo(CURRENT_BATTERY_VOLTAGE, CURRENT_BATTERY_TEMP,
+                        CURRENT_CHARGE_COUNTER,
+                        UPDATED_BATTERY_HEALTH));
+
+        waitForHandlerToExecute();
+
+        verifyNumberOfTimesBroadcastSent(1);
+
+        // updating counter just after the health update does not triggers broadcast.
+        mBatteryService.update(
+                createHealthInfo(CURRENT_BATTERY_VOLTAGE, CURRENT_BATTERY_TEMP,
+                        UPDATED_CHARGE_COUNTER,
+                        UPDATED_BATTERY_HEALTH));
+
+        waitForHandlerToExecute();
+
+        verifyNumberOfTimesBroadcastSent(1);
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_RATE_LIMIT_BATTERY_CHANGED_BROADCAST)
+    public void voltageUpdated_lessThanOnePercent_flagDisabled_broadcastSent() {
+        mBatteryService.update(createHealthInfo(VOLTAGE_LESS_THEN_ONE_PERCENT, CURRENT_BATTERY_TEMP,
+                CURRENT_CHARGE_COUNTER, CURRENT_BATTERY_HEALTH));
+
+        waitForHandlerToExecute();
+
+        verifyNumberOfTimesBroadcastSent(1);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_RATE_LIMIT_BATTERY_CHANGED_BROADCAST)
+    public void onlyChargeCounterUpdated_broadcastNotSent() {
+        mBatteryService.update(
+                createHealthInfo(CURRENT_BATTERY_VOLTAGE, CURRENT_BATTERY_TEMP,
+                        UPDATED_CHARGE_COUNTER,
+                        CURRENT_BATTERY_HEALTH));
+
+        waitForHandlerToExecute();
+
+        verifyNumberOfTimesBroadcastSent(0);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_RATE_LIMIT_BATTERY_CHANGED_BROADCAST)
+    public void chargeCounterUpdated_tempUpdatedLessThanOneDegree_broadcastNotSent() {
+        mBatteryService.update(
+                createHealthInfo(CURRENT_BATTERY_VOLTAGE, TEMP_LESS_THEN_ONE_DEGREE_CELSIUS,
+                        UPDATED_CHARGE_COUNTER,
+                        CURRENT_BATTERY_HEALTH));
+
+        waitForHandlerToExecute();
+
+        verifyNumberOfTimesBroadcastSent(0);
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_RATE_LIMIT_BATTERY_CHANGED_BROADCAST)
+    public void onlyChargeCounterUpdated_broadcastSent() {
+        mBatteryService.update(
+                createHealthInfo(CURRENT_BATTERY_VOLTAGE, CURRENT_BATTERY_TEMP,
+                        UPDATED_CHARGE_COUNTER,
+                        CURRENT_BATTERY_HEALTH));
+
+        waitForHandlerToExecute();
+
+        verifyNumberOfTimesBroadcastSent(1);
+    }
+
+    private HealthInfo createHealthInfo(
+            int batteryVoltage,
+            int batteryTemperature,
+            int batteryChargeCounter,
+            int batteryHealth) {
+        HealthInfo h = new HealthInfo();
+        h.batteryVoltageMillivolts = batteryVoltage;
+        h.batteryTemperatureTenthsCelsius = batteryTemperature;
+        h.batteryChargeCounterUah = batteryChargeCounter;
+        h.batteryStatus = 5;
+        h.batteryHealth = batteryHealth;
+        h.batteryPresent = true;
+        h.batteryLevel = 100;
+        h.maxChargingCurrentMicroamps = 298125;
+        h.batteryCurrentAverageMicroamps = -2812;
+        h.batteryCurrentMicroamps = 298125;
+        h.maxChargingVoltageMicrovolts = 3000;
+        h.batteryCycleCount = 50;
+        h.chargingState = 4;
+        h.batteryCapacityLevel = 100;
+        return h;
+    }
+
+    // Creates a new battery service objects and sets the initial values.
+    private void createBatteryService() throws InterruptedException {
+        final HandlerThread handlerThread = new HandlerThread("BatteryServiceTest");
+        handlerThread.start();
+
+        mBatteryService = new BatteryService(mContextMock, handlerThread.getLooper());
+
+        // trigger the update to set the initial values.
+        mBatteryService.update(
+                createHealthInfo(CURRENT_BATTERY_VOLTAGE, CURRENT_BATTERY_TEMP,
+                        CURRENT_CHARGE_COUNTER,
+                        CURRENT_BATTERY_HEALTH));
+
+        waitForHandlerToExecute();
+    }
+
+    private void waitForHandlerToExecute() {
+        final CountDownLatch latch = new CountDownLatch(1);
+        mBatteryService.getHandlerForTest().post(latch::countDown);
+        boolean isExecutionComplete = false;
+
+        try {
+            isExecutionComplete = latch.await(HANDLER_IDLE_TIME_MS, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            fail("Handler interrupted before executing the message " + e);
+        }
+
+        assertTrue("Timed out while waiting for Handler to execute.", isExecutionComplete);
+    }
+
+    private void verifyNumberOfTimesBroadcastSent(int numberOfTimes) {
+        // Increase the numberOfTimes by 1 as one broadcast was sent initially during the test
+        // setUp.
+        verify(() -> ActivityManager.broadcastStickyIntent(any(),
+                        eq(new String[]{mSystemUiPackage}), eq(AppOpsManager.OP_NONE),
+                        eq(BatteryService.BATTERY_CHANGED_OPTIONS), eq(UserHandle.USER_ALL)),
+                times(++numberOfTimes));
+    }
+}