[bt][hfp] Transfer of HF Indicator Values procedure

Bug: 64565
Test: Added to bt-hfp-audio-gateway-tests

Change-Id: Ie3b9ac6dd74b05d456be77311974f65292525d99
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/513287
Commit-Queue: Ani Ramakrishnan <aniramakri@google.com>
Fuchsia-Auto-Submit: Ani Ramakrishnan <aniramakri@google.com>
Reviewed-by: Jeff Belgum <belgum@google.com>
diff --git a/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/BUILD.gn b/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/BUILD.gn
index f0527e9..8428786 100644
--- a/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/BUILD.gn
+++ b/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/BUILD.gn
@@ -66,6 +66,7 @@
     "src/procedure/ring.rs",
     "src/procedure/slc_initialization.rs",
     "src/procedure/subscriber_number_information.rs",
+    "src/procedure/transfer_hf_indicator.rs",
     "src/procedure/volume_synchronization.rs",
     "src/profile.rs",
     "src/protocol.rs",
diff --git a/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/src/peer/service_level_connection.rs b/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/src/peer/service_level_connection.rs
index 9aa3295..bf9d895 100644
--- a/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/src/peer/service_level_connection.rs
+++ b/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/src/peer/service_level_connection.rs
@@ -679,7 +679,7 @@
     /// Expects a message to be received by the peer. If provided, validates the contents
     /// of the received message.
     #[track_caller]
