CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 4s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Web typecheck and unit tests (push) Successful in 20s
CI & Build / Python tests (push) Successful in 20s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
Android / Core and FFI clippy and tests (push) Successful in 1m12s
CI & Build / integration (push) Successful in 1m44s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m17s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m30s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m27s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Successful in 9m15s
Android / Build the server image (push) Successful in 1s
Family idea #5105, practice 12, as the operator chose on 2026-10-08: the token is encrypted, and Android backup stays on. The core: - Adds a TokenSeal trait in sync/state.rs, with set_sealed_link and open_token. - A sealed token is stored as "sealed:<value>". - A plain token, stored before this change or while sealing failed, is sealed in place on its next read. - A sealed token that won't open is dropped, and the server address and cursor are kept, so the app reads as unlinked and asks to sign in again. That is what happens after Android restores the app onto another phone. - The desktop passes no seal and keeps storing the token as before. The FFI: - Exports TokenSeal as a uniffi foreign trait (seal_token / open_token, null rather than an exception). - Requires it in Inkwell's constructor, so there is no moment a token could be stored unsealed. - Routes credentials(), unlink() and store_link() through it. Kotlin: - KeystoreTokenSeal is AES-GCM under an Android Keystore key, using the SealedBox framing from Minstrel's KeystoreSessionVault (Scribe snippet #5025), with no new dependency. - SealedBoxTest checks the framing on the JVM. allowBackup stays true, and the manifest says why. An unlinked phone's notes exist only on the phone, and the backup is their one other copy. The backup carries a token nothing can open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
215 lines
11 KiB
XML
215 lines
11 KiB
XML
<?xml version="1.0" encoding="utf-8"?>
|
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
|
|
<!--
|
|
INTERNET is requested but nothing uses it until the user links a server.
|
|
The app is local-first: the store, capture and the whole board work with
|
|
this permission never exercised.
|
|
-->
|
|
<uses-permission android:name="android.permission.INTERNET" />
|
|
<!-- Only to answer "is this connection metered?" before the app downloads its own
|
|
update in the background. Normal permission, no prompt, no location. -->
|
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
|
|
<!--
|
|
Four more permissions are NOT declared here and still reach the merged
|
|
manifest, contributed by WorkManager for the automatic sync:
|
|
|
|
RECEIVE_BOOT_COMPLETED reschedules the periodic sync after a restart,
|
|
instead of it silently stopping until the app is
|
|
next opened by hand
|
|
ACCESS_NETWORK_STATE evaluates the "needs a network" constraint, so a
|
|
run is not attempted with no route to the server
|
|
WAKE_LOCK holds the device awake for the seconds a sync
|
|
takes, so it is not suspended mid-request
|
|
FOREGROUND_SERVICE used only for expedited work; nothing here asks
|
|
for it, and it arrives with the library
|
|
|
|
Verified against the built APK's merged manifest, not assumed. Noted here
|
|
because all four appear in the app's permission list and nothing else in
|
|
this file would explain where they came from.
|
|
-->
|
|
|
|
<!--
|
|
usesCleartextTraffic, deliberately.
|
|
|
|
Android blocks plain HTTP by default from API 28, and the core explicitly
|
|
supports a self-hosted server on a LAN — `http://192.168.1.10:8000` is a
|
|
case it has a test for. Leaving the platform default would make this app
|
|
unusable for exactly the people it is built for, with a transport error
|
|
they could do nothing about.
|
|
|
|
Scoped by the fact that the app talks to ONE host: the server the user
|
|
typed in. There is no ad SDK, no analytics, nothing else making requests.
|
|
A network-security-config would be tighter in principle, but it matches on
|
|
domains and IP literals rather than CIDR ranges, so it cannot express
|
|
"any address on my own network" — the case that actually matters here.
|
|
|
|
The trade is not made silently: the sync screen shows an unmissable
|
|
warning when the probed address is http://, BEFORE any credential field
|
|
appears. See SyncScreen.kt.
|
|
-->
|
|
<!--
|
|
Reminders.
|
|
|
|
POST_NOTIFICATIONS is a runtime permission from API 33. It is asked for in
|
|
context — the first time the app opens holding a reminder that could fire,
|
|
never at launch on an empty board, where there would be nothing to explain
|
|
why it is being asked.
|
|
|
|
SCHEDULE_EXACT_ALARM rather than USE_EXACT_ALARM. USE_EXACT_ALARM is granted
|
|
at install with no prompt, and is reserved for apps whose whole purpose is an
|
|
alarm clock or calendar; a note app claiming it would be claiming something
|
|
untrue. SCHEDULE_EXACT_ALARM is the one the person can grant or refuse, and
|
|
refusing costs precision, not the feature — see Reminders.scheduleNext.
|
|
|
|
RECEIVE_BOOT_COMPLETED already arrives via WorkManager (below), but is
|
|
declared here too because ReminderReceiver now depends on it directly. A
|
|
permission this file relies on should be visible in this file.
|
|
-->
|
|
<!--
|
|
Updating this app from the server it syncs with (M12 step 7).
|
|
|
|
REQUEST_INSTALL_PACKAGES lets the app hand an APK to the system installer at
|
|
all. It is NOT what makes an install look suspicious to on-device heuristics
|
|
— Mihon declares it too — the legacy ACTION_VIEW install intent was, and this
|
|
app uses a PackageInstaller session instead. See AppUpdate.kt and Scribe note
|
|
2437. The person must additionally grant "install unknown apps" in system
|
|
settings; the update card asks before downloading anything.
|
|
|
|
UPDATE_PACKAGES_WITHOUT_USER_ACTION (API 31+) is what removes the install
|
|
confirmation on the UPDATE path, and only there — Android will not let an app
|
|
silently put a NEW package on a device, which is correct. It also only applies
|
|
when the new build is signed with the same key as the installed one, which is
|
|
why signing had to land before any of this could work.
|
|
-->
|
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
|
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
|
|
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
|
|
|
<!--
|
|
allowBackup stays true, which family idea #5105 (practice 12) says to turn
|
|
off. Inkwell is local-first, so an unlinked phone's notes exist only on the
|
|
phone, and Android's backup is their one other copy. The device token is what
|
|
the practice protects, and it is stored sealed under a Keystore key that never
|
|
leaves the phone (KeystoreTokenSeal.kt). A backup therefore carries the notes
|
|
and a token nothing can open, and a restored app asks to sign in again.
|
|
-->
|
|
<application
|
|
android:name=".InkwellApplication"
|
|
android:allowBackup="true"
|
|
android:icon="@mipmap/ic_launcher"
|
|
android:label="@string/app_name"
|
|
android:roundIcon="@mipmap/ic_launcher_round"
|
|
android:supportsRtl="true"
|
|
android:theme="@style/Theme.Inkwell"
|
|
android:usesCleartextTraffic="true">
|
|
<!--
|
|
launchMode="singleTop" exists for the SHARE filters below.
|
|
|
|
The reminder notification adds FLAG_ACTIVITY_SINGLE_TOP to its own
|
|
intent, so onNewIntent already worked for that one. A share intent is
|
|
built by the OTHER app — Chrome, a reader, the text-selection toolbar —
|
|
and nothing here can add a flag to it. Without singleTop declared on the
|
|
activity itself, every share while the app is running would stack a
|
|
second MainActivity on top of the first: a second view model, a second
|
|
board, and a back press that lands on a stale copy of the same app.
|
|
-->
|
|
<activity
|
|
android:name=".MainActivity"
|
|
android:exported="true"
|
|
android:launchMode="singleTop"
|
|
android:windowSoftInputMode="adjustResize"
|
|
android:theme="@style/Theme.Inkwell">
|
|
<intent-filter>
|
|
<action android:name="android.intent.action.MAIN" />
|
|
<category android:name="android.intent.category.LAUNCHER" />
|
|
</intent-filter>
|
|
|
|
<!--
|
|
Capture without opening the app first: Share → Inkwell from
|
|
anywhere, and the selection toolbar in any text field.
|
|
|
|
Text, and images one at a time or several at once. A shared image
|
|
becomes a note carrying it as an attachment, stored on the phone
|
|
and uploaded on the next sync that reaches a server (#5169).
|
|
|
|
image/* rather than */*: a photo or a screenshot is what people
|
|
share into a notes app. Claiming every type would put Inkwell in the
|
|
share sheet for APKs, contacts and calendar entries, where it would
|
|
be noise. Other files attach from inside the editor, whose picker
|
|
takes any type.
|
|
-->
|
|
<intent-filter>
|
|
<action android:name="android.intent.action.SEND" />
|
|
<category android:name="android.intent.category.DEFAULT" />
|
|
<data android:mimeType="text/plain" />
|
|
</intent-filter>
|
|
<intent-filter>
|
|
<action android:name="android.intent.action.SEND" />
|
|
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
|
<category android:name="android.intent.category.DEFAULT" />
|
|
<data android:mimeType="image/*" />
|
|
</intent-filter>
|
|
|
|
<!--
|
|
The label is what appears in the text-selection menu beside Copy and
|
|
Share, where "Inkwell" would say who rather than what.
|
|
-->
|
|
<intent-filter android:label="@string/capture_process_text">
|
|
<action android:name="android.intent.action.PROCESS_TEXT" />
|
|
<category android:name="android.intent.category.DEFAULT" />
|
|
<data android:mimeType="text/plain" />
|
|
</intent-filter>
|
|
</activity>
|
|
|
|
<!--
|
|
Not exported: every intent that reaches it is one this app created, with
|
|
an explicit component. Exporting would let any app on the device mark
|
|
someone's reminders as done.
|
|
|
|
The two system broadcasts are the exception and need the filter, because
|
|
the system is the sender. Both exist for the same reason — pending alarms
|
|
do not survive either a reboot or an app update, so without this a phone
|
|
that restarts overnight would quietly stop reminding anyone of anything.
|
|
-->
|
|
<!--
|
|
Where the system reports what happened to an install we committed. Not
|
|
exported: the only sender is the PendingIntent this app handed to
|
|
PackageInstaller. Without it a failed install would be indistinguishable
|
|
from someone declining the dialog (Scribe #2438).
|
|
-->
|
|
<!--
|
|
Hands an attachment to the app that opens its type. Not exported, as a
|
|
FileProvider must not be: access is granted one URI at a time, on the
|
|
intent that opens it. It serves only the cache copies listed in
|
|
res/xml/file_paths.xml, never the note store.
|
|
-->
|
|
<provider
|
|
android:name="androidx.core.content.FileProvider"
|
|
android:authorities="${applicationId}.files"
|
|
android:exported="false"
|
|
android:grantUriPermissions="true">
|
|
<meta-data
|
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
android:resource="@xml/file_paths" />
|
|
</provider>
|
|
|
|
<receiver
|
|
android:name=".UpdateReceiver"
|
|
android:exported="false" />
|
|
|
|
<receiver
|
|
android:name=".ReminderReceiver"
|
|
android:exported="false">
|
|
<intent-filter>
|
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
|
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
|
</intent-filter>
|
|
</receiver>
|
|
</application>
|
|
</manifest>
|