Open Source / ALEAPP #1781

Bluetooth file transfers from btopp.db

When a phone sends or receives a file over Bluetooth, the Bluetooth app can leave a record in a small database. ALEAPP had no reader for it. This change adds one, and this post is plain about how little data we have seen in it: nearly every database we examined was empty.

  • Daniel Addai, SectraTEK Lab
  • Merged
  • Merged 25 October 2026
  • CC BY 4.0

Summary

This contribution adds Bluetooth File Transfers, an artifact that reads the btopp table of the Android Bluetooth app's btopp.db. It looks under either Bluetooth package name, reads one database per Android user, and adds a Source File column. Direction, status, visibility, confirmation and scanned are shown as stored, and the notes give what the Android source says each code means.

The honest part comes first. On the four Android 16 images the pull request was written against, btopp.db exists with its full table and holds no rows. The artifact notes in the merged change later widen the count: 40 research images carry the database, 100 copies counting storage views, second users and one rollback copy, and two copies held one row each. So the code has been run on three synthetic cases and on two real rows.

  • 1New artifact
  • 16Columns in the report
  • 3Synthetic test cases
  • 2 of 100Copies that held a row

The issue

A familiar question in an examination is whether a phone handed a file to another device nearby. Bluetooth object push is one route. The Android Bluetooth app keeps a table with one row per transfer, and until now an ALEAPP report had no entry for it. An examiner who wanted the answer opened the database by hand, or did not look.

Two traps sit in the way. The first is the path. The database lives under com.android.bluetooth on some builds and com.google.android.bluetooth on others, 14 and 26 of the 40 research images that carry it. A reader that checks one folder finds nothing on the other and says nothing. The second is that the table is usually empty. On all four Android 16 images it was present, complete and empty. An empty table is consistent with no transfer by this route, but it does not show that no file was ever sent, because Quick Share, Nearby Share and messaging apps do not write here.

Figure 1 separates what this table records from what it cannot.

Where a Bluetooth file transfer leaves a record, and where it does notTop lane: the Bluetooth app sends or receives a file, writes a row in the btopp table of btopp.db, and the Bluetooth File Transfers artifact reports it. Bottom lane: Quick Share, Nearby Share and messaging apps are not expected to write to btopp.db. Below: the three places the database was seen.A transfer by the Bluetooth appFile sent or receivedby the Bluetooth app(object push)One row in table btoppbtopp.db in encryptedstorage, one per userBluetooth File Transfers16 report columns,values as storedA transfer by another mechanismQuick Share, Nearby Share,messaging appsnot examinedNot expected in btopp.dban empty table says nothingabout theseWhere the database was seencom.android.bluetooth/databases/btopp.dbcom.google.android.bluetooth/databases/btopp.dbdata/misc_de/<user>/rollback/<id>/<package>/ (read as its own database)
Show this figure as a table
Figure 1 as a table
MechanismRecord in btopp.dbPath
Bluetooth app (object push)yes, one row per transfercom.android.bluetooth or com.google.android.bluetooth, databases/btopp.db
Quick Share, Nearby Share, messaging appsnot examined, not expectednot applicable
Rollback copyread as its own databasedata/misc_de//rollback///
Figure 1A row in this table records a transfer by the Bluetooth app only. Dashed boxes mark what the table cannot tell you. Source: ALEAPP pull request 1781, notes and paths in scripts/artifacts/bluetoothOpp.py (diagram, no values).

The essence

Bluetooth File Transfers reports one row per row of the btopp table, in each btopp.db found. Timestamp is converted to UTC with the core's unit detection; every other column is reported as stored, with the integers left as integers. The notes record what the AOSP source says the codes mean and say plainly that a device maker's build may differ, which was not examined.

Report columns and where they come from
Report columnSource columnReported as
TimestamptimestampUTC; unit detected from magnitude
ID_idas stored
Direction (as stored)directioninteger; AOSP: 0 outbound, 1 inbound
DestinationdestinationBluetooth device address as stored
Device Namedevice_nameblank where the schema has no such column
File Path, Hint, URI, MIME Type_data, hint, uri, mimetypeas stored; whether the file still exists is not checked
Status (as stored)statusinteger; AOSP: 190 pending, 192 running, 200 success, 400 to 599 errors
Total Bytes, Current Bytestotal_bytes, current_bytesas stored
Visibility, Confirm, Scanned (as stored)visibility, confirm, scannedintegers; AOSP meanings in the notes
Source Filepath of the databasenames the copy a row came from

