Open Source / ALEAPP #1786

Reading App Ops history databases in ALEAPP

Android records which app used the camera, microphone or location in small history databases. A report read only the last-access file. This pull request adds artifacts for the databases and for rows that sit outside the live view.

  • Daniel Addai, SectraTEK Lab
  • In review
  • CC BY 4.0

View pull request #1786 on GitHub (opens in a new tab)

Summary

Android 16 keeps a record of which app used the camera, microphone, location and other protected resources in two small SQLite databases under system/appops/. This contribution adds the first ALEAPP artifacts that read them, and a companion artifact that lists rows that exist only before the write-ahead log is applied.

  • App Ops Access History (Database) reads the app_op_accesses table with the write-ahead log applied: package, uid, Android user, op name and code, access time in UTC, process state, flags, durations and counts.
  • App Ops Access History (Database File Only) lists rows that are in the main database file but not in the log-applied view, with a Row Status column.
  • On five real extractions the first artifact read 328,519 rows, and an independent recount and a row-by-row comparison found no mismatch.
  • Status: In review. The pull request is open as a draft and no maintainer review has been posted yet.
  • 2artifacts added
  • 29files changed
  • 3,609lines added
  • 328,519rows reproduced on five images

Methodology note

Drafted with AI assistance and reviewed by the author before publication. The pull request was also prepared with AI assistance, as its description states.

The issue

An examiner is asked whether an app used the microphone during the week before a phone was seized. On Android 16 the answer is stored more than once. A small XML file, appops_accesses.xml, gives the last access time for each app and op. ALEAPP already reads it, and that file is a good place to start. But it keeps only the latest value per key. It cannot say how often, or whether there was earlier use.

The history sits in two SQLite databases, one for a short list of sensitive ops and one for the rest. Until this change no artifact read them, so the examiner saw the last time and none of the days behind it. A second gap is less obvious. These databases use write-ahead logging, so the live view of the database and the bytes of the main file can differ. Rows the device had already removed can still sit in the main file, and an examiner who opens only one file sees only one of the two.

