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
4 changes: 2 additions & 2 deletions README.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ Pas de Play Store : aucun compte n'est nécessaire pour installer.
## Construction locale

Prérequis : JDK 17, SDK Android 37, NDK `27.0.12077973` et CMake 3.22.1 (épinglés dans
`app/build.gradle.kts`). Gradle 9.8.1 arrive avec le wrapper.
`app/build.gradle.kts`). Gradle 9.8.2 arrive avec le wrapper.

```bash
./gradlew testDebugUnitTest lintDebug
Expand All @@ -256,7 +256,7 @@ et l'APK universel `versionCode × 10`.

## Technique

- Kotlin 2.4, Jetpack Compose (Material 3), Hilt, Room sur **SQLCipher** 4.16, DataStore
- Kotlin 2.4, Jetpack Compose (Material 3), Hilt, Room sur **SQLCipher** 4.20
- Bouncy Castle (Argon2id), Keystore Android, AES-256-GCM de la plateforme
- `org.jetbrains:markdown` (aperçu), whisper.cpp par JNI (dictée)
- **Aucune bibliothèque réseau**
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ No Play Store: no account is needed to install.
## Local build

Requirements: JDK 17, Android SDK 37, NDK `27.0.12077973` and CMake 3.22.1 (pinned in
`app/build.gradle.kts`). Gradle 9.8.1 comes with the wrapper.
`app/build.gradle.kts`). Gradle 9.8.2 comes with the wrapper.

```bash
./gradlew testDebugUnitTest lintDebug
Expand All @@ -243,7 +243,7 @@ and the universal APK `versionCode × 10`.

## Stack

- Kotlin 2.4, Jetpack Compose (Material 3), Hilt, Room on **SQLCipher** 4.16, DataStore
- Kotlin 2.4, Jetpack Compose (Material 3), Hilt, Room on **SQLCipher** 4.20
- Bouncy Castle (Argon2id), Android Keystore, AES-256-GCM from the platform
- `org.jetbrains:markdown` (preview), whisper.cpp through JNI (dictation)
- **No network library**
Expand Down
18 changes: 8 additions & 10 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Third-party notices — Notes Tech

Notes Tech 3.1 (`com.filestech.notes_tech`) is written in Kotlin with Jetpack Compose and is licensed
Notes Tech 3 (`com.filestech.notes_tech`) is written in Kotlin with Jetpack Compose and is licensed
under the Apache License 2.0 ([LICENSE](LICENSE)). Everything it ships that it did not write is listed
here. The list was read on 2026-10-10 from the resolved `releaseRuntimeClasspath` of the `:app` module
for 3.1.0, after the move to AGP 9 (earlier versions: see this file at their tag, e.g. `v3.0.0`), and
each licence from the POM that the library publishes — not from memory.
here. The list was read on 2026-11-09 from the resolved `releaseRuntimeClasspath` of the `:app` module,
built with AGP 9.5.0 (earlier versions: see this file at their tag, e.g. `v3.0.0`), and each licence
from the POM that the library publishes — not from memory.

**No Google Play Services, no ML Kit, no Firebase, no analytics SDK, no crash reporter, no network
library.** The app does not hold the `INTERNET` permission.
Expand All @@ -13,21 +13,19 @@ library.** The app does not hold the `INTERNET` permission.

| Library | Version | Publisher | Licence |
|---|---|---|---|
| AndroidX: Core, Activity, AppCompat, Lifecycle, Navigation, SavedState, Fragment, Emoji2, Window, Startup, Tracing, Profile Installer, DocumentFile, Biometric, Transition and their support modules | various | Google / AOSP | Apache 2.0 |
| AndroidX: Core, Activity, AppCompat, Lifecycle, Navigation, SavedState, Fragment, Emoji2, Window, Startup, Tracing, Profile Installer, Biometric and their support modules | various | Google / AOSP | Apache 2.0 |
| Jetpack Compose: UI, Foundation, Animation, Runtime, Material 3, Material Icons | BOM 2026.09.00 (UI 1.12.1, Material 3 1.4.0) | Google / AOSP | Apache 2.0 |
| Room (`androidx.room`) and `androidx.sqlite` | 2.8.5 / 2.6.2 | Google / AOSP | Apache 2.0 |
| DataStore Preferences | 1.1.1 | Google / AOSP | Apache 2.0 |
| Room (`androidx.room`) and `androidx.sqlite` | 2.8.5 / 2.7.0 | Google / AOSP | Apache 2.0 |
| Hilt and Dagger (`com.google.dagger`, `androidx.hilt`) | 2.60.1 / 1.4.0 | Google | Apache 2.0 |
| Guava `listenablefuture`, JSR-305 annotations | 1.0 / 3.0.2 | Google | Apache 2.0 |
| Kotlin standard library | 2.4.10 | JetBrains | Apache 2.0 |
| kotlinx.coroutines | 1.11.0 | JetBrains | Apache 2.0 |
| `org.jetbrains:markdown` — the Markdown parser behind the preview | 0.7.14 | JetBrains | Apache 2.0 |
| `org.jetbrains:markdown` — the Markdown parser behind the preview | 0.7.18 | JetBrains | Apache 2.0 |
| JetBrains annotations, JSpecify | 23.0.0 / 1.0.0 | JetBrains / JSpecify | Apache 2.0 |
| `javax.inject`, `jakarta.inject-api` | 1 / 2.0.1 | JSR-330 / Eclipse Foundation | Apache 2.0 |
| Okio (pulled in by DataStore) | 3.4.0 | Square | Apache 2.0 |
| Timber | 5.0.1 | Jake Wharton | Apache 2.0 |
| **Bouncy Castle** `bcprov-jdk18on` — **Argon2id**, the key derivation of the vaults | 1.86 | The Legion of the Bouncy Castle | **MIT** — full text below |
| **SQLCipher for Android** — the encrypted database | 4.16.0 | Zetetic LLC | **BSD-3-Clause** — full text below |
| **SQLCipher for Android** — the encrypted database | 4.20.0 | Zetetic LLC | **BSD-3-Clause** — full text below |

