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
12 changes: 10 additions & 2 deletions README.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 7 additions & 1 deletion SECURITY.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<MainActivity>? = null
private var presentationDejaVue = false
private var existantes = emptySet<String>()

@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"
}
}
Loading
Loading