Added ACTION_BATTERY_LEVEL_CHANGED

Sent when the current battery level changes.

It has EXTRA_EVENTS that carries a list of Bundle instances representing
individual battery level changes with associated extras from
ACTION_BATTERY_CHANGED

Each event has EXTRA_EVENT_TIMESTAMP representing time when it occured.

Test: manual
Bug: 74020080
Change-Id: I993005950299c5298c9111ca51cc7717e1f029de
diff --git a/api/system-current.txt b/api/system-current.txt
index 6eef3a2..1f8776e 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -839,6 +839,7 @@
   }
 
   public class Intent implements java.lang.Cloneable android.os.Parcelable {
+    field public static final java.lang.String ACTION_BATTERY_LEVEL_CHANGED = "android.intent.action.BATTERY_LEVEL_CHANGED";
     field public static final java.lang.String ACTION_CALL_EMERGENCY = "android.intent.action.CALL_EMERGENCY";
     field public static final java.lang.String ACTION_CALL_PRIVILEGED = "android.intent.action.CALL_PRIVILEGED";
     field public static final java.lang.String ACTION_FACTORY_RESET = "android.intent.action.FACTORY_RESET";
@@ -3610,6 +3611,11 @@
 
 package android.os {
 
+  public class BatteryManager {
+    field public static final java.lang.String EXTRA_EVENTS = "android.os.extra.EVENTS";
+    field public static final java.lang.String EXTRA_EVENT_TIMESTAMP = "android.os.extra.EVENT_TIMESTAMP";
+  }
+
   public final class ConfigUpdate {
     field public static final java.lang.String ACTION_UPDATE_CARRIER_PROVISIONING_URLS = "android.intent.action.UPDATE_CARRIER_PROVISIONING_URLS";
     field public static final java.lang.String ACTION_UPDATE_CT_LOGS = "android.intent.action.UPDATE_CT_LOGS";
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index e7aead1..ce32278 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -2448,6 +2448,23 @@
      */
     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
     public static final String ACTION_BATTERY_CHANGED = "android.intent.action.BATTERY_CHANGED";
+
+
+    /**
+     * Broadcast Action: Sent when the current battery level changes.
+     *
+     * It has {@link android.os.BatteryManager#EXTRA_EVENTS} that carries a list of {@link Bundle}
+     * instances representing individual battery level changes with associated
+     * extras from {@link #ACTION_BATTERY_CHANGED}.
+     *
+     * <p class="note">
+     * This broadcast requires {@link android.Manifest.permission#BATTERY_STATS} permission.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final String ACTION_BATTERY_LEVEL_CHANGED =
+            "android.intent.action.BATTERY_LEVEL_CHANGED";
     /**
      * Broadcast Action:  Indicates low battery condition on the device.
      * This broadcast corresponds to the "Low battery warning" system dialog.
diff --git a/core/java/android/os/BatteryManager.java b/core/java/android/os/BatteryManager.java
index 8b4f02e..6363161 100644
--- a/core/java/android/os/BatteryManager.java
+++ b/core/java/android/os/BatteryManager.java
@@ -16,6 +16,7 @@
 
 package android.os;
 
+import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.content.Context;
 import android.content.Intent;
@@ -138,6 +139,23 @@
      */
     public static final String EXTRA_SEQUENCE = "seq";
 
+    /**
+     * Extra for {@link android.content.Intent#ACTION_BATTERY_LEVEL_CHANGED}:
+     * Contains list of Bundles representing battery events
+     * @hide
+     */
+    @SystemApi
+    public static final String EXTRA_EVENTS = "android.os.extra.EVENTS";
+
+    /**
+     * Extra for event in {@link android.content.Intent#ACTION_BATTERY_LEVEL_CHANGED}:
+     * Long value representing time when event occurred as returned by
+     * {@link android.os.SystemClock#elapsedRealtime()}
+     * @hide
+     */
+    @SystemApi
+    public static final String EXTRA_EVENT_TIMESTAMP = "android.os.extra.EVENT_TIMESTAMP";
+
     // values for "status" field in the ACTION_BATTERY_CHANGED Intent
     public static final int BATTERY_STATUS_UNKNOWN = Constants.BATTERY_STATUS_UNKNOWN;
     public static final int BATTERY_STATUS_CHARGING = Constants.BATTERY_STATUS_CHARGING;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 3324fc3..38d96ba 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -56,6 +56,7 @@
     <protected-broadcast android:name="android.intent.action.SPLIT_CONFIGURATION_CHANGED" />
     <protected-broadcast android:name="android.intent.action.LOCALE_CHANGED" />
     <protected-broadcast android:name="android.intent.action.BATTERY_CHANGED" />
+    <protected-broadcast android:name="android.intent.action.BATTERY_LEVEL_CHANGED" />
     <protected-broadcast android:name="android.intent.action.BATTERY_LOW" />
     <protected-broadcast android:name="android.intent.action.BATTERY_OKAY" />
     <protected-broadcast android:name="android.intent.action.ACTION_POWER_CONNECTED" />
diff --git a/services/core/java/com/android/server/BatteryService.java b/services/core/java/com/android/server/BatteryService.java
index 8265262..d31a605 100644
--- a/services/core/java/com/android/server/BatteryService.java
+++ b/services/core/java/com/android/server/BatteryService.java
@@ -20,6 +20,7 @@
 import android.database.ContentObserver;
 import android.os.BatteryStats;
 
+import android.os.Bundle;
 import android.os.PowerManager;
 import android.os.ResultReceiver;
 import android.os.ShellCallback;
@@ -35,7 +36,6 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.PackageManager;
 import android.hidl.manager.V1_0.IServiceManager;
 import android.hidl.manager.V1_0.IServiceNotification;
 import android.hardware.health.V1_0.HealthInfo;
@@ -73,6 +73,8 @@
 import java.io.IOException;
 import java.io.PrintWriter;
 
+import java.util.ArrayDeque;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.NoSuchElementException;
@@ -117,6 +119,8 @@
     private static final int BATTERY_SCALE = 100;    // battery capacity is a percentage
 
     private static final long HEALTH_HAL_WAIT_MS = 1000;
+    private static final long BATTERY_LEVEL_CHANGE_THROTTLE_MS = 60_000;
+    private static final int MAX_BATTERY_LEVELS_QUEUE_SIZE = 100;
 
     // Used locally for determining when to make a last ditch effort to log
     // discharge stats before the device dies.
@@ -178,7 +182,8 @@
     private HealthServiceWrapper mHealthServiceWrapper;
     private HealthHalCallback mHealthHalCallback;
     private BatteryPropertiesRegistrar mBatteryPropertiesRegistrar;
-    private HandlerThread mHandlerThread;
+    private ArrayDeque<Bundle> mBatteryLevelsEventQueue;
+    private long mLastBatteryLevelChangedSentMs;
 
     public BatteryService(Context context) {
         super(context);
@@ -198,6 +203,8 @@
         mShutdownBatteryTemperature = mContext.getResources().getInteger(
                 com.android.internal.R.integer.config_shutdownBatteryTemperature);
 
+        mBatteryLevelsEventQueue = new ArrayDeque<>();
+
         // watch for invalid charger messages if the invalid_charger switch exists
         if (new File("/sys/devices/virtual/switch/invalid_charger/state").exists()) {
             UEventObserver invalidChargerObserver = new UEventObserver() {
@@ -585,7 +592,11 @@
             // 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.)
-            sendIntentLocked();
+            sendBatteryChangedIntentLocked();
+            if (mLastBatteryLevel != mHealthInfo.batteryLevel) {
+                sendBatteryLevelChangedIntentLocked();
+            }
+
 
             // Update the battery LED
             mLed.updateLightsLocked();
@@ -610,7 +621,7 @@
         }
     }
 
-    private void sendIntentLocked() {
+    private void sendBatteryChangedIntentLocked() {
         //  Pack up the values and broadcast them to everyone
         final Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED);
         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY
@@ -639,12 +650,51 @@
                     + ", info:" + mHealthInfo.toString());
         }
 
-        mHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                ActivityManager.broadcastStickyIntent(intent, UserHandle.USER_ALL);
-            }
-        });
+        mHandler.post(() -> ActivityManager.broadcastStickyIntent(intent, UserHandle.USER_ALL));
+    }
+
+    private void sendBatteryLevelChangedIntentLocked() {
+        Bundle event = new Bundle();
+        long now = SystemClock.elapsedRealtime();
+        event.putInt(BatteryManager.EXTRA_SEQUENCE, mSequence);
+        event.putInt(BatteryManager.EXTRA_STATUS, mHealthInfo.batteryStatus);
+        event.putInt(BatteryManager.EXTRA_HEALTH, mHealthInfo.batteryHealth);
+        event.putBoolean(BatteryManager.EXTRA_PRESENT, mHealthInfo.batteryPresent);
+        event.putInt(BatteryManager.EXTRA_LEVEL, mHealthInfo.batteryLevel);
+        event.putBoolean(BatteryManager.EXTRA_BATTERY_LOW, mSentLowBatteryBroadcast);
+        event.putInt(BatteryManager.EXTRA_SCALE, BATTERY_SCALE);
+        event.putInt(BatteryManager.EXTRA_PLUGGED, mPlugType);
+        event.putInt(BatteryManager.EXTRA_VOLTAGE, mHealthInfo.batteryVoltage);
+        event.putLong(BatteryManager.EXTRA_EVENT_TIMESTAMP, now);
+
+        boolean queueWasEmpty = mBatteryLevelsEventQueue.isEmpty();
+        mBatteryLevelsEventQueue.add(event);
+        // Make sure queue is bounded and doesn't exceed intent payload limits
+        if (mBatteryLevelsEventQueue.size() > MAX_BATTERY_LEVELS_QUEUE_SIZE) {
+            mBatteryLevelsEventQueue.removeFirst();
+        }
+
+        if (queueWasEmpty) {
+            // send now if last event was before throttle interval, otherwise delay
+            long delay = now - mLastBatteryLevelChangedSentMs > BATTERY_LEVEL_CHANGE_THROTTLE_MS
+                    ? 0 : mLastBatteryLevelChangedSentMs + BATTERY_LEVEL_CHANGE_THROTTLE_MS - now;
+            mHandler.postDelayed(this::sendEnqueuedBatteryLevelChangedEvents, delay);
+        }
+    }
+
+    private void sendEnqueuedBatteryLevelChangedEvents() {
+        ArrayList<Bundle> events;
+        synchronized (mLock) {
+            events = new ArrayList<>(mBatteryLevelsEventQueue);
+            mBatteryLevelsEventQueue.clear();
+        }
+        final Intent intent = new Intent(Intent.ACTION_BATTERY_LEVEL_CHANGED);
+        intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+        intent.putParcelableArrayListExtra(BatteryManager.EXTRA_EVENTS, events);
+
+        mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
+                android.Manifest.permission.BATTERY_STATS);
+        mLastBatteryLevelChangedSentMs = SystemClock.elapsedRealtime();
     }
 
     private void logBatteryStatsLocked() {