SQLCipher bundles **SQLite** and **LibTomCrypt**, both released into the public domain by their
authors.
Expand Down
2 changes: 0 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -396,8 +396,6 @@ dependencies {
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)

implementation(libs.androidx.datastore.preferences)

implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
implementation(libs.hilt.navigation.compose)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ class NotesDatabaseFactory(
/**
* `PRAGMA cipher_compatibility = 4` avant le calage de la clé.
*
* Défensif et assumé comme tel : la 4.16 utilise déjà le format v4 par défaut, donc ce pragma
* ne change rien **aujourd'hui**. Il protège d'une montée de version majeure de SQLCipher qui
* Défensif et assumé comme tel : toute la série 4.x (4.20 aujourd'hui) utilise déjà le format v4
* par défaut, donc ce pragma ne change rien **aujourd'hui**. Il protège d'une montée de version majeure de SQLCipher qui
* passerait au format v5 en silence — auquel cas les bases existantes deviendraient
* illisibles sans le moindre avertissement. La version Flutter pose le même pragma pour la
* même raison (`database.dart:474`).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,13 @@ internal class GestesDeNoteEnCours(
/** What a rotation, or the process dying, must not lose: the questions open, by id. */
val Sauvegarde: Saver<GestesDeNoteEnCours, Any> = listSaver(
save = { etat ->
// A lambda, not `::ArrayList`: that reference resolves to the Java constructor taking a
// `Collection<? extends E>`, which Kotlin 2.4 flags as a Java type mismatch.
listOf(
etat.suppression?.let(::ArrayList),
etat.deplacement?.let(::ArrayList),
etat.couleur?.let(::ArrayList),
etat.sortie?.ids?.let(::ArrayList),
etat.suppression?.let { ArrayList(it) },
etat.deplacement?.let { ArrayList(it) },
etat.couleur?.let { ArrayList(it) },
etat.sortie?.ids?.let { ArrayList(it) },
etat.sortie?.dossierId,
etat.sortieNombre,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,16 @@ class MarkdownPreviewReaderTest {
fun `inline HTML and inline math are shown as written`() {
assertThat(runsOf("<b>x</b> \$a+b\$")).containsExactly(run("<b>x</b> \$a+b\$"))
}

/**
* `==…==` became a node of the GFM parser in 0.7.15 (highlighting). 2.0.9 has none: it stays
* text, delimiters included — a node the reader does not know must not take characters away.
*/
@Test
fun `highlighting is shown as written, as 2_0_9 shows it`() {
assertThat(runsOf("a ==b== c")).containsExactly(run("a ==b== c"))
assertThat(runsOf("**x ==y==**")).containsExactly(run("x ==y==", RunStyle.BOLD))
}
}

@Nested
Expand Down
5 changes: 5 additions & 0 deletions docs/01-DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,11 @@ vérité pour l'état d'une même application.
**Écarté** : DataStore, qui est l'API moderne et que le projet embarque déjà. Le coût de la
migration l'emporte sur le bénéfice, et la dépendance reste pour la phase 7.

**Dépendance retirée le 2026-11-09** : la phase 7 (dictée) s'est faite sans elle, et rien dans le code
ne l'a jamais importée. R8 en retirait le code Java, mais chaque APK embarquait encore sa bibliothèque
native (`lib/<abi>/libdatastore_shared_counter.so`, lu dans la 3.3.0 publiée) et Dependabot en
proposait les montées de version (#34). Les réglages restent où ce choix les a mis.

⚠️ **Les formats ont été LUS dans le greffon** (`shared_preferences_android-2.4.26`, version figée
par `pubspec.lock`), pas supposés. Le piège cher : `setInt` écrit un **`putLong`**. Le délai
d'auto-verrouillage des coffres est un `int` ; `getInt` aurait levé `ClassCastException` au premier
Expand Down
16 changes: 8 additions & 8 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[versions]
# Build toolchain — the one Agenda Tech and App Manager Tech run (AGP 9.4.1 / Kotlin 2.4.10 / KSP 2.3.12
# / JDK 17). AGP 9 compiles Kotlin itself: there is no `org.jetbrains.kotlin.android` plugin any more.
agp = "9.4.1"
# Build toolchain — AGP 9.5.0 (as SMS Tech since 2026-11-05; needs Gradle 9.6.0 or later) / Kotlin 2.4.10 /
# KSP 2.3.12 / JDK 17. AGP 9 compiles Kotlin itself: there is no `org.jetbrains.kotlin.android` plugin any more.
agp = "9.5.0"
# Capped below 2.4.20: CodeQL 2.27 refuses to compile from there ("currently supports versions below
# 2.4.20", measured on Agenda Tech). This is the EXACT compiler version: the root build.gradle.kts pins
# the Kotlin Gradle Plugin to it with `strictly`, since AGP 9 would otherwise bring its own.
Expand All @@ -15,7 +15,6 @@ activity-compose = "1.13.0"
lifecycle = "2.11.0"
navigation-compose = "2.10.2"
room = "2.8.5"
datastore = "1.1.1"
splashscreen = "1.2.0"
# The app lock's fingerprint / face unlock (D-023). Same version as Agenda Tech and SMS Tech: 1.1.0 is
# the last stable release, and the two portfolio apps that ship it have measured its pitfalls
Expand All @@ -30,8 +29,10 @@ coroutines = "1.11.0"

# SQLCipher — même artefact que celui embarqué par `sqflite_sqlcipher` côté Flutter, qui est en
# 4.10.0. Le format de fichier est stable sur toute la série 4.x et `PRAGMA cipher_compatibility = 4`
# est posé explicitement à l'ouverture : une base écrite par la 4.10 s'ouvre avec la 4.16.
sqlcipher = "4.16.0"
# est posé explicitement à l'ouverture : une base écrite par la 4.10 s'ouvre avec la 4.20
# (`LegacyDatabaseOpeningTest`, instrumenté). 4.19.1 change ce que rend `getColumnType` pour NULL :
# rien ici ne le lit, Room passe par `isNull`.
sqlcipher = "4.20.0"

# Bouncy Castle — uniquement pour `Argon2BytesGenerator` (RFC 9106), qui n'a pas d'équivalent
# dans la plateforme. On utilise l'API bas niveau `org.bouncycastle.crypto.*`, PAS le fournisseur
Expand All @@ -43,7 +44,7 @@ timber = "5.0.1"

# The Markdown preview's parser (D-024): pure Kotlin, Apache 2.0, a single dependency on
# kotlin-stdlib. Only the GFM parser is used; the rendering is written here, in Compose.
jetbrains-markdown = "0.7.14"
jetbrains-markdown = "0.7.18"

# Désucrage des bibliothèques de base — `java.time` n'est natif qu'à partir de l'API 26, or le
# plancher est à 24 (cf. `app/build.gradle.kts`). Sans ça, tout formatage de date écrirait du
Expand Down Expand Up @@ -91,7 +92,6 @@ androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }

androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }

hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }
Expand Down
4 changes: 2 additions & 2 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=dce76f55f8e251a3a1f130eb120f30b3d271de2b76c9b0729d316b5a1b6dc01f
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip
distributionSha256Sum=21bd5da2de49b642904077f38c0efdc7778e92b3ed0889392dca61b97e389607
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
Expand Down
51 changes: 45 additions & 6 deletions tools/check-instrumented-results.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@

Trois pièges que ce script évite
--------------------------------
1. **Une hypothèse non remplie est comptée comme un ÉCHEC dans le XML d'AGP**, pas comme un test
ignoré (mesuré le 2026-10-29 : `skipped="0"`, et les `AssumptionViolatedException` sous
`<failure>`). Il faut donc lire la cause de chaque échec ; le total ne dit rien.
1. **Une hypothèse non remplie n'a pas la même forme selon la version d'AGP.** Jusqu'à AGP 9.4, un
ÉCHEC (mesuré le 2026-10-29 : `skipped="0"`, et les `AssumptionViolatedException` sous
`<failure>`) ; depuis AGP 9.5, un `<skipped>` dont le TEXTE porte l'exception et sa pile (mesuré
le 2026-11-09, PR #51 — ce script, qui ne lisait que `<failure>`, a alors jugé « sans raison » les
onze sauts admis). Les deux formes sont lues, et dans les deux la cause se lit ; le total ne dit
rien. Un `<skipped>` sans hypothèse levée — un `@Ignore`, un saut sans raison — n'est jamais admis.
2. **Un test ignoré n'est pas un test vert** (`docs/04-PIEGES.md` §45). Seules les raisons de
[RAISONS_ADMISES] passent ; toute autre hypothèse levée fait échouer le contrôle — une nouvelle
raison s'ajoute ici, en revue, jamais par défaut.
Expand All @@ -42,6 +45,7 @@
import sys
import tempfile
import xml.etree.ElementTree as ET
from xml.sax.saxutils import escape

# 501 cas le 2026-10-29. Un plancher, pas un compte exact : retirer des tests le fait baisser ici,
# dans le même commit.
Expand Down Expand Up @@ -73,12 +77,26 @@ def lire(dossier: pathlib.Path) -> list[tuple[str, str, str | None]]:
if echec is None:
echec = element.find("error")
texte = None if echec is None else (echec.get("message") or "") + "\n" + (echec.text or "")
if element.find("skipped") is not None:
texte = "AssumptionViolatedException: <skipped>"
sautee = element.find("skipped")
if sautee is not None:
texte = raison_du_saut(sautee)
cas.append((element.get("classname", ""), element.get("name", ""), texte))
return cas


def raison_du_saut(sautee: ET.Element) -> str:
"""Pourquoi un test a été sauté, tel que le XML le dit — cf. le premier piège.

AGP 9.5 met l'hypothèse levée, exception et pile, dans le texte du `<skipped>`. Un saut qui n'en
porte pas n'a rien à juger : il est rendu sans le mot `AssumptionViolatedException`, donc compté
comme un échec, même si son texte cite une raison admise.
"""
raison = ((sautee.get("message") or "") + "\n" + (sautee.text or "")).strip()
if "AssumptionViolatedException" in raison:
return raison
return f"<skipped> sans hypothèse levée : {raison or 'aucune raison'}"


def juger(cas: list[tuple[str, str, str | None]]) -> list[str]:
"""Les défauts, vide si les résultats sont acceptables."""
defauts = []
Expand Down Expand Up @@ -116,13 +134,20 @@ def resumer(cas: list[tuple[str, str, str | None]]) -> str:
# ── Contrôle négatif ────────────────────────────────────────────────────────────────────────────


class Saut(str):
"""Dans l'auto-contrôle, un cas écrit en `<skipped>` (AGP 9.5) au lieu de `<failure>` (AGP 9.4)."""


def _xml(cas: list[tuple[str, str, str | None]]) -> str:
lignes = ['<?xml version="1.0" encoding="UTF-8"?>', "<testsuites><testsuite>"]
for classe, test, texte in cas:
if texte is None:
lignes.append(f'<testcase name="{test}" classname="{classe}"/>')
else:
lignes.append(f'<testcase name="{test}" classname="{classe}"><failure>{texte}</failure></testcase>')
balise = "skipped" if isinstance(texte, Saut) else "failure"
lignes.append(
f'<testcase name="{test}" classname="{classe}"><{balise}>{escape(texte)}</{balise}></testcase>',
)
lignes.append("</testsuite></testsuites>")
return "\n".join(lignes)

Expand All @@ -142,6 +167,20 @@ def auto_controle() -> int:
False,
),
"trop peu de tests": (ordinaires[:10] + [temoin_ok], False),
# AGP 9.5 : l'hypothèse dans le texte d'un <skipped>, suivie de sa pile.
"AGP 9.5 : raison admise dans un <skipped>": (
ordinaires + [temoin_ok, ("com.x.VoixTest", "v", Saut(admis[2] + "\n\tat org.junit.Assume.assumeTrue"))],
True,
),
"AGP 9.5 : raison non admise dans un <skipped>": (
ordinaires + [temoin_ok, ("com.x.ATest", "i", Saut("org.junit.AssumptionViolatedException: autre"))],
False,
),
"AGP 9.5 : <skipped> vide": (ordinaires + [temoin_ok, ("com.x.ATest", "i", Saut(""))], False),
"AGP 9.5 : @Ignore qui cite une raison admise": (
ordinaires + [temoin_ok, ("com.x.ATest", "i", Saut("no biometric enrolled on this device"))],
False,
),
}
rate = []
with tempfile.TemporaryDirectory() as dossier:
Expand Down
Loading