-    fn expect_peer_ready(
+    pub fn expect_peer_ready(
         exec: &mut fasync::Executor,
         remote: &mut Channel,
         expected: Option<Vec<u8>>,
@@ -710,7 +710,7 @@
 
     /// Serializes the AT Response into a byte buffer.
     #[track_caller]
-    fn serialize_at_response(response: at::Response) -> Vec<u8> {
+    pub fn serialize_at_response(response: at::Response) -> Vec<u8> {
         let mut buf = Vec::new();
         response.serialize(&mut buf).expect("serialization is ok");
         buf
diff --git a/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/src/peer/task.rs b/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/src/peer/task.rs
index 58f8592..28cae83 100644
--- a/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/src/peer/task.rs
+++ b/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/src/peer/task.rs
@@ -36,7 +36,7 @@
     error::Error,
     procedure::{AgUpdate, InformationRequest, ProcedureMarker},
     profile::ProfileEvent,
-    protocol::indicators::{AgIndicator, AgIndicators},
+    protocol::indicators::{AgIndicator, AgIndicators, HfIndicator},
 };
 
 pub(super) struct PeerTask {
@@ -233,6 +233,10 @@
                 self.calls.send_dtmf_code(code).await;
                 self.connection.receive_ag_request(marker, response()).await;
             }
+            InformationRequest::SendHfIndicator { indicator, response } => {
+                self.hf_indicator_update(indicator);
+                self.connection.receive_ag_request(marker, response()).await;
+            }
             InformationRequest::SetNrec { enable, response } => {
                 let result = if let Some(handler) = &mut self.handler {
                     if let Ok(Ok(())) = handler.set_nrec_mode(enable).await {
@@ -335,6 +339,22 @@
         self
     }
 
+    /// Sends an HF Indicator update to the client.
+    fn hf_indicator_update(&mut self, indicator: HfIndicator) {
+        match indicator {
+            ind @ HfIndicator::EnhancedSafety(_) => {
+                debug!("Received EnhancedSafety HF Indicator update: {:?}", ind);
+            }
+            HfIndicator::BatteryLevel(v) => {
+                if let Some(handler) = &mut self.handler {
+                    if let Err(e) = handler.report_headset_battery_level(v) {
+                        log::warn!("Couldn't report headset battery level: {:?}", e);
+                    }
+                }
+            }
+        }
+    }
+
     /// Request to send the phone `status` by initiating the Phone Status Indicator
     /// procedure.
     async fn ring_update(&mut self, call: Call) {
@@ -403,6 +423,7 @@
     use {
         super::*,
         async_utils::PollExt,
+        at_commands::{self as at, SerDe},
         fidl_fuchsia_bluetooth_bredr::{ProfileMarker, ProfileRequestStream},
         fidl_fuchsia_bluetooth_hfp::{
             CallState, PeerHandlerMarker, PeerHandlerRequest, SignalStrength,
@@ -415,10 +436,16 @@
 
     use crate::{
         peer::service_level_connection::{
-            tests::{create_and_initialize_slc, expect_data_received_by_peer},
+            tests::{
+                create_and_initialize_slc, expect_data_received_by_peer, expect_peer_ready,
+                serialize_at_response,
+            },
             SlcState,
         },
-        protocol::{features::HfFeatures, indicators::AgIndicatorsReporting},
+        protocol::{
+            features::HfFeatures,
+            indicators::{AgIndicatorsReporting, HfIndicators},
+        },
     };
 
     fn arb_signal() -> impl Strategy<Value = Option<SignalStrength>> {
@@ -788,4 +815,77 @@
         // Check that the task's ringer has an active call with the expected call index.
         assert!(task.ringer.ringing());
     }
+
+    #[test]
+    fn incoming_hf_indicator_battery_level_is_propagated_to_peer_handler_stream() {
+        // Set up the executor, peer, and background call manager task
+        let mut exec = fasync::Executor::new().unwrap();
+
+        // Setup the peer task with the specified SlcState to enable the battery level HF indicator.
+        let mut hf_indicators = HfIndicators::default();
+        hf_indicators.enable_indicators(vec![at::BluetoothHFIndicator::BatteryLevel]);
+        let state = SlcState { hf_indicators, ..SlcState::default() };
+        let (connection, mut remote) = create_and_initialize_slc(state);
+        let (peer, mut sender, receiver, _profile) = setup_peer_task(Some(connection));
+
+        let (proxy, mut stream) =
+            fidl::endpoints::create_proxy_and_stream::<PeerHandlerMarker>().unwrap();
+        // The battery level that will be reported by the peer.
+        let expected_level = 79;
+
+        fasync::Task::local(async move {
+            // First request is always the network info.
+            match stream.next().await {
+                Some(Ok(PeerHandlerRequest::WatchNetworkInformation { responder })) => {
+                    responder
+                        .send(NetworkInformation::EMPTY)
+                        .expect("Successfully send network information");
+                }
+                x => panic!("Expected watch network information request: {:?}", x),
+            };
+            // A vec to hold all the stream items we don't care about for this test.
+            let mut junk_drawer = vec![];
+
+            // Filter out all items that are irrelevant to this particular test, placing them in
+            // the junk_drawer.
+            let mut stream = stream.filter_map(move |item| {
+                let item = match item {
+                    Ok(PeerHandlerRequest::ReportHeadsetBatteryLevel { level, .. }) => Some(level),
+                    x => {
+                        junk_drawer.push(x);
+                        None
+                    }
+                };
+                ready(item)
+            });
+            let actual_battery_level = stream.next().await.unwrap();
+            assert_eq!(actual_battery_level, expected_level);
+            // Call manager should collect all further requests, without responding.
+            stream.collect::<Vec<_>>().await;
+        })
+        .detach();
+
+        // Pass in the client end connected to the call manager
+        let result = exec.run_singlethreaded(sender.send(PeerRequest::Handle(proxy)));
+        assert!(result.is_ok());
+
+        // Run the PeerTask.
+        let run_fut = peer.run(receiver);
+        pin_mut!(run_fut);
+        assert!(exec.run_until_stalled(&mut run_fut).is_pending());
+
+        // Peer sends us a battery level HF indicator update.
+        let battery_level_cmd = at::Command::Biev {
+            anum: at::BluetoothHFIndicator::BatteryLevel,
+            value: expected_level as i64,
+        };
+        let mut buf = Vec::new();
+        battery_level_cmd.serialize(&mut buf).expect("serialization is ok");
+        remote.as_ref().write(&buf[..]).expect("channel write is ok");
+        // Run the main future - the spawned task should receive the HF indicator and report it.
+        assert!(exec.run_until_stalled(&mut run_fut).is_pending());
+
+        // Since we (the AG) received a valid HF indicator, we expect to send an OK back to the peer.
+        expect_peer_ready(&mut exec, &mut remote, Some(serialize_at_response(at::Response::Ok)));
+    }
 }
diff --git a/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/src/procedure.rs b/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/src/procedure.rs
index 928eabe..2742f45 100644
--- a/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/src/procedure.rs
+++ b/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/src/procedure.rs
@@ -16,7 +16,7 @@
     },
     protocol::{
         features::AgFeatures,
-        indicators::{AgIndicator, AgIndicators, HfIndicators},
+        indicators::{AgIndicator, AgIndicators, HfIndicator, HfIndicators},
     },
 };
 
@@ -62,6 +62,9 @@
 /// Defines the implementation of the Phone Status Procedures.
 pub mod phone_status;
 
+/// Defines the implementation of the Transfer of HF Indicator Values Procedure.
+pub mod transfer_hf_indicator;
+
 /// Defines the implementation of the Volume Level Synchronization Procedure.
 pub mod volume_synchronization;
 
@@ -79,6 +82,7 @@
 use ring::RingProcedure;
 use slc_initialization::SlcInitProcedure;
 use subscriber_number_information::{build_cnum_response, SubscriberNumberInformationProcedure};
+use transfer_hf_indicator::TransferHfIndicatorProcedure;
 use volume_synchronization::VolumeSynchronizationProcedure;
 
 const THREE_WAY_SUPPORT: &[&str] = &["0", "1", "1X", "2", "2X", "3", "4"];
@@ -172,6 +176,8 @@
     Answer,
     /// The Hang Up procedure as defined in HFP v1.8 Sections 4.14 - 4.15
     HangUp,
+    /// The Transfer of HF Indicator Values procedure as defined in HFP v1.8 Section 4.36.1.5.
+    TransferHfIndicator,
 }
 
 impl ProcedureMarker {
@@ -197,6 +203,7 @@
             Self::Ring => Box::new(RingProcedure::new()),
             Self::Answer => Box::new(AnswerProcedure::new()),
             Self::HangUp => Box::new(HangUpProcedure::new()),
+            Self::TransferHfIndicator => Box::new(TransferHfIndicatorProcedure::new()),
         }
     }
 
@@ -225,6 +232,7 @@
             at::Command::Vts { .. } => Ok(Self::Dtmf),
             at::Command::Answer { .. } => Ok(Self::Answer),
             at::Command::Chup { .. } => Ok(Self::HangUp),
+            at::Command::Biev { .. } => Ok(Self::TransferHfIndicator),
             _ => Err(ProcedureError::NotImplemented),
         }
     }
@@ -243,6 +251,8 @@
 
     SetNrec { enable: bool, response: Box<dyn FnOnce(Result<(), ()>) -> AgUpdate> },
 
+    SendHfIndicator { indicator: HfIndicator, response: Box<dyn FnOnce() -> AgUpdate> },
+
     SendDtmf { code: DtmfCode, response: Box<dyn FnOnce() -> AgUpdate> },
 
     SpeakerVolumeSynchronization { level: Gain, response: Box<dyn FnOnce() -> AgUpdate> },
@@ -265,6 +275,7 @@
             GetSubscriberNumberInformation { .. } => Self::SubscriberNumberInformation,
             SetNrec { .. } => Self::Nrec,
             SendDtmf { .. } => Self::Dtmf,
+            SendHfIndicator { .. } => Self::TransferHfIndicator,
             SpeakerVolumeSynchronization { .. } | MicrophoneVolumeSynchronization { .. } => {
                 Self::VolumeSynchronization
             }
@@ -288,6 +299,10 @@
             Self::QueryCurrentCalls { .. } => "QueryCurrentCalls ",
             // DTFM Code values are not displayed in Debug representation
             Self::SendDtmf { .. } => "SendDtmf",
+            Self::SendHfIndicator { indicator, .. } => {
+                s = format!("SendHfIndicator({:?})", indicator);
+                &s
+            }
             Self::SpeakerVolumeSynchronization { level, .. } => {
                 s = format!("SpeakerVolumeSynchronization({:?})", level);
                 &s
diff --git a/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/src/procedure/slc_initialization.rs b/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/src/procedure/slc_initialization.rs
index db150e9..6fa04dd 100644
--- a/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/src/procedure/slc_initialization.rs
+++ b/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/src/procedure/slc_initialization.rs
@@ -333,7 +333,7 @@
 
         match update {
             at::Command::Bind { indicators } => {
-                state.hf_indicators.set(indicators);
+                state.hf_indicators.enable_indicators(indicators);
                 Box::new(HfSupportedIndicatorsReceived)
             }
             m => SlcErrorState::unexpected_hf(m),
diff --git a/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/src/procedure/transfer_hf_indicator.rs b/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/src/procedure/transfer_hf_indicator.rs
new file mode 100644
index 0000000..4f80035
--- /dev/null
+++ b/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/src/procedure/transfer_hf_indicator.rs
@@ -0,0 +1,210 @@
+// Copyright 2021 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use super::{
+    AgUpdate, InformationRequest, Procedure, ProcedureError, ProcedureMarker, ProcedureRequest,
+};
+
+use crate::peer::service_level_connection::SlcState;
+use at_commands as at;
+
+/// Represents the current state of the HF request to transmit an HF Indicator value as
+/// defined in HFP v1.8 Section 4.36.1.5.
+#[derive(Debug, PartialEq, Clone, Copy)]
+enum State {
+    /// Initial state of the Procedure.
+    Start,
+    /// A request has been received from the HF to transmit the indicator via the AG.
+    SendRequest,
+    /// Terminal state of the procedure.
+    Terminated,
+}
+
+impl State {
+    /// Transition to the next state in the procedure.
+    fn transition(&mut self) {
+        match *self {
+            Self::Start => *self = Self::SendRequest,
+            Self::SendRequest => *self = Self::Terminated,
+            Self::Terminated => *self = Self::Terminated,
+        }
+    }
+}
+
+/// The Hf may send an updated HF Indicator value via this procedure. Defined in
+/// HFP v1.8 Section 4.36.1.5.
+///
+/// This procedure is implemented from the perspective of the AG. Namely, outgoing `requests`
+/// typically request information about the current state of the AG, to be sent to the remote
+/// peer acting as the HF.
+#[derive(Debug)]
+pub struct TransferHfIndicatorProcedure {
+    /// The current state of the procedure
+    state: State,
+}
+
+impl Default for TransferHfIndicatorProcedure {
+    fn default() -> Self {
+        Self { state: State::Start }
+    }
+}
+
+impl TransferHfIndicatorProcedure {
+    /// Create a new Transfer HF Indicator procedure in the Start state.
+    pub fn new() -> Self {
+        Self::default()
+    }
+}
+
+impl Procedure for TransferHfIndicatorProcedure {
+    fn marker(&self) -> ProcedureMarker {
+        ProcedureMarker::TransferHfIndicator
+    }
+
+    fn hf_update(&mut self, update: at::Command, state: &mut SlcState) -> ProcedureRequest {
+        match (self.state, update) {
+            (State::Start, at::Command::Biev { anum, value }) => {
+                self.state.transition();
+                // Per HFP v1.8 Section 4.36.1.5, we should send Error if the request `anum` is
+                // disabled, or the `value` is out of bounds.
+                if let Ok(indicator) = state.hf_indicators.update_indicator_value(anum, value) {
+                    InformationRequest::SendHfIndicator {
+                        indicator,
+                        response: Box::new(|| AgUpdate::Ok),
+                    }
+                    .into()
+                } else {
+                    self.state.transition();
+                    AgUpdate::Error.into()
+                }
+            }
+            (_, update) => ProcedureRequest::Error(ProcedureError::UnexpectedHf(update)),
+        }
+    }
+
+    fn ag_update(&mut self, update: AgUpdate, _state: &mut SlcState) -> ProcedureRequest {
+        match (self.state, update) {
+            (State::SendRequest, update @ AgUpdate::Ok) => {
+                self.state.transition();
+                update.into()
+            }
+            (_, update) => ProcedureRequest::Error(ProcedureError::UnexpectedAg(update)),
+        }
+    }
+
+    fn is_terminated(&self) -> bool {
+        self.state == State::Terminated
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::protocol::indicators::HfIndicator;
+    use matches::assert_matches;
+
+    #[test]
+    fn correct_marker() {
+        let marker = TransferHfIndicatorProcedure::new().marker();
+        assert_eq!(marker, ProcedureMarker::TransferHfIndicator);
+    }
+
+    #[test]
+    fn procedure_handles_invalid_messages() {
+        let mut proc = TransferHfIndicatorProcedure::new();
+        let req = proc.hf_update(at::Command::CindRead {}, &mut SlcState::default());
+        assert_matches!(req, ProcedureRequest::Error(ProcedureError::UnexpectedHf(_)));
+
+        let req = proc.ag_update(AgUpdate::ThreeWaySupport, &mut SlcState::default());
+        assert_matches!(req, ProcedureRequest::Error(ProcedureError::UnexpectedAg(_)));
+    }
+
+    #[test]
+    fn procedure_with_invalid_battery_value_sends_error_message() {
+        let mut proc = TransferHfIndicatorProcedure::new();
+        let mut state = SlcState::default();
+        state.hf_indicators.enable_indicators(vec![
+            at::BluetoothHFIndicator::BatteryLevel,
+            at::BluetoothHFIndicator::EnhancedSafety,
+        ]);
+
+        // Battery level is not within the range [0,100].
+        let cmd = at::Command::Biev { anum: at::BluetoothHFIndicator::BatteryLevel, value: 164 };
+        let req = proc.hf_update(cmd, &mut state);
+        let expected = vec![at::Response::Error];
+        assert_matches!(req, ProcedureRequest::SendMessages(m) if m == expected);
+        assert!(proc.is_terminated());
+    }
+
+    #[test]
+    fn procedure_with_valid_battery_value_sends_ok() {
+        let mut proc = TransferHfIndicatorProcedure::new();
+        let mut state = SlcState::default();
+        state.hf_indicators.enable_indicators(vec![
+            at::BluetoothHFIndicator::BatteryLevel,
+            at::BluetoothHFIndicator::EnhancedSafety,
+        ]);
+
+        let cmd = at::Command::Biev { anum: at::BluetoothHFIndicator::BatteryLevel, value: 76 };
+        let req = proc.hf_update(cmd, &mut state);
+        let update = match req {
+            ProcedureRequest::Info(InformationRequest::SendHfIndicator {
+                indicator: HfIndicator::BatteryLevel(76),
+                response,
+            }) => response(),
+            x => panic!("Expected SendHFInd request but got: {:?}", x),
+        };
+
+        let req = proc.ag_update(update, &mut state);
+        assert_matches!(
+            req,
+            ProcedureRequest::SendMessages(msgs) if msgs == vec![at::Response::Ok]
+        );
+        assert!(proc.is_terminated());
+    }
+
+    #[test]
+    fn procedure_with_invalid_safety_value_sends_error_message() {
+        let mut proc = TransferHfIndicatorProcedure::new();
+        let mut state = SlcState::default();
+        state.hf_indicators.enable_indicators(vec![
+            at::BluetoothHFIndicator::BatteryLevel,
+            at::BluetoothHFIndicator::EnhancedSafety,
+        ]);
+
+        // Enhanced Safety can only be 0 or 1.
+        let cmd = at::Command::Biev { anum: at::BluetoothHFIndicator::EnhancedSafety, value: 7 };
+        let req = proc.hf_update(cmd, &mut state);
+        let expected = vec![at::Response::Error];
+        assert_matches!(req, ProcedureRequest::SendMessages(m) if m == expected);
+        assert!(proc.is_terminated());
+    }
+
+    #[test]
+    fn procedure_with_valid_safety_value_sends_ok() {
+        let mut proc = TransferHfIndicatorProcedure::new();
+        let mut state = SlcState::default();
+        state.hf_indicators.enable_indicators(vec![
+            at::BluetoothHFIndicator::BatteryLevel,
+            at::BluetoothHFIndicator::EnhancedSafety,
+        ]);
+
+        let cmd = at::Command::Biev { anum: at::BluetoothHFIndicator::EnhancedSafety, value: 1 };
+        let req = proc.hf_update(cmd, &mut state);
+        let update = match req {
+            ProcedureRequest::Info(InformationRequest::SendHfIndicator {
+                indicator: HfIndicator::EnhancedSafety(true),
+                response,
+            }) => response(),
+            x => panic!("Expected SendHFInd request but got: {:?}", x),
+        };
+
+        let req = proc.ag_update(update, &mut state);
+        assert_matches!(
+            req,
+            ProcedureRequest::SendMessages(msgs) if msgs == vec![at::Response::Ok]
+        );
+        assert!(proc.is_terminated());
+    }
+}
diff --git a/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/src/protocol/indicators.rs b/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/src/protocol/indicators.rs
index 8ffc78a..39582a0 100644
--- a/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/src/protocol/indicators.rs
+++ b/src/connectivity/bluetooth/profiles/bt-hfp-audio-gateway/src/protocol/indicators.rs
@@ -16,16 +16,23 @@
 pub(crate) const ROAM_INDICATOR_INDEX: usize = 6;
 pub(crate) const BATT_CHG_INDICATOR_INDEX: usize = 7;
 
-/// A single HF Indicator status + value.
+/// The supported HF indicators.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum HfIndicator {
+    EnhancedSafety(bool),
+    BatteryLevel(u8),
+}
+
+/// A single indicator status + value.
 #[derive(Clone, Copy, Debug)]
-pub struct HfIndicator<T: Clone + Copy + Debug> {
+struct Indicator<T: Clone + Copy + Debug> {
     /// Whether this indicator is enabled or not.
     enabled: bool,
     /// The value of the indicator.
     value: Option<T>,
 }
 
-impl<T: Clone + Copy + Debug> Default for HfIndicator<T> {
+impl<T: Clone + Copy + Debug> Default for Indicator<T> {
     fn default() -> Self {
         Self { enabled: false, value: None }
     }
@@ -36,14 +43,18 @@
 #[derive(Clone, Copy, Debug, Default)]
 pub struct HfIndicators {
     /// The Enhanced Safety HF indicator. There are only two potential values (enabled, disabled).
-    enhanced_safety: HfIndicator<bool>,
+    enhanced_safety: Indicator<bool>,
     /// The Battery Level HF indicator. Can be any integer value between [0, 100].
-    battery_level: HfIndicator<u8>,
+    battery_level: Indicator<u8>,
 }
 
 impl HfIndicators {
-    /// Sets the HF indicators based on the provided AT `indicators`.
-    pub fn set(&mut self, indicators: Vec<at::BluetoothHFIndicator>) {
+    /// The Maximum Battery Level value for the `battery_level` indicator.
+    /// Defined in HFP v1.8 Section 4.35.
+    const MAX_BATTERY_LEVEL: u8 = 100;
+
+    /// Enables the supported HF indicators based on the provided AT `indicators`.
+    pub fn enable_indicators(&mut self, indicators: Vec<at::BluetoothHFIndicator>) {
         for ind in indicators {
             if ind == at::BluetoothHFIndicator::EnhancedSafety {
                 self.enhanced_safety.enabled = true;
@@ -54,6 +65,39 @@
         }
     }
 
+    /// Updates the `indicator` with the provided `value`.
+    /// Returns Error if the indicator is disabled or if the `value` is out of bounds.
+    /// Returns a valid HfIndicator on success.
+    pub fn update_indicator_value(
+        &mut self,
+        indicator: at::BluetoothHFIndicator,
+        value: i64,
+    ) -> Result<HfIndicator, ()> {
+        let ind = match indicator {
+            at::BluetoothHFIndicator::EnhancedSafety if self.enhanced_safety.enabled => {
+                if value != 0 && value != 1 {
+                    return Err(());
+                }
+                let v = value != 0;
+                self.enhanced_safety.value = Some(v);
+                HfIndicator::EnhancedSafety(v)
+            }
+            at::BluetoothHFIndicator::BatteryLevel if self.battery_level.enabled => {
+                if value < 0 || value > Self::MAX_BATTERY_LEVEL.into() {
+                    return Err(());
+                }
+                let v = value as u8;
+                self.battery_level.value = Some(v);
+                HfIndicator::BatteryLevel(v)
+            }
+            ind => {
+                log::warn!("Received HF indicator update for disabled indicator: {:?}", ind);
+                return Err(());
+            }
+        };
+        Ok(ind)
+    }
+
     /// Returns the +BIND response for the current HF indicator status.
     pub fn bind_response(&self) -> Vec<at::Response> {
         vec![
@@ -431,6 +475,94 @@
 #[cfg(test)]
 mod tests {
     use super::*;
+    use matches::assert_matches;
+
+    #[test]
+    fn update_hf_indicators_with_invalid_values_is_error() {
+        let mut hf_indicators = HfIndicators::default();
+        hf_indicators.enable_indicators(vec![
+            at::BluetoothHFIndicator::BatteryLevel,
+            at::BluetoothHFIndicator::EnhancedSafety,
+        ]);
+
+        let battery_too_low = -18;
+        assert_matches!(
+            hf_indicators
+                .update_indicator_value(at::BluetoothHFIndicator::BatteryLevel, battery_too_low),
+            Err(())
+        );
+        let battery_too_high = 1243;
+        assert_matches!(
+            hf_indicators
+                .update_indicator_value(at::BluetoothHFIndicator::BatteryLevel, battery_too_high),
+            Err(())
+        );
+
+        let negative_safety = -1;
+        assert_matches!(
+            hf_indicators
+                .update_indicator_value(at::BluetoothHFIndicator::EnhancedSafety, negative_safety),
+            Err(())
+        );
+        let large_safety = 8;
+        assert_matches!(
+            hf_indicators
+                .update_indicator_value(at::BluetoothHFIndicator::EnhancedSafety, large_safety),
+            Err(())
+        );
+    }
+
+    #[test]
+    fn update_disabled_hf_indicators_with_valid_values_is_error() {
+        // Default is no indicators set. Therefore any updates are errors.
+        let mut hf_indicators = HfIndicators::default();
+
+        let valid_battery = 32;
+        assert_matches!(
+            hf_indicators
+                .update_indicator_value(at::BluetoothHFIndicator::BatteryLevel, valid_battery),
+            Err(())
+        );
+
+        let valid_safety = 0;
+        assert_matches!(
+            hf_indicators
+                .update_indicator_value(at::BluetoothHFIndicator::EnhancedSafety, valid_safety),
+            Err(())
+        );
+    }
+
+    #[test]
+    fn update_hf_indicators_with_valid_values_is_ok() {
+        let mut hf_indicators = HfIndicators::default();
+        // Default values.
+        assert_eq!(hf_indicators.enhanced_safety.value, None);
+        assert_eq!(hf_indicators.enhanced_safety.enabled, false);
+        assert_eq!(hf_indicators.battery_level.value, None);
+        assert_eq!(hf_indicators.battery_level.enabled, false);
+
+        // Enable both.
+        hf_indicators.enable_indicators(vec![
+            at::BluetoothHFIndicator::BatteryLevel,
+            at::BluetoothHFIndicator::EnhancedSafety,
+        ]);
+
+        let valid_battery = 83;
+        assert_matches!(
+            hf_indicators
+                .update_indicator_value(at::BluetoothHFIndicator::BatteryLevel, valid_battery),
+            Ok(HfIndicator::BatteryLevel(83))
+        );
+        assert_eq!(hf_indicators.battery_level.value, Some(valid_battery as u8));
+
+        let valid_safety = 0;
+        assert_matches!(
+            hf_indicators
+                .update_indicator_value(at::BluetoothHFIndicator::EnhancedSafety, valid_safety),
+            Ok(HfIndicator::EnhancedSafety(false))
+        );
+        assert_eq!(hf_indicators.enhanced_safety.value, Some(false));
+    }
 
     #[test]
     fn default_indicators_reporting_is_disabled_with_all_indicators_enabled() {
diff --git a/src/connectivity/lib/at-commands/definitions/hfp.at b/src/connectivity/lib/at-commands/definitions/hfp.at
index 5fed5cd..345b3ce 100644
--- a/src/connectivity/lib/at-commands/definitions/hfp.at
+++ b/src/connectivity/lib/at-commands/definitions/hfp.at
@@ -81,6 +81,10 @@
 # HFP 1.8 4.36.1.3
 response BindStatus { +BIND:anum:BluetoothHFIndicator,state:BoolAsInt }
 
+# Bluetooth HF Indicator value command
+# HFP 1.8 4.36.1.5
+command { AT+BIEV=anum:BluetoothHFIndicator,value:Integer}
+
 # Standard Call hold read command.
 # HFP 1.8 4.34.2
 command { AT+CHLD=? }