Our methodology

We began with the schema. We listed the columns of btopp in every copy we could find and found three variants: 14 columns with user version 1 in 29 images, the same 14 plus device_name and reserved with user version 2 in 10 images, and the 14 plus carrier_name in one image. The artifact reads the columns common to the variants and leaves Device Name blank where the column is absent; carrier_name is not read.

Meaning came from the Android source. The notes cite the AOSP Bluetooth module for the direction, visibility, confirm and status codes, and for how the provider fills the timestamp when the inserting code gives none. We did not translate the codes in the report, because whether a device maker's build uses the same values was not examined. The timestamp is read with the core's magnitude-based unit detection; on the two real rows it was a 13-digit value, read as milliseconds, which is an inference from the digit count and nothing more.

For checking we leaned on invented data and on independent counts. Three synthetic cases with invented values cover an empty table, a database under the Google module path, and two Android users. The storage-view test mode passes: a duplicate view of one copy is unchanged, and a second user exactly doubles the rows. On the four Android 16 images an independent count(*) returned 0 in each, so no real row was parsed in the original work. The suite passed 135 cases with no failure, three more than before. The tool's output checker had no table to read on the real reports, so it was not applicable.

Figure 2 shows the steps. Figure 3 shows the shape of the research images.

How btopp.db is read, from file to report columnsFour steps: find btopp.db under either Bluetooth package folder; collapse duplicate storage views to one copy per Android user; read the btopp table, with absent columns reading blank; convert the timestamp and report sixteen columns.Find the filebtopp.db in theBluetooth app folderAndroid or GooglepackageCollapse viewsunique_files()one copy per user;rollback copy is itsown databaseRead the tableFROM btoppORDER BY _id15 columns; a columnthe schema lacks: blankConvert and reportTimestamp to UTC,unit from magnitudeother fields as stored,plus Source File (16)Direction, status, visibility, confirm and scanned stay integers; the notes carry the AOSP meaning of each code.Diagram of the parser steps in bluetoothOpp.py; no values are shown.
Show this figure as a table
Figure 2 as a table
StepWhat the parser does
Find the filebtopp.db under com.android.bluetooth or com.google.android.bluetooth
Collapse viewsunique_files(): one copy per Android user; a rollback copy is read as its own database
Read the tableSELECT 15 columns FROM btopp ORDER BY _id; columns the schema lacks read blank
Convert and reportTimestamp to UTC; other fields as stored; Source File added (16 columns)
Figure 2From file to report. Nothing is translated in the report; meaning lives in the artifact notes, with the AOSP source cited. Source: ALEAPP pull request 1781, scripts/artifacts/bluetoothOpp.py (diagram of the steps).
Research images by schema variant and by package folderBar chart on one scale from 0 to 30 images. Schema variants: 14 columns in 29 images, 16 columns in 10 images, 14 columns plus carrier_name in 1 image. Package folder: com.android.bluetooth in 14 images, com.google.android.bluetooth in 26 images.0102030Research images that carry btopp.db (40 in all)By schema variant14 columns, user_version 12916 columns, user_version 21014 plus carrier_name1By package foldercom.android.bluetooth14com.google.android.bluetooth26Copies that held a row: 2 of 100 (one row each).
Show this figure as a table
Figure 3 as a table
GroupingValueImages
Schema14 columns, user_version 129
Schema16 columns, user_version 2 (adds device_name, reserved)10
Schema14 columns plus carrier_name, user_version 21
Package foldercom.android.bluetooth14
Package foldercom.google.android.bluetooth26
Figure 3Schema variants and package folders across the research images. Device Name is blank where the column is absent. Counts are real aggregates, not synthetic. Source: ALEAPP pull request 1781, notes of get_bluetooth_opp in bluetoothOpp.py (validation boundary).

The value

The value today is a place to look and a decoded structure, not a stream of findings. An examiner now gets, in the report, any Bluetooth object-push record the phone kept: when the transfer was initialized, the destination address, the file name and type, the byte counts, and the direction and status codes, with the AOSP meaning in the notes. Where the table is empty the report says so, and the notes say what an empty table does not show.

