From c635f1c79ebfc65bc40029a0ab51b0b62cddef3f Mon Sep 17 00:00:00 2001 From: Patrice Haltaya Date: Fri, 30 Oct 2026 15:31:02 +0100 Subject: [PATCH 1/2] fix(navigation): a second quick tap no longer empties the screen or creates a second note On 3.1.1 and 3.2.0 as published, two taps on a note's back arrow less than ~0.5 s apart left a WHITE screen: every onBack called popBackStack() unguarded, and the editor, still drawn during its exit transition, took the second tap and popped the note list too. A double tap on a note stacked it twice; a double tap on "New note" stacked two editors and created two notes, the second one left empty. - NotesTechNavHost: every navigation goes through depuis(entry) and happens only if that entry is still the current one. Not dropUnlessResumed: it would also drop what the editor navigates on its own (a deletion done, the unlock sheet dismissed, a linked note created) while its entry is not resumed yet. - HomeRoute: the "New note" tap goes through dropUnlessResumed, so a tap during the editor's opening transition creates nothing. - HomeViewModel.createNote: one creation at a time, like import and export (kept for slow phones; no test can open that window on the emulator). DoubleToucherTest drives the whole app with Compose's clock paused between the two gestures. Each test was seen red: the arrow and the note on the 3.2.0 code, the two "New note" ones without the screen's guard. Measured without defect: two quick system Backs, the arrow then Back, a double tap on Import (one picker), backup/vault/menu actions (already guarded), trash and list gestures (a second run finds nothing to do). Full instrumented suite on the emulator: 547 tests, the usual 12 environment failures. Co-Authored-By: Claude Opus 5.5 --- .../notes_tech/ui/DoubleToucherTest.kt | 183 ++++++++++++++++++ .../notes_tech/ui/NotesTechNavHost.kt | 76 +++++--- .../filestech/notes_tech/ui/home/HomeRoute.kt | 6 +- .../notes_tech/ui/home/HomeViewModel.kt | 14 +- 4 files changed, 250 insertions(+), 29 deletions(-) create mode 100644 app/src/androidTest/java/com/filestech/notes_tech/ui/DoubleToucherTest.kt diff --git a/app/src/androidTest/java/com/filestech/notes_tech/ui/DoubleToucherTest.kt b/app/src/androidTest/java/com/filestech/notes_tech/ui/DoubleToucherTest.kt new file mode 100644 index 0000000..9a14292 --- /dev/null +++ b/app/src/androidTest/java/com/filestech/notes_tech/ui/DoubleToucherTest.kt @@ -0,0 +1,183 @@ +package com.filestech.notes_tech.ui + +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.hasContentDescription +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.v2.createEmptyComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ActivityScenario +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.filestech.notes_tech.R +import com.filestech.notes_tech.data.prefs.AppSettings +import com.filestech.notes_tech.data.prefs.LegacyPreferences +import com.filestech.notes_tech.data.repository.NotesRepository +import com.filestech.notes_tech.domain.model.Folder +import com.filestech.notes_tech.security.applock.AppLockStore +import com.google.common.truth.Truth.assertThat +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.util.UUID +import javax.inject.Inject + +/** + * 🔴 **A second tap reaches a screen that is already leaving** (3.2.1). On 3.1.1 and 3.2.0 as + * published, two taps on « Fermer » less than ~0.5 s apart left a WHITE screen: the second one + * popped the list too, and nothing was left to show. A double tap on a note stacked it twice. + * + * The clock is paused between the two gestures: the first one's navigation is not drawn yet, the + * screen it leaves is still there to receive the second — what a quick finger does during the + * transition. Both tests fail on the unfixed 3.2.0 code (measured). Two quick system Backs, or the + * arrow then a system Back, already returned to the list there (measured too): the editor's + * `BackHandler` leaves with its entry, so they are not tested here. + * + * ⚠️ It writes to the app's real database: one note, with a random suffix, deleted in [tearDown]. + */ +@OptIn(ExperimentalTestApi::class) +@HiltAndroidTest +@RunWith(AndroidJUnit4::class) +class DoubleToucherTest { + + @get:Rule(order = 0) + val hilt = HiltAndroidRule(this) + + @get:Rule(order = 1) + val compose = createEmptyComposeRule() + + @Inject + lateinit var notes: NotesRepository + + @Inject + lateinit var store: AppLockStore + + @Inject + lateinit var prefs: LegacyPreferences + + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val titre = "Double tap ${UUID.randomUUID().toString().take(8)}" + private var scenario: ActivityScenario? = null + private var presentationDejaVue = false + private var existantes = emptySet() + + @Before + fun setUp() { + hilt.inject() + check(!store.configuredNow()) { "An app lock is configured on this device: turn it off first." } + val settings = AppSettings(prefs) + presentationDejaVue = !settings.shouldShowSplash() + settings.markSplashShown() + runBlocking { + notes.create(folderId = Folder.INBOX_ID, title = titre, content = "Body.") + existantes = notes.listAllAlive().map { it.id }.toSet() + } + scenario = ActivityScenario.launch(MainActivity::class.java) + compose.waitUntilAtLeastOneExists(hasText(titre), TIMEOUT_MILLIS) + } + + @After + fun tearDown() { + compose.mainClock.autoAdvance = true + scenario?.close() + runBlocking { + // The test's note, and those « Nouvelle note » created. + notes.listAllAlive().filter { it.title == titre || it.id !in existantes } + .forEach { notes.deletePermanently(it.id) } + } + if (!presentationDejaVue) prefs.remove(SPLASH_SHOWN_KEY) + } + + private fun texte(id: Int): String = context.getString(id) + + private fun ouvrirLaNote() { + compose.onNodeWithText(titre).performClick() + compose.waitUntilAtLeastOneExists(hasContentDescription(texte(R.string.common_close)), TIMEOUT_MILLIS) + } + + /** The list, not a white screen and not another editor: its search field is there. */ + private fun laListeEstLa() { + compose.waitUntilAtLeastOneExists(hasText(texte(R.string.home_search_hint)), TIMEOUT_MILLIS) + compose.onNodeWithText(titre).assertExists() + } + + @Test + fun two_quick_taps_on_close_return_to_the_list() { + ouvrirLaNote() + val fermer = compose.onNodeWithContentDescription(texte(R.string.common_close)) + + compose.mainClock.autoAdvance = false + fermer.performClick() + fermer.performClick() + compose.mainClock.autoAdvance = true + + laListeEstLa() + } + + @Test + fun a_double_tap_on_a_note_opens_it_once() { + val carte = compose.onNodeWithText(titre) + + compose.mainClock.autoAdvance = false + carte.performClick() + carte.performClick() + compose.mainClock.autoAdvance = true + compose.waitUntilAtLeastOneExists(hasContentDescription(texte(R.string.common_close)), TIMEOUT_MILLIS) + + // One close is enough: there is one editor, not two stacked. + compose.onNodeWithContentDescription(texte(R.string.common_close)).performClick() + laListeEstLa() + } + + @Test + fun a_double_tap_on_new_note_creates_and_opens_one_note() { + val nouvelle = compose.onNodeWithContentDescription(texte(R.string.home_new_note)) + + compose.mainClock.autoAdvance = false + nouvelle.performClick() + nouvelle.performClick() + compose.mainClock.autoAdvance = true + compose.waitUntilAtLeastOneExists(hasContentDescription(texte(R.string.common_close)), TIMEOUT_MILLIS) + + laListeApresUnSeulFermer() + } + + /** The second tap comes once the note exists and the editor is opening: the screen drops it. */ + @Test + fun a_tap_on_new_note_while_the_editor_opens_is_dropped() { + val nouvelle = compose.onNodeWithContentDescription(texte(R.string.home_new_note)) + + compose.mainClock.autoAdvance = false + nouvelle.performClick() + compose.waitUntil(TIMEOUT_MILLIS) { creees() == 1 } + // The creation's event is collected and the navigation made — no frame drawn yet. + compose.waitForIdle() + nouvelle.performClick() + compose.mainClock.autoAdvance = true + compose.waitUntilAtLeastOneExists(hasContentDescription(texte(R.string.common_close)), TIMEOUT_MILLIS) + + laListeApresUnSeulFermer() + } + + private fun creees(): Int = runBlocking { notes.listAllAlive().count { it.id !in existantes } } + + /** One editor, one note: a single close is enough to come back, and one note was created. */ + private fun laListeApresUnSeulFermer() { + compose.onNodeWithContentDescription(texte(R.string.common_close)).performClick() + laListeEstLa() + assertThat(creees()).isEqualTo(1) + } + + private companion object { + const val TIMEOUT_MILLIS = 15_000L + + /** `AppSettings.KEY_SPLASH_SHOWN`, written out: the test restores exactly what it set. */ + const val SPLASH_SHOWN_KEY = "splash_shown_v1" + } +} diff --git a/app/src/main/java/com/filestech/notes_tech/ui/NotesTechNavHost.kt b/app/src/main/java/com/filestech/notes_tech/ui/NotesTechNavHost.kt index 1c10509..789b693 100644 --- a/app/src/main/java/com/filestech/notes_tech/ui/NotesTechNavHost.kt +++ b/app/src/main/java/com/filestech/notes_tech/ui/NotesTechNavHost.kt @@ -1,6 +1,7 @@ package com.filestech.notes_tech.ui import androidx.compose.runtime.Composable +import androidx.navigation.NavBackStackEntry import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.NavHost @@ -22,20 +23,24 @@ import com.filestech.notes_tech.ui.voice.VoiceSetupRoute * ⚠️ **Aucune chaîne de route n'est écrite ici** : elles viennent toutes de [Destination]. Un * `navigate("editor/$id")` dispersé dans un écran compilerait et se casserait au premier renommage, * sans que rien ne le signale avant l'exécution. + * + * 🔴 **Every navigation goes through [depuis]**: a screen navigates only while it is the one on top. */ @Composable fun NotesTechNavHost(navController: NavHostController) { NavHost(navController = navController, startDestination = Destination.Home.route) { - composable(Destination.Home.route) { + composable(Destination.Home.route) { entree -> HomeRoute( onOpenNote = { note, recherche -> - navController.navigate(Destination.Editor(note.id, recherche = recherche).route) + navController.depuis(entree) { navigate(Destination.Editor(note.id, recherche = recherche).route) } + }, + onEditNote = { + navController.depuis(entree) { navigate(Destination.Editor(it.id, ecrire = true).route) } }, - onEditNote = { navController.navigate(Destination.Editor(it.id, ecrire = true).route) }, - onOpenSearch = { navController.navigate(Destination.Search.route) }, - onOpenTrash = { navController.navigate(Destination.Trash.route) }, - onOpenSettings = { navController.navigate(Destination.Settings.route) }, - onOpenAbout = { navController.navigate(Destination.About.route) }, + onOpenSearch = { navController.depuis(entree) { navigate(Destination.Search.route) } }, + onOpenTrash = { navController.depuis(entree) { navigate(Destination.Trash.route) } }, + onOpenSettings = { navController.depuis(entree) { navigate(Destination.Settings.route) } }, + onOpenAbout = { navController.depuis(entree) { navigate(Destination.About.route) } }, ) } @@ -53,57 +58,74 @@ fun NotesTechNavHost(navController: NavHostController) { defaultValue = null }, ), - ) { + ) { entree -> // L'identifiant n'est pas relu ici : `SavedStateHandle` le remet au ViewModel, qui est // le seul à en avoir besoin. Le faire transiter par le composable ajouterait un second // chemin pour la même valeur. NoteEditorRoute( - onBack = { navController.popBackStack() }, + onBack = { navController.depuis(entree) { popBackStack() } }, // Ouvrir une note liée **empile** une entrée, comme l'application publiée : le // chemin parcouru se remonte lien par lien avec le bouton retour. - onOpenNote = { navController.navigate(Destination.Editor(it).route) }, + onOpenNote = { navController.depuis(entree) { navigate(Destination.Editor(it).route) } }, // 🔴 **Le micro sans modèle EMMÈNE ici**, il ne se contente pas de dire qu'il // manque quelque chose. Cet écran n'était atteignable que depuis les réglages, et // rien dans l'éditeur ne l'indiquait. Cf. `ControleurDeDictee.demarrer`. - onInstallerLaDictee = { navController.navigate(Destination.VoiceSetup.route) }, + onInstallerLaDictee = { navController.depuis(entree) { navigate(Destination.VoiceSetup.route) } }, ) } - composable(Destination.Search.route) { + composable(Destination.Search.route) { entree -> SearchRoute( - onBack = { navController.popBackStack() }, + onBack = { navController.depuis(entree) { popBackStack() } }, // The note opens with "Find in note" on what was searched (3.2.0, D-033). onOpenNote = { note, recherche -> - navController.navigate(Destination.Editor(note.id, recherche = recherche).route) + navController.depuis(entree) { navigate(Destination.Editor(note.id, recherche = recherche).route) } }, ) } - composable(Destination.Trash.route) { - TrashRoute(onBack = { navController.popBackStack() }) + composable(Destination.Trash.route) { entree -> + TrashRoute(onBack = { navController.depuis(entree) { popBackStack() } }) } - composable(Destination.Settings.route) { + composable(Destination.Settings.route) { entree -> SettingsRoute( - onBack = { navController.popBackStack() }, - onOpenAbout = { navController.navigate(Destination.About.route) }, - onOpenVoiceSetup = { navController.navigate(Destination.VoiceSetup.route) }, + onBack = { navController.depuis(entree) { popBackStack() } }, + onOpenAbout = { navController.depuis(entree) { navigate(Destination.About.route) } }, + onOpenVoiceSetup = { navController.depuis(entree) { navigate(Destination.VoiceSetup.route) } }, ) } - composable(Destination.About.route) { + composable(Destination.About.route) { entree -> AboutRoute( - onBack = { navController.popBackStack() }, - onOpenLegal = { navController.navigate(Destination.Legal.route) }, + onBack = { navController.depuis(entree) { popBackStack() } }, + onOpenLegal = { navController.depuis(entree) { navigate(Destination.Legal.route) } }, ) } - composable(Destination.VoiceSetup.route) { - VoiceSetupRoute(onBack = { navController.popBackStack() }) + composable(Destination.VoiceSetup.route) { entree -> + VoiceSetupRoute(onBack = { navController.depuis(entree) { popBackStack() } }) } - composable(Destination.Legal.route) { - LegalRoute(onBack = { navController.popBackStack() }) + composable(Destination.Legal.route) { entree -> + LegalRoute(onBack = { navController.depuis(entree) { popBackStack() } }) } } } + +/** + * Runs [navigation] only if [entree] is still the screen on top. + * + * 🔴 **A screen that is leaving must not navigate again** (3.2.1). During the exit transition + * (~0.5 s) the screen being left is still drawn and still takes taps. On 3.1.1 and 3.2.0 as + * published, a second tap on « Fermer » reached `popBackStack()` from the editor already popped, + * and popped the note list too: an empty NavHost, a WHITE screen, left only by Back. A double tap + * on a note or a menu entry stacked its screen twice. Measured on the emulator, `DoubleToucherTest`. + * + * Not `dropUnlessResumed`: it also drops what the editor navigates on its own while its entry is + * not resumed yet (a deletion done, the vault-unlock sheet dismissed, a linked note created), and + * the note would stay on screen. The screen on top may always navigate, whatever its state. + */ +private inline fun NavHostController.depuis(entree: NavBackStackEntry, navigation: NavHostController.() -> Unit) { + if (currentBackStackEntry === entree) navigation() +} diff --git a/app/src/main/java/com/filestech/notes_tech/ui/home/HomeRoute.kt b/app/src/main/java/com/filestech/notes_tech/ui/home/HomeRoute.kt index a73a03f..dc345cb 100644 --- a/app/src/main/java/com/filestech/notes_tech/ui/home/HomeRoute.kt +++ b/app/src/main/java/com/filestech/notes_tech/ui/home/HomeRoute.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.compose.dropUnlessResumed import com.filestech.notes_tech.R import com.filestech.notes_tech.domain.model.Folder import com.filestech.notes_tech.domain.model.Note @@ -223,7 +224,10 @@ fun HomeRoute( val coffreAOuvrir = gestes.appuiLong(note, foldersState.folders, foldersState.unlockedFolderIds) if (coffreAOuvrir != null) dossierAOuvrir = coffreAOuvrir }, - onNewNote = homeViewModel::createNote, + // 🔴 Dropped once the list is no longer the screen in front (3.2.1): a second tap + // during the editor's opening transition created a second, empty note. The tap + // that comes while the creation runs is dropped by the ViewModel. + onNewNote = dropUnlessResumed { homeViewModel.createNote() }, onOpenDrawer = { portee.launch { drawerState.open() } }, onOpenSearch = onOpenSearch, onOpenSettings = onOpenSettings, diff --git a/app/src/main/java/com/filestech/notes_tech/ui/home/HomeViewModel.kt b/app/src/main/java/com/filestech/notes_tech/ui/home/HomeViewModel.kt index 80ce011..8eb7d0f 100644 --- a/app/src/main/java/com/filestech/notes_tech/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/filestech/notes_tech/ui/home/HomeViewModel.kt @@ -18,6 +18,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CancellationException import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -112,6 +113,9 @@ class HomeViewModel @Inject constructor( private val events = MutableSharedFlow(extraBufferCapacity = 4) val eventFlow = events.asSharedFlow() + /** The note creation running, if any — see [createNote]. */ + private var creation: Job? = null + /** * Le texte de recherche **freiné**, et lui seul déclenche une requête. * @@ -185,9 +189,17 @@ class HomeViewModel @Inject constructor( * Le contrôle `isUnlocked` ci-dessous n'est donc PAS la protection, seulement la **courtoisie** * qui propose la saisie du secret au lieu d'un message d'erreur. La protection, c'est la * transaction. + * + * 🔴 **One creation at a time** (3.2.1): two quick taps on « Nouvelle note » created two notes, + * the second one left empty in the list (measured, `DoubleToucherTest`). The tap that comes once + * the editor is opening is dropped by the screen (`HomeRoute`, `dropUnlessResumed`); the one that + * comes while the creation still runs is dropped here, as `ImportViewModel` and `ExportViewModel` + * drop theirs. ⚠️ No test reaches this second case: on the emulator the creation is over before + * any second tap. It is kept for slow phones, where the database write takes longer. */ fun createNote() { - viewModelScope.launch { + if (creation?.isActive == true) return + creation = viewModelScope.launch { val cible = folderId.value ?: Folder.INBOX_ID val dossier = folders.find(cible) if (dossier?.isVault == true && !vaults.isUnlocked(cible)) { From c9b1a435ea3a3e909a327e2186041135b58107f6 Mon Sep 17 00:00:00 2001 From: Patrice Haltaya Date: Fri, 30 Oct 2026 15:31:02 +0100 Subject: [PATCH 2/2] =?UTF-8?q?release:=203.2.1=20(504)=20=E2=80=94=20chan?= =?UTF-8?q?gelogs,=20READMEs,=20SECURITY,=20D-035?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit versionCode base 504 (APKs 5040 universal, 5041/5042/5043 per ABI). Changelogs in five languages (223 to 280 characters). README and SECURITY in English and French: what's new in 3.2.1, "no security change". docs/13-VERSION-3.2.md: D-035, the double tap. Co-Authored-By: Claude Opus 5.5 --- README.fr.md | 12 +++++-- README.md | 12 +++++-- SECURITY.fr.md | 8 ++++- SECURITY.md | 7 +++- docs/13-VERSION-3.2.md | 35 +++++++++++++++++++ .../android/de-DE/changelogs/5041.txt | 1 + .../android/de-DE/changelogs/5042.txt | 1 + .../android/de-DE/changelogs/5043.txt | 1 + .../android/en-US/changelogs/5041.txt | 1 + .../android/en-US/changelogs/5042.txt | 1 + .../android/en-US/changelogs/5043.txt | 1 + .../android/es-ES/changelogs/5041.txt | 1 + .../android/es-ES/changelogs/5042.txt | 1 + .../android/es-ES/changelogs/5043.txt | 1 + .../android/fr-FR/changelogs/5041.txt | 1 + .../android/fr-FR/changelogs/5042.txt | 1 + .../android/fr-FR/changelogs/5043.txt | 1 + .../android/it-IT/changelogs/5041.txt | 1 + .../android/it-IT/changelogs/5042.txt | 1 + .../android/it-IT/changelogs/5043.txt | 1 + version.properties | 4 +-- 21 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 fastlane/metadata/android/de-DE/changelogs/5041.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/5042.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/5043.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/5041.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/5042.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/5043.txt create mode 100644 fastlane/metadata/android/es-ES/changelogs/5041.txt create mode 100644 fastlane/metadata/android/es-ES/changelogs/5042.txt create mode 100644 fastlane/metadata/android/es-ES/changelogs/5043.txt create mode 100644 fastlane/metadata/android/fr-FR/changelogs/5041.txt create mode 100644 fastlane/metadata/android/fr-FR/changelogs/5042.txt create mode 100644 fastlane/metadata/android/fr-FR/changelogs/5043.txt create mode 100644 fastlane/metadata/android/it-IT/changelogs/5041.txt create mode 100644 fastlane/metadata/android/it-IT/changelogs/5042.txt create mode 100644 fastlane/metadata/android/it-IT/changelogs/5043.txt diff --git a/README.fr.md b/README.fr.md index 766b52a..228d2ff 100644 --- a/README.fr.md +++ b/README.fr.md @@ -4,7 +4,7 @@ 🇬🇧 [English version](README.md) -**v3.2.0 — octobre 2026** · [Politique de confidentialité](PRIVACY.md) · [Conditions d'utilisation](TERMS.md) · [Sécurité](SECURITY.fr.md) +**v3.2.1 — octobre 2026** · [Politique de confidentialité](PRIVACY.md) · [Conditions d'utilisation](TERMS.md) · [Sécurité](SECURITY.fr.md) Application de prise de notes Markdown chiffrée pour Android, écrite en **Kotlin** avec Jetpack Compose. **100 % locale, sans permission Internet.** Interface en **anglais, français, allemand, @@ -21,6 +21,14 @@ vérifier.** --- +## Nouveautés de la 3.2.1 + +- **Corrigé** : deux touchers rapides sur la flèche de retour d'une note laissaient un écran blanc — + le second, pendant l'animation de fermeture, quittait aussi la liste des notes (aussi en 3.1.1 et + 3.2.0 ; Retour fermait l'appli, rien n'était perdu). Un écran qui s'en va ne navigue plus. +- **Corrigé** : un double toucher rapide sur « Nouvelle note » créait une seconde note vide, et un + double toucher sur une note l'ouvrait deux fois. + ## Nouveautés de la 3.2.0 - **Sauvegarde chiffrée** (Réglages → Sauvegarde chiffrée) : vos notes et dossiers, corbeille comprise, @@ -191,7 +199,7 @@ avec sa propre clé. Une copie installée depuis F-Droid reçoit la 3.0.0 par F- ## Installation 1. **APK publié** — depuis les [Releases GitHub](https://github.com/gitubpatrice/notes_tech/releases), - l'APK universel (`notes-tech-universel-3.2.0.apk`, tous téléphones), ou le fichier plus léger de + l'APK universel (`notes-tech-universel-3.2.1.apk`, tous téléphones), ou le fichier plus léger de votre appareil (`arm64-v8a` convient à presque tous les téléphones depuis 2016). Vérifiez l'empreinte SHA-256 publiée dans les notes de version. 2. **F-Droid** — [f-droid.org/packages/com.filestech.notes_tech](https://f-droid.org/packages/com.filestech.notes_tech/), construite et signée par F-Droid. diff --git a/README.md b/README.md index d64026a..35b7c15 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ 🇫🇷 [Version française](README.fr.md) -**v3.2.0 — October 2026** · [Privacy policy](PRIVACY.md) · [Terms of use](TERMS.md) · [Security](SECURITY.md) +**v3.2.1 — October 2026** · [Privacy policy](PRIVACY.md) · [Terms of use](TERMS.md) · [Security](SECURITY.md) Encrypted Markdown note-taking app for Android, written in **Kotlin** with Jetpack Compose. **100% local, no Internet permission.** Interface in **English, French, German, Italian and @@ -20,6 +20,14 @@ technically unable to send anything, and you can check that in its manifest.** --- +## What's new in 3.2.1 + +- **Fixed**: two quick taps on a note's back arrow left a blank white screen — the second tap, during + the closing animation, left the note list too (also in 3.1.1 and 3.2.0; Back closed the app, nothing + was lost). A screen that is leaving no longer navigates. +- **Fixed**: a quick double tap on "New note" created a second, empty note, and a double tap on a + note opened it twice. + ## What's new in 3.2.0 - **Encrypted backup** (Settings → Encrypted backup): your notes and folders, trash included, in one @@ -180,7 +188,7 @@ A copy installed from F-Droid receives the 3.0.0 through F-Droid. ## Installation 1. **Published APK** — from [GitHub Releases](https://github.com/gitubpatrice/notes_tech/releases), - the universal APK (`notes-tech-universel-3.2.0.apk`, any phone), or the lighter one for your device + the universal APK (`notes-tech-universel-3.2.1.apk`, any phone), or the lighter one for your device (`arm64-v8a` fits almost every phone since 2016). Check the SHA-256 published in the release notes. 2. **F-Droid** — [f-droid.org/packages/com.filestech.notes_tech](https://f-droid.org/packages/com.filestech.notes_tech/), built and signed by F-Droid. diff --git a/SECURITY.fr.md b/SECURITY.fr.md index b8dcf6d..3491d79 100644 --- a/SECURITY.fr.md +++ b/SECURITY.fr.md @@ -2,10 +2,16 @@ *English version: [SECURITY.md](SECURITY.md)* -**Version actuelle : v3.2.0 — octobre 2026.** Notes Tech 3.0 est une réécriture en Kotlin. Le journal +**Version actuelle : v3.2.1 — octobre 2026.** Notes Tech 3.0 est une réécriture en Kotlin. Le journal de sécurité des versions Flutter 1.x et 2.x est conservé tel quel, dans [`SECURITY.fr.md` au tag v2.0.9](https://github.com/gitubpatrice/notes_tech/blob/v2.0.9/SECURITY.fr.md). +## v3.2.1 — aucun changement de sécurité (2026-10-30) + +Un correctif de navigation : deux touchers rapides sur la flèche de retour d'une note laissaient un +écran vide, un double toucher sur « Nouvelle note » créait une seconde note. Permissions, stockage et +chiffrement sont ceux de la 3.2.0. + ## v3.2.0 — sauvegarde chiffrée, import, partage entrant, le champ du corps (2026-10-30) - **Sauvegarde chiffrée** (`.ntbackup` v1) : un en-tête de 48 octets authentifié comme données associées diff --git a/SECURITY.md b/SECURITY.md index addee73..66627c3 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,10 +2,15 @@ *Version française : [SECURITY.fr.md](SECURITY.fr.md)* -**Current version: v3.2.0 — October 2026.** Notes Tech 3.0 is a rewrite in Kotlin. The security log +**Current version: v3.2.1 — October 2026.** Notes Tech 3.0 is a rewrite in Kotlin. The security log of the Flutter versions 1.x and 2.x is kept as it was, in [`SECURITY.md` at tag v2.0.9](https://github.com/gitubpatrice/notes_tech/blob/v2.0.9/SECURITY.md). +## v3.2.1 — no security change (2026-10-30) + +A navigation fix: two quick taps on a note's back arrow left an empty screen, a double tap on "New +note" created a second note. Permissions, storage and encryption are those of 3.2.0. + ## v3.2.0 — encrypted backup, import, incoming share, the body field (2026-10-30) - **Encrypted backup** (`.ntbackup` v1): a 48-byte header authenticated as associated data (format diff --git a/docs/13-VERSION-3.2.md b/docs/13-VERSION-3.2.md index c743d17..565513a 100644 --- a/docs/13-VERSION-3.2.md +++ b/docs/13-VERSION-3.2.md @@ -317,3 +317,38 @@ deux défauts, chaque image pourtant dessinée en moins de 12 ms. (restauration avec nouveau code), partage entrant, barre de recherche. - Chaque nouveau test vu échouer une fois, sabotage à l'appui. - Relecture externe (GPT) du format et du code de la sauvegarde ; audit de sécurité avant la release. + +## 7. 3.2.1 — le double toucher (2026-10-30) + +### D-035 — Seul l'écran du dessus navigue ; « Nouvelle note » ne crée qu'une note à la fois + +**Le défaut, publié** (3.1.1 et 3.2.0, sans doute depuis la 3.0.0) : deux touchers rapides sur « Fermer » d'une +note, à moins d'environ 0,5 s, laissaient un **écran blanc**. Chaque `onBack` du `NavHost` appelait +`popBackStack()` sans garde ; pendant l'animation de sortie, l'éditeur est encore dessiné et prend le second +toucher, qui dépilait aussi la liste — un `NavHost` vide. Retour fermait l'appli, rien n'était perdu. Mesuré sur +la 3.2.0 publiée (émulateur) : 0,4 s → blanc, 0,7 s → normal. Vu pendant le test de mise à jour de la 3.2.0, mis à +tort sur le compte des touches envoyées par adb, et publié quand même : la règle est désormais qu'une anomalie +vue en testant se diagnostique AVANT le tag. + +Même famille, mesurée : un double toucher sur une note l'empilait deux fois ; un double toucher sur +« Nouvelle note » empilait deux éditeurs (S9) et créait **deux notes**, la seconde vide (`DoubleToucherTest`). +Mesuré SANS défaut : deux Retour système rapides, et la flèche suivie d'un Retour — le `BackHandler` de l'éditeur +part avec son entrée ; l'import (un seul sélecteur ouvert, S9) ; la sauvegarde, les coffres, les actions du menu +d'une note (déjà gardés) ; corbeille, gestes de la liste, suppression de dossier (un second appui ne trouve plus +rien à faire, ou le dialogue est déjà fermé). + +**Décision** : +- `NotesTechNavHost` : toute navigation passe par `depuis(entree)` — elle n'a lieu que si l'entrée du composable + qui la demande est encore `currentBackStackEntry`. **Pas `dropUnlessResumed`** : il jetterait aussi ce que + l'éditeur navigue de lui-même (suppression réussie, feuille de déverrouillage fermée, note liée créée) tant que + son entrée n'est pas `RESUMED`, par exemple pendant sa propre animation d'entrée, et la note resterait affichée. +- `HomeViewModel.createNote` : une création à la fois (`Job` en vol, comme l'import et l'export). +- `HomeRoute` : le toucher sur « Nouvelle note » passe par `dropUnlessResumed` — une fois la note créée et + l'éditeur en train de s'ouvrir, la liste n'est plus `RESUMED` et un second toucher ne crée plus de note vide. + +**Preuve** : `DoubleToucherTest` (horloge de Compose arrêtée entre les deux gestes), quatre tests, chacun vu +rouge (« Fermer » et la note sur le code de la 3.2.0), verts avec les gardes. Retirée seule, la garde de navigation fait tomber ses deux tests, celle de +l'écran les deux de « Nouvelle note ». ⚠️ **Celle du ViewModel, aucun** : sur l'émulateur la création est finie +avant tout second toucher. Elle reste pour les téléphones lents, comme celles de l'import et de l'export. Suite +instrumentée complète : 547 tests, les 12 échecs habituels de l'émulateur (Keystore logiciel, biométrie, modèle +Whisper). diff --git a/fastlane/metadata/android/de-DE/changelogs/5041.txt b/fastlane/metadata/android/de-DE/changelogs/5041.txt new file mode 100644 index 0000000..dbcde06 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/5041.txt @@ -0,0 +1 @@ +Behoben: Zwei schnelle Tipps auf den Zurück-Pfeil einer Notiz hinterließen einen leeren weißen Bildschirm (auch in 3.1.1 und 3.2.0). Ein schneller Doppeltipp auf „Neue Notiz“ erstellt keine zweite, leere Notiz mehr, und ein Doppeltipp auf eine Notiz öffnet sie nicht mehr zweimal. diff --git a/fastlane/metadata/android/de-DE/changelogs/5042.txt b/fastlane/metadata/android/de-DE/changelogs/5042.txt new file mode 100644 index 0000000..dbcde06 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/5042.txt @@ -0,0 +1 @@ +Behoben: Zwei schnelle Tipps auf den Zurück-Pfeil einer Notiz hinterließen einen leeren weißen Bildschirm (auch in 3.1.1 und 3.2.0). Ein schneller Doppeltipp auf „Neue Notiz“ erstellt keine zweite, leere Notiz mehr, und ein Doppeltipp auf eine Notiz öffnet sie nicht mehr zweimal. diff --git a/fastlane/metadata/android/de-DE/changelogs/5043.txt b/fastlane/metadata/android/de-DE/changelogs/5043.txt new file mode 100644 index 0000000..dbcde06 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/5043.txt @@ -0,0 +1 @@ +Behoben: Zwei schnelle Tipps auf den Zurück-Pfeil einer Notiz hinterließen einen leeren weißen Bildschirm (auch in 3.1.1 und 3.2.0). Ein schneller Doppeltipp auf „Neue Notiz“ erstellt keine zweite, leere Notiz mehr, und ein Doppeltipp auf eine Notiz öffnet sie nicht mehr zweimal. diff --git a/fastlane/metadata/android/en-US/changelogs/5041.txt b/fastlane/metadata/android/en-US/changelogs/5041.txt new file mode 100644 index 0000000..fffbbee --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/5041.txt @@ -0,0 +1 @@ +Fixed: two quick taps on a note's back arrow left a blank white screen (also in 3.1.1 and 3.2.0). A quick double tap on “New note” no longer creates a second, empty note, and a double tap on a note no longer opens it twice. diff --git a/fastlane/metadata/android/en-US/changelogs/5042.txt b/fastlane/metadata/android/en-US/changelogs/5042.txt new file mode 100644 index 0000000..fffbbee --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/5042.txt @@ -0,0 +1 @@ +Fixed: two quick taps on a note's back arrow left a blank white screen (also in 3.1.1 and 3.2.0). A quick double tap on “New note” no longer creates a second, empty note, and a double tap on a note no longer opens it twice. diff --git a/fastlane/metadata/android/en-US/changelogs/5043.txt b/fastlane/metadata/android/en-US/changelogs/5043.txt new file mode 100644 index 0000000..fffbbee --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/5043.txt @@ -0,0 +1 @@ +Fixed: two quick taps on a note's back arrow left a blank white screen (also in 3.1.1 and 3.2.0). A quick double tap on “New note” no longer creates a second, empty note, and a double tap on a note no longer opens it twice. diff --git a/fastlane/metadata/android/es-ES/changelogs/5041.txt b/fastlane/metadata/android/es-ES/changelogs/5041.txt new file mode 100644 index 0000000..03f6778 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/5041.txt @@ -0,0 +1 @@ +Corregido: dos toques rápidos en la flecha atrás de una nota dejaban una pantalla en blanco (también en la 3.1.1 y la 3.2.0). Un doble toque rápido en «Nueva nota» ya no crea una segunda nota vacía, y un doble toque en una nota ya no la abre dos veces. diff --git a/fastlane/metadata/android/es-ES/changelogs/5042.txt b/fastlane/metadata/android/es-ES/changelogs/5042.txt new file mode 100644 index 0000000..03f6778 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/5042.txt @@ -0,0 +1 @@ +Corregido: dos toques rápidos en la flecha atrás de una nota dejaban una pantalla en blanco (también en la 3.1.1 y la 3.2.0). Un doble toque rápido en «Nueva nota» ya no crea una segunda nota vacía, y un doble toque en una nota ya no la abre dos veces. diff --git a/fastlane/metadata/android/es-ES/changelogs/5043.txt b/fastlane/metadata/android/es-ES/changelogs/5043.txt new file mode 100644 index 0000000..03f6778 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/5043.txt @@ -0,0 +1 @@ +Corregido: dos toques rápidos en la flecha atrás de una nota dejaban una pantalla en blanco (también en la 3.1.1 y la 3.2.0). Un doble toque rápido en «Nueva nota» ya no crea una segunda nota vacía, y un doble toque en una nota ya no la abre dos veces. diff --git a/fastlane/metadata/android/fr-FR/changelogs/5041.txt b/fastlane/metadata/android/fr-FR/changelogs/5041.txt new file mode 100644 index 0000000..b9c8b03 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/5041.txt @@ -0,0 +1 @@ +Corrigé : deux touchers rapides sur la flèche de retour d'une note laissaient un écran blanc (aussi en 3.1.1 et 3.2.0). Un double toucher sur « Nouvelle note » ne crée plus une seconde note vide, et un double toucher sur une note ne l'ouvre plus deux fois. diff --git a/fastlane/metadata/android/fr-FR/changelogs/5042.txt b/fastlane/metadata/android/fr-FR/changelogs/5042.txt new file mode 100644 index 0000000..b9c8b03 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/5042.txt @@ -0,0 +1 @@ +Corrigé : deux touchers rapides sur la flèche de retour d'une note laissaient un écran blanc (aussi en 3.1.1 et 3.2.0). Un double toucher sur « Nouvelle note » ne crée plus une seconde note vide, et un double toucher sur une note ne l'ouvre plus deux fois. diff --git a/fastlane/metadata/android/fr-FR/changelogs/5043.txt b/fastlane/metadata/android/fr-FR/changelogs/5043.txt new file mode 100644 index 0000000..b9c8b03 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/5043.txt @@ -0,0 +1 @@ +Corrigé : deux touchers rapides sur la flèche de retour d'une note laissaient un écran blanc (aussi en 3.1.1 et 3.2.0). Un double toucher sur « Nouvelle note » ne crée plus une seconde note vide, et un double toucher sur une note ne l'ouvre plus deux fois. diff --git a/fastlane/metadata/android/it-IT/changelogs/5041.txt b/fastlane/metadata/android/it-IT/changelogs/5041.txt new file mode 100644 index 0000000..d012629 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/5041.txt @@ -0,0 +1 @@ +Corretto: due tocchi rapidi sulla freccia indietro di una nota lasciavano una schermata bianca (anche nella 3.1.1 e nella 3.2.0). Un doppio tocco rapido su «Nuova nota» non crea più una seconda nota vuota, e un doppio tocco su una nota non la apre più due volte. diff --git a/fastlane/metadata/android/it-IT/changelogs/5042.txt b/fastlane/metadata/android/it-IT/changelogs/5042.txt new file mode 100644 index 0000000..d012629 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/5042.txt @@ -0,0 +1 @@ +Corretto: due tocchi rapidi sulla freccia indietro di una nota lasciavano una schermata bianca (anche nella 3.1.1 e nella 3.2.0). Un doppio tocco rapido su «Nuova nota» non crea più una seconda nota vuota, e un doppio tocco su una nota non la apre più due volte. diff --git a/fastlane/metadata/android/it-IT/changelogs/5043.txt b/fastlane/metadata/android/it-IT/changelogs/5043.txt new file mode 100644 index 0000000..d012629 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/5043.txt @@ -0,0 +1 @@ +Corretto: due tocchi rapidi sulla freccia indietro di una nota lasciavano una schermata bianca (anche nella 3.1.1 e nella 3.2.0). Un doppio tocco rapido su «Nuova nota» non crea più una seconda nota vuota, e un doppio tocco su una nota non la apre più due volte. diff --git a/version.properties b/version.properties index 00e5467..c348ccb 100644 --- a/version.properties +++ b/version.properties @@ -21,5 +21,5 @@ # 500 is a clean step above the whole 2.0.x line, and leaves 408..499 to any Flutter bug-fix # release shipped during the transition. ⚠️ Before publishing, compare with the LAST published # Flutter tag, not with this comment: the base must stay above it. -versionCode=503 -versionName=3.2.0 +versionCode=504 +versionName=3.2.1