Add SessionTrack for session status tracking

1. Stop the ongoing sessions on country code switching.
   When the country code is switching from A to B,
   Active Sessions restricted by B country should be stopped.

2. Delay per-country calibrations when it's ACTIVE
   Device calibrations shouldn't be executed when device is ACTIVE state.
   Execute per-country calibrations only when the device is in IDLE.

3. (Optional/Experimental) Issue URSK_DELETE_CMD for CCC session
   closing. This is activated only when DELETE_URSK_FOR_CCC_SESSION=1
   is set from config.

4. (Optional/Expermental) Call suspend to kernel driver on idle.
   It calls ioctl/suspend when it's idle for a given duration
   and ioctl/resume the device before sending any commands.
   This is only activated when AUTO_SUSPEND_ENABLED=1 set from config.

Bug: 323275554
Test: Change country code, regression_2_test_suite, stress_1_test_suite
Change-Id: Iceac1478e9100adaf44b63c8a78257c178851b52
(cherry picked from commit f13904b23d47746816730e963e080005a068c143)
Signed-off-by: Ikjoon Jang <ikjn@google.com>
diff --git a/extns/inc/uci_defs.h b/extns/inc/uci_defs.h
index 837f4e0..009e5f6 100644
--- a/extns/inc/uci_defs.h
+++ b/extns/inc/uci_defs.h
@@ -50,15 +50,20 @@
 #define UCI_PBF_SHIFT 4
 #define UCI_PBF_ST_CONT 0x10    /* start or continuing fragment */
 
+/* Ocet 3 = Payload Length(L) */
+#define UCI_PAYLOAD_LENGTH_OFFSET 3
+
 /* GID: Group Identifier (byte 0) */
-#define UCI_GID_MASK 0x0F
-#define UCI_GID_CORE 0x00             /* 0000b UCI Core group */
-#define UCI_GID_SESSION_MANAGE 0x01   /* 0001b Session Config commands */
-#define UCI_GID_ANDROID 0x0C          /* 1100b Android vendor group */
-#define UCI_GID_PROPRIETARY_0X0A 0x0A /* 1010b Proprietary Group */
-#define UCI_GID_PROPRIETARY 0x0E      /* 1110b Proprietary Group */
-#define UCI_GID_PROPRIETARY_0X0F 0x0F /* 1111b Proprietary Group */
-#define UCI_GID_INTERNAL 0x0B         /* 1011b Internal Group */
+#define UCI_GID_MASK              0x0F
+#define UCI_GID_CORE              0x00 /* UCI Core group */
+#define UCI_GID_SESSION_MANAGE    0x01 /* Session Config Group */
+#define UCI_GID_SESSION_CONTROL   0x02 /* Session Control Group */
+#define UCI_GID_ANDROID           0x0C /* Android vendor group */
+#define UCI_GID_PROPRIETARY_0X0A  0x0A /* Proprietary Group */
+#define UCI_GID_PROPRIETARY       0x0E /* Proprietary Group */
+#define UCI_GID_PROPRIETARY_0X0F  0x0F /* Proprietary Group */
+#define UCI_GID_INTERNAL          0x0B /* Internal Group */
+
 /* 0100b - 1100b RFU */
 #define UCI_OID_GET_CAPS_INFO 0x03
 
@@ -116,13 +121,42 @@
 #define UCI_MSG_CORE_GENERIC_ERROR_NTF 7
 
 /*********************************************************
- * UCI session config Group-2: Opcodes and size of command
+ * UCI session config Group-1: Opcodes and size of command
  ********************************************************/
 #define UCI_MSG_SESSION_STATUS_NTF 2
-#define UCI_MSG_SESSION_SET_APP_CONFIG 3
-#define UCI_MSG_SESSION_QUERY_DATA_SIZE 0x0B
+#define UCI_MSG_SESSION_STATUS_NTF_HANDLE_OFFSET      4
+#define UCI_MSG_SESSION_STATUS_NTF_STATE_OFFSET       8
+#define UCI_MSG_SESSION_STATUS_NTF_REASON_OFFSET      9
+#define UCI_MSG_SESSION_STATUS_NTF_LENGTH             10
+
+#define UCI_MSG_SESSION_STATE_INIT                    (0x00)
+#define UCI_MSG_SESSION_STATE_INIT_CMD_LEN            (9)
+#define UCI_MSG_SESSION_STATE_INIT_CMD_ID_OFFSET      (4)
+#define UCI_MSG_SESSION_STATE_INIT_CMD_TYPE_OFFSET    (8)
+#define UCI_MSG_SESSION_STATE_INIT_RSP_LEN            (9)
+#define UCI_MSG_SESSION_STATE_INIT_RSP_STATUS_OFFSET  (4)
+#define UCI_MSG_SESSION_STATE_INIT_RSP_HANDLE_OFFSET  (5)
+#define UCI_MSG_SESSION_STATE_DEINIT                  (0x01)
+#define UCI_MSG_SESSION_STATE_ACTIVE                  (0x02)
+#define UCI_MSG_SESSION_STATE_IDLE                    (0x03)
+#define UCI_MSG_SESSION_STATE_UNDEFINED               (0xFF)  // SW defined
+
+#define UCI_MSG_SESSION_SET_APP_CONFIG                3
+#define UCI_MSG_SESSION_SET_APP_CONFIG_HANDLE_OFFSET  4
+
+#define UCI_MSG_SESSION_QUERY_DATA_SIZE               0x0B
 #define UCI_MSG_SESSION_QUERY_DATA_SIZE_STATUS_OFFSET 8
 
+// Session Type field in SESSION_INIT_CMD
+constexpr uint8_t kSessionType_Ranging = 0x00;
+constexpr uint8_t kSessionType_RangingAndData = 0x01;
+constexpr uint8_t kSessionType_CCCRanging = 0xA0;
+
+/*********************************************************
+ * UCI session config Group-2: Opcodes and size of command
+ ********************************************************/
+#define UCI_MSG_SESSION_STOP                          0x01
+
 /**********************************************************
  * UCI Android Vendor Group-C: Opcodes and size of commands
  **********************************************************/
@@ -146,6 +180,7 @@
 /**********************************************
  * UWB Prop Group Opcode-F Opcodes
  **********************************************/
+#define UCI_MSG_URSK_DELETE               0x01
 #define UCI_MSG_SET_DEVICE_CALIBRATION    0x21
 #define UCI_MSG_GET_DEVICE_CALIBRATION    0x22
 
diff --git a/halimpl/hal/phNxpUciHal.cc b/halimpl/hal/phNxpUciHal.cc
index 92593c7..3c7131a 100644
--- a/halimpl/hal/phNxpUciHal.cc
+++ b/halimpl/hal/phNxpUciHal.cc
@@ -37,6 +37,7 @@
 #include "hal_nxpuwb.h"
 #include "phNxpConfig.h"
 #include "phNxpUciHal_utils.h"
+#include "sessionTrack.h"
 
 using namespace std;
 using android::base::StringPrintf;
@@ -263,6 +264,8 @@
         }
     } else if ((gid == UCI_GID_SESSION_MANAGE) && (oid == UCI_MSG_SESSION_SET_APP_CONFIG)) {
       return phNxpUciHal_handle_set_app_config(&nxpucihal_ctrl.cmd_len, nxpucihal_ctrl.p_cmd_data);
+    } else if ((gid == UCI_GID_SESSION_MANAGE) && (oid == UCI_MSG_SESSION_STATE_INIT)) {
+      SessionTrack_onSessionInit(nxpucihal_ctrl.cmd_len, nxpucihal_ctrl.p_cmd_data);
     }
   } else if (mt == UCI_MT_RSP) {
     if ((gid == UCI_GID_CORE) && (oid == UCI_MSG_CORE_GET_CAPS_INFO)) {
@@ -449,6 +452,8 @@
   }
   uint16_t len = 0;
 
+  SessionTrack_keepAlive();
+
   CONCURRENCY_LOCK();
   phNxpUciHal_process_ext_cmd_rsp(data_len, p_data, &len);
   CONCURRENCY_UNLOCK();
@@ -883,6 +888,8 @@
 
   CONCURRENCY_LOCK();
 
+  SessionTrack_deinit();
+
   nxpucihal_ctrl.halStatus = HAL_STATUS_CLOSE;
 
   if (NULL != gpphTmlUwb_Context->pDevHandle) {
@@ -1485,6 +1492,8 @@
     return status;
   }
 
+  SessionTrack_init();
+
   // report to upper-layer
   phTmlUwb_DeferredCall(std::make_shared<phLibUwb_Message>(UCI_HAL_INIT_CPLT_MSG));
 
diff --git a/halimpl/hal/phNxpUciHal_ext.cc b/halimpl/hal/phNxpUciHal_ext.cc
index ebf62e9..d6bbfda 100644
--- a/halimpl/hal/phNxpUciHal_ext.cc
+++ b/halimpl/hal/phNxpUciHal_ext.cc
@@ -23,10 +23,12 @@
 
 #include "phNxpConfig.h"
 #include "phNxpLog.h"
-#include "phNxpUciHal_ext.h"
 #include "phNxpUciHal.h"
+#include "phNxpUciHal_ext.h"
+#include "phNxpUciHal_utils.h"
 #include "phTmlUwb.h"
 #include "phUwbCommon.h"
+#include "sessionTrack.h"
 
 /* Timeout value to wait for response from DEVICE_TYPE_SR1xx */
 #define MAX_COMMAND_RETRY_COUNT           5
@@ -1219,8 +1221,6 @@
 
   // TX_BASE_BAND_CONTROL
   {
-    NXPLOG_UCIHAL_D("Apply TX_BASE_BAND_CONTROL: ddfs_enable=%u, dc_suppress=%u", ddfs_enable, dc_suppress);
-
     uint8_t flag = 0;
     if (ddfs_enable)
       flag |= 0x01;
@@ -1261,7 +1261,7 @@
 
 extern bool isCountryCodeMapCreated;
 
-static void apply_per_country_calibrations(void)
+void apply_per_country_calibrations(void)
 {
   // TX-POWER can be provided by
   // 1) COUNTRY_CODE_CAPS with offset values.
@@ -1290,10 +1290,6 @@
  ******************************************************************************/
 void phNxpUciHal_handle_set_country_code(const char country_code[2])
 {
-  //
-  // TODO: stop all active session which are affected by new country code
-  // TODO: delay per-country calibrations when there's active sessions (or juststop them?)
-  //
   NXPLOG_UCIHAL_D("Apply country code %c%c", country_code[0], country_code[1]);
 
   phNxpUciHal_Runtime_Settings_t *rt_set = &nxpucihal_ctrl.rt_settings;
@@ -1305,9 +1301,7 @@
   }
 
   if (NxpConfig_SetCountryCode(country_code)) {
-    // Load extra calibration files first, so COUNTRY_CODE_CAPS can overrides the settings.
-
-    // per-country extra calibrations are only triggered when 'COUNTRY_CODE_CAPS' is not provided
+    // Load ExtraCal restrictions
     uint16_t mask= 0;
     if (NxpConfig_GetNum("cal.restricted_channels", &mask, sizeof(mask))) {
       NXPLOG_UCIHAL_D("Restriction flag, restricted channel mask=0x%x", mask);
@@ -1326,7 +1320,7 @@
                       country_code[0], country_code[1]);
     }
 
-    // Load 'COUNTRY_CODE_CAPS' and apply it to 'conf_map'
+    // Load 'COUNTRY_CODE_CAPS' restrictions (via 'conf_map')
     uint8_t cc_caps[UCI_MAX_DATA_LEN];
     long retlen = 0;
     if (NxpConfig_GetByteArray(NAME_NXP_UWB_COUNTRY_CODE_CAPS, cc_caps, sizeof(cc_caps), &retlen) && retlen) {
@@ -1342,10 +1336,10 @@
         NXPLOG_UCIHAL_D("Country code caps loaded");
       }
     }
+    // Apply per-country calibration, it's handled by SessionTrack
+    SessionTrack_onCountryCodeChanged();
   }
 
-  apply_per_country_calibrations();
-
   // send country code response to upper layer
   nxpucihal_ctrl.rx_data_len = 5;
   static uint8_t rsp_data[5] = { 0x4c, 0x01, 0x00, 0x01 };
@@ -1390,6 +1384,9 @@
     return false;
   }
 
