Skip to content

fix: use Build.DEVICE instead of Build.MODEL for hostname fallback - #183

Merged
ErikBjare merged 4 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/use-build-device-for-hostname
Jul 12, 2026
Merged

ErikBjare merged 4 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/use-build-device-for-hostname

Conversation

@TimeToBuildBob

Copy link
Copy Markdown
Contributor

Problem

getDeviceName() in RustInterface.kt and SyncInterface.kt falls back to android.os.Build.MODEL when Settings.Global.DEVICE_NAME is null. Build.MODEL returns the OEM marketing name (e.g. "Poco F8 Ultra") which contains spaces — that breaks hostname-based bucket matching in aw-webui and aw-server.

Reported in #176: a Xiaomi Poco F8 Ultra produced "Poco F8 Ultra" as its hostname. The webui then fails to associate it with the Android bucket view because the hostname contains spaces.

Fix

Replace the Build.MODEL fallback with Build.DEVICE, which returns the device codename (poco_f8_ultra — lowercase, underscore-delimited, no spaces). This is the correct format for a network hostname.

Property Example value Hostname-safe?
Settings.Global.DEVICE_NAME "Poco F8 Ultra" ❌ (spaces)
Build.MODEL "Poco F8 Ultra" ❌ (spaces)
Build.DEVICE "poco_f8_ultra" ✅

Changes

  • RustInterface.kt: Build.MODEL → Build.DEVICE
  • SyncInterface.kt: Build.MODEL → Build.DEVICE

Closes #176 (contributes — the hostname fix is one part of the broader stability tracker).

Build.MODEL returns the OEM marketing name (e.g. 'Poco F8 Ultra') which
contains spaces that break hostname-based bucket matching in aw-webui and
aw-server. Build.DEVICE returns the device codename (e.g. 'poco_f8_ultra')
which is lower-case, underscore-delimited, and safe to use as a hostname.

Fixes the class of bug reported in ActivityWatch#176 where
Xiaomi/Poco devices produced space-containing hostnames.
@greptile-apps

greptile-apps Bot commented Jul 12, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR makes Android hostname fallback and normalization safer. The main changes are:

  • Use Build.DEVICE instead of Build.MODEL when no configured device name is available.
  • Ignore blank configured device names before falling back.
  • Normalize hostnames to lowercase safe characters in both Rust and sync interfaces.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
mobile/src/main/java/net/activitywatch/android/RustInterface.kt Updates local bucket hostname selection to skip blank names and apply strict normalization.
mobile/src/main/java/net/activitywatch/android/SyncInterface.kt Applies the same normalized hostname selection before passing the value into sync calls.

Reviews (4): Last reviewed commit: "fix: filter blank DEVICE_NAME before fal..." | Re-trigger Greptile

Comment thread mobile/src/main/java/net/activitywatch/android/RustInterface.kt Outdated
Comment thread mobile/src/main/java/net/activitywatch/android/SyncInterface.kt Outdated
Comment thread mobile/src/main/java/net/activitywatch/android/RustInterface.kt Outdated
Comment thread mobile/src/main/java/net/activitywatch/android/SyncInterface.kt Outdated
Settings.Global.DEVICE_NAME can itself contain spaces (e.g. 'Poco F8 Ultra')
so the previous fix only addressed the fallback path. Sanitize the final
hostname by trimming, lowercasing, and replacing spaces with underscores,
ensuring bucket hostname matching works even when DEVICE_NAME is configured
with a human-readable name.
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

Comment thread mobile/src/main/java/net/activitywatch/android/RustInterface.kt Outdated
Comment thread mobile/src/main/java/net/activitywatch/android/SyncInterface.kt Outdated
Use strict regex [^a-z0-9_-]+ instead of only space replacement, so
configured device names with quotes or other special chars don't break
bucket JSON. Also guard whitespace-only DEVICE_NAME with takeIf so it
falls through to Build.DEVICE rather than resolving to empty string.
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

Comment thread mobile/src/main/java/net/activitywatch/android/RustInterface.kt
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

CI-green and mergeable (Greptile 5/5) — waiting only on a maintainer click.

This PR is ready to merge, but the bot has pull-only access to this repo and can't self-merge — surfacing it here so it isn't lost. The monitoring loop will stop re-flagging it now that this note is posted.

