Compare commits

...

21 Commits

Author SHA1 Message Date
7e7079df00 Widgets: close the rest of the rollover gaps (#228)
Review follow-up on the midnight alarm.

The month widget could still show the wrong month after the rollover, and by way
of the exact workaround #228's reporter described. ShiftMonthAction stored an
absolute index on every tap, so paging forward and back — which looks like a
no-op and was how people forced a redraw — silently pinned the widget to the
month that was current at the time. It kept redrawing correctly and kept drawing
August into September, today's circle nowhere on the grid. Landing back on the
current month now clears the key instead of writing it, so the widget goes back
to following the date. Paging somewhere else and staying there is left alone;
that one is a choice, and the today button undoes it.

The alarm also had no way back once something dropped it without telling us — a
force-stop, a battery-restricted transition, an OEM freeze — short of the user
opening the app. onUpdate is the one wake-up the system still owns through
updatePeriodMillis, so both receivers re-arm from it. That doubles as a window
narrower: setAndAllowWhileIdle's delivery slack scales with how far out the alarm
was set, so re-arming every half hour keeps midnight's window at minutes rather
than hours.

Boot and package-replace now re-arm and stop there. The host sends
APPWIDGET_UPDATE after both, so the redraw was a second pair of wide provider
reads and RemoteViews serialisations in a cold process, exactly when the device
is busiest.

Smaller things: the unreachable lookahead fallback armed 5 seconds out, which
would have been a wake-loop rather than the late redraw the comment claimed —
an hour now. The action guard's comment was copied from ReminderScheduleReceiver
along with its claim that the broadcasts are protected; PROVIDER_CHANGED is not,
and the real reason the guard is worth having is that the receiver must stay
exported. Application start does its sync off the main thread now.

Tests moved onto frozen historical transitions — Berlin 2024 both ways, Havana
2018 — since the 2026 dates they used depend on DST rules that can still change
under a tzdata bump. The Berlin "fall back" case never repeated midnight, so it
now says what it actually checks (a 25-hour day must not overshoot) and a real
repeated-midnight zone, pre-2019 Sao Paulo, pins the known limitation instead of
implying it is handled. Dropped the two assertions that restated the code, and
added the one that was missing: that the receiver's action guard admits the
action the alarm is sent with, which is the single point where the whole thing
would fail silently.
2026-08-27 20:17:22 +02:00
809013997d Widgets: wake at midnight instead of waiting to be poked (#228)
Both home-screen widgets kept highlighting yesterday as "today" after the date
changed — and the agenda kept dimming events against yesterday — until the user
paged the month arrows or removed and re-added the widget.

The data layer was never the problem: the month cache guard already drops its
window as soon as the anchor date differs, which is exactly why an arrow tap
fixed it instantly. What was missing was anything to *trigger* a redraw at the
day boundary.

The manifest asked for DATE_CHANGED and the receiver's KDoc presented it as the
rollover mechanism, but DATE_CHANGED is not on the implicit-broadcast exemption
list, so a manifest-declared receiver has not been given it since Android 8 —
this has never worked on any device the app supports. That left only
updatePeriodMillis, which the system defers in doze and OxygenOS-style skins
throttle harder still; the reporter's is a 30-minute backstop that in practice
never ran.

So the app now holds its own wake-up. WidgetRolloverScheduler arms a single
alarm for just after the next local midnight and every firing re-arms the next,
the same shape as ReminderAlarmScheduler. It is deliberately inexact:
setAndAllowWhileIdle needs no permission and survives doze, and a rollover a few
minutes late is invisible on a sleeping screen — exact alarms stay reserved for
reminder snooze. The target is the *actual* start of the day rather than a
literal 00:00, so it stays right in Havana, where DST means midnight does not
happen, and it walks on past a whole date skipped by a date-line move.

The alarm is only armed while a widget is actually placed: onEnabled/onDisabled
on both Glance receivers re-sync it, and sync() cancels only when neither kind is
left, so removing the month widget never stops the agenda one rolling over.
WidgetUpdateReceiver re-arms on everything that can invalidate the alarm — boot
and package-replace wipe it, a clock or timezone change moves the boundary it
was aimed at — and CalendulaApp does the same on start, which is what arms
existing installs that upgrade into this without re-adding their widget. It also
gained an action allowlist, matching ReminderScheduleReceiver: it is exported and
the broadcasts it takes are protected.

DATE_CHANGED stays in the filter as a free extra for any OEM that does deliver
it, but the docs no longer claim anything depends on it.

Adding the two overrides makes the receivers as structurally alike as the widgets
they wrap, which is how #89 collapsed Glance's provider map, so the keep rule now
covers GlanceAppWidgetReceiver as well. Verified in the releaseTest mapping: all
four classes keep their real names.

Tests cover the arithmetic that decides when to wake — ordinary days, the
re-arming instant itself, both DST transitions, a zone whose midnight does not
exist, a half-hour offset, and the same instant seen from two zones.

Closes #228.
2026-08-27 19:58:49 +02:00
Jean-Luc Makiola
a2172dce12 Google Play is live: README + production track (#244)
All checks were successful
Release — F-Droid repo + Gitea/Codeberg release + Play / detect (push) Successful in 8s
Release — F-Droid repo + Gitea/Codeberg release + Play / release (push) Has been skipped
Release — F-Droid repo + Gitea/Codeberg release + Play / play (push) Has been skipped
Calendula is published on Google Play, so the pipeline and the docs stop treating it as pending.

**README**
- Google Play badge next to F-Droid/Obtainium, linking the store page.
- Install table: the Play row goes from "Coming soon / —" to a real channel.
- The "coming soon" section becomes a live one with the store link; the closed-testing "testers wanted" call drops out. The note that Play signs with Google's key (so switching channels needs an uninstall) stays, and the trailing shared-signing-key line now flags Play as the exception.

**Play track**
- Release uploads default to `production` instead of `internal` — in the lane (`fastlane/Fastfile`) and in the workflow default (`.gitea/workflows/release.yaml`). Merging a bumped versionName to main is already the human gate, so the manual Console promotion only added delay.
- `docs/RELEASING.md` updated to match, including the escape hatches (`PLAY_TRACK=internal` to stage, `PLAY_RELEASE_STATUS=draft` to hold).

Note: if a `PLAY_TRACK` repo variable is set on Gitea it still wins over the new default — needs checking there.

No issue for this one.

Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/244
2026-08-27 19:09:04 +02:00
Jean-Luc Makiola
4099f66e91 Translations update from Weblate (#232)
All checks were successful
Release — F-Droid repo + Gitea/Codeberg release + Play / detect (push) Successful in 7s
Release — F-Droid repo + Gitea/Codeberg release + Play / release (push) Has been skipped
Release — F-Droid repo + Gitea/Codeberg release + Play / play (push) Has been skipped
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/232
2026-08-27 19:08:28 +02:00
49112b11b3 fix(deps): update test dependencies to v6.1.3 (#202)
All checks were successful
Release — F-Droid repo + Gitea/Codeberg release + Play / detect (push) Successful in 6s
Release — F-Droid repo + Gitea/Codeberg release + Play / release (push) Has been skipped
Release — F-Droid repo + Gitea/Codeberg release + Play / play (push) Has been skipped
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/202
2026-08-27 19:07:52 +02:00
1218bb523c fix(deps): update lifecycleruntime to v2.11.0 (#203)
All checks were successful
Release — F-Droid repo + Gitea/Codeberg release + Play / detect (push) Successful in 5s
Release — F-Droid repo + Gitea/Codeberg release + Play / release (push) Has been skipped
Release — F-Droid repo + Gitea/Codeberg release + Play / play (push) Has been skipped
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/203
2026-08-27 19:07:29 +02:00
17600a3905 fix(deps): update material3 (alpha) to v1.5.0-alpha26 (#235)
All checks were successful
Release — F-Droid repo + Gitea/Codeberg release + Play / detect (push) Successful in 7s
Release — F-Droid repo + Gitea/Codeberg release + Play / release (push) Has been skipped
Release — F-Droid repo + Gitea/Codeberg release + Play / play (push) Has been skipped
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/235
2026-08-27 19:07:02 +02:00
Julie
db5104a7d0 Translated using Weblate (Slovak)
Currently translated at 32.0% (184 of 574 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/sk/
2026-08-27 03:40:53 +00:00
Julie
6306064fc3 Translated using Weblate (Slovak)
Currently translated at 25.0% (144 of 574 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/sk/
2026-08-26 23:38:37 +00:00
Julie
d5a4761f2c Translated using Weblate (Slovak)
Currently translated at 13.5% (78 of 574 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/sk/
2026-08-26 21:23:50 +00:00
Dmitri Gabinski
66d6681798 Translated using Weblate (Belarusian)
Currently translated at 24.9% (143 of 574 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/be/
2026-08-26 21:23:49 +00:00
Weblate
fb61169656 Update translation files
Updated by "Remove blank strings" add-on in Weblate.

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/
2026-08-26 13:57:44 +00:00
Julie
62079fba0b Added translation using Weblate (Slovak) 2026-08-26 13:57:44 +00:00
Dmitri Gabinski
cc0fc8edc3 Translated using Weblate (Belarusian)
Currently translated at 23.8% (137 of 574 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/be/
2026-08-26 13:57:43 +00:00
Sup99999
62174fcd53 Translated using Weblate (Arabic)
Currently translated at 97.7% (561 of 574 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/ar/
2026-08-25 16:03:15 +00:00
Dmitri Gabinski
55c1b2ac25 Translated using Weblate (Belarusian)
Currently translated at 4.5% (26 of 574 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/be/
2026-08-25 02:58:46 +00:00
Sup99999
0ccc610144 Translated using Weblate (Arabic)
Currently translated at 95.9% (551 of 574 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/ar/
2026-08-25 02:58:45 +00:00
Weblate
5d009b7dc0 Update translation files
Updated by "Remove blank strings" add-on in Weblate.

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/
2026-08-24 06:55:51 +00:00
Dmitri Gabinski
28924daf02 Added translation using Weblate (Belarusian) 2026-08-24 06:55:51 +00:00
Sup99999
e71fb2a625 Translated using Weblate (Arabic)
Currently translated at 92.6% (532 of 574 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/ar/
2026-08-22 14:29:52 +00:00
SpiritOTHawk
aa5f76fc1b Translated using Weblate (Russian)
Currently translated at 28.2% (162 of 574 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/ru/
2026-08-22 14:29:52 +00:00
19 changed files with 924 additions and 57 deletions

View File

@@ -551,12 +551,11 @@ jobs:
env: env:
VERSION: ${{ needs.detect.outputs.version }} VERSION: ${{ needs.detect.outputs.version }}
VERSION_CODE: ${{ needs.detect.outputs.version_code }} VERSION_CODE: ${{ needs.detect.outputs.version_code }}
# Where the bundle lands. `internal` by default so a release reaches # Where the bundle lands. `production` — the release itself is already the
# testers rather than the public, and promotion to production stays a # gate (a bumped versionName only reaches main after on-device review), so
# deliberate human action in the Play Console — the same posture as # a second manual promotion in the Play Console bought nothing but delay.
# holding UI releases for on-device review. Override with the PLAY_TRACK # Override with the PLAY_TRACK repo variable to stage a release instead.
# repo variable once the flow is trusted. PLAY_TRACK: ${{ vars.PLAY_TRACK || 'production' }}
PLAY_TRACK: ${{ vars.PLAY_TRACK || 'internal' }}
PLAY_RELEASE_STATUS: ${{ vars.PLAY_RELEASE_STATUS || 'completed' }} PLAY_RELEASE_STATUS: ${{ vars.PLAY_RELEASE_STATUS || 'completed' }}
# Set PLAY_DRY_RUN=true to validate the edit against the API and discard # Set PLAY_DRY_RUN=true to validate the edit against the API and discard
# it instead of committing — used to rehearse the first upload. # it instead of committing — used to rehearse the first upload.

View File

@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Fixed
- **Home-screen widgets now turn the page at midnight.** Both the month and the
agenda widget kept highlighting yesterday as "today" — and the agenda kept
greying out the wrong events as already past — until you paged the month back
and forth or removed and re-added the widget. Calendula now wakes itself at the
day boundary and redraws, and re-arms after a reboot, a clock change or a
flight into another timezone. Paging the month widget forward and back also
stops quietly pinning it to that month, so it follows the date again instead of
being stranded on the month you happened to be looking at ([#228]).
## [2.19.3] — 2026-08-22 ## [2.19.3] — 2026-08-22
### Added ### Added
@@ -1471,3 +1481,4 @@ automatically, with zero telemetry and no internet permission.
[#192]: https://codeberg.org/jlmakiola/calendula/issues/192 [#192]: https://codeberg.org/jlmakiola/calendula/issues/192
[#196]: https://codeberg.org/jlmakiola/calendula/issues/196 [#196]: https://codeberg.org/jlmakiola/calendula/issues/196
[#214]: https://codeberg.org/jlmakiola/calendula/issues/214 [#214]: https://codeberg.org/jlmakiola/calendula/issues/214
[#228]: https://codeberg.org/jlmakiola/calendula/issues/228

View File

@@ -18,6 +18,8 @@ Reads, writes, and reminds — on top of the system calendar, with zero network
<p> <p>
<a href="https://f-droid.org/packages/de.jeanlucmakiola.calendula/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="56"></a> <a href="https://f-droid.org/packages/de.jeanlucmakiola.calendula/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="56"></a>
&nbsp; &nbsp;
<a href="https://play.google.com/store/apps/details?id=de.jeanlucmakiola.calendula"><img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" alt="Get it on Google Play" height="56"></a>
&nbsp;
<a href="https://apps.obtainium.imranr.dev/redirect?r=obtainium://add/https://codeberg.org/jlmakiola/calendula"><img src="https://github.com/ImranR98/Obtainium/blob/main/assets/graphics/badge_obtainium.png?raw=true" alt="Get it on Obtainium" height="56"></a> <a href="https://apps.obtainium.imranr.dev/redirect?r=obtainium://add/https://codeberg.org/jlmakiola/calendula"><img src="https://github.com/ImranR98/Obtainium/blob/main/assets/graphics/badge_obtainium.png?raw=true" alt="Get it on Obtainium" height="56"></a>
&nbsp; &nbsp;
<a href="https://ko-fi.com/jeanlucmakiola"><img src="https://storage.ko-fi.com/cdn/brandasset/v2/support_me_on_kofi_badge_beige.png" alt="Support me on Ko-fi" height="56"></a> <a href="https://ko-fi.com/jeanlucmakiola"><img src="https://storage.ko-fi.com/cdn/brandasset/v2/support_me_on_kofi_badge_beige.png" alt="Support me on Ko-fi" height="56"></a>
@@ -86,7 +88,7 @@ Pick whichever channel you already use — they all install the same app:
| [Official F-Droid](#f-droid-recommended) | On F-Droid's build schedule | Recommended; no extra setup | | [Official F-Droid](#f-droid-recommended) | On F-Droid's build schedule | Recommended; no extra setup |
| [Self-hosted F-Droid repo](#self-hosted-f-droid-repo-fastest-updates) | Minutes after a release | Fastest; needs the repo added once | | [Self-hosted F-Droid repo](#self-hosted-f-droid-repo-fastest-updates) | Minutes after a release | Fastest; needs the repo added once |
| [Codeberg release / Obtainium](#codeberg-release--obtainium) | Per release | Plain APK download, or automated by Obtainium | | [Codeberg release / Obtainium](#codeberg-release--obtainium) | Per release | Plain APK download, or automated by Obtainium |
| [Google Play](#google-play-coming-soon) | — | Coming soon | | [Google Play](#google-play) | Per release | Signed with Google's key — see below |
| [Build from source](#build-from-source) | Whenever you build | Full control | | [Build from source](#build-from-source) | Whenever you build | Full control |
### F-Droid (recommended) ### F-Droid (recommended)
@@ -131,18 +133,14 @@ or do it by hand: *Add App* → paste `https://codeberg.org/jlmakiola/calendula`
→ *Add*. Either way, Obtainium tracks the releases and prompts you when a new → *Add*. Either way, Obtainium tracks the releases and prompts you when a new
one appears. one appears.
### Google Play (coming soon) ### Google Play
Calendula is on its way to Google Play as an additional channel. It isn't live Calendula is live on Google Play:
yet — this section gets a link once it is. Play builds will be signed with **[play.google.com/store/apps/details?id=de.jeanlucmakiola.calendula](https://play.google.com/store/apps/details?id=de.jeanlucmakiola.calendula)**.
Google's key rather than mine, so switching between Play and any other channel
will require an uninstall.
> **Testers wanted.** Play requires a round of closed testing before the app can Play builds are signed with Google's key rather than mine, so switching between
> go public, and I'm still looking for testers. If you'd like to help, email Play and any other channel requires an uninstall (and with it, a fresh start for
> **[business@jeanlucmakiola.de](mailto:business@jeanlucmakiola.de)** with the app settings — your events live in the system calendar and are unaffected).
> Google account address you want to use — that address is what I need to add you
> to the closed test.
### Build from source ### Build from source
@@ -151,7 +149,7 @@ The build is a plain Gradle build with no proprietary dependencies — see
<sub>Official F-Droid, the self-hosted repo, and the Codeberg releases all share <sub>Official F-Droid, the self-hosted repo, and the Codeberg releases all share
the same signing key, so you can switch freely between them without the same signing key, so you can switch freely between them without
reinstalling.</sub> reinstalling. Google Play is the exception — see above.</sub>
## 📚 Documentation ## 📚 Documentation

View File

@@ -50,3 +50,12 @@
# the real names also survives app updates, which would otherwise renumber the # the real names also survives app updates, which would otherwise renumber the
# obfuscated name and orphan the stored mapping. # obfuscated name and orphan the stored mapping.
-keep class * extends androidx.glance.appwidget.GlanceAppWidget -keep class * extends androidx.glance.appwidget.GlanceAppWidget
# Belt and braces one level up: MonthWidgetReceiver and AgendaWidgetReceiver are
# nearly as alike (same supertype, same overrides, only a differing property
# initializer), and Glance's provider map is keyed off the receiver component
# too. AGP's manifest-derived keep rules already cover them, and the rule above
# keeps the two widgets distinct enough that the receivers' constructors differ
# so this is redundant today. It is here because #89 cost a release to diagnose
# and the guarantee should not rest on a component staying in the manifest.
-keep class * extends androidx.glance.appwidget.GlanceAppWidgetReceiver

View File

@@ -331,8 +331,13 @@
<!-- Keeps both widgets fresh: the calendar provider broadcasts <!-- Keeps both widgets fresh: the calendar provider broadcasts
PROVIDER_CHANGED on any data change (our writes and external sync), PROVIDER_CHANGED on any data change (our writes and external sync),
and the system broadcasts the date/time ones at midnight / clock and the day boundary arrives as the app's own ROLLOVER alarm (#228),
changes so "today" highlighting rolls over. --> delivered by an explicit PendingIntent so it needs no filter here.
DATE_CHANGED is kept as a free extra only — it is not an exempted
implicit broadcast, so a manifest-declared receiver is not given it
on Android 8+. TIME_SET / TIMEZONE_CHANGED move the day boundary,
and boot / package-replace wipe the alarm, so all four re-arm it.
Exported: the system broadcasts arrive from outside the app. -->
<receiver <receiver
android:name=".widget.WidgetUpdateReceiver" android:name=".widget.WidgetUpdateReceiver"
android:exported="true"> android:exported="true">
@@ -346,6 +351,8 @@
<action android:name="android.intent.action.DATE_CHANGED" /> <action android:name="android.intent.action.DATE_CHANGED" />
<action android:name="android.intent.action.TIME_SET" /> <action android:name="android.intent.action.TIME_SET" />
<action android:name="android.intent.action.TIMEZONE_CHANGED" /> <action android:name="android.intent.action.TIMEZONE_CHANGED" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter> </intent-filter>
</receiver> </receiver>

View File

@@ -10,6 +10,7 @@ import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesScheduler
import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesSyncWorker import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesSyncWorker
import de.jeanlucmakiola.calendula.data.reminders.ReminderMaintenanceScheduler import de.jeanlucmakiola.calendula.data.reminders.ReminderMaintenanceScheduler
import de.jeanlucmakiola.calendula.data.reminders.ReminderMaintenanceWorker import de.jeanlucmakiola.calendula.data.reminders.ReminderMaintenanceWorker
import de.jeanlucmakiola.calendula.widget.WidgetRolloverScheduler
import de.jeanlucmakiola.floret.crash.CrashConfig import de.jeanlucmakiola.floret.crash.CrashConfig
import de.jeanlucmakiola.floret.crash.CrashReporter import de.jeanlucmakiola.floret.crash.CrashReporter
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -44,6 +45,21 @@ class CalendulaApp : Application() {
reconcileSpecialDates() reconcileSpecialDates()
reconcileCalendarVisibility() reconcileCalendarVisibility()
startReminderDelivery() startReminderDelivery()
reconcileWidgetRollover()
}
/**
* Re-arm the widgets' midnight rollover from whatever is actually placed
* (#228). Idempotent, and it covers the cases no broadcast reaches — an
* install upgrading into the fix, or an alarm dropped by a force-stop, is
* armed again the next time the app is opened. Off the main thread because
* it makes a handful of binder calls and every process start runs it,
* including ones a worker or a receiver triggered.
*/
private fun reconcileWidgetRollover() {
CoroutineScope(SupervisorJob() + Dispatchers.Default).launch {
WidgetRolloverScheduler.sync(this@CalendulaApp)
}
} }
/** /**

View File

@@ -0,0 +1,122 @@
package de.jeanlucmakiola.calendula.widget
import android.app.AlarmManager
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import androidx.core.content.getSystemService
import de.jeanlucmakiola.calendula.widget.agenda.AgendaWidgetReceiver
import de.jeanlucmakiola.calendula.widget.month.MonthWidgetReceiver
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn
import kotlinx.datetime.plus
import kotlinx.datetime.toLocalDateTime
import kotlin.time.Clock
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.seconds
import kotlin.time.Instant
/**
* Holds the app's own wake-up for the next local midnight, so the home-screen
* widgets roll "today" over on the day boundary (#228).
*
* The widgets used to lean on `ACTION_DATE_CHANGED`, but that broadcast is not
* on the implicit-broadcast exemption list, so a manifest-declared receiver has
* never been given it since Android 8 — leaving only `updatePeriodMillis`, which
* the system defers in doze and OEM skins throttle harder still. The result was
* yesterday staying highlighted (and the agenda's past-event dimming staying
* anchored to yesterday) until something else forced a redraw.
*
* Exactly one alarm exists at a time and every firing re-arms the next one, the
* same shape as [de.jeanlucmakiola.calendula.data.reminders.ReminderAlarmScheduler].
* It is deliberately **inexact**: `setAndAllowWhileIdle` needs no permission and
* survives doze (which plain `set` does not), and a rollover that lands a few
* minutes late is invisible on a sleeping screen. Exact alarms stay reserved for
* reminder snooze.
*/
object WidgetRolloverScheduler {
/**
* Fire just *after* midnight, never exactly on it. An alarm delivered a few
* milliseconds early would still read the old date and re-arm for an instant
* later; the offset makes "the day has changed" unambiguous.
*/
internal val ROLLOVER_SLACK = 5.seconds
/**
* Arm the next rollover, or cancel a pending one when no widget is placed.
* Idempotent, so every trigger (boot, app start, widget added/removed, the
* rollover itself, a clock or timezone change) can just call it.
*/
fun sync(context: Context) {
val appContext = context.applicationContext
val alarmManager = appContext.getSystemService<AlarmManager>() ?: return
val pendingIntent = rolloverPendingIntent(appContext)
if (!hasPlacedWidgets(appContext)) {
alarmManager.cancel(pendingIntent)
return
}
val triggerAt = nextRolloverAt(Clock.System.now(), TimeZone.currentSystemDefault())
alarmManager.setAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP, triggerAt.toEpochMilliseconds(), pendingIntent,
)
}
/**
* The instant just after the next local midnight following [now] in [zone].
*
* Uses the *actual* start of the day rather than 00:00, so it stays correct
* where a DST jump means midnight never happens (Havana springs from 00:00 to
* 01:00) and where a whole local date is skipped by a date-line move (Apia
* had no 30 December 2011) — the loop then walks on to the next real day.
*
* The mirror case, a zone that rewinds *across* midnight so the day starts
* twice, resolves to the earlier start; the widget would then run an hour
* ahead of the clock. No entry in the current tz database does that (Brazil,
* which used to, dropped DST in 2019), and `updatePeriodMillis` covers it,
* so it is not worth carrying state to detect.
*/
fun nextRolloverAt(now: Instant, zone: TimeZone): Instant {
val date = now.toLocalDateTime(zone).date
var days = 1
while (days <= MAX_LOOKAHEAD_DAYS) {
val candidate = date.plus(days, DateTimeUnit.DAY).atStartOfDayIn(zone) + ROLLOVER_SLACK
if (candidate > now) return candidate
days++
}
// Unreachable for any zone in the tz database. Deliberately an hour and
// not the slack: a 5-second retry would just hit this branch again and
// wake the device in a loop.
return now + 1.hours
}
private fun hasPlacedWidgets(context: Context): Boolean {
val manager = AppWidgetManager.getInstance(context) ?: return false
return PROVIDERS.any {
manager.getAppWidgetIds(ComponentName(context, it)).isNotEmpty()
}
}
private fun rolloverPendingIntent(context: Context): PendingIntent =
PendingIntent.getBroadcast(
context,
ROLLOVER_REQUEST_CODE,
Intent(context, WidgetUpdateReceiver::class.java)
.setAction(WidgetUpdateReceiver.ACTION_ROLLOVER),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
private val PROVIDERS = listOf(
MonthWidgetReceiver::class.java,
AgendaWidgetReceiver::class.java,
)
/** Fixed: there is only ever one rollover alarm, and re-arming must replace it. */
private const val ROLLOVER_REQUEST_CODE = 0x0DA1
/** A gap of more than a couple of days does not exist in any tz database entry. */
private const val MAX_LOOKAHEAD_DAYS = 3
}

View File

@@ -12,19 +12,47 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
/** /**
* Redraws both home-screen widgets when their data goes stale. Triggered by: * Redraws both home-screen widgets when their data goes stale, and keeps the
* midnight rollover alarm armed. Triggered by:
* - `PROVIDER_CHANGED` from the calendar provider — fires on any data change, * - `PROVIDER_CHANGED` from the calendar provider — fires on any data change,
* so it covers both the app's own writes and external sync. * so it covers both the app's own writes and external sync.
* - `DATE_CHANGED` / `TIME_SET` / `TIMEZONE_CHANGED` — so "today" highlighting * - [ACTION_ROLLOVER], the app's own alarm from [WidgetRolloverScheduler] —
* and the upcoming window roll over at midnight / on a clock change. * the day boundary, so "today" highlighting and the agenda's past-event
* dimming move on (#228).
* - `TIME_SET` / `TIMEZONE_CHANGED` — a clock or zone change moves the day
* boundary relative to the armed alarm, so both redraw *and* re-arm.
* - `BOOT_COMPLETED` / `MY_PACKAGE_REPLACED` — both wipe pending alarms. The
* package-replaced one is also what arms existing installs that upgrade into
* the fix without re-adding their widget.
* *
* Both widgets also carry an `updatePeriodMillis` backstop in their provider * `DATE_CHANGED` is still in the manifest filter as a free extra, but nothing
* XML, and the month widget's refresh button forces an immediate redraw. * depends on it: it is not an exempted implicit broadcast, so a manifest-declared
* receiver has not actually been given it since Android 8. The widgets also carry
* an `updatePeriodMillis` backstop in their provider XML, and the month widget's
* refresh button forces an immediate redraw.
*
* Exported for the system broadcasts; an extra redraw triggered by another app
* is harmless.
*/ */
class WidgetUpdateReceiver : BroadcastReceiver() { class WidgetUpdateReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
val pending = goAsync() // The receiver has to stay exported for the system broadcasts, so an
// explicit intent can reach it with anything in it. Nothing here reads
// the intent's data and nothing crosses a trust boundary, but narrowing
// to the actions we actually asked for keeps a stray broadcast from
// costing two wide provider reads.
if (intent.action !in HANDLED_ACTIONS) return
val appContext = context.applicationContext val appContext = context.applicationContext
// Re-arm first: whatever happens to the redraw, the next day boundary is
// covered. Boot and package-replace dropped the alarm outright; a
// rollover just consumed it; a clock change invalidated it.
WidgetRolloverScheduler.sync(appContext)
// Boot and package-replace only cost us the alarm. The host sends
// APPWIDGET_UPDATE after both anyway, so redrawing here would just repeat
// two wide provider reads and two RemoteViews serialisations in a cold
// process, at the moment the device is most contended.
if (intent.action in REARM_ONLY_ACTIONS) return
val pending = goAsync()
// Calendar data may have changed (sync / our own write) — drop the cached // Calendar data may have changed (sync / our own write) — drop the cached
// month window so the widgets reload fresh. Month paging does NOT call // month window so the widgets reload fresh. Month paging does NOT call
// this, so arrow taps stay instant. // this, so arrow taps stay instant.
@@ -38,4 +66,23 @@ class WidgetUpdateReceiver : BroadcastReceiver() {
} }
} }
} }
companion object {
/** The app's own midnight wake-up; see [WidgetRolloverScheduler]. */
const val ACTION_ROLLOVER = "de.jeanlucmakiola.calendula.widget.ROLLOVER"
/** Both wipe pending alarms, and the host redraws the widgets itself after them. */
private val REARM_ONLY_ACTIONS = setOf(
Intent.ACTION_BOOT_COMPLETED,
Intent.ACTION_MY_PACKAGE_REPLACED,
)
internal val HANDLED_ACTIONS = REARM_ONLY_ACTIONS + setOf(
ACTION_ROLLOVER,
Intent.ACTION_PROVIDER_CHANGED,
Intent.ACTION_DATE_CHANGED,
Intent.ACTION_TIME_CHANGED,
Intent.ACTION_TIMEZONE_CHANGED,
)
}
} }

View File

@@ -1,7 +1,10 @@
package de.jeanlucmakiola.calendula.widget.agenda package de.jeanlucmakiola.calendula.widget.agenda
import android.appwidget.AppWidgetManager
import android.content.Context
import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver import androidx.glance.appwidget.GlanceAppWidgetReceiver
import de.jeanlucmakiola.calendula.widget.WidgetRolloverScheduler
/** /**
* Host-facing receiver for the agenda widget. Declared in the manifest with the * Host-facing receiver for the agenda widget. Declared in the manifest with the
@@ -10,4 +13,33 @@ import androidx.glance.appwidget.GlanceAppWidgetReceiver
*/ */
class AgendaWidgetReceiver : GlanceAppWidgetReceiver() { class AgendaWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = AgendaWidget() override val glanceAppWidget: GlanceAppWidget = AgendaWidget()
/** First agenda widget placed — start rolling "today" over at midnight (#228). */
override fun onEnabled(context: Context) {
super.onEnabled(context)
WidgetRolloverScheduler.sync(context)
}
/**
* Last agenda widget removed. [WidgetRolloverScheduler.sync] only cancels the
* alarm if no month widget is left either, so removing one kind never stops
* the other from rolling over.
*/
override fun onDisabled(context: Context) {
super.onDisabled(context)
WidgetRolloverScheduler.sync(context)
}
/**
* Self-heal on the system's own `updatePeriodMillis` wake-up — see
* [de.jeanlucmakiola.calendula.widget.month.MonthWidgetReceiver.onUpdate].
*/
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
WidgetRolloverScheduler.sync(context)
}
} }

View File

@@ -152,7 +152,19 @@ class ShiftMonthAction : ActionCallback {
val delta = parameters[deltaKey] ?: 0 val delta = parameters[deltaKey] ?: 0
updateAppWidgetState(context, glanceId) { prefs -> updateAppWidgetState(context, glanceId) { prefs ->
val cur = prefs[MONTH_INDEX_KEY] ?: currentMonthIndex(systemZone()) val cur = prefs[MONTH_INDEX_KEY] ?: currentMonthIndex(systemZone())
prefs[MONTH_INDEX_KEY] = cur + delta val next = cur + delta
// Landing back on the current month clears the key rather than
// storing today's index, so the widget goes back to *following* the
// date instead of being pinned to the month that happened to be
// current when it was tapped. Paging forward and back is the very
// workaround #228's reporter used to force a redraw; storing the
// index there would have left them stuck on that month for good once
// it stopped being the current one.
if (next == currentMonthIndex(systemZone())) {
prefs.remove(MONTH_INDEX_KEY)
} else {
prefs[MONTH_INDEX_KEY] = next
}
} }
MonthWidget().update(context.applicationContext, glanceId) MonthWidget().update(context.applicationContext, glanceId)
} }

View File

@@ -1,7 +1,10 @@
package de.jeanlucmakiola.calendula.widget.month package de.jeanlucmakiola.calendula.widget.month
import android.appwidget.AppWidgetManager
import android.content.Context
import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver import androidx.glance.appwidget.GlanceAppWidgetReceiver
import de.jeanlucmakiola.calendula.widget.WidgetRolloverScheduler
/** /**
* Host-facing receiver for the month widget. Declared in the manifest with the * Host-facing receiver for the month widget. Declared in the manifest with the
@@ -9,4 +12,37 @@ import androidx.glance.appwidget.GlanceAppWidgetReceiver
*/ */
class MonthWidgetReceiver : GlanceAppWidgetReceiver() { class MonthWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = MonthWidget() override val glanceAppWidget: GlanceAppWidget = MonthWidget()
/** First month widget placed — start rolling "today" over at midnight (#228). */
override fun onEnabled(context: Context) {
super.onEnabled(context)
WidgetRolloverScheduler.sync(context)
}
/**
* Last month widget removed. [WidgetRolloverScheduler.sync] only cancels the
* alarm if no agenda widget is left either, so removing one kind never stops
* the other from rolling over.
*/
override fun onDisabled(context: Context) {
super.onDisabled(context)
WidgetRolloverScheduler.sync(context)
}
/**
* The `updatePeriodMillis` backstop is the one wake-up the *system* still
* owns, so it doubles as the rollover alarm's self-heal: anything that drops
* a pending alarm without a broadcast — a force-stop, a battery-restricted
* transition, an OEM freeze — is repaired here rather than waiting for the
* app to be opened. Re-arming closer to midnight also narrows the inexact
* alarm's delivery window, which scales with how far out it was set.
*/
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
WidgetRolloverScheduler.sync(context)
}
} }

View File

@@ -38,7 +38,7 @@
<string name="settings_theme_system">النظام</string> <string name="settings_theme_system">النظام</string>
<string name="settings_theme_light">فاتح</string> <string name="settings_theme_light">فاتح</string>
<string name="settings_theme_dark">داكن</string> <string name="settings_theme_dark">داكن</string>
<string name="month_open_menu">افتح القائمة</string> <string name="month_open_menu">فتح القائمة</string>
<string name="month_more_actions">المزيد من الإجراءات</string> <string name="month_more_actions">المزيد من الإجراءات</string>
<string name="event_detail_edit">تعديل</string> <string name="event_detail_edit">تعديل</string>
<string name="event_detail_delete">حذف</string> <string name="event_detail_delete">حذف</string>
@@ -176,7 +176,7 @@
<string name="reminder_at_time">في وقت الحدث</string> <string name="reminder_at_time">في وقت الحدث</string>
<string name="event_untitled">(بدون عنوان)</string> <string name="event_untitled">(بدون عنوان)</string>
<string name="reminder_channel_name">تذكيرات الحدث</string> <string name="reminder_channel_name">تذكيرات الحدث</string>
<string name="reminder_channel_description">الإشعارات في أوقات التذكير الخاصه بأحداثك</string> <string name="reminder_channel_description">الإشعارات في أوقات التذكير بأحداثك</string>
<string name="reminder_onboarding_title">لا تفوّت أي حدث أبدًا</string> <string name="reminder_onboarding_title">لا تفوّت أي حدث أبدًا</string>
<string name="reminder_onboarding_body">أندرويد لا يعرض تذكيرات الأحداث من نفسه — بل يجب أن يقوم تطبيق تقويم بذلك. دع Calendula يتولى هذه المهمة.</string> <string name="reminder_onboarding_body">أندرويد لا يعرض تذكيرات الأحداث من نفسه — بل يجب أن يقوم تطبيق تقويم بذلك. دع Calendula يتولى هذه المهمة.</string>
<string name="reminder_benefit_delivery_body">كل تذكير لأحداثك يصِل كإشعار، في الوقت المحدد تمامًا.</string> <string name="reminder_benefit_delivery_body">كل تذكير لأحداثك يصِل كإشعار، في الوقت المحدد تمامًا.</string>
@@ -238,7 +238,7 @@
<string name="settings_time_format_24h">٢٤-ساعة (١٤:٠٠)</string> <string name="settings_time_format_24h">٢٤-ساعة (١٤:٠٠)</string>
<string name="agenda_range_day">اليوم</string> <string name="agenda_range_day">اليوم</string>
<string name="settings_agenda_show_today">إظهار اليوم دائمًا</string> <string name="settings_agenda_show_today">إظهار اليوم دائمًا</string>
<string name="settings_agenda_show_today_hint">اترك اليوم في أعلى الجدول و ودجته، حتى بعد عدم وجود أي شيء اليوم.</string> <string name="settings_agenda_show_today_hint">اترك اليوم في أعلى الجدول و أداته المصغرة، حتى بعد عدم وجود أي شيء اليوم.</string>
<string name="agenda_range_this_month">هذا الشهر</string> <string name="agenda_range_this_month">هذا الشهر</string>
<string name="agenda_range_this_week">هذا الأسبوع</string> <string name="agenda_range_this_week">هذا الأسبوع</string>
<string name="settings_past_events_show">إظهار</string> <string name="settings_past_events_show">إظهار</string>
@@ -249,7 +249,7 @@
<string name="event_edit_timezone_all">جميع المناطق الزمنية</string> <string name="event_edit_timezone_all">جميع المناطق الزمنية</string>
<string name="event_edit_timezone_none">لا منطقة زمنية تطابق ”%1$s“</string> <string name="event_edit_timezone_none">لا منطقة زمنية تطابق ”%1$s“</string>
<string name="event_edit_timezone_local_time">%1$s توقيتك</string> <string name="event_edit_timezone_local_time">%1$s توقيتك</string>
<string name="event_edit_timezone_recent">الأحدث</string> <string name="event_edit_timezone_recent">الأخيرة</string>
<string name="event_edit_recurrence_incomplete">أدخل رقمًا من ١ إلى ٩٩٩</string> <string name="event_edit_recurrence_incomplete">أدخل رقمًا من ١ إلى ٩٩٩</string>
<plurals name="duration_minutes"> <plurals name="duration_minutes">
<item quantity="zero">(%d) لا دقائق</item> <item quantity="zero">(%d) لا دقائق</item>
@@ -286,7 +286,7 @@
<string name="agenda_span_starts">يبدأ %1$s</string> <string name="agenda_span_starts">يبدأ %1$s</string>
<string name="agenda_span_ends">ينتهي %1$s</string> <string name="agenda_span_ends">ينتهي %1$s</string>
<string name="today_jump_action">الانتقال إلى اليوم</string> <string name="today_jump_action">الانتقال إلى اليوم</string>
<string name="widget_refresh">تحديث</string> <string name="widget_refresh">إعادة تحميل</string>
<string name="settings_app_name">اسم التطبيق</string> <string name="settings_app_name">اسم التطبيق</string>
<string name="settings_dim_completed">تعتيم الأحداث المكتملة</string> <string name="settings_dim_completed">تعتيم الأحداث المكتملة</string>
<string name="settings_dim_completed_summary">تعتيم الأحداث التي انتهت بالفعل في عرض الشهر والأسبوع</string> <string name="settings_dim_completed_summary">تعتيم الأحداث التي انتهت بالفعل في عرض الشهر والأسبوع</string>
@@ -335,8 +335,8 @@
<item quantity="many">%d يوم</item> <item quantity="many">%d يوم</item>
<item quantity="other">%d يوم</item> <item quantity="other">%d يوم</item>
</plurals> </plurals>
<string name="settings_agenda_range_bar_hint">اعرض شريطًا في الجزء العلوي من الجدول يحدد التواريخ المعروضة، مع زر لتبديل النطاق للجلسة</string> <string name="settings_agenda_range_bar_hint">اعرض شريطًا في الجزء العلوي من الجدول يحدد التواريخ المعروضة، مع زر لتبديل المدى للجلسة</string>
<string name="settings_agenda_range_bar">شريط النطاق</string> <string name="settings_agenda_range_bar">شريط المدى</string>
<string name="settings_month_header">عرض الشهر</string> <string name="settings_month_header">عرض الشهر</string>
<string name="month_style_continuous">تمرير الشهور</string> <string name="month_style_continuous">تمرير الشهور</string>
<string name="month_style_dense">أسابيع سلسة</string> <string name="month_style_dense">أسابيع سلسة</string>
@@ -424,7 +424,7 @@
<string name="settings_views_subtitle">العرض، التخطيط، الترتيب</string> <string name="settings_views_subtitle">العرض، التخطيط، الترتيب</string>
<string name="settings_event_form_subtitle">الخانات الافتراضية و السلوك</string> <string name="settings_event_form_subtitle">الخانات الافتراضية و السلوك</string>
<string name="settings_notifications_subtitle">التذكيرات و التسليم</string> <string name="settings_notifications_subtitle">التذكيرات و التسليم</string>
<string name="settings_special_dates_subtitle">أعياد ميلاد جهات الاتصال و احتفالات ذكرى</string> <string name="settings_special_dates_subtitle">أعياد ميلاد جهات الاتصال و الذكرات السنوية</string>
<string name="settings_special_dates_type_birthday">أعياد الميلاد</string> <string name="settings_special_dates_type_birthday">أعياد الميلاد</string>
<string name="settings_special_dates_type_custom">تواريخ أخرى</string> <string name="settings_special_dates_type_custom">تواريخ أخرى</string>
<string name="settings_license">الترخيص</string> <string name="settings_license">الترخيص</string>
@@ -432,7 +432,7 @@
<string name="settings_special_dates_disable_confirm">إيقاف</string> <string name="settings_special_dates_disable_confirm">إيقاف</string>
<string name="settings_special_dates_sync_now">زامن الآن</string> <string name="settings_special_dates_sync_now">زامن الآن</string>
<string name="settings_special_dates_never_synced">لم تتم المزامنة بعد</string> <string name="settings_special_dates_never_synced">لم تتم المزامنة بعد</string>
<string name="settings_special_dates_paused_hint">لم يعد بإمكان Calendula قراءة جهات الاتصال الخاصة بك، لذلك لا يتم تحديث هذه التقويمات.</string> <string name="settings_special_dates_paused_hint">لم يعد بإمكان Calendula قراءة جهات اتصالك، لذلك لا يتم تحديث هذه التقويمات.</string>
<string name="calendars_manage_in_app">الإدارة في التطبيق</string> <string name="calendars_manage_in_app">الإدارة في التطبيق</string>
<string name="calendars_enable_all">تفعيل الكل</string> <string name="calendars_enable_all">تفعيل الكل</string>
<string name="calendars_disable_all">تعطيل الكل</string> <string name="calendars_disable_all">تعطيل الكل</string>
@@ -500,11 +500,11 @@
<string name="settings_qs_tile">إضافة مربع الإعدادات السريعة</string> <string name="settings_qs_tile">إضافة مربع الإعدادات السريعة</string>
<string name="import_button">استيراد</string> <string name="import_button">استيراد</string>
<string name="crash_dialog_dismiss">ليس الآن</string> <string name="crash_dialog_dismiss">ليس الآن</string>
<string name="crash_report_copied">تم نسخ التقرير إلى الحافظة</string> <string name="crash_report_copied">تم نسخ التقرير إلى حافظتك</string>
<string name="crash_report_open_failed">تعذّر فتح متتبع المشكلات. التقرير موجود في الحافظة الخاصة بك.</string> <string name="crash_report_open_failed">تعذّر فتح متتبع المشكلات. التقرير موجود في حافظتك.</string>
<string name="crash_report_issue_title">تقرير العطل</string> <string name="crash_report_issue_title">تقرير العطل</string>
<string name="crash_report_clip_label">%1$s تقرير العطل</string> <string name="crash_report_clip_label">%1$s تقرير العطل</string>
<string name="crash_report_body_paste">_(كان التقرير طويلًا جدًا لهذا الرابط — الصقه من الحافظة هنا.)_</string> <string name="crash_report_body_paste">_(كان التقرير طويلًا جدًا لهذا الرابط — الصقه من حافظتك هنا.)_</string>
<string name="special_dates_calendar_birthday">أعياد الميلاد</string> <string name="special_dates_calendar_birthday">أعياد الميلاد</string>
<string name="special_dates_calendar_custom">التواريخ الخاصة</string> <string name="special_dates_calendar_custom">التواريخ الخاصة</string>
<string name="event_edit_recurrence_next_none">هذه القاعدة لا تتكرر أبدًا</string> <string name="event_edit_recurrence_next_none">هذه القاعدة لا تتكرر أبدًا</string>
@@ -515,18 +515,18 @@
<string name="month_split_expand">إظهار الشهر بأكمله</string> <string name="month_split_expand">إظهار الشهر بأكمله</string>
<string name="settings_week_day_header">الأسبوع و اليوم</string> <string name="settings_week_day_header">الأسبوع و اليوم</string>
<string name="settings_default_view_hint">طريقة العرض التي يفتحها Calendula عندما تقوم ببدءه.</string> <string name="settings_default_view_hint">طريقة العرض التي يفتحها Calendula عندما تقوم ببدءه.</string>
<string name="settings_week_start_hint">اليوم الذي يبدأ به كل أسبوع، في جميع طرق العرض والودجتز.</string> <string name="settings_week_start_hint">اليوم الذي يبدأ به كل أسبوع، في جميع طُرق العرض والأدوات المصغّرة.</string>
<string name="settings_time_format_hint">كيف الأوقات تُكتب في جميع أنحاء التطبيق. تلقائيًا يتبع إعداد نظامك.</string> <string name="settings_time_format_hint">كيف الأوقات تُكتب في جميع أنحاء التطبيق. تلقائيًا يتبع إعداد نظامك.</string>
<string name="settings_past_events_hint">ما الذي يفعله جدول بالأحداث التي انتهت بالفعل.</string> <string name="settings_past_events_hint">ما الذي يفعله جدول بالأحداث التي انتهت بالفعل.</string>
<string name="calendars_visibility_a11y">إظهار \"%1$s\"</string> <string name="calendars_visibility_a11y">إظهار \"%1$s\"</string>
<string name="calendars_visibility_notice_title">بعض التقويمات متوقفة</string> <string name="calendars_visibility_notice_title">بعض التقويمات متوقفة</string>
<string name="calendars_visibility_notice_message">Calendula يعرض التقويمات التي تم تشغيلها لهذا الجهاز، وبعض من خاصتك متوقفة. لرؤية أحداثهم، قم بتشغيلهم ضمن الإعدادات ← التقويمات.</string> <string name="calendars_visibility_notice_message">Calendula يعرض التقويمات التي تم تشغيلها لهذا الجهاز، وبعض من خاصتك متوقفة. لرؤية أحداثهم، قم بتشغيلهم ضمن الإعدادات ← التقويمات.</string>
<string name="calendar_picker_missing_title">تفتقد تقويمًا؟</string> <string name="calendar_picker_missing_title">تفتقد تقويمًا؟</string>
<string name="calendar_picker_missing_summary">قد يكون متوقفًا عن التشغيل، للقراءة فقط أو مملوءًا من جهات الاتصال الخاصة بك — يمكنك إدارة تقويماتك هنا.</string> <string name="calendar_picker_missing_summary">قد يكون متوقفًا عن التشغيل، للقراءة فقط أو مملوءًا من جهات اتصالك — يمكنك إدارة تقويماتك هنا.</string>
<string name="calendars_state_read_only">للقراءة فقط</string> <string name="calendars_state_read_only">للقراءة فقط</string>
<string name="calendars_state_not_synced">لم تتم المزامنة مع هذا الجهاز</string> <string name="calendars_state_not_synced">لم تتم المزامنة مع هذا الجهاز</string>
<string name="calendars_state_managed">تم ملؤه من جهات اتصالك</string> <string name="calendars_state_managed">تم ملؤه من جهات اتصالك</string>
<string name="calendars_managed_delete_locked">تم ملء هذا التقويم من جهات الاتصال الخاصة بك، لذلك سيقوم Calendula بإنشائه مرة أخرى في المزامنة التالية. قم بإيقاف تشغيل التواريخ الخاصة ضمن الإعدادات ← التواريخ الخاصة لحذفها.</string> <string name="calendars_managed_delete_locked">تم ملء هذا التقويم من جهات اتصالك، لذلك سيقوم Calendula بإنشائه مرة أخرى في المزامنة التالية. قم بإيقاف تشغيل التواريخ الخاصة ضمن الإعدادات ← التواريخ الخاصة لحذفها.</string>
<string name="duration_custom_max">كحد أقصى %1$s</string> <string name="duration_custom_max">كحد أقصى %1$s</string>
<string name="settings_calendar_duration_inherits">الافتراضي (%1$s)</string> <string name="settings_calendar_duration_inherits">الافتراضي (%1$s)</string>
<string name="settings_calendar_duration_use_default">استخدام المدة الافتراضية (%1$s)</string> <string name="settings_calendar_duration_use_default">استخدام المدة الافتراضية (%1$s)</string>
@@ -656,26 +656,53 @@
<string name="settings_special_dates_calendar_hint">اضبط لون كل تقويم وإمكانية ظهوره في إعدادات التقويمات.</string> <string name="settings_special_dates_calendar_hint">اضبط لون كل تقويم وإمكانية ظهوره في إعدادات التقويمات.</string>
<string name="calendars_visibility_hint">قم بإيقاف تقويم لإخفائه على هذا الجهاز — تختفي أحداثه من التطبيق ويتوقف عن تذكيرك. هذا هو مفتاح التبديل نفسه الذي تستخدمه تطبيقات التقويم الأخرى خاصتك، لذلك تقوم بإخفائه أيضًا. لا شيء يتم حذفه، ولا يتأثر أي جهاز آخر، ويمكنك إعادة تشغيله من هنا في أي وقت.</string> <string name="calendars_visibility_hint">قم بإيقاف تقويم لإخفائه على هذا الجهاز — تختفي أحداثه من التطبيق ويتوقف عن تذكيرك. هذا هو مفتاح التبديل نفسه الذي تستخدمه تطبيقات التقويم الأخرى خاصتك، لذلك تقوم بإخفائه أيضًا. لا شيء يتم حذفه، ولا يتأثر أي جهاز آخر، ويمكنك إعادة تشغيله من هنا في أي وقت.</string>
<string name="settings_allday_reminder_fires_at">يتم الإشعار في %1$s</string> <string name="settings_allday_reminder_fires_at">يتم الإشعار في %1$s</string>
<string name="settings_agenda_widget_range">نطاق ودجت الجدول</string> <string name="settings_agenda_widget_range">مدى أداة الجدول</string>
<string name="settings_widget_size">حجم ودجت الجدول</string> <string name="settings_widget_size">حجم أداة الجدول</string>
<string name="settings_widgets_subtitle">ودجت الجدول، عنصر الإعدادات السريعة</string> <string name="settings_widgets_subtitle">أداة الجدول، عنصر الإعدادات السريعة</string>
<string name="settings_drag_to_reschedule">السحب لتعديل الموعد</string> <string name="settings_drag_to_reschedule">السحب لتعديل الموعد</string>
<string name="settings_drag_to_reschedule_summary">انقل حدثًا عن طريق سحبه إلى يوم أو وقت آخر</string> <string name="settings_drag_to_reschedule_summary">انقل حدثًا عن طريق سحبه إلى يوم أو وقت آخر</string>
<string name="event_move_blocked_series_end">لا يمكن نقل حدث بعد نهاية سلسلته</string> <string name="event_move_blocked_series_end">لا يمكن نقل حدث بعد نهاية سلسلته</string>
<string name="crash_dialog_report">تقرير</string> <string name="crash_dialog_report">تقرير</string>
<string name="settings_quick_switch_hint">اختر أي طُرق العرض التي يتم التنقل بينها باستخدام الزر الموجود في أعلى اليمين، واسحب لإعادة ترتيبها. تظل طرق العرض التي تم إيقاف تشغيلها متاحة من قائمة التنقل.</string> <string name="settings_quick_switch_hint">اختر أي طُرق العرض التي يتم التنقل بينها باستخدام الزر في أعلى اليمين، واسحب لإعادة ترتيبهم. تظل طُرق العرض التي تم إيقافها متاحة من قائمة التنقل.</string>
<string name="calendars_account_from_source">%1$s (%2$s)</string> <string name="calendars_account_from_source">%1$s (%2$s)</string>
<string name="duration_hours_minutes">%1$s %2$s</string> <string name="duration_hours_minutes">%1$s %2$s</string>
<string name="settings_widgets_hint">إعدادات لوجدتز الشاشة الرئيسية وعنصر لوحة الإعدادات السريعة. أضف وجدت عن طريق الضغط لفترة طويلة على شاشتك الرئيسية.</string> <string name="settings_widgets_hint">إعدادات لأدوات الشاشة الرئيسية وعنصر الإعدادات السريعة. أضف أداة عن طريق الضغط لفترة طويلة على شاشتك الرئيسية.</string>
<string name="import_warning_attendees">لم يتم استيراد قوائم الضيوف.</string> <string name="import_warning_attendees">لم يتم استيراد قوائم الضيوف.</string>
<string name="settings_reliable_delivery_exempt">معفى من تحسين البطارية — تصل التذكيرات في الوقت المحدد.</string> <string name="settings_reliable_delivery_exempt">معفى من تحسين البطارية — تصل التذكيرات في الوقت المحدد.</string>
<string name="settings_reliable_delivery">توصيل موثوق</string> <string name="settings_reliable_delivery">توصيل موثوق</string>
<string name="settings_allday_reminder_time_hint">تنطلق التذكيرات لأحداث التي تستمر طوال اليوم عند %1$s</string> <string name="settings_allday_reminder_time_hint">تنطلق التذكيرات لأحداث التي تستمر طوال اليوم عند %1$s</string>
<string name="agenda_range_override_hint">فقط للآن — يعيد التعيين إلى المدى المحفوظ الخاص بك عند إعادة فتح Calendula.</string> <string name="agenda_range_override_hint">فقط للآن — يعيد التعيين للمدى المحفوظ خاصتك عند إعادة فتح Calendula.</string>
<string name="settings_past_events_sample_morning">مراجعة الفريق</string> <string name="settings_past_events_sample_morning">مراجعة الفريق</string>
<string name="settings_past_events_sample_midday">غداء مع روبن</string> <string name="settings_past_events_sample_midday">غداء مع روبن</string>
<string name="settings_past_events_sample_evening">ممارسة الجوقة</string> <string name="settings_past_events_sample_evening">ممارسة الجوقة</string>
<string name="settings_special_dates_last_synced">آخر مزامنة %1$s</string> <string name="settings_special_dates_last_synced">آخر مزامنة %1$s</string>
<string name="settings_special_dates_disable_type_message">يؤدي هذا لحذف التقويم ”%1$s“ وأحداثه. سيتم فقدان أي تذكيرات أو ملاحظات أضفتها إليه.</string> <string name="settings_special_dates_disable_type_message">يؤدي هذا لحذف التقويم ”%1$s“ وأحداثه. سيتم فقدان أي تذكيرات أو ملاحظات أضفتها إليه.</string>
<string name="special_dates_default_title_birthday">عيد ميلاد {name} ({year})</string> <string name="special_dates_default_title_birthday">عيد ميلاد {name} ({year})</string>
<string name="event_move_action">نقل…</string>
<string name="settings_font_specimen_heading">الخميس، 14 مايو</string>
<string name="settings_font_specimen_body">مراجعة الفريق في 10:00، ثم الغداء مع روبن في المقهى في ماركتبلاتز.</string>
<string name="settings_section_widgets">الأدوات المصغّرة والعناصر</string>
<string name="settings_widget_size_hint">مدى حجم النص الذي يعرضه جدول أداة الشاشة الرئيسية. أداة الشهر ليس لديه إعداد — تتناسب شبكته دائمًا نفسه للمساحة التي تمنحها إيّاه.</string>
<string name="settings_agenda_widget_range_hint">كم مدى يعرض جدول أداة الشاشة الرئيسية الأحداث.</string>
<string name="settings_agenda_range_hint">كم مدى يعرض شاشة الجدول الأحداث.</string>
<string name="app_name">Calendula</string>
<string name="font_atkinson_hyperlegible">Atkinson Hyperlegible</string>
<string name="settings_special_dates_show_year">إظهار السنة</string>
<string name="settings_special_dates_show_year_hint">تضمين {year} في العناوين عندما يكون معروفًا</string>
<string name="special_dates_default_title_custom">{name}</string>
<string name="special_dates_default_title_anniversary">ذكرى {name} السنوية ({year})</string>
<string name="special_dates_calendar_anniversary">الذكرات السنوية</string>
<string name="crash_dialog_message">أُغلق %1$s بشكل غير متوقع في المرة الأخيرة. يمكنك المساعدة في إصلاحه عن طريق إرسال هذا التقرير كمشكلة. سيبقى على جهازك حتى تختار مشاركته، ولا يتضمن أي بيانات شخصية أو محتوى تقويم — فقط التفاصيل الفنية أدناه.</string>
<string name="font_lora">Lora</string>
<string name="font_jetbrains_mono">JetBrains Mono</string>
<string name="onboarding_backup_benefit_folder_title">مجلد تختاره</string>
<string name="import_warning_timezone">منطقة زمنية غير معروفة ترجع إلى الخاصة بجهازك.</string>
<string name="settings_calendar_reminders_managed_hint">تعيين في تواريخ جهات الاتصال الخاصة</string>
<string name="settings_special_dates_disable_all_message">يؤدي هذا إلى حذف تقويمات تواريخ جهات الاتصال وأحداثها. سيتم فقدان أي تذكيرات أو ملاحظات أضفتها إليها.</string>
<string name="settings_special_dates_type_anniversary">الذكرات السنوية</string>
<string name="settings_special_dates_template_hint">استخدم {name} لجهة الاتصال و{year} للسنة (سنة الميلاد، أو سنة بدء الذكرى السنوية؛ مخفية عندما تكون غير معروفة).</string>
<string name="settings_special_dates_template">صيغة العنوان</string>
<string name="settings_calendar_reminders_hint">تجاوز الافتراضي لكل تقويم — بشكل منفصل للمحدد بوقت وأحداث طوال اليوم. يمكن لتقويم الاحتفاظ بالافتراضي، إسقاطه أو تعيين خاصّته.</string>
<string name="event_move_occurrence_only">سيؤدي نقل السلسلة بأكملها لتغيير الأيام التي تقع فيها، لذلك يمكن نقل هذا الحدث فقط.</string>
<string name="agenda_empty_title">أنت على اطلاع</string>
</resources> </resources>

View File

@@ -0,0 +1,149 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="backup_interval_units">
<item>Minutes</item>
<item>Hours</item>
<item>Days</item>
<item>Weeks</item>
</string-array>
<string name="app_tagline">Сучасны каляндар.</string>
<string name="state_loading">Загрузка…</string>
<string name="state_retry">Паўтарыць спробу</string>
<string name="state_failure_unknown">Узнікла нейкая праблема.</string>
<string name="state_failure_permission">Патрабуецца доступ да календара.</string>
<string name="state_failure_permission_action">Дазволіць доступ</string>
<string name="state_failure_no_calendars">Календары не сканфігураваныя.</string>
<string name="state_failure_no_calendars_action">Адкрыць сыстэмныя налады календароў</string>
<string name="state_failure_provider">Немагчыма прачытаць каляндар.</string>
<string name="permission_rationale_title">Прагляд усіх падзеяў у прыгожым выглядзе</string>
<string name="permission_rationale_body">Для паказу і кіраваньня падзеямі Calendula патрабуецца доступ да вашага календара.</string>
<string name="permission_request_button">Дазволіць доступ да календара</string>
<string name="permission_denied_title">Няма доступу да календара</string>
<string name="permission_denied_body">Calendula ня можа паказваць падзеі бяз доступу да календара.</string>
<string name="permission_open_settings_button">Адкрыць сыстэмныя налады</string>
<string name="permission_retry_button">Паўтарыць спробу</string>
<string name="permission_benefit_private_title">Распрацавана з акцэнтам на прыватнасьць</string>
<string name="permission_benefit_private_body">Бяз доступу да Інтэрнэту, без тэлемэтрыі — нічога і ніколі не пакідае ваш тэлефон.</string>
<string name="permission_benefit_sync_title">Усе вашы календары разам</string>
<string name="permission_benefit_sync_body">Google, CalDAV, лякальныя — паказваюцца ўсе, сынхранізаваныя з прыладай.</string>
<string name="permission_benefit_privacy_title">Ніякага адсочваньня, ніколі</string>
<string name="permission_benefit_privacy_body">Без тэлемэтрыі, без аналітыкі, без рэклямы.</string>
<string name="permission_privacy_footnote">Усё застаецца на прыладзе, доступ да Інтэрнэту не патрабуецца</string>
<string name="month_prev">Папярэдні месяц</string>
<string name="month_next">Наступны месяц</string>
<string name="month_today_action">Сёньня</string>
<string name="month_more_actions">Іншыя дзеяньні</string>
<string name="month_open_menu">Адкрыць мэню</string>
<string name="month_action_settings">Налады</string>
<string name="month_a11y_today_prefix">Сёньня</string>
<string name="week_today_action">Гэты тыдзень</string>
<string name="week_number_label">Тдз.</string>
<string name="day_today_action">Сёньня</string>
<string name="event_detail_back">Назад</string>
<string name="event_detail_edit">Праўка</string>
<string name="event_detail_delete">Выдаліць</string>
<string name="event_detail_share">Падзяліцца</string>
<string name="event_detail_duplicate">Дублікат</string>
<string name="event_share_chooser_title">Падзяліцца падзеяй</string>
<string name="event_share_failed">Не атрымалася падзяліцца гэтай падзеяй.</string>
<string name="event_delete_title">Выдаліць падзею?</string>
<string name="event_delete_body">Падзея выдаленая з вашага календара і з усіх прыладаў, зь якімі адбываецца сынхранізацыя.</string>
<string name="event_delete_recurring_title">Выдаліць пэрыядычную падзею</string>
<string name="event_delete_option_occurrence">Толькі гэтую падзею</string>
<string name="event_delete_option_following">Гэтую і ўсе наступныя падзеі</string>
<string name="event_delete_option_series">Усе падзеі ў пасьлядоўнасьці</string>
<string name="event_edit_recurring_title">Рэдагаваць пэрыядычную падзею</string>
<string name="event_delete_failed">Не атрымалася выдаліць падзею</string>
<string name="event_delete_write_denied">Для выдаленьня падзеяў Calendula патрабуецца дазвол на запіс</string>
<string name="event_move_action">Перанесці…</string>
<string name="event_move_recurring_title">Перанесці пэрыядычную падзею</string>
<string name="event_move_occurrence_only">Перанос усёй пасьлядоўнасьці зьменіць дні, на якія яна прыпадае, таму перанесці можна толькі гэтую падзею.</string>
<string name="event_move_done">Перанесена на %1$s</string>
<string name="event_move_undo">АдрабІць</string>
<string name="event_move_undone">Перанос скасаваны</string>
<string name="event_move_failed">Не атрымалася перанесьці падзею</string>
<string name="event_move_write_denied">Для пераносу падзеяў Calendula патрабуецца дазвол на запіс</string>
<string name="event_move_gone">Гэтай падзеі больш не існуе</string>
<string name="event_move_blocked_series_end">Нельга перанесьці падзею на дату пасьля заканчэньня яе пасьлядоўнасьці</string>
<string name="dialog_cancel">Адмена</string>
<string name="dialog_ok">ОК</string>
<string name="event_edit_new_title">Новая падзея</string>
<string name="event_edit_close">Закрыць</string>
<string name="event_edit_save">Захаваць</string>
<string name="event_edit_title_hint">Дадаць загаловак</string>
<string name="event_edit_managed_hint">Кіруецца «%1$s» — загаловак, дата і паўтор сынхранізуюцца з вашымі кантактамі. Напаміны, месцазнаходжаньне і нататкі даступныя вам для рэдагаваньня.</string>
<string name="event_edit_starts">Пачатак</string>
<string name="event_edit_ends">Заканчэньне</string>
<string name="event_edit_error_end_before_start">Заканчваецца раней, чым пачынаецца</string>
<string name="event_edit_error_no_calendar">Няма календара, у які дазволены запіс</string>
<string name="event_edit_save_failed">Не атрымалася захаваць падзею</string>
<string name="event_edit_write_denied">Для стварэньня падзеяў Calendula патрабуецца дазвол на запіс</string>
<string name="event_edit_more_fields">Дадатковыя палі</string>
<string name="event_edit_add">Дадаць</string>
<string name="event_edit_add_reminder">Дадаць напамін</string>
<string name="event_edit_remove_reminder">Выдаліць напамін</string>
<string name="event_edit_attendees">Наведвальнікі</string>
<string name="event_edit_add_guest">Дадаць наведвальніка</string>
<string name="event_edit_add_guest_hint">Дадаць наведвальніка праз электронную пошту…</string>
<string name="event_edit_add_guest_from_contacts">Дадаць з кантактаў</string>
<string name="event_edit_location_from_contacts">Выбраць адрас з кантактаў</string>
<string name="event_edit_remove_guest">Выдаліць наведвальніка</string>
<string name="event_edit_attendee_required">Неабходны</string>
<string name="event_edit_attendee_optional">Неабавязковы</string>
<string name="event_edit_attendees_note_synced">Calendula не рассылае запрашэньні. Наведвальнікі могуць атрымаць электронныя лісты з вашага ўліковага запісу календара пры сынхранізацыі.</string>
<string name="event_edit_attendees_note_local">Захавана на гэтай прыладзе. Ніхто не атрымаў абвесткі.</string>
<string name="event_edit_reminder_custom">Карыстальніцкі</string>
<string name="reminder_unit_minutes">хв.</string>
<string name="reminder_unit_hours">гадз.</string>
<string name="reminder_unit_days">сут.</string>
<string name="reminder_unit_weeks">тыд.</string>
<string name="event_edit_availability">Даступнасьць</string>
<string name="event_edit_visibility">Бачнасьць</string>
<string name="event_edit_timezone_device">Часавы пояс прылады</string>
<string name="event_edit_timezone_device_summary">Сочыць за вашым месцазнаходжаньнем</string>
<string name="event_edit_timezone_search">Пошук часавых паясоў</string>
<string name="event_edit_timezone_recent">Апошнія</string>
<string name="event_edit_timezone_all">Усе часавыя паясы</string>
<string name="event_edit_timezone_none">Няма часавага поясу, які адпавядае «%1$s»</string>
<string name="event_edit_timezone_local_time">%1$s паводле вашага часу</string>
<string name="event_edit_color">Колер</string>
<string name="event_edit_color_default">Колер календара</string>
<string name="event_edit_color_custom">Карыстальніцкі колер</string>
<string name="event_edit_color_reset">Скід</string>
<string name="event_edit_color_unsupported">Недаступна для гэтага календара</string>
<string name="event_edit_color_unsupported_hint">Для гэтага календара не апублікаваны набор колераў. Для падобных календароў у наладах можна задаць карыстальніцкія колеры.</string>
<string name="event_edit_color_sync_warning">Пры наступнай сынхранізацыі гэты каляндар можа страціць або перазапісаць колер.</string>
<string name="event_edit_conflict_title">Падзея зьмянілася ў іншым месцы</string>
<string name="event_edit_conflict_body">Падчас рэдагаваньня вамі гэтая падзея была зьменена ў выніку сынхранізацыі або іншай праграмай. Што адбудзецца з вашымі зьменамі?</string>
<string name="event_edit_conflict_overwrite">Захаваць мае змены</string>
<string name="event_edit_conflict_overwrite_hint">Толькі адрэдагаваныя вамі палі перазапішуць зьнешнія зьмены</string>
<string name="event_edit_conflict_discard">Адкінуць мае зьмены</string>
<string name="event_edit_conflict_discard_hint">Падзея застаецца такой, якой яна ёсьць зараз</string>
<string name="event_edit_gone_title">Падзея выдалена</string>
<string name="event_edit_gone_body">Гэтая падзея ўжо выдаленая, напрыклад на іншай прыладзе. Вашыя зьмены болей немагчыма захаваць.</string>
<string name="import_reminder_prompt_title">Ужыць напамін па змаўчаньні?</string>
<string name="import_reminder_prompt_body_none">Гэтая падзея была імпартаваная без напаміну.</string>
<plurals name="import_reminder_prompt_body_existing">
<item quantity="one">"Гэтая падзея была імпартаваная з наст. колькасьцю напамінаў: %1$d."</item>
<item quantity="few">"Гэтая падзея была імпартаваная з наст. колькасьцю напамінаў: %1$d."</item>
<item quantity="many">"Гэтая падзея была імпартаваная з наст. колькасьцю напамінаў: %1$d."</item>
<item quantity="other">"Гэтая падзея была імпартаваная з наст. колькасьцю напамінаў: %1$d."</item>
</plurals>
<string name="import_reminder_prompt_apply">Ужыць змаўчаньні</string>
<string name="import_reminder_prompt_keep">Пакінуць як ёсьць</string>
<string name="event_edit_recurrence_none">Не паўтараецца</string>
<string name="event_edit_recurrence_custom">Карыстальніцкі варыянт</string>
<string name="event_edit_recurrence_every">Кожн.</string>
<string name="recurrence_unit_days">сут.</string>
<string name="recurrence_unit_weeks">тыд.</string>
<string name="recurrence_unit_months">мес.</string>
<string name="recurrence_unit_years">год/гады</string>
<string name="event_edit_recurrence_incomplete">Увядзеце лік ад 1 да 999</string>
<string name="event_edit_recurrence_next">Наступ.: %1$s</string>
<string name="event_edit_recurrence_next_none">Гэтае правіла ніколі не паўтараецца</string>
<string name="event_edit_recurrence_ends">Заканчэньне</string>
<string name="event_edit_recurrence_end_never">Ніколі</string>
<string name="event_edit_recurrence_end_until">На дату</string>
<string name="event_edit_recurrence_end_count">Пасьля некалькіх разоў</string>
<string name="event_edit_recurrence_times">разы(оў)</string>
</resources>

View File

@@ -133,4 +133,47 @@
<string name="import_reminder_prompt_apply">Применить стандартное</string> <string name="import_reminder_prompt_apply">Применить стандартное</string>
<string name="import_reminder_prompt_keep">Оставить как есть</string> <string name="import_reminder_prompt_keep">Оставить как есть</string>
<string name="event_edit_recurrence_none">Не повторяется</string> <string name="event_edit_recurrence_none">Не повторяется</string>
<string name="event_edit_recurrence_custom">Свой</string>
<string name="event_edit_recurrence_every">Каждый</string>
<string name="recurrence_unit_days">дни</string>
<string name="recurrence_unit_weeks">недели</string>
<string name="recurrence_unit_months">месяцы</string>
<string name="recurrence_unit_years">года</string>
<string name="event_edit_recurrence_incomplete">Введите число от 1 до 999</string>
<string name="event_edit_recurrence_next_none">Это правило никогда не повторяется</string>
<string name="event_edit_recurrence_ends">Заканчивается</string>
<string name="event_edit_recurrence_end_never">Никогда</string>
<string name="event_edit_recurrence_end_until">До</string>
<string name="event_edit_recurrence_end_count">После определённого количества</string>
<string name="event_availability_busy">Занят</string>
<string name="event_access_default">Стандартный</string>
<string name="event_access_public">Публичный</string>
<string name="event_detail_all_day">Весь день</string>
<string name="event_detail_calendar">Календарь</string>
<string name="event_detail_calendar_unknown">Неизвестный календарь</string>
<string name="event_detail_location">Местоположение</string>
<string name="event_detail_description">Описание</string>
<string name="event_detail_recurring">Повторяющееся событие</string>
<string name="recurrence_daily">Каждый день</string>
<string name="recurrence_weekly">Каждую неделю</string>
<string name="recurrence_monthly">Каждый месяц</string>
<string name="recurrence_yearly">Каждый год</string>
<string name="recurrence_every_n_days">Каждые %1$d дней</string>
<string name="recurrence_every_n_weeks">Каждые %1$d недель</string>
<string name="recurrence_every_n_months">Каждые %1$d месяцев</string>
<string name="recurrence_every_n_years">Каждые %1$d лет</string>
<string name="event_detail_not_found">Это событие больше не существует.</string>
<string name="event_attendee_accepted">Принято</string>
<string name="event_attendee_declined">Отклонено</string>
<string name="event_attendee_needs_action">Без ответа</string>
<string name="event_detail_reminders">Напоминания</string>
<string name="event_detail_timezone">Временная зона</string>
<string name="event_status_cancelled">Отменён</string>
<string name="event_availability_free">Бесплатно</string>
<string name="event_access_private">Приватно</string>
<string name="event_access_confidential">Конфиденциально</string>
<string name="event_access_public_summary">Любой с доступом может видеть все детали</string>
<string name="event_access_private_summary">Другие видят только, что вы заняты</string>
<string name="event_detail_self_response">Ваш ответ: %1$s</string>
<string name="reminder_at_time">Во время начала события</string>
</resources> </resources>

View File

@@ -0,0 +1,190 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="backup_interval_units">
<item>Minutes</item>
<item>Hours</item>
<item>Days</item>
<item>Weeks</item>
</string-array>
<string name="app_name">Calendula</string>
<string name="app_tagline">Moderný kalendár.</string>
<string name="state_loading">Načítava sa…</string>
<string name="state_retry">Opakovať</string>
<string name="state_failure_unknown">Niečo sa pokazilo.</string>
<string name="state_failure_permission">Je potrebný prístup ku kalendáru.</string>
<string name="state_failure_permission_action">Udeliť prístup</string>
<string name="state_failure_no_calendars">Nie sú nakonfigurované žiadne kalendáre.</string>
<string name="state_failure_no_calendars_action">Otvoriť systémové nastavenie kalendára</string>
<string name="state_failure_provider">Nepodarilo sa načítať kalendár.</string>
<string name="permission_rationale_title">Prehľadávajte všetky svoje udalosti v krásnom prevedení</string>
<string name="permission_rationale_body">Calendula potrebuje prístup k vášmu kalendáru, aby mohla zobraziť a spravovať vaše udalosti.</string>
<string name="permission_request_button">Udeliť prístup k kalendáru</string>
<string name="permission_denied_title">Prístup ku kalendáru bol zamietnutý</string>
<string name="permission_denied_body">Calendula nemôže zobraziť udalosti bez prístupu ku kalendáru. Môžete ho znova udeliť v nastavení systému.</string>
<string name="permission_open_settings_button">Otvoriť systémové nastavenia</string>
<string name="permission_retry_button">Skúsiť znova</string>
<string name="permission_benefit_private_title">Súkromné už od začiatku</string>
<string name="permission_benefit_private_body">Žiadny prístup k internetu, žiadna telemetria - nikdy nič z vášho telefónu neunikne.</string>
<string name="permission_benefit_sync_title">Všetky vaše kalendáre pohromade</string>
<string name="permission_benefit_sync_body">Google, CalDAV, lokálne kalendáre - čokoľvek synchronizované zo zariadením sa proste objaví.</string>
<string name="permission_benefit_privacy_title">Nikdy žiadne sledovanie</string>
<string name="permission_benefit_privacy_body">Žiadna telemetria, žiadne analytické nástroje, bez reklám.</string>
<string name="permission_privacy_footnote">Ostáva na vašom zariadení · bez prístupu k internetu</string>
<string name="month_prev">Predchádzajúci mesiac</string>
<string name="month_next">Nasledujúci mesiac</string>
<string name="month_today_action">Dnes</string>
<string name="month_more_actions">Ďalšie možnosti</string>
<string name="month_open_menu">Otvoriť menu</string>
<string name="month_action_settings">Nastavenia</string>
<string name="month_a11y_today_prefix">Dnes</string>
<string name="week_today_action">Tento víkend</string>
<string name="week_number_label">týžd.</string>
<string name="day_today_action">Dnes</string>
<string name="event_detail_back">Späť</string>
<string name="event_detail_edit">Upraviť</string>
<string name="event_detail_delete">Vymazať</string>
<string name="event_detail_share">Zdielať</string>
<string name="event_detail_duplicate">Duplikovať</string>
<string name="event_share_chooser_title">Zdielať udalosť</string>
<string name="event_share_failed">Túto udalosť nieje možné zdieľať.</string>
<string name="event_delete_title">Odstrániť udalosť?</string>
<string name="event_delete_body">Udalosť je odstránená z vášho kalendára aj zo všetkých synchronizovaných zariadení.</string>
<string name="event_delete_recurring_title">Odstrániť opakujúcu sa udalosť</string>
<string name="event_delete_option_occurrence">Iba túto udalosť</string>
<string name="event_delete_option_following">Túto aj všetky nasledujúce udalosti</string>
<string name="event_delete_option_series">Všetky udalosti v sérii</string>
<string name="event_edit_recurring_title">Upraviť opakujúcu sa udalosť</string>
<string name="event_delete_failed">Túto udalosť nebolo možné odstrániť</string>
<string name="event_delete_write_denied">Calendula potrebuje povolenie k zápisu, aby mohla odstrániť udalosti</string>
<string name="event_move_action">Presunúť…</string>
<string name="event_move_recurring_title">Presunúť opakujúcu sa udalosť</string>
<string name="event_move_occurrence_only">Presunútie celej série by sa zmenilo poradie dňou, na ktorú séria pripadá, takže presunúť je možné iba túto udalosť.</string>
<string name="event_move_done">Presunuté na %1$s</string>
<string name="event_move_undo">Vrátiť späť</string>
<string name="event_move_undone">Navrátené naspäť</string>
<string name="event_move_failed">Nemožno presunúť udalosť</string>
<string name="event_move_write_denied">Calendula potrebuje povolenie k zápisu, aby mohla presúvať udalosti</string>
<string name="event_move_gone">Táto udalosť už neexistuje</string>
<string name="event_move_blocked_series_end">Nie je možné presunúť udalosť za koniec danej série</string>
<string name="dialog_cancel">Zrušiť</string>
<string name="dialog_ok">OK</string>
<string name="event_edit_new_title">Nová udalosť</string>
<string name="event_edit_close">Zavrieť</string>
<string name="event_edit_save">Uložiť</string>
<string name="event_edit_title_hint">Pridať názov</string>
<string name="event_edit_managed_hint">Spravové kalendárom “%1$s” — názov, dátum a opakovanie je synchronizované z vašich kontaktov. Pripomienky, poloha a poznámky môžte upraviť.</string>
<string name="event_edit_starts">Začiatok</string>
<string name="event_edit_ends">Koniec</string>
<string name="event_edit_error_end_before_start">Končí pred začiatkom</string>
<string name="event_edit_error_no_calendar">Není dostupný kalendár, ktorý je možno upravovať</string>
<string name="event_edit_save_failed">Nemožno udalosť uložiť</string>
<string name="event_edit_write_denied">Calendula potrebuje povolenie k zápisu, aby mohla vytvárať udalosti</string>
<string name="event_edit_more_fields">Viac poli</string>
<string name="event_edit_add">Pridať</string>
<string name="event_edit_add_reminder">Pridať pripomienku</string>
<string name="event_edit_remove_reminder">Odstrániť pripomienku</string>
<string name="event_edit_attendees">Hostia</string>
<string name="event_edit_add_guest">Pridať hosťa</string>
<string name="event_edit_add_guest_hint">Pridať hosťa e-mailom…</string>
<string name="event_edit_add_guest_from_contacts">Pridať z kontaktov</string>
<string name="event_edit_location_from_contacts">Vybrať adresu z kontaktov</string>
<string name="event_edit_remove_guest">Odstrániť hosťa</string>
<string name="event_edit_attendee_required">Nutné</string>
<string name="event_edit_attendee_optional">Voliteľné</string>
<string name="event_edit_attendees_note_synced">Calendula neposiela pozvánky. Váš kalendárny účet ich môže poslať keď sa synchronizuje.</string>
<string name="event_edit_attendees_note_local">Uložené na tomto zariadení. Nikomu nebude poslaná notifikácia.</string>
<string name="event_edit_reminder_custom">Vlastné</string>
<string name="reminder_unit_minutes">minúty</string>
<string name="reminder_unit_hours">hodiny</string>
<string name="reminder_unit_days">dni</string>
<string name="reminder_unit_weeks">týždne</string>
<string name="event_edit_availability">Dostupnosť</string>
<string name="event_edit_visibility">Viditeľnosť</string>
<string name="event_edit_timezone_device">Časové pásmo zariadenia</string>
<string name="event_edit_timezone_device_summary">Sleduje vás, nech ste kdekoľvek</string>
<string name="event_edit_timezone_search">Vyhľadať časové pásmo</string>
<string name="event_edit_timezone_recent">Nedávne</string>
<string name="event_edit_timezone_all">Všetky časové pásma</string>
<string name="event_edit_timezone_none">Žiadne časové pásmo nezodpovedá “%1$s”</string>
<string name="event_edit_timezone_local_time">%1$s vášho času</string>
<string name="event_edit_color">Farba</string>
<string name="event_edit_color_default">Farba kalendáru</string>
<string name="event_edit_color_custom">Vlastná farba</string>
<string name="event_edit_color_reset">Obnoviť</string>
<string name="event_edit_color_unsupported">Není k dispozícii pre tento kalendár</string>
<string name="event_edit_color_unsupported_hint">Tento kalendár nemá vlastnú sadu farieb. Pre takýto kalendár môžte povoliť vlastné farby v nastaveniach.</string>
<string name="event_edit_color_sync_warning">Tento kalendár môže vymazať alebo prepísať farbu pri ďalšej synchronizácii.</string>
<string name="event_edit_conflict_title">Udalosť bola zmenená niekde inde</string>
<string name="event_edit_conflict_body">Počas úprav sa táto udalosť zmenila — synchronizáciou alebo inou aplikáciou. Čo sa má stať s vašimi zmenami?</string>
<string name="event_edit_conflict_overwrite">Uložiť zmeny</string>
<string name="event_edit_conflict_overwrite_hint">Iba u upravených polí sa prepíše zmena z iného zdroja</string>
<string name="event_edit_conflict_discard">Zrušiť zmeny</string>
<string name="event_edit_conflict_discard_hint">Udalosť ostáva bez zmeny</string>
<string name="event_edit_gone_title">Udalosť odstránená</string>
<string name="event_edit_gone_body">Počas úprav bola táto udalosť odstránená, napríklad na inom zariadení. Vaše zmeny už nie je možné uložiť.</string>
<string name="import_reminder_prompt_title">Chcete použiť predvolenú pripomienku?</string>
<string name="import_reminder_prompt_body_none">Táto udalosť bola importovaná bez pripomienky.</string>
<plurals name="import_reminder_prompt_body_existing">
<item quantity="one">Táto udalosť bola importovaná s %1$d pripomienkou.</item>
<item quantity="few">Táto udalosť bola importovaná s %1$d pripomienkami.</item>
<item quantity="many">Táto udalosť bola importovaná s %1$d pripomienkami.</item>
<item quantity="other">Táto udalosť bola importovaná s %1$d pripomienkami.</item>
</plurals>
<string name="import_reminder_prompt_apply">Nastaviť predvolené</string>
<string name="import_reminder_prompt_keep">Nechať bez zmeny</string>
<string name="event_edit_recurrence_none">Neopakuje sa</string>
<string name="event_edit_recurrence_custom">Vlastné</string>
<string name="event_edit_recurrence_every">Každý</string>
<string name="recurrence_unit_days">dni</string>
<string name="recurrence_unit_weeks">týždne</string>
<string name="recurrence_unit_months">mesiace</string>
<string name="recurrence_unit_years">roky</string>
<string name="event_edit_recurrence_incomplete">Zadajte číslo od 1 do 999</string>
<string name="event_edit_recurrence_next">Nasledujúce: %1$s</string>
<string name="event_edit_recurrence_next_none">Toto pravidlo sa nikdy neopakuje</string>
<string name="event_edit_recurrence_ends">Koniec</string>
<string name="event_edit_recurrence_end_never">Nikdy</string>
<string name="event_edit_recurrence_end_until">K určitému dátumu</string>
<string name="event_edit_recurrence_end_count">Po určitom počtu opakovaní</string>
<string name="event_edit_recurrence_times">opakovaní</string>
<string name="event_edit_error_recurrence_ends_before_start">Opakovanie končí pred začiatkom udalosti</string>
<string name="event_availability_busy">Zaneprázdnený</string>
<string name="event_access_default">Predvolený</string>
<string name="event_access_public">Verejný</string>
<string name="event_detail_all_day">Celý deň</string>
<string name="event_detail_calendar">Kalendár</string>
<string name="event_detail_calendar_unknown">Neznámy kalendár</string>
<string name="event_detail_location">Poloha</string>
<string name="event_detail_description">Popis</string>
<string name="event_detail_attendees">Účastníci</string>
<string name="event_detail_recurrence">Opakovanie</string>
<string name="event_detail_recurring">Opakovaná udalosť</string>
<string name="recurrence_daily">Každý deň</string>
<string name="recurrence_weekly">Každý týždeň</string>
<string name="recurrence_monthly">Každý mesiac</string>
<string name="recurrence_yearly">Každý rok</string>
<string name="recurrence_every_n_days">Každý %1$d. deň</string>
<string name="recurrence_every_n_weeks">Každý %1$d. týždeň</string>
<string name="recurrence_every_n_months">Každý %1$d. mesiac</string>
<string name="recurrence_every_n_years">Každý %1$d. rok</string>
<string name="recurrence_on_days">%1$s v %2$s</string>
<string name="recurrence_with_until">%1$s do%2$s</string>
<string name="recurrence_with_count">%1$s, %2$d opakovaní</string>
<string name="event_detail_not_found">Táto udalosť už neexistuje.</string>
<string name="event_attendee_accepted">Akceptované</string>
<string name="event_attendee_declined">Zamietnuté</string>
<string name="event_attendee_tentative">Predbežne</string>
<string name="event_attendee_needs_action">Bez odozvy</string>
<string name="event_attendee_unknown"></string>
<string name="event_detail_reminders">Pripomienky</string>
<string name="event_detail_timezone">Časové pásmo</string>
<string name="event_status_tentative">Predbežne</string>
<string name="event_status_cancelled">Zrušené</string>
<string name="event_availability_free">Voľno</string>
<string name="event_access_private">Súkromné</string>
<string name="event_access_confidential">Tajné</string>
<string name="event_access_default_summary">Bežná činnosť kalendáru</string>
<string name="event_access_public_summary">Kdekoľvek s prístupom vidí všetky detaily</string>
<string name="event_access_private_summary">Ostatný vidia iba že ste zaneprázdnený</string>
<string name="event_access_confidential_summary">Označené ako utajené; záleží na kalendárnom účtu čo toto znamená</string>
</resources>

View File

@@ -0,0 +1,166 @@
package de.jeanlucmakiola.calendula.widget
import com.google.common.truth.Truth.assertThat
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import org.junit.jupiter.api.Test
import kotlin.time.Duration
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Instant
/**
* The rollover alarm is what makes the widgets stop highlighting yesterday
* (#228), so the "when is the next local midnight" arithmetic is the one piece
* worth pinning down — especially where midnight is not 00:00.
*/
class WidgetRolloverSchedulerTest {
private val berlin = TimeZone.of("Europe/Berlin")
private fun at(local: String, zone: TimeZone): Instant =
LocalDateTime.parse(local).toInstant(zone)
private fun nextRollover(local: String, zone: TimeZone = berlin): Instant =
WidgetRolloverScheduler.nextRolloverAt(at(local, zone), zone)
// --- the ordinary day ----------------------------------------------------
@Test
fun `midday rolls over at the coming midnight`() {
val next = nextRollover("2026-08-27T12:00:00")
assertThat(next).isEqualTo(at("2026-08-28T00:00:05", berlin))
}
@Test
fun `a second before midnight still targets tonight, not tomorrow night`() {
val next = nextRollover("2026-08-27T23:59:59")
assertThat(next).isEqualTo(at("2026-08-28T00:00:05", berlin))
}
@Test
fun `at midnight exactly the target is the next day, never the current instant`() {
// The alarm has just fired and is re-arming: it must move a whole day on,
// otherwise the widget would wake itself in a tight loop.
val now = at("2026-08-28T00:00:00", berlin)
val next = WidgetRolloverScheduler.nextRolloverAt(now, berlin)
assertThat(next).isEqualTo(at("2026-08-29T00:00:05", berlin))
assertThat(next - now).isGreaterThan(Duration.ZERO)
}
@Test
fun `re-arming from the slack instant itself moves a full day on`() {
// What actually happens in practice: the receiver runs at midnight + slack.
val now = at("2026-08-28T00:00:05", berlin)
assertThat(WidgetRolloverScheduler.nextRolloverAt(now, berlin))
.isEqualTo(at("2026-08-29T00:00:05", berlin))
}
@Test
fun `the result is always in the future for every minute of a day`() {
val zone = berlin
// Spans Berlin's 2024 spring-forward, the case most likely to produce a
// target in the past and so an alarm that fires immediately, forever.
var probe = LocalDateTime.parse("2024-03-29T00:00:00").toInstant(zone)
val end = LocalDateTime.parse("2024-04-01T00:00:00").toInstant(zone)
while (probe < end) {
assertThat(WidgetRolloverScheduler.nextRolloverAt(probe, zone)).isGreaterThan(probe)
probe += 1.minutes
}
}
// --- daylight saving -----------------------------------------------------
@Test
fun `spring forward keeps the rollover one day away, not one hour short`() {
// Berlin skipped 02:00-03:00 on 31 March 2024, so that day was 23h long.
// A rollover computed as "now + 24h" would land at 01:00 on 1 April.
val now = at("2024-03-30T12:00:00", berlin)
val next = WidgetRolloverScheduler.nextRolloverAt(now, berlin)
assertThat(next).isEqualTo(at("2024-03-31T00:00:05", berlin))
assertThat(next - now).isLessThan(24.hours)
}
@Test
fun `fall back does not overshoot into the repeated hour`() {
// Berlin repeated 02:00-03:00 on 27 October 2024: midnight itself is
// unambiguous, but the day is 25h long, so "now + 24h" would land at
// 23:00 on the 26th and never roll the date over at all.
val now = at("2024-10-26T12:00:00", berlin)
val next = WidgetRolloverScheduler.nextRolloverAt(now, berlin)
assertThat(next).isEqualTo(at("2024-10-27T00:00:05", berlin))
assertThat(next.toLocalDateTime(berlin).date).isEqualTo(LocalDate.parse("2024-10-27"))
}
@Test
fun `a zone that repeats midnight takes the first start of day`() {
// Sao Paulo used to end DST by moving 00:00 back to 23:00, so the day
// began twice. Pinned deliberately: the widget then runs an hour ahead
// of the clock until the next redraw, which is the accepted trade
// (Brazil dropped DST in 2019, so no live zone does this).
val saoPaulo = TimeZone.of("America/Sao_Paulo")
val next = WidgetRolloverScheduler.nextRolloverAt(
at("2018-02-16T12:00:00", saoPaulo), saoPaulo,
)
val local = next.toLocalDateTime(saoPaulo)
assertThat(local.date).isEqualTo(LocalDate.parse("2018-02-17"))
assertThat(local.hour).isEqualTo(0)
}
@Test
fun `a zone where midnight does not exist rolls over at the real start of day`() {
// Cuba starts DST at 00:00, so 11 March 2018 began at 01:00 in Havana.
// Targeting a literal 00:00 there would arm an instant on the wrong day.
val havana = TimeZone.of("America/Havana")
val next = WidgetRolloverScheduler.nextRolloverAt(at("2018-03-10T12:00:00", havana), havana)
val local = next.toLocalDateTime(havana)
assertThat(local.date).isEqualTo(LocalDate.parse("2018-03-11"))
assertThat(local.hour).isEqualTo(1)
assertThat(local.minute).isEqualTo(0)
// And it is genuinely the first instant of that date, not a guess.
assertThat(next).isEqualTo(
LocalDate.parse("2018-03-11").atStartOfDayIn(havana) +
WidgetRolloverScheduler.ROLLOVER_SLACK,
)
}
// --- timezone changes ----------------------------------------------------
@Test
fun `the same instant rolls over at different times in different zones`() {
// Flying east and getting TIMEZONE_CHANGED must re-arm to the new local
// midnight — the arithmetic follows the zone, not a cached offset.
val instant = at("2026-08-27T12:00:00", berlin)
val tokyo = TimeZone.of("Asia/Tokyo")
val berlinNext = WidgetRolloverScheduler.nextRolloverAt(instant, berlin)
val tokyoNext = WidgetRolloverScheduler.nextRolloverAt(instant, tokyo)
assertThat(tokyoNext).isNotEqualTo(berlinNext)
assertThat(tokyoNext).isLessThan(berlinNext)
assertThat(tokyoNext.toLocalDateTime(tokyo).hour).isEqualTo(0)
}
@Test
fun `a half-hour offset zone still lands on its own midnight`() {
val kathmandu = TimeZone.of("Asia/Kathmandu")
val next = WidgetRolloverScheduler.nextRolloverAt(
at("2026-08-27T12:00:00", kathmandu), kathmandu,
)
val local = next.toLocalDateTime(kathmandu)
assertThat(local.date.toString()).isEqualTo("2026-08-28")
assertThat(local.hour).isEqualTo(0)
}
// --- the wiring ----------------------------------------------------------
@Test
fun `the receiver actually handles the action the alarm is sent with`() {
// The single point where the whole fix would die silently: the alarm
// fires, the receiver drops it on the action guard, nothing redraws.
assertThat(WidgetUpdateReceiver.HANDLED_ACTIONS)
.contains(WidgetUpdateReceiver.ACTION_ROLLOVER)
}
}

View File

@@ -172,9 +172,12 @@ F-Droid but not Play:
Until those are fixed, Play's graphics are managed by hand in the console. Then Until those are fixed, Play's graphics are managed by hand in the console. Then
pass `images:true` to the `listing` lane. pass `images:true` to the `listing` lane.
**Track.** Uploads go to `internal` by default; promotion to production stays a **Track.** Uploads go straight to `production` at a full rollout
manual action in the Play Console, matching the rule that UI releases wait for (`PLAY_RELEASE_STATUS=completed`). The merge to `main` is already the human
on-device review. Override with the `PLAY_TRACK` repo variable. gate — a release only gets there after on-device review — so a second manual
promotion in the Play Console added delay without adding a decision. Set the
`PLAY_TRACK` repo variable (e.g. `internal`) to stage a release instead, or
`PLAY_RELEASE_STATUS=draft` to hold it unpublished.
### Manual re-sign / recovery ### Manual re-sign / recovery
@@ -233,7 +236,7 @@ Two consequences worth remembering:
| Variable | Default | Purpose | | Variable | Default | Purpose |
| --- | --- | --- | | --- | --- | --- |
| `PLAY_TRACK` | `internal` | Play track the bundle is uploaded to. | | `PLAY_TRACK` | `production` | Play track the bundle is uploaded to. Set to `internal`/`alpha`/`beta` to stage instead. |
| `PLAY_RELEASE_STATUS` | `completed` | `completed`, `draft`, `inProgress` or `halted`. | | `PLAY_RELEASE_STATUS` | `completed` | `completed`, `draft`, `inProgress` or `halted`. |
| `PLAY_DRY_RUN` | `false` | `true` validates the Play edit against the API and discards it — use to rehearse. | | `PLAY_DRY_RUN` | `false` | `true` validates the Play edit against the API and discards it — use to rehearse. |

View File

@@ -29,7 +29,7 @@ platform :android do
supply( supply(
aab: aab, aab: aab,
track: options[:track] || "internal", track: options[:track] || "production",
release_status: options[:release_status] || "completed", release_status: options[:release_status] || "completed",
# The APK belongs to F-Droid and the Codeberg download; Play only ever # The APK belongs to F-Droid and the Codeberg download; Play only ever

View File

@@ -5,16 +5,16 @@ ksp = "2.3.11"
hilt = "2.60.1" hilt = "2.60.1"
coreKtx = "1.19.0" coreKtx = "1.19.0"
appcompat = "1.7.1" appcompat = "1.7.1"
lifecycleRuntime = "2.10.0" lifecycleRuntime = "2.11.0"
activityCompose = "1.13.0" activityCompose = "1.13.0"
composeBom = "2026.06.01" composeBom = "2026.06.01"
# Material 3 Expressive APIs currently live only in the 1.5 alpha line. # Material 3 Expressive APIs currently live only in the 1.5 alpha line.
# Pin explicitly to override the BOM (which ships stable 1.4.0). # Pin explicitly to override the BOM (which ships stable 1.4.0).
# Re-evaluate when 1.5.0 stable lands. # Re-evaluate when 1.5.0 stable lands.
material3 = "1.5.0-alpha25" material3 = "1.5.0-alpha26"
datastore = "1.2.1" datastore = "1.2.1"
junit = "6.1.2" junit = "6.1.3"
junitPlatform = "6.1.2" junitPlatform = "6.1.3"
truth = "1.4.5" truth = "1.4.5"
androidxJunit = "1.3.0" androidxJunit = "1.3.0"
espressoCore = "3.7.0" espressoCore = "3.7.0"