Skip to content

Android: merge manufacturer data with duplicate company id (don't drop a structure)#258

Merged
navidecklabs merged 3 commits into
Navideck:mainfrom
ashekochikhin:android-manufacturer-data-merge
Jun 24, 2026
Merged

Android: merge manufacturer data with duplicate company id (don't drop a structure)#258
navidecklabs merged 3 commits into
Navideck:mainfrom
ashekochikhin:android-manufacturer-data-merge

Conversation

@ashekochikhin

Copy link
Copy Markdown
Contributor

Fixes #256.

Summary

On Android, manufacturerDataList is built from
ScanRecord.getManufacturerSpecificData() — a SparseArray<byte[]> keyed by
company id. When one advertisement carries two manufacturer-specific (0xFF) AD
structures with the same company id (e.g. split across ADV_IND and SCAN_RSP),
the framework's SparseArray.put overwrites one, so the plugin only ever surfaces
a single structure.

This parses the raw ScanRecord.getBytes() instead and concatenates every 0xFF
structure per company id
(record order), so no payload is dropped. Falls back to
the framework SparseArray parse when raw bytes are unavailable.

Changes

  • android/.../UniversalBleHelper.ktScanResult.manufacturerDataList now walks
    the raw scan-record bytes and merges same-company-id manufacturer data. No public
    API change; Android-only.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces custom parsing of raw BLE advertisement bytes in UniversalBleHelper.kt to extract and merge manufacturer-specific data by company ID, falling back to the standard API if no raw data is parsed. The reviewer recommends adding unit tests to verify this parsing logic, especially for handling duplicate company IDs, and provides a Kotlin test implementation using Mockito.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@ashekochikhin

Copy link
Copy Markdown
Contributor Author

@fotiDim could you please look at this?
Don't you think it may be included in further release?

@fotiDim

fotiDim commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

@ashekochikhin thanks for the PR. I will have a look until Monday. Please hold on.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes an Android-specific scan parsing issue where manufacturerDataList could drop one of multiple manufacturer-specific (AD type 0xFF) structures sharing the same company ID by parsing ScanRecord.getBytes() directly and concatenating payloads per company ID.

Changes:

  • Update ScanResult.manufacturerDataList on Android to parse raw scan-record bytes and concatenate all 0xFF structures per company ID (preserving record order), with a fallback to framework parsing when raw bytes are unavailable.
  • Add Kotlin unit tests covering duplicate-company-ID merging, single MSD parsing, multiple company IDs, and malformed/truncated data behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt Parse raw scan record bytes to merge duplicate company IDs instead of relying on framework SparseArray behavior.
android/src/test/kotlin/com/navideck/universal_ble/UniversalBleHelperTest.kt Add unit tests validating correct merging and parsing edge cases.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +14 to +19
private fun scanResultWithRawBytes(raw: ByteArray?): ScanResult {
val scanResult = mock(ScanResult::class.java)
val scanRecord = mock(ScanRecord::class.java)
`when`(scanResult.scanRecord).thenReturn(scanRecord)
`when`(scanRecord.bytes).thenReturn(raw)
return scanResult
@rohitsangwan01

Copy link
Copy Markdown
Contributor

Thanks for the PR! I agree that we shouldn't drop duplicate manufacturer data entries.

One thing I'd like to change though is the merging. If we concatenate payloads with the same company ID, we lose the original advertisement structure and end up creating a payload that wasn't actually sent.

Since the library API already supports multiple Manufacturer Specific Data entries in a single advertisement, could you instead return a list, one UniversalManufacturerData for each 0xFF AD structure, even if the company ID is the same? That way we preserve the advertisement exactly as it was received.

@ashekochikhin

Copy link
Copy Markdown
Contributor Author

Thanks for the PR! I agree that we shouldn't drop duplicate manufacturer data entries.

One thing I'd like to change though is the merging. If we concatenate payloads with the same company ID, we lose the original advertisement structure and end up creating a payload that wasn't actually sent.

Since the library API already supports multiple Manufacturer Specific Data entries in a single advertisement, could you instead return a list, one UniversalManufacturerData for each 0xFF AD structure, even if the company ID is the same? That way we preserve the advertisement exactly as it was received.

Yes, returning one entry per 0xFF structure is possible.

One thing to flag though: it would differ from how the Android framework itself does it. AOSP's ScanRecord.parseFromBytes() actually merges
(concatenates) manufacturer data with the same company id instead of keeping them separate. It's behind the flag scan_record_manufacturer_data_merge, made
unconditional around Jan 2025, and present in the SDK stub since android-35 / Android 15.

  case DATA_TYPE_MANUFACTURER_SPECIFIC_DATA:
      int manufacturerId = ((scanRecord[currentPos + 1] & 0xFF) << 8)
              + (scanRecord[currentPos] & 0xFF);
      byte[] manufacturerDataBytes = extractBytes(scanRecord, currentPos + 2, dataLength - 2);
      if (manufacturerData.contains(manufacturerId)) {
          byte[] firstValue = manufacturerData.get(manufacturerId);
          ByteBuffer buffer = ByteBuffer.allocate(firstValue.length + manufacturerDataBytes.length);
          buffer.put(firstValue);
          buffer.put(manufacturerDataBytes);
          manufacturerData.put(manufacturerId, buffer.array());   // ← concatenate, don't overwrite
      } else {
          manufacturerData.put(manufacturerId, manufacturerDataBytes);
      }
      break;

So on Android 15+ the platform gives one merged entry, not two. Merging here would keep us consistent with that. What do you think?

@fotiDim

fotiDim commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

In that case I think merging makes more sense. Consistency within a platform is more important than cross-platform consistency. Maybe in the future we merge iOS advertisements as well to be consistent.

@rohitsangwan01 what do you think?

@rohitsangwan01

Copy link
Copy Markdown
Contributor

@fotiDim yes in that case, i think we can merge as it is

@navidecklabs navidecklabs merged commit ce41640 into Navideck:main Jun 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Android: manufacturerDataList drops a structure when two manufacturer-specific entries share a company id

5 participants