diff --git a/README.fr.md b/README.fr.md index 24f81044..2c5a7bd4 100644 --- a/README.fr.md +++ b/README.fr.md @@ -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 @@ -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** diff --git a/README.md b/README.md index 44fd3924..5bd4214f 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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** diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index c968c059..c63eb79a 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -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. @@ -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. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c7f59826..8a096f1f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/java/com/filestech/notes_tech/data/local/NotesDatabaseFactory.kt b/app/src/main/java/com/filestech/notes_tech/data/local/NotesDatabaseFactory.kt index cd61c4d6..078839c1 100644 --- a/app/src/main/java/com/filestech/notes_tech/data/local/NotesDatabaseFactory.kt +++ b/app/src/main/java/com/filestech/notes_tech/data/local/NotesDatabaseFactory.kt @@ -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`). diff --git a/app/src/main/java/com/filestech/notes_tech/ui/home/GestesDeNoteEnCours.kt b/app/src/main/java/com/filestech/notes_tech/ui/home/GestesDeNoteEnCours.kt index 3d3748ee..f36bfb7a 100644 --- a/app/src/main/java/com/filestech/notes_tech/ui/home/GestesDeNoteEnCours.kt +++ b/app/src/main/java/com/filestech/notes_tech/ui/home/GestesDeNoteEnCours.kt @@ -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 = listSaver( save = { etat -> + // A lambda, not `::ArrayList`: that reference resolves to the Java constructor taking a + // `Collection`, 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, ) diff --git a/app/src/test/java/com/filestech/notes_tech/domain/markdown/MarkdownPreviewReaderTest.kt b/app/src/test/java/com/filestech/notes_tech/domain/markdown/MarkdownPreviewReaderTest.kt index affe2195..dc534da2 100644 --- a/app/src/test/java/com/filestech/notes_tech/domain/markdown/MarkdownPreviewReaderTest.kt +++ b/app/src/test/java/com/filestech/notes_tech/domain/markdown/MarkdownPreviewReaderTest.kt @@ -265,6 +265,16 @@ class MarkdownPreviewReaderTest { fun `inline HTML and inline math are shown as written`() { assertThat(runsOf("x \$a+b\$")).containsExactly(run("x \$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 diff --git a/docs/01-DECISIONS.md b/docs/01-DECISIONS.md index 9f905b6c..54bed585 100644 --- a/docs/01-DECISIONS.md +++ b/docs/01-DECISIONS.md @@ -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//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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 894d4fb2..6b024f5e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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. @@ -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 @@ -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 @@ -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 @@ -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" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a78c88e5..5907d39c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/tools/check-instrumented-results.py b/tools/check-instrumented-results.py index cd96ee55..8ebfeede 100644 --- a/tools/check-instrumented-results.py +++ b/tools/check-instrumented-results.py @@ -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 - ``). 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 + ``) ; depuis AGP 9.5, un `` dont le TEXTE porte l'exception et sa pile (mesuré + le 2026-11-09, PR #51 — ce script, qui ne lisait que ``, 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 `` 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. @@ -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. @@ -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: " + 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 ``. 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" 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 = [] @@ -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 `` (AGP 9.5) au lieu de `` (AGP 9.4).""" + + def _xml(cas: list[tuple[str, str, str | None]]) -> str: lignes = ['', ""] for classe, test, texte in cas: if texte is None: lignes.append(f'') else: - lignes.append(f'{texte}') + balise = "skipped" if isinstance(texte, Saut) else "failure" + lignes.append( + f'<{balise}>{escape(texte)}', + ) lignes.append("") return "\n".join(lignes) @@ -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 , suivie de sa pile. + "AGP 9.5 : raison admise dans un ": ( + 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 ": ( + ordinaires + [temoin_ok, ("com.x.ATest", "i", Saut("org.junit.AssumptionViolatedException: autre"))], + False, + ), + "AGP 9.5 : 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: