diff --git a/README.md b/README.md index 51ad69a..5344e43 100644 --- a/README.md +++ b/README.md @@ -1,83 +1,132 @@
Clockula is the clock sibling to [Calendula](https://codeberg.org/jlmakiola/calendula) (calendar) and [Agendula](https://codeberg.org/jlmakiola/agendula) (tasks). The -name follows theirs: *clock*, given Calendula's `-ula` ending. A Calendula flower +name follows theirs: *clock* with Calendula's `-ula` ending. A Calendula flower head is botanically a cluster of many small *florets*, so the three apps are -florets of one bloom. They also share one library, [floret-kit](https://codeberg.org/jlmakiola/floret-kit). +florets of one bloom. They share one library, +[floret-kit](https://codeberg.org/jlmakiola/floret-kit). -> 🧪 **Beta.** Clockula 1.0.0 is in beta: feature-complete, and being tested -> before the stable release. See [Install](#install) to try it. +1.0.0 is in its last betas before the stable release. -## The honest difference from its siblings +## Features -Calendula is a pure front-end over Android's `CalendarContract`. Agendula speaks -the dmfs `TaskContract`. Both put a good interface on a standard someone else -already maintains. +### Alarms -There is no open provider behind a clock. Alarms, timers and laps are -Clockula's own data, so Clockula is the first app in the family to own its -storage, and does so by design. The first section of -[`docs/PLAN.md`](docs/PLAN.md) sets out what takes the place of a shared -standard, and it comes down to three things: +- Repeat schedules, snooze, your own ringtones and per-alarm settings +- Scheduled as real alarm clocks: exempt from Doze, and shown on the lock + screen and in the status bar +- Re-resolved against your current time zone whenever it changes, so DST and + travel don't shift them +- A self-check screen that reads your device's actual settings. If one of them + would stop an alarm ringing, Clockula names it and opens that settings page, + instead of failing quietly at 7am. -- Clockula speaks Android's standard alarm contract. It implements - `android.provider.AlarmClock` in full, so your assistant and your automation - apps can set alarms and timers in it exactly as they would in any stock clock, - and the system status bar and lock screen show its next alarm. -- World clocks are IANA time zones, not a bespoke city list that goes stale. -- Your data exports to a documented, versioned JSON file whenever you want - it, because data you can't take with you isn't really yours. +### Timers, stopwatch and world clock -## Alarms that actually ring +- Several timers at once, controllable from the notification shade +- A stopwatch with laps +- World clocks from the IANA time-zone database built into Android, so the + city list doesn't go stale +- A running timer or stopwatch stays reachable from every tab -An alarm app is only worth having if you can trust it, so Clockula treats reliability -as the main feature. Alarms are scheduled as real alarm clocks (doze-exempt and -visible to the system) and re-resolved against your current time -zone whenever anything moves underneath them, so DST and travel don't silently -shift them. +### Widgets -When something on your device *would* stop an alarm ringing, Clockula tells you -which setting it is and takes you there, instead of failing quietly at 7am. +- Three home-screen widgets: next alarm, timer and clock +- Each widget picks its own theme, or takes its colours from your wallpaper + +### Works with other apps + +- Clockula implements Android's standard `AlarmClock` intents in full. Your + voice assistant and automation apps can set alarms and timers in it the same + way they would in the stock clock. +- Alarms, timers, world clocks and settings export to a JSON file whose format + is documented in [`docs/BACKUP.md`](docs/BACKUP.md), and import back from it. + +### Design and privacy + +- Material 3 Expressive throughout, with dynamic colour on Android 12+, light + and dark themes, and expressive motion and shapes +- A per-app language setting. Clockula ships in English for now; + [help translate](#translations). +- No telemetry, no analytics and no internet permission + +## Where your data lives + +Calendula works on top of Android's `CalendarContract`, and Agendula can work on +top of the dmfs `TaskContract`. Both put an interface on a standard someone else +maintains. Clocks have no such provider, so Clockula keeps alarms, timers and +laps in its own database, the first app in the family to do so. The first +section of [`docs/PLAN.md`](docs/PLAN.md) explains how it makes up for the +missing standard: the `AlarmClock` intents, IANA time zones and the documented +backup format listed above. ## Install -Betas are published as pre-releases on -[Codeberg](https://codeberg.org/jlmakiola/clockula/releases), with the signed -APK and a `.sha256` checksum attached. Download the APK there, or add -`https://codeberg.org/jlmakiola/clockula` to -[Obtainium](https://github.com/ImranR98/Obtainium) and switch on *Include -prereleases* for it so it finds the beta. A beta updates in place to later betas -and to the stable release. +| Channel | Available | Notes | +| --- | --- | --- | +| [Codeberg release / Obtainium](#codeberg-release--obtainium) | Now, as betas | Plain APK download, or automatic with Obtainium | +| [Self-hosted F-Droid repo](#self-hosted-f-droid-repo) | From 1.0.0 | Add the repo once | +| [Build from source](#build-from-source) | Now | Full control | -From the stable 1.0.0 on, every release also goes to the self-hosted F-Droid -repository that serves Calendula and Agendula. +### Codeberg release / Obtainium -## Documentation +Every release is on +[Codeberg](https://codeberg.org/jlmakiola/clockula/releases) with the signed +APK and a `.sha256` checksum. Betas are published as pre-releases. Download the +APK and install it directly. -Start with [`docs/`](docs/): [`PLAN.md`](docs/PLAN.md) is the spec and the -reasoning, [`ROADMAP.md`](docs/ROADMAP.md) is the status, -[`ARCHITECTURE.md`](docs/ARCHITECTURE.md) is how the code is put together, and -[`RELEASING.md`](docs/RELEASING.md) is the release runbook. +For automatic updates, use [Obtainium](https://github.com/ImranR98/Obtainium). +On your phone, +[add Clockula in one tap](https://apps.obtainium.imranr.dev/redirect?r=obtainium://add/https://codeberg.org/jlmakiola/clockula), +or add it by hand: *Add App*, paste `https://codeberg.org/jlmakiola/clockula`, +then *Add*. While Clockula is in beta, switch on *Include prereleases* for it, +or Obtainium won't find anything. A beta updates in place to later betas and to +the stable release. -## Building +### Self-hosted F-Droid repo + +From the stable 1.0.0 on, every release is also published to the self-hosted +F-Droid repository that serves Calendula and Agendula. If you already have it, +Clockula will show up there. Otherwise add it once: + +1. In your F-Droid client, open *Settings → Repositories → Add* (or open the + link below on your phone): + + ``` + https://apps.dev.jeanlucmakiola.de/dev/fdroid/repo?fingerprint=C2C0640402BF458FC0ED957AF0B37AA4C14022E72F89CE90B5965B458CF73425 + ``` + + Repo: `https://apps.dev.jeanlucmakiola.de/dev/fdroid/repo` · + fingerprint (SHA-256): + `C2C0 6404 02BF 458F C0ED 957A F0B3 7AA4 C140 22E7 2F89 CE90 B596 5B45 8CF7 3425` + +2. Refresh, search for Clockula and install it. + +### Build from source ```sh git clone --recurse-submodules https://codeberg.org/jlmakiola/clockula.git @@ -86,10 +135,53 @@ cd clockula ``` floret-kit is a submodule built from source as a Gradle composite build, so -`--recurse-submodules` is not optional. The included build needs to find the -Android SDK on its own: either export `ANDROID_HOME`, or drop a `sdk.dir=…` line -in a (gitignored) `floret-kit/local.properties`. +`--recurse-submodules` is required. The included build has to find the Android +SDK on its own: either export `ANDROID_HOME`, or put a `sdk.dir=…` line in a +(gitignored) `floret-kit/local.properties`. [CONTRIBUTING.md](CONTRIBUTING.md) +covers tests and lint. + +The F-Droid repo and the Codeberg releases share the same signing key, so +you can switch between them without reinstalling. + +## Documentation + +- [Contributing](CONTRIBUTING.md): the workflow, the build and the + architectural rule changes are reviewed against +- [Plan](docs/PLAN.md): the spec and the reasoning behind it +- [Roadmap](docs/ROADMAP.md): what has shipped and what's next +- [Architecture](docs/ARCHITECTURE.md): how the code is put together +- [Backup format](docs/BACKUP.md): the JSON export schema +- [Releasing](docs/RELEASING.md): the release runbook +- [Privacy policy](docs/PRIVACY.md): the source of the page at + [jeanlucmakiola.de/clockula/privacy](https://jeanlucmakiola.de/clockula/privacy) + +## Contributing + +Bug reports, ideas and patches are welcome on +[Codeberg](https://codeberg.org/jlmakiola/clockula/issues). Translations go +through [Weblate](#translations), not pull requests. Read +[CONTRIBUTING.md](CONTRIBUTING.md) before writing code. + +## Translations + +Clockula is written in English. Translations are managed on a self-hosted +Weblate, and partial ones are fine: untranslated strings fall back to English. + +[Help translate Clockula](https://weblate.dev.jeanlucmakiola.de/engage/clockula/) + +You don't need to code. Register on the Weblate server, pick a language (or +request one) and translate in your browser. + +## Privacy + +Clockula has no analytics, tracking, ads, crash-reporting SDK or third-party +services. It doesn't have the internet permission, so it can't send anything +anywhere. Your alarms, timers and world clocks stay in the app's private +storage on your device. + +[Privacy policy](https://jeanlucmakiola.de/clockula/privacy), written in +[`docs/PRIVACY.md`](docs/PRIVACY.md), which is what that page renders. ## Licence -MIT. See [`LICENSE`](LICENSE). +[MIT](LICENSE), Jean-Luc Makiola, 2026 diff --git a/floret-kit b/floret-kit index 6ef77bd..8b33635 160000 --- a/floret-kit +++ b/floret-kit @@ -1 +1 @@ -Subproject commit 6ef77bd5b247024dfb997553cdcfc8fb27c84a01 +Subproject commit 8b33635a852d404cd72d2483dd5bdc4926f13276 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 258f005..57575e1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] -agp = "9.2.1" -kotlin = "2.3.21" -ksp = "2.3.9" -hilt = "2.59.2" +agp = "9.4.1" +kotlin = "2.4.20" +ksp = "2.3.11" +hilt = "2.60.1" coreKtx = "1.19.1" appcompat = "1.8.0" lifecycleRuntime = "2.10.0" diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 1b33c55..5097068 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b21b69e..1eca32e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,8 +1,10 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip -distributionSha256Sum=bafc141b619ad6350fd975fc903156dd5c151998cc8b058e8c1044ab5f7b031f +distributionSha256Sum=bafd5ce9cfaea0fbccfdc8439a1ac42fbd4cd9c89dc9a988228d8a2639a58e6c +distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.0-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 23d15a9..249efbb 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -172,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -212,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" diff --git a/gradlew.bat b/gradlew.bat index db3a6ac..3185a43 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -19,12 +19,39 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -45,13 +72,14 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. -goto fail +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -59,36 +87,26 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. -goto fail +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel :execute @rem Setup the command line -set CLASSPATH= -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem This label must not be changed. We rely on old scripts being able to jump to this point. +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL%