Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7b7ffdeb authored by Himanshu Rawat's avatar Himanshu Rawat Committed by Gerrit Code Review
Browse files

Merge changes I6e951a6b,Ie9c1eebb,Id4072f7b into main

* changes:
  Dump BTA HH connection control blocks
  Allow multiple service discoveries in HID
  Early reservation of HID control block
parents 7d167f25 b68f783e
Loading
Loading
Loading
Loading
+66 −64
Original line number Diff line number Diff line
@@ -199,22 +199,30 @@ void bta_hh_disc_cmpl(void) {
 * Returns          void
 *
 ******************************************************************************/
static void bta_hh_sdp_cback(tSDP_STATUS result, uint16_t attr_mask, tHID_DEV_SDP_INFO* sdp_rec) {
  tBTA_HH_DEV_CB* p_cb = bta_hh_cb.p_cur;
  uint8_t hdl = 0;
static void bta_hh_sdp_cback(const RawAddress& bd_addr, tSDP_STATUS result, uint16_t attr_mask,
                             tHID_DEV_SDP_INFO* sdp_rec) {
  tBTA_HH_STATUS status = BTA_HH_ERR_SDP;
  tAclLinkSpec link_spec = {
          .addrt = {.type = BLE_ADDR_PUBLIC, .bda = bd_addr},
          .transport = BT_TRANSPORT_BR_EDR,
  };
  tBTA_HH_DEV_CB* p_cb = bta_hh_find_cb(link_spec);
  if (p_cb == nullptr) {
    log::error("Unknown device {}", bd_addr);
    return;
  }

  /* make sure sdp succeeded and hh has not been disabled */
  if ((result == SDP_SUCCESS) && (p_cb != NULL)) {
  if (result == SDP_SUCCESS) {
    /* security is required for the connection, add attr_mask bit*/
    attr_mask |= HID_SEC_REQUIRED;

    log::verbose("p_cb:{} result:0x{:02x}, attr_mask:0x{:02x}, handle:0x{:x}", fmt::ptr(p_cb),
                 result, attr_mask, p_cb->hid_handle);
    log::verbose("Device:{} result:0x{:02x}, attr_mask:0x{:02x}, handle:0x{:x}", bd_addr, result,
                 attr_mask, p_cb->hid_handle);

    /* check to see type of device is supported , and should not been added
     * before */
    if (bta_hh_tod_spt(p_cb, sdp_rec->sub_class)) {
      uint8_t hdl = 0;
      /* if not added before */
      if (p_cb->hid_handle == BTA_HH_INVALID_HANDLE) {
        /*  add device/update attr_mask information */
@@ -247,14 +255,12 @@ static void bta_hh_sdp_cback(tSDP_STATUS result, uint16_t attr_mask, tHID_DEV_SD
  }

  /* free disc_db when SDP is completed */
  osi_free_and_reset((void**)&bta_hh_cb.p_disc_db);
  osi_free_and_reset((void**)&p_cb->p_disc_db);

  /* send SDP_CMPL_EVT into state machine */
  tBTA_HH_DATA bta_hh_data;
  bta_hh_data.status = status;
  bta_hh_sm_execute(p_cb, BTA_HH_SDP_CMPL_EVT, &bta_hh_data);

  return;
}
/*******************************************************************************
 *
@@ -265,12 +271,19 @@ static void bta_hh_sdp_cback(tSDP_STATUS result, uint16_t attr_mask, tHID_DEV_SD
 * Returns          void
 *
 ******************************************************************************/
static void bta_hh_di_sdp_cback(const RawAddress& /* bd_addr */, tSDP_RESULT result) {
  tBTA_HH_DEV_CB* p_cb = bta_hh_cb.p_cur;
static void bta_hh_di_sdp_cback(const RawAddress& bd_addr, tSDP_RESULT result) {
  tBTA_HH_STATUS status = BTA_HH_ERR_SDP;
  tSDP_DI_GET_RECORD di_rec;
  tHID_STATUS ret;
  log::verbose("p_cb:{} result:0x{:02x}", fmt::ptr(p_cb), result);
  tAclLinkSpec link_spec = {
          .addrt = {.type = BLE_ADDR_PUBLIC, .bda = bd_addr},
          .transport = BT_TRANSPORT_BR_EDR,
  };
  tBTA_HH_DEV_CB* p_cb = bta_hh_find_cb(link_spec);
  if (p_cb == nullptr) {
    log::error("Unknown device {}", bd_addr);
    return;
  }

  log::verbose("device:{} result:0x{:02x}", bd_addr, result);

  /* if DI record does not exist on remote device, vendor_id in
   * tBTA_HH_DEV_DSCP_INFO will be set to 0xffff and we will allow the
@@ -278,38 +291,38 @@ static void bta_hh_di_sdp_cback(const RawAddress& /* bd_addr */, tSDP_RESULT res
   * HID devices do not set this. So for IOP purposes, we allow the connection
   * to go through and update the DI record to invalid DI entry.
   */
  if (((result == SDP_SUCCESS) || (result == SDP_NO_RECS_MATCH)) && (p_cb != NULL)) {
  if (result == SDP_SUCCESS || result == SDP_NO_RECS_MATCH) {
    if (result == SDP_SUCCESS &&
        get_legacy_stack_sdp_api()->device_id.SDP_GetNumDiRecords(bta_hh_cb.p_disc_db) != 0) {
        get_legacy_stack_sdp_api()->device_id.SDP_GetNumDiRecords(p_cb->p_disc_db) != 0) {
      tSDP_DI_GET_RECORD di_rec;

      /* always update information with primary DI record */
      if (get_legacy_stack_sdp_api()->device_id.SDP_GetDiRecord(1, &di_rec, bta_hh_cb.p_disc_db) ==
      if (get_legacy_stack_sdp_api()->device_id.SDP_GetDiRecord(1, &di_rec, p_cb->p_disc_db) ==
          SDP_SUCCESS) {
        bta_hh_update_di_info(p_cb, di_rec.rec.vendor, di_rec.rec.product, di_rec.rec.version, 0,
                              0);
      }

    } else /* no DI record available */
    {
    } else /* no DI record available */ {
      bta_hh_update_di_info(p_cb, BTA_HH_VENDOR_ID_INVALID, 0, 0, 0, 0);
    }

    ret = HID_HostGetSDPRecord(p_cb->link_spec.addrt.bda, bta_hh_cb.p_disc_db,
    tHID_STATUS ret = HID_HostGetSDPRecord(p_cb->link_spec.addrt.bda, p_cb->p_disc_db,
                                           p_bta_hh_cfg->sdp_db_size, bta_hh_sdp_cback);
    if (ret == HID_SUCCESS) {
      status = BTA_HH_OK;
    } else {
      log::verbose("failure Status 0x{:2x}", ret);
      log::warn("failure Status 0x{:2x}", ret);
    }
  }

  if (status != BTA_HH_OK) {
    osi_free_and_reset((void**)&bta_hh_cb.p_disc_db);
    osi_free_and_reset((void**)&p_cb->p_disc_db);
    /* send SDP_CMPL_EVT into state machine */
    tBTA_HH_DATA bta_hh_data;
    bta_hh_data.status = status;
    bta_hh_sm_execute(p_cb, BTA_HH_SDP_CMPL_EVT, &bta_hh_data);
  }
  return;
}

/*******************************************************************************
@@ -324,35 +337,35 @@ static void bta_hh_di_sdp_cback(const RawAddress& /* bd_addr */, tSDP_RESULT res
 * Returns          void
 *
 ******************************************************************************/
static void bta_hh_start_sdp(tBTA_HH_DEV_CB* p_cb, const tBTA_HH_DATA* p_data) {
  if (!bta_hh_cb.p_disc_db) {
    bta_hh_cb.p_disc_db = (tSDP_DISCOVERY_DB*)osi_malloc(p_bta_hh_cfg->sdp_db_size);
static void bta_hh_start_sdp(tBTA_HH_DEV_CB* p_cb) {
  if (p_cb->p_disc_db != nullptr) {
    /* Incoming/outgoing collision case. DUT initiated HID connection at the
     * same time as the remote connected HID control channel.
     * When flow reaches here due to remote initiated connection, DUT may be
     * doing SDP. In such case, just do nothing and the ongoing SDP completion
     * or failure will handle this case.
     */
    log::warn("Ignoring as SDP already in progress");
    return;
  }

  p_cb->p_disc_db = (tSDP_DISCOVERY_DB*)osi_malloc(p_bta_hh_cfg->sdp_db_size);

  /* Do DI discovery first */
  if (get_legacy_stack_sdp_api()->device_id.SDP_DiDiscover(
                p_data->api_conn.link_spec.addrt.bda, bta_hh_cb.p_disc_db,
                p_bta_hh_cfg->sdp_db_size, bta_hh_di_sdp_cback) == SDP_SUCCESS) {
      /* SDP search started successfully
       * Connection will be triggered at the end of successful SDP search
       */
              p_cb->link_spec.addrt.bda, p_cb->p_disc_db, p_bta_hh_cfg->sdp_db_size,
              bta_hh_di_sdp_cback) == SDP_SUCCESS) {
    // SDP search started successfully. Connection will be triggered at the end of successful SDP
    // search
  } else {
    log::error("SDP_DiDiscover failed");

      osi_free_and_reset((void**)&bta_hh_cb.p_disc_db);
    osi_free_and_reset((void**)&p_cb->p_disc_db);

    tBTA_HH_DATA bta_hh_data;
    bta_hh_data.status = BTA_HH_ERR_SDP;
    bta_hh_sm_execute(p_cb, BTA_HH_SDP_CMPL_EVT, &bta_hh_data);
  }
  } else if (bta_hh_cb.p_disc_db) {
    /* Incoming/outgoing collision case. DUT initiated HID connection at the
     * same time as the remote connected HID control channel.
     * When flow reaches here due to remote initiated connection, DUT may be
     * doing SDP. In such case, just do nothing and the ongoing SDP completion
     * or failure will handle this case.
     */
    log::warn("Ignoring as SDP already in progress");
  }
}

/*******************************************************************************
@@ -447,12 +460,10 @@ void bta_hh_sdp_cmpl(tBTA_HH_DEV_CB* p_cb, const tBTA_HH_DATA* p_data) {
 * Returns          void
 *
 ******************************************************************************/
static void bta_hh_bredr_conn(tBTA_HH_DEV_CB* p_cb, const tBTA_HH_DATA* p_data) {
  bta_hh_cb.p_cur = p_cb;

static void bta_hh_bredr_conn(tBTA_HH_DEV_CB* p_cb) {
  /* If previously virtually cabled device */
  if (p_cb->app_id) {
    tBTA_HH_DATA bta_hh_data;
    tBTA_HH_DATA bta_hh_data = {};
    bta_hh_data.status = BTA_HH_OK;

    log::verbose("skip SDP for known devices");
@@ -461,7 +472,7 @@ static void bta_hh_bredr_conn(tBTA_HH_DEV_CB* p_cb, const tBTA_HH_DATA* p_data)
      uint8_t hdl;
      if (HID_HostAddDev(p_cb->link_spec.addrt.bda, p_cb->attr_mask, &hdl) == HID_SUCCESS) {
        /* update device CB with newly register device handle */
        bta_hh_add_device_to_list(p_cb, hdl, p_cb->attr_mask, NULL, p_cb->sub_class,
        bta_hh_add_device_to_list(p_cb, hdl, p_cb->attr_mask, nullptr, p_cb->sub_class,
                                  p_cb->dscp_info.ssr_max_latency, p_cb->dscp_info.ssr_min_tout,
                                  p_cb->app_id);
        /* update cb_index[] map */
@@ -473,7 +484,7 @@ static void bta_hh_bredr_conn(tBTA_HH_DEV_CB* p_cb, const tBTA_HH_DATA* p_data)

    bta_hh_sm_execute(p_cb, BTA_HH_SDP_CMPL_EVT, &bta_hh_data);
  } else { /* First time connection, start SDP */
    bta_hh_start_sdp(p_cb, p_data);
    bta_hh_start_sdp(p_cb);
  }
}

@@ -487,15 +498,13 @@ static void bta_hh_bredr_conn(tBTA_HH_DEV_CB* p_cb, const tBTA_HH_DATA* p_data)
 *
 ******************************************************************************/
void bta_hh_connect(tBTA_HH_DEV_CB* p_cb, const tBTA_HH_DATA* p_data) {
  p_cb->link_spec = p_data->api_conn.link_spec;
  p_cb->mode = p_data->api_conn.mode;
  bta_hh_cb.p_cur = p_cb;

  // Initiate HID host connection
  if (p_cb->link_spec.transport == BT_TRANSPORT_LE) {
    bta_hh_le_open_conn(p_cb, p_data->api_conn.link_spec);
    bta_hh_le_open_conn(p_cb);
  } else {
    bta_hh_bredr_conn(p_cb, p_data);
    bta_hh_bredr_conn(p_cb);
  }
}

@@ -601,8 +610,6 @@ void bta_hh_open_cmpl_act(tBTA_HH_DEV_CB* p_cb, const tBTA_HH_DATA* p_data) {
 *
 ******************************************************************************/
void bta_hh_open_act(tBTA_HH_DEV_CB* p_cb, const tBTA_HH_DATA* p_data) {
  tBTA_HH_API_CONN conn_data;

  uint8_t dev_handle = p_data ? (uint8_t)p_data->hid_cback.hdr.layer_specific : p_cb->hid_handle;

  log::verbose("Device[{}] connected", dev_handle);
@@ -619,13 +626,8 @@ void bta_hh_open_act(tBTA_HH_DEV_CB* p_cb, const tBTA_HH_DATA* p_data) {
    /* store the handle here in case sdp fails - need to disconnect */
    p_cb->incoming_hid_handle = dev_handle;

    memset(&conn_data, 0, sizeof(tBTA_HH_API_CONN));
    conn_data.link_spec = p_cb->link_spec;
    bta_hh_cb.p_cur = p_cb;
    bta_hh_bredr_conn(p_cb, (tBTA_HH_DATA*)&conn_data);
    bta_hh_bredr_conn(p_cb);
  }

  return;
}

/*******************************************************************************
+11 −0
Original line number Diff line number Diff line
@@ -348,3 +348,14 @@ void BTA_HhRemoveDev(uint8_t dev_handle) {

  bta_sys_sendmsg(p_buf);
}

/*******************************************************************************
 *
 * Function         BTA_HhDump
 *
 * Description      Dump BTA HH control block
 *
 * Returns          void
 *
 ******************************************************************************/
void BTA_HhDump(int fd) { bta_hh_dump(fd); }
+8 −8
Original line number Diff line number Diff line
@@ -223,6 +223,8 @@ typedef struct {
#define BTA_HH_LE_SCPS_NOTIFY_ENB 0x02
  uint8_t scps_notify; /* scan refresh supported/notification enabled */
  bool security_pending;

  tSDP_DISCOVERY_DB* p_disc_db;
} tBTA_HH_DEV_CB;

/******************************************************************************
@@ -230,15 +232,12 @@ typedef struct {
 ******************************************************************************/
typedef struct {
  tBTA_HH_DEV_CB kdev[BTA_HH_MAX_DEVICE];   /* device control block */
  tBTA_HH_DEV_CB* p_cur;                    /* current device control
                                                   block idx, used in sdp */
  uint8_t cb_index[BTA_HH_MAX_KNOWN];       /* maintain a CB index
                                          map to dev handle */
  uint8_t le_cb_index[BTA_HH_LE_MAX_KNOWN]; /* maintain a CB index map to LE dev
                                             handle */
  tGATT_IF gatt_if;
  tBTA_HH_CBACK* p_cback; /* Application callbacks */
  tSDP_DISCOVERY_DB* p_disc_db;
  uint8_t cnt_num; /* connected device number */
  bool w4_disable; /* w4 disable flag */
} tBTA_HH_CB;
@@ -252,7 +251,7 @@ extern tBTA_HH_CFG* p_bta_hh_cfg;
 *  Function prototypes
 ****************************************************************************/
bool bta_hh_hdl_event(const BT_HDR_RIGID* p_msg);
void bta_hh_sm_execute(tBTA_HH_DEV_CB* p_cb, uint16_t event, const tBTA_HH_DATA* p_data);
void bta_hh_sm_execute(tBTA_HH_DEV_CB* p_cb, tBTA_HH_INT_EVT event, const tBTA_HH_DATA* p_data);

/* action functions */
void bta_hh_api_disc_act(tBTA_HH_DEV_CB* p_cb, const tBTA_HH_DATA* p_data);
@@ -270,8 +269,9 @@ void bta_hh_open_cmpl_act(tBTA_HH_DEV_CB* p_cb, const tBTA_HH_DATA* p_data);
void bta_hh_open_failure(tBTA_HH_DEV_CB* p_cb, const tBTA_HH_DATA* p_data);

/* utility functions */
uint8_t bta_hh_find_cb(const tAclLinkSpec& link_spec);
tBTA_HH_DEV_CB* bta_hh_find_cb(const tAclLinkSpec& link_spec);
tBTA_HH_DEV_CB* bta_hh_get_cb(const tAclLinkSpec& link_spec);
tBTA_HH_DEV_CB* bta_hh_find_cb_by_handle(uint8_t hid_handle);
bool bta_hh_tod_spt(tBTA_HH_DEV_CB* p_cb, uint8_t sub_class);
void bta_hh_clean_up_kdev(tBTA_HH_DEV_CB* p_cb);

@@ -282,8 +282,6 @@ void bta_hh_update_di_info(tBTA_HH_DEV_CB* p_cb, uint16_t vendor_id, uint16_t pr
                           uint16_t version, uint8_t flag, uint8_t ctry_code);
void bta_hh_cleanup_disable(tBTA_HH_STATUS status);

uint8_t bta_hh_dev_handle_to_cb_idx(uint8_t dev_handle);

/* action functions used outside state machine */
void bta_hh_api_enable(tBTA_HH_CBACK* p_cback, bool enable_hid, bool enable_hogp);
void bta_hh_api_disable(void);
@@ -295,7 +293,7 @@ tBTA_HH_STATUS bta_hh_read_ssr_param(const tAclLinkSpec& link_spec, uint16_t* p_
/* functions for LE HID */
void bta_hh_le_enable(void);
void bta_hh_le_deregister(void);
void bta_hh_le_open_conn(tBTA_HH_DEV_CB* p_cb, const tAclLinkSpec& link_spec);
void bta_hh_le_open_conn(tBTA_HH_DEV_CB* p_cb);
void bta_hh_le_api_disc_act(tBTA_HH_DEV_CB* p_cb);
void bta_hh_le_get_dscp_act(tBTA_HH_DEV_CB* p_cb);
void bta_hh_le_write_dev_act(tBTA_HH_DEV_CB* p_cb, const tBTA_HH_DATA* p_data);
@@ -322,6 +320,8 @@ void bta_hh_headtracker_parse_service(tBTA_HH_DEV_CB* p_dev_cb, const gatt::Serv
bool bta_hh_headtracker_supported(tBTA_HH_DEV_CB* p_dev_cb);
uint16_t bta_hh_get_uuid16(tBTA_HH_DEV_CB* p_dev_cb, bluetooth::Uuid uuid);

void bta_hh_dump(int fd);

#if (BTA_HH_DEBUG == TRUE)
void bta_hh_trace_dev_db(void);
#endif
+18 −29
Original line number Diff line number Diff line
@@ -241,19 +241,16 @@ void bta_hh_le_deregister(void) { BTA_GATTC_AppDeregister(bta_hh_cb.gatt_if); }
 *
 ******************************************************************************/
static uint8_t bta_hh_le_get_le_dev_hdl(uint8_t cb_index) {
  uint8_t i;
  for (i = 0; i < ARRAY_SIZE(bta_hh_cb.le_cb_index); i++) {
  uint8_t available_handle = BTA_HH_IDX_INVALID;
  for (uint8_t i = 0; i < ARRAY_SIZE(bta_hh_cb.le_cb_index); i++) {
    if (bta_hh_cb.le_cb_index[i] == cb_index) {
      return BTA_HH_GET_LE_DEV_HDL(i);
    } else if (available_handle == BTA_HH_IDX_INVALID &&
               bta_hh_cb.le_cb_index[i] == BTA_HH_IDX_INVALID) {
      available_handle = BTA_HH_GET_LE_DEV_HDL(i);
    }
  }

  for (i = 0; i < ARRAY_SIZE(bta_hh_cb.le_cb_index); i++) {
    if (bta_hh_cb.le_cb_index[i] == BTA_HH_IDX_INVALID) {
      return BTA_HH_GET_LE_DEV_HDL(i);
    }
  }
  return BTA_HH_IDX_INVALID;
  return available_handle;
}

/*******************************************************************************
@@ -265,21 +262,17 @@ static uint8_t bta_hh_le_get_le_dev_hdl(uint8_t cb_index) {
 * Parameters:
 *
 ******************************************************************************/
void bta_hh_le_open_conn(tBTA_HH_DEV_CB* p_cb, const tAclLinkSpec& link_spec) {
  tBTA_HH_STATUS status = BTA_HH_ERR_NO_RES;

  /* update cb_index[] map */
void bta_hh_le_open_conn(tBTA_HH_DEV_CB* p_cb) {
  p_cb->hid_handle = bta_hh_le_get_le_dev_hdl(p_cb->index);
  if (p_cb->hid_handle == BTA_HH_IDX_INVALID) {
    tBTA_HH_STATUS status = BTA_HH_ERR_NO_RES;
    bta_hh_sm_execute(p_cb, BTA_HH_SDP_CMPL_EVT, (tBTA_HH_DATA*)&status);
    return;
  }

  p_cb->link_spec = link_spec;
  bta_hh_cb.le_cb_index[BTA_HH_GET_LE_CB_IDX(p_cb->hid_handle)] = p_cb->index;
  p_cb->in_use = true;
  bta_hh_cb.le_cb_index[BTA_HH_GET_LE_CB_IDX(p_cb->hid_handle)] = p_cb->index;  // Update index map

  BTA_GATTC_Open(bta_hh_cb.gatt_if, link_spec.addrt.bda, BTM_BLE_DIRECT_CONNECTION, false);
  BTA_GATTC_Open(bta_hh_cb.gatt_if, p_cb->link_spec.addrt.bda, BTM_BLE_DIRECT_CONNECTION, false);
}

/*******************************************************************************
@@ -291,15 +284,13 @@ void bta_hh_le_open_conn(tBTA_HH_DEV_CB* p_cb, const tAclLinkSpec& link_spec) {
 *
 ******************************************************************************/
static tBTA_HH_DEV_CB* bta_hh_le_find_dev_cb_by_conn_id(uint16_t conn_id) {
  uint8_t i;
  tBTA_HH_DEV_CB* p_dev_cb = &bta_hh_cb.kdev[0];

  for (i = 0; i < BTA_HH_MAX_DEVICE; i++, p_dev_cb++) {
  for (uint8_t i = 0; i < BTA_HH_MAX_DEVICE; i++) {
    tBTA_HH_DEV_CB* p_dev_cb = &bta_hh_cb.kdev[i];
    if (p_dev_cb->in_use && p_dev_cb->conn_id == conn_id) {
      return p_dev_cb;
    }
  }
  return NULL;
  return nullptr;
}

/*******************************************************************************
@@ -311,16 +302,14 @@ static tBTA_HH_DEV_CB* bta_hh_le_find_dev_cb_by_conn_id(uint16_t conn_id) {
 *
 ******************************************************************************/
static tBTA_HH_DEV_CB* bta_hh_le_find_dev_cb_by_bda(const tAclLinkSpec& link_spec) {
  uint8_t i;
  tBTA_HH_DEV_CB* p_dev_cb = &bta_hh_cb.kdev[0];

  for (i = 0; i < BTA_HH_MAX_DEVICE; i++, p_dev_cb++) {
  for (uint8_t i = 0; i < BTA_HH_MAX_DEVICE; i++) {
    tBTA_HH_DEV_CB* p_dev_cb = &bta_hh_cb.kdev[i];
    if (p_dev_cb->in_use && p_dev_cb->link_spec.addrt.bda == link_spec.addrt.bda &&
        p_dev_cb->link_spec.transport == BT_TRANSPORT_LE) {
      return p_dev_cb;
    }
  }
  return NULL;
  return nullptr;
}

/*******************************************************************************
@@ -968,9 +957,9 @@ static void bta_hh_le_encrypt_cback(RawAddress bd_addr, tBT_TRANSPORT transport,
          .transport = transport,
  };

  tBTA_HH_DEV_CB* p_dev_cb = bta_hh_get_cb(link_spec);
  tBTA_HH_DEV_CB* p_dev_cb = bta_hh_find_cb(link_spec);
  if (p_dev_cb == nullptr) {
    log::error("unexpected encryption callback, ignore");
    log::error("Unexpected encryption callback for {}", bd_addr);
    return;
  }

+227 −209

File changed.

Preview size limit exceeded, changes collapsed.

Loading