Android Timeline Analysis: ALEAPP, Epochs and UTC
Build an Android device timeline: every timestamp format on the phone, converting to UTC, ALEAPP and Plaso output, merging sources, and pivoting from anchors.
A phone produces tens of thousands of timestamped records a day, in at least six different formats, written by dozens of components that disagree slightly about what "now" is. A device timeline turns that into one ordered sequence of events in UTC, each traceable to its source. This guide covers the formats you will meet on Android, how to convert them, how to use ALEAPP and Plaso output, how to merge sources without losing provenance, and how to pivot from an anchor event.
Timestamp formats on Android
| Format | Example value | Where | Convert |
|---|---|---|---|
| Unix milliseconds (UTC) | 1789400592120 | SMS date, call log, WhatsApp, Signal, Google Messages, Digital Wellbeing, notification history, appops | /1000, then Unix seconds |
| Unix seconds (UTC) | 1789400592 | MMS date, Telegram, MediaStore date_added, Bluetooth Timestamp | Direct |
| Hex milliseconds | 1a0a0962ef8 | packages.xml it, ut, ft | Hex to decimal, then /1000 |
| Offset from a base | base 1789344000000 + offset 56592120 | usagestats events | Add to the file name value |
| WebKit / Chrome time | 13433874192120000 | Chrome, Samsung Internet | /1000000 - 11644473600 |
| ISO 8601 with offset | 2026-09-14T17:43:12.000+02:00 | Timeline.json | Parse with offset |
| Local wall-clock | 09-14 17:43:12.120 | logcat, battery history anchors, some dumps | Add year and zone |
| EXIF local | 2026:09:14 17:43:12 + OffsetTimeOriginal | photos | Apply offset or device zone |
| Uptime | [56592.120394] | Kernel log | Add boot time |
Two quick sanity checks catch most errors: a Unix millisecond value for 2026 has 13 digits and starts with 17; a WebKit value has 17 digits and starts with 13. Dates in 1970 mean a missing base or a seconds/milliseconds mix-up; dates in 1601 mean a WebKit value read as zero-based.
# Unix ms
python3 -c "import datetime as d; print(d.datetime.fromtimestamp(1789400592120/1000, d.UTC))"
# WebKit
python3 -c "import datetime as d; print(d.datetime(1601,1,1,tzinfo=d.UTC)+d.timedelta(microseconds=13433874192120000))"
# packages.xml hex ms
python3 -c "import datetime as d; print(d.datetime.fromtimestamp(int('1a0a0962ef8',16)/1000, d.UTC))"
-- SQLite equivalents
SELECT datetime(1789400592120/1000, 'unixepoch');
SELECT datetime(13433874192120000/1000000 - 11644473600, 'unixepoch');
DCode and CyberChef decode single values interactively when you need to confirm an unfamiliar format.
Establish the clock before anything else
Every timeline depends on knowing how the device clock relates to real time. At acquisition:
date -u +%FT%T.%NZ; adb shell date -u +%FT%T; adb shell getprop persist.sys.timezone
adb shell settings get global auto_time
adb shell settings get global auto_time_zone
- Offset. Difference between the device clock and your reference, recorded in the acquisition notes. Apply it to device-generated times if it is significant, and say so.
- Automatic time. If
auto_timewas off, the user could have set any time. Look for clock changes: battery history printsTIME:anchors after a change, and logcat and usagestats show jumps. - Time zone history. The configured zone at acquisition is not necessarily the zone at the time of the event. Travel changes it automatically. UTC-native sources are unaffected; local-time sources are.
Start with ALEAPP
ALEAPP (Android Logs Events And Protobuf Parser) processes a full file system extraction (tar, zip or folder) and produces an HTML report, TSV files per artifact and a timeline database.
python3 aleapp.py -t tar -i case0142_ffs.tar -o case0142/aleapp/
ls case0142/aleapp/ALEAPP_Reports_*/_Timeline/ # timeline database
ls case0142/aleapp/ALEAPP_Reports_*/_TSV\ Exports/
ALEAPP converts most timestamps to a readable form and keeps the source file path for each artifact. Before relying on a row:
- Check the module supports your Android and app version (ALEAPP modules are community-maintained and vary in age).
- Confirm the time zone ALEAPP used for display; set it explicitly if the tool offers an option, and keep UTC for merging.
- Spot-check critical rows against the raw database with the queries on the artifact pages.
Add Plaso for breadth
Plaso (log2timeline.py and psort.py) extracts events from many file types, including SQLite databases it recognises and file system metadata, into a single storage file. It is useful for file system times and for sources ALEAPP does not cover:
log2timeline.py --storage-file case0142.plaso case0142_ffs/
psort.py -o l2tcsv -w case0142_plaso.csv case0142.plaso
Its Android coverage is narrower than ALEAPP's, so treat it as a complement, not a replacement.
Merging sources
Whatever tools produce the rows, a merged timeline should have, for every event:
| Column | Example |
|---|---|
utc | 2026-09-14T15:43:12.120Z |
source | usagestats daily/1789344000000 |
artifact | ACTIVITY_RESUMED |
subject | com.whatsapp/.Conversation |
detail | Free text: message text, URL, file name |
original_value | 56592120 (offset) |
tool | ALEAPP 3.x module usagestats |
Keep the original value and source path: they let anyone reproduce the conversion. A spreadsheet works for a few thousand rows; for more, load the CSVs into SQLite or Timesketch, which provides search, tagging and collaborative review.
# Merge TSV exports into one SQLite table for querying
sqlite3 timeline.db <<'SQL'
CREATE TABLE ev(utc TEXT, source TEXT, artifact TEXT, subject TEXT, detail TEXT, original_value TEXT, tool TEXT);
.mode tabs
.import usage.tsv ev
.import messages.tsv ev
.import browser.tsv ev
SQL
sqlite3 timeline.db "SELECT * FROM ev WHERE utc BETWEEN '2026-09-14T15:30' AND '2026-09-14T16:00' ORDER BY utc;"
Deduplicate without losing corroboration
The same real-world action appears in several sources: a WhatsApp message sent at 17:43 local time exists in msgstore.db, maybe in the notification history of the recipient's phone, and is framed by usagestats and Digital Wellbeing foreground events and a battery +top transition. Do not delete the duplicates; group them. An event supported by three independent writers is stronger than one, and small offsets between them (milliseconds to seconds) are normal because each component timestamps at a different moment.
Pivot from anchors
Timelines are too large to read linearly. Start from an anchor and expand:
- An install. The
ittime of a suspicious package in packages.xml. Look back for the download in browser history and the Settings screens in usagestats; look forward for accessibility grants, first network activity and permission use. See the malware triage guide. - A message. A key message's send time. Look at what was on screen before and after, which notifications arrived, whether the phone was moving (location sources), whether media was created.
- A location. A Timeline visit or a photo with GPS. Look for Wi-Fi joins, Bluetooth connections, app use and communications in the same window. See the location guide.
- A power event. A boot or shutdown near the incident. Explain gaps and look for a factory reset or a forced reboot.
Work in windows (minutes around the anchor, then hours, then days), and write down each hypothesis and the rows that support or contradict it.
Converting local times in practice
Local-time sources need the zone rules for the date of the event, not today's offset. Python's zoneinfo applies daylight saving history correctly:
from datetime import datetime
from zoneinfo import ZoneInfo
tz = ZoneInfo("Europe/Paris") # from persist.sys.timezone
# logcat line "09-14 17:43:12.120" from a bugreport dated 2026
local = datetime(2026, 9, 14, 17, 43, 12, 120000, tzinfo=tz)
print(local.astimezone(ZoneInfo("UTC")).isoformat()) # 2026-09-14T15:43:12.120000+00:00
Ambiguous and missing local times exist around daylight saving changes: a local 02:30 on the night clocks go back happens twice. If an event falls in such a window, say so and give both possible UTC values unless another source resolves it.
Worked example: an install as anchor
A suspicious package is found during triage. Its it value in packages.xml converts to 2026-09-14 15:43:12 UTC. The merged timeline around it, in UTC:
| UTC | Source | Event |
|---|---|---|
| 15:40:03 | Chrome History (visits) | Visit to a file-sharing page, transition typed |
| 15:41:47 | Chrome History (downloads) | APK download completed to /sdcard/Download/ |
| 15:42:10 | usagestats | Package installer activity resumed |
| 15:43:12 | packages.xml | Package first installed, installer package installer, initiator Chrome |
| 15:44:30 | usagestats | Settings accessibility screen resumed |
| 15:45:02 | settings_secure.xml | File modified; accessibility service now enabled (from current state and file time) |
| 15:47:55 | appops | First fine location access by the package |
Each row cites its source and can be reproduced from the raw artifact. The sequence takes seven minutes and shows a deliberate, hands-on installation and configuration. Adding the lock state (a KEYGUARD_HIDDEN at 15:39) and the owner's usual pattern (the phone was normally unused at that hour) turns it into a strong narrative.
Pattern of life as context
Before calling an event unusual, establish what usual looks like: first and last use of the day, typical apps by hour, charging habits. The pattern-of-life guide shows how to derive it from usage sources. A deviation on the day of interest (the phone unused at the time it is always used, or used at 03:00 for the first time in weeks) is often the most persuasive finding in the report.
Common mistakes
- Mixing seconds and milliseconds. MMS and Telegram use seconds among millisecond-based neighbours.
- Forgetting the usagestats base. Offsets decoded without the file name base land in 1970.
- Treating local as UTC. Logcat, battery anchors, EXIF and some dumps are local.
- Ignoring clock changes. A manual clock change shifts every later local record until it is corrected.
- Over-trusting inferred events. Timeline "visits", ALEAPP labels and parsed event names are interpretations; the raw values are the evidence.
- Losing provenance. A row without its source path and original value cannot be defended.
Reporting
- State the reference time source, measured device offset, configured time zone and whether automatic time was on.
- Present times in UTC, with local time alongside where it helps the reader, and name the zone.
- Cite the source file and original value for every event relied on.
- List tool names and versions, and the manual queries used to validate key rows.
- Show corroborating sources for critical events, and explain discrepancies.
Checklist
- Record clock offset, time zone and automatic time settings at acquisition.
- Run ALEAPP on the full file system extraction; add Plaso and manual parsers for gaps.
- Convert every source to UTC with a documented method.
- Merge into one table with source, original value and tool columns.
- Group duplicates as corroboration; do not discard them.
- Pivot from anchors and compare with the device's usual pattern.
- Validate every reported event against the raw artifact.