+  uint32_t session_handle = le_bytes_to_cpu<uint32_t>(&p_data[UCI_MSG_SESSION_SET_APP_CONFIG_HANDLE_OFFSET]);
+  uint8_t ch = 0;
+
   // Create local copy of cmd_data for data manipulation
   uint8_t uciCmd[UCI_MAX_DATA_LEN];
   uint16_t packet_len = *data_len;
@@ -1418,7 +1415,7 @@
 
     // check restricted channel
     if (tlv_tag == UCI_PARAM_ID_CHANNEL_NUMBER && tlv_len == 1) {
-      uint8_t ch = p_data[i + 2];
+      ch = p_data[i + 2];
 
       if (((ch == CHANNEL_NUM_5) && (rt_set->restricted_channel_mask & (1 << 5))) ||
           ((ch == CHANNEL_NUM_9) && (rt_set->restricted_channel_mask & (1 << 9)))) {
@@ -1471,5 +1468,7 @@
     *data_len = packet_len;
   }
 
+  SessionTrack_onAppConfig(session_handle, ch);
+
   return false;
 }
diff --git a/halimpl/hal/phNxpUciHal_ext.h b/halimpl/hal/phNxpUciHal_ext.h
index e7ef9ae..d5a8926 100644
--- a/halimpl/hal/phNxpUciHal_ext.h
+++ b/halimpl/hal/phNxpUciHal_ext.h
@@ -71,4 +71,5 @@
 void phNxpUciHal_process_response();
 void phNxpUciHal_handle_set_country_code(const char country_code[2]);
 bool phNxpUciHal_handle_set_app_config(uint16_t *data_len, uint8_t *p_data);
+void apply_per_country_calibrations(void);
 #endif /* _PHNXPNICHAL_EXT_H_ */
