Codeberg #15, foundation for the contact special-dates calendars. Declares the optional, feature-gated READ_CONTACTS permission (never requested at startup) and adds the offline, read-only contacts reader. - AndroidManifest: READ_CONTACTS with a comment documenting the opt-in/offline one-way-mirror contract. - domain/contacts: SpecialDateType + ContactSpecialDate model and a pure parseContactEventDate covering full (yyyy-MM-dd), year-less (--MM-dd) and compact (yyyyMMdd) shapes, with Feb-29 handling via a leap anchor. - data/contacts: ContactSpecialDatesDataSource querying ContactsContract.Data Event rows, split by TYPE, deduped per (contact, type); returns empty without the permission so sync can degrade to a stalled state. Hilt-bound. - Unit tests for the date parser (full/year-less/compact/Feb-29/malformed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
236 lines
11 KiB
XML
236 lines
11 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">
|
||
|
||
<uses-permission android:name="android.permission.READ_CALENDAR" />
|
||
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
|
||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||
<!--
|
||
Optional and feature-gated: only the "Contact special dates" feature reads
|
||
contacts, and only after the user enables it and grants this at runtime
|
||
(never requested at startup). Everything stays offline — birthdays and
|
||
other contact dates are mirrored one-way into local calendars; contacts
|
||
are never written and nothing leaves the device (the app has no INTERNET
|
||
permission). See docs/design/contact-special-dates.md.
|
||
-->
|
||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||
<!--
|
||
Lets the "Reliable delivery" setting open the direct system dialog to
|
||
exempt Calendula from battery optimisation (so reminder broadcasts aren't
|
||
delayed by Doze). Used only to launch that dialog; falls back to the
|
||
battery-optimisation list if the OS declines the direct intent.
|
||
-->
|
||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||
|
||
<!--
|
||
Re-fire a snoozed reminder at an exact time (the calendar provider won't —
|
||
its alert is already fired). USE_EXACT_ALARM is auto-granted to calendar
|
||
apps on API 33+; SCHEDULE_EXACT_ALARM covers API 31–32 (user-revocable,
|
||
with an inexact fallback if withheld). F-Droid-clean: no Play allowlisting.
|
||
-->
|
||
<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+): without this, getLaunchIntentForPackage
|
||
returns null and the calendar manager's per-account "manage" button can't
|
||
open the source sync app (DAVx5, ICSx5, Google Calendar, …). The LAUNCHER
|
||
intent makes launchable apps visible so we can launch whichever app owns a
|
||
calendar account's authenticator. -->
|
||
<queries>
|
||
<intent>
|
||
<action android:name="android.intent.action.MAIN" />
|
||
<category android:name="android.intent.category.LAUNCHER" />
|
||
</intent>
|
||
</queries>
|
||
|
||
<application
|
||
android:name=".CalendulaApp"
|
||
android:allowBackup="true"
|
||
android:enableOnBackInvokedCallback="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:roundIcon="@mipmap/ic_launcher_round"
|
||
android:supportsRtl="true"
|
||
android:theme="@style/Theme.Calendula"
|
||
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>
|
||
|
||
<!-- Be selectable as the system calendar app. Android has no API for
|
||
an app to make itself the default, so registering the filters
|
||
launchers and the OS use is what lets the user pick Calendula
|
||
from the system chooser when a date action fires (issue #9).
|
||
APP_CALENDAR is the "open the calendar app" action; the VIEW
|
||
filters below catch a tapped date. -->
|
||
<intent-filter>
|
||
<action android:name="android.intent.action.MAIN" />
|
||
<category android:name="android.intent.category.DEFAULT" />
|
||
<category android:name="android.intent.category.APP_CALENDAR" />
|
||
</intent-filter>
|
||
<!-- A launcher/clock date tap fires ACTION_VIEW on the provider's
|
||
time Uri (content://com.android.calendar/time/<epochMillis>);
|
||
some surfaces use the time/epoch mime type. We open the day view
|
||
on that date (MainActivity.calendarTimeDateOrNull). -->
|
||
<intent-filter>
|
||
<action android:name="android.intent.action.VIEW" />
|
||
<category android:name="android.intent.category.DEFAULT" />
|
||
<data
|
||
android:scheme="content"
|
||
android:host="com.android.calendar"
|
||
android:pathPrefix="/time" />
|
||
</intent-filter>
|
||
<intent-filter>
|
||
<action android:name="android.intent.action.VIEW" />
|
||
<category android:name="android.intent.category.DEFAULT" />
|
||
<data android:mimeType="time/epoch" />
|
||
</intent-filter>
|
||
|
||
<!-- Open a .ics file (file manager / email attachment / browser). -->
|
||
<intent-filter>
|
||
<action android:name="android.intent.action.VIEW" />
|
||
<category android:name="android.intent.category.DEFAULT" />
|
||
<category android:name="android.intent.category.BROWSABLE" />
|
||
<data android:scheme="content" android:mimeType="text/calendar" />
|
||
<data android:scheme="file" android:mimeType="text/calendar" />
|
||
</intent-filter>
|
||
<!-- Receive a .ics shared from another app. -->
|
||
<intent-filter>
|
||
<action android:name="android.intent.action.SEND" />
|
||
<category android:name="android.intent.category.DEFAULT" />
|
||
<data android:mimeType="text/calendar" />
|
||
</intent-filter>
|
||
|
||
<!-- Launcher long-press shortcuts (e.g. "New event"). -->
|
||
<meta-data
|
||
android:name="android.app.shortcuts"
|
||
android:resource="@xml/shortcuts" />
|
||
</activity>
|
||
|
||
<!-- Standalone surface for a captured crash report. MainActivity routes
|
||
here on a startup crash-loop, so it stays clear of the app's Hilt
|
||
graph and Compose content. Not exported: launched only by us. -->
|
||
<activity
|
||
android:name=".ui.crash.CrashReportActivity"
|
||
android:exported="false"
|
||
android:excludeFromRecents="true"
|
||
android:launchMode="singleTask" />
|
||
|
||
<!-- Quick Settings tile: a one-tap "New event" shortcut in the QS panel.
|
||
Exported with BIND_QUICK_SETTINGS_TILE so only the system QS host can
|
||
bind it; the action mirrors the launcher "New event" shortcut. -->
|
||
<service
|
||
android:name=".qs.NewEventTileService"
|
||
android:exported="true"
|
||
android:icon="@drawable/ic_qs_new_event"
|
||
android:label="@string/qs_tile_new_event_label"
|
||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||
<intent-filter>
|
||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||
</intent-filter>
|
||
</service>
|
||
|
||
<!-- The provider broadcasts EVENT_REMINDER at reminder time but posts
|
||
no notification itself — a calendar app must (v1.4, Etar model).
|
||
Exported: the broadcast arrives from the provider's process. -->
|
||
<receiver
|
||
android:name=".data.reminders.EventReminderReceiver"
|
||
android:exported="true">
|
||
<intent-filter>
|
||
<action android:name="android.intent.action.EVENT_REMINDER" />
|
||
<data
|
||
android:host="com.android.calendar"
|
||
android:scheme="content" />
|
||
</intent-filter>
|
||
</receiver>
|
||
|
||
<!-- Snooze / dismiss actions on a reminder notification, plus the snooze
|
||
re-show alarm. Not exported: only our own notification buttons and
|
||
AlarmManager PendingIntents target it. -->
|
||
<receiver
|
||
android:name=".data.reminders.ReminderActionReceiver"
|
||
android:exported="false" />
|
||
|
||
<!-- Home-screen widgets (Glance). Exported: the launcher/host binds them. -->
|
||
<receiver
|
||
android:name=".widget.agenda.AgendaWidgetReceiver"
|
||
android:label="@string/widget_agenda_label"
|
||
android:exported="true">
|
||
<intent-filter>
|
||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||
</intent-filter>
|
||
<meta-data
|
||
android:name="android.appwidget.provider"
|
||
android:resource="@xml/appwidget_info_agenda" />
|
||
</receiver>
|
||
|
||
<receiver
|
||
android:name=".widget.month.MonthWidgetReceiver"
|
||
android:label="@string/widget_month_label"
|
||
android:exported="true">
|
||
<intent-filter>
|
||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||
</intent-filter>
|
||
<meta-data
|
||
android:name="android.appwidget.provider"
|
||
android:resource="@xml/appwidget_info_month" />
|
||
</receiver>
|
||
|
||
<!-- Keeps both widgets fresh: the calendar provider broadcasts
|
||
PROVIDER_CHANGED on any data change (our writes and external sync),
|
||
and the system broadcasts the date/time ones at midnight / clock
|
||
changes so "today" highlighting rolls over. -->
|
||
<receiver
|
||
android:name=".widget.WidgetUpdateReceiver"
|
||
android:exported="true">
|
||
<intent-filter>
|
||
<action android:name="android.intent.action.PROVIDER_CHANGED" />
|
||
<data
|
||
android:host="com.android.calendar"
|
||
android:scheme="content" />
|
||
</intent-filter>
|
||
<intent-filter>
|
||
<action android:name="android.intent.action.DATE_CHANGED" />
|
||
<action android:name="android.intent.action.TIME_SET" />
|
||
<action android:name="android.intent.action.TIMEZONE_CHANGED" />
|
||
</intent-filter>
|
||
</receiver>
|
||
|
||
<!-- Hands .ics files we stage in the cache to other apps via a content
|
||
Uri (single-event share). Authority tracks applicationId so the
|
||
debug suffix doesn't break getUriForFile. -->
|
||
<provider
|
||
android:name="androidx.core.content.FileProvider"
|
||
android:authorities="${applicationId}.fileprovider"
|
||
android:exported="false"
|
||
android:grantUriPermissions="true">
|
||
<meta-data
|
||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||
android:resource="@xml/file_paths" />
|
||
</provider>
|
||
|
||
<!-- Persists the per-app language (M4) 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>
|