The limits are firm. A row is a record in the Bluetooth app's table and is not by itself proof that a file reached the other device. Whether a device maker's build updates the timestamp later was not examined. Transfers by Quick Share, Nearby Share or messaging apps are not expected here. The pull request asks for one thing from other examiners: a populated btopp.db, from a device that has sent or received a file over Bluetooth, would close the remaining gap. Figure 4 shows how a populated row would read, with invented values.

A synthetic Bluetooth File Transfers row, with the AOSP meaning of each codeEleven report columns for one invented row, with the value as stored and, for coded fields, what the AOSP source says the number means. Synthetic example with invented values.Synthetic example row (invented values)Report columnValue as storedWhat the AOSP source says (not translated in the report)Timestamp2026-01-01 10:00:00 UTCtime the transfer was initializedDirection11 inbound, 0 outboundDestinationAA:BB:CC:00:00:02Bluetooth address of the other deviceDevice NameSynthetic Phoneblank where the schema has no columnFile Path.../Download/synthetic.jpgthe _data column; file may no longer existMIME Typeimage/jpegas storedStatus200190 pending, 192 running, 200 success, 400 to 599 errorsTotal Bytes1048576as storedCurrent Bytes1048576as storedVisibility00 visible, 1 hiddenConfirm10 pending, 1 confirmed, 2 auto-confirmed, 3 deniedA row is a record in the Bluetooth app's table. It is not by itself proof that a file reached the other device.
Show this figure as a table
Figure 4 as a table
Report columnValue as stored (synthetic)AOSP meaning
Direction1inbound (0 is outbound)
Status200success
Visibility0visible
Confirm1confirmed
Figure 4What the report holds for one row and how to read the coded fields. The report keeps the integers; the notes give the meaning. Source: synthetic example with invented values; code meanings from the artifact notes in bluetoothOpp.py, which cite the AOSP Bluetooth source.

Where this sits in the Lab's research

No Lab report is devoted to this store; the method above comes from the pull request. It appears as one of the case studies in Lab Technical Report LAB-TR-0006, Section 7.1 and Table 8, which records the Bluetooth transfer table as present and empty on all four images, with row content and timestamp unit not verified. That is the same boundary stated here, before the later corpus count.

Cite as: Addai, D. (2026). Empty Is Not Absent: Silent Failure Modes in Open-Source Mobile Forensic Parsers on iOS 26 and Android 16. LAB-TR-0006, version 1.0, preprint. doi:10.5281/zenodo.23717929.

Sources

Every number and claim on this page, and where it comes from. Figures marked synthetic example use invented values.

Sources for the numbers and claims in this post
Number or claimWhere it comes from
Merged 25 October 2026; 8 files, +416 and -0 linesPull request 1781 metadata (mergedAt, files, additions, deletions)
btopp.db present with a 16-column table, user_version 2, in all four Android 16 images; 0 rows in eachPR body, first checklist item; LAB-TR-0006 Section 7.1
40 research images, 100 copies; 14 images under com.android.bluetooth, 26 under com.google.android.bluetoothDiff, notes of get_bluetooth_opp (validation boundary)
Schema variants: 14 columns in 29 images, 16 columns in 10, 14 plus carrier_name in 1Diff, same notes
Two copies held one row each, both in the 16-column schema; 13-digit timestamps read as millisecondsDiff, same notes
Direction, visibility, confirm and status code meanings; timestamp filled with the current time by the providerDiff, notes citing the AOSP Bluetooth module (BluetoothShare.java, BluetoothOppProvider.java)
Three synthetic cases (empty, Google module path, two users); second user exactly doubled, duplicate view unchangedPR body, third checklist item and Anything reviewers should know
run_test_cases.py PASS=135, upstream PASS=132, three new cases; --storage-views PASS=163, KNOWN_FAIL=3PR body, test record table
Independent count(*) is 0 on each of four imagesPR body, first checklist item
Quick Share, Nearby Share and messaging apps not examined and not expected hereDiff, notes of get_bluetooth_opp
Figure 4 valuesSynthetic example, invented for illustration; code meanings from the notes above