diff --git a/halimpl/hal/sessionTrack.cc b/halimpl/hal/sessionTrack.cc
new file mode 100644
index 0000000..9b7170a
--- /dev/null
+++ b/halimpl/hal/sessionTrack.cc
@@ -0,0 +1,529 @@
+#include <bit>
+#include <mutex>
+#include <unordered_map>
+#include <thread>
+#include <vector>
+
+#include "phNxpConfig.h"
+#include "phNxpUciHal.h"
+#include "phNxpUciHal_ext.h"
+#include "phNxpUciHal_utils.h"
+
+extern phNxpUciHal_Control_t nxpucihal_ctrl;
+
+//
+// SessionTrack
+//
+// Keeps track of device/session state.
+//
+// 1. Per-country calibrations
+//
+// When the country code is switching from A to B,
+// a. Active Sessions restricted by B country should be stopped.
+// b. Per-country device calibrations should be delayed to where
+//    device stays in IDLE.
+//
+// 2. Issue URSK_DELETE_CMD on SESSION_DEINIT_RSP (optional/experimental)
+//
+// Calls URSK_DELETE_CMD for every CCC session closing,
+// for the cases where CCC session ID was created but not started.
+// (This is only activated when DELETE_URSK_FOR_CCC_SESSION=1 is set
+// from config)
+//
+// 3. Call suspend to kernel driver on idle (optional/experimental)
+//
+// (This is only activated when AUTO_SUSPEND_ENABLED=1 is set from config)
+// Tracks the each session's status and automatically requests suspend
+// to kernel driver when it's idle for a given duration,
+// and resumes the device before sending any commands.
+// SessionTracks detects UWBS is in idle when there's no session created.
+//
+
+class SessionTrack {
+private:
+  // Session
+  struct SessionInfo {
+    uint32_t  session_id_;
+    uint8_t   session_type_;
+    uint8_t   session_state_;
+    uint8_t   channel_;
+    SessionInfo(uint32_t session_id, uint8_t session_type) :
+      session_id_(session_id),
+      session_type_(session_type),
+      session_state_(UCI_MSG_SESSION_STATE_UNDEFINED),
+      channel_(0) {
+    }
+  };
+  enum class SessionTrackWorkType {
+    IDLE = 0,
+    REFRESH_IDLE,
+    ACTIVATE,
+    IDLE_TIMER_FIRED,
+    DELETE_URSK,
+    STOP,
+  };
+  enum class PowerState {
+    SUSPEND = 0,
+    IDLE,
+    ACTIVE,
+  };
+  struct SessionTrackMsg {
+    SessionTrackWorkType type_;
+    std::shared_ptr<SessionInfo> session_info_;
+    bool sync_;
+    std::condition_variable cond_;
+
+    SessionTrackMsg(SessionTrackWorkType type, bool sync) : type_(type), sync_(sync) { }
+
+    // Per-session work item
+    SessionTrackMsg(SessionTrackWorkType type, std::shared_ptr<SessionInfo> session_info, bool sync) :
+      type_(type), session_info_(session_info), sync_(sync) { }
+  };
+  static constexpr unsigned long kAutoSuspendTimeoutDefaultMs_ = (30 * 1000);
+  static constexpr long kQueueTimeoutMs = 500;
+
+private:
+  std::shared_ptr<phNxpUciHal_RxHandler> rx_handler_session_status_ntf_;
+  std::unordered_map<uint32_t, std::shared_ptr<SessionInfo>> sessions_;
+  std::mutex sessions_lock_;
+
+  bool auto_suspend_enabled_;
+  bool delete_ursk_ccc_enabled_;
+  bool calibration_delayed_;
+  std::atomic<PowerState> power_state_;
+  bool idle_timer_started_;
+  unsigned long idle_timeout_ms_;
+
+  std::thread worker_thread_;
+  std::mutex sync_mutex_;
+  uint32_t idle_timer_;
+  std::unique_ptr<MessageQueue<SessionTrackMsg>> msgq_;
+
+public:
+  SessionTrack() :
+    auto_suspend_enabled_(false),
+    delete_ursk_ccc_enabled_(false),
+    calibration_delayed_(false),
+    power_state_(PowerState::IDLE),
+    idle_timer_started_(false),
+    idle_timeout_ms_(kAutoSuspendTimeoutDefaultMs_)
+  {
+    sessions_.clear();
+
+    msgq_ = std::make_unique<MessageQueue<SessionTrackMsg>>("SessionTrack");
+    worker_thread_ = std::thread(&SessionTrack::PowerManagerWorker, this);
+
+    unsigned long numval = 0;
+
+    if (NxpConfig_GetNum(NAME_DELETE_URSK_FOR_CCC_SESSION, &numval, sizeof(numval)) && numval) {
+      delete_ursk_ccc_enabled_ = true;
+    }
+
+    if (NxpConfig_GetNum(NAME_AUTO_SUSPEND_ENABLE, &numval, sizeof(numval)) && numval) {
+      auto_suspend_enabled_ = true;
+
+      NxpConfig_GetNum(NAME_AUTO_SUSPEND_TIMEOUT_MS, &idle_timeout_ms_, sizeof(idle_timeout_ms_));
+
+      // Idle timer is only activated when AUTO_SUSPEND_ENABLED=1
+      // device suspend won't be triggered when it's not activated.
+      idle_timer_ = phOsalUwb_Timer_Create();
+      RefreshIdle();
+    }
+
+    // register SESSION_STATUS_NTF rx handler
+    rx_handler_session_status_ntf_ = phNxpUciHal_rx_handler_add(
+      UCI_MT_NTF, UCI_GID_SESSION_MANAGE, UCI_MSG_SESSION_STATUS_NTF,
+      false, false,
+      std::bind(&SessionTrack::OnSessionStatusNtf, this, std::placeholders::_1, std::placeholders::_2));
+  }
+
+  virtual ~SessionTrack() {
+    phNxpUciHal_rx_handler_del(rx_handler_session_status_ntf_);
+
+    if (auto_suspend_enabled_) {
+      phOsalUwb_Timer_Delete(idle_timer_);
+    }
+    auto msg = std::make_shared<SessionTrackMsg>(SessionTrackWorkType::STOP, true);
+    QueueSessionTrackWork(msg);
+    worker_thread_.join();
+  }
+
+  // Called upon upper-layer's SESSION_INIT_CMD
+  void OnSessionInit(size_t packet_len, const uint8_t *packet)
+  {
+    if (packet_len != UCI_MSG_SESSION_STATE_INIT_CMD_LEN)
+      return;
+
+    uint32_t session_id = le_bytes_to_cpu<uint32_t>(&packet[UCI_MSG_SESSION_STATE_INIT_CMD_ID_OFFSET]);
+    uint8_t session_type = packet[UCI_MSG_SESSION_STATE_INIT_CMD_TYPE_OFFSET];
+
+    // Check SESSION_INIT_RSP for SessionID - Handle matching
+    auto session_init_rsp_cb =
+      [this, session_id, session_type](size_t packet_len, const uint8_t *packet)
+    {
+      if (packet_len != UCI_MSG_SESSION_STATE_INIT_RSP_LEN )
+        return;
+
+      uint8_t status = packet[UCI_MSG_SESSION_STATE_INIT_RSP_STATUS_OFFSET];
+      uint32_t handle = le_bytes_to_cpu<uint32_t>(&packet[UCI_MSG_SESSION_STATE_INIT_RSP_HANDLE_OFFSET]);
+      if (status != UWBSTATUS_SUCCESS)
+        return;
+
+      bool was_idle;
+      {
+        std::lock_guard<std::mutex> lock(sessions_lock_);
+
+        was_idle = IsDeviceIdle();
+
+        sessions_.emplace(std::make_pair(handle,
+                                         std::make_shared<SessionInfo>(session_id, session_type)));
+      }
+      if (was_idle) {
+        NXPLOG_UCIHAL_D("Queue Active");
+        auto msg = std::make_shared<SessionTrackMsg>(SessionTrackWorkType::ACTIVATE, false);
+        QueueSessionTrackWork(msg);
+      }
+    };
+
+    // XXX: This rx handler can be called multiple times on
+    // UCI_STATUS_COMMAND_RETRY(0xA) from SESSION_INIT_CMD
+    phNxpUciHal_rx_handler_add(UCI_MT_RSP, UCI_GID_SESSION_MANAGE,
+      UCI_MSG_SESSION_STATE_INIT, false, true, session_init_rsp_cb);
+  }
+
+  // Called by upper-layer's SetAppConfig command handler
+  void OnChannelConfig(uint32_t session_handle, uint8_t channel) {
+    // Update channel info
+    std::lock_guard<std::mutex> lock(sessions_lock_);
+    auto pSessionInfo = GetSessionInfo(session_handle);
+    if (!pSessionInfo)
+      return;
+    pSessionInfo->channel_ = channel;
+  }
+
+  // Called by upper-layer's SetCountryCode command handler,
+  // Check whether per-country calibration can be executed.
+  // phNxpUciHal_Runtime_Settings_t should've been updated.
+  void OnCountryCodeChanged() {
+    phNxpUciHal_Runtime_Settings_t *rt_set = &nxpucihal_ctrl.rt_settings;
+    NXPLOG_UCIHAL_D("SessionTrack: OnCountryCodeChanged");
+
+    calibration_delayed_ = false;
+    std::vector<uint32_t> blocked_session_handles;
+    {
+      std::lock_guard<std::mutex> lock(sessions_lock_);
+      for (const auto elem : sessions_) {
+        auto session_handle = elem.first;
+        auto pSessionInfo = elem.second;
+
+        if(pSessionInfo->session_state_ != UCI_MSG_SESSION_STATE_ACTIVE)
+          continue;
+        // there's active sessions existed, delay per-country calibrations
+        calibration_delayed_ = true;
+        if (!rt_set->uwb_enable || rt_set->restricted_channel_mask & (1 << pSessionInfo->channel_)) {
+          blocked_session_handles.push_back(session_handle);
+        }
+      }
+    }
+
+    if (rt_set->uwb_enable && !calibration_delayed_) {
+      NXPLOG_UCIHAL_D("SessionTrack: no active sessions, execute per-country cal now.")
+      apply_per_country_calibrations();
+    } else {
+      NXPLOG_UCIHAL_D("SessionTrack: device is in active state, delay per-country cal.")
+      // stop all sessions affected by new country code's restrictions
+      for (auto session_handle : blocked_session_handles) {
+        NXPLOG_UCIHAL_D("SessionTrack: stop session (handle=0x%08x) due to country code restrictions", session_handle);
+        // Can issue an UCI command. This function is only called from upper-layer thread.
+        StopRanging(session_handle);
+      }
+    }
+  }
+
+  void RefreshIdle() {
+    auto msg = std::make_shared<SessionTrackMsg>(SessionTrackWorkType::REFRESH_IDLE, true);
+    QueueSessionTrackWork(msg);
+  }
+
+private:
+  // Send SESSION_STOP_CMD
+  void StopRanging(uint32_t session_handle) {
+    uint8_t session_handle_bytes[4];
+    cpu_to_le_bytes(session_handle_bytes, session_handle);
+
+    std::vector<uint8_t> packet{(UCI_MT_CMD << UCI_MT_SHIFT) | UCI_GID_SESSION_CONTROL, UCI_MSG_SESSION_STOP, 0, 0};
+    packet.insert(packet.end(), std::begin(session_handle_bytes), std::end(session_handle_bytes));
+    packet[UCI_PAYLOAD_LENGTH_OFFSET] = packet.size() - UCI_MSG_HDR_SIZE;
+
+    auto ret = phNxpUciHal_send_ext_cmd(packet.size(), packet.data());
+    if (ret != UWBSTATUS_SUCCESS) {
+      NXPLOG_UCIHAL_E("SessionTrack: Failed to stop session handle 0x%08x", session_handle);
+    }
+  }
+
+  // Send URSK_DELETE_CMD
+  void DeleteUrsk(std::shared_ptr<SessionInfo> session_info) {
+    if (!session_info)
+      return;
+
+    phNxpUciHal_rx_handler_add(UCI_MT_RSP, UCI_GID_PROPRIETARY_0X0F,
+      UCI_MSG_URSK_DELETE, true, true,
+      [](size_t packet_len, const uint8_t *packet) {
+        if (packet_len < 5)
+          return;
+        if (packet[4] != UWBSTATUS_SUCCESS) {
+          NXPLOG_UCIHAL_E("SessionTrack: URSR_DELETE failed, rsp status=0x%x", packet[4]);
+        }
+      }
+    );
+    phNxpUciHal_rx_handler_add(UCI_MT_NTF, UCI_GID_PROPRIETARY_0X0F,
+      UCI_MSG_URSK_DELETE, true, true,
+      [](size_t packet_len, const uint8_t *packet) {
+        if (packet_len < 6)
+          return;
+        uint8_t status = packet[4];
+        uint8_t nr = packet[5];
+        if (packet_len != (6 + 5 * nr)) {
+          NXPLOG_UCIHAL_E("SessionTrack: unrecognized packet type of URSK_DELETE_NTF");
+        } else {
+          for (auto i = 6; i < packet_len; i += 5) {
+            uint32_t session_id = le_bytes_to_cpu<uint32_t>(&packet[i]);
+            uint8_t del_status = packet[i + 4];
+            if (status != UWBSTATUS_SUCCESS) {
+              NXPLOG_UCIHAL_E("SessionTrack: URSK_DELETE failed, ntf status=0x%x", status);
+            } else {
+              NXPLOG_UCIHAL_D("SessionTrack: URSK_DELETE done");
+            }
+          }
+        }
+      }
+    );
+
+    NXPLOG_UCIHAL_D("SessionTrack: URSK_DELETE for session ID 0x%x", session_info->session_id_);
+    uint8_t session_id_bytes[4];
+    cpu_to_le_bytes(session_id_bytes, session_info->session_id_);
+
+    std::vector<uint8_t> packet{(UCI_MT_CMD << UCI_MT_SHIFT) | UCI_GID_PROPRIETARY_0X0F,
+      UCI_MSG_URSK_DELETE, 0, 0};
+
+    packet.push_back(1);  // Num of Session IDs = 1
+    packet.insert(packet.end(), std::begin(session_id_bytes), std::end(session_id_bytes));
+    packet[UCI_PAYLOAD_LENGTH_OFFSET] = packet.size() - UCI_MSG_HDR_SIZE;
+
+    auto ret = phNxpUciHal_send_ext_cmd(packet.size(), packet.data());
+    if (ret != UWBSTATUS_SUCCESS) {
+      NXPLOG_UCIHAL_E("SessionTrack: Failed to delete URSK for session id 0x%08x",
+        session_info->session_id_);
+    }
+  }
+
+  // UCI_MSG_SESSION_STATUS_NTF rx handler
+  void OnSessionStatusNtf(size_t packet_len, const uint8_t* packet) {
+    if (packet_len != UCI_MSG_SESSION_STATUS_NTF_LENGTH) {
+      NXPLOG_UCIHAL_E("SessionTrack: SESSION_STATUS_NTF packet parse error");
+      return;
+    }
+
+    uint32_t session_handle = le_bytes_to_cpu<uint32_t>(&packet[UCI_MSG_SESSION_STATUS_NTF_HANDLE_OFFSET]);
+    uint8_t session_state = packet[UCI_MSG_SESSION_STATUS_NTF_STATE_OFFSET];
+
+    bool is_idle = false;
+    {
+      std::lock_guard<std::mutex> lock(sessions_lock_);
+
+      if (session_state == UCI_MSG_SESSION_STATE_DEINIT) {
+        NXPLOG_UCIHAL_D("SessionTrack: remove session handle 0x%08x", session_handle);
+        auto pSessionInfo = GetSessionInfo(session_handle);
+
+        if (delete_ursk_ccc_enabled_ && pSessionInfo &&
+            pSessionInfo->session_type_ == kSessionType_CCCRanging) {
+          // If this CCC ranging session, issue DELETE_URSK_CMD for this session.
+          auto msg = std::make_shared<SessionTrackMsg>(SessionTrackWorkType::DELETE_URSK, pSessionInfo, false);
+          QueueSessionTrackWork(msg);
+        }
+        sessions_.erase(session_handle);
+        is_idle = IsDeviceIdle();
+      } else {
+        NXPLOG_UCIHAL_D("SessionTrack: update session handle 0x%08x state %u", session_handle, session_state);
+        auto pSessionInfo = GetSessionInfo(session_handle);
+        if (pSessionInfo) {
+          pSessionInfo->session_state_ = session_state;
+        }
+      }
+    }
+
+    if (is_idle) { // transition to IDLE
+      NXPLOG_UCIHAL_D("Queue Idle");
+      auto msg = std::make_shared<SessionTrackMsg>(SessionTrackWorkType::IDLE, false);
+      QueueSessionTrackWork(msg);
+    }
+  }
+
+  static void IdleTimerCallback(uint32_t TimerId, void* pContext) {
+    SessionTrack *mgr = static_cast<SessionTrack*>(pContext);
+    auto msg = std::make_shared<SessionTrackMsg>(SessionTrackWorkType::IDLE_TIMER_FIRED, false);
+    mgr->QueueSessionTrackWork(msg);
+  }
+
+  void PowerIdleTimerStop() {
+    if (!auto_suspend_enabled_)
+      return;
+
+    NXPLOG_UCIHAL_D("SessionTrack: stop idle timer");
+    if (idle_timer_started_) {
+      if (phOsalUwb_Timer_Stop(idle_timer_) != UWBSTATUS_SUCCESS) {
+        NXPLOG_UCIHAL_E("SessionTrack: idle timer stop failed");
+      }
+      idle_timer_started_ = false;
+    }
+  }
+  void PowerIdleTimerRefresh() {
+    if (!auto_suspend_enabled_)
+      return;
+
+    NXPLOG_UCIHAL_D("SessionTrack: refresh idle timer, %lums", idle_timeout_ms_);
+    if (idle_timer_started_) {
+      if (phOsalUwb_Timer_Stop(idle_timer_) != UWBSTATUS_SUCCESS) {
+        NXPLOG_UCIHAL_E("SessionTrack: idle timer stop failed");
+      }
+    }
+    if (phOsalUwb_Timer_Start(idle_timer_, idle_timeout_ms_, IdleTimerCallback, this) != UWBSTATUS_SUCCESS) {
+      NXPLOG_UCIHAL_E("SessionTrack: idle timer start failed");
+    }
+    idle_timer_started_ = true;
+  }
+
+  // Worker thread for auto suspend
+  void PowerManagerWorker() {
+    NXPLOG_UCIHAL_D("SessionTrack: worker thread started.")
+
+    bool stop_thread = false;
+    while (!stop_thread) {
+      auto msg = msgq_->recv();
+      if (!msg) {
+        NXPLOG_UCIHAL_E("Power State: CRITICAL: worker thread received a bad message!, stop the queue");
+        break;
+      }
+      NXPLOG_UCIHAL_D("SessionTrack: work %d state %d",
+        static_cast<int>(msg->type_), static_cast<int>(power_state_.load()));
+
+      switch (msg->type_) {
+      case SessionTrackWorkType::IDLE:
+        if (calibration_delayed_) {
+          NXPLOG_UCIHAL_D("SessionTrack: No active session, execute per-country calibrations");
+          CONCURRENCY_LOCK();
+          apply_per_country_calibrations();
+          CONCURRENCY_UNLOCK();
+          calibration_delayed_ = false;
+        }
+        power_state_ = PowerState::IDLE;
+        PowerIdleTimerRefresh();
+        break;
+      case SessionTrackWorkType::REFRESH_IDLE:
+        if (power_state_ == PowerState::SUSPEND) {
+          NXPLOG_UCIHAL_D("SessionTrack: resume");
+          phTmlUwb_Resume();
+          power_state_ = PowerState::IDLE;
+        }
+        if (power_state_ == PowerState::IDLE) {
+          PowerIdleTimerRefresh();
+        }
+        break;
+      case SessionTrackWorkType::ACTIVATE:
+        if (power_state_ == PowerState::SUSPEND) {
+          NXPLOG_UCIHAL_E("SessionTrack: activated while in suspend!");
+          phTmlUwb_Resume();
+        }
+        PowerIdleTimerStop();
+        power_state_ = PowerState::ACTIVE;
+        break;
+      case SessionTrackWorkType::IDLE_TIMER_FIRED:
+        if (power_state_ == PowerState::IDLE) {
+          NXPLOG_UCIHAL_D("SessionTrack: idle timer expired, go suspend");
+          power_state_ = PowerState::SUSPEND;
+          phTmlUwb_Suspend();
+        } else {
+          NXPLOG_UCIHAL_E("SessionTrack: idle timer expired while in %d",
+            static_cast<int>(power_state_.load()));
+        }
+        break;
+      case SessionTrackWorkType::DELETE_URSK:
+        DeleteUrsk(msg->session_info_);
+        break;
+      case SessionTrackWorkType::STOP:
+        stop_thread = true;
+        break;
+      default:
+        NXPLOG_UCIHAL_E("SessionTrack: worker thread received a bad message!");
+        break;
+      }
+      if (msg->sync_)
+        msg->cond_.notify_one();
+    }
+    if (idle_timer_started_) {
+      PowerIdleTimerStop();
+    }
+
+    NXPLOG_UCIHAL_D("SessionTrack: worker thread exit.");
+  }
+
+  void QueueSessionTrackWork(std::shared_ptr<SessionTrackMsg> msg) {
+    msgq_->send(msg);
+
+    if (msg->sync_) {
+      std::unique_lock<std::mutex> lock(sync_mutex_);
+      if (msg->cond_.wait_for(lock, std::chrono::milliseconds(kQueueTimeoutMs)) == std::cv_status::timeout) {
+        NXPLOG_UCIHAL_E("SessionTrack: timeout to process %d", static_cast<int>(msg->type_));
+      }
+    }
+  }
+
+  std::shared_ptr<SessionInfo> GetSessionInfo(uint32_t session_handle) {
+    auto it = sessions_.find(session_handle);
+    if (it == sessions_.end()) {
+      NXPLOG_UCIHAL_E("SessionTrack: Session 0x%08x not registered", session_handle);
+      return NULL;
+    }
+    return it->second;
+  }
+
+  bool IsDeviceIdle() {
+    return sessions_.size() == 0;
+  }
+};
+
+static std::unique_ptr<SessionTrack> gSessionTrack;
+
+void SessionTrack_init()
+{
+  gSessionTrack = std::make_unique<SessionTrack>();
+}
+
+void SessionTrack_deinit()
+{
+  gSessionTrack.reset();
+}
+
+void SessionTrack_onCountryCodeChanged()
+{
+  if (gSessionTrack)
+    gSessionTrack->OnCountryCodeChanged();
+}
+
+void SessionTrack_onAppConfig(uint32_t session_handle, uint8_t channel)
+{
+  if (gSessionTrack)
+    gSessionTrack->OnChannelConfig(session_handle, channel);
+}
+
+void SessionTrack_keepAlive()
+{
+  if (gSessionTrack)
+    gSessionTrack->RefreshIdle();
+}
+
+void SessionTrack_onSessionInit(size_t packet_len, const uint8_t *packet)
+{
+  if (gSessionTrack)
+    gSessionTrack->OnSessionInit(packet_len, packet);
+}
\ No newline at end of file
diff --git a/halimpl/hal/sessionTrack.h b/halimpl/hal/sessionTrack.h
new file mode 100644
index 0000000..142fad7
--- /dev/null
+++ b/halimpl/hal/sessionTrack.h
@@ -0,0 +1,12 @@
+#ifndef _SESSIONTRACK_H_
+#define _SESSIONTRACK_H_
+
+#include <cstdint>
+
+void SessionTrack_init();
+void SessionTrack_deinit();
+void SessionTrack_onCountryCodeChanged();
+void SessionTrack_onAppConfig(uint32_t session_handle, uint8_t channel);
+void SessionTrack_keepAlive();
+void SessionTrack_onSessionInit(size_t packet_len, const uint8_t *packet);
+#endif
\ No newline at end of file
diff --git a/halimpl/utils/phNxpConfig.h b/halimpl/utils/phNxpConfig.h
index 8a5ff89..5afd9eb 100644
--- a/halimpl/utils/phNxpConfig.h
+++ b/halimpl/utils/phNxpConfig.h
@@ -77,6 +77,11 @@
 /* libuwb-countrycode.conf parameters */
 #define NAME_NXP_COUNTRY_CODE_VERSION "VERSION"
 
+#define NAME_AUTO_SUSPEND_ENABLE        "AUTO_SUSPEND_ENABLE"
+#define NAME_AUTO_SUSPEND_TIMEOUT_MS    "AUTO_SUSPEND_TIMEOUT_MS"
+
+#define NAME_DELETE_URSK_FOR_CCC_SESSION    "DELETE_URSK_FOR_CCC_SESSION"
+
 /* default configuration */
 #define default_storage_location "/data/vendor/uwb"