diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml new file mode 100644 index 0000000..884f69b --- /dev/null +++ b/.github/workflows/android-build.yml @@ -0,0 +1,37 @@ +name: Android build + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: Compile and test + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Set up JDK 17 + uses: actions/setup-java@v6 + with: + distribution: temurin + java-version: "17" + cache: gradle + + - name: Set up Android SDK + uses: android-actions/setup-android@v4 + + - name: Install Android API 36 build tools + run: sdkmanager "platforms;android-36" "build-tools;36.0.0" + + - name: Compile debug and release APKs and run unit tests + run: | + chmod +x gradlew + ./gradlew --no-daemon testDebugUnitTest assembleDebug assembleRelease diff --git a/.github/workflows/release-apk.yml b/.github/workflows/release-apk.yml new file mode 100644 index 0000000..60bfe6b --- /dev/null +++ b/.github/workflows/release-apk.yml @@ -0,0 +1,83 @@ +name: Build and publish Android APK + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + release: + name: Build, sign and publish APK + runs-on: ubuntu-latest + steps: + - name: Check out tagged source + uses: actions/checkout@v7 + + - name: Set up JDK 17 + uses: actions/setup-java@v6 + with: + distribution: temurin + java-version: "17" + cache: gradle + + - name: Set up Android SDK + uses: android-actions/setup-android@v4 + + - name: Install Android API 36 build tools + run: sdkmanager "platforms;android-36" "build-tools;36.0.0" + + - name: Validate signing secrets and decode keystore + env: + ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + run: | + test -n "$ANDROID_KEYSTORE_BASE64" || { echo "::error::Missing secret ANDROID_KEYSTORE_BASE64"; exit 1; } + test -n "$ANDROID_KEY_ALIAS" || { echo "::error::Missing secret ANDROID_KEY_ALIAS"; exit 1; } + test -n "$ANDROID_KEYSTORE_PASSWORD" || { echo "::error::Missing secret ANDROID_KEYSTORE_PASSWORD"; exit 1; } + test -n "$ANDROID_KEY_PASSWORD" || { echo "::error::Missing secret ANDROID_KEY_PASSWORD"; exit 1; } + printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/child-timer-release.jks" + test -s "$RUNNER_TEMP/child-timer-release.jks" + echo "ANDROID_SIGNING_KEYSTORE=$RUNNER_TEMP/child-timer-release.jks" >> "$GITHUB_ENV" + + - name: Build release variant + run: | + chmod +x gradlew + ./gradlew --no-daemon assembleRelease + + - name: Sign and verify APK + env: + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + run: | + UNSIGNED_APK="app/build/outputs/apk/release/app-release-unsigned.apk" + test -f "$UNSIGNED_APK" || { echo "::error::Unsigned release APK not found at $UNSIGNED_APK"; exit 1; } + + SDK_ROOT="${ANDROID_SDK_ROOT:-$ANDROID_HOME}" + APKSIGNER="$SDK_ROOT/build-tools/36.0.0/apksigner" + test -x "$APKSIGNER" || { echo "::error::apksigner not found at $APKSIGNER"; exit 1; } + + SIGNED_APK="ChildTimer-${GITHUB_REF_NAME}.apk" + "$APKSIGNER" sign \ + --ks "$ANDROID_SIGNING_KEYSTORE" \ + --ks-key-alias "$ANDROID_KEY_ALIAS" \ + --ks-pass "env:ANDROID_KEYSTORE_PASSWORD" \ + --key-pass "env:ANDROID_KEY_PASSWORD" \ + --out "$SIGNED_APK" \ + "$UNSIGNED_APK" + "$APKSIGNER" verify --verbose "$SIGNED_APK" + echo "SIGNED_APK=$SIGNED_APK" >> "$GITHUB_ENV" + + - name: Publish GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "$GITHUB_REF_NAME" "$SIGNED_APK" \ + --title "Child Timer $GITHUB_REF_NAME" \ + --generate-notes \ + --verify-tag diff --git a/.gitignore b/.gitignore index aa724b7..e307f95 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,16 @@ /.idea/assetWizardSettings.xml .DS_Store /build +**/build/ /captures .externalNativeBuild .cxx local.properties + +# Local signing keys must never be committed. +*.jks +*.keystore +*.p12 + +# Build outputs are published through GitHub Releases instead. +*.apk diff --git a/README.md b/README.md index 46d5d65..7ed9c7d 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,178 @@ -# Child Timer 🦁⏱️ - -A cheerful, modern Android visual countdown timer for children, built with Kotlin and Jetpack Compose. Inspired by visual time discs (like the Time Timer), **Child Timer** makes the passage of time concrete, friendly, and engaging for toddlers and young kids through delightful animal companions, routine presets, sound cues, and celebratory star rewards. - ---- - -## ✨ Features - -- **12 Adorable Animal Companions:** - - 🦁 **Leo the Lion** (*Léo le Lion*) - Brave & energetic - - 🦊 **Rusty the Fox** (*Rox le Renard*) - Clever & quick - - 🐰 **Bunny Hop** (*Pompom le Lapin*) - Playful & joyful - - 🐼 **Bamboo Panda** (*Bambou le Panda*) - Calm & serene - - 🦒 **Gigi the Giraffe** (*Gigi la Girafe*) - Tall & curious - - 🐻 **Barnaby Bear** (*Barnabé l'Ours*) - Big warm hugs - - 🐒 **Milo the Monkey** (*Ouistiti le Singe*) - Cheerful acrobat - - 🐊 **Chomp the Croc** (*Croc le Croco*) - Sparkling shiny teeth - - 🦉 **Hoot the Owl** (*Hibou Sage*) - Wise & focused - - 🦓 **Zigzag Zebra** (*Zigzag le Zèbre*) - Fast striped sprinter - - 🐿️ **Nutty Squirrel** (*Noisette l'Écureuil*) - Acorn collector - - 🐘 **Peanut Elephant** (*Pif l'Éléphant*) - Strong & patient - -- **Visual Countdown Dial:** - - Time-Timer style colored sweep wedge that shrinks as time passes. - - Centered animal avatar with breathing pulse animation during countdown. - - Big, legible MM:SS digital readout. - -- **Routine Presets:** - - 🪥 **Brush Teeth:** 2 minutes - - 🧸 **Tidy Up Toys:** 5 minutes - - 📖 **Story Time:** 10 minutes - - 🎨 **Drawing & Craft:** 15 minutes - - 🛁 **Bath & Bubbles:** 20 minutes - - 💤 **Quiet Nap / Rest:** 30 minutes - - ⏱️ **Custom Time:** Quick adjustment (+1m, +5m, -1m) - -- **Sound & Haptics Feedback:** - - Sweet synthesized chimes for Start, Pause, and Completion fanfare via Android's `ToneGenerator`. - - Optional soft ticking every second. - - Haptic vibration feedback for tactile responses. - -- **Celebration & Rewards:** - - Bursting confetti animation on countdown completion. - - Celebratory animal victory quotes. - - Star Trophy counter (`+1 ⭐`) to celebrate positive habits and completed routines. - -- **Child Lock & Screen Awake:** - - Child lock preventing toddlers from accidentally stopping or altering the timer. - - Keeps the device screen awake during active countdowns. - ---- - -## 🛠️ Tech Stack & Architecture - -- **Language:** Kotlin 2.3 -- **UI Framework:** Jetpack Compose (Material Design 3) -- **Architecture:** MVVM (Model-View-ViewModel) with Kotlin Coroutines and StateFlow -- **Navigation:** Android Navigation 3 -- **Audio & Haptics:** `android.media.ToneGenerator` & `android.os.Vibrator` -- **Target SDK:** 36 (Android 16) / Min SDK: 24 (Android 7.0) +
Le temps devient visible, simplement.
+Un minuteur visuel Android conçu pour aider les enfants à comprendre le temps qui passe, sans chiffres au cadran ni compte à rebours numérique.
+
+
+
+
+
+
+
+
+ ⬇️ Télécharger l’APK Android (build de test)
+ Le fichier actuel est identique à app-debug.apk. Pour une version de production signée, consulte les GitHub Releases.
+
AudioTrack et retour haptique via les API Android.
+
+## 🔧 Construire l'application
+
+### Prérequis
+
+Installe un JDK 17 et le SDK Android avec la plateforme Android API 36.
+
+### Compiler et tester
+
+Sur Linux ou macOS :
+
+~~~bash
+./gradlew testDebugUnitTest assembleDebug
+~~~
+
+Sur Windows :
+
+~~~bat
+gradlew.bat testDebugUnitTest assembleDebug
+~~~
+
+L'APK de débogage est généré dans :
+
+~~~text
+app/build/outputs/apk/debug/app-debug.apk
+~~~
+
+Pour produire la variante release non signée localement :
+
+~~~bash
+./gradlew assembleRelease
+~~~
+
+La variante release n'est pas installable tant qu'elle n'a pas été signée. La procédure de publication automatique est décrite ci-dessous.
+
+## 📦 Publier une release APK
+
+Le workflow GitHub Actions .github/workflows/release-apk.yml est conçu pour construire, signer et joindre l'APK à une **GitHub Release** lorsqu'un tag v* est poussé. Il nécessite une clé de signature privée, à conserver hors du dépôt.
+
+### 1. Créer une clé de signature
+
+Sur ta machine, exécute :
+
+~~~bash
+keytool -genkeypair -v -keystore child-timer-release.jks -alias child-timer -keyalg RSA -keysize 2048 -validity 10000
+~~~
+
+**Conserve précieusement ce fichier et ses mots de passe. Ne commite jamais la clé dans Git.** La même clé doit être réutilisée pour les prochaines versions afin de permettre les mises à jour de l'application.
+
+### 2. Ajouter les secrets GitHub
+
+Dans **Settings → Secrets and variables → Actions**, crée les secrets suivants :
+
+- ANDROID_KEYSTORE_BASE64 : contenu Base64 du fichier .jks.
+- ANDROID_KEY_ALIAS : alias utilisé à la création de la clé (ici child-timer).
+- ANDROID_KEYSTORE_PASSWORD : mot de passe du keystore.
+- ANDROID_KEY_PASSWORD : mot de passe de la clé.
+
+Sous Windows PowerShell, cette commande copie le contenu Base64 du keystore dans le presse-papiers :
+
+~~~powershell
+[Convert]::ToBase64String([IO.File]::ReadAllBytes(".\child-timer-release.jks")) | Set-Clipboard
+~~~
+
+Colle cette valeur uniquement dans le secret GitHub correspondant, jamais dans un fichier du dépôt.
+
+### 3. Déclencher la publication
+
+La version actuelle du projet indique versionName = "1.0". Après avoir vérifié la version et la configuration, crée puis pousse le tag correspondant :
+
+~~~bash
+git tag v1.0
+git push origin v1.0
+~~~
+
+Le workflow construit un APK release, le signe, vérifie sa signature puis crée une release avec l'APK en pièce jointe. Les prochaines versions devront augmenter versionCode et mettre à jour versionName dans app/build.gradle.kts avant de pousser leur tag.
+
+**Avant la toute première release publique**, vérifie également que l'identifiant Android applicationId = "com.aistudio.childtimer.kxrqzp" est bien celui que tu souhaites conserver. Le changer après la publication empêcherait l'APK suivant d'être reconnu comme une mise à jour de cette application.
+
+Pour installer rapidement la build de test, utilise le lien de téléchargement en haut du README. Les versions de production signées seront disponibles dans les [GitHub Releases](https://github.com/aciderix/Child-timer/releases) une fois la clé de signature configurée.
+
+## 🔒 Confidentialité
+
+Le manifeste Android ne déclare pas de permission réseau ; il déclare uniquement la permission de vibration. Les sons du timer sont synthétisés localement. L'application n'a pas besoin de compte utilisateur pour son fonctionnement actuel.
+
+## 🧭 Périmètre actuel
+
+Cette version est centrée sur le cadran animalier. Elle ne propose pas encore de compte à rebours chiffré, de pause/reprise, de préréglages de routines, de durée personnalisée ou d'historique. Le README décrit volontairement le fonctionnement de l'écran réellement lancé par l'application, et non les anciennes interfaces présentes dans le code source.
+
+## 📄 Licence
+
+Aucun fichier de licence n'est actuellement fourni dans le dépôt. Les conditions de réutilisation et de redistribution restent donc à définir.
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index bafb7be..247d498 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -15,19 +15,7 @@ android {
versionName = "1.0"
}
- signingConfigs {
- getByName("debug") {
- storeFile = file("${rootDir}/debug.keystore")
- storePassword = "android"
- keyAlias = "androiddebugkey"
- keyPassword = "android"
- }
- }
-
buildTypes {
- debug {
- signingConfig = signingConfigs.getByName("debug")
- }
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
@@ -74,17 +62,19 @@ dependencies {
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material.icons.extended)
+
// Tooling
debugImplementation(libs.androidx.compose.ui.tooling)
+
// Instrumented tests
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.test.manifest)
- // Local tests: jUnit, coroutines, Android runner
+ // Local tests
testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test)
- // Instrumented tests: jUnit rules and runners
+ // Instrumented tests
androidTestImplementation(libs.androidx.test.core)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.androidx.test.runner)
diff --git a/docs/screenshots/README.md b/docs/screenshots/README.md
new file mode 100644
index 0000000..556fc3c
--- /dev/null
+++ b/docs/screenshots/README.md
@@ -0,0 +1 @@
+Dépose ici la capture d’écran du cadran, en mode paysage, sous le nom main-screen.png. Le README racine l’affichera automatiquement.
\ No newline at end of file
diff --git a/docs/screenshots/main-screen.png b/docs/screenshots/main-screen.png
new file mode 100644
index 0000000..33bfe18
Binary files /dev/null and b/docs/screenshots/main-screen.png differ