@ErikBjare
ErikBjare merged commit 39e840f into ActivityWatch:master Jul 12, 2026
8 checks passed
ErikBjare pushed a commit that referenced this pull request Sep 17, 2026
* fix(android): migrate unsanitized hostname after sanitizer change

Sanitizing Settings.Global.DEVICE_NAME (PR #183) forked already-syncing
devices onto a new sync folder without moving the old one, and
migrateHostname only rewrote unknown/Unknown so buckets kept names like
"POCO F8 Ultra". Rewrite local bucket hostnames before the datastore
opens, rename or drop the leftover folder for this device_id, and delete
the matching stale SAF tree so Syncthing stops replicating the fork.

Fixes #272

Git-Session-Id: 85705cb4-3c19-5fef-94f7-5b17e26c12ef

* fix(android): harden sanitized-hostname migration edge cases

Review follow-up on #273:

- BackgroundService: queue the bucket-hostname rewrite behind serverStarted
  instead of skipping silently, so a failed first attempt still converges
  instead of skipping forever on every later start.
- planFolderMigration: without a local device id, only fold a single
  unambiguous legacy candidate; never rename every legacy dir (could fold
  another device's data under the current hostname).
- SyncInterface SAF cleanup: never delete a legacy hostname dir when the
  local device id is unavailable; deletion stays scoped to the local
  device's subdirectory otherwise.

Git-Session-Id: 5bb5a763-f523-55ef-b462-cd8f5b49e049

* fix(android): run stale SAF hostname cleanup after a successful mirror

Deleting the legacy hostname tree before mirroring let the mirror re-copy
a leftover local legacy folder into the SAF tree, so the stale fork
persisted; a cancelled/partial mirror could also leave SAF without data
the local copy still holds. Cleanup now runs only after the mirror
completes fully, and the local legacy folder is removed from SAF on each
run after being mirrored.

Git-Session-Id: f29f34fb-edc6-5e99-ad09-d77623ffdedd

* fix(android): address Greptile P1/P2 on sanitized-hostname migration

- Run migrateSanitizedHostnameIdentity and the sequenced server start on
  Dispatchers.IO (serialized across overlapping onStartCommand) so the
  sync-tree traversal and sqlite.db write transaction cannot ANR service
  startup.
- Scope the bucket-hostname rewrite to locally-owned buckets with
  'id NOT LIKE %-synced-from-%' (reserved remote-origin marker in aw-sync)
  so a synced peer's colliding hostname is never relabeled.
- Guard the deferred rewrite so repeated starts while the server is
  running queue at most one polling thread per process.

Git-Session-Id: cca8bd5d-395c-5568-905c-c44546218e4d

* fix(android): serialize server start and deferred rewrite on the migration lock

Server start now runs under sanitizedMigrationLock so overlapping
onStartCommand invocations and the deferred rewrite thread are mutually
exclusive with the server opening sqlite.db. The queued rewrite re-checks
serverStarted under the lock before writing.

Git-Session-Id: cca8bd5d-395c-5568-905c-c44546218e4d

* fix(android): address AI-review P1s on hostname migration

- SanitizedHostnameMigration: never wholesale-rename a no-device-id legacy
  dir that holds multiple device-id subdirs (could fold other devices'
  data under the current hostname)
- SyncInterface: defer legacy-folder migration out of init to the first
  sync operation (init runs on the main thread; migration can ANR)
- BackgroundService: drop the 60s bound on the deferred bucket rewrite —
  a long-running server would time out and the rewrite would never
  converge; poll until the server task exits instead

Git-Session-Id: 50ca7f02-5783-539e-b180-594f5d69e18e

* fix(android): retry folder migration on partial failure; stop rewrite poll at teardown

- migrateSyncFolders returns MigrationResult(moved, failed); SyncInterface
  records migration complete only when failed == 0, so a failed rename or
  deletion is retried on the next sync instead of skipped.
- Deferred bucket-hostname rewrite guard and thread reference moved to the
  companion object (recreated service instances shared one guard), and
  onDestroy interrupts the polling thread so teardown does not leak it.

Git-Session-Id: 7a94d0aa-ed38-5149-9c62-bf4e3a65eb11

* fix(android): serialize folder migration process-wide

BackgroundService (service start) and SyncInterface (first sync) both run
SanitizedHostnameMigration.migrateSyncFolders; without a shared lock they
could plan from and mutate the same directories concurrently.

Git-Session-Id: 7a94d0aa-ed38-5149-9c62-bf4e3a65eb11

* fix(android): make sanitizedMigrationLock static

The deferred rewrite thread outlives the service instance that queued it;
an instance-level lock let a recreated instance's server start open the
database while the old thread was still rewriting it.

Git-Session-Id: 7a94d0aa-ed38-5149-9c62-bf4e3a65eb11

* fix(android): address Greptile P2s on hostname migration

- BackgroundService: assign hostnameRewriteThread so cancelQueuedHostnameRewrite
  can actually interrupt the polling thread (was always a no-op: Thread{} created
  but reference never stored)
- BackgroundService.onDestroy: call cancelQueuedHostnameRewrite() so the daemon
  thread stops polling when the service is torn down (comment said it did; code did not)
- SanitizedHostnameMigration.planFolderMigration: when localDeviceId is null and the
  sanitized dir already exists, only plan DeleteHostnameDir for empty legacy dirs.
  Previously it always planned the delete; applyFolderMigration rejected it when the
  dir had device-id entries, making MigrationResult.failed > 0 and causing
  ensureLegacyFoldersMigrated to retry the same failing action on every sync.
- Update test to assert correct (no-action) behavior for the non-empty-dir case and
  add a complementary test for the empty-dir (delete-eligible) case.

Git-Session-Id: 1223

* fix(android): join cancelled rewrite thread on teardown to close recreation race

Greptile P1: hostnameRewriteQueued lives in the companion object (shared
across service instances), so a recreated service could observe the guard
still set between cancelQueuedHostnameRewrite()'s interrupt() and the old
thread's finally block, skip queueing, and miss the deferred rewrite until
another full start (not guaranteed).

cancelQueuedHostnameRewrite() now joins the interrupted thread (bounded,
3s > 1s poll interval) so the guard is settled before a recreated instance
runs the migration; queueHostnameRewriteAfterServerExit() additionally
re-queues when the guard is set but the owning thread is no longer alive.

Git-Session-Id: 84cd2de6-e033-5129-98b8-2d5c7b78f268

* fix(android): cut hostname-rewrite cancel join to 500ms to bound main-thread teardown stall

The interrupt() in cancelQueuedHostnameRewrite() wakes the poll sleep
immediately, so the join only needs to cover poll-wake plus the worker's
finally block (milliseconds). The previous 3s bound stalled the main
thread in onDestroy() whenever the worker was mid-SQLite-rewrite, where
interrupt cannot cancel the transaction; in that case the worker completes
the rewrite and sets the preference itself, so a long join buys nothing.

Git-Session-Id: 8d81f917-4443-5800-9969-89574183b0ba

* ci: retrigger E2E after known NativeWindowInsetsTest.syncToggleReceivesRealTap flake

The previous run failed only on syncToggleReceivesRealTap ("A screen tap must
change the persisted setting expected:<true> but was:<false>") with no System UI
ANR in the log. The failing head differs from its last green run on this branch
only by an unrelated BackgroundService.kt change, and the job cannot be rerun
(TimeToBuildBob is pull-only). No code change.

Git-Session-Id: 293f1f27-ca8d-554e-b7ff-3be621a9cac7

* fix(android): stop retrying an undeletable legacy hostname dir, skip server start after teardown

The planner counts subdirectories when deciding a legacy hostname dir is empty,
but applyFolderMigration refuses to delete a dir with any entry. A legacy dir
holding only regular files was therefore planned for deletion, rejected, counted
as failed=1, and retried on every sync forever (ensureLegacyFoldersMigrated never
marks itself done). migrateSyncFolders now passes the set of hostname dirs holding
entries the planner cannot account for and does not plan a delete for those.

The server start moved into an IO coroutine, which can run after onDestroy; it
now re-checks a serviceDestroyed flag after acquiring sanitizedMigrationLock so a
torn-down service does not start a server nobody owns. Service has no isDestroyed
before API 35, hence the explicit flag.

Git-Session-Id: 95fc8809-06f8-53bb-8aa0-0a522af14149
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.

Elevated crash/ANR rate (8.17% user-perceived crashes) and declining Play Store rating

2 participants