Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/android-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Android build

on:
push:
branches:
- main

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Feature branch pushes skip CI

When a developer pushes to a non-main branch before opening a pull request, the push trigger matches only main, so pushes to other branches without a pull request event do not compile or test the change, despite the description promising validation on pushes.

Remove the branch restriction or configure the intended feature-branch push patterns.

Prompt for AI agents
In `.github/workflows/android-build.yml` at line 6:

**Feature branch pushes skip CI**

When a developer pushes to a non-main branch before opening a pull request, the `push` trigger matches only `main`, so pushes to other branches without a pull request event do not compile or test the change, despite the description promising validation on pushes.

Remove the branch restriction or configure the intended feature-branch push patterns.

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
83 changes: 83 additions & 0 deletions .github/workflows/release-apk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: Build and publish Android APK

on:
push:
tags:
- "v*"

permissions:
contents: write

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High · Build code can access release credentials

When a tagged revision or Gradle dependency executes malicious build logic, the checkout action persists its credential, so Gradle build code can read the workflow's contents: write token from Git configuration and modify the repository or releases.

Set persist-credentials: false for checkout and provide release credentials only to the publishing step.

Also at .github/workflows/release-apk.yml:17.

Prompt for AI agents
In `.github/workflows/release-apk.yml` at line 9:

**Build code can access release credentials**

When a tagged revision or Gradle dependency executes malicious build logic, the checkout action persists its credential, so Gradle build code can read the workflow's `contents: write` token from Git configuration and modify the repository or releases.

Set `persist-credentials: false` for checkout and provide release credentials only to the publishing step.

Also at `.github/workflows/release-apk.yml:17`.


jobs:
release:
name: Build, sign and publish APK
runs-on: ubuntu-latest
steps:
- name: Check out tagged source

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Older build can win the release

When a tag is force-updated while workflows for its old and new targets overlap, an older run checks out the commit from its tag-push event and can finish first, publishing that APK under the tag after the tag has moved to a newer commit; the newer run then cannot create the same release, so its artifact is not published.

Serialize release jobs by tag and verify the event commit is still the tag's target before publishing.

Also at .github/workflows/release-apk.yml:80.

Prompt for AI agents
In `.github/workflows/release-apk.yml` at line 16:

**Older build can win the release**

When a tag is force-updated while workflows for its old and new targets overlap, an older run checks out the commit from its tag-push event and can finish first, publishing that APK under the tag after the tag has moved to a newer commit; the newer run then cannot create the same release, so its artifact is not published.

Serialize release jobs by tag and verify the event commit is still the tag's target before publishing.

Also at `.github/workflows/release-apk.yml:80`.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Untested APK is published

When a tag points to a commit whose unit tests have failed or have not passed, the release job runs assembleRelease without running testDebugUnitTest or waiting for the unit-test workflow, so it signs and publishes an unvalidated APK.

Require the unit-test workflow to pass before the release job signs and publishes the APK.

Prompt for AI agents
In `.github/workflows/release-apk.yml` at line 50:

**Untested APK is published**

When a tag points to a commit whose unit tests have failed or have not passed, the release job runs `assembleRelease` without running `testDebugUnitTest` or waiting for the unit-test workflow, so it signs and publishes an unvalidated APK.

Require the unit-test workflow to pass before the release job signs and publishes the APK.


- 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" \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Failed upload leaves release incomplete

When gitHub creates the release but uploading the APK fails, gh release create leaves the release record behind, and a retry exits when it encounters that existing release instead of attaching the APK, so users are left without a downloadable APK.

Make publishing idempotent by uploading or replacing the APK asset when the release already exists.

Prompt for AI agents
In `.github/workflows/release-apk.yml` at line 80:

**Failed upload leaves release incomplete**

When gitHub creates the release but uploading the APK fails, `gh release create` leaves the release record behind, and a retry exits when it encounters that existing release instead of attaching the APK, so users are left without a downloadable APK.

Make publishing idempotent by uploading or replacing the APK asset when the release already exists.

--title "Child Timer $GITHUB_REF_NAME" \
--generate-notes \
--verify-tag
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
238 changes: 178 additions & 60 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)
<div align="center">
<h1><img src="app/src/main/res/drawable/ic_app_fox.png" alt="" width="48" valign="middle"> Child Timer</h1>
<p><strong>Le temps devient visible, simplement.</strong></p>
<p>Un minuteur visuel Android conçu pour aider les enfants à comprendre le temps qui passe, sans chiffres au cadran ni compte à rebours numérique.</p>
<p>
<img src="https://img.shields.io/badge/Plateforme-Android-3DDC84?logo=android&logoColor=white" alt="Android">
<img src="https://img.shields.io/badge/Minimum-Android%207.0%2B-4285F4" alt="Android 7.0 minimum">
<img src="https://img.shields.io/badge/Langage-Kotlin-7F52FF?logo=kotlin&logoColor=white" alt="Kotlin">
<img src="https://img.shields.io/badge/Interface-Jetpack%20Compose-4285F4" alt="Jetpack Compose">
</p>
</div>

<p align="center">
<img src="docs/screenshots/main-screen.png" alt="Child Timer en mode paysage : cadran circulaire et animaux pour choisir une durée" width="900">
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Resolved
</p>

