Lab Technical Report LAB-TR-0003

Where Android Records Who Used What: App Ops History in Databases, XML Files and Bug Reports

Daniel Addai

SectraTEK Lab, Prudential Associates

Lab Technical Report, preprint, not peer reviewed

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.

Storage forms of App Ops historyThe App Ops service appears in five places. Two history databases (short interval, one-minute windows; long interval, fifteen-minute windows) and the file appops_accesses.xml with the last access per key were present on 5 of 5 Android 16 images. The bug report text dump was present in 5 of 5 bug reports. An older XML history layout was seen on one Android 14 image and not parsed.App Ops service (system_server, AppOpsManager)app_op_history_short_interval.db1-minute windows, sensitive ops; 5 of 5 imagesapp_op_history_long_interval.db15-minute windows, other ops; 5 of 5 imagesappops_accesses.xmllast access and reject time per key; 5 of 5 imagesBug report: App Ops service dumpmodes and access lines (text); 5 of 5 reportsappops/history XML (older layout)Android 14; 1 image; not parsed
Figure 1: Where App Ops history is kept: the four forms examined and an older XML layout that was only observed. Counts give the number of images or reports in which each form was present (evidence in Table 2).
Table 1: What each storage form records about an access, from the AOSP source, and where it was present.
FormOne record isTime it givesWhat it cannot giveRetention in sourcePresent on
Short-interval databaseAggregate per uid, package, op, tag, state and flags per 1-minute windowAccess time of the first event in the windowTimes of later accesses in the window (only their count)7 days default, 30 maximum [9]5 of 5 Android 16 images
Long-interval databaseSame schema, 15-minute windowSameSameSame5 of 5 Android 16 images
Single-file database app_op_history.dbDiscrete events (source layout)Discretised access time–7 days default, 30 maximum [8]Constructed fixtures only
appops_accesses.xmlOne entry per key, last access and last rejectLast access time [6]Earlier accesses and how many there wereNo history; latest value only5 of 5 Android 16 images
Bug report dumpText lines: modes, accesses, rejects, running startsPrinted time with offset from the dump instantA stored historySnapshot at dump time5 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).

How a database row and the XML last-access time relate (schematic)Schematic after the AOSP source. A database row covers one window W (1 minute in the short-interval database, 15 minutes in the long-interval database) and stores the time of the first access in the window and an access count n. The XML file stores the last access per key. (a) With one access in the window the two times are equal. (b) With several accesses the XML time lies inside the window of a row whose count is above one. (c) An XML time with no database row starting within W before it is not explained by this mechanism.(a) One access in the window: the times are equal (matched)XML last accessdatabase access_timecount n = 1(b) Several accesses: the XML time lies inside the windowXML last accessdatabase access_timecount n > 1(c) No row starts within W before the XML time: not explainedXML last accessno database row starts in this spanone window W: 1 min (short), 15 min (long database)
Figure 2: Why an XML last-access time can differ from every database access time (schematic, after the AOSP source). A row covers one window W and stores the time of its first access and a count n; the XML file stores the last access. (a) With one access in the window the two times are equal. (b) With several, the XML time lies inside the window of a row with n > 1. (c) An XML time with no row starting within W before it is not explained by this mechanism.
t matched  ⇔  ∃ r ∈ Rk : a(r) = t(1)
w(t) = r ∈ Rk : a(r) ≤ t < a(r) + W(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.

Table 2: Evidence classes, with count, platform and use in the study.
ClassCountPlatformUsed for
Full file system images, Samsung One UI 8.x (I1 to I5)5Android 16History databases, XML cross-check
Older full file system images2Android 14 (1), older Samsung release (1)Negative evidence: no history databases
Bug reports (R1 to R5)5Samsung (2, Android 16), Huawei (Android 10), Motorola (Android 15), Transsion-brand (Android 10)Dump form
Synthetic fixtures12 + 46 testsConstructedLayouts not seen on a device, damaged files; 7 + 5 database cases, 9 bug report cases (54 archives)
AOSP source2 branchesandroid16-qpr1-release, android16-qpr2-releaseSchema, 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.

E = |{ t unmatched : count(w(t)) > 1 }| / |{ t unmatched }|(3)

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).

