Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 13s
CI & Build / integration (push) Successful in 22s
CI & Build / Build & push image (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 6m21s
Capture without opening the app first — the input half of #1899. Two ways in: the share sheet from anywhere, and the text-selection toolbar in any app's text field. ## The note is created, not pre-filled The obvious build is "open the editor on a draft holding the shared text". That silently loses it. `NoteEditorScreen`'s flush is guarded by `bodyText != note.body`, so a draft handed the text already has nothing to save — share a link, press back without typing, and it is gone. Which is exactly the shape of a share: the common case is walking away. So `captureShared` makes the row first and opens the editor on the real note. A share has already said "keep this"; creating it is what honours that, and back then leaves a saved note rather than a decision. ## launchMode="singleTop" The reminder notification adds FLAG_ACTIVITY_SINGLE_TOP to its own intent, which is why `onNewIntent` already worked there. A share intent is built by the OTHER app and nothing here can add a flag to it, so the activity has to declare it. Without that, every share while the app was running would stack a second MainActivity — a second view model, a second board, and a back press landing on a stale copy of the same app. ## Subject and text, both A browser sends EXTRA_SUBJECT as the page title and EXTRA_TEXT as the URL. Keeping both makes the note read as its title, because the core names a note by its first line — the difference between a board you can scan and a column of identical links. `distinct` because plenty of senders put the same string in both. The extras are removed on read, like the reminder's note id and for the same reason: the activity keeps its launch intent, so without consuming them a rotation would replay the share and mint the note again. ## Not included: images `image/*` is deliberately absent from the filter. Nothing in this app can create an attachment — the core has `delete_attachment` and no counterpart, and the FFI exposes neither. Declaring the mime type would put ThoughtSync in front of people in the share sheet for a job it cannot do, and fail after they had already chosen it. Adding it needs an attachment-creation path through the core, the FFI and sync, which is its own piece of work. The desktop half of #1899 — a global hotkey — is not in this commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3MMqUtzX1TJgA1oypvm1c
181 lines
9.0 KiB
XML
181 lines
9.0 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" />
|
|
|
|
<application
|
|
android:name=".ThoughtSyncApplication"
|
|
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.ThoughtSync"
|
|
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.ThoughtSync">
|
|
<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 → ThoughtSync from
|
|
anywhere, and the selection toolbar in any text field.
|
|
|
|
text/plain ONLY, and image/* deliberately absent. Nothing in this
|
|
app can create an attachment — the core has `delete_attachment` and
|
|
no counterpart, and the FFI exposes neither. Claiming images in the
|
|
share sheet would put this app in front of people for a job it
|
|
cannot do and fail after they had chosen it.
|
|
-->
|
|
<intent-filter>
|
|
<action android:name="android.intent.action.SEND" />
|
|
<category android:name="android.intent.category.DEFAULT" />
|
|
<data android:mimeType="text/plain" />
|
|
</intent-filter>
|
|
|
|
<!--
|
|
The label is what appears in the text-selection menu beside Copy and
|
|
Share, where "ThoughtSync" 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).
|
|
-->
|
|
<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>
|