Skip to content

Add local network permission handling for private CalDAV servers - #146

Open
patrickunterwegs wants to merge 3 commits into
mainfrom
claude/cool-franklin-3qsh69
Open

Add local network permission handling for private CalDAV servers#146
patrickunterwegs wants to merge 3 commits into
mainfrom
claude/cool-franklin-3qsh69

Conversation

@patrickunterwegs

Copy link
Copy Markdown
Member

Summary

Adds permission handling for accessing private/local network CalDAV servers on Android 17+ and iOS, with UI feedback during account setup. The app now detects when a user is connecting to a private network host and requests the necessary OS permission before attempting the connection.

Key Changes

  • Permission framework (PermissionRequester.kt): New common interface for platform-specific permission handling with AppPermission.LOCAL_NETWORK enum

    • Android implementation: Requests android.permission.ACCESS_LOCAL_NETWORK (API 37+)
    • iOS implementation: Reports UNKNOWN status (iOS raises consent implicitly on first connection)
    • Desktop/Web implementations: Report NOT_APPLICABLE
  • Private network detection (LocalNetworkAddress.kt): New utility to identify private/local hosts

    • Recognizes IPv4 private ranges (10/8, 172.16/12, 192.168/16, 169.254/16, 127/8)
    • Recognizes IPv6 private ranges (fe80::/10, fc00::/7, ::1)
    • Recognizes mDNS names (.local suffix) and single-label hostnames
    • Includes comprehensive test coverage
  • Account setup UI (AddPrincipalBottomSheet.kt):

    • New LocalNetworkPermissionNotice composable showing permission status with icon and message
    • Centralizes credential submission through submit() function that checks permission before dispatching
    • Displays notice only for relevant hosts (private network + permission applicable)
    • Provides "Manage" button to open app settings
    • Handles permission state refresh on resume and when host changes
  • Platform manifests: Added NSLocalNetworkUsageDescription to iOS Info.plist files and ACCESS_LOCAL_NETWORK permission to Android manifests with explanatory comments

  • Localization: Added four new strings for permission states (granted, denied, unknown, manage button)

Implementation Details

  • Permission state is tracked separately from credentials to avoid blocking the UI while the OS permission dialog is displayed
  • The notice appears while the form is still incomplete, providing early feedback
  • On Android, the permission is only enforced on API 37+; earlier versions have unrestricted local network access
  • iOS cannot query permission state but will raise its own prompt on first connection; the UI shows "unknown" status with a manage button
  • The implementation follows the existing MVI pattern and integrates cleanly with the account setup flow

https://claude.ai/code/session_019HgjhLvQ2D1St6oxS6nx8T

Android 17 (API 37) gates local network access behind the
android.permission.ACCESS_LOCAL_NETWORK runtime permission; below that it
came for free with INTERNET. Since targetSdk is 37, adding a CalDAV account
on a LAN address silently failed: denied TCP connects do not fail fast, they
time out, so the app looked broken rather than blocked. Reproduced against
Radicale on a phone where targetSdk 36 connects and 37 times out.

Introduce the repo's first runtime-permission API, kept generic so further
permissions only need a new AppPermission constant and a branch in the
Android actual:

- PermissionRequester (expect/actual) with status/request/openAppSettings,
  following the rememberX() shape used by ImagePicker. Android implements it
  for real; iOS can only open settings, since its own prompt is raised
  implicitly on first connection and cannot be queried; Desktop and Web
  report NOT_APPLICABLE.
- isPrivateNetworkHost() classifies RFC1918, link-local, ULA, loopback,
  .local and single-label hosts, so people syncing with a hosted provider
  never see a "nearby devices" prompt.

In the add-account sheet, both routes to OnAddPrincipal now funnel through
one submit() that requests the permission first when the host is private,
and the server field shows whether access is granted with a button to review
it in system settings. Rediscovery and sync are deliberately not gated: they
require an account added earlier, so that flow has already been through this.

Also declare NSLocalNetworkUsageDescription in the three iOS Info.plists.
iOS already prompts and works, but the key supplies the purpose string shown
in that prompt and is effectively mandatory on iOS 18+.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HgjhLvQ2D1St6oxS6nx8T
Requesting the local network permission now ends the tap: the account is not
added behind the OS dialog, the user taps Add account again once they have
answered it.

Resuming for them meant holding the credentials in a second piece of state
and deciding what to do on a refusal - and the people who reach this are
pointing the app at a server on their own network, so tapping again is no
burden. The permission callback only updates the indicator now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HgjhLvQ2D1St6oxS6nx8T
Adding an account now always runs discovery, permission or not, so both
paths to OnAddPrincipal go back to exactly what they are on main and the
sheet's behaviour change is just the notice and its button. Someone
pointing the app at a LAN server can read the timeout and act on the
notice sitting right above the button.

The manage button now picks its action from the status: DENIED is the one
state the OS may still prompt for, so it asks there; GRANTED can only be
revoked in settings, and UNKNOWN is iOS, which has nothing to ask through,
so both open settings.

Android cannot tell "never asked" from "refused" - both read DENIED - so
this prompts on the first-run case and does nothing visible after a
permanent denial. Accepted to keep the sheet at one piece of permission
state and no gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HgjhLvQ2D1St6oxS6nx8T
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.

2 participants