Lab Technical Report LAB-TR-0003
Where Android Records Who Used What: App Ops History in Databases, XML Files and Bug Reports
SectraTEK Lab, Prudential Associates
research@sectratek.com
Lab Technical Report, preprint, not peer reviewed
Version 1.2 · First published 27 October 2026; this version 28 October 2026 · CC BY 4.0 · doi:10.5281/zenodo.23717924
Abstract
Android's App Ops service records which app used a protected resource, such as the camera, microphone or location, and when. On Android 16 the record exists in three forms: SQLite history databases, an XML file of last accesses, and a text dump in every bug report. Forensic writing covers mainly the XML file, and we found no description that relates the three, so their differences are unexplained. We derived what each form stores from the platform source, built a reader for the databases, and measured the forms on five Android 16 Samsung full file system images and five bug reports from four vendor families. The reader reproduced all 328,519 stored rows field by field. The forms record different moments: a database row holds the first event of a time window and a count, the XML file the last access. Only 54 percent of the XML times inside the database span equal a database time; a pre-registered test places 81 percent of the rest inside a window that counted more than one access, as the source predicts, and leaves 19 percent unexplained. A time from one form should therefore be matched to a window, not to an equal value.
CCS Concepts • Applied computing → System forensics.
Keywords Android forensics, App Ops, AppOpsManager, access history, SQLite, write-ahead log, bug report, timestamps, tool validation
ACM Reference FormatDaniel Addai. 2026. Where Android Records Who Used What: App Ops History in Databases, XML Files and Bug Reports. Lab Technical Report LAB-TR-0003, version 1.2. SectraTEK Lab, Prudential Associates. Preprint, not peer reviewed. https://lab.sectratek.com/research/reports/lab-tr-0003/ DOI: https://doi.org/10.5281/zenodo.23717924
1 Introduction
Android mediates access to sensitive resources through the App Ops service, which applications see as AppOpsManager [13]. Each protected operation, such as using the camera, recording audio or reading the location, is an op with a numeric code. For each package the service keeps a mode (allowed, ignored, foreground only and so on) and a record of accesses. For an examiner the property that matters is attribution: App Ops is one of the few places where the use of a sensor is tied to a package and a time.
A motivating example. Suppose an examiner must establish when an app last recorded audio. On an Android 16 device the answer is stored more than once (Listing 1). The XML file appops_accesses.xml gives a last-access time for the app and op. A history database gives rows that each cover a time window, with the time of the first access in that window and a count. A bug report, if one was taken, prints the access as text, relative to the moment of the dump. An examiner who looks up the XML time in the database finds an equal time for only about half of such times on the images we examined (Section 5.3). For most of the rest the database holds a row that starts seconds earlier and counts several accesses. Read alone, either file seems to contradict the other.
Listing 1: One access as three forms record it (schematic; invented package name, field names simplified, times symbolic).
appops_accesses.xml one entry per key, latest only pkg=org.example.recorder op=RECORD_AUDIO last_access = T + d app_op_history_short_interval.db, table app_op_accesses package_name=org.example.recorder op=RECORD_AUDIO access_time = T (first access in the window) access_count = n (n > 1: later accesses have no time) bug report, App Ops service dump snapshot at dump time RECORD_AUDIO Access: <printed time> (<offset before dump>)
The problem. The three forms are not copies of one record. By the platform source they store different things: an aggregate over a window that keeps the first event, the last access per key, and a snapshot printed at dump time (Section 3). We know of no description that sets the three side by side, says how they relate, or reports how far back each reaches in real images (Section 2). Without that relation the forms appear to disagree, and an examiner who reads only one may miss an access or misread a time. A reader of the databases must, in addition, survive the damaged or unreadable files that real extractions contain.
Approach. We read the forms from the Android Open Source Project (AOSP) source at pinned branches, built and verified a reader for the history databases, and measured all three forms on real evidence: five Android 16 Samsung full file system images and five bug reports from four vendor families. We then tested, with a plan written before the counts, the explanation the source gives for the difference between the XML and database times. We ask four research questions.
- RQ1 (Content and reach). What do the history databases hold, and how far back do they reach on real images?
- RQ2 (Agreement). Do the databases and the XML file agree on time and unit, and does the source explain where they differ?
- RQ3 (Dump form). What does the bug report dump record, and are its times internally consistent?
- RQ4 (Reading conditions). Which conditions (write-ahead log, damaged files, op code drift, negative times) change what a reader returns?
Contributions. This report makes four contributions.
- A side-by-side description of the App Ops storage forms, grounded in AOSP source at pinned branches, stating what each can and cannot say about one access (Sections 2 and 3).
- A measurement of the history databases on five Android 16 images: volume, time span, rows present only before the write-ahead log is applied, and a field-by-field verification of all 328,519 rows read (Sections 5.1, 5.2 and 5.6).
- A cross-check of the XML file against the databases, with a pre-registered test of the source-derived explanation for the times that do not match; it accounts for about four fifths of them (Sections 5.3 and 5.4).
- A measurement of the dump form in five bug reports, including a time-consistency check, and three defects of a shared database-reading routine found with adversarial inputs (Sections 5.5 and 5.6).
Scope. The database results hold for five Samsung One UI 8.x images on Android 16, and the dump results for five bug reports. The reference throughout is what the devices stored: agreement is measured against the stored values and between the stored forms. Section 7 states the limits. Section 2 gives the background and positions the work, Section 3 sets out what each form records, Section 4 the method, Section 5 the results and Section 6 what they mean for relating one access across the three forms.
2 Background and Related Work
This section summarises how the Android 16 source stores App Ops history and positions the work.
2.1 App Ops history in the Android 16 source
In the android16-qpr1-release branch, HistoricalRegistry persists app op history in two SQLite databases with the same schema [9]. app_op_history_short_interval.db aggregates accesses of a designated set of sensitive ops over a default window of one minute, and app_op_history_long_interval.db aggregates the remaining ops over a default window of fifteen minutes. The default retention is seven days with a maximum of thirty days, controlled by a device configuration property [9].
The table is named app_op_accesses. Its columns include the uid, package name, device id, op code, attribution tag, uid state, op flags, attribution flags, chain id, access time, access duration, total duration, access count and reject count [4]. The source documents a row as an aggregate: events that share the key fields within one time window are counted together, and the access time column holds the actual time of the first event in the window [4]. A row is therefore not one access. Writes to the database happen on a periodic job every ten minutes, and expired entries are deleted by a job every six hours [3].
2.2 The last-access file and the single-file layout
The XML form is different in kind. appops_accesses.xml is read and written through AppOpsRecentAccessPersistence and stores, per key, the last access time, last reject time and duration [6]; the class receives its file from its caller, and we know the file name from the images. A single-file layout, named app_op_history.db, is defined by DiscreteOpsDbHelper [7] for the discrete registry, whose defaults are the same seven-day cutoff and thirty-day maximum [8]. We read that layout from source and tested it only with constructed fixtures.
2.3 Write-ahead logging
SQLite databases in write-ahead log mode keep committed transactions in a separate -wal file until a checkpoint copies them into the main file [16]. An examiner who holds the main file without its log therefore sees a different database from the one the device saw. This property underlies the file-only rows of Section 5.6.
2.4 The dump form in bug reports
A bug report is generated on the device and bundles system state, including a text dump of each system service [10]. The App Ops dump prints the mode of each op per uid and package, a line for each recorded access, reject or running start with a time and an offset from the dump instant, and, on some builds, a short history table. Its content is a snapshot in the vendor's own print order, not a stored history. The dump is read here with the single-pass section reader of LAB-TR-0001 [2].
2.5 Descriptions of App Ops evidence
Practitioner writing on Android pattern of life describes appops.xml as a source that shows when apps last exercised sensitive permissions, with only the most recent access per app and permission pair retained [12]. That text names appops.xml; we did not check whether its description also covers appops_accesses.xml, the file present on the Android 16 images. A forensic textbook outlines the dump form [15].
2.6 Parsers and tool reliability
ALEAPP [11] is a widely used open-source parser for Android artifacts. At the time of this work it read the XML forms and had no reader for the history databases, and so did not relate the XML times to the database windows. Work on the reliability of forensic tools [14] motivates the verification and the adversarial inputs used here: a parser that silently drops data is a failure an examiner cannot see.
Position of this work. The AOSP source that defines the databases is public, the XML form is documented by practitioners [12], and the dump form is outlined in a textbook [15]; none of these is new. What this work adds is measurement on real images, a field-by-field verification of a reader, a cross-check between the forms with a tested explanation of their difference, and the defect findings. It is engineering and empirical description, not a new method. In a non-systematic search we found no peer-reviewed study of the history databases.
3 Three Records of One Access
This section sets out, from the source, what each form records about an access and defines the terms used in the rest of the report. Figure 1 shows where App Ops history is kept, and Table 1 compares the forms.
| Form | One record is | Time it gives | What it cannot give | Retention in source | Present on |
|---|---|---|---|---|---|
| Short-interval database | Aggregate per uid, package, op, tag, state and flags per 1-minute window | Access time of the first event in the window | Times of later accesses in the window (only their count) | 7 days default, 30 maximum [9] | 5 of 5 Android 16 images |
| Long-interval database | Same schema, 15-minute window | Same | Same | Same | 5 of 5 Android 16 images |
Single-file database app_op_history.db | Discrete events (source layout) | Discretised access time | – | 7 days default, 30 maximum [8] | Constructed fixtures only |
appops_accesses.xml | One entry per key, last access and last reject | Last access time [6] | Earlier accesses and how many there were | No history; latest value only | 5 of 5 Android 16 images |
| Bug report dump | Text lines: modes, accesses, rejects, running starts | Printed time with offset from the dump instant | A stored history | Snapshot at dump time | 5 of 5 bug reports |
3.1 Terms
A row is one record of the app_op_accesses table; it aggregates the accesses of one key in one window of length W (1 minute in the short-interval and 15 minutes in the long-interval database, the source defaults). Its access time is the time of the first event in the window and its access count the number of accesses. The last-access time is the value the XML file keeps per key. An XML time is in span if it falls inside the time span of the databases of the same image. A file-only row is a row present in the main database file but absent or different once the write-ahead log is applied. The dump instant is defined in Section 4.5.
3.2 The first-versus-last prediction
Because a row keeps the first event of its window and the XML file keeps the last access, the two times coincide only when the last access was also the first in its window, for example when the window held a single access (Figure 2). For an in-span XML time t of key k, let Rk be the database rows with the same package and op, and a(r) the access time of row r. The time is matched if a row carries it exactly (Equation 1) and unmatched otherwise. For an unmatched time, the window row is the row that starts at or before t within one window length (Equation 2).
The source thus predicts that unmatched times fall inside the window of a row whose access count is above one, while matched times mostly belong to rows with a count of one. Section 4.4 describes how we tested this prediction.
4 Method
This section describes the evidence, the reader and its checks, the test of the prediction, and the dump form.
4.1 Evidence
Table 2 lists the evidence as aggregate classes. The five Android 16 images are labelled I1 to I5 in the order the results are tabulated, and the five bug reports R1 to R5; the labels carry no meaning about acquisition, cannot be traced to a device or its owner, and do not map to each other.
| Class | Count | Platform | Used for |
|---|---|---|---|
| Full file system images, Samsung One UI 8.x (I1 to I5) | 5 | Android 16 | History databases, XML cross-check |
| Older full file system images | 2 | Android 14 (1), older Samsung release (1) | Negative evidence: no history databases |
| Bug reports (R1 to R5) | 5 | Samsung (2, Android 16), Huawei (Android 10), Motorola (Android 15), Transsion-brand (Android 10) | Dump form |
| Synthetic fixtures | 12 + 46 tests | Constructed | Layouts not seen on a device, damaged files; 7 + 5 database cases, 9 bug report cases (54 archives) |
| AOSP source | 2 branches | android16-qpr1-release, android16-qpr2-release | Schema, retention, op names |
4.2 Reading the databases
The reader accepts the three file names of Table 1, applies the write-ahead log when one belongs to the file, and reads one database per Android user. A second view, file only, lists the file-only rows. We compared the schema found in real databases with the AppOpHistoryTable source at the pinned branch [4]. Op names come from an op-name table shared across artifacts, derived from AOSP, for codes 0 to 155, plus codes 156 to 169 from the android16-qpr2-release branch of AppOpsManager.java, where the op count constant is 170 [5]. The two branches agree on codes 156 to 166. Code 96 has no name in the table we used, and codes above 169 are left unnamed.
4.3 Verification of the reader
Verification used three checks that differ in method from the reader; the author wrote all of them, with AI assistance. First, row counts produced by the reader were compared with counts from plain SQL on the same tables. Second, every row of the exported table (328,519 rows) was compared field by field with the source tables. Third, for each in-span last-access time in appops_accesses.xml, we tested whether a database row for the same package and op carried an access time equal to it to the millisecond (Equation 1); agreement on such times establishes a shared unit. Agreement in these checks shows that the reader reproduces what is stored. It does not show that the stored value is a correct account of what the user did.
4.4 Test of the first-versus-last prediction
We tested the prediction of Section 3.2 on the in-span XML times. The analysis plan and decision rule were written before the counts were computed and are kept, dated, in the Lab's record; this is a pre-registration in the Lab's record, not in an external registry. For a matched time the database row is the row with the equal access time; for an unmatched time it is the window row of Equation 2. The outcome is the access count of that row. Rows whose count is zero record rejects only and are reported separately. The explained share E is the share of unmatched times whose window row has an access count above one (Equation 3). The pre-registered unit keeps every in-span record; the paper's unit removes repeated times that occur under several uid states, and both are reported.
4.5 The dump form
For bug reports, six artifacts were built on the shared section reader, two of them for App Ops (modes, and accesses with their reject and running-start lines). Each table was compared field by field, including the source line number, with a second, separately implemented parser that uses a different method (an indentation tree); it was written by the same team and is not part of the open-source project. That gives 30 table-by-report comparisons for the six artifacts, 10 of them for the two App Ops tables. Counts of uid-level mode lines were also made with awk on the raw text. For time consistency we define the dump instant of each access line as its printed time minus its printed offset (Equation 4), and the spread of a report as the range of these instants (Equation 5).
Equations 6 and 7 map the printed uid heading to a numeric uid. They are derived, not documented by a source we fetched, and were checked against the package dump (Section 5.5).
4.6 Adversarial and synthetic inputs
A synthetic fixture generator writes databases with invented values and example package names. We built 12 unit tests, 7 synthetic cases and 5 further cases with expected outputs, including variants in which one database file was zeroed or unreadable, a log did not belong to its file, the temporary folder was missing, and times were negative.
4.7 Tools and versions
The reader was tested as one change against an unchanged base (Artifact Availability). Static checks (pylint score 10.00 and lint checks) and a test-case runner were run on the change and on the unchanged base: the change passed 162 test cases against 150 on the base, the difference being the 12 new cases. The 180 unit tests that already failed on the base were identical by name on both. The bug report readers ran in one pass over each report; a run of the six bug report permission artifacts (plus Device Facts) on the five reports together took 13 s of wall time, and the new readers added about 0.45 s to the single pass over a 214 MB, 2.2-million-line report.
4.8 Reference values
The reference throughout is what the devices stored. The real evidence consists of extractions of research devices in the Lab's research collection, in an unknown usage state, so the reference values are the stored tables themselves (for the reader), the AOSP source (for layout and defaults) and, for the cross-check, a second stored form of the same subsystem.
5 Results
The databases hold a deep but aggregated history that the reader reproduces exactly; the XML file shares their unit but records a different moment of each access; and the dump is internally consistent but narrow. Table 3 answers each research question, and the subsections give the evidence.
| Question | Answer | Evidence |
|---|---|---|
| RQ1 Content and reach | 328,519 aggregated rows on five images, spanning 3.6 to 8.8 days and 37.9 days on one image; the reader reproduced every row with 0 mismatches. | Sec. 5.1 and 5.2, Figure 3 |
| RQ2 Agreement | Same unit. 1,717 of 3,180 in-span XML times (54.0 percent) equal a database time; 1,182 of the other 1,463 (80.8 percent) lie in a window that counted more than one access, as the source predicts; 281 (19.2 percent) are unexplained. | Sec. 5.3 and 5.4, Figure 4, Tables 4 to 7 |
| RQ3 Dump form | Internally consistent: every access line resolves to one dump instant per report (spread 0.0 s). Narrow: on the Android 15 and 16 reports 99.1 to 99.8 percent of uid-level mode lines are "ignore", and the one-hour table appears in one report of five. | Sec. 5.5, Table 8, Figure 5 |
| RQ4 Reading conditions | 941 rows on two images exist only before the log is applied; three defects of a shared reading routine, one of which let a damaged file discard a good sibling database; 14 op codes are named from source only. | Sec. 5.6, Table 9 |
5.1 Volume and reach of the databases (RQ1)
With the write-ahead log applied, the short and long databases together held 328,519 rows over the five Android 16 images (Figure 3). The volume differs by two orders of magnitude between images, and a single image (I3) contributes 87 percent of all rows. The time span of the access times in a database was between 3.6 and 8.8 days, and 37.9 days on one image; the span was not tabulated per image. Uids of user 150 (a second Android user) appear on three images, 777 rows in all. The Android 14 image and the older Samsung image contained no history database; the Android 14 image carried the older XML history layout and a discrete folder instead.
5.2 The reader reproduces the stored tables (RQ1)
SQL counts equalled the reader's counts on all images, and all 328,519 exported rows equalled the source fields, with 0 mismatches. Running the reader over two image roots together doubled every artifact exactly, which is the expected behaviour for a per-root reader.
5.3 XML and database: same unit, different moments (RQ2)
Inside the time span of each image's databases, 3,180 last-access times in appops_accesses.xml were available for comparison. For 1,717 of them (54.0 percent) a database row for the same package and op carried an access time equal to the millisecond (Figure 4), so the two forms share a unit. For the remaining 1,463 (46.0 percent) no equal time was found. Section 5.4 shows that the first-versus-last mechanism accounts for 1,182 of these.
5.4 The first-versus-last explanation holds for four fifths of the rest (RQ2)
The prediction of Section 3.2 held. Table 4 gives the outcome and Table 5 the test statistics. Among the matched times with a row count of at least one, 8.4 percent (143 of 1,703) belong to a row with a count above one, which is expected when several accesses share a millisecond. Among the unmatched times that lie inside a window, 99.8 percent (1,182 of 1,184) do. The direction is the same on each of the five images, and the pre-registered analysis on all 3,216 in-span records reaches the same decision. A random time inside the same span, placed on the same key, that happens to fall in a window lands in a row with a count above one in 81.6 percent of cases, so the observed 99.8 percent is not only a property of busy keys.
| XML time | Times | Row count above 1 | Row count 1 | Row count 0 | No window row |
|---|---|---|---|---|---|
| Matched (equal database time) | 1,717 | 143 | 1,560 | 14 | 0 |
| Unmatched | 1,463 | 1,182 | 2 | 0 | 279 |
| Measure | Value |
|---|---|
| Matched, row count above 1 | 143 of 1,703 (8.4%) |
| Unmatched in a window, row count above 1 | 1,182 of 1,184 (99.8%) |
| Difference (95% CI, Newcombe) | 0.914 (0.900 to 0.927) |
| Fisher exact test, one-sided | p < 10−300 |
| Odds ratio (95% CI)a | 6,447 (1,594 to 26,083) |
| Per image, unmatched in a window with count above 1 | 59/59, 175/175, 575/576, 298/299, 75/75 |
| All 3,216 records: difference | 0.915 |
| All 3,216 records: Mantel-Haenszel odds ratio across images (95% CI) | 6,918 (1,667 to 28,715) |
| Leave one image out: difference | 0.911 to 0.920 |
| Cluster bootstrap by image and package: difference | 0.899 to 0.930 |
| Placebo: random in-span time on the same key, in a window, count above 1b | 81.6% |
| Explained share E (Equation 3) | 80.8% (1,182 of 1,463); per image 58.5 to 96.1% |
| Unexplained | 281 (19.2%): 2 in a row with count 1, 279 with no window row |
a The interval is wide because only two unmatched times sit in a row with a count of one, so the difference is the better measure. b The placebo, like the gap analysis below, was run after the main test.
What the test shows and does not show. The mechanism accounts for 1,182 of the 1,463 unmatched times, about four fifths overall (58.5 to 96.1 percent per image), and leaves 281, 19.2 percent, unexplained. The test is close to definitional: a time inside a window whose row holds a single access would be hard to produce, so the result mainly shows that no such counter-example occurs, and how large the explained share is. It does not show that the XML value is the last access, which we take from the source [6], and it does not explain the unmatched times without a window row.
Where the XML time lies in the window. The XML time is usually seconds, not minutes, after the first event of the window: the median gap is 2.7 s in the short database and 5.5 s in the long database, and 37 to 38 percent of the gaps are under one second. The last access is therefore typically not near the end of the window. This is an exploratory observation made after the main test.
Other candidate causes. We checked whether something other than the first-versus-last mechanism produces the unmatched times (Table 6). These checks use all 3,216 in-span records, of which 1,737 are matched and 1,479 unmatched; the 3,180 distinct times remove repeated times that occur under several uid states, and the conclusion is the same on both. A time-zone or unit offset is excluded, and so is a second Android user. A change of the device clock could not be tested, because the order of the row identifiers in these databases is not chronological and no independent device time was joined to the images. The Lab did not alter any device clock; the clocks are as acquired, and whether an owner changed one before the device came to the Lab is not known. On each of the five images the newest database record is dated 0.6 to 35.2 hours before the end of the extraction and none is dated after it, which agrees with a running clock at acquisition but does not exclude an earlier change. Table 7 classifies the unmatched records that have no window row by the nearest row of the same key. A delayed or unflushed write, pruning by retention, ops that are not logged to the database, and a changed key all fit these cases; none of them was tested.
| Check | Outcome |
|---|---|
| Shift the 1,479 unmatched records by 56 multiples of 30 minutes, −14 to +14 hours, and by short control shifts | No equal database time |
| Primary Android user only (3,070 records) | Same result |
| Latest time per key only | Same result |
| Last 15 minutes of each database excluded (delayed write) | Same result |
| Match also requires the same uid, uid state and flags | Same result |
| Second Android user | All 61 unmatched times in a window are in rows with count above 1 |
| Change of the device clock | Not testable; none by the Lab |
| Nearest row of the key | Records |
|---|---|
| No row in either database | 72 |
| Previous row less than 1 hour earlier | 81 |
| Previous row 1 hour or more earlier | 85 |
| Only later rows | 44 |
| Total | 282 |
5.5 The dump form in five bug reports (RQ3)
The two App Ops tables of the bug report side yielded the row counts of Table 8 and Figure 5. All 10 comparisons for these tables with the second parser were equal, as were the other 20 comparisons for the remaining four artifacts. The sums of the uid-level mode lines (the Op Count column) equalled the counts made with awk on the raw text on all five reports.
| Report | Vendor family, Android | Mode rows | Access rows | Mode lines read | Ignore share |
|---|---|---|---|---|---|
| R1 | Samsung (mid-range), 16 | 3,001 | 2,883 | 32,633 | 99.7% |
| R2 | Samsung (flagship), 16 | 5,592 | 5,721 | 42,419 | 99.1% |
| R3 | Huawei, 10 | 639 | 1,146 | 152 | 43.4% |
| R4 | Motorola, 15 | 4,261 | 4,128 | 89,199 | 99.8% |
| R5 | Transsion-brand, 10 | 709 | 1,373 | 120 | 52.5% |
Mode lines read are the uid-level lines counted by the reader and by awk. Ignore share is the share of uid-level lines whose mode is "ignore", weighted by the lines per row (the Op Count column) of the exported table.
Mode lines. On R1, R2 and R4, 99.7, 99.1 and 99.8 percent of uid-level mode lines are "ignore", one for nearly every uid. They are grouped into one row per uid and mode with the list of ops; on R1, 32,633 lines became 806 rows. Package-level op lines stay one row each. On the two Android 10 reports (R3, R5) the uid-level lines are few (152 and 120) and the ignore share is lower (43.4 and 52.5 percent), so the dominance of "ignore" is a property of the Android 15 and 16 reports in this sample.
Time consistency. For every access line, Equation 4 yields the same instant per report: the spread of Equation 5 is 0.0 s on every row of all five reports, and that instant falls inside the same second as the footer time of the App Ops section. All five reports are in a UTC+0 zone, so the converted time equals the printed time on every row; other zones and daylight-saving changes were tested on constructed text only.
Version differences. Android 10 reports print no attribution tag lines and no capability lines, and print no proxy text (146 and 134 proxy-related lines without it on R3 and R5); Android 15 and 16 print the proxy text on every such line (25, 41 and 50 lines on the three reports that have them). Uid headings of the form u0a159 occur on all reports, so the numeric uid is a derived column (Equations 6 and 7). Of 1,085 u0aN (package, uid) pairs, 1,084 agree with the app id in the package dump, and all 13 uNsN pairs agree, so the two equations hold on 1,097 of 1,098 pairs. The remaining pair is a package whose app id differs in the package list.
History table and event log. The one-hour table titled "Location, Microphone, and Camera App Ops (Last 1 Hour)" appears in one report of five (R2) and is not read. A "Uid State Changes Event Log" with about 600 timestamped lines occurs in three reports (R1, R2, R4) and is not read either. In the registry source the one-hour look-back is used in the service dump: it prints location, microphone and camera ops of the last hour, at most 500 records [9]. That matches the title of the table, but we did not confirm on the dumps that this is the code path that printed it.
5.6 Conditions that change what a reader returns (RQ4)
The write-ahead log. On I1 and I5, 703 and 238 rows (941 in all) were in the main database file but absent or different once the log was applied. Changed values were not counted separately from removed rows, and the rows were not dated against the retention window. The observation indicates that the main file can contain history that the live database no longer shows: an examiner who copies only the main file, or only the log, reaches a different table from the one the device presented.
Op codes. Codes 163 and 166 occur in the data. Their names come from the android16-qpr2-release table [5], as do the names of the other twelve codes from 156 to 169, which do not occur in our data. None of the fourteen names was confirmed on a device, so all fourteen are flagged as unconfirmed. The shared table stops at code 155, and code 96 and codes above 169 are unnamed in the table we used.
Defects. The adversarial inputs found three real defects in the way a shared database-reading routine was used (Table 9). All three were fixed, each with a regression case.
| Defect | Effect | Resolution |
|---|---|---|
| The shared routine does not catch a database error | A zeroed or encrypted file ended the whole artifact and lost the good sibling database | Error is logged and the file skipped; a log that does not belong to the file falls back to the file alone, with a log line |
| Ordering by access time on the long database uses a temporary file | A missing temporary folder made the read fail with "unable to open database file" | Ordering is done in Python after the read |
| Conversion of negative epoch values differs between Windows and Linux | Would break the Linux baseline for negative times | Conversion by epoch plus a time difference; a negative value is treated as unreadable |
6 Discussion
The results matter less as counts than as a guide to reading: each form answers a different question about the same access, and comparing them needs the right unit of comparison.
6.1 What each record can say about one access
The XML file answers when a package last used an op, as the source defines it [6], and nothing about how often. The databases answer in which windows a package used an op and how many times, back to the retention limit; each row gives the first access of its window, so later accesses in that window have a count but no time. The dump gives the state of the service at one instant, with internally consistent times and no stored history. It is present in a bug report, which can be obtained without a full file system extraction, but its content is narrower: the mode lines are dominated by "ignore" and carry little per-app information, the one-hour table was present in one report of five, and the dump instant check does not show that the printed times match another form of the same event.
Equality between XML and database times therefore finds the same access only when the last access was also the first of its window. Window containment (Equation 2) is the comparison that follows from the source. A missing equal time is thus not, by itself, a disagreement between the files: on these images it was, in about four cases of five, a later access inside an aggregated window, typically seconds after the window's first event. For the remaining times no cause is known, and the candidate causes of Section 5.4 were not tested. None of the three forms says who held the device (Section 6.2).
6.2 A row is an aggregate
A database row is an aggregate over a time window with an access count and a reject count [4]. The 328,519 rows are therefore a count of aggregates, not of accesses, and the access counts were not summed in this study (Section 5.4 uses them only to classify rows). Statements such as "the camera was used 40 times" need the count columns and the window length, and still do not show who held the phone.
6.3 Retention against the source
The source default is seven days and the maximum is thirty [9], with expired rows removed by a job every six hours [3]. Spans of 3.6 days are consistent with a short usage history or a recent reset; a span of 8.8 days is longer than the default plus the six-hour deletion interval, and 37.9 days is longer than the source maximum. The cause was not determined. Candidate explanations, none tested, are a configured retention property (the source reads one from the device configuration and applies the thirty-day maximum only on non-debuggable builds [9]), a vendor change to the registry, a clock change on the device, and rows kept in the main file past their deletion (Section 5.6). Until the cause is known the retention values are observations on five images, not a property of the platform.
6.4 The write-ahead log changes the answer
The 941 file-only rows show that the result of reading depends on whether the write-ahead log travels with the main file [16]. For acquisition this suggests preserving the -wal and -shm files together with the database; for reporting it suggests stating which view was used.
6.5 Readers must survive unreadable files
The three defects of Table 9 are of one kind: a file the tool cannot read must not remove the data of a file it can. This is the reliability concern raised for forensic tools in general [14]. We recommend that any reader of these databases log unreadable files and carry on, and that reports name the files that were skipped.
7 Threats to Validity
Internal validity. Counts were produced by the reader under study and re-derived by code that differs in method: plain SQL for the databases, and for the bug reports the second, separately implemented parser and awk counts. The author wrote the reader and these checks, with AI assistance, so independence is by method, not by person. Agreement shows that the reader does what its specification says, not that the specification matches what the operating system meant. The XML cross-check uses the same package and op keys on both sides, so a shared logging path could correlate errors. The test of Section 5.4 is close to definitional, because a row with one access cannot produce a later last-access time inside its window; it therefore shows the absence of counter-examples and the size of the explained share, not an independent confirmation that the XML value is the last access.
Construct validity. A row is a record the device stored or printed, not proof of a user action, and rows are aggregates (Section 6.2). Time values are reported as stored and converted only where the zone is printed. The window lengths in the test are the source defaults.
External validity. All five images are Samsung One UI 8.x on Android 16. No Pixel or other vendor image was available, so the layout cannot be called platform-wide. The single-file app_op_history.db layout is known from source and constructed fixtures only. The Android 11 to 14 XML history was seen on one image and was not parsed. The images are extractions of research devices in an unknown usage state, so row counts reflect usage, not system capability. Retention was measured on five images only and the cause of pruning was not analysed. The op name table depends on the pinned AOSP branches, and vendor ops may differ.
Conclusion validity. Except for the test of Section 5.4, which reports exact tests and confidence intervals on pooled and per-image counts, no statistical inference is made; percentages elsewhere describe the sample only. Intervals in Section 5.4 treat records as independent in the exact test and are cross-checked with a bootstrap by image and package.
Reproducibility. Synthetic fixtures are released with the reader. The real-data results, including the test of Section 5.4, can be checked only with comparable images (Artifact Availability).
Limitations. We did not observe the actions that produced these records, so the completeness of the databases (whether every access is recorded) is not measured, and a row is not shown to correspond to a deliberate user action. That the XML value is the last access is taken from the source. The retention and the layout may differ on other vendors and on Android 16 builds not examined. The cause of the long retention spans and of the 19.2 percent of unmatched XML times left unexplained is unknown. The one-hour table and the uid state event log of the dump are not read. The search for prior work was not systematic; it remains to be repeated against IEEE Xplore, the ACM Digital Library and ScienceDirect.
8 Conclusion and Future Work
On Android 16, App Ops records an access to a protected resource in three forms, and they do not say the same thing. We related the forms through the platform source and measured them with a verified reader. On five Android 16 Samsung images the App Ops history databases hold a substantial, aggregated history, with spans of 3.6 to 8.8 days and 37.9 days on one image, and the reader reproduces every stored row. The XML file and the databases share a time unit and only partly overlap; a pre-registered test explains four fifths of the non-overlap by the difference between the first and the last access in an aggregated window. The main database file can hold rows that the live database no longer shows, the bug report dump is internally consistent but narrow, and a reader for these databases must survive unreadable siblings. Matching a time to a window rather than to an equal value, and skipping unreadable files, apply to any reader of these forms, open-source tools included.
Future work, in order of value: (1) a second vendor: a Pixel or other non-Samsung device on Android 16, and a build with the single-file layout; (2) open questions: the retention values of Section 6.3 and the 19 percent of unmatched XML times without an explanation, for example with records whose producing accesses are known, which would also separate first from last access; (3) the older XML history of Android 11 to 14, seen on one image and not yet parsed.
Artifact Availability
The reader for the history databases is one commit on the ALEAPP [11] main branch at commit 2553b045, the unchanged base of Section 4.7, and is an open draft pull request (#1786, no review decision yet); the bug report App Ops readers are merged (pull request #1816) [1]. ALEAPP is under the MIT licence; the lint checks and the test-case runner of Section 4.7 are those of its repository. Synthetic fixtures are released with the reader. The real images and bug reports, and the second parser, are not released. The analysis script of Section 5.4 (aggregate output only) and its plan are kept in the Lab's record; the script imports the XML reader from the ALEAPP checkout and will be released with the synthetic fixtures if it is made independent of that checkout.
Data Handling Statement
Test data were of two kinds: (1) full file system extractions and Android bug reports of research devices (test devices) provided to the Lab by their owners and held in its research collection, used read-only for research with the owners' permission and never copied into any repository, fixture, paper or public file; (2) fully synthetic databases and bug report text written by the author with invented values (reserved example package names and placeholder identifiers). The seven full file system images and five bug reports in this paper are of kind (1); the Lab did not alter any device or its clock, and the extractions are as acquired. Only aggregate counts, device classes, vendor families, operating system versions and table and column names are reported. No names, numbers, accounts, serial numbers, package lists or file paths from the real data appear in this report. A leak scan compared the strings and names of every released fixture with the real sources before release. The test of Section 5.4 used the same five images, read-only, and produced aggregate counts only. No offensive technique, exploit or credential-recovery method is described.
Declaration of AI Assistance
Parts of the tooling, the data checks and the drafting of this work were produced with AI assistance (Anthropic Claude). The author reviewed and ran the results and is responsible for the content.
References
- Daniel Addai. 2026. ALEAPP pull requests #1786 (“Add App Ops access history database artifacts (Android 16)”, open draft) and #1816 (“Bug report artifacts: permissions and app ops, usage intervals, alarms and jobs, notifications”, merged). GitHub. Retrieved October 27, 2026 from https://github.com/abrignoni/ALEAPP/pull/1786 and https://github.com/abrignoni/ALEAPP/pull/1816
- Daniel Addai. 2026. Making Silent Failures Visible: A Single-Pass Section Reader for Android Bug Reports. Lab Technical Report LAB-TR-0001, version 1.0. SectraTEK Lab, Prudential Associates. Preprint, not peer reviewed. https://lab.sectratek.com/research/reports/lab-tr-0001/
- Android Open Source Project. [n. d.]. AppOpHistoryHelper.java. Branch android16-qpr1-release, platform/frameworks/base. Retrieved October 27, 2026 from https://android.googlesource.com/platform/frameworks/base/+/refs/heads/android16-qpr1-release/services/core/java/com/android/server/appop/AppOpHistoryHelper.java
- Android Open Source Project. [n. d.]. AppOpHistoryTable.java. Branch android16-qpr1-release, platform/frameworks/base. Retrieved October 27, 2026 from https://android.googlesource.com/platform/frameworks/base/+/refs/heads/android16-qpr1-release/services/core/java/com/android/server/appop/AppOpHistoryTable.java
- Android Open Source Project. [n. d.]. AppOpsManager.java. Branch android16-qpr2-release, platform/frameworks/base. Retrieved October 27, 2026 from https://android.googlesource.com/platform/frameworks/base/+/refs/heads/android16-qpr2-release/core/java/android/app/AppOpsManager.java
- Android Open Source Project. [n. d.]. AppOpsRecentAccessPersistence.java. Branch android16-qpr1-release, platform/frameworks/base. Retrieved October 27, 2026 from https://android.googlesource.com/platform/frameworks/base/+/refs/heads/android16-qpr1-release/services/core/java/com/android/server/appop/AppOpsRecentAccessPersistence.java
- Android Open Source Project. [n. d.]. DiscreteOpsDbHelper.java. Branch android16-qpr1-release, platform/frameworks/base. Retrieved October 27, 2026 from https://android.googlesource.com/platform/frameworks/base/+/refs/heads/android16-qpr1-release/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java
- Android Open Source Project. [n. d.]. DiscreteOpsRegistry.java. Branch android16-qpr1-release, platform/frameworks/base. Retrieved October 27, 2026 from https://android.googlesource.com/platform/frameworks/base/+/refs/heads/android16-qpr1-release/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java
- Android Open Source Project. [n. d.]. HistoricalRegistry.java. Branch android16-qpr1-release, platform/frameworks/base. Retrieved October 27, 2026 from https://android.googlesource.com/platform/frameworks/base/+/refs/heads/android16-qpr1-release/services/core/java/com/android/server/appop/HistoricalRegistry.java
- Android Open Source Project. [n. d.]. Read bug reports. Retrieved October 27, 2026 from https://source.android.com/docs/core/tests/debug/read-bug-reports
- Alexis Brignoni et al. [n. d.]. ALEAPP: Android Logs Events And Protobuf Parser. GitHub repository (MIT licence). Retrieved October 27, 2026 from https://github.com/abrignoni/ALEAPP
- Andrea Fortuna. 2026. Android pattern of life: hidden artifacts that reconstruct a user's daily routine. Blog post, April 23, 2026. Retrieved October 27, 2026 from https://andreafortuna.org/2026/04/23/android-pattern-of-life-hidden-artifacts-reconstruct-daily-routine/
- Google. [n. d.]. AppOpsManager. Android Developers, API reference. Retrieved October 27, 2026 from https://developer.android.com/reference/android/app/AppOpsManager
- Graeme Horsman. 2019. Tool testing and reliability issues in the field of digital forensics. Digital Investigation 28 (2019), 163–175. https://doi.org/10.1016/j.diin.2019.01.009 (Bibliographic data confirmed from search results; full text not re-read.)
- Oleg Skulkin, Donnie Tindall, and Rohit Tamma. 2018. Learning Android Forensics (2nd ed.). Packt Publishing. (Not re-verified: authors, edition and year from search results; the publisher page returned an access error and the App Ops section was seen only as a preview.)
- SQLite. [n. d.]. Write-Ahead Logging. Retrieved October 27, 2026 from https://www.sqlite.org/wal.html