<p align="center">
<a href="ChildTimer.apk?raw=1"><strong>⬇️ Télécharger l’APK Android (build de test)</strong></a><br>
<sub>Le fichier actuel est identique à <code>app-debug.apk</code>. Pour une version de production signée, consulte les GitHub Releases.</sub>
</p>

## 🧸 À propos

**Child Timer** est une application Android minimaliste qui représente une durée sous la forme d'un secteur coloré sur un cadran. L'enfant choisit l'un des animaux placés autour du cadran, puis lance le timer. Le secteur turquoise se réduit progressivement jusqu'à la fin du temps choisi.

L'interface est volontairement immersive : elle fonctionne en **paysage**, masque les barres système Android et maintient l'écran éveillé pendant l'utilisation.

## ✨ Fonctionnalités

- **12 animaux illustrés** disposés autour du cadran.
- **Durées de 5 à 60 minutes**, par pas de 5 minutes : un animal correspond à une durée.
- **Cadran sans chiffres** et sans affichage numérique du temps restant.
- **Secteur turquoise animé** qui matérialise le temps disponible puis diminue pendant le décompte.
- **Bouton Lecture** qui apparaît après le choix d'un animal.
- **Retour sonore doux** lors de la sélection et du démarrage, puis une mélodie à la fin.
- **Vibrations discrètes** pour accompagner certaines interactions.
- **Petite célébration visuelle** à la fin du timer, avec des étoiles et un renard au centre.
- **Plein écran paysage** pour que le cadran reste au centre de l'expérience.

### Les animaux et leurs durées

| Position du cadran | Animal | Durée |
|---|---|---:|
| 1 h | <img src="app/src/main/res/drawable/ours.png" alt="Ours" width="42"> Ours | 5 min |
| 2 h | <img src="app/src/main/res/drawable/hibou.png" alt="Hibou" width="42"> Hibou | 10 min |
| 3 h | <img src="app/src/main/res/drawable/lapin.png" alt="Lapin" width="42"> Lapin | 15 min |
| 4 h | <img src="app/src/main/res/drawable/renard.png" alt="Renard" width="42"> Renard | 20 min |
| 5 h | <img src="app/src/main/res/drawable/croco.png" alt="Croco" width="42"> Croco | 25 min |
| 6 h | <img src="app/src/main/res/drawable/zebre.png" alt="Zèbre" width="42"> Zèbre | 30 min |
| 7 h | <img src="app/src/main/res/drawable/singe.png" alt="Singe" width="42"> Singe | 35 min |
| 8 h | <img src="app/src/main/res/drawable/elephant.png" alt="Éléphant" width="42"> Éléphant | 40 min |
| 9 h | <img src="app/src/main/res/drawable/girafe.png" alt="Girafe" width="42"> Girafe | 45 min |
| 10 h | <img src="app/src/main/res/drawable/ecureuil.png" alt="Écureuil" width="42"> Écureuil | 50 min |
| 11 h | <img src="app/src/main/res/drawable/panda.png" alt="Panda" width="42"> Panda | 55 min |
| 12 h | <img src="app/src/main/res/drawable/lion.png" alt="Lion" width="42"> Lion | 60 min |


## ▶️ Utilisation

1. Ouvre l'application en mode paysage.
2. Touche l'animal correspondant à la durée souhaitée. Le secteur turquoise montre la durée sélectionnée.
3. Appuie sur le gros bouton **Lecture** qui apparaît à droite.
4. Laisse l'enfant observer le secteur diminuer. À la fin, une animation, un son et une vibration signalent que le temps est écoulé.

### Commandes disponibles

- **Activer ou couper le son :** maintenir le petit moyeu au centre du cadran pendant environ 3 secondes.
- **Réinitialiser avant la fin :** toucher rapidement le moyeu central 10 fois.
- **Revenir au choix de durée après la fin :** toucher le renard au centre.

> Le contrôle du son et la réinitialisation reposent actuellement sur des gestes cachés, plutôt que sur des boutons visibles.

## 📱 Compatibilité

- Android **7.0 (API 24) ou supérieur**.
- Interface prévue pour le **paysage**.
- SDK Android ciblé : **36**.

## 🛠️ Technologies

- **Kotlin**
- **Jetpack Compose** et Material 3
- **Android Gradle Plugin 9.0.1**
- **Gradle 9.1.0**
- **JDK 17**
- Sons synthétisés localement via <code>AudioTrack</code> 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 <code>.github/workflows/release-apk.yml</code> est conçu pour construire, signer et joindre l'APK à une **GitHub Release** lorsqu'un tag <code>v*</code> 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 :

- <code>ANDROID_KEYSTORE_BASE64</code> : contenu Base64 du fichier <code>.jks</code>.
- <code>ANDROID_KEY_ALIAS</code> : alias utilisé à la création de la clé (ici <code>child-timer</code>).
- <code>ANDROID_KEYSTORE_PASSWORD</code> : mot de passe du keystore.
- <code>ANDROID_KEY_PASSWORD</code> : 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 <code>versionName = "1.0"</code>. 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 <code>versionCode</code> et mettre à jour <code>versionName</code> dans <code>app/build.gradle.kts</code> avant de pousser leur tag.

**Avant la toute première release publique**, vérifie également que l'identifiant Android <code>applicationId = "com.aistudio.childtimer.kxrqzp"</code> 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.
Loading
Loading