One flow, one back-stack entry, as a stepper rather than four destinations —
the steps are not independently reachable, and "back" from the browser step
abandons a server-side flow rather than popping a screen. It hangs off Settings
with the same sliding-section pattern Storage -> Export uses.
address -> discovery -> (Nextcloud browser approval | username + password)
-> pick lists -> add account
Provider warnings come before the attempt, not after: type a Fastmail or iCloud
address and the app password rule is stated while you type, which is the single
most common support ticket a CalDAV client inherits. Google is refused with the
reason. A login-flow host mismatch is shown, not refused — reverse proxies are
ordinary on self-hosted installs.
PreemptiveBasicInterceptor is deleted. The vendored BasicDigestAuthHandler
already sends Basic preemptively over HTTPS, also does Digest (Baikal defaults
to it, OkHttp has none), caches the working scheme, and scopes by registrable
domain — which is what iCloud's cross-host home set needs. Two implementations
of one job is the defect chunk 1 removed from ICalendarWriter.
⚠️ That handler compares its `domain` against the *registrable* domain, so
passing the full host meant credentials were withheld from every request to
every subdomain — i.e. every self-hosted Nextcloud, silently 401ing forever.
Pinned by CalDavHttpTest.
CalDavGateway and AccountCreator put the network and the database behind
interfaces so the sign-in state machine is testable without a server, a
database, a Keystore or an AccountManager. It had no tests, and the review
found eight issues in it.
Account creation is transactional and rolls its lists back explicitly:
account_id is ON DELETE SET NULL, so deleting the row alone leaves orphan
lists behind and every retry adds another set.
192 lines
9.1 KiB
XML
192 lines
9.1 KiB
XML
<?xml version="1.0" encoding="utf-8"?>
|
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
xmlns:tools="http://schemas.android.com/tools">
|
|
|
|
<!-- External tasks-provider access, for StorageMode.EXTERNAL only. Both
|
|
permission sets are declared, since the manifest is static; the active one
|
|
(org.tasks.* for tasks.org, org.dmfs.* for OpenTasks) is requested at
|
|
runtime by the permission flow, and only once the user has actually
|
|
selected External mode. Both are dangerous-level.
|
|
|
|
StorageMode.OWN needs nothing here: it is a Room database in our own data
|
|
directory. Agendula publishes no ContentProvider and declares no
|
|
permissions of its own. -->
|
|
<uses-permission android:name="org.dmfs.permission.READ_TASKS" />
|
|
<uses-permission android:name="org.dmfs.permission.WRITE_TASKS" />
|
|
<uses-permission android:name="org.tasks.permission.READ_TASKS" />
|
|
<uses-permission android:name="org.tasks.permission.WRITE_TASKS" />
|
|
|
|
<!-- CalDAV sync. ACCESS_NETWORK_STATE is merged in by work-runtime anyway,
|
|
but it shows in F-Droid's permission diff, so declare it deliberately
|
|
rather than letting it appear from nowhere.
|
|
|
|
READ_SYNC_SETTINGS / WRITE_SYNC_SETTINGS are what the ContentResolver
|
|
sync APIs need. No FOREGROUND_SERVICE: sync is a plain worker, and the
|
|
dataSync FGS type would bring the Android 15 six-hours-per-24 budget
|
|
(whose failure mode is a fatal RemoteServiceException) and a Play
|
|
requirement for a video demo.
|
|
|
|
Two more permissions appear in the merged manifest without being
|
|
declared here, and both come from work-runtime: WAKE_LOCK, and
|
|
FOREGROUND_SERVICE. The latter is not us taking the FGS route — below
|
|
API 31 WorkManager implements expedited work with a foreground service,
|
|
and minSdk is 29, so it is load-bearing for the "Sync now" button.
|
|
Removing it with tools:node="remove" would break expedited work on
|
|
exactly the older devices that need it most. Noted because it shows in
|
|
F-Droid's permission diff and would otherwise look unexplained. -->
|
|
<uses-permission android:name="android.permission.INTERNET" />
|
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
|
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
|
|
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
|
<!-- Exact due-time reminders: USE_EXACT_ALARM on 33+, SCHEDULE on 31-32. -->
|
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
|
|
android:maxSdkVersion="32" />
|
|
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
|
|
|
|
<!-- Package visibility (Android 11+): see the tasks providers so
|
|
resolveContentProvider works, and launchable apps so we can open the
|
|
provider / a store listing during onboarding. -->
|
|
<queries>
|
|
<provider android:authorities="org.dmfs.tasks" />
|
|
<provider android:authorities="org.tasks.opentasks" />
|
|
<intent>
|
|
<action android:name="android.intent.action.MAIN" />
|
|
<category android:name="android.intent.category.LAUNCHER" />
|
|
</intent>
|
|
<!-- Custom Tabs provider detection. Without this entry it silently finds
|
|
nothing on API 30+, and the Nextcloud login flow falls back to an
|
|
external browser for no visible reason. -->
|
|
<intent>
|
|
<action android:name="android.support.customtabs.action.CustomTabsService" />
|
|
</intent>
|
|
</queries>
|
|
|
|
<application
|
|
android:name=".AgendulaApp"
|
|
android:allowBackup="true"
|
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
|
android:fullBackupContent="@xml/backup_rules"
|
|
android:icon="@mipmap/ic_launcher"
|
|
android:label="@string/app_name"
|
|
android:localeConfig="@xml/locales_config"
|
|
android:networkSecurityConfig="@xml/network_security_config"
|
|
android:roundIcon="@mipmap/ic_launcher_round"
|
|
android:supportsRtl="true"
|
|
android:theme="@style/Theme.Agendula"
|
|
tools:targetApi="35">
|
|
<activity
|
|
android:name=".MainActivity"
|
|
android:exported="true"
|
|
android:launchMode="singleTop"
|
|
android:windowSoftInputMode="adjustResize">
|
|
<intent-filter>
|
|
<action android:name="android.intent.action.MAIN" />
|
|
<category android:name="android.intent.category.LAUNCHER" />
|
|
</intent-filter>
|
|
</activity>
|
|
|
|
<!-- Standalone crash-report surface; MainActivity routes here on a
|
|
startup crash-loop. Not exported, kept out of recents. -->
|
|
<activity
|
|
android:name=".ui.crash.CrashReportActivity"
|
|
android:exported="false"
|
|
android:excludeFromRecents="true"
|
|
android:launchMode="singleTask" />
|
|
|
|
<!-- Reminder alarm fires here (internal PendingIntent → not exported). -->
|
|
<receiver
|
|
android:name=".data.reminders.DueReminderReceiver"
|
|
android:exported="false" />
|
|
|
|
<!-- Re-arm alarms after reboot. -->
|
|
<receiver
|
|
android:name=".data.reminders.BootReceiver"
|
|
android:exported="true">
|
|
<intent-filter>
|
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
|
</intent-filter>
|
|
</receiver>
|
|
|
|
<!-- Re-sync reminders when an external provider changes — DAVx5 pulling
|
|
tasks while Agendula is backgrounded. External mode only: in OWN mode
|
|
nothing outside the app can change our data, and Room's
|
|
InvalidationTracker covers our own writes. An intent-filter host must
|
|
be a literal, so both external authorities are listed. -->
|
|
<receiver
|
|
android:name=".data.reminders.ProviderChangeReceiver"
|
|
android:exported="true">
|
|
<intent-filter>
|
|
<action android:name="android.intent.action.PROVIDER_CHANGED" />
|
|
<data android:scheme="content" android:host="org.tasks.opentasks" />
|
|
<data android:scheme="content" android:host="org.dmfs.tasks" />
|
|
</intent-filter>
|
|
</receiver>
|
|
|
|
<!-- Sync plumbing. The stub provider exists only to give the sync
|
|
adapter an authority to register against: Agendula publishes no real
|
|
ContentProvider since :provider was deleted, and without an authority
|
|
ContentService.hasAuthorityAccess() makes every ContentResolver sync
|
|
call a silent no-op at targetSdk >= 34. -->
|
|
<provider
|
|
android:name=".data.sync.SyncStubProvider"
|
|
android:authorities="${applicationId}.sync"
|
|
android:exported="false"
|
|
android:syncable="true" />
|
|
|
|
<!-- Exported and guarded by ACCOUNT_MANAGER. Note that
|
|
android.permission.ACCOUNT_AUTHENTICATOR does not exist. -->
|
|
<service
|
|
android:name=".data.sync.AuthenticatorService"
|
|
android:exported="true"
|
|
android:permission="android.permission.ACCOUNT_MANAGER">
|
|
<intent-filter>
|
|
<action android:name="android.accounts.AccountAuthenticator" />
|
|
</intent-filter>
|
|
<meta-data
|
|
android:name="android.accounts.AccountAuthenticator"
|
|
android:resource="@xml/authenticator" />
|
|
</service>
|
|
|
|
<service
|
|
android:name=".data.sync.SyncAdapterService"
|
|
android:exported="true"
|
|
android:permission="android.permission.BIND_SYNC_ADAPTER">
|
|
<intent-filter>
|
|
<action android:name="android.content.SyncAdapter" />
|
|
</intent-filter>
|
|
<meta-data
|
|
android:name="android.content.SyncAdapter"
|
|
android:resource="@xml/sync_adapter" />
|
|
</service>
|
|
|
|
<!-- WorkManager's on-demand initialisation. Removing the default
|
|
initializer is what lets AgendulaApp supply a HiltWorkerFactory, so
|
|
@HiltWorker workers can take injected dependencies. -->
|
|
<provider
|
|
android:name="androidx.startup.InitializationProvider"
|
|
android:authorities="${applicationId}.androidx-startup"
|
|
android:exported="false"
|
|
tools:node="merge">
|
|
<meta-data
|
|
android:name="androidx.work.WorkManagerInitializer"
|
|
android:value="androidx.startup"
|
|
tools:node="remove" />
|
|
</provider>
|
|
|
|
<!-- Persists the per-app language on API < 33, where the platform
|
|
per-app-languages API is unavailable. On 33+ this is a no-op. -->
|
|
<service
|
|
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
|
android:enabled="false"
|
|
android:exported="false">
|
|
<meta-data
|
|
android:name="autoStoreLocales"
|
|
android:value="true" />
|
|
</service>
|
|
</application>
|
|
|
|
</manifest>
|