dump_instant(line) = printed_time(line) − printed_offset(line)(4)
spread(report) = max(dump_instant) − min(dump_instant)(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).

numeric_uid(uUaN) = U × 100000 + 10000 + N(6)
numeric_uid(uUsN) = U × 100000 + N(7)

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.

Table 3: Answers to the research questions on the evidence examined.
QuestionAnswerEvidence
RQ1 Content and reach328,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 AgreementSame 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 formInternally 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 conditions941 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.

Rows in the history databases per imageHorizontal bars per Android 16 image. Database rows with the write-ahead log applied: I1 2,940; I2 8,587; I3 286,819; I4 23,269; I5 6,904; 328,519 in total. File-only rows: I1 703; I5 238; I2, I3 and I4 0; 941 in total.0100k200k300kI12,940 (703 file only)I28,587I3286,819I423,269I56,904 (238 file only)Database rows with the log applied; linear scale
Figure 3: Rows in the history databases per Android 16 image, on a linear scale. I3 holds 87 percent of the 328,519 rows; file-only rows, 941 in all, exist on I1 (703) and I5 (238).

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.

XML last-access times against database access timesA stacked bar of the 3,180 XML last-access times that fall inside the time span of the databases of their image: 1,717 equal a database access time for the same package and op to the millisecond; 1,182 do not, but lie inside the window of a database row whose access count is above one; 281 are not explained (2 lie in a row with a count of one and 279 have no window row). 1,463 times have no equal database time.3,180 XML last-access times inside the database span1,7171,1822811,463 with no equal database timeEqual database access time (same package, op, ms)Inside the window of a row with count above 1Unexplained: 2 in a row with count 1,279 with no window row
Figure 4: The 3,180 in-span XML last-access times by how they relate to the history databases. Equality holds for about half; most of the rest lie inside a window whose row counted several accesses (Section 5.4, Table 4).

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.

