Merge "Apps can opt in to be stoppable in the task manager" into main
diff --git a/core/java/android/net/vcn/flags.aconfig b/core/java/android/net/vcn/flags.aconfig
index 1adefe5..1b2c575 100644
--- a/core/java/android/net/vcn/flags.aconfig
+++ b/core/java/android/net/vcn/flags.aconfig
@@ -18,13 +18,6 @@
 }
 
 flag {
-    name: "safe_mode_timeout_config"
-    namespace: "vcn"
-    description: "Feature flag for adjustable safe mode timeout"
-    bug: "317406085"
-}
-
-flag {
     name: "fix_config_garbage_collection"
     namespace: "vcn"
     description: "Handle race condition in subscription change"
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
index 4f315a2..76aa5bf 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
@@ -75,6 +75,24 @@
                 @Override
                 public void onAudioDevicesAdded(@NonNull AudioDeviceInfo[] addedDevices) {
                     applyDefaultSelectedTypeToAllPresets();
+
+                    // Activate the last hot plugged valid input device, to match the output device
+                    // behavior.
+                    @AudioDeviceType int deviceTypeToActivate = mSelectedInputDeviceType;
+                    for (AudioDeviceInfo info : addedDevices) {
+                        if (InputMediaDevice.isSupportedInputDevice(info.getType())) {
+                            deviceTypeToActivate = info.getType();
+                        }
+                    }
+
+                    // Only activate if we find a different valid input device. e.g. if none of the
+                    // addedDevices is supported input device, we don't need to activate anything.
+                    if (mSelectedInputDeviceType != deviceTypeToActivate) {
+                        mSelectedInputDeviceType = deviceTypeToActivate;
+                        AudioDeviceAttributes deviceAttributes =
+                                createInputDeviceAttributes(mSelectedInputDeviceType);
+                        setPreferredDeviceForAllPresets(deviceAttributes);
+                    }
                 }
 
                 @Override
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
index 782cee2..d808a25 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
@@ -24,6 +24,7 @@
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -138,6 +139,18 @@
                 /* address= */ "");
     }
 
+    private AudioDeviceAttributes getUsbHeadsetDeviceAttributes() {
+        return new AudioDeviceAttributes(
+                AudioDeviceAttributes.ROLE_INPUT,
+                AudioDeviceInfo.TYPE_USB_HEADSET,
+                /* address= */ "");
+    }
+
+    private AudioDeviceAttributes getHdmiDeviceAttributes() {
+        return new AudioDeviceAttributes(
+                AudioDeviceAttributes.ROLE_INPUT, AudioDeviceInfo.TYPE_HDMI, /* address= */ "");
+    }
+
     private void onPreferredDevicesForCapturePresetChanged(InputRouteManager inputRouteManager) {
         final List<AudioDeviceAttributes> audioDeviceAttributesList =
                 new ArrayList<AudioDeviceAttributes>();
@@ -303,21 +316,47 @@
     }
 
     @Test
