Skip to content

Add Samsung Bixby Routines Support for Samsung Android - #1157

Merged
abrignoni merged 2 commits into
abrignoni:mainfrom
Gear-I:Bixby-Routines
Aug 26, 2026
Merged

Add Samsung Bixby Routines Support for Samsung Android#1157
abrignoni merged 2 commits into
abrignoni:mainfrom
Gear-I:Bixby-Routines

Conversation

@Gear-I

@Gear-I Gear-I commented Aug 19, 2026

Copy link
Copy Markdown

Summary

Adds a new ALEAPP module, samsungBixbyRoutines.py, covering
com.samsung.android.app.routines/databases/routine.db (Samsung's Bixby
Routines automation app). No existing parser touches this database.

The module registers three artifacts, all under a new "Bixby Routines"
category:

  • Samsung Bixby Routines - User Routines - parses the routine table,
    the only table that holds user-authored automations (name, icon, favorite/
    notification flags, toggle time). Empty on the device this was tested
    against, but the module still runs and reports 0 rows rather than being
    silently absent, since a device with actively used Routines would have
    real data here.
  • Samsung Bixby Routines - Service Log - parses routine_history, an
    internal timestamped log kept by the Routines background service (job
    scheduling, metaloader/catalog-update events, widget callbacks). LEFT
    JOINs against routine to resolve a routine name if routine_id ever
    points to a real row; on the tested device every row carried routine_id = -1; so this resolves to blank there. Documented in the artifact notes
    as service telemetry, not evidence of a specific automation firing.
  • Samsung Bixby Routines - App Preferences - parses the preference
    key/value table. A fixed set of keys confirmed (by inspection) to hold
    epoch-millisecond timestamps (init_time, latest_time,
    sleep_expired_time, update_noti_time) are additionally decoded into a
    Converted Timestamp column; every other key is reported as stored, not
    guessed at.

The sibling condition and action tables were deliberately excluded —
they're Samsung's static, app-shipped catalog of available trigger/action
building blocks (same row shape on every device running the same app
build), not user data.

Testing

Verified end-to-end against a real Samsung Galaxy S21 Ultra (Android 11)
full filesystem extraction: installed ALEAPP's own requirements, ran the
unmodified aleapp.py CLI (-t fs) against the extracted routine.db,
and confirmed clean output with no errors — 0 user routines, 50 service
log rows, 15 preference rows, with timestamp columns converting correctly
(e.g. init_time → 2023-12-12 17:54:05 UTC).

Note for reviewers: this database ships with a routine.db-wal file in
real extractions. open_sqlite_db_readonly's mode=ro connection already
merges it correctly (confirmed the row counts match a manual .db + .db-wal
merge) — no special handling was needed in the module, but it's worth
keeping in mind if anyone spot-checks this artifact against a bare copy of
just the .db file, which will undercount.

Co-authored-by: Claude Sonnet 5 (Anthropic)

@abrignoni

abrignoni commented Aug 25, 2026

Copy link
Copy Markdown
Owner

Test data is now included for every changed artifact module. Thank you!

@abrignoni abrignoni added the needs-test-data Artifact PR without test data for the changed modules label Aug 25, 2026
@github-actions github-actions Bot added the fixture-needed Cites a public image; a maintainer can generate the fixture label Aug 25, 2026
@github-actions github-actions Bot removed fixture-needed Cites a public image; a maintainer can generate the fixture needs-test-data Artifact PR without test data for the changed modules labels Aug 25, 2026
@Gear-I

Gear-I commented Aug 25, 2026

Copy link
Copy Markdown
Author

Test data has been provided for the artifact module along with fixture.

@abrignoni

Copy link
Copy Markdown
Owner

Thanks for this, and thanks for including the test fixtures and a real run rather than a mock harness. Reviewed at 3d04d69. The three artifacts run clean, the case files pass the gate, lint and the claim language checker are clean, and the icons all resolve. Excluding condition and action is the right call, and I confirmed it: both hold resource ids and provider class names, no user data.

A few things to fix before this goes in.

The module reads each database once per storage spelling, not once per Android user. A full file system extraction carries the same app directory under data/data, data/user/<n> and data_mirror/data_ce/..., and the glob matches all of them. storagePathViews.unique_files(context) collapses those, and about 95 modules in the repo already use it. I built a tree with the three user 0 spellings plus a second Android user given a deliberate row delta and ran it:

artifact reported correct
Service Log 190 90
App Preferences 55 25

Swapping context.get_files_found() for unique_files(context) gives 90 and 25 with both users preserved, and the committed baselines still pass.