Table 4: The 3,180 in-span XML last-access times by match status and by the access count of the database row of the same package and op that contains them.
XML timeTimesRow count above 1Row count 1Row count 0No window row
Matched (equal database time)1,7171431,560140
Unmatched1,4631,18220279
Table 5: Test of the first-versus-last prediction: effect, robustness and explained share.
MeasureValue
Matched, row count above 1143 of 1,703 (8.4%)
Unmatched in a window, row count above 11,182 of 1,184 (99.8%)
Difference (95% CI, Newcombe)0.914 (0.900 to 0.927)
Fisher exact test, one-sidedp < 10−300
Odds ratio (95% CI)a6,447 (1,594 to 26,083)
Per image, unmatched in a window with count above 159/59, 175/175, 575/576, 298/299, 75/75
All 3,216 records: difference0.915
All 3,216 records: Mantel-Haenszel odds ratio across images (95% CI)6,918 (1,667 to 28,715)
Leave one image out: difference0.911 to 0.920
Cluster bootstrap by image and package: difference0.899 to 0.930
Placebo: random in-span time on the same key, in a window, count above 1b81.6%
Explained share E (Equation 3)80.8% (1,182 of 1,463); per image 58.5 to 96.1%
Unexplained281 (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.

Table 6: Alternative explanations checked on all 3,216 in-span records.
CheckOutcome
Shift the 1,479 unmatched records by 56 multiples of 30 minutes, −14 to +14 hours, and by short control shiftsNo equal database time
Primary Android user only (3,070 records)Same result
Latest time per key onlySame result
Last 15 minutes of each database excluded (delayed write)Same result
Match also requires the same uid, uid state and flagsSame result
Second Android userAll 61 unmatched times in a window are in rows with count above 1
Change of the device clockNot testable; none by the Lab
Table 7: The 282 unmatched records without a window row (all 3,216 records), by the nearest database row of the same package and op.
Nearest row of the keyRecords
No row in either database72
Previous row less than 1 hour earlier81
Previous row 1 hour or more earlier85
Only later rows44
Total282

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.

Table 8: App Ops rows in five bug reports, with the uid-level mode lines read and the share of "ignore" mode.
ReportVendor family, AndroidMode rowsAccess rowsMode lines readIgnore share
R1Samsung (mid-range), 163,0012,88332,63399.7%
R2Samsung (flagship), 165,5925,72142,41999.1%
R3Huawei, 106391,14615243.4%
R4Motorola, 154,2614,12889,19999.8%
R5Transsion-brand, 107091,37312052.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.

App Ops rows per bug reportGrouped bars for five bug reports, mode rows then access rows: R1 (Samsung, Android 16) 3,001 and 2,883; R2 (Samsung, Android 16) 5,592 and 5,721; R3 (Huawei, Android 10) 639 and 1,146; R4 (Motorola, Android 15) 4,261 and 4,128; R5 (Transsion-brand, Android 10) 709 and 1,373.Mode rowsAccess rows02,0004,0006,000R1Samsung 163,0012,883R2Samsung 165,5925,721R3Huawei 106391,146R4Motorola 154,2614,128R5Transsion 107091,373Rows per table
Figure 5: Rows of the App Ops Modes and App Ops Accesses tables per bug report (values as in Table 8).

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.

Table 9: Defects found with adversarial inputs, their effect and the fix.
DefectEffectResolution
The shared routine does not catch a database errorA zeroed or encrypted file ended the whole artifact and lost the good sibling databaseError 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 fileA 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 LinuxWould break the Linux baseline for negative timesConversion 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

  1. 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
  2. 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/
  3. 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
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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/
  13. Google. [n. d.]. AppOpsManager. Android Developers, API reference. Retrieved October 27, 2026 from https://developer.android.com/reference/android/app/AppOpsManager
  14. 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.)
  15. 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.)
  16. SQLite. [n. d.]. Write-Ahead Logging. Retrieved October 27, 2026 from https://www.sqlite.org/wal.html

Cite and download

Cite as

Daniel 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

Citation guide

DOI (all versions): 10.5281/zenodo.23717924
DOI of version 1.1: 10.5281/zenodo.23717925
Archived at Zenodo: https://zenodo.org/records/23717925 (PDF and files)

lab-tr-0003.bib
@techreport{addai2026labtr0003,
  author      = {Addai, Daniel},
  title       = {Where Android Records Who Used What: App Ops History in Databases, XML Files and Bug Reports},
  institution = {SectraTEK Lab, Prudential Associates},
  type        = {Lab Technical Report},
  number      = {LAB-TR-0003},
  year        = {2026},
  doi         = {10.5281/zenodo.23717924},
  url         = {https://doi.org/10.5281/zenodo.23717924},
  howpublished = {https://lab.sectratek.com/research/reports/lab-tr-0003/},
  note        = {Version 1.2, preprint, not peer reviewed. CC BY 4.0}
}

Licence: CC BY 4.0. The print layout of this page is set for US Letter in two columns.

Versions and corrections

VersionDateChange
1.02026-10-27First release.
1.12026-10-27Added the pre-registered test of the XML and database times (now Section 5.4, Tables 4 to 7). Text and layout revised to ACM style; no result changed.
1.12026-10-27Posted on Zenodo (preprint), DOI 10.5281/zenodo.23717925.
1.22026-10-28Added the statement that the Lab did not alter any device clock and the range by which the newest database record on each of the five images precedes the end of the extraction (Section 5.4, Table 6 and Data Handling).

Corrections: none so far. Errors can be reported through the corrections page.