Closes M12. The phone can now notice that its server has a newer build and install it, instead of the operator copying an APK to a device by hand. **A PackageInstaller session, not an install intent.** The obvious route — ACTION_VIEW on the APK — is exactly what on-device install heuristics are tuned against, and it is what produced the "bypassing Android security" warning on Minstrel (Scribe note 2437). It also never tells the OS that this app is the legitimate updater of its own package, and it returns nothing: a failed install is indistinguishable from someone dismissing the dialog. The session says who is doing what, and on Android 12+ declares no user action required — which, with UPDATE_PACKAGES_WITHOUT_USER_ACTION, removes the confirmation entirely on the UPDATE path. Only there: Android will not let an app quietly put a NEW package on a device, which is right. It also only applies when the new build carries the same signing key as the installed one, which is why signing had to land first. Two things from that research deliberately NOT done: `setRequestUpdateOwnership` was chased and turned out to be a red herring, and REQUEST_INSTALL_PACKAGES is not the differentiator either — Mihon declares it too. The mechanism was the whole difference. **The outcome comes back.** `commit` takes an IntentSender and the result lands at `UpdateReceiver`, so a failure can be shown rather than guessed at, and STATUS_PENDING_USER_ACTION is handled — that is the ordinary path below API 31 and still possible above it, since the OS is entitled to ask anyway. Someone declining is reported as no error at all: calling a deliberate choice a failure is how an app sounds broken when it is not. **The network work stays in Rust.** Two FFI additions — `clientUpdate` and `downloadClientUpdate` — because the device token lives in the core, and pulling it into Kotlin to make an HTTP call would spread the one secret this app holds across two languages for nothing. The core also owns the comparison, so the rule "version CODE decides, never the name" lives in the layer that has to get it right for every surface. The download is streamed to disk, not buffered: 55 MiB in memory on a phone is how an update gets killed halfway through. It lands in `update.apk.part` and is renamed only once size and sha256 both match, so an interrupted download can never be mistaken for a finished one. The digest is not a trust anchor — the signature is, and Android checks it — but it catches a truncated transfer before the installer is bothered with it. The advertised path is joined to the base URL this device is LINKED to rather than followed as given, so a server cannot point the download at a host nobody agreed to. **Updates are linked-only, and it says so.** An unlinked install has no update path, so it gets one sentence explaining where updates come from rather than a Check button that silently finds nothing — the same lesson as the desktop's unlink copy (issue 2110). And the "install unknown apps" grant is asked for BEFORE downloading, so nobody spends 55 MiB to be told no. Every Android API here was read out of `android-36/android.jar` with javap first, and the two new FFI methods out of freshly generated bindings, rather than recalled: `suspend fun clientUpdate(installedVersionCode: Long): ClientUpdate?` and `downloadClientUpdate(destPath: String)`. Also fixes `check-symbols.py`, which reported four false positives on `UpdateOutcome.Result` — its object-member index collected functions and properties but not nested TYPES, and a data class inside an object is an ordinary member.
203 lines
12 KiB
XML
203 lines
12 KiB
XML
<?xml version="1.0" encoding="utf-8"?>
|
|
<resources>
|
|
<string name="app_name">ThoughtSync</string>
|
|
|
|
<!-- Search bar -->
|
|
<string name="search_hint">Search your notes</string>
|
|
<string name="search_clear">Clear search</string>
|
|
<string name="nav_open">Open navigation</string>
|
|
<string name="nav_labels">Labels</string>
|
|
|
|
<!-- Compose sheet -->
|
|
<string name="compose_open">New note</string>
|
|
<string name="compose_kind_note">Note</string>
|
|
<string name="compose_kind_list">List</string>
|
|
<string name="compose_title_hint">Title</string>
|
|
<string name="compose_body_hint">Take a note…</string>
|
|
<string name="compose_list_hint">One item per line</string>
|
|
<string name="compose_discard">Discard</string>
|
|
<string name="compose_save">Save</string>
|
|
|
|
<!-- Board -->
|
|
<string name="board_empty_note">Empty note</string>
|
|
<plurals name="board_more_items">
|
|
<item quantity="one">+%d more item</item>
|
|
<item quantity="other">+%d more items</item>
|
|
</plurals>
|
|
|
|
<!-- Empty states. Each destination says something true of ITSELF; a single
|
|
"nothing here" reads as encouragement on the board and as a fault in Trash. -->
|
|
<string name="board_empty_title">Nothing here yet</string>
|
|
<string name="board_empty_body">Tap + to start a note or a list. Everything stays on this device until you connect a server.</string>
|
|
<string name="empty_search_title">No matches</string>
|
|
<string name="empty_search_body">Nothing matched “%1$s”.</string>
|
|
<string name="empty_trash_title">Trash is empty</string>
|
|
<string name="empty_trash_body">Deleted notes wait here before they are removed for good.</string>
|
|
<string name="empty_archive_title">Nothing archived</string>
|
|
<string name="empty_archive_body">Archived notes leave the board but stay searchable.</string>
|
|
<string name="empty_reminders_title">No reminders</string>
|
|
<string name="empty_reminders_body">Notes with a reminder set will appear here.</string>
|
|
|
|
<!-- Editor -->
|
|
<string name="board_open_note">Open note</string>
|
|
<string name="editor_back">Back to notes</string>
|
|
<string name="editor_title_hint">Title</string>
|
|
<string name="editor_body_hint">Note</string>
|
|
<string name="editor_add_item">Add item</string>
|
|
<string name="editor_remove_item">Remove item</string>
|
|
<string name="editor_remove_label">Remove label</string>
|
|
<string name="editor_reminder">Set a reminder</string>
|
|
<string name="editor_make_list">Make a checklist</string>
|
|
<string name="editor_make_note">Switch to a note</string>
|
|
<string name="editor_more">More actions</string>
|
|
<string name="editor_pin">Pin</string>
|
|
<string name="editor_unpin">Unpin</string>
|
|
<string name="editor_labels">Labels…</string>
|
|
<string name="editor_archive">Archive</string>
|
|
<string name="editor_unarchive">Unarchive</string>
|
|
<string name="editor_trash">Move to trash</string>
|
|
<string name="editor_restore">Restore</string>
|
|
<string name="editor_cancel">Cancel</string>
|
|
|
|
<!-- Deleting for good is the only thing in the app that cannot be undone, so
|
|
the copy says exactly that rather than asking "Are you sure?". -->
|
|
<string name="editor_delete_forever">Delete forever</string>
|
|
<string name="editor_delete_forever_title">Delete this note?</string>
|
|
<string name="editor_delete_forever_body">It will be removed from this device and from every device you sync with. This cannot be undone.</string>
|
|
<string name="editor_delete_forever_confirm">Delete</string>
|
|
|
|
<!-- Pickers -->
|
|
<string name="color_picker_title">Color</string>
|
|
<string name="label_picker_title">Labels</string>
|
|
<string name="label_new_hint">Type a label and press enter</string>
|
|
<string name="label_from_tag">from #tag</string>
|
|
<string name="label_none_body">No labels yet. Type one above, or write a #tag in a note and it becomes one.</string>
|
|
<string name="picker_next">Next</string>
|
|
<string name="picker_set">Set</string>
|
|
<string name="picker_time_title">Pick a time</string>
|
|
|
|
<!-- Reminders -->
|
|
<string name="reminder_title">Remind me</string>
|
|
<string name="reminder_later_today">Later today</string>
|
|
<string name="reminder_tomorrow">Tomorrow</string>
|
|
<string name="reminder_next_week">Next week</string>
|
|
<string name="reminder_pick">Pick a date & time</string>
|
|
<string name="reminder_clear">Remove reminder</string>
|
|
<string name="reminder_done">Done</string>
|
|
<string name="reminder_snooze_hour">Snooze 1h</string>
|
|
<string name="reminder_snooze_day">Snooze 1d</string>
|
|
<string name="recurrence_none">Once</string>
|
|
<string name="recurrence_daily">Daily</string>
|
|
<string name="recurrence_weekly">Weekly</string>
|
|
<string name="recurrence_monthly">Monthly</string>
|
|
<string name="recurrence_yearly">Yearly</string>
|
|
|
|
<!-- Store failure -->
|
|
<string name="store_unavailable_title">Your notes couldn\'t be opened</string>
|
|
<string name="store_unavailable_body">The note store on this device could not be read. Reinstalling will start a fresh one, but anything not synced to a server would be lost.</string>
|
|
|
|
<!-- Sync. Opt-in, and the copy has to carry that: being unlinked is the
|
|
normal resting state of a local-first app, not unfinished setup. -->
|
|
<string name="sync_title">Sync</string>
|
|
<string name="sync_badge_on">On</string>
|
|
<string name="sync_badge_unsent">Unsent</string>
|
|
|
|
<!-- Linked -->
|
|
<string name="sync_connected_to">Connected to</string>
|
|
<string name="sync_linked_as">as %1$s</string>
|
|
<string name="sync_last_synced">Last synced %1$s</string>
|
|
<string name="sync_never">never</string>
|
|
<string name="sync_unsent">This device has changes that haven\'t been sent yet.</string>
|
|
<string name="sync_now">Sync now</string>
|
|
<string name="sync_disconnect">Disconnect</string>
|
|
<string name="sync_disconnect_title">Stop syncing with this server?</string>
|
|
<string name="sync_disconnect_body">Your notes stay on this device, and the copy on the server is left alone. This device\'s access token is revoked, so it can\'t be used to reach the server again.</string>
|
|
<string name="reminder_channel">Reminders</string>
|
|
<string name="reminder_channel_description">Notifies you when a note\'s reminder is due.</string>
|
|
<string name="reminder_notifications_blocked_title">Reminders can\'t notify you</string>
|
|
<string name="reminder_notifications_blocked_body">Notifications are turned off for ThoughtSync, so reminders will only show here on the board.</string>
|
|
<string name="reminder_open_settings">Open settings</string>
|
|
<string name="reminder_inexact_title">Reminders may arrive late</string>
|
|
<string name="reminder_inexact_body">Without permission for exact alarms, Android delivers reminders when it next wakes the phone — usually within a few minutes, sometimes longer.</string>
|
|
<string name="reminder_allow_exact">Allow exact timing</string>
|
|
<string name="sync_automatic">Sync automatically</string>
|
|
<string name="sync_automatic_on">Checks about every 15 minutes, and whenever you open the app.</string>
|
|
<string name="sync_automatic_off">Only when you pull the board down or tap Sync now.</string>
|
|
<string name="update_installed_version">This app is build %1$d.</string>
|
|
<string name="update_available">Build %1$s is available (%2$d MB).</string>
|
|
<string name="update_current">You\'re on the newest build this server has.</string>
|
|
<string name="update_check">Check for an update</string>
|
|
<string name="update_install">Update</string>
|
|
<string name="update_failed_title">The update didn\'t install</string>
|
|
<string name="update_permission_title">Android needs your permission</string>
|
|
<string name="update_permission_body">ThoughtSync has to be allowed to install apps before it can update itself. This is a one-time setting.</string>
|
|
<string name="update_permission_action">Allow installing</string>
|
|
<string name="update_needs_server">App updates come from a server you connect. Until then, install new builds yourself.</string>
|
|
<string name="sync_footer">Your notes live on this device either way — syncing just keeps a server copy in step, so your other devices can catch up.</string>
|
|
<string name="sync_failed_title">Sync failed</string>
|
|
<string name="sync_rejected_title">The server wouldn\'t accept some changes</string>
|
|
<plurals name="sync_rejected_body">
|
|
<item quantity="one">%1$d change was rejected: %2$s</item>
|
|
<item quantity="other">%1$d changes were rejected: %2$s</item>
|
|
</plurals>
|
|
<string name="sync_degraded_title">Some features aren\'t available here</string>
|
|
<string name="sync_degraded_body">This server doesn\'t support: %1$s. Everything else syncs normally.</string>
|
|
|
|
<!-- Unlinked -->
|
|
<string name="sync_offline_title">Working offline on this device</string>
|
|
<string name="sync_offline_body">Everything works without a server — your notes are stored on this phone. Connect a ThoughtSync server if you want them to reach your other devices.</string>
|
|
<string name="sync_address_label">Server address</string>
|
|
<string name="sync_address_hint">notes.example.com</string>
|
|
<string name="sync_address_help">Uses https unless you type http:// yourself.</string>
|
|
<string name="sync_check">Check</string>
|
|
<string name="sync_probe_failed">Couldn\'t reach that server</string>
|
|
<string name="sync_link_failed">Couldn\'t connect</string>
|
|
<string name="sync_server_generic">ThoughtSync server</string>
|
|
<string name="sync_server_version">v%1$s</string>
|
|
<string name="sync_compat_ok">Fully compatible.</string>
|
|
<string name="sync_compat_degraded">Compatible, but these features aren\'t available on this server: %1$s.</string>
|
|
|
|
<!-- Shown before any credential field, whenever the probed address is http://.
|
|
Android blocks cleartext by default and this app allows it so that a
|
|
self-hosted server on a LAN works at all; this is the other half of
|
|
that trade. -->
|
|
<string name="sync_insecure_title">This connection isn\'t encrypted</string>
|
|
<string name="sync_insecure_body">You\'re about to sign in over plain http. Anyone on the same network can read your password and your notes. Use https unless this is a server you control, on a network you trust.</string>
|
|
|
|
<string name="sync_signin">Sign in</string>
|
|
<string name="sync_mode_password">Email and password</string>
|
|
<string name="sync_mode_token">Device token</string>
|
|
<string name="sync_email">Email</string>
|
|
<string name="sync_password">Password</string>
|
|
<string name="sync_token">Paste a device token</string>
|
|
<string name="sync_token_help">Create one in the web app under Account → Linked devices.</string>
|
|
<string name="sync_device_name">Name for this device</string>
|
|
<string name="sync_device_name_help">Shown in your account\'s list of linked devices.</string>
|
|
<string name="sync_connect">Connect and sync</string>
|
|
|
|
<!-- An unlink whose server-side revoke didn\'t land leaves a live credential.
|
|
Never a transient message: someone disconnecting to retire a phone has to
|
|
still find this when they come back to check. -->
|
|
<string name="sync_revoke_title">This device\'s token is still valid on the server</string>
|
|
<string name="sync_revoke_unsupported">This server is older than in-app sign-out, so this device\'s token had to be left in place. Revoke it in the web app under Account → Linked devices.</string>
|
|
<string name="sync_revoke_failed">%1$s Until it\'s revoked, this device\'s token still works — you can revoke it in the web app under Account → Linked devices.</string>
|
|
|
|
<!-- What a sync did. Counts what MOVED; batches, pages and cursors are real
|
|
numbers that answer nobody\'s question. -->
|
|
<string name="sync_summary">Synced — %1$s.</string>
|
|
<string name="sync_summary_sent">sent %1$d</string>
|
|
<string name="sync_summary_received">received %1$d</string>
|
|
<string name="sync_summary_uptodate">Already up to date.</string>
|
|
<plurals name="sync_summary_attachments">
|
|
<item quantity="one">%d attachment</item>
|
|
<item quantity="other">%d attachments</item>
|
|
</plurals>
|
|
<plurals name="sync_summary_attachments_failed">
|
|
<item quantity="one">%d attachment didn\'t download — it\'ll retry on the next sync.</item>
|
|
<item quantity="other">%d attachments didn\'t download — they\'ll retry on the next sync.</item>
|
|
</plurals>
|
|
|
|
<!-- Errors -->
|
|
<string name="error_dismiss">Dismiss</string>
|
|
</resources>
|