-    public void onAudioDevicesAdded_shouldApplyDefaultSelectedDeviceToAllPresets() {
+    public void onAudioDevicesAdded_shouldActivateAddedDevice() {
         final AudioManager audioManager = mock(AudioManager.class);
-        AudioDeviceAttributes wiredHeadsetDeviceAttributes = getWiredHeadsetDeviceAttributes();
-        when(audioManager.getDevicesForAttributes(INPUT_ATTRIBUTES))
-                .thenReturn(Collections.singletonList(wiredHeadsetDeviceAttributes));
-
         InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
         AudioDeviceInfo[] devices = {mockWiredHeadsetInfo()};
         inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
 
-        // Called twice, one after initiation, the other after onAudioDevicesAdded call.
-        verify(audioManager, atLeast(2)).getDevicesForAttributes(INPUT_ATTRIBUTES);
+        // The only added wired headset will be activated.
         for (@MediaRecorder.Source int preset : PRESETS) {
-            verify(audioManager, atLeast(2))
-                    .setPreferredDeviceForCapturePreset(preset, wiredHeadsetDeviceAttributes);
+            verify(audioManager, atLeast(1))
+                    .setPreferredDeviceForCapturePreset(preset, getWiredHeadsetDeviceAttributes());
+        }
+    }
+
+    @Test
+    public void onAudioDevicesAdded_shouldActivateLastAddedDevice() {
+        final AudioManager audioManager = mock(AudioManager.class);
+        InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
+        AudioDeviceInfo[] devices = {mockWiredHeadsetInfo(), mockUsbHeadsetInfo()};
+        inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+
+        // When adding multiple valid input devices, the last added device (usb headset in this
+        // case) will be activated.
+        for (@MediaRecorder.Source int preset : PRESETS) {
+            verify(audioManager, never())
+                    .setPreferredDeviceForCapturePreset(preset, getWiredHeadsetDeviceAttributes());
+            verify(audioManager, atLeast(1))
+                    .setPreferredDeviceForCapturePreset(preset, getUsbHeadsetDeviceAttributes());
+        }
+    }
+
+    @Test
+    public void onAudioDevicesAdded_doNotActivateInvalidAddedDevice() {
+        final AudioManager audioManager = mock(AudioManager.class);
+        InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
+        AudioDeviceInfo[] devices = {mockHdmiInfo()};
+        inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+
+        // Do not activate since HDMI is not a valid input device.
+        for (@MediaRecorder.Source int preset : PRESETS) {
+            verify(audioManager, never())
+                    .setPreferredDeviceForCapturePreset(preset, getHdmiDeviceAttributes());
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 3d9eb53..a39ca5d 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -103,6 +103,7 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.FlagsParameterization;
 import android.service.dreams.IDreamManager;
 import android.service.trust.TrustAgentService;
@@ -129,6 +130,7 @@
 import com.android.keyguard.logging.KeyguardUpdateMonitorLogger;
 import com.android.keyguard.logging.SimLogger;
 import com.android.settingslib.fuelgauge.BatteryStatus;
+import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider;
@@ -190,6 +192,7 @@
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4.class)
 @TestableLooper.RunWithLooper
+@EnableFlags(Flags.FLAG_USER_ENCRYPTED_SOURCE)
 public class KeyguardUpdateMonitorTest extends SysuiTestCase {
     private static final String PKG_ALLOWING_FP_LISTEN_ON_OCCLUDING_ACTIVITY =
             "test_app_fp_listen_on_occluding_activity";
@@ -1292,12 +1295,15 @@
 
     @Test
     public void testIsUserUnlocked() {
+        when(mUserManager.isUserUnlocked(mSelectedUserInteractor.getSelectedUserId())).thenReturn(
+                true);
         // mUserManager will report the user as unlocked on @Before
         assertThat(
                 mKeyguardUpdateMonitor.isUserUnlocked(mSelectedUserInteractor.getSelectedUserId()))
                 .isTrue();
         // Invalid user should not be unlocked.
         int randomUser = 99;
+        when(mUserManager.isUserUnlocked(randomUser)).thenReturn(false);
         assertThat(mKeyguardUpdateMonitor.isUserUnlocked(randomUser)).isFalse();
     }
 
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 4731cfb..0c2ce8d 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -376,6 +376,7 @@
         ":ravenwood-empty-res",
         ":framework-platform-compat-config",
         ":services-platform-compat-config",
+        "texts/ravenwood-build.prop",
     ],
     device_first_srcs: [
         ":apex_icu.dat",
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
index e61a054..678a97b 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
@@ -22,7 +22,6 @@
 import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_RESOURCE_APK;
 import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERBOSE_LOGGING;
 import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERSION_JAVA_SYSPROP;
-import static com.android.ravenwood.common.RavenwoodCommonUtils.getRavenwoodRuntimePath;
 
 import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
@@ -95,8 +94,6 @@
     private static final String LIBRAVENWOOD_INITIALIZER_NAME = "ravenwood_initializer";
     private static final String RAVENWOOD_NATIVE_SYSPROP_NAME = "ravenwood_sysprop";
     private static final String RAVENWOOD_NATIVE_RUNTIME_NAME = "ravenwood_runtime";
-    private static final String RAVENWOOD_BUILD_PROP =
-            getRavenwoodRuntimePath() + "ravenwood-data/build.prop";
 
     /**
      * When enabled, attempt to dump all thread stacks just before we hit the
@@ -209,7 +206,7 @@
         System.load(RavenwoodCommonUtils.getJniLibraryPath(RAVENWOOD_NATIVE_RUNTIME_NAME));
 
         // Do the basic set up for the android sysprops.
-        RavenwoodSystemProperties.initialize(RAVENWOOD_BUILD_PROP);
+        RavenwoodSystemProperties.initialize();
         setSystemProperties(null);
 
         // Do this after loading RAVENWOOD_NATIVE_RUNTIME_NAME (which backs Os.setenv()),
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
index 9bc45be..3e4619f 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
@@ -16,21 +16,30 @@
 
 package android.platform.test.ravenwood;
 
-import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_SYSPROP;
+import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERBOSE_LOGGING;
+import static com.android.ravenwood.common.RavenwoodCommonUtils.getRavenwoodRuntimePath;
 
-import com.android.ravenwood.common.RavenwoodCommonUtils;
+import android.util.Log;
 
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Set;
 
 public class RavenwoodSystemProperties {
     private static final String TAG = "RavenwoodSystemProperties";
 
+    /** We pull in propeties from this file. */
+    private static final String RAVENWOOD_BUILD_PROP = "ravenwood-data/ravenwood-build.prop";
+
+    /** This is the actual build.prop we use to build the device (contents depends on lunch). */
+    private static final String DEVICE_BUILD_PROP = "ravenwood-data/build.prop";
+
+    /** The default values. */
     private static final Map<String, String> sDefaultValues = new HashMap<>();
 
     private static final String[] PARTITIONS = {
@@ -43,52 +52,54 @@
             "vendor_dlkm",
     };
 
-    /**
-     * More info about property file loading: system/core/init/property_service.cpp
-     * In the following logic, the only partition we would need to consider is "system",
-     * since we only read from system-build.prop
-     */
-    static void initialize(String propFile) {
-        // Load all properties from build.prop
+    private static Map<String, String> readProperties(String propFile) {
+        // Use an ordered map just for cleaner dump log.
+        final Map<String, String> ret = new LinkedHashMap<>();
         try {
             Files.readAllLines(Path.of(propFile)).stream()
                     .map(String::trim)
                     .filter(s -> !s.startsWith("#"))
                     .map(s -> s.split("\\s*=\\s*", 2))
                     .filter(a -> a.length == 2)
-                    .forEach(a -> sDefaultValues.put(a[0], a[1]));
+                    .forEach(a -> ret.put(a[0], a[1]));
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
+        return ret;
+    }
 
-        // If ro.product.${name} is not set, derive from ro.product.${partition}.${name}
-        // If ro.product.cpu.abilist* is not set, derive from ro.${partition}.product.cpu.abilist*
-        for (var entry : Set.copyOf(sDefaultValues.entrySet())) {
-            final String key;
-            if (entry.getKey().startsWith("ro.product.system.")) {
-                var name = entry.getKey().substring(18);
-                key = "ro.product." + name;
+    /**
+     * Load default sysprops from {@link #RAVENWOOD_BUILD_PROP}. We also pull in
+     * certain properties from the acutual device's build.prop {@link #DEVICE_BUILD_PROP} too.
+     *
+     * More info about property file loading: system/core/init/property_service.cpp
+     * In the following logic, the only partition we would need to consider is "system",
+     * since we only read from system-build.prop
+     */
+    static void initialize() {
+        var path = getRavenwoodRuntimePath();
+        var ravenwoodProps = readProperties(path + RAVENWOOD_BUILD_PROP);
+        var deviceProps = readProperties(path + DEVICE_BUILD_PROP);
 
-            } else if (entry.getKey().startsWith("ro.system.product.cpu.abilist")) {
-                var name = entry.getKey().substring(22);
-                key = "ro.product.cpu." + name;
+        Log.i(TAG, "Default system properties:");
+        ravenwoodProps.forEach((key, origValue) -> {
+            final String value;
+
+            // If a value starts with "$$$", then this is a reference to the device-side value.
+            if (origValue.startsWith("$$$")) {
+                var deviceKey = origValue.substring(3);
+                var deviceValue = deviceProps.get(deviceKey);
+                if (deviceValue == null) {
+                    throw new RuntimeException("Failed to initialize system properties. Key '"
+                             + deviceKey + "' doesn't exist in the device side build.prop");
+                }
+                value = deviceValue;
             } else {
-                continue;
+                value = origValue;
             }
-            if (!sDefaultValues.containsKey(key)) {
-                sDefaultValues.put(key, entry.getValue());
-            }
-        }
-
-        // Some other custom values
-        sDefaultValues.put("ro.board.first_api_level", "1");
-        sDefaultValues.put("ro.product.first_api_level", "1");
-        sDefaultValues.put("ro.soc.manufacturer", "Android");
-        sDefaultValues.put("ro.soc.model", "Ravenwood");
-        sDefaultValues.put(RAVENWOOD_SYSPROP, "1");
-
-        // Log all values
-        sDefaultValues.forEach((key, value) -> RavenwoodCommonUtils.log(TAG, key + "=" + value));
+            Log.i(TAG, key + "=" + value);
+            sDefaultValues.put(key, value);
+        });
 
         // Copy ro.product.* and ro.build.* to all partitions, just in case
         // We don't want to log these because these are just a lot of duplicate values
@@ -104,6 +115,13 @@
                 }
             }
         }
+        if (RAVENWOOD_VERBOSE_LOGGING) {
+            // Dump all properties for local debugging.
+            Log.v(TAG, "All system properties:");
+            for (var key : sDefaultValues.keySet().stream().sorted().toList()) {
+                Log.v(TAG, "" + key + "=" + sDefaultValues.get(key));
+            }
+        }
     }
 
     private volatile boolean mIsImmutable;
diff --git a/ravenwood/texts/build.prop-sample-cuttlefish b/ravenwood/texts/build.prop-sample-cuttlefish
new file mode 100644
index 0000000..f78b727
--- /dev/null
+++ b/ravenwood/texts/build.prop-sample-cuttlefish
@@ -0,0 +1,132 @@
+# This is file is generated with `aosp_cf_x86_64_phone-trunk_staging-eng` on 2024-11-06.
+# We have this file here only as a reference. We don't actually use this file anywhere.
+
+####################################
+# from generate_common_build_props
+# These properties identify this partition image.
+####################################
+ro.product.system.brand=Android
+ro.product.system.device=generic
+ro.product.system.manufacturer=Android
+ro.product.system.model=mainline
+ro.product.system.name=mainline
+ro.system.product.cpu.abilist=x86_64,x86,arm64-v8a,armeabi-v7a,armeabi
+ro.system.product.cpu.abilist32=x86,armeabi-v7a,armeabi
+ro.system.product.cpu.abilist64=x86_64,arm64-v8a
+ro.system.build.date=Tue Nov  5 13:25:43 PST 2024
+ro.system.build.date.utc=1730841943
+ro.system.build.fingerprint=generic/aosp_cf_x86_64_phone/vsoc_x86_64:Baklava/MAIN/eng.omakot:eng/test-keys
+ro.system.build.id=MAIN
+ro.system.build.tags=test-keys
+ro.system.build.type=eng
+ro.system.build.version.incremental=eng.omakot
+ro.system.build.version.release=15
+ro.system.build.version.release_or_codename=Baklava
+ro.system.build.version.sdk=35
+####################################
+# from gen_build_prop.py:generate_build_info
+####################################
+# begin build properties
+ro.build.legacy.id=MAIN
+ro.build.display.id=aosp_cf_x86_64_phone-eng Baklava MAIN eng.omakot test-keys
+ro.build.version.incremental=eng.omakot
+ro.build.version.sdk=35
+ro.build.version.preview_sdk=1
+ro.build.version.preview_sdk_fingerprint=2ef06129940d459014cf4dede3950d71
+ro.build.version.codename=Baklava
+ro.build.version.all_codenames=Baklava
+ro.build.version.known_codenames=Base,Base11,Cupcake,Donut,Eclair,Eclair01,EclairMr1,Froyo,Gingerbread,GingerbreadMr1,Honeycomb,HoneycombMr1,HoneycombMr2,IceCreamSandwich,IceCreamSandwichMr1,JellyBean,JellyBeanMr1,JellyBeanMr2,Kitkat,KitkatWatch,Lollipop,LollipopMr1,M,N,NMr1,O,OMr1,P,Q,R,S,Sv2,Tiramisu,UpsideDownCake,VanillaIceCream,Baklava
+ro.build.version.release=15
+ro.build.version.release_or_codename=Baklava
+ro.build.version.release_or_preview_display=Baklava
+ro.build.version.security_patch=2024-08-05
+ro.build.version.base_os=
+ro.build.version.min_supported_target_sdk=28
+ro.build.date=Tue Nov  5 13:25:43 PST 2024
+ro.build.date.utc=1730841943
+ro.build.type=eng
+ro.build.user=omakoto
+ro.build.host=omakoto-ct1.c.googlers.com
+ro.build.tags=test-keys
+ro.build.flavor=aosp_cf_x86_64_phone-eng
+# ro.product.cpu.abi and ro.product.cpu.abi2 are obsolete,
+# use ro.product.cpu.abilist instead.
+ro.product.cpu.abi=x86_64
+ro.product.locale=en-US
+ro.wifi.channels=
+# ro.build.product is obsolete; use ro.product.device
+ro.build.product=vsoc_x86_64
+# Do not try to parse description or thumbprint
+ro.build.description=aosp_cf_x86_64_phone-eng Baklava MAIN eng.omakot test-keys
+# end build properties
+####################################
+# from variable ADDITIONAL_SYSTEM_PROPERTIES
+####################################
+ro.treble.enabled=true
+ro.llndk.api_level=202504
+ro.actionable_compatible_property.enabled=true
+persist.debug.dalvik.vm.core_platform_api_policy=just-warn
+ro.postinstall.fstab.prefix=/system
+ro.kernel.android.checkjni=1
+ro.secure=0
+ro.allow.mock.location=1
+dalvik.vm.lockprof.threshold=500
+ro.debuggable=1
+dalvik.vm.image-dex2oat-filter=extract
+init.svc_debug.no_fatal.zygote=true
+net.bt.name=Android
+ro.force.debuggable=0
+####################################
+# from variable PRODUCT_SYSTEM_PROPERTIES
+####################################
+debug.atrace.tags.enableflags=0
+persist.traced.enable=1
+dalvik.vm.image-dex2oat-Xms=64m
+dalvik.vm.image-dex2oat-Xmx=64m
+dalvik.vm.dex2oat-Xms=64m
+dalvik.vm.dex2oat-Xmx=512m
+dalvik.vm.usejit=true
+dalvik.vm.dexopt.secondary=true
+dalvik.vm.dexopt.thermal-cutoff=2
+dalvik.vm.appimageformat=lz4
+ro.dalvik.vm.native.bridge=0
+pm.dexopt.post-boot=verify
+pm.dexopt.first-boot=verify
+pm.dexopt.boot-after-ota=verify
+pm.dexopt.boot-after-mainline-update=verify
+pm.dexopt.install=speed-profile
+pm.dexopt.install-fast=skip
+pm.dexopt.install-bulk=speed-profile
+pm.dexopt.install-bulk-secondary=verify
+pm.dexopt.install-bulk-downgraded=verify
+pm.dexopt.install-bulk-secondary-downgraded=verify
+pm.dexopt.bg-dexopt=speed-profile
+pm.dexopt.ab-ota=speed-profile
+pm.dexopt.inactive=verify
+pm.dexopt.cmdline=verify
+pm.dexopt.shared=speed
+dalvik.vm.disable-art-service-dexopt=true
+dalvik.vm.disable-odrefresh=true
+dalvik.vm.dex2oat-resolve-startup-strings=true
+dalvik.vm.dex2oat-max-image-block-size=524288
+dalvik.vm.minidebuginfo=true
+dalvik.vm.dex2oat-minidebuginfo=true
+dalvik.vm.madvise.vdexfile.size=104857600
+dalvik.vm.madvise.odexfile.size=104857600
+dalvik.vm.madvise.artfile.size=4294967295
+dalvik.vm.usap_pool_enabled=false
+dalvik.vm.usap_refill_threshold=1
+dalvik.vm.usap_pool_size_max=3
+dalvik.vm.usap_pool_size_min=1
+dalvik.vm.usap_pool_refill_delay_ms=3000
+dalvik.vm.useartservice=true
+dalvik.vm.enable_pr_dexopt=true
+ro.cp_system_other_odex=1
+ro.apex.updatable=true
+ro.launcher.depth.widget=0
+####################################
+# from variable PRODUCT_SYSTEM_DEFAULT_PROPERTIES
+####################################
+# Auto-added by post_process_props.py
+persist.sys.usb.config=adb
+# end of file
diff --git a/ravenwood/texts/ravenwood-build.prop b/ravenwood/texts/ravenwood-build.prop
new file mode 100644
index 0000000..93a18cf
--- /dev/null
+++ b/ravenwood/texts/ravenwood-build.prop
@@ -0,0 +1,44 @@
+# This file contains system properties used on ravenwood.
+
+ro.is_on_ravenwood=1
+
+ro.board.first_api_level=1
+ro.product.first_api_level=1
+ro.soc.manufacturer=Android
+ro.soc.model=Ravenwood
+ro.debuggable=1
+
+# The ones starting with "ro.product" or "ro.bild" will be copied to all "partitions" too.
+# See RavenwoodSystemProperties.
+ro.product.brand=Android
+ro.product.device=Ravenwood
+ro.product.manufacturer=Android
+ro.product.model=Ravenwood
+ro.product.name=Ravenwood
+ro.product.cpu.abilist=x86_64
+ro.product.cpu.abilist32=
+ro.product.cpu.abilist64=x86_64
+
+ro.build.date=Thu Jan 01 00:00:00 GMT 2024
+ro.build.date.utc=1704092400
+ro.build.id=MAIN
+ro.build.tags=dev-keys
+ro.build.type=userdebug
+ro.build.version.incremental=userdebug.ravenwood.20240101
+
+# These are what we used to use on Ravenwood, copied here as a reference.
+#ro.build.version.codename=REL
+#ro.build.version.all_codenames=REL
+#ro.build.version.known_codenames=REL
+#ro.build.version.release=14
+#ro.build.version.release_or_codename=VanillaIceCream
+#ro.build.version.sdk=34
+
+# We pull in the following values from the real build.prop file.
+ro.build.version.codename=$$$ro.build.version.codename
+ro.build.version.all_codenames=$$$ro.build.version.codename
+ro.build.version.known_codenames=$$$ro.build.version.codename
+ro.build.version.release=$$$ro.build.version.release
+ro.build.version.release_or_codename=$$$ro.build.version.release_or_codename
+ro.build.version.release_or_preview_display=$$$ro.build.version.release_or_preview_display
+ro.build.version.sdk=$$$ro.build.version.sdk
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/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index 5fc3e33..05bc69a 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -1015,7 +1015,8 @@
                     permission, attributionSource, message, forDataDelivery, startDataDelivery,
                     fromDatasource, attributedOp);
             // Finish any started op if some step in the attribution chain failed.
-            if (startDataDelivery && result != PermissionChecker.PERMISSION_GRANTED) {
+            if (startDataDelivery && result != PermissionChecker.PERMISSION_GRANTED
+                    && result != PermissionChecker.PERMISSION_SOFT_DENIED) {
                 if (attributedOp == AppOpsManager.OP_NONE) {
                     finishDataDelivery(AppOpsManager.permissionToOpCode(permission),
                             attributionSource.asState(), fromDatasource);
@@ -1244,6 +1245,7 @@
             final boolean hasChain = attributionChainId != ATTRIBUTION_CHAIN_ID_NONE;
             AttributionSource current = attributionSource;
             AttributionSource next = null;
+            AttributionSource prev = null;
             // We consider the chain trusted if the start node has UPDATE_APP_OPS_STATS, and
             // every attributionSource in the chain is registered with the system.
             final boolean isChainStartTrusted = !hasChain || checkPermission(context,
@@ -1310,6 +1312,22 @@
                         selfAccess, singleReceiverFromDatasource, attributedOp,
                         proxyAttributionFlags, proxiedAttributionFlags, attributionChainId);
 
+                if (startDataDelivery && opMode != AppOpsManager.MODE_ALLOWED) {
+                    // Current failed the perm check, so if we are part-way through an attr chain,
+                    // we need to clean up the already started proxy op higher up the chain.  Note,
+                    // proxy ops are verified two by two, which means we have to clear the 2nd next
+                    // from the previous iteration (since it is actually curr.next which failed
+                    // to pass the perm check).
+                    if (prev != null) {
+                        final var cutAttrSourceState = prev.asState();
+                        if (cutAttrSourceState.next.length > 0) {
+                            cutAttrSourceState.next[0].next = new AttributionSourceState[0];
+                        }
+                        finishDataDelivery(context, attributedOp,
+                                cutAttrSourceState, fromDatasource);
+                    }
+                }
+
                 switch (opMode) {
                     case AppOpsManager.MODE_ERRORED: {
                         if (permission.equals(Manifest.permission.BLUETOOTH_CONNECT)) {
@@ -1335,6 +1353,8 @@
                     return PermissionChecker.PERMISSION_GRANTED;
                 }
 
+                // an attribution we have already possibly started an op for
+                prev = current;
                 current = next;
             }
         }
diff --git a/services/core/java/com/android/server/vcn/VcnContext.java b/services/core/java/com/android/server/vcn/VcnContext.java
index 6ce8685..9213d96 100644
--- a/services/core/java/com/android/server/vcn/VcnContext.java
+++ b/services/core/java/com/android/server/vcn/VcnContext.java
@@ -74,10 +74,6 @@
         return mFeatureFlags;
     }
 
-    public boolean isFlagSafeModeTimeoutConfigEnabled() {
-        return mFeatureFlags.safeModeTimeoutConfig();
-    }
-
     /**
      * Verifies that the caller is running on the VcnContext Thread.
      *
diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
index 2d3bc84..2325f35 100644
--- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
+++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
@@ -1263,7 +1263,7 @@
         final PersistableBundleWrapper carrierConfig = snapshot.getCarrierConfigForSubGrp(subGrp);
         int resultSeconds = defaultSeconds;
 
-        if (vcnContext.isFlagSafeModeTimeoutConfigEnabled() && carrierConfig != null) {
+        if (carrierConfig != null) {
             resultSeconds =
                     carrierConfig.getInt(
                             VcnManager.VCN_SAFE_MODE_TIMEOUT_SECONDS_KEY, defaultSeconds);
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));
+    }
+}
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
index 49665f7..613b926 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
@@ -360,12 +360,10 @@
 
     private void verifyGetSafeModeTimeoutMs(
             boolean isInTestMode,
-            boolean isConfigTimeoutSupported,
             PersistableBundleWrapper carrierConfig,
             long expectedTimeoutMs)
             throws Exception {
         doReturn(isInTestMode).when(mVcnContext).isInTestMode();
-        doReturn(isConfigTimeoutSupported).when(mVcnContext).isFlagSafeModeTimeoutConfigEnabled();
 
         final TelephonySubscriptionSnapshot snapshot = mock(TelephonySubscriptionSnapshot.class);
         doReturn(carrierConfig).when(snapshot).getCarrierConfigForSubGrp(TEST_SUB_GRP);
@@ -377,16 +375,7 @@
     }
 
     @Test
-    public void testGetSafeModeTimeoutMs_configTimeoutUnsupported() throws Exception {
-        verifyGetSafeModeTimeoutMs(
-                false /* isInTestMode */,
-                false /* isConfigTimeoutSupported */,
-                null /* carrierConfig */,
-                TimeUnit.SECONDS.toMillis(SAFEMODE_TIMEOUT_SECONDS));
-    }
-
-    @Test
-    public void testGetSafeModeTimeoutMs_configTimeoutSupported() throws Exception {
+    public void testGetSafeModeTimeoutMs() throws Exception {
         final int carrierConfigTimeoutSeconds = 20;
         final PersistableBundleWrapper carrierConfig = mock(PersistableBundleWrapper.class);
         doReturn(carrierConfigTimeoutSeconds)
@@ -395,17 +384,14 @@
 
         verifyGetSafeModeTimeoutMs(
                 false /* isInTestMode */,
-                true /* isConfigTimeoutSupported */,
                 carrierConfig,
                 TimeUnit.SECONDS.toMillis(carrierConfigTimeoutSeconds));
     }
 
     @Test
-    public void testGetSafeModeTimeoutMs_configTimeoutSupported_carrierConfigNull()
-            throws Exception {
+    public void testGetSafeModeTimeoutMs_carrierConfigNull() throws Exception {
         verifyGetSafeModeTimeoutMs(
                 false /* isInTestMode */,
-                true /* isConfigTimeoutSupported */,
                 null /* carrierConfig */,
                 TimeUnit.SECONDS.toMillis(SAFEMODE_TIMEOUT_SECONDS));
     }
@@ -420,7 +406,6 @@
 
         verifyGetSafeModeTimeoutMs(
                 true /* isInTestMode */,
-                true /* isConfigTimeoutSupported */,
                 carrierConfig,
                 TimeUnit.SECONDS.toMillis(carrierConfigTimeoutSeconds));
     }
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
index 4c7b25a..8374fd9 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
@@ -222,7 +222,6 @@
         doReturn(mTestLooper.getLooper()).when(mVcnContext).getLooper();
         doReturn(mVcnNetworkProvider).when(mVcnContext).getVcnNetworkProvider();
         doReturn(mFeatureFlags).when(mVcnContext).getFeatureFlags();
-        doReturn(true).when(mVcnContext).isFlagSafeModeTimeoutConfigEnabled();
 
         doReturn(mUnderlyingNetworkController)
                 .when(mDeps)