The parser did not read these databases; now it does. The XML artifacts are unchanged, and the older history/*.xml files remain unread.

Which forms of App Ops history a report readsA table of seven App Ops files or records, with what reads each before and after the change. The XML files appops_accesses.xml and appops.xml and the discrete folder were already read and are unchanged. The two history databases and the rows kept only in the main database file had no artifact and now have two. The older history XML files are still read by no artifact.File or recordBefore this changeWith this changeappops_accesses.xmllast access per keyApp Ops Recent AccessesUnchangedappops.xmlpermission modesApp Ops Permission ModesUnchangedappops/discretediscrete native recordsDiscreteNativeUnchangedapp_op_history_short_interval.dbsystem/appops, 1-minute windowsNo artifactAccess History (Database)app_op_history_long_interval.dbsystem/appops, 15-minute windowsNo artifactAccess History (Database)rows only in the main filepresent before the -wal is appliedNo artifactDatabase File Onlyappops/history/*.xmlolder history layoutNo artifactNo artifact (not touched)
Figure 1Seven places where Android 16 records App Ops history, and what reads them. The two databases and the log-only rows are the gap this change closes. Source: PR body (What this changes); diff notes of appOpsHistoryDb.py (diagram).
Show this figure as a table
Figure 1 as a table
File or recordBeforeWith this change
appops_accesses.xmlApp Ops Recent AccessesUnchanged
appops.xmlApp Ops Permission ModesUnchanged
appops/discreteDiscreteNativeUnchanged
app_op_history_short_interval.dbNo artifactAccess History (Database)
app_op_history_long_interval.dbNo artifactAccess History (Database)
rows only in the main fileNo artifactDatabase File Only
appops/history/*.xmlNo artifactNo artifact (not touched)

The essence

The main artifact lists one report row for each row of app_op_accesses, in time order, after SQLite has applied the log. It turns the stored numbers into names an examiner can read and keeps the raw values beside them. The companion artifact reads the main file alone and reports any row that the log-applied view lacks or changes.

A row is an aggregate, not one access. By the Android source it covers a time window, holds the time of the first access in that window and counts the rest. The report keeps that meaning in the column names and notes.

How the report columns map to the stored columns
Report columnStored columnHow it is shown
Access Timeaccess_timeMilliseconds since the Unix epoch, shown in UTC; the stored value is kept in a second column
Package Name, UIDpackage_name, uidAs stored; one uid can have several packages
Android Useruiduid divided by 100000, so a second user or a Secure Folder is in the same table
Permission, Op Codeop_codeShort name from the public Android table, with the number kept
App State At Accessuid_stateNamed state, such as TOP or BACKGROUND; unknown values shown as stored
Access Flag, Attribution Flagsop_flags, attribution_flagsNames of the set bits; unknown bits as a number
Access and Total Durationaccess_duration, total_durationAs stored; -1 means no duration, 0 a start
Access Count, Reject Countaccess_count, reject_countAs stored
Intervalfile nameShort, Long, or Not aggregated, taken from the database file name

Our methodology

Find the format. We read the platform source at pinned branches to learn the file names, the table, the window lengths, the retention defaults and the meaning of each column, then compared the table definition with the databases on real images. Three layouts are told apart by file name: the two aggregated databases, and the single-file layout of the Android 16.0 release, which was built from source and tested on a constructed database only.

Check it on real data, and without our code. On five Android 16 Samsung extractions the main artifact gave 2,940, 8,587, 286,819, 23,269 and 6,904 rows, 328,519 in all. Counts made with plain sqlite3 and the log applied gave the same numbers, and a row-by-row comparison of the exported file against the source tables found no mismatch on any of the 328,519 rows. Running the reader over two copies of two images doubled every artifact exactly, as a per-root reader should. User 150 appears on three of the five images, on 777 rows.

Check the unit against a second record. Of 3,180 access times in appops_accesses.xml that fall inside the span of the database on the same image, 1,717 equal a database Access Time for the same package and op to the millisecond. That confirms the unit and the epoch. The rest are expected to differ, because the XML holds the last access and the database holds the first access of a window.

Break it on purpose. Synthetic fixtures with invented values cover empty tables, a database without the table, a zero-filled file beside a good one, random bytes, a zero-length file, a log with no shared-memory file, a log that belongs to another database, text and negative and out-of-range times, and counts of two to the fortieth power. Three defects came out of this: a database error from the shared reading routine ended the whole artifact and lost the good sibling file; sorting in SQL on the long database failed when the temporary folder was missing; and negative times converted differently on Windows and Linux. Each is fixed and has a regression case.

Compare with the base. The run-test script gave 162 passes on the branch against 150 on an unchanged copy of the base, the difference being the 12 new cases. The same 180 unit tests failed by name on both, for Windows path and permission reasons unrelated to this work.

Pipeline of the two App Ops history artifactsTwo flows. The first reads the history database files with the write-ahead log applied, queries the app_op_accesses table, decodes each row and sorts it in Python before writing 19 columns and the source file. The second reads the main database file alone, reads it again with the log, matches rows by row ID, and reports rows that are missing or different with a Row Status column.App Ops Access History (Database)Source filesshort_interval.dblong_interval.dbapp_op_history.dbOpenwrite-ahead logapplied by SQLitebad file: log, skipQueryapp_op_accesses15 columnsno ORDER BYDecodeuser = uid / 100000op code to namems to UTC, flagsReport19 columnsand Source Filesorted in PythonApp Ops Access History (Database File Only)Read the file aloneimmutable: lognot appliedRead it with the logsame query asthe artifact aboveMatch by Row IDabsent: Not in thedatabase; else DifferentReportRow Statuscolumn addedNothing is reported for a database with no -wal, or an empty one, because the two reads are then the same.
Figure 2How a database file becomes report rows. The second flow is how rows that exist only before the write-ahead log is applied are found. Source: diff, scripts/artifacts/appOpsHistoryDb.py (functions appops_history_db, appops_history_db_file_only, _read_main_file_only; diagram).
Rows read from the History databases on five imagesA horizontal bar chart on a linear scale from zero to 300,000 rows. Image 1 has 2,940 rows and 703 file-only rows. Image 2 has 8,587. Image 3 has 286,819. Image 4 has 23,269. Image 5 has 6,904 and 238 file-only rows. The five images together hold 328,519 rows.Rows per Android 16 Samsung image (log applied; linear scale)0100k200k300kImage 12,940703 file onlyImage 28,587Image 3286,819Image 423,269Image 56,904238 file onlyDatabase rows, log applied (328,519 in all)Rows only in the main file (941 in all)
Figure 3Rows read per image. One image holds most of the rows; the file-only rows appear on two images. An independent count with no ALEAPP code gave the same numbers. Source: PR body (checklist: five Android 16 Samsung images); Lab Technical Report LAB-TR-0003, Section 5.1 and Figure 3. Counts only, no synthetic values.
Show this figure as a table
Figure 3 as a table
ImageDatabase rows (log applied)File-only rows
Image 12,940703
Image 28,5870
Image 3286,8190
Image 423,2690
Image 56,904238
All328,519941

The value

An examiner can now ask a question the XML file cannot answer: in which windows did this package use this op, and how many times, back to the retention limit? On the images tested, the camera, microphone and the two location ops were in the short databases only. Of the 328,519 rows, the largest single image held 286,819.

The file-only artifact helps in a different way. On two images, 703 and 238 rows were in the main file but not in the log-applied view, and every such row was older than the oldest row left in the live view. This is consistent with a retention purge. The database does not record why a row was removed, and the notes say so. It indicates that the main file can hold history the live view no longer shows. An examiner who copies the database without its log, or the log without the database, reaches a different table from the one the device saw.

Equality between the XML time and a database time is the wrong test. The XML time should be matched to a window (Figure 4). That is how the two records are used together without reading a mismatch as a conflict.

What it does not show. A row records that the platform noted an op for a package. It does not say what the app did with it, whether anything was captured or whether the user knew. A row is an aggregate and not a count of accesses unless the count columns are read with the window length. A missing row is not evidence that an app did not use an op, because records are kept for a limited time. The observed spans differed from the Android defaults on some images, and the cause was not determined.

What is pending. The pull request is open as a draft, no maintainer review has been posted and continuous integration had not run when it was written. It lists three unverified points: other Android versions and vendors (a Pixel check would be welcome), the single-file layout of the Android 16.0 release, which is tested on a constructed database only, and the names of ops 156 to 169, which come from the Android source and were not confirmed on a device.

Why an XML time need not equal a database timeTop: a schematic window with three accesses. The database row keeps the time of the first access and a count of three; the XML file keeps the time of the last access. Bottom: a stacked bar of 3,180 XML last-access times. 1,717 equal a database time, 1,182 lie inside a window whose count is above one, and 281 are not explained.One window with three accesses (synthetic example)Aggregation window: 1 minute (short database) or 15 minutes (long database), device defaultsAccess 1database Access TimeAccess 2Access 3XML last accessAccess Count = 3only the first access has its own timeWhere 3,180 XML last-access times fall against the database (five Android 16 images)03,1801,717 equal a database Access Time for the same package and op, to the millisecond1,182 differ, but lie inside the window of a row whose Access Count is above 1281 are not explained by this mechanism
Figure 4The top panel is a schematic with invented positions. The bar is measured on five images. A time from one form should be matched to a window, not to an equal value. Source: Top: schematic after AOSP source (synthetic example). Bottom: PR body (checklist, second path) and Lab Technical Report LAB-TR-0003, Sections 5.3 and 5.4.
Show this figure as a table
Figure 4 as a table
GroupTimesShare
Equal database Access Time1,71754.0 percent
Differ, inside a window with count above 11,18237.2 percent
Not explained by this mechanism2818.8 percent
All in-span XML times3,180100 percent

Where this sits in the Lab's research

This contribution is the reader behind Lab Technical Report LAB-TR-0003, Where Android Records Who Used What: App Ops History in Databases, XML Files and Bug Reports (version 1.2, preprint, not peer reviewed; DOI 10.5281/zenodo.23720064 on Zenodo, concept DOI 10.5281/zenodo.23717924). The report relates the three forms of one access, which are the databases, the XML file and the bug report dump. It also carries the verification of the 328,519 rows, the test of why the XML and database times differ, and the three defects described above. Read it at Lab Technical Report LAB-TR-0003.

The bug report form is covered by the merged contribution for permissions and app ops in bug reports.

Sources

Sources for the numbers and claims on this page
Claim or numberWhere it comes from
Two artifacts; fields, files and path pattern readPR body (What this changes); diff, scripts/artifacts/appOpsHistoryDb.py
3,609 lines added, 0 removed, 29 files changed, 2 commits; opened 25 October 2026; draft; no reviews or commentsPR metadata (gh pr view, 5 November 2026)
328,519 rows; 2,940 / 8,587 / 286,819 / 23,269 / 6,904 per imagePR body (checklist); LAB-TR-0003 Section 5.1
File-only rows 703 and 238 (941 in all); older than the oldest live rowPR body (checklist); diff notes of appops_history_db_file_only; LAB-TR-0003 Section 5.6
No mismatch on any of the 328,519 rows; independent sqlite3 countsPR body (checklist); LAB-TR-0003 Section 5.2
Two roots double every artifact; user 150 on 3 of 5 images, 777 rowsPR body (checklist); diff notes of appops_history_db
1,717 of 3,180 XML times equal a database time; 1,182 inside a window with count above 1; 281 unexplainedPR body (checklist: 1,717 of 3,180); LAB-TR-0003 Sections 5.3 and 5.4 (1,182 and 281)
12 new unit tests, 12 new cases; PASS=162 against 150 on the base; the same 180 failing namesPR body (Test record)
Adversarial inputs and three defects foundPR body (Test record: adversarial inputs; Anything reviewers should know); LAB-TR-0003 Table 9
Pixel 8 Pro research images, Android 16 and 17; Android 14 image holds noneDiff notes of appops_history_db and appops_history_db_file_only (added after the PR body was written)
Camera, microphone and location ops in the short databases onlyDiff notes of appops_history_db
Unverified: other versions and vendors; single-file layout; op names 156 to 169PR body (checklist: unverified on real data)
LAB-TR-0003 title, version 1.2, DOIsLAB-TR-0003 front page; Lab DOI list