source_path is assigned inside the loop, so the last file wins. The rendered page said located at: data/data/.../routine.db while most of the rows came from other containers, including the second user. There is no Source Path column either, so a row cannot be attributed to a user profile. Even after the dedupe, a two user device gives two containers, so this wants '\n'.join(sources) and a Source Path column when more than one container contributes.

routine_extra is a TEXT column of the routine table, not a table. The description says the wiring "lives in routine_extra" and the notes say "the table held zero rows". There is no object by that name in sqlite_master. The trigger and action wiring is in condition_instance and action_instance, which carry routine_id, package, condition_tag/action_tag, intent_param and bundle_data. Worth correcting, because the note points an examiner away from the tables that hold the answer.

The WAL note is backwards. Measured on your fixture:

routine_history preference
.db + -wal (what the module reads) 50 15
.db alone (immutable=1) 53 16

The bare file has more rows, not fewer. routine_history is a rolling buffer: 50 rows live while the autoincrement counter had reached 6252, so old rows get trimmed. The main file still holds 6 rows that have since been trimmed and is missing the 3 newest. Reading with the log applied is the right default, but the bare file is a different row set rather than a smaller one, and that belongs in the artifact notes where an examiner sees it, not only here.

On forensic value, the tables you skipped are where the value is. Everything parsed here had rows on your device, and everything with real evidentiary weight was empty, which makes sense because that device had no routines configured. But empty is a reason to report a checked absence, not to leave the table out:

table why it matters
routine_running_history routine_id, routine_name, running_type, timestamp. Which automation fired, by name, when. The Service Log notes tell the examiner the artifact is not evidence of what ran, while the table that answers that sits unparsed in the same file.
location_history address, keyword, locality, longitude, latitude, update_time. The named places behind location triggered routines, and it supports KML.
condition_instance / action_instance what each routine is actually wired to do

Two smaller notes. if db_records is None never fires, since get_sqlite_db_records returns [] on error, so a failed query still sets source_path. And the case file uses top level os_name/os_version where the merged case files use image_info with image_name, which makes test_module.py warn when re-recording.

One thing worth keeping in the notes because it is good corroboration: update_noti_time in the preference table lands 359 ms after the newest routine_history row, so two values written by different code paths agree on when the service last ran.

I have a patch with all of the above plus the four extra artifacts, baselines re-recorded and the gate green at 7 of 7. Happy to send it over or push it to a branch, whichever is easier for you. Good work on this, the hard part is already done.

@Gear-I

Gear-I commented Aug 26, 2026

Copy link
Copy Markdown
Author

Feel free to push it to the branch. I appreciate your help with this. Thank you.

…bles

Reads each routine.db once per Android user instead of once per storage
spelling. The glob matches data/data, data/user/<n> and the data_mirror
views of the same directory, so on a full file system extraction the rows
were reported two or three times. Measured on a tree holding the three
user 0 spellings plus a second Android user: the Service Log reported 190
rows where 90 is correct, and App Preferences 55 where 25 is correct.

Source paths now name every container the rows came from, and a Source
Path column is added when more than one container contributed, so a row
on a multi-user device can be attributed to its user profile.

Adds four artifacts for tables the module did not read:

- Routine Run History (routine_running_history): a routine name and id
  against a timestamp.
- Routine Conditions (condition_instance) and Routine Actions
  (action_instance): the per-routine trigger and action wiring, joined to
  routine on routine_id.
- Saved Places (location_history): coordinates with address, locality and
  keyword, with KML output.

All four hold no rows in the tested extraction, so their sample_data
records a checked absence and their notes say the column meanings come
from the table definition rather than from decoded rows.

Corrects two statements in the artifact metadata. routine_extra is a TEXT
column of the routine table, not a separate table, and the trigger and
action wiring is in condition_instance and action_instance. The service
log notes now state that routine_history is a rolling buffer, that the
window it covers is set by service write volume rather than by the
acquisition period, and that reading the database without its write-ahead
log yields a different row set rather than a smaller one.

Undocumented integers are reported as stored. Test cases and baselines
re-recorded for all seven artifacts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@abrignoni

Copy link
Copy Markdown
Owner

I went ahead and pushed the patch to this branch as a8e2c32 rather than send it separately, so it is all in one place. Your commit is untouched underneath it.

It does the storage view collapse, joins the source paths and adds a Source Path column for multi-user devices, corrects the routine_extra and WAL statements, and adds the four artifacts for routine_running_history, condition_instance, action_instance and location_history. Those four record a checked absence for this extraction and say in their notes that the column meanings come from the table definition rather than from decoded rows. Baselines re-recorded, gate green at 7 of 7, all checks passing.

Author fields now read @Gear-I, @AlexisBrignoni, Claude. Merging.

Thanks again, this was a good find and a well-put-together first pass.

@abrignoni
abrignoni merged commit 3b70c16 into abrignoni:main Aug 26, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants