diff --git a/README.fr.md b/README.fr.md index 57f6e627..a2c67dbf 100644 --- a/README.fr.md +++ b/README.fr.md @@ -21,6 +21,26 @@ vérifier.** --- +## Nouveautés de la 3.4.0 + +- **Des notes à durée de vie** : ⋮ › Durée de vie… — une note s'efface pour de bon après 1 heure, + 24 heures, 7 ou 30 jours, ou quand on la referme après l'avoir rouverte. Sa carte montre ce qu'il lui + reste. L'effacement a lieu à la prochaine ouverture de l'appli ou sur la liste, pas à la seconde près, + et une telle note n'entre ni dans la sauvegarde ni dans l'export. +- **Partager une note scellée** : ⋮ › Partager scellée… — la note est chiffrée (Argon2id, AES-256-GCM) + dans un fichier `.ntnote` que seul un code de 24 signes ouvre, affiché une fois et gardé nulle part. + Envoyez le fichier par n'importe quelle appli et le code par un autre moyen ; elle peut être « à + lecture unique » chez son destinataire. Une note scellée reçue s'ouvre par « Ouvrir avec » ou ⋮ › + Ouvrir une note scellée, et arrive dans la Boîte de réception une fois son code tapé — jamais dans un + coffre. +- **Dépôt rapide** : écrire une note sans déverrouiller Notes Tech, depuis une tuile des réglages rapides + ou un appui long sur l'icône. Il ne fait qu'écrire, dans la Boîte de réception : rien de ce que l'appli + contient ne s'y lit. +- **Raccourcis sur l'icône** : Nouvelle note, Dicter, Dépôt rapide. +- **Clavier sans apprentissage** (Réglages › Sécurité, activé par défaut) : chaque champ demande au + clavier de ne rien apprendre ; les champs de secret le demandent toujours. +- Sous le capot : AGP 9.5.0, Gradle 9.8.2, SQLCipher 4.20.0 ; DataStore, jamais utilisé, retiré. + ## Nouveautés de la 3.3.0 Suggérées dans [#48](https://github.com/gitubpatrice/notes_tech/issues/48). diff --git a/README.md b/README.md index 228f0c86..cbc40469 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,23 @@ technically unable to send anything, and you can check that in its manifest.** --- +## What's new in 3.4.0 + +- **Notes with a lifetime**: ⋮ › Lifetime… — a note is erased for good after 1 hour, 24 hours, 7 or 30 + days, or when it is closed after its next reading. Its card shows what is left. The erasing happens at + the next opening of the app or on the list, not to the second, and such a note goes into neither the + backup nor the export. +- **Share a note sealed**: ⋮ › Share sealed… — the note is encrypted (Argon2id, AES-256-GCM) into a + `.ntnote` file that only a 24-sign code opens, shown once and kept nowhere. Send the file by any app and + the code another way; it can be "read once" by its recipient. A sealed note received opens with "Open + with" or ⋮ › Open a sealed note, and lands in the Inbox once its code is typed — never in a vault. +- **Quick drop**: write a note without unlocking Notes Tech, from a quick settings tile or a long press + on the app's icon. It only writes, to the Inbox: nothing the app holds can be read there. +- **Shortcuts on the icon**: New note, Dictate, Quick drop. +- **Keyboard without learning** (Settings › Security, on by default): every field asks the keyboard not + to learn what is typed; secret fields always do. +- Under the hood: AGP 9.5.0, Gradle 9.8.2, SQLCipher 4.20.0; DataStore, never used, removed. + ## What's new in 3.3.0 Suggested in [#48](https://github.com/gitubpatrice/notes_tech/issues/48). diff --git a/REPRISE.md b/REPRISE.md index e50c98e9..f90163e6 100644 --- a/REPRISE.md +++ b/REPRISE.md @@ -3,7 +3,15 @@ > Écrit le 2026-08-15 au soir, mis à jour le 2026-08-19, le 2026-09-24, le 2026-09-25, le 2026-10-05, puis le 2026-10-10, puis le 2026-10-30 (deux fois), le 2026-10-31, **puis le 2026-11-09**. À lire en premier, avant `docs/00-PLAN.md`. > Ce fichier ne remplace pas les docs : il dit **où on en est** et **quoi faire ensuite**. -## 🧭 À LA REPRISE (écrit le 2026-11-09) — la 3.3.0 est PUBLIÉE +## 🧭 À LA REPRISE (écrit le 2026-11-09 au soir) — la 3.4.0 est EN COURS (branche `feat/3.4.0`) + +Patrice : « fais 2, 3, 4 ce serait génial pour la 3.4.0 + raccourcis et clavier sans apprentissage », +« en intégrant les upgrade AGP aussi » (déjà dans `main`, PR #51). **Spécification, décisions D-040 à +D-044 et état coché au fur et à mesure : `docs/15-VERSION-3.4.md`** — la lire en premier. +La release n'est PAS demandée : la proposer à la fin, comme pour la 3.3.0. Avant le tag, regarder +comment F-Droid a construit SMS 1.30.0 (première build sous AGP 9.5). + +## Précédemment (écrit le 2026-11-09) — la 3.3.0 est PUBLIÉE Issue #48 (koad1x) : trois suggestions. Patrice : « fais les 3 maintenant pour la version 3.3.0, proprement, avec vigilance », carte blanche (coder, tester, commiter, fusionner), « tous les audits diff --git a/SECURITY.fr.md b/SECURITY.fr.md index 7e8402ba..ce6ee823 100644 --- a/SECURITY.fr.md +++ b/SECURITY.fr.md @@ -2,10 +2,36 @@ *English version: [SECURITY.md](SECURITY.md)* -**Version actuelle : v3.3.0 — novembre 2026.** Notes Tech 3.0 est une réécriture en Kotlin. Le journal +**Version actuelle : v3.4.0 — novembre 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.4.0 — de nouvelles portes, tenues étroites (2026-11-09) + +- **Le dépôt rapide écrit sans le verrou de l'appli.** Le verrou est une barrière de lecture — la base + s'ouvre sans secret de l'utilisateur, comme depuis la 3.0.0. L'activité du dépôt n'est pas exportée + (la tuile et le raccourci la démarrent au nom de l'appli), ne lit rien, écrit une note dans la Boîte de + réception — jamais un coffre —, tient une fenêtre protégée, coupe elle-même les actions de texte des + autres applis et le classifieur (elle peut être la première activité de son processus), garde son texte + hors de l'état sauvegardé, et n'écrit rien pendant une panique en cours. La tuile demande d'abord le + déverrouillage du téléphone. +- **Les raccourcis de l'icône** arrivent sur une activité non exportée qui dépose la demande en mémoire ; + `MainActivity` continue d'ignorer ce que porte un intent ; la demande ne s'exécute qu'appli déverrouillée. +- **Les notes scellées** reprennent le conteneur de la sauvegarde (Argon2id m=64 Mio t=3, AES-256-GCM, + l'en-tête en AAD) sous leur propre magie. Seul un code généré de 24 signes (120 bits) en ouvre une ; il + est affiché une fois, gardé nulle part, copié seulement par le presse-papiers sensible ; le nom du + fichier ne dit rien de la note. À la réception : une activité `VIEW` exportée ne lit qu'un `content://` + d'une autre appli, borné en taille (4 Mio) et en temps, la magie vérifiée d'abord ; rien n'est + déchiffré avant le déverrouillage ; le code tapé n'atteint jamais un Bundle ; un code faux et un fichier + abîmé se ressemblent ; une note reçue va dans la Boîte de réception, jamais un coffre. +- **Les notes à durée de vie** sont effacées par le DELETE même de « Supprimer définitivement » (index + plein texte, `secure-delete`, liens) ; elles sont exclues de la sauvegarde et de l'export. Une durée de + vie suit l'horloge du téléphone : avancer la date efface les notes dont elle dépasse le terme. « Après + lecture » efface à la fermeture de l'éditeur — retour, ou appli fermée —, jamais sur une rotation ; si + Android arrête l'appli avant, la note part à sa fermeture suivante. +- **Clavier** : `IME_FLAG_NO_PERSONALIZED_LEARNING` sur chaque champ par défaut, toujours sur les champs + de secret. + ## v3.3.0 — archives et sélection : les règles des coffres, pour plusieurs notes à la fois (2026-11-09) La sélection agit sur plusieurs notes à la fois — archiver, déplacer, mettre à la corbeille, épingler, diff --git a/SECURITY.md b/SECURITY.md index 3eb27c53..2cc141be 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,10 +2,34 @@ *Version française : [SECURITY.fr.md](SECURITY.fr.md)* -**Current version: v3.3.0 — November 2026.** Notes Tech 3.0 is a rewrite in Kotlin. The security log +**Current version: v3.4.0 — November 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.4.0 — new doors, kept narrow (2026-11-09) + +- **Quick drop writes without the app lock.** The lock is a gate on reading — the database opens with no + user secret, as since 3.0.0. The drop's activity is not exported (the tile and the shortcut start it as + this app), reads nothing, writes one note to the Inbox — never a vault —, holds a protected window, cuts + the other apps' text actions and the classifier itself (it can be the first activity of its process), + keeps its text out of the saved state, and writes nothing while a panic is pending. The tile asks the + phone to be unlocked first. +- **Launcher shortcuts** land on a non-exported activity that deposits the request in memory; + `MainActivity` keeps ignoring what an intent carries; the request runs only once the app is unlocked. +- **Sealed notes** use the backup's container (Argon2id m=64 MiB t=3, AES-256-GCM, the header as AAD) + under their own magic. Only a generated code of 24 signs (120 bits) opens one; it is shown once, kept + nowhere, copied only through the sensitive clipboard; the file's name says nothing of the note. + Receiving: an exported `VIEW` activity reads only a `content://` of another app, bounded in size + (4 MiB) and in time, the magic checked first; nothing is decrypted before the app is unlocked; the + typed code never reaches a Bundle; a wrong code and a damaged file look the same; a received note goes + to the Inbox, never a vault. +- **Notes with a lifetime** are erased by the very DELETE of "Delete permanently" (FTS index, + `secure-delete`, links); they are left out of the backup and the export. A lifetime follows the phone's + clock: moving the date forward erases the notes whose time it passes. "After reading" erases when the + editor closes — going back, or the app closed —, never on a rotation; if Android ends the app first, the + note goes at its next closing. +- **Keyboard**: `IME_FLAG_NO_PERSONALIZED_LEARNING` on every field by default, always on secret fields. + ## v3.3.0 — archive and selection: the vault rules, for many notes at once (2026-11-09) The selection acts on many notes at once — archive, move, trash, pin, favourite, colour, permanent diff --git a/app/schemas/com.filestech.notes_tech.data.local.NotesDatabase/11.json b/app/schemas/com.filestech.notes_tech.data.local.NotesDatabase/11.json new file mode 100644 index 00000000..2a152a66 --- /dev/null +++ b/app/schemas/com.filestech.notes_tech.data.local.NotesDatabase/11.json @@ -0,0 +1,290 @@ +{ + "formatVersion": 1, + "database": { + "version": 11, + "identityHash": "1a9615da6e5de5c045cf8c66cc3c3970", + "entities": [ + { + "tableName": "folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `parent_id` TEXT, `color` INTEGER, `icon` TEXT, `created_at` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL, `vault_salt` BLOB, `vault_kek_wrapped` BLOB, `vault_iv` BLOB, `vault_verifier` BLOB, `vault_mode` TEXT, `vault_pin_blob` BLOB, `vault_pin_iv` BLOB, `vault_attempts` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`), FOREIGN KEY(`parent_id`) REFERENCES `folders`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT" + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER" + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "vaultSalt", + "columnName": "vault_salt", + "affinity": "BLOB" + }, + { + "fieldPath": "vaultKekWrapped", + "columnName": "vault_kek_wrapped", + "affinity": "BLOB" + }, + { + "fieldPath": "vaultIv", + "columnName": "vault_iv", + "affinity": "BLOB" + }, + { + "fieldPath": "vaultVerifier", + "columnName": "vault_verifier", + "affinity": "BLOB" + }, + { + "fieldPath": "vaultMode", + "columnName": "vault_mode", + "affinity": "TEXT" + }, + { + "fieldPath": "vaultPinBlob", + "columnName": "vault_pin_blob", + "affinity": "BLOB" + }, + { + "fieldPath": "vaultPinIv", + "columnName": "vault_pin_iv", + "affinity": "BLOB" + }, + { + "fieldPath": "vaultAttempts", + "columnName": "vault_attempts", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "idx_folders_parent", + "unique": false, + "columnNames": [ + "parent_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `idx_folders_parent` ON `${TABLE_NAME}` (`parent_id`)" + } + ], + "foreignKeys": [ + { + "table": "folders", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "parent_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `encrypted_content` BLOB, `folder_id` TEXT NOT NULL, `tags` TEXT NOT NULL DEFAULT '', `pinned` INTEGER NOT NULL DEFAULT 0, `favorite` INTEGER NOT NULL DEFAULT 0, `archived` INTEGER NOT NULL DEFAULT 0, `trashed_at` INTEGER, `created_at` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL, `enc_v` INTEGER NOT NULL DEFAULT 1, `color_id` INTEGER, `expires_at` INTEGER, `read_once` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`), FOREIGN KEY(`folder_id`) REFERENCES `folders`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encryptedContent", + "columnName": "encrypted_content", + "affinity": "BLOB" + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "archived", + "columnName": "archived", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "trashedAt", + "columnName": "trashed_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "encVersion", + "columnName": "enc_v", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "colorId", + "columnName": "color_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "expiresAt", + "columnName": "expires_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "readOnce", + "columnName": "read_once", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "idx_notes_folder_active", + "unique": false, + "columnNames": [ + "folder_id", + "archived", + "trashed_at", + "updated_at" + ], + "orders": [ + "ASC", + "ASC", + "ASC", + "DESC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `idx_notes_folder_active` ON `${TABLE_NAME}` (`folder_id` ASC, `archived` ASC, `trashed_at` ASC, `updated_at` DESC)" + }, + { + "name": "idx_notes_trashed", + "unique": false, + "columnNames": [ + "trashed_at" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `idx_notes_trashed` ON `${TABLE_NAME}` (`trashed_at`)" + }, + { + "name": "idx_notes_updated", + "unique": false, + "columnNames": [ + "updated_at" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `idx_notes_updated` ON `${TABLE_NAME}` (`updated_at`)" + } + ], + "foreignKeys": [ + { + "table": "folders", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "folder_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1a9615da6e5de5c045cf8c66cc3c3970')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/filestech/notes_tech/data/local/LegacyDatabaseOpeningTest.kt b/app/src/androidTest/java/com/filestech/notes_tech/data/local/LegacyDatabaseOpeningTest.kt index 8d58d4fe..ae3574b9 100644 --- a/app/src/androidTest/java/com/filestech/notes_tech/data/local/LegacyDatabaseOpeningTest.kt +++ b/app/src/androidTest/java/com/filestech/notes_tech/data/local/LegacyDatabaseOpeningTest.kt @@ -253,6 +253,8 @@ class LegacyDatabaseOpeningTest { updatedAt = 1L, encVersion = 1, colorId = null, + expiresAt = null, + readOnce = false, ), ) diff --git a/app/src/androidTest/java/com/filestech/notes_tech/data/local/MigrationVersDixTest.kt b/app/src/androidTest/java/com/filestech/notes_tech/data/local/MigrationVersDixTest.kt index ed4ec781..a45c160e 100644 --- a/app/src/androidTest/java/com/filestech/notes_tech/data/local/MigrationVersDixTest.kt +++ b/app/src/androidTest/java/com/filestech/notes_tech/data/local/MigrationVersDixTest.kt @@ -89,6 +89,25 @@ class MigrationVersDixTest { assertThat(empreinte).isNotEqualTo(EMPREINTE_DU_SCHEMA_9) } + /** + * 3.4.0 (D-043) — **the base of every 3.1 to 3.3 user**: schema 10, adopted by Room. It must reach 11 + * through `MIGRATION_10_11` alone, every note kept. + */ + @Test + fun a_base_of_3_3_0_at_10_is_migrated_to_11_and_keeps_every_note(): Unit = runBlocking { + poserLaBaseDe330() + val avant = lignesBrutes() + + val base = provider.get() + + verifierMigree(base, avant) + val empreinte = base.openHelper.readableDatabase.query("SELECT identity_hash FROM room_master_table").use { + it.moveToFirst() + it.getString(0) + } + assertThat(empreinte).isNotEqualTo(EMPREINTE_DU_SCHEMA_10) + } + private suspend fun verifierMigree(base: NotesDatabase, avant: Map>) { val db = base.openHelper.readableDatabase val version = db.query("PRAGMA user_version").use { @@ -99,7 +118,7 @@ class MigrationVersDixTest { val colonnes = db.query("PRAGMA table_info(notes)").use { curseur -> buildList { while (curseur.moveToNext()) add(curseur.getString(1)) } } - assertThat(colonnes).contains("color_id") + assertThat(colonnes).containsAtLeast("color_id", "expires_at", "read_once") // Every note is there, its content and its sealed blob as they were, and no colour yet. val apres = base.noteDao().listAllAlive() @@ -109,12 +128,21 @@ class MigrationVersDixTest { assertThat(ligne.content).isEqualTo(contenu) assertThat(ligne.encryptedContent?.toList()).isEqualTo(blob?.toList()) assertThat(ligne.colorId).isNull() + assertThat(ligne.expiresAt).isNull() + assertThat(ligne.readOnce).isFalse() } // And the column takes a colour — the FTS update trigger runs on that write, and must hold. val premiere = apres.first() assertThat(base.noteWriteDao().updateColor(premiere.id, colorId = 3, updatedAt = 1L)).isEqualTo(1) assertThat(base.noteDao().findById(premiere.id)?.colorId).isEqualTo(3) + + // And a lifetime (3.4.0): written, then the purge erases that note and that note only. + assertThat(base.noteWriteDao().updateLifetime(premiere.id, expiresAt = 5L, readOnce = false)).isEqualTo(1) + assertThat(base.noteWriteDao().deleteExpired(now = 4L)).isEqualTo(0) + assertThat(base.noteWriteDao().deleteExpired(now = 5L)).isEqualTo(1) + assertThat(base.noteDao().findById(premiere.id)).isNull() + assertThat(base.noteDao().listAllAlive().map { it.id }).containsExactlyElementsIn(avant.keys - premiere.id) } /** The notes as the Flutter base holds them, read with SQLCipher directly, before Room opens it. */ @@ -130,6 +158,15 @@ class MigrationVersDixTest { db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '$EMPREINTE_DU_SCHEMA_9')") } + /** A base as 3.1.0 to 3.3.0 leave it: schema 10 — the colour — and Room's identity of 10. */ + private fun poserLaBaseDe330() = ouvrirBrut().use { db -> + // The real migration object: SQLCipher's database is a SupportSQLiteDatabase. + NotesDatabase.MIGRATION_9_10.migrate(db) + db.execSQL("PRAGMA user_version = 10") + db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)") + db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '$EMPREINTE_DU_SCHEMA_10')") + } + private fun ouvrirBrut(): SQLiteDatabase = SQLiteDatabase.openDatabase( fichier.absolutePath, SqlCipherRawKey.encode(kek), @@ -141,5 +178,8 @@ class MigrationVersDixTest { private companion object { /** `identityHash` of `app/schemas/…/9.json`: the schema every 3.0.0 base was validated against. */ const val EMPREINTE_DU_SCHEMA_9 = "5ba6eb3f27afffe903eca6b0e5e9b38d" + + /** `identityHash` of `app/schemas/.../10.json`, the schema of 3.1.0 to 3.3.0. */ + const val EMPREINTE_DU_SCHEMA_10 = "e8320016c1e5a1f8f00635be6c9cad1f" } } diff --git a/app/src/androidTest/java/com/filestech/notes_tech/data/local/NoteLinkTest.kt b/app/src/androidTest/java/com/filestech/notes_tech/data/local/NoteLinkTest.kt index 5dfd9387..73e9260a 100644 --- a/app/src/androidTest/java/com/filestech/notes_tech/data/local/NoteLinkTest.kt +++ b/app/src/androidTest/java/com/filestech/notes_tech/data/local/NoteLinkTest.kt @@ -159,6 +159,8 @@ class NoteLinkTest { updatedAt = 1L, encVersion = 1, colorId = null, + expiresAt = null, + readOnce = false, ) private fun openDatabase(): NotesDatabase { diff --git a/app/src/androidTest/java/com/filestech/notes_tech/data/prefs/ReglagesHeritesTest.kt b/app/src/androidTest/java/com/filestech/notes_tech/data/prefs/ReglagesHeritesTest.kt index b6889db3..9a8cf4fa 100644 --- a/app/src/androidTest/java/com/filestech/notes_tech/data/prefs/ReglagesHeritesTest.kt +++ b/app/src/androidTest/java/com/filestech/notes_tech/data/prefs/ReglagesHeritesTest.kt @@ -228,6 +228,23 @@ class ReglagesHeritesTest { assertThat(fichier.contains("flutter.theme_mode_leurre")).isTrue() } + /** + * D-040 (3.4.0) : le clavier sans apprentissage est ACTIVÉ tant que rien n'est écrit — un fichier + * vidé (première ouverture, panique) le laisse actif — et s'écrit sous le préfixe du greffon. + */ + @Test + fun le_clavier_sans_apprentissage_est_actif_par_defaut_et_s_ecrit_dans_le_fichier_herite() { + assertThat(reglages.keyboardNoLearningNow()).isTrue() + + reglages.setKeyboardNoLearning(false) + + assertThat(fichier.getBoolean("flutter.keyboard_no_learning", true)).isFalse() + assertThat(reglages.keyboardNoLearningNow()).isFalse() + // Le contrôle : la clé nue, sans préfixe, n'est pas lue. + fichier.edit().remove("flutter.keyboard_no_learning").putBoolean("keyboard_no_learning", false).commit() + assertThat(reglages.keyboardNoLearningNow()).isTrue() + } + private companion object { /** Transcrit du greffon `shared_preferences`, pas lu depuis `LegacyPreferences`. */ const val FICHIER_DU_GREFFON = "FlutterSharedPreferences" diff --git a/app/src/androidTest/java/com/filestech/notes_tech/data/repository/NotesRepositoryTest.kt b/app/src/androidTest/java/com/filestech/notes_tech/data/repository/NotesRepositoryTest.kt index c23a978f..c9657a3f 100644 --- a/app/src/androidTest/java/com/filestech/notes_tech/data/repository/NotesRepositoryTest.kt +++ b/app/src/androidTest/java/com/filestech/notes_tech/data/repository/NotesRepositoryTest.kt @@ -418,6 +418,8 @@ class NotesRepositoryTest { updatedAt = horloge.millis(), encVersion = 1, colorId = null, + expiresAt = null, + readOnce = false, ), ) diff --git a/app/src/androidTest/java/com/filestech/notes_tech/data/scellee/FichierDeNoteScelleeTest.kt b/app/src/androidTest/java/com/filestech/notes_tech/data/scellee/FichierDeNoteScelleeTest.kt new file mode 100644 index 00000000..46633e84 --- /dev/null +++ b/app/src/androidTest/java/com/filestech/notes_tech/data/scellee/FichierDeNoteScelleeTest.kt @@ -0,0 +1,102 @@ +package com.filestech.notes_tech.data.scellee + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.filestech.notes_tech.data.scellee.FichierDeNoteScellee.NoteScelleeRefuseeException +import com.filestech.notes_tech.data.scellee.FichierDeNoteScellee.Raison +import com.filestech.notes_tech.security.sauvegarde.FichierDeSauvegarde +import com.filestech.notes_tech.security.sauvegarde.SecretDeSauvegarde +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertThrows +import org.junit.Test +import org.junit.runner.RunWith +import java.io.ByteArrayOutputStream + +/** + * D-044 (3.4.0) — **a sealed note opens with its code, and only with it**; anything else is refused, said + * by its reason. On the phone: the JSON is Android's, the derivation the vaults' Argon2id. + */ +@RunWith(AndroidJUnit4::class) +class FichierDeNoteScelleeTest { + + private val note = NoteScellee( + titre = "Code de l'immeuble", + contenu = "B 4521 — « ne pas oublier » 🔑\nseconde ligne", + couleur = 3, + etiquettes = listOf("maison", "accès"), + lectureUnique = true, + ) + private val code = SecretDeSauvegarde.nouveauCode() + + @Test + fun the_note_comes_back_whole_with_its_code_however_it_is_typed() { + val fichier = FichierDeNoteScellee.sceller(note, code) + + // Typed lowercase, without its dashes, with spaces: the backup's tolerant reading. + val tape = code.lowercase().replace("-", " ") + assertThat(FichierDeNoteScellee.ouvrir(fichier, tape)).isEqualTo(note) + assertThat(FichierDeNoteScellee.aLaMagique(fichier)).isTrue() + // Nothing of the note in clear in the file. + assertThat(String(fichier, Charsets.ISO_8859_1)).doesNotContain("4521") + } + + @Test + fun control_another_code_is_refused_as_a_wrong_code_or_damaged_file() { + val fichier = FichierDeNoteScellee.sceller(note.copy(couleur = null, etiquettes = emptyList()), code) + + assertThat(refus { FichierDeNoteScellee.ouvrir(fichier, SecretDeSauvegarde.nouveauCode()) }) + .isEqualTo(Raison.CODE_OU_FICHIER) + } + + @Test + fun a_damaged_file_is_refused_like_a_wrong_code() { + val fichier = FichierDeNoteScellee.sceller(note, code) + fichier[fichier.size - 20] = (fichier[fichier.size - 20].toInt() xor 1).toByte() + + assertThat(refus { FichierDeNoteScellee.ouvrir(fichier, code) }).isEqualTo(Raison.CODE_OU_FICHIER) + } + + /** Refused before any derivation: a text that is not a code. */ + @Test + fun what_is_not_a_code_is_refused_before_anything_is_computed() { + val fichier = FichierDeNoteScellee.sceller(note, code) + + assertThat(refus { FichierDeNoteScellee.ouvrir(fichier, "mot de passe") }).isEqualTo(Raison.CODE_MAL_FORME) + } + + @Test + fun a_backup_or_any_other_file_is_not_a_sealed_note() { + val sauvegarde = ByteArrayOutputStream().also { sortie -> + FichierDeSauvegarde.ecrire(sortie, ByteArray(32), FichierDeSauvegarde.nouvelEnTete()).close() + }.toByteArray() + val autre = "%PDF-1.7 un document quelconque".toByteArray() + + assertThat(FichierDeNoteScellee.aLaMagique(sauvegarde)).isFalse() + assertThat(FichierDeNoteScellee.aLaMagique(autre)).isFalse() + assertThat(refus { FichierDeNoteScellee.ouvrir(sauvegarde, code) }).isEqualTo(Raison.PAS_UNE_NOTE_SCELLEE) + assertThat(refus { FichierDeNoteScellee.ouvrir(autre, code) }).isEqualTo(Raison.PAS_UNE_NOTE_SCELLEE) + } + + /** A file made by hand: tags and title come in as the app would have written them (3-axes audit). */ + @Test + fun a_hand_made_file_gives_clean_tags_and_a_one_line_title() { + val fabriquee = note.copy( + titre = " Fac\u202Eture\nsuite\t", + etiquettes = listOf("a,b", " a ", "", "c\n", "b"), + ) + + val lue = FichierDeNoteScellee.ouvrir(FichierDeNoteScellee.sceller(fabriquee, code), code) + + assertThat(lue.titre).isEqualTo("Facture suite") + assertThat(lue.etiquettes).containsExactly("a", "b", "c").inOrder() + } + + @Test + fun a_file_past_the_bound_is_refused_unread() { + val enorme = FichierDeNoteScellee.MAGIQUE + ByteArray(FichierDeNoteScellee.TAILLE_MAX.toInt()) + + assertThat(refus { FichierDeNoteScellee.ouvrir(enorme, code) }).isEqualTo(Raison.TROP_GROSSE) + } + + private fun refus(ouverture: () -> Unit): Raison = + assertThrows(NoteScelleeRefuseeException::class.java) { ouverture() }.raison +} diff --git a/app/src/androidTest/java/com/filestech/notes_tech/ui/applock/AppLockSettingsScreenTest.kt b/app/src/androidTest/java/com/filestech/notes_tech/ui/applock/AppLockSettingsScreenTest.kt index c0be1766..34473a5e 100644 --- a/app/src/androidTest/java/com/filestech/notes_tech/ui/applock/AppLockSettingsScreenTest.kt +++ b/app/src/androidTest/java/com/filestech/notes_tech/ui/applock/AppLockSettingsScreenTest.kt @@ -201,6 +201,7 @@ class AppLockSettingsScreenTest { onLocale = {}, onSort = {}, onSecureWindow = { calls += "secure" }, + onKeyboardNoLearning = {}, onAutoLock = {}, onPanic = {}, onOpenAbout = {}, diff --git a/app/src/androidTest/java/com/filestech/notes_tech/ui/editor/ChampDuCorpsTest.kt b/app/src/androidTest/java/com/filestech/notes_tech/ui/editor/ChampDuCorpsTest.kt index d911bc5d..07aef692 100644 --- a/app/src/androidTest/java/com/filestech/notes_tech/ui/editor/ChampDuCorpsTest.kt +++ b/app/src/androidTest/java/com/filestech/notes_tech/ui/editor/ChampDuCorpsTest.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.core.view.inputmethod.EditorInfoCompat import androidx.test.ext.junit.runners.AndroidJUnit4 import com.filestech.notes_tech.domain.recherche.RechercheDansLeTexte.Plage +import com.filestech.notes_tech.ui.secure.LocalClavierSansApprentissage import com.filestech.notes_tech.ui.secure.LocalCopieDeSaisie import com.filestech.notes_tech.ui.secure.LocalSaisieProtegee import com.google.common.truth.Truth.assertThat @@ -53,6 +54,7 @@ class ChampDuCorpsTest { private var etat by mutableStateOf(TextFieldValue()) private var protegee by mutableStateOf(false) + private var sansApprentissage by mutableStateOf(false) private var surlignage by mutableStateOf(Surlignage.AUCUN) private val recues = mutableListOf() private val copiees = mutableListOf() @@ -70,6 +72,7 @@ class ChampDuCorpsTest { MaterialTheme { CompositionLocalProvider( LocalSaisieProtegee provides protegee, + LocalClavierSansApprentissage provides sansApprentissage, LocalCopieDeSaisie provides { texte -> copiees += texte attente?.await() @@ -268,6 +271,22 @@ class ChampDuCorpsTest { assertThat(options() and drapeau).isEqualTo(drapeau) } + /** D-040 (3.4.0): the setting asks it for every note, a note that is not in a vault included. */ + @Test + fun the_setting_asks_the_keyboard_not_to_learn_for_an_ordinary_note() { + poser("ordinaire") + val drapeau = EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING + fun options() = surLeChamp { EditorInfo().also { onCreateInputConnection(it) }.imeOptions } + + assertThat(options() and drapeau).isEqualTo(0) + + regle.runOnIdle { sansApprentissage = true } + regle.waitForIdle() + + assertThat(options() and drapeau).isEqualTo(drapeau) + assertThat(surLeChamp { protegee }).isFalse() + } + /** * 🔴 The keyboard reads a window around the cursor, never the note: the Samsung Keyboard asks for * the whole extracted text after every keystroke, and a 450 000-character note failed the Binder diff --git a/app/src/androidTest/java/com/filestech/notes_tech/ui/editor/EditeurTest.kt b/app/src/androidTest/java/com/filestech/notes_tech/ui/editor/EditeurTest.kt index f31aaa61..2db3f654 100644 --- a/app/src/androidTest/java/com/filestech/notes_tech/ui/editor/EditeurTest.kt +++ b/app/src/androidTest/java/com/filestech/notes_tech/ui/editor/EditeurTest.kt @@ -181,7 +181,9 @@ class EditeurTest { onInfos = { infos += Unit }, onDeplacer = { deplacements += Unit }, onArchiver = { archivages += it }, - onCouleur = { couleurs += Unit }, + feuilles = FeuillesDeLaFiche(couleur = { + couleurs += Unit + }, dureeDeVie = {}, partageScelle = {}), onExporter = { exports += Unit }, onCopier = { copies += Unit }, onCorbeille = { corbeilles += Unit }, diff --git a/app/src/androidTest/java/com/filestech/notes_tech/ui/editor/FermetureDuCoffreTest.kt b/app/src/androidTest/java/com/filestech/notes_tech/ui/editor/FermetureDuCoffreTest.kt index ea94960c..7e999aa5 100644 --- a/app/src/androidTest/java/com/filestech/notes_tech/ui/editor/FermetureDuCoffreTest.kt +++ b/app/src/androidTest/java/com/filestech/notes_tech/ui/editor/FermetureDuCoffreTest.kt @@ -11,6 +11,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.filestech.notes_tech.R import com.filestech.notes_tech.data.export.NoteExporter import com.filestech.notes_tech.data.prefs.AppSettings +import com.filestech.notes_tech.data.repository.DureesDeVie import com.filestech.notes_tech.data.repository.FoldersRepository import com.filestech.notes_tech.data.repository.LinksRepository import com.filestech.notes_tech.data.repository.NotesRepository @@ -56,6 +57,9 @@ class FermetureDuCoffreTest { @Inject lateinit var notes: NotesRepository + @Inject + lateinit var durees: DureesDeVie + @Inject lateinit var links: LinksRepository @@ -277,6 +281,7 @@ class FermetureDuCoffreTest { links = links, exporter = exporter, folders = folders, + durees = durees, vaults = vaults, settings = settings, clipboard = clipboard, diff --git a/app/src/androidTest/java/com/filestech/notes_tech/ui/editor/LectureUniqueTest.kt b/app/src/androidTest/java/com/filestech/notes_tech/ui/editor/LectureUniqueTest.kt new file mode 100644 index 00000000..7e7c7783 --- /dev/null +++ b/app/src/androidTest/java/com/filestech/notes_tech/ui/editor/LectureUniqueTest.kt @@ -0,0 +1,171 @@ +package com.filestech.notes_tech.ui.editor + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStore +import androidx.lifecycle.viewmodel.CreationExtras +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.filestech.notes_tech.data.export.NoteExporter +import com.filestech.notes_tech.data.prefs.AppSettings +import com.filestech.notes_tech.data.repository.DureesDeVie +import com.filestech.notes_tech.data.repository.FoldersRepository +import com.filestech.notes_tech.data.repository.LinksRepository +import com.filestech.notes_tech.data.repository.NotesRepository +import com.filestech.notes_tech.domain.model.DureeDeVie +import com.filestech.notes_tech.domain.model.Folder +import com.filestech.notes_tech.domain.model.Note +import com.filestech.notes_tech.security.clipboard.SensitiveClipboard +import com.filestech.notes_tech.security.vault.FolderVaultService +import com.filestech.notes_tech.ui.navigation.Destination +import com.google.common.truth.Truth.assertThat +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +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 + +/** + * D-043 (3.4.0) — **"after reading": the note goes when its editor closes**, on the real editor and the + * real database. The editor's ViewModel is cleared as the back stack clears it when the screen is left; + * a rotation, which keeps the ViewModel, erases nothing — that is why the erasing sits in `onCleared`. + * Each case has its control. + */ +@HiltAndroidTest +@RunWith(AndroidJUnit4::class) +class LectureUniqueTest { + + @get:Rule + val hilt = HiltAndroidRule(this) + + @Inject + lateinit var notes: NotesRepository + + @Inject + lateinit var durees: DureesDeVie + + @Inject + lateinit var links: LinksRepository + + @Inject + lateinit var exporter: NoteExporter + + @Inject + lateinit var folders: FoldersRepository + + @Inject + lateinit var vaults: FolderVaultService + + @Inject + lateinit var settings: AppSettings + + @Inject + lateinit var clipboard: SensitiveClipboard + + private val suffixe = UUID.randomUUID().toString().take(8) + private val portee = CoroutineScope(SupervisorJob() + Dispatchers.Default) + private val magasin = ViewModelStore() + private val creees = mutableListOf() + + @Before + fun setUp() { + hilt.inject() + } + + @After + fun tearDown(): Unit = runBlocking { + withContext(Dispatchers.Main) { magasin.clear() } + portee.cancel() + creees.forEach { notes.deletePermanently(it) } + } + + @Test + fun a_note_read_once_is_erased_when_its_editor_closes(): Unit = runBlocking { + val note = nouvelleNote("à lire une fois") + durees.poser(note.id, DureeDeVie.APRES_LECTURE) + + ouvrirPuisFermer(note) { editeur -> + assertThat(editeur.effaceeALaFermeture).isTrue() + } + + withTimeout(ATTENTE_MS) { + while (notes.find(note.id) != null) delay(PAS_MS) + } + } + + /** The choice made in this session counts from the NEXT opening: this closing erases nothing. */ + @Test + fun control_a_lifetime_chosen_while_the_note_is_open_does_not_erase_it_at_this_closing(): Unit = runBlocking { + val note = nouvelleNote("choisie maintenant") + + ouvrirPuisFermer(note) { editeur -> + editeur.setLifetime(DureeDeVie.APRES_LECTURE) + withTimeout(ATTENTE_MS) { editeur.state.first { it.note?.readOnce == true } } + assertThat(editeur.effaceeALaFermeture).isFalse() + } + + delay(ATTENTE_DE_L_EFFACEMENT_MS) + assertThat(notes.find(note.id)?.readOnce).isTrue() + } + + @Test + fun control_a_note_without_a_lifetime_stays(): Unit = runBlocking { + val note = nouvelleNote("ordinaire") + + ouvrirPuisFermer(note) { editeur -> assertThat(editeur.effaceeALaFermeture).isFalse() } + + delay(ATTENTE_DE_L_EFFACEMENT_MS) + assertThat(notes.find(note.id)).isNotNull() + } + + private suspend fun nouvelleNote(texte: String): Note = + notes.create(folderId = Folder.INBOX_ID, title = "Lecture unique $suffixe", content = "$texte $suffixe") + .also { creees += it.id } + + private suspend fun ouvrirPuisFermer(note: Note, pendant: suspend (NoteEditorViewModel) -> Unit) { + val editeur = withContext(Dispatchers.Main) { editeur(note) } + withTimeout(ATTENTE_MS) { editeur.state.first { !it.loading } } + pendant(editeur) + withContext(Dispatchers.Main) { magasin.clear() } + } + + @Suppress("UNCHECKED_CAST") + private fun editeur(note: Note): NoteEditorViewModel { + val fabrique = object : ViewModelProvider.Factory { + override fun create(modelClass: Class, extras: CreationExtras): T = NoteEditorViewModel( + notes = notes, + links = links, + exporter = exporter, + folders = folders, + durees = durees, + vaults = vaults, + settings = settings, + clipboard = clipboard, + applicationScope = portee, + savedState = SavedStateHandle(mapOf(Destination.ARG_NOTE_ID to note.id)), + ) as T + } + return ViewModelProvider.create(magasin, fabrique)[NoteEditorViewModel::class] + } + + private companion object { + const val ATTENTE_MS = 10_000L + const val PAS_MS = 100L + + /** Longer than an erasing takes on the S9 (a DELETE), to see that none came. */ + const val ATTENTE_DE_L_EFFACEMENT_MS = 1_500L + } +} diff --git a/app/src/androidTest/java/com/filestech/notes_tech/ui/editor/LiensDansUnCoffreTest.kt b/app/src/androidTest/java/com/filestech/notes_tech/ui/editor/LiensDansUnCoffreTest.kt index 100e24c5..71c55015 100644 --- a/app/src/androidTest/java/com/filestech/notes_tech/ui/editor/LiensDansUnCoffreTest.kt +++ b/app/src/androidTest/java/com/filestech/notes_tech/ui/editor/LiensDansUnCoffreTest.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.viewmodel.CreationExtras import androidx.test.ext.junit.runners.AndroidJUnit4 import com.filestech.notes_tech.data.export.NoteExporter import com.filestech.notes_tech.data.prefs.AppSettings +import com.filestech.notes_tech.data.repository.DureesDeVie import com.filestech.notes_tech.data.repository.FoldersRepository import com.filestech.notes_tech.data.repository.LinksRepository import com.filestech.notes_tech.data.repository.NotesRepository @@ -55,6 +56,9 @@ class LiensDansUnCoffreTest { @Inject lateinit var notes: NotesRepository + @Inject + lateinit var durees: DureesDeVie + @Inject lateinit var links: LinksRepository @@ -185,6 +189,7 @@ class LiensDansUnCoffreTest { links = links, exporter = exporter, folders = folders, + durees = durees, vaults = vaults, settings = settings, clipboard = clipboard, diff --git a/app/src/androidTest/java/com/filestech/notes_tech/ui/editor/RechercheDansLaNoteEcranTest.kt b/app/src/androidTest/java/com/filestech/notes_tech/ui/editor/RechercheDansLaNoteEcranTest.kt index b172b712..05fa2f15 100644 --- a/app/src/androidTest/java/com/filestech/notes_tech/ui/editor/RechercheDansLaNoteEcranTest.kt +++ b/app/src/androidTest/java/com/filestech/notes_tech/ui/editor/RechercheDansLaNoteEcranTest.kt @@ -87,7 +87,7 @@ class RechercheDansLaNoteEcranTest { onInfos = {}, onDeplacer = {}, onArchiver = {}, - onCouleur = {}, + feuilles = FeuillesDeLaFiche(couleur = {}, dureeDeVie = {}, partageScelle = {}), onExporter = {}, onCopier = {}, onCorbeille = {}, diff --git a/app/src/androidTest/java/com/filestech/notes_tech/ui/home/AccueilTest.kt b/app/src/androidTest/java/com/filestech/notes_tech/ui/home/AccueilTest.kt index 72fbe472..3a06fc1d 100644 --- a/app/src/androidTest/java/com/filestech/notes_tech/ui/home/AccueilTest.kt +++ b/app/src/androidTest/java/com/filestech/notes_tech/ui/home/AccueilTest.kt @@ -126,6 +126,7 @@ class AccueilTest { onOpenSearch = { recherchesOuvertes += Unit }, onOpenSettings = { reglagesOuverts += Unit }, onOpenAbout = {}, + onOpenSealedNote = {}, onDismissVaultLostBanner = { banniereFermee += Unit }, ) } diff --git a/app/src/androidTest/java/com/filestech/notes_tech/ui/home/GestesSurDesNotesTest.kt b/app/src/androidTest/java/com/filestech/notes_tech/ui/home/GestesSurDesNotesTest.kt index 6118d3bc..8e44641f 100644 --- a/app/src/androidTest/java/com/filestech/notes_tech/ui/home/GestesSurDesNotesTest.kt +++ b/app/src/androidTest/java/com/filestech/notes_tech/ui/home/GestesSurDesNotesTest.kt @@ -13,6 +13,7 @@ import com.filestech.notes_tech.data.local.NotesDatabaseFactory import com.filestech.notes_tech.data.local.SqlCipherRawKey import com.filestech.notes_tech.data.prefs.AppSettings import com.filestech.notes_tech.data.prefs.LegacyPreferences +import com.filestech.notes_tech.data.repository.DureesDeVie import com.filestech.notes_tech.data.repository.FoldersRepository import com.filestech.notes_tech.data.repository.NotesEnLot import com.filestech.notes_tech.data.repository.NotesRepository @@ -113,6 +114,7 @@ class GestesSurDesNotesTest { HomeViewModel( notes = notes, enLot = enLot, + durees = DureesDeVie(provider, horloge), folders = dossiers, search = SearchRepository(provider), settings = AppSettings(LegacyPreferences(preferences)), diff --git a/app/src/androidTest/java/com/filestech/notes_tech/ui/scellee/NotesScelleesRecuesTest.kt b/app/src/androidTest/java/com/filestech/notes_tech/ui/scellee/NotesScelleesRecuesTest.kt new file mode 100644 index 00000000..8a490c04 --- /dev/null +++ b/app/src/androidTest/java/com/filestech/notes_tech/ui/scellee/NotesScelleesRecuesTest.kt @@ -0,0 +1,169 @@ +package com.filestech.notes_tech.ui.scellee + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStore +import androidx.lifecycle.viewmodel.CreationExtras +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.filestech.notes_tech.data.repository.NotesRepository +import com.filestech.notes_tech.data.scellee.FichierDeNoteScellee +import com.filestech.notes_tech.data.scellee.FichierDeNoteScellee.Raison +import com.filestech.notes_tech.data.scellee.NoteScellee +import com.filestech.notes_tech.domain.model.Folder +import com.filestech.notes_tech.domain.model.NoteColor +import com.filestech.notes_tech.security.sauvegarde.SecretDeSauvegarde +import com.filestech.notes_tech.ui.scellee.NotesScelleesViewModel.Etat +import com.google.common.truth.Truth.assertThat +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +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 + +/** + * D-044 (3.4.0) — **a sealed note received becomes ONE note of the Inbox, with what its sender sealed**, + * on the real ViewModel and the real database: a wrong code first is refused and lets the right one + * through, the colour, the tags and "after reading" are kept, opening again adds nothing; and a second + * file deposited while the first is being opened waits for it instead of spoiling it (3-axes audit). + */ +@HiltAndroidTest +@RunWith(AndroidJUnit4::class) +class NotesScelleesRecuesTest { + + @get:Rule + val hilt = HiltAndroidRule(this) + + @Inject + lateinit var enAttente: NotesScelleesEnAttente + + @Inject + lateinit var notes: NotesRepository + + private val suffixe = UUID.randomUUID().toString().take(8) + private val magasin = ViewModelStore() + private val creees = mutableListOf() + + @Before + fun setUp() { + hilt.inject() + enAttente.abandonner() + } + + @After + fun tearDown(): Unit = runBlocking { + withContext(Dispatchers.Main) { magasin.clear() } + enAttente.abandonner() + creees.forEach { notes.deletePermanently(it) } + } + + @Test + fun the_right_code_after_a_wrong_one_gives_one_note_with_what_was_sealed(): Unit = runBlocking { + val code = SecretDeSauvegarde.nouveauCode() + val scellee = scellee("Code de l'immeuble", lectureUnique = true) + val recues = viewModel() + + deposerPuisPrendre(recues, FichierDeNoteScellee.sceller(scellee, code)) + withContext(Dispatchers.Main) { recues.ouvrir(SecretDeSauvegarde.nouveauCode()) } + assertThat(attendreUneFin(recues)).isEqualTo(Etat.Code(erreur = Raison.CODE_OU_FICHIER)) + + withContext(Dispatchers.Main) { recues.ouvrir(code.lowercase()) } + val id = withTimeout(ATTENTE_MS) { recues.aOuvrir.first() }.also(creees::add) + + val note = requireNotNull(notes.find(id)) + assertThat(note.folderId).isEqualTo(Folder.INBOX_ID) + assertThat(note.title).isEqualTo(scellee.titre) + assertThat(note.content).isEqualTo(scellee.contenu) + assertThat(note.tags).containsExactly("maison", "accès").inOrder() + assertThat(note.color).isEqualTo(NoteColor.fromId(scellee.couleur)) + assertThat(note.readOnce).isTrue() + assertThat(note.encrypted).isNull() + assertThat(recues.etat.value).isEqualTo(Etat.Aucune) + } + + /** Control: the bytes are let go once the note is in — opening again adds nothing. */ + @Test + fun control_opening_again_adds_no_second_note(): Unit = runBlocking { + val code = SecretDeSauvegarde.nouveauCode() + val scellee = scellee("Une seule fois", lectureUnique = false) + val recues = viewModel() + + deposerPuisPrendre(recues, FichierDeNoteScellee.sceller(scellee, code)) + withContext(Dispatchers.Main) { recues.ouvrir(code) } + creees += withTimeout(ATTENTE_MS) { recues.aOuvrir.first() } + withContext(Dispatchers.Main) { recues.ouvrir(code) } + + assertThat(recues.etat.value).isEqualTo(Etat.Aucune) + assertThat(notes.listAllAlive().count { it.title == scellee.titre }).isEqualTo(1) + } + + /** 🔴 3-axes audit of 3.4.0, M4: the second file is not taken under the first one's decryption. */ + @Test + fun a_second_file_deposited_while_the_first_opens_waits_for_it(): Unit = runBlocking { + val premierCode = SecretDeSauvegarde.nouveauCode() + val secondCode = SecretDeSauvegarde.nouveauCode() + val premiere = scellee("Premiere", lectureUnique = false) + val seconde = scellee("Seconde", lectureUnique = false) + val secondFichier = FichierDeNoteScellee.sceller(seconde, secondCode) + val recues = viewModel() + + deposerPuisPrendre(recues, FichierDeNoteScellee.sceller(premiere, premierCode)) + // One block on the main thread: the first opening cannot end in between, whatever the phone's speed. + withContext(Dispatchers.Main) { + recues.ouvrir(premierCode) + assertThat(recues.etat.value).isEqualTo(Etat.Code(enCours = true)) + enAttente.deposer(secondFichier) + recues.prendre() + assertThat(recues.etat.value).isEqualTo(Etat.Code(enCours = true)) + } + creees += withTimeout(ATTENTE_MS) { recues.aOuvrir.first() } + + // The first is in, the second taken after it: its own code opens it. + assertThat(withTimeout(ATTENTE_MS) { recues.etat.first { it == Etat.Code() } }).isEqualTo(Etat.Code()) + withContext(Dispatchers.Main) { recues.ouvrir(secondCode) } + creees += withTimeout(ATTENTE_MS) { recues.aOuvrir.first() } + + val titres = creees.mapNotNull { notes.find(it)?.title } + assertThat(titres).containsExactly(premiere.titre, seconde.titre).inOrder() + } + + private fun scellee(titre: String, lectureUnique: Boolean) = NoteScellee( + titre = "$titre $suffixe", + contenu = "B 4521 — « ne pas oublier » 🔑\nseconde ligne", + couleur = NoteColor.entries.last().id, + etiquettes = listOf("maison", "accès"), + lectureUnique = lectureUnique, + ) + + private suspend fun deposerPuisPrendre(recues: NotesScelleesViewModel, fichier: ByteArray) = + withContext(Dispatchers.Main) { + enAttente.deposer(fichier) + recues.prendre() + assertThat(recues.etat.value).isEqualTo(Etat.Code()) + } + + private suspend fun attendreUneFin(recues: NotesScelleesViewModel): Etat = + withTimeout(ATTENTE_MS) { recues.etat.first { it is Etat.Code && !it.enCours } } + + @Suppress("UNCHECKED_CAST") + private suspend fun viewModel(): NotesScelleesViewModel = withContext(Dispatchers.Main) { + val fabrique = object : ViewModelProvider.Factory { + override fun create(modelClass: Class, extras: CreationExtras): T = + NotesScelleesViewModel(enAttente, notes) as T + } + ViewModelProvider.create(magasin, fabrique)[NotesScelleesViewModel::class] + } + + private companion object { + /** Two Argon2id derivations (64 MiB, t=3) and an INSERT, on the S9, with margin. */ + const val ATTENTE_MS = 20_000L + } +} diff --git a/app/src/androidTest/java/com/filestech/notes_tech/ui/secure/ClavierSansApprentissageTest.kt b/app/src/androidTest/java/com/filestech/notes_tech/ui/secure/ClavierSansApprentissageTest.kt new file mode 100644 index 00000000..957143c5 --- /dev/null +++ b/app/src/androidTest/java/com/filestech/notes_tech/ui/secure/ClavierSansApprentissageTest.kt @@ -0,0 +1,96 @@ +package com.filestech.notes_tech.ui.secure + +import android.view.inputmethod.EditorInfo +import androidx.activity.ComponentActivity +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.platform.InterceptPlatformTextInput +import androidx.compose.ui.test.hasSetTextAction +import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule +import androidx.compose.ui.test.performClick +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue +import androidx.core.view.inputmethod.EditorInfoCompat +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.filestech.notes_tech.ui.vault.ChampDePhraseSecrete +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.awaitCancellation +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * D-040 (3.4.0) — **the keyboard is asked not to learn**, read where the keyboard reads it: an outer + * interceptor receives the request that opens the keyboard and fills an `EditorInfo` from it, as in + * [SaisieDeCoffreTest]. Each case has its control. + */ +@RunWith(AndroidJUnit4::class) +class ClavierSansApprentissageTest { + + @get:Rule + val regle = createAndroidComposeRule() + + private val drapeau = EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING + + @Test + fun the_setting_on_asks_every_field_not_to_learn() { + assertThat(optionsDuClavier(actif = true) { ChampOrdinaire() } and drapeau).isEqualTo(drapeau) + } + + @Test + fun the_control_the_setting_off_asks_nothing() { + assertThat(optionsDuClavier(actif = false) { ChampOrdinaire() } and drapeau).isEqualTo(0) + } + + /** A secret's field asks it whatever the setting says. */ + @Test + fun a_secret_field_asks_it_even_with_the_setting_off() { + val options = optionsDuClavier(actif = false) { + var phrase by remember { mutableStateOf("") } + ChampDePhraseSecrete( + valeur = phrase, + onValeurChange = { phrase = it }, + label = "phrase", + actionClavier = ImeAction.Done, + actif = true, + ) + } + + assertThat(options and drapeau).isEqualTo(drapeau) + } + + @Composable + private fun ChampOrdinaire() { + var valeur by remember { mutableStateOf(TextFieldValue("")) } + TextField(value = valeur, onValueChange = { valeur = it }) + } + + @OptIn(ExperimentalComposeUiApi::class) + private fun optionsDuClavier(actif: Boolean, champ: @Composable () -> Unit): Int { + var options: Int? = null + regle.setContent { + InterceptPlatformTextInput( + interceptor = { requete, _ -> + val attributs = EditorInfo() + requete.createInputConnection(attributs) + options = attributs.imeOptions + awaitCancellation() + }, + ) { + ClavierSansApprentissage(actif = actif, contenu = champ) + } + } + regle.onNode(hasSetTextAction()).performClick() + regle.waitUntil(ATTENTE_MS) { options != null } + return options!! + } + + private companion object { + const val ATTENTE_MS = 5_000L + } +} diff --git a/app/src/androidTest/java/com/filestech/notes_tech/ui/settings/ReglagesTest.kt b/app/src/androidTest/java/com/filestech/notes_tech/ui/settings/ReglagesTest.kt index c88729bb..553fa37f 100644 --- a/app/src/androidTest/java/com/filestech/notes_tech/ui/settings/ReglagesTest.kt +++ b/app/src/androidTest/java/com/filestech/notes_tech/ui/settings/ReglagesTest.kt @@ -75,6 +75,7 @@ class ReglagesTest { private val langues = mutableListOf() private val tris = mutableListOf() private val fenetresProtegees = mutableListOf() + private val clavierSansApprentissage = mutableListOf() private val delais = mutableListOf() private val paniques = mutableListOf() private val aPropos = mutableListOf() @@ -120,6 +121,7 @@ class ReglagesTest { onLocale = { langues += it }, onSort = { tris += it }, onSecureWindow = { fenetresProtegees += it }, + onKeyboardNoLearning = { clavierSansApprentissage += it }, onAutoLock = { delais += it }, onPanic = { paniques += Unit }, onOpenAbout = { aPropos += Unit }, @@ -267,22 +269,24 @@ class ReglagesTest { .getQuantityString(R.plurals.settings_vault_auto_lock_minutes, 15, 15) poser(SettingsUiState(vaultAutoLockMinutes = 0)) - regle.onNodeWithText(texte(R.string.settings_vault_auto_lock_never)).assertIsDisplayed() + regle.onNodeWithText(texte(R.string.settings_vault_auto_lock_never)).performScrollTo().assertIsDisplayed() poser(SettingsUiState(vaultAutoLockMinutes = 15)) - regle.onNodeWithText(quinze).assertIsDisplayed() + regle.onNodeWithText(quinze).performScrollTo().assertIsDisplayed() regle.onNodeWithText(texte(R.string.settings_vault_auto_lock_never)).assertDoesNotExist() } /** - * 🔴🔴 **Un SEUL nœud basculable, qui porte le libellé ET l'état** — c'est la propriété de §77. + * 🔴🔴 **Un SEUL nœud basculable par réglage, qui porte le libellé ET l'état** — c'est la propriété + * de §77. * - * Avant correctif, l'écran en portait deux : le `ListItem` avec le texte, et un `Switch` **sans - * nom** en `trailingContent`. Ce test l'interdit de trois façons à la fois, et chacune tomberait - * séparément si le correctif était défait : + * Avant correctif, l'écran en portait deux pour la fenêtre protégée : le `ListItem` avec le texte, et + * un `Switch` **sans nom** en `trailingContent`. Ce test l'interdit de trois façons à la fois, et + * chacune tomberait séparément si le correctif était défait : * - * 1. `assertCountEquals(1)` — deux cibles pour un seul réglage, dont une sous le minimum - * accessible, c'est exactement ce que l'idiome `onCheckedChange = null` évite ; + * 1. `assertCountEquals(2)` — deux réglages à bascule depuis la 3.4.0 (fenêtre protégée, clavier + * sans apprentissage), donc deux nœuds : un de plus serait un `Switch` sans nom, une seconde + * cible pour un seul réglage, ce que l'idiome `onCheckedChange = null` évite ; * 2. le nœud basculable **porte le libellé** de la ligne : sans ça il s'annonce « interrupteur, * activé » sans dire de quoi, ce qui était le défaut mesuré ; * 3. son état suit `state.secureWindow`, et le clic remonte **l'inverse**. @@ -295,8 +299,8 @@ class ReglagesTest { fun un_seul_noeud_basculable_porte_le_libelle_de_la_fenetre_protegee_et_son_etat() { poser(SettingsUiState(secureWindow = true)) - regle.onAllNodes(isToggleable()).assertCountEquals(1) - val interrupteur = regle.onNode(isToggleable()) + regle.onAllNodes(isToggleable()).assertCountEquals(2) + val interrupteur = regle.onNode(isToggleable() and hasText(texte(R.string.settings_secure_window))) interrupteur.assertIsOn() assertThat( interrupteur.fetchSemanticsNode().config.getOrNull(SemanticsProperties.Text) @@ -308,7 +312,22 @@ class ReglagesTest { assertThat(fenetresProtegees).containsExactly(false) poser(SettingsUiState(secureWindow = false)) - regle.onNode(isToggleable()).assertIsOff() + regle.onNode(isToggleable() and hasText(texte(R.string.settings_secure_window))).assertIsOff() + } + + /** D-040 (3.4.0) : la ligne du clavier, comme celle de la fenêtre protégée — un nœud, son libellé, son état. */ + @Test + fun le_clavier_sans_apprentissage_est_un_seul_noeud_qui_suit_son_etat() { + poser(SettingsUiState(keyboardNoLearning = true)) + + val interrupteur = regle.onNode(isToggleable() and hasText(texte(R.string.settings_keyboard_no_learning))) + interrupteur.performScrollTo().assertIsOn() + interrupteur.performClick() + assertThat(clavierSansApprentissage).containsExactly(false) + assertThat(fenetresProtegees).isEmpty() + + poser(SettingsUiState(keyboardNoLearning = false)) + regle.onNode(isToggleable() and hasText(texte(R.string.settings_keyboard_no_learning))).assertIsOff() } /** diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fda4bbb1..719859ec 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -73,8 +73,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_raccourci_dictee.xml b/app/src/main/res/drawable/ic_raccourci_dictee.xml new file mode 100644 index 00000000..cb5040cf --- /dev/null +++ b/app/src/main/res/drawable/ic_raccourci_dictee.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_raccourci_note.xml b/app/src/main/res/drawable/ic_raccourci_note.xml new file mode 100644 index 00000000..654f78a7 --- /dev/null +++ b/app/src/main/res/drawable/ic_raccourci_note.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_tuile_depot.xml b/app/src/main/res/drawable/ic_tuile_depot.xml new file mode 100644 index 00000000..db1a33a7 --- /dev/null +++ b/app/src/main/res/drawable/ic_tuile_depot.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/raw-de/privacy.md b/app/src/main/res/raw-de/privacy.md index 2c70c8e5..e031963f 100644 --- a/app/src/main/res/raw-de/privacy.md +++ b/app/src/main/res/raw-de/privacy.md @@ -1,6 +1,6 @@ # Datenschutzerklärung — Notes Tech -_Zuletzt aktualisiert: 30. Oktober 2026_ +_Zuletzt aktualisiert: 9. November 2026_ ## In einem Satz @@ -17,6 +17,10 @@ Notes Tech erhebt, überträgt und speichert keinerlei Daten auf entfernten Serv - **Rückverweise `[[Titel]]`**: lokaler invertierter Index, nie übertragen. - **Verschlüsselte Sicherung (optional)**: eine `.ntbackup`-Datei, die Sie selbst erstellen, an dem Ort, den Sie über die Android-Dokumentauswahl wählen. Sie enthält Ihre Ordner und Notizen, Papierkorb inbegriffen, verschlüsselt mit **AES-256-GCM** unter einem Schlüssel, der mit **Argon2id** (m=64MB, t=3) aus einem von der App gezogenen Code, den Sie notieren, oder aus einer eigenen Passphrase abgeleitet wird — Notes Tech bewahrt keines von beiden auf. Sie wird zuerst, bereits verschlüsselt, im privaten Speicher der App geschrieben, dann an den gewählten Ort kopiert und aus dem Speicher der App gelöscht. Die Notizen eines Passphrase-Tresors bleiben in der Datei durch seine Passphrase versiegelt. Ein PIN-Tresor wird nur offen mitgenommen, mit seinem Schlüssel: In der Datei schützt ihn allein das Geheimnis der Sicherung; ein geschlossener wird ausgelassen. Die Wiederherstellung fügt hinzu, was fehlt, und überschreibt nichts. - **Importierte und geteilte Notizen**: Markdown- oder Textdateien – oder ein ZIP davon –, die Sie in der Dokumentauswahl von Android wählen, und Texte oder Dateien, die Sie aus einer anderen App an Notes Tech teilen, werden zu gewöhnlichen Notizen in Ihren Ordnern, nie in einem Tresor. Sie werden einmal gelesen, auf dem Telefon; die Originale bleiben unverändert, und neben den erstellten Notizen wird nichts aufbewahrt. Höchstens 500.000 Zeichen pro Notiz. +- **Versiegelte Notizen (optional)**: Eine Notiz, die Sie versiegelt teilen, wird in eine `.ntnote`-Datei verschlüsselt — **AES-256-GCM** unter einem Schlüssel, der per **Argon2id** (m=64MB, t=3) aus einem Code abgeleitet wird, den die App erzeugt, einmal anzeigt und nirgends speichert. Die Datei wird im privaten Speicher der App geschrieben und dann der App übergeben, die Sie zum Senden wählen; den Code übermitteln Sie auf anderem Weg. Ihr Name verrät nichts über die Notiz. Eine empfangene versiegelte Notiz wird einmal aus der Datei gelesen, die Sie mit Notes Tech öffnen, und wird erst nach Eingabe ihres Codes zu einer Notiz — im Eingang, nie in einem Tresor. +- **Schnellablage**: Über eine Kachel der Schnelleinstellungen oder eine Verknüpfung des App-Symbols schreibt sie eine Notiz in den Eingang, ohne die App zu entsperren; sie liest und zeigt nichts von dem, was die App enthält. +- **Notizen mit Lebensdauer (optional)**: endgültig gelöscht, sobald ihre Zeit abgelaufen ist — beim nächsten Öffnen der App oder bei der Rückkehr zur Liste —, oder beim Schließen nach dem Lesen, wenn Sie „Nach dem Lesen“ gewählt haben. Sie gehen weder in die verschlüsselte Sicherung noch in den Export ein. +- **Tastatur**: Die App bittet die Tastatur, nichts von dem zu lernen, was Sie tippen (Einstellung, standardmäßig aktiviert). Es ist eine Bitte, die eine Tastatur ignorieren kann; was eine Tastatur selbst speichert, entzieht sich jeder App. - **Diktiermodell (Whisper `.bin`)**: Sie beschaffen es selbst — die App zeigt den Dateinamen und seine Quelle an — und importieren es dann über die Android-Dokumentauswahl. Notes Tech hat keine Internetberechtigung und **bietet keine Möglichkeit, etwas herunterzuladen**. Sein SHA-256 wird beim Import und vor jedem Laden geprüft. - **Beim Diktat aufgenommenes Audio**: in eine temporäre Datei im privaten Speicher der App geschrieben — die Transkriptions-Engine liest eine Datei, anders geht es nicht — und **gelöscht, sobald die Transkription vorliegt**. Es wird außerdem beim Start der App und durch den Panikmodus gelöscht, sodass ein abruptes Beenden nichts zurücklässt. - **Einstellungen (Design, Sortierung, aktiviertes Diktat, automatische Tresorsperre, Optionen der App-Sperre)**: unverschlüsselt in den lokalen Einstellungen gespeichert (keine sensiblen Daten). diff --git a/app/src/main/res/raw-es/privacy.md b/app/src/main/res/raw-es/privacy.md index f49dcfad..27a1f8e1 100644 --- a/app/src/main/res/raw-es/privacy.md +++ b/app/src/main/res/raw-es/privacy.md @@ -1,6 +1,6 @@ # Política de privacidad — Notes Tech -_Última actualización: 30 de octubre de 2026_ +_Última actualización: 9 de noviembre de 2026_ ## En una frase @@ -17,6 +17,10 @@ Notes Tech no recopila, no transmite ni almacena ningún dato en servidores remo - **Retroenlaces `[[Título]]`**: índice invertido local, nunca transmitido. - **Copia de seguridad cifrada (opcional)**: un archivo `.ntbackup` que usted mismo crea, en el lugar que elige mediante el selector de documentos de Android. Contiene sus carpetas y sus notas, papelera incluida, cifradas con **AES-256-GCM** con una clave derivada mediante **Argon2id** (m=64MB, t=3) de un código que genera la aplicación y que usted anota, o de una frase de contraseña propia; Notes Tech no guarda ni uno ni otra. Primero se escribe, ya cifrado, en el almacenamiento privado de la aplicación; luego se copia al lugar elegido y se borra de ese almacenamiento. Las notas de una caja fuerte con frase de contraseña siguen selladas por su frase en el archivo. Una caja fuerte con PIN solo viaja abierta, con su clave: en el archivo solo la protege el secreto de la copia; cerrada, queda fuera. La restauración añade lo que falta y no sobrescribe nada. - **Notas importadas y compartidas**: los archivos Markdown o de texto —o un archivo ZIP que los contenga— que usted elige en el selector de documentos de Android, y los textos o archivos que comparte con Notes Tech desde otra app, se convierten en notas normales en sus carpetas, nunca en una caja fuerte. Se leen una sola vez, en el teléfono; los originales no se modifican y no se conserva nada aparte de las notas creadas. Como máximo 500.000 caracteres por nota. +- **Notas selladas (opcionales)**: una nota que elige compartir sellada se cifra en un archivo `.ntnote` — **AES-256-GCM** con una clave derivada mediante **Argon2id** (m=64MB, t=3) de un código que la app genera, muestra una sola vez y no guarda en ningún sitio. El archivo se escribe en el almacenamiento privado de la app y luego se confía a la app que elija para enviarlo; el código lo transmite por otra vía. Su nombre no dice nada de la nota. Una nota sellada que recibe se lee una vez del archivo que abre con Notes Tech, y solo se convierte en una nota — de la Bandeja de entrada, nunca de una caja fuerte — una vez escrito su código. +- **Depósito rápido**: desde un mosaico de los ajustes rápidos o un acceso directo del icono, escribe una nota en la Bandeja de entrada sin desbloquear la app; no lee ni muestra nada de lo que contiene. +- **Notas con duración (opcionales)**: se borran definitivamente al acabar su plazo — en la siguiente apertura de la app o al volver a la lista —, o al cerrarlas tras su lectura si eligió «Tras la lectura». No entran ni en la copia de seguridad cifrada ni en la exportación. +- **Teclado**: la app pide al teclado que no aprenda nada de lo que escribe (ajuste activado por defecto). Es una petición, que un teclado puede ignorar; lo que un teclado guarda por su cuenta escapa a cualquier app. - **Modelo de dictado por voz (Whisper `.bin`)**: lo obtiene usted mismo —la aplicación muestra el nombre del archivo y su origen— y luego lo importa mediante el selector de documentos de Android. Notes Tech no tiene permiso de acceso a Internet y **no ofrece ninguna forma de descargar nada**. Su SHA-256 se comprueba al importarlo y antes de cada carga. - **Audio captado durante el dictado**: se escribe en un archivo temporal del almacenamiento privado de la aplicación —el motor de transcripción lee un archivo, no hay otra manera— y **se borra en cuanto llega la transcripción**. También se borra al iniciar la aplicación y con el modo pánico, de modo que un cierre brusco no deja nada. - **Ajustes (tema, orden, dictado activado, bloqueo automático de las cajas fuertes, opciones del bloqueo de la aplicación)**: guardados sin cifrar en las preferencias locales (ningún dato sensible). diff --git a/app/src/main/res/raw-fr/privacy.md b/app/src/main/res/raw-fr/privacy.md index a146e121..f0e5bd64 100644 --- a/app/src/main/res/raw-fr/privacy.md +++ b/app/src/main/res/raw-fr/privacy.md @@ -1,6 +1,6 @@ # Politique de confidentialité — Notes Tech -_Dernière mise à jour : 30 octobre 2026_ +_Dernière mise à jour : 9 novembre 2026_ ## En une phrase @@ -17,6 +17,10 @@ Notes Tech ne collecte, ne transmet et ne stocke aucune donnée sur des serveurs - **Backlinks `[[Titre]]`** : index inversé local, jamais transmis. - **Sauvegarde chiffrée (facultative)** : un fichier `.ntbackup` que vous créez vous-même, à l'endroit que vous choisissez par le sélecteur de documents Android. Il contient vos dossiers et vos notes, corbeille comprise, chiffrés en **AES-256-GCM** sous une clé dérivée par **Argon2id** (m=64MB, t=3) d'un code tiré par l'application et que vous notez, ou d'une passphrase de votre choix — Notes Tech ne garde ni l'un ni l'autre. Il est d'abord écrit, déjà chiffré, dans le stockage privé de l'application, puis copié à l'endroit choisi et effacé du stockage de l'application. Les notes d'un coffre à passphrase restent scellées par sa passphrase dans le fichier. Un coffre à PIN ne voyage qu'ouvert, avec sa clé : dans le fichier, seul le secret de la sauvegarde le protège ; fermé, il est laissé de côté. La restauration ajoute ce qui manque et n'écrase rien. - **Notes importées et partagées** : les fichiers Markdown ou texte — ou une archive ZIP de ces fichiers — que vous choisissez dans le sélecteur de documents d'Android, et les textes ou fichiers que vous partagez vers Notes Tech depuis une autre appli, deviennent des notes ordinaires dans vos dossiers, jamais dans un coffre. Ils sont lus une fois, sur le téléphone ; les originaux ne sont pas modifiés et rien n'est gardé à côté des notes créées. 500 000 caractères au plus par note. +- **Notes scellées (facultatives)** : une note que vous choisissez de partager scellée est chiffrée dans un fichier `.ntnote` — **AES-256-GCM** sous une clé dérivée par **Argon2id** (m=64MB, t=3) d'un code que l'application tire, affiche une seule fois et ne conserve nulle part. Le fichier est écrit dans le stockage privé de l'application, puis confié à l'application que vous choisissez pour l'envoyer ; vous transmettez le code par un autre moyen. Son nom ne dit rien de la note. Une note scellée que vous recevez est lue une fois depuis le fichier que vous ouvrez avec Notes Tech, et ne devient une note — de la Boîte de réception, jamais d'un coffre — qu'une fois son code tapé. +- **Dépôt rapide** : depuis une tuile des réglages rapides ou un raccourci de l'icône, il écrit une note dans la Boîte de réception sans déverrouiller l'application ; il ne lit et n'affiche rien de ce qu'elle contient. +- **Notes à durée de vie (facultatives)** : effacées définitivement une fois leur délai passé — à la prochaine ouverture de l'application ou au retour sur la liste —, ou à la fermeture après leur lecture si vous avez choisi « Après lecture ». Elles n'entrent ni dans la sauvegarde chiffrée ni dans l'export. +- **Clavier** : l'application demande au clavier de ne rien apprendre de ce que vous tapez (réglage activé par défaut). C'est une demande, qu'un clavier peut ignorer ; ce qu'un clavier garde de lui-même échappe à toute application. - **Modèle de dictée vocale (Whisper `.bin`)** : vous vous le procurez vous-même — l'application affiche le nom du fichier et sa source — puis vous l'importez par le sélecteur de documents Android. Notes Tech n'a pas la permission Internet et **n'expose aucun moyen de télécharger quoi que ce soit**. Son empreinte SHA-256 est vérifiée à l'import et avant chaque chargement. - **Audio capturé pendant la dictée** : écrit dans un fichier temporaire du stockage privé de l'application — le moteur de transcription lit un fichier, il ne peut pas en être autrement — puis **effacé dès la transcription obtenue**. Il l'est également au démarrage de l'application et par le mode panique, de sorte qu'un arrêt brutal ne laisse rien derrière lui. - **Préférences (thème, tri, dictée activée, auto-lock coffre, options du verrouillage)** : stockées en clair dans les préférences locales (pas de donnée sensible). diff --git a/app/src/main/res/raw-it/privacy.md b/app/src/main/res/raw-it/privacy.md index 0b068d5a..e1ba85ce 100644 --- a/app/src/main/res/raw-it/privacy.md +++ b/app/src/main/res/raw-it/privacy.md @@ -1,6 +1,6 @@ # Informativa sulla privacy — Notes Tech -_Ultimo aggiornamento: 30 ottobre 2026_ +_Ultimo aggiornamento: 9 novembre 2026_ ## In una frase @@ -17,6 +17,10 @@ Notes Tech non raccoglie, non trasmette e non conserva alcun dato su server remo - **Collegamenti inversi `[[Titolo]]`**: indice invertito locale, mai trasmesso. - **Backup cifrato (facoltativo)**: un file `.ntbackup` che crea lei stesso, nel punto che sceglie tramite il selettore di documenti di Android. Contiene le sue cartelle e le sue note, cestino compreso, cifrate con **AES-256-GCM** con una chiave derivata tramite **Argon2id** (m=64MB, t=3) da un codice estratto dall'app e che lei annota, o da una passphrase di sua scelta — Notes Tech non conserva né l'uno né l'altra. Viene prima scritto, già cifrato, nella memoria privata dell'app, poi copiato nel punto scelto e cancellato dalla memoria dell'app. Le note di una cassaforte con passphrase restano sigillate dalla sua passphrase nel file. Una cassaforte con PIN viaggia solo aperta, con la sua chiave: nel file la protegge soltanto il segreto del backup; chiusa, viene esclusa. Il ripristino aggiunge ciò che manca e non sovrascrive nulla. - **Note importate e condivise**: i file Markdown o di testo — o un archivio ZIP che li contiene — che lei sceglie nel selettore di documenti di Android, e i testi o i file che condivide con Notes Tech da un'altra app, diventano note ordinarie nelle sue cartelle, mai in una cassaforte. Vengono letti una volta, sul telefono; gli originali non vengono modificati e nulla viene conservato oltre alle note create. Al massimo 500.000 caratteri per nota. +- **Note sigillate (facoltative)**: una nota che sceglie di condividere sigillata viene cifrata in un file `.ntnote` — **AES-256-GCM** con una chiave derivata tramite **Argon2id** (m=64MB, t=3) da un codice che l'app genera, mostra una sola volta e non conserva da nessuna parte. Il file viene scritto nella memoria privata dell'app, poi affidato all'app che sceglie per inviarlo; il codice lo trasmette in un altro modo. Il suo nome non dice nulla della nota. Una nota sigillata che riceve viene letta una volta dal file che apre con Notes Tech, e diventa una nota — della cartella In arrivo, mai di una cassaforte — solo dopo aver digitato il suo codice. +- **Deposito rapido**: da un riquadro delle impostazioni rapide o da una scorciatoia dell'icona, scrive una nota nella cartella In arrivo senza sbloccare l'app; non legge né mostra nulla di ciò che l'app contiene. +- **Note con durata (facoltative)**: cancellate definitivamente allo scadere del tempo — alla successiva apertura dell'app o al ritorno all'elenco —, o alla chiusura dopo la lettura se ha scelto «Dopo la lettura». Non entrano né nel backup cifrato né nell'esportazione. +- **Tastiera**: l'app chiede alla tastiera di non imparare nulla di ciò che digita (impostazione attiva per impostazione predefinita). È una richiesta, che una tastiera può ignorare; ciò che una tastiera conserva da sé sfugge a qualsiasi app. - **Modello di dettatura vocale (Whisper `.bin`)**: se lo procura lei stesso — l'app mostra il nome del file e la sua origine — e poi lo importa tramite il selettore di documenti di Android. Notes Tech non ha l'autorizzazione di accesso a Internet e **non offre alcun modo di scaricare nulla**. Il suo SHA-256 viene verificato all'importazione e prima di ogni caricamento. - **Audio acquisito durante la dettatura**: scritto in un file temporaneo nello spazio privato dell'app — il motore di trascrizione legge un file, non c'è altro modo — e **cancellato appena arriva la trascrizione**. Viene cancellato anche all'avvio dell'app e dalla modalità panico, così una chiusura improvvisa non lascia nulla. - **Impostazioni (tema, ordinamento, dettatura attivata, blocco automatico delle casseforti, opzioni del blocco dell'app)**: conservate in chiaro nelle preferenze locali (nessun dato sensibile). diff --git a/app/src/main/res/raw/privacy.md b/app/src/main/res/raw/privacy.md index fda13ea5..05aa7048 100644 --- a/app/src/main/res/raw/privacy.md +++ b/app/src/main/res/raw/privacy.md @@ -1,6 +1,6 @@ # Privacy policy — Notes Tech -_Last updated: 30 October 2026_ +_Last updated: 9 November 2026_ ## In one sentence @@ -17,6 +17,10 @@ Notes Tech does not collect, transmit or store any data on remote servers. Every - **Backlinks `[[Title]]`**: local inverted index, never transmitted. - **Encrypted backup (optional)**: a `.ntbackup` file you create yourself, where you choose through the Android document picker. It holds your folders and notes, trash included, encrypted with **AES-256-GCM** under a key derived by **Argon2id** (m=64MB, t=3) from a code the app draws and you write down, or from a passphrase of your own — Notes Tech keeps neither. It is first written, already encrypted, in the app's private storage, then copied where you chose and erased from the app's storage. A passphrase vault's notes stay sealed by its passphrase in the file. A PIN vault travels only open, with its key: in the file, only the backup's secret protects it; a closed one is left out. Restoring adds what is missing and overwrites nothing. - **Imported and shared notes**: Markdown or text files — or a ZIP of them — you choose in the Android document picker, and text or files you share to Notes Tech from another app, become ordinary notes in your folders, never in a vault. They are read once, on the phone; the originals are left untouched and nothing is kept beside the notes created. At most 500,000 characters per note. +- **Sealed notes (optional)**: a note you choose to share sealed is encrypted into a `.ntnote` file — **AES-256-GCM** under a key derived by **Argon2id** (m=64MB, t=3) from a code the app draws, shows once and keeps nowhere. The file is written in the app's private storage, then handed to the app you choose to send it; you pass the code on another way. Its name says nothing of the note. A sealed note you receive is read once from the file you open with Notes Tech, and becomes a note — of the Inbox, never of a vault — only once you type its code. +- **Quick drop**: from a quick settings tile or a shortcut of the app's icon, it writes a note into the Inbox without unlocking the app; it reads and shows nothing of what the app holds. +- **Notes with a lifetime (optional)**: erased for good once their time is up — at the next opening of the app or return to the list —, or when closed after their reading if you chose "After reading". They go into neither the encrypted backup nor the export. +- **Keyboard**: the app asks the keyboard not to learn what you type (a setting, on by default). It is a request, which a keyboard may ignore; what a keyboard keeps on its own is out of any app's reach. - **Voice dictation model (Whisper `.bin`)**: you obtain it yourself — the app shows the file name and its source — then import it through the Android document picker. Notes Tech has no Internet permission and **exposes no way to download anything**. Its SHA-256 is verified on import and before every load. - **Audio captured during dictation**: written to a temporary file in the app's private storage — the transcription engine reads a file, there is no way around it — then **wiped as soon as the transcription is returned**. It is also wiped on app start and by panic mode, so an abrupt shutdown leaves nothing behind. - **Settings (theme, sort, dictation enabled, vault auto-lock, app lock options)**: stored in clear in local preferences (no sensitive data). diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 49042e7a..aea9c21c 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -344,6 +344,75 @@ Dunkel In den letzten Apps ausblenden Verhindert Bildschirmfotos und blendet die Vorschau der App in der Android-Übersicht der letzten Apps aus. + Tastatur ohne Lernen + Bittet Ihre Tastatur, nichts von dem zu lernen, was Sie in Notes Tech tippen. Vorschläge bleiben; eine Tastatur kann die Bitte ignorieren. + Neue Notiz + Neue Notiz + Diktieren + Notiz diktieren + Schnellablage + Wird im Eingang gespeichert, ohne Notes Tech zu entsperren. Nichts, was die App enthält, ist hier lesbar. + Text + Ablegen + Die Notiz konnte nicht abgelegt werden. Ihr Text ist noch da. + Notiz im Eingang abgelegt + Diesen Text verwerfen? + Er wurde nicht abgelegt, und nichts bewahrt ihn auf, sobald dieser Bildschirm geschlossen ist. + Verwerfen + Lebensdauer + Lebensdauer + Keine – die Notiz bleibt + 1 Stunde + 24 Stunden + 7 Tage + 30 Tage + Nach dem Lesen + Gelöscht, wenn Sie sie nach dem erneuten Öffnen schließen. + Endgültig gelöscht, sobald die Zeit abgelaufen ist, beim nächsten Öffnen der App oder bei der Rückkehr zur Liste – nicht auf die Sekunde genau. Nicht in Sicherungen und Exporten enthalten. Die Zeit folgt der Uhr des Telefons: Wer das Datum vorstellt, verkürzt sie. + Lebensdauer: %1$s + Diese Notiz wird gelöscht, wenn Sie sie schließen. + %1$d Min. + %1$d Std. + %1$d T. + bald + Wird bald gelöscht + Wird nach dem nächsten Lesen gelöscht + + Wird in %1$d Minute gelöscht + Wird in %1$d Minuten gelöscht + + + Wird in %1$d Stunde gelöscht + Wird in %1$d Stunden gelöscht + + + Wird in %1$d Tag gelöscht + Wird in %1$d Tagen gelöscht + + Versiegelt teilen + Versiegelte Notiz öffnen + Versiegelte Notiz + Die Notiz wird in eine Datei verschlüsselt, die nur ihr Code öffnet. Senden Sie die Datei über eine beliebige App und den Code auf anderem Weg – mündlich, per SMS: Wer beides hat, kann sie lesen. + Beim Empfänger nur einmal lesbar + Nach dem ersten Lesen in seinem Notes Tech gelöscht. Die Datei kann er trotzdem behalten. + Versiegeln + Wird versiegelt… + Der Code – auf anderem Weg als die Datei senden: + Nur jetzt angezeigt: Er wird nirgends gespeichert. + Code kopieren + Datei teilen + Die Notiz konnte nicht versiegelt werden. + Versiegelte Notiz erhalten + Geben Sie den Code ein, den Sie auf anderem Weg als die Datei erhalten haben. + Code + Öffnen + Wird geöffnet… + Falscher Code oder beschädigte Datei. + Ein Code hat 24 Buchstaben und Ziffern. + Diese versiegelte Notiz stammt aus einem neueren Notes Tech: Aktualisieren Sie die App. + Diese versiegelte Notiz ist nicht lesbar. + Diese Datei ist keine versiegelte Notiz von Notes Tech. + Diese Datei ist zu groß für eine versiegelte Notiz. Automatische Tresorsperre %1$d Minute diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index bbbe6477..f0b16e88 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -363,6 +363,78 @@ Oscuro Ocultar en aplicaciones recientes Impide las capturas de pantalla y oculta la vista previa de la aplicación en el selector de tareas de Android. + Teclado sin aprendizaje + Pide al teclado que no aprenda nada de lo que escribe en Notes Tech. Las sugerencias se mantienen; un teclado puede ignorar la petición. + Nueva nota + Nueva nota + Dictar + Dictar una nota + Depósito rápido + Se escribe en la Bandeja de entrada sin desbloquear Notes Tech. Aquí no se puede leer nada de lo que contiene la app. + Texto + Depositar + No se pudo depositar la nota. Su texto sigue aquí. + Nota depositada en la Bandeja de entrada + ¿Descartar este texto? + No se ha depositado, y nada lo conserva una vez cerrada esta pantalla. + Descartar + Duración + Duración + Ninguna — la nota se queda + 1 hora + 24 horas + 7 días + 30 días + Tras la lectura + Se borra cuando la cierra tras haberla vuelto a abrir. + Se borra definitivamente al acabar el plazo, en la siguiente apertura de la app o al volver a la lista — no al segundo. Queda fuera de las copias de seguridad y de las exportaciones. El plazo sigue el reloj del teléfono: adelantar la fecha lo acorta. + Duración: %1$s + Esta nota se borra cuando la cierra. + %1$d min + %1$d h + %1$d d + pronto + Se borra pronto + Se borra tras su próxima lectura + + Se borra en %1$d minuto + Se borra en %1$d minutos + Se borra en %1$d minutos + + + Se borra en %1$d hora + Se borra en %1$d horas + Se borra en %1$d horas + + + Se borra en %1$d día + Se borra en %1$d días + Se borra en %1$d días + + Compartir sellada + Abrir una nota sellada + Nota sellada + La nota se cifra en un archivo que solo su código abre. Envíe el archivo con cualquier app y el código por otra vía — de viva voz, por SMS: quien tenga ambos puede leerla. + Lectura única para el destinatario + Se borra tras su primera lectura en su Notes Tech. Aun así, puede conservar el archivo. + Sellar + Sellando… + El código — envíelo por otra vía que el archivo: + Solo se muestra ahora: no se guarda en ningún sitio. + Copiar el código + Compartir el archivo + No se pudo sellar la nota. + Nota sellada recibida + Escriba el código recibido por otra vía que el archivo. + Código + Abrir + Abriendo… + Código incorrecto, o archivo dañado. + Un código tiene 24 letras y cifras. + Esta nota sellada viene de un Notes Tech más reciente: actualice la app. + Esta nota sellada no se puede leer. + Este archivo no es una nota sellada de Notes Tech. + Este archivo es demasiado grande para ser una nota sellada. Bloqueo automático de las cajas fuertes %1$d minuto diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 6632381b..34f3b60a 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -368,6 +368,78 @@ Sombre Masquer dans les apps récentes Empêche la capture d\'écran et masque l\'aperçu de l\'app dans le sélecteur Android. + Clavier sans apprentissage + Demande au clavier de ne rien apprendre de ce que vous tapez dans Notes Tech. Les suggestions restent ; un clavier peut ignorer la demande. + Nouvelle note + Nouvelle note + Dicter + Dicter une note + Dépôt rapide + S\'écrit dans la Boîte de réception sans déverrouiller Notes Tech. Rien de ce que l\'appli contient ne se lit ici. + Texte + Déposer + La note n\'a pas pu être déposée. Votre texte est toujours là. + Note déposée dans la Boîte de réception + Abandonner ce texte ? + Il n\'a pas été déposé, et rien ne le garde une fois cet écran fermé. + Abandonner + Durée de vie + Durée de vie + Aucune — la note reste + 1 heure + 24 heures + 7 jours + 30 jours + Après lecture + Effacée quand vous la refermez après l\'avoir rouverte. + Effacée pour de bon une fois le délai passé, à la prochaine ouverture de l\'appli ou au retour sur la liste — pas à la seconde près. Elle n\'entre ni dans les sauvegardes ni dans les exports. Le délai suit l\'horloge du téléphone : avancer la date l\'écourte. + Durée de vie : %1$s + Cette note s\'efface quand vous la refermez. + %1$d min + %1$d h + %1$d j + bientôt + S\'efface bientôt + S\'efface après sa prochaine lecture + + S\'efface dans %1$d minute + S\'efface dans %1$d minutes + S\'efface dans %1$d minutes + + + S\'efface dans %1$d heure + S\'efface dans %1$d heures + S\'efface dans %1$d heures + + + S\'efface dans %1$d jour + S\'efface dans %1$d jours + S\'efface dans %1$d jours + + Partager scellée + Ouvrir une note scellée + Note scellée + La note est chiffrée dans un fichier que seul son code ouvre. Envoyez le fichier par n\'importe quelle appli, et le code par un autre moyen — de vive voix, par SMS : qui a les deux peut la lire. + Lecture unique chez le destinataire + Effacée après sa première lecture dans son Notes Tech. Il peut toutefois garder le fichier. + Sceller + Scellement… + Le code — à envoyer autrement que le fichier : + Affiché maintenant seulement : il n\'est gardé nulle part. + Copier le code + Partager le fichier + La note n\'a pas pu être scellée. + Note scellée reçue + Tapez le code reçu par un autre moyen que le fichier. + Code + Ouvrir + Ouverture… + Code incorrect, ou fichier abîmé. + Un code compte 24 lettres et chiffres. + Cette note scellée vient d\'un Notes Tech plus récent : mettez l\'appli à jour. + Cette note scellée est illisible. + Ce fichier n\'est pas une note scellée de Notes Tech. + Ce fichier est trop gros pour être une note scellée. Verrouillage automatique des coffres %1$d minute diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 77ca6021..38ff4753 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -363,6 +363,78 @@ Scuro Nascondi nelle app recenti Impedisce gli screenshot e nasconde l\'anteprima dell\'app nel selettore delle attività di Android. + Tastiera senza apprendimento + Chiede alla tastiera di non imparare nulla di ciò che digita in Notes Tech. I suggerimenti restano; una tastiera può ignorare la richiesta. + Nuova nota + Nuova nota + Dettare + Dettare una nota + Deposito rapido + Viene scritta nella cartella In arrivo senza sbloccare Notes Tech. Qui non si può leggere nulla di ciò che l\'app contiene. + Testo + Deposita + Non è stato possibile depositare la nota. Il testo è ancora qui. + Nota depositata nella cartella In arrivo + Scartare questo testo? + Non è stato depositato, e nulla lo conserva una volta chiusa questa schermata. + Scarta + Durata + Durata + Nessuna — la nota resta + 1 ora + 24 ore + 7 giorni + 30 giorni + Dopo la lettura + Cancellata quando la chiude dopo averla riaperta. + Cancellata definitivamente allo scadere del tempo, alla successiva apertura dell\'app o al ritorno all\'elenco — non al secondo. Esclusa da backup ed esportazioni. Il tempo segue l\'orologio del telefono: spostare avanti la data lo accorcia. + Durata: %1$s + Questa nota si cancella quando la chiude. + %1$d min + %1$d h + %1$d g + a breve + Si cancella a breve + Si cancella dopo la prossima lettura + + Si cancella tra %1$d minuto + Si cancella tra %1$d minuti + Si cancella tra %1$d minuti + + + Si cancella tra %1$d ora + Si cancella tra %1$d ore + Si cancella tra %1$d ore + + + Si cancella tra %1$d giorno + Si cancella tra %1$d giorni + Si cancella tra %1$d giorni + + Condividi sigillata + Apri una nota sigillata + Nota sigillata + La nota viene cifrata in un file che solo il suo codice apre. Invii il file con qualsiasi app, e il codice in un altro modo — a voce, per SMS: chi ha entrambi può leggerla. + Lettura unica per il destinatario + Cancellata dopo la prima lettura nel suo Notes Tech. Può comunque conservare il file. + Sigilla + Sigillatura… + Il codice — da inviare in modo diverso dal file: + Mostrato solo ora: non viene conservato da nessuna parte. + Copia il codice + Condividi il file + Non è stato possibile sigillare la nota. + Nota sigillata ricevuta + Digiti il codice ricevuto in un modo diverso dal file. + Codice + Apri + Apertura… + Codice errato, o file danneggiato. + Un codice ha 24 lettere e cifre. + Questa nota sigillata proviene da un Notes Tech più recente: aggiorni l\'app. + Questa nota sigillata è illeggibile. + Questo file non è una nota sigillata di Notes Tech. + Questo file è troppo grande per essere una nota sigillata. Blocco automatico delle casseforti %1$d minuto diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d4da5958..5b4cae83 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -419,6 +419,75 @@ Dark Hide in recent apps Prevents screenshots and hides the app preview in the Android task switcher. + Keyboard without learning + Asks your keyboard not to learn what you type in Notes Tech. Suggestions stay; a keyboard may ignore the request. + New note + New note + Dictate + Dictate a note + Quick drop + Written to the Inbox without unlocking Notes Tech. Nothing the app holds can be read here. + Text + Drop + The note could not be dropped. Your text is still here. + Note dropped in the Inbox + Discard this text? + It has not been dropped, and nothing keeps it once this screen is closed. + Discard + Lifetime + Lifetime + None — the note stays + 1 hour + 24 hours + 7 days + 30 days + After reading + Erased when you close it after opening it again. + Erased for good once the time is up, at the next opening of the app or return to the list — not to the second. Left out of backups and exports. The time follows the phone\'s clock: moving the date forward ends it. + Lifetime: %1$s + This note erases itself when you close it. + %1$d min + %1$d h + %1$d d + soon + Erases itself soon + Erases itself after its next reading + + Erases itself in %1$d minute + Erases itself in %1$d minutes + + + Erases itself in %1$d hour + Erases itself in %1$d hours + + + Erases itself in %1$d day + Erases itself in %1$d days + + Share sealed + Open a sealed note + Sealed note + The note is encrypted into a file that only its code opens. Send the file by any app, and the code another way — aloud, by text message: whoever has both can read it. + Read once by the recipient + Erased after its first reading in their Notes Tech. They can still keep the file. + Seal + Sealing… + The code — send it another way than the file: + Shown only now: it is kept nowhere. + Copy the code + Share the file + The note could not be sealed. + Sealed note received + Type the code you received another way than the file. + Code + Open + Opening… + Wrong code, or damaged file. + A code has 24 letters and digits. + This sealed note comes from a newer Notes Tech: update the app. + This sealed note cannot be read. + This file is not a Notes Tech sealed note. + This file is too big to be a sealed note. Vault auto-lock %1$d minute diff --git a/app/src/test/java/com/filestech/notes_tech/data/repository/DureesDeVieTest.kt b/app/src/test/java/com/filestech/notes_tech/data/repository/DureesDeVieTest.kt new file mode 100644 index 00000000..3867a255 --- /dev/null +++ b/app/src/test/java/com/filestech/notes_tech/data/repository/DureesDeVieTest.kt @@ -0,0 +1,74 @@ +package com.filestech.notes_tech.data.repository + +import com.filestech.notes_tech.data.local.DatabaseProvider +import com.filestech.notes_tech.data.local.NotesDatabase +import com.filestech.notes_tech.data.local.dao.NoteWriteDao +import com.filestech.notes_tech.domain.model.DureeDeVie +import com.google.common.truth.Truth.assertThat +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import java.time.Clock +import java.time.Duration +import java.time.Instant +import java.time.ZoneOffset + +/** What a lifetime writes (D-043, 3.4.0) — the purge itself runs on a real base in `MigrationVersDixTest`. */ +@DisplayName("Durées de vie") +class DureesDeVieTest { + + private val maintenant = Instant.parse("2026-11-09T20:00:00Z") + private val ecriture = mockk() + private lateinit var durees: DureesDeVie + + @BeforeEach + fun setUp() { + val base = mockk { every { noteWriteDao() } returns ecriture } + val bases = mockk { coEvery { get() } returns base } + coEvery { ecriture.updateLifetime(any(), any(), any()) } returns 1 + coEvery { ecriture.deleteExpired(any()) } returns 2 + durees = DureesDeVie(bases, Clock.fixed(maintenant, ZoneOffset.UTC)) + } + + @Test + fun `a duration counts from now, without the reading flag`() = runTest { + assertThat(durees.poser("n", DureeDeVie.UNE_HEURE)).isTrue() + durees.poser("n", DureeDeVie.TRENTE_JOURS) + + coVerify { ecriture.updateLifetime("n", maintenant.plus(Duration.ofHours(1)).toEpochMilli(), false) } + coVerify { ecriture.updateLifetime("n", maintenant.plus(Duration.ofDays(30)).toEpochMilli(), false) } + } + + @Test + fun `after reading has no date, only its flag`() = runTest { + durees.poser("n", DureeDeVie.APRES_LECTURE) + + coVerify { ecriture.updateLifetime("n", null, true) } + } + + @Test + fun `none clears both`() = runTest { + durees.poser("n", null) + + coVerify { ecriture.updateLifetime("n", null, false) } + } + + @Test + fun `the purge erases what is over at now, and says how many`() = runTest { + assertThat(durees.purger()).isEqualTo(2) + + coVerify { ecriture.deleteExpired(maintenant.toEpochMilli()) } + } + + @Test + fun `an unknown note is not written`() = runTest { + coEvery { ecriture.updateLifetime("absente", any(), any()) } returns 0 + + assertThat(durees.poser("absente", DureeDeVie.UN_JOUR)).isFalse() + } +} diff --git a/app/src/test/java/com/filestech/notes_tech/data/scellee/LectureScelleeTest.kt b/app/src/test/java/com/filestech/notes_tech/data/scellee/LectureScelleeTest.kt new file mode 100644 index 00000000..6ee16ee9 --- /dev/null +++ b/app/src/test/java/com/filestech/notes_tech/data/scellee/LectureScelleeTest.kt @@ -0,0 +1,74 @@ +package com.filestech.notes_tech.data.scellee + +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import java.io.ByteArrayInputStream +import java.io.InputStream + +/** + * D-044 (3.4.0) — reading a file "opened with" Notes Tech: bounded, and a file that is no sealed note is + * dropped at its first bytes — the filter takes any `application/octet-stream`, a video included. + */ +@DisplayName("Lecture d'une note scellée") +class LectureScelleeTest { + + @Test + fun `a sealed note is read whole`() { + val fichier = FichierDeNoteScellee.MAGIQUE + ByteArray(100_000) { it.toByte() } + + val lue = lireBorne(ByteArrayInputStream(fichier)) + + assertThat(lue).isInstanceOf(LectureScellee.Lue::class.java) + assertThat((lue as LectureScellee.Lue).octets).isEqualTo(fichier) + } + + @Test + fun `another file is dropped at its first bytes, not read to its end`() { + val flux = FluxCompte("%PDF-1.7".toByteArray() + ByteArray(10_000_000)) + + assertThat(lireBorne(flux)).isEqualTo(LectureScellee.PasUneNoteScellee) + // One buffer at most: the rest of a large file is never read. + assertThat(flux.lus).isAtMost(64 * 1024) + } + + @Test + fun `a file past the bound is refused`() { + val fichier = FichierDeNoteScellee.MAGIQUE + ByteArray(FichierDeNoteScellee.TAILLE_MAX.toInt()) + + assertThat(lireBorne(ByteArrayInputStream(fichier))).isEqualTo(LectureScellee.TropGrosse) + } + + @Test + fun `an empty or too short file is no sealed note`() { + assertThat(lireBorne(ByteArrayInputStream(ByteArray(0)))).isEqualTo(LectureScellee.PasUneNoteScellee) + assertThat(lireBorne(ByteArrayInputStream("NTSE".toByteArray()))).isEqualTo(LectureScellee.PasUneNoteScellee) + } + + /** A stream that hands nothing back without ever ending — as a hostile provider could. */ + @Test + fun `a stream that gives nothing without ending is refused, not looped on`() { + val vide = object : InputStream() { + override fun read(): Int = 0 + + override fun read(b: ByteArray, off: Int, len: Int): Int = 0 + } + + assertThat(lireBorne(vide)).isEqualTo(LectureScellee.Illisible) + } + + /** Counts what is really read. */ + private class FluxCompte(octets: ByteArray) : InputStream() { + private val entree = ByteArrayInputStream(octets) + var lus = 0 + private set + + override fun read(): Int = entree.read().also { if (it >= 0) lus++ } + + override fun read(b: ByteArray, off: Int, len: Int): Int { + val n = entree.read(b, off, len) + if (n > 0) lus += n + return n + } + } +} diff --git a/app/src/test/java/com/filestech/notes_tech/ui/common/ResteDeVieTest.kt b/app/src/test/java/com/filestech/notes_tech/ui/common/ResteDeVieTest.kt new file mode 100644 index 00000000..f6600c0c --- /dev/null +++ b/app/src/test/java/com/filestech/notes_tech/ui/common/ResteDeVieTest.kt @@ -0,0 +1,50 @@ +package com.filestech.notes_tech.ui.common + +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import java.time.Duration +import java.time.Instant + +/** What a card says of a note's lifetime (D-043, 3.4.0). */ +@DisplayName("Reste de vie d'une note") +class ResteDeVieTest { + + private val maintenant = Instant.parse("2026-11-09T20:00:00Z") + + private fun dans(duree: Duration) = resteDeVie(maintenant, maintenant.plus(duree), lectureUnique = false) + + @Test + fun `no lifetime, nothing to say`() { + assertThat(resteDeVie(maintenant, null, lectureUnique = false)).isNull() + } + + @Test + fun `after reading wins over a date`() { + assertThat(resteDeVie(maintenant, maintenant.plusSeconds(60), lectureUnique = true)) + .isEqualTo(ResteDeVie.ApresLecture) + } + + @Test + fun `rounded up, never less than what is left`() { + assertThat(dans(Duration.ofSeconds(1))).isEqualTo(ResteDeVie.Minutes(1)) + assertThat(dans(Duration.ofMinutes(59).plusSeconds(30))).isEqualTo(ResteDeVie.Heures(1)) + assertThat(dans(Duration.ofMinutes(59))).isEqualTo(ResteDeVie.Minutes(59)) + assertThat(dans(Duration.ofHours(1))).isEqualTo(ResteDeVie.Heures(1)) + assertThat(dans(Duration.ofHours(23).plusMinutes(1))).isEqualTo(ResteDeVie.Heures(24)) + } + + @Test + fun `hours below two days, days beyond`() { + assertThat(dans(Duration.ofHours(47))).isEqualTo(ResteDeVie.Heures(47)) + assertThat(dans(Duration.ofHours(48))).isEqualTo(ResteDeVie.Jours(2)) + assertThat(dans(Duration.ofDays(7))).isEqualTo(ResteDeVie.Jours(7)) + assertThat(dans(Duration.ofDays(30))).isEqualTo(ResteDeVie.Jours(30)) + } + + @Test + fun `the time is up, the purge has not run yet`() { + assertThat(dans(Duration.ZERO)).isEqualTo(ResteDeVie.Bientot) + assertThat(dans(Duration.ofMinutes(-5))).isEqualTo(ResteDeVie.Bientot) + } +} diff --git a/app/src/test/java/com/filestech/notes_tech/ui/depot/DepotViewModelTest.kt b/app/src/test/java/com/filestech/notes_tech/ui/depot/DepotViewModelTest.kt new file mode 100644 index 00000000..c58003c4 --- /dev/null +++ b/app/src/test/java/com/filestech/notes_tech/ui/depot/DepotViewModelTest.kt @@ -0,0 +1,99 @@ +package com.filestech.notes_tech.ui.depot + +import com.filestech.notes_tech.data.repository.NotesRepository +import com.filestech.notes_tech.domain.importation.LongueurDUneNote +import com.filestech.notes_tech.domain.model.Folder +import com.filestech.notes_tech.domain.model.Note +import com.filestech.notes_tech.security.panic.PanicJournal +import com.google.common.truth.Truth.assertThat +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.confirmVerified +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** + * The blind drop (D-042, 3.4.0): it writes one note to the Inbox and reads nothing — its only call on + * the notes is [NotesRepository.create], checked with `confirmVerified`. + */ +@DisplayName("Dépôt à l'aveugle") +class DepotViewModelTest { + + private val notes = mockk() + private val journal = mockk() + private lateinit var depot: DepotViewModel + + @BeforeEach + fun setUp() { + Dispatchers.setMain(UnconfinedTestDispatcher()) + every { journal.isPending() } returns false + coEvery { notes.create(any(), any(), any(), any()) } returns mockk() + depot = DepotViewModel(notes, journal) + } + + @AfterEach + fun tearDown() = Dispatchers.resetMain() + + @Test + fun `a drop writes one note to the Inbox, then nothing typed stays on the screen`() { + depot.titre(" Idée ") + depot.texte("Le texte\nsur deux lignes") + + depot.deposer() + + coVerify(exactly = 1) { notes.create(Folder.INBOX_ID, "Idée", "Le texte\nsur deux lignes", any()) } + confirmVerified(notes) + assertThat(depot.etat.value).isEqualTo(EtatDuDepot(depose = true)) + } + + @Test + fun `nothing typed, nothing dropped`() { + depot.titre(" ") + + depot.deposer() + + assertThat(depot.etat.value.peutDeposer).isFalse() + coVerify(exactly = 0) { notes.create(any(), any(), any(), any()) } + } + + /** A panic cut before its end is the app's to finish: the drop must not write into that database. */ + @Test + fun `while a panic is pending, nothing is written and the text stays`() { + every { journal.isPending() } returns true + depot.texte("à garder") + + depot.deposer() + + coVerify(exactly = 0) { notes.create(any(), any(), any(), any()) } + assertThat(depot.etat.value.echec).isTrue() + assertThat(depot.etat.value.texte).isEqualTo("à garder") + } + + @Test + fun `a refused write keeps the text, to try again`() { + coEvery { notes.create(any(), any(), any(), any()) } throws IllegalStateException("base fermee") + depot.texte("à garder") + + depot.deposer() + + assertThat(depot.etat.value).isEqualTo(EtatDuDepot(texte = "à garder", echec = true)) + assertThat(depot.etat.value.peutDeposer).isTrue() + } + + @Test + fun `a title or a text beyond a note's limits is not taken`() { + depot.titre("t".repeat(NotesRepository.TITLE_MAX_LENGTH + 1)) + depot.texte("x".repeat(LongueurDUneNote.CARACTERES_MAX + 1)) + + assertThat(depot.etat.value.titre).isEmpty() + assertThat(depot.etat.value.texte).isEmpty() + } +} diff --git a/app/src/test/java/com/filestech/notes_tech/ui/raccourcis/RaccourcisTest.kt b/app/src/test/java/com/filestech/notes_tech/ui/raccourcis/RaccourcisTest.kt new file mode 100644 index 00000000..cbeabe18 --- /dev/null +++ b/app/src/test/java/com/filestech/notes_tech/ui/raccourcis/RaccourcisTest.kt @@ -0,0 +1,48 @@ +package com.filestech.notes_tech.ui.raccourcis + +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** The launcher shortcuts (D-041, 3.4.0): what an action asks, and how long the request waits. */ +@DisplayName("Raccourcis de l'icône") +class RaccourcisTest { + + private var maintenant = 1_000L + private val enAttente = RaccourcisEnAttente().also { it.horloge = { maintenant } } + + @Test + fun `each shortcut's action asks its request, and any other action asks nothing`() { + val nouvelleNote = RaccourciActivity.raccourciDe(RaccourciActivity.ACTION_NOUVELLE_NOTE) + assertThat(nouvelleNote).isEqualTo(Raccourci.NOUVELLE_NOTE) + assertThat(RaccourciActivity.raccourciDe(RaccourciActivity.ACTION_DICTEE)).isEqualTo(Raccourci.DICTEE) + assertThat(RaccourciActivity.raccourciDe("android.intent.action.MAIN")).isNull() + assertThat(RaccourciActivity.raccourciDe(null)).isNull() + } + + @Test + fun `a request is carried out once`() { + enAttente.deposer(Raccourci.DICTEE) + + assertThat(enAttente.prendre()).isEqualTo(Raccourci.DICTEE) + assertThat(enAttente.prendre()).isNull() + } + + @Test + fun `a request left waiting past its life does nothing at the next unlock`() { + enAttente.deposer(Raccourci.NOUVELLE_NOTE) + maintenant += RaccourcisEnAttente.DUREE_DE_VIE_MS + 1 + + assertThat(enAttente.prendre()).isNull() + // Taken all the same: nothing is left for a later unlock. + assertThat(enAttente.contenu.value).isNull() + } + + @Test + fun `a newer request replaces one not yet carried out`() { + enAttente.deposer(Raccourci.NOUVELLE_NOTE) + enAttente.deposer(Raccourci.DICTEE) + + assertThat(enAttente.prendre()).isEqualTo(Raccourci.DICTEE) + } +} diff --git a/app/src/test/java/com/filestech/notes_tech/ui/settings/CodeEpeleTest.kt b/app/src/test/java/com/filestech/notes_tech/ui/settings/CodeEpeleTest.kt index 75cdfa6b..a84a69aa 100644 --- a/app/src/test/java/com/filestech/notes_tech/ui/settings/CodeEpeleTest.kt +++ b/app/src/test/java/com/filestech/notes_tech/ui/settings/CodeEpeleTest.kt @@ -1,5 +1,6 @@ package com.filestech.notes_tech.ui.settings +import com.filestech.notes_tech.ui.common.codeEpele import com.google.common.truth.Truth.assertThat import org.junit.jupiter.api.Test diff --git a/app/src/test/java/com/filestech/notes_tech/ui/startup/StartupViewModelTest.kt b/app/src/test/java/com/filestech/notes_tech/ui/startup/StartupViewModelTest.kt index 5c316f25..72fe76ce 100644 --- a/app/src/test/java/com/filestech/notes_tech/ui/startup/StartupViewModelTest.kt +++ b/app/src/test/java/com/filestech/notes_tech/ui/startup/StartupViewModelTest.kt @@ -2,6 +2,7 @@ package com.filestech.notes_tech.ui.startup import com.filestech.notes_tech.data.local.DatabaseProvider import com.filestech.notes_tech.data.local.NotesDatabase +import com.filestech.notes_tech.data.repository.DureesDeVie import com.filestech.notes_tech.security.panic.PanicJournal import com.filestech.notes_tech.security.panic.PanicService import com.google.common.truth.Truth.assertThat @@ -27,6 +28,7 @@ class StartupViewModelTest { private val base = mockk() private val journal = mockk() + private val durees = mockk(relaxed = true) @BeforeEach fun setUp() { @@ -42,7 +44,7 @@ class StartupViewModelTest { fun pending_panic_is_resumed() { every { journal.isPending() } returns true - val demarrage = StartupViewModel(base, journal) + val demarrage = StartupViewModel(base, journal, durees) assertThat(demarrage.state.value).isEqualTo(StartupState.ResumingPanic) coVerify(exactly = 0) { base.get() } @@ -53,7 +55,7 @@ class StartupViewModelTest { fun no_pending_panic_opens_the_database() { every { journal.isPending() } returns false - val demarrage = StartupViewModel(base, journal) + val demarrage = StartupViewModel(base, journal, durees) assertThat(demarrage.state.value).isEqualTo(StartupState.Ready) coVerify(exactly = 1) { base.get() } diff --git a/audits/verifier-schema-room-vs-flutter.py b/audits/verifier-schema-room-vs-flutter.py index 9311707c..ca9d0d59 100644 --- a/audits/verifier-schema-room-vs-flutter.py +++ b/audits/verifier-schema-room-vs-flutter.py @@ -97,6 +97,10 @@ # point — and the instrumented migration test replays the real Kotlin object on a real base. MIGRATIONS = { 10: ["ALTER TABLE notes ADD COLUMN color_id INTEGER"], + 11: [ + "ALTER TABLE notes ADD COLUMN expires_at INTEGER", + "ALTER TABLE notes ADD COLUMN read_once INTEGER NOT NULL DEFAULT 0", + ], } SCHEMA_DIR = pathlib.Path(__file__).resolve().parent.parent / "app" / "schemas" diff --git a/docs/15-VERSION-3.4.md b/docs/15-VERSION-3.4.md new file mode 100644 index 00000000..332cef7c --- /dev/null +++ b/docs/15-VERSION-3.4.md @@ -0,0 +1,210 @@ +# Notes Tech 3.4.0 — dépôt à l'aveugle, notes à durée de vie, notes scellées à partager + +> Écrit le 2026-11-09 au soir. Origine : Patrice a demandé « des supers idées […] vraiment uniques », +> puis : « fais 2, 3, 4 ce serait génial pour la 3.4.0 + raccourcis et clavier sans apprentissage », +> « en intégrant les upgrade AGP aussi » (déjà dans `main` : PR #51). Ses quatre réponses du même soir +> sont les options recommandées ci-dessous. +> +> **Ce document est la spécification.** Il est écrit AVANT le code pour survivre à une compaction de la +> conversation ; chaque section dit ce qui est fait (✅) au fur et à mesure. + +## Faits relevés avant de concevoir (exploration du 2026-11-09) + +- **Le verrou de l'appli est une barrière d'interface**, pas un chiffrement : la base s'ouvre au + démarrage sans secret de l'utilisateur (`AppLockKeystore.kt:8-9`, `KeystoreSealedKekSource` sans + `setUserAuthenticationRequired`). `NotesRepository.create` ne consulte aucun verrou. Écrire une note + appli verrouillée est donc possible sans rien changer à la clé. +- **`MainActivity` ignore tout ce que porte un intent** (`sansNavigationImposee`, `MainActivity.kt:76-79`) : + elle est exportée, aucune autre appli ne doit pouvoir y imposer une navigation. Le partage reçu passe + par une activité à part qui dépose en mémoire (`PartagesEnAttente`, 2 min) puis lance l'appli ; rien + n'est écrit tant que l'appli est verrouillée. +- **La sauvegarde chiffrée** (`FichierDeSauvegarde`) : en-tête de 48 octets (magie, version, Argon2id + t=3 m=64 Mio p=1, sel 16, nonce 12) servant d'AAD, AES-256-GCM en flux, code généré de 24 signes + Crockford (120 bits, `SecretDeSauvegarde`). Constantes privées câblées : à paramétrer pour un autre + conteneur. +- **Réglages** dans `FlutterSharedPreferences` (`LegacyPreferences`) ; la panique les efface tous sauf + trois clés (`PanicService.kt:691`) — un nouveau réglage y revient à sa valeur par défaut. +- **`IME_FLAG_NO_PERSONALIZED_LEARNING`** n'est posé que pour les notes de coffre (`SaisieDeCoffre`, + `ChampDuCorps:486`), couplé au presse-papiers sensible et au glisser. +- **Room** : version 10, une seule migration additive (`MIGRATION_9_10`, `color_id`), schémas exportés, + rejouée par `audits/verifier-schema-room-vs-flutter.py`. Le trigger `notes_au` réécrit la ligne FTS à + tout UPDATE. +- **Aucun** raccourci, tuile, widget, ni destination « nouvelle note » ; la dictée ne démarre que d'un + toucher sur le micro de l'éditeur. + +## D-040 — Clavier sans apprentissage, pour toute l'appli, activé par défaut + +- Réglage booléen `keyboard_no_learning` (défaut **vrai**, Patrice) : tous les champs de Notes Tech + demandent au clavier `IME_FLAG_NO_PERSONALIZED_LEARNING`. La panique le remet à vrai (défaut). +- **Les champs de secret l'ont toujours**, quel que soit le réglage (phrases de coffre et de sauvegarde, + code d'une note scellée). +- Découplé de « protégée » : le presse-papiers sensible et le glisser restent réservés aux notes de coffre. +- Honnêteté : c'est une DEMANDE au clavier ; Gboard et SwiftKey la respectent, un clavier peut l'ignorer. + Les suggestions restent ; seul l'apprentissage est coupé. + +## D-041 — Raccourcis sur l'icône : Nouvelle note, Dictée, Dépôt rapide + +- Raccourcis **dynamiques**, publiés par `MainActivity.onCreate` (`publierLesRaccourcis`), aucune + permission. Pas de `shortcuts.xml` : un raccourci statique écrit son paquet en dur, et l'appli est + construite sous plusieurs identifiants (release, `.next`, `.next.debug`). Publiés avec le contexte de + l'activité : libellés dans la langue choisie dans l'appli ; une appli au premier plan n'est jamais + limitée par le service des raccourcis. +- « Nouvelle note » et « Dictée » visent une activité **non exportée** de transit qui dépose la demande en + mémoire puis lance l'appli : `MainActivity` continue d'ignorer les intents, aucune autre appli ne peut + imposer une création ou un micro. La demande s'exécute **après** le déverrouillage de l'appli. +- « Dictée » crée une note et ouvre le micro comme un toucher sur le bouton : sans modèle → écran + d'installation ; sans permission → demande de permission. +- « Dépôt rapide » ouvre le dépôt à l'aveugle (D-042). + +## D-042 — Dépôt à l'aveugle : écrire sans déverrouiller, sans rien pouvoir relire + +- Une activité à part, en écriture seule : un champ de titre, un champ de texte, « Déposer », « Annuler ». + Elle ne lit **rien** de la base, n'affiche aucune note, aucun dossier, aucun nombre ; après « Déposer », + l'écran se referme (« Note déposée dans la Boîte de réception »). **Aucun lien** vers le reste de + l'appli : un « Ouvrir Notes Tech » prévu au départ n'a pas été fait, une porte de moins. +- Elle peut être la PREMIÈRE activité de son processus (la tuile la démarre dans un processus neuf) : + elle pose elle-même ce que `MainActivity` pose d'ordinaire — fenêtre protégée, actions de texte des + autres applis et classifieur coupés, langue choisie, thème, clavier sans apprentissage. +- Rien n'est écrit pendant une panique en cours (`PanicJournal.isPending`) : le texte reste à l'écran. +- Ouverte par la **tuile des réglages rapides** et le **raccourci** « Dépôt rapide ». Si le TÉLÉPHONE est + verrouillé, Android demande d'abord son déverrouillage (`unlockAndRun`) — seul le verrou de Notes Tech + est contourné (Patrice). +- La note va dans la **Boîte de réception**, jamais dans un coffre ; fenêtre protégée (pas de capture, + pas d'aperçu dans les applis récentes) ; clavier sans apprentissage selon D-040. +- Pas de brouillon gardé hors de l'écran : quitter sans déposer ne laisse rien (confirmation si du texte + a été tapé). + +## D-043 — Note à durée de vie + +- Durées proposées : **1 h, 24 h, 7 jours, 30 jours, après lecture** (Patrice). Menu ⋮ de l'éditeur, + « Durée de vie… » ; « Aucune » la retire. +- « Après lecture » : la note est effacée quand on la **referme après l'avoir rouverte** (la session où + l'option est posée ne compte pas). +- L'effacement est un effacement définitif (même chemin que « Supprimer définitivement » : index plein + texte et liens compris, `secure_delete`). Il a lieu **à l'ouverture de l'appli et au retour sur + l'accueil**, pas à la seconde près (pas d'alarme, pas de permission) — dit tel quel dans l'interface. +- **Hors sauvegardes et exports** (Patrice) : une restauration ne doit pas la faire revivre. +- Colonnes `expires_at INTEGER` (nullable) et `read_once INTEGER NOT NULL DEFAULT 0` : migration 10 → 11, + additive. +- La carte montre la durée restante ; une note de coffre fermé garde sa durée de vie (la date n'est pas + un secret : elle n'est pas chiffrée, comme `updated_at`). + +## D-044 — Note scellée à partager (`.ntnote`) + +- Menu ⋮ de l'éditeur, « Partager scellée… » : la note (titre, texte, couleur, étiquettes) est chiffrée + dans un fichier `.ntnote` par un **code généré de 24 signes** (même code que la sauvegarde), affiché + une fois ; on envoie le fichier par n'importe quelle messagerie et le code par un autre canal. +- Même chiffrement que la sauvegarde (Argon2id, AES-256-GCM, en-tête de 48 octets en AAD), avec sa + propre magie (`NTSEALED`, 8 octets comme `NTBACKUP`) et sa propre borne : **4 Mio** (et non 1 Mio comme + écrit d'abord : une note peut faire 500 000 caractères, jusqu'à 1,5 Mo en UTF-8, plus le JSON). +- **Réalisation** (plan écrit avant le code, 2026-11-09 au soir) : + - `FichierDeSauvegarde` reçoit la magie et la borne en PARAMÈTRES, avec les valeurs de la sauvegarde par + défaut : la sauvegarde ne change pas d'un octet, ses tests restent tels quels. + - `data/scellee/FichierDeNoteScellee` : `sceller(note, secret): ByteArray` et `ouvrir(octets, secret)`, + en mémoire (une note est petite) ; JSON `android.util.JsonWriter/Reader` comme la sauvegarde, + `{"format":"notes-tech-sealed-note","version":1,"title","content","color","tags","readOnce"}` ; + limites d'une note à l'ouverture (titre 200, texte 500 000) ; tests instrumentés. + - Code : `SecretDeSauvegarde.nouveauCode()` (24 signes, 120 bits) et sa saisie tolérante. + - Envoi : menu ⋮ de l'éditeur « Partager scellée… » → feuille (explication, interrupteur « Lecture + unique chez le destinataire », « Créer ») → scellement hors du fil principal (Argon2 ≈ 1 s) → fichier + dans `cache/exports//note-scellee-AAAAMMJJ.ntnote` (purgé au démarrage et par la panique, + servi par le `FileProvider` des exports) → le code s'affiche, fenêtre protégée, puis « Partager le + fichier » (`ui/common/Partage.kt`). + - Réception : `OuvrirUneNoteScelleeActivity`, exportée, `ACTION_VIEW` sur `application/octet-stream`, + `content://` d'une autre appli seulement (même garde que le partage reçu), lecture bornée (4 Mio, + délai), magie vérifiée — un autre fichier : un message bref et rien d'autre. Sinon dépôt en mémoire + (`DepotEnMemoire`) puis ouverture de l'appli ; menu ⋮ de l'accueil « Ouvrir une note scellée » → + sélecteur SAF → même dépôt. Après le déverrouillage, un dialogue demande le code ; code juste → note + créée dans la Boîte de réception (jamais un coffre), couleur et étiquettes reprises, « après lecture » + si demandé, puis l'éditeur s'ouvre ; code faux ou fichier abîmé → même message, on peut réessayer. +- **Le nom du fichier ne dit rien de la note** (`note-scellee-AAAAMMJJ.ntnote`) : un titre dans un nom de + fichier fuirait par la messagerie. +- Option « Lecture unique chez le destinataire » : la note reçue y arrive avec la durée de vie « après + lecture » (D-043). Honnête : le destinataire peut garder le fichier. +- Réception (Patrice) : « Ouvrir avec » (`ACTION_VIEW` sur `application/octet-stream`, activité à part, + comme le partage reçu) et menu « Ouvrir une note scellée » de l'accueil. Un fichier qui n'est pas une + note scellée est refusé poliment. Le code se tape **après** le déverrouillage ; la note s'affiche, puis + « Ajouter » la met dans la Boîte de réception, jamais dans un coffre. +- Une note de coffre se partage scellée seulement coffre ouvert, comme l'export. + +## État + +> **Audits et relectures (2026-11-09)** — tous appliqués avant la PR, sauf ce que « Limites connues » dit. +> - **Relecture externe GPT** (0,024 $) : 3 défauts corrigés — note reçue créée en UN insert (couleur et +> « après lecture » compris), lecture d'un fichier scellé bornée (une à la fois, fil dédié, flux FERMÉ +> passé le délai, lecture de 0 octet refusée, toute `RuntimeException` d'un fournisseur rattrapée). +> - **Cohérence** (82/100) : C1-C7, C9, C10 appliqués (code oublié à toute sortie de la feuille, rappel du +> réglage clavier sans défaut, sélecteur dans son propre ViewModel, `purgerSansEchec`, purge sous +> `repeatOnLifecycle(STARTED)`, KDoc replacés, « … » retirés) ; C8 (idiomes d'événement) laissé. +> - **Traductions** : aucun grave ; 3 moyens corrigés (it, de, journaux de-DE). +> - **3 axes** : 0 critique, 0 élevé, 7 moyens. M3 déjà corrigé par la relecture externe ; **M4** un second +> fichier scellé attend la fin du premier ; **M5** `FLAG_SECURE` dès la première image sur les dialogues +> d'un code (les deux scellés ET les trois de la sauvegarde, même défaut : `ProprietesDeDialogueSecret`) ; +> **M7** étiquettes et titre d'un fichier fabriqué normalisés comme à l'import ; **M1/M2** l'horloge du +> téléphone et le vrai moment de l'effacement « après lecture » dits dans la feuille et SECURITY. +> - **Tests** : `NotesScelleesRecuesTest` (3 : faux puis bon code → UNE note avec couleur, étiquettes, +> lecture unique ; pas de doublon ; le second fichier attend), un fichier fabriqué dans +> `FichierDeNoteScelleeTest`, `ReglagesTest` avec la ligne du clavier (2 échecs de la suite complète : +> le nouvel interrupteur, « Jamais » descendu sous l'écran). 106 cas des classes touchées verts sur le S9. +> +> **Limites connues (non corrigées, consignées)** : +> - **M6** : la tuile (non « active ») fait lier le service, donc démarrer le processus, à chaque ouverture +> du volet où elle est visible ; le démarrage purge `cache/exports`. Un `.ntnote` (ou un export) partagé +> vers une appli qui le lirait plus tard, Notes Tech tuée entre-temps, peut disparaître. Rare, sans perte +> (l'envoi échoue, on repartage). Non mesuré : One UI 2 (S9) refuse `cmd statusbar add-tile`. Pistes : +> `ACTIVE_TILE` (état affiché à vérifier selon les versions) ou purge au premier écran plutôt qu'au +> démarrage du processus (touche la purge du clair : à peser). +> - Mes fichiers (Samsung) n'ouvre pas `.ntnote` (type vide, il conclut seul) ; le menu « Ouvrir une note +> scellée » marche partout. +> - Le partage reçu (3.2.0) lit encore sans délai qui ferme le flux : à durcir plus tard. +> - Faibles de l'audit laissés : `DepotActivity` garde un brouillon en mémoire après Home (voulu : copier +> un numéro ailleurs puis revenir) ; collage de plus de 500 000 caractères ignoré sans message ; l'écran +> d'export ne dit pas que les notes à durée de vie en sont exclues ; pas d'index partiel pour la purge. + +- [x] **D-040 clavier** (`5e18eab`) — tests instrumentés sur le S9 : `ClavierSansApprentissageTest` (3), + `ChampDuCorpsTest` +1, `ReglagesHeritesTest` +1, `SaisieDeCoffreTest` inchangé (8) ; la ligne du réglage + vue dans l'écran, section Sécurité. +- [x] **D-041 raccourcis** (`5e18eab`) — `RaccourcisTest` (4, JVM). Sur le S9 (lanceur Samsung) : les trois + raccourcis s'affichent à l'appui long, le lanceur démarre bien l'activité NON exportée (aucune + `SecurityException`), « Nouvelle note » ouvre une note vierge côté écriture, « Dicter une note » sans + modèle ouvre l'écran d'installation. Comme le bouton « Nouvelle note » depuis la 2.0.9, une note vide + quittée reste (pas d'effacement des notes vides : comportement inchangé). +- [x] **D-042 dépôt** (`5e18eab`) — `DepotViewModelTest` (5, JVM : Boîte de réception, `confirmVerified` + — rien d'autre n'est appelé —, panique en cours, échec qui garde le texte, limites). Sur le S9, **avec + le verrou de l'appli activé** (code posé pour l'essai) : dépôt par le raccourci sans code, l'appli + demande toujours son code à l'ouverture, la note est dans la Boîte de réception ; la tuile + (`cmd statusbar add-tile` / `click-tile`) ouvre le dépôt. **Non essayé sur appareil** : la tuile + téléphone verrouillé (`unlockAndRun`) — le S9 n'a pas de code d'écran, en poser un change un réglage + global. +- [x] **D-043 durée de vie** — données (`3ac16da`) : schéma 11, migration 10 → 11 additive, vérifiée par le + script de CI et sur le S9 (`MigrationVersDixTest`, dont le chemin réel d'une base de la 3.3.0) ; purge = + le DELETE de « Supprimer définitivement » ; export ZIP et sauvegarde les excluent. Interface : menu ⋮ de + l'éditeur « Durée de vie… », feuille à boutons radio, badge et phrase TalkBack sur la carte (aussi sur + une note de coffre fermé : la durée n'est pas un secret), purge à l'ouverture (`StartupViewModel`) et + chaque minute sur la liste (`HomeRoute`). « Après lecture » efface à `onCleared` de l'éditeur, pas à la + disposition de l'écran (une rotation l'aurait effacée), après le dernier enregistrement ; annoncé à + l'ouverture. detekt a fait sortir la durée de vie de `NotesRepository` (`DureesDeVie`, seuil de + fonctions) et regrouper couleur + durée de vie en `FeuillesDeLaFiche` (seuil de paramètres de l'écran). + Tests : `ResteDeVieTest` (5) et `DureesDeVieTest` (5) sur la JVM ; sur le S9 `LectureUniqueTest` (3 : + effacée à la fermeture ; contrôle, choisie dans la session ; témoin sans durée), `EditeurTest` (36), + `RechercheDansLaNoteEcranTest` (5), `FermetureDuCoffreTest` (9), `LiensDansUnCoffreTest` (3), + `GestesSurDesNotesTest` (14), `AccueilTest` (20) — 0 échec. +- [x] **D-044 scellée** — format (`e1c18e9`) : `FichierDeSauvegarde` paramétré (magie, borne ; la + sauvegarde inchangée, ses tests aussi), `FichierDeNoteScellee` (`NTSEALED`, 4 Mio, JSON, seul un code + ouvre). Envoi et réception (`9464509`) : feuille « Partager scellée… » (fichier `note-AAAAMMJJ.ntnote` + — le nom ne dit rien de la note —, code affiché une fois, copie par le presse-papiers sensible), + activité « Ouvrir avec » exportée et menu « Ouvrir une note scellée », dépôt en mémoire, code demandé + une fois l'appli déverrouillée. L'affichage du code passe dans `ui/common/CodeAffiche`, partagé avec la + sauvegarde. Tests : `FichierDeNoteScelleeTest` (6, S9), `LectureScelleeTest` (4, JVM : un autre fichier + est abandonné à ses premiers octets). **De bout en bout sur le S9** : note créée, scellée en lecture + unique (201 octets, `NTSEALED`, le texte absent du fichier), poussée dans Téléchargements, ouverte par + le menu de l'accueil ; un faux code refusé (« Code incorrect, ou fichier abîmé ») ; le bon, tapé en + minuscules avec des espaces, ouvre la note dans la Boîte de réception avec l'annonce de la lecture + unique ; refermée, elle s'efface — seule l'originale reste. Puis « Durée de vie… » → « 1 heure » sur + l'originale : message « Durée de vie : 1 heure », badge « 1 h » sur la carte. **Non essayé sur + appareil** : « Ouvrir avec » depuis une messagerie (l'activité exportée) — à faire avec une vraie + messagerie, ou au moins un gestionnaire de fichiers. +- [x] Textes 5 langues · [x] version 3.4.0 (507) · [x] journaux 5071-5073 · [x] README, SECURITY, confidentialité +- [x] Audits (3 axes, cohérence, i18n) · [x] relecture externe · [x] S9 (suite complète puis classes touchées) · [ ] CI +- [ ] Avant le tag : build F-Droid sous AGP 9.5 — au 2026-11-09 au soir, F-Droid n'a construit ni SMS 1.30.0 + (pas de journal `_307`) ni Notes 3.3.0 : aucune preuve encore ; essai de mise à jour depuis la 3.3.0 sur le S9 diff --git a/fastlane/metadata/android/de-DE/changelogs/5071.txt b/fastlane/metadata/android/de-DE/changelogs/5071.txt new file mode 100644 index 00000000..75adc174 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/5071.txt @@ -0,0 +1 @@ +Neu: Notizen, die sich selbst löschen — nach 1 Std., 24 Std., 7 oder 30 Tagen oder nach dem Lesen (⋮ › Lebensdauer). Versiegelt teilen: eine verschlüsselte Datei, die nur ihr Code öffnet, über jede App gesendet (⋮ › Versiegelt teilen). Schnellablage: eine Notiz schreiben, ohne die App zu entsperren — per Kachel der Schnelleinstellungen oder langem Druck auf das Symbol (auch Neue Notiz, Diktieren). Die Tastatur wird gebeten, nichts zu lernen. diff --git a/fastlane/metadata/android/de-DE/changelogs/5072.txt b/fastlane/metadata/android/de-DE/changelogs/5072.txt new file mode 100644 index 00000000..75adc174 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/5072.txt @@ -0,0 +1 @@ +Neu: Notizen, die sich selbst löschen — nach 1 Std., 24 Std., 7 oder 30 Tagen oder nach dem Lesen (⋮ › Lebensdauer). Versiegelt teilen: eine verschlüsselte Datei, die nur ihr Code öffnet, über jede App gesendet (⋮ › Versiegelt teilen). Schnellablage: eine Notiz schreiben, ohne die App zu entsperren — per Kachel der Schnelleinstellungen oder langem Druck auf das Symbol (auch Neue Notiz, Diktieren). Die Tastatur wird gebeten, nichts zu lernen. diff --git a/fastlane/metadata/android/de-DE/changelogs/5073.txt b/fastlane/metadata/android/de-DE/changelogs/5073.txt new file mode 100644 index 00000000..75adc174 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/5073.txt @@ -0,0 +1 @@ +Neu: Notizen, die sich selbst löschen — nach 1 Std., 24 Std., 7 oder 30 Tagen oder nach dem Lesen (⋮ › Lebensdauer). Versiegelt teilen: eine verschlüsselte Datei, die nur ihr Code öffnet, über jede App gesendet (⋮ › Versiegelt teilen). Schnellablage: eine Notiz schreiben, ohne die App zu entsperren — per Kachel der Schnelleinstellungen oder langem Druck auf das Symbol (auch Neue Notiz, Diktieren). Die Tastatur wird gebeten, nichts zu lernen. diff --git a/fastlane/metadata/android/de-DE/full_description.txt b/fastlane/metadata/android/de-DE/full_description.txt index 39ab105e..20188245 100644 --- a/fastlane/metadata/android/de-DE/full_description.txt +++ b/fastlane/metadata/android/de-DE/full_description.txt @@ -22,6 +22,10 @@ Notes Tech: Markdown-Notizen, 100 % lokal und im Ruhezustand verschlüsselt. In • Notizen öffnen sich zum Lesen (Markdown: Überschriften, Listen, Links); „Bearbeiten“ zum Schreiben, automatisches Speichern • Langes Drücken wählt Notizen aus: archivieren, verschieben, in den Papierkorb mit Rückgängig, anheften, einfärben oder löschen, alle auf einmal • Ein Archiv für die Notizen, die Sie aus Ihren Listen heraushalten +• Notizen, die sich selbst löschen: nach 1 Stunde, 24 Stunden, 7 oder 30 Tagen oder nach dem Lesen +• Versiegelt teilen: eine verschlüsselte Datei, die nur ihr Code öffnet, über jede App gesendet +• Schnellablage: eine Notiz schreiben, ohne die App zu entsperren, per Schnelleinstellungs-Kachel oder Verknüpfungen des Symbols +• Die Tastatur wird gebeten, nichts von dem zu lernen, was Sie tippen • Eine Pastellfarbe pro Notiz in den Listen (bei einer Tresornotiz nur bei entsperrtem Tresor) • Rückverweise [[Titel]] mit Autovervollständigung und Bereich „Notizen, die hierher verlinken“ • Sofortige FTS5-Suche, Suche in der Notiz mit hervorgehobenen Treffern, einstellbare Sortierung diff --git a/fastlane/metadata/android/en-US/changelogs/5071.txt b/fastlane/metadata/android/en-US/changelogs/5071.txt new file mode 100644 index 00000000..f4d4ccdd --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/5071.txt @@ -0,0 +1 @@ +New: notes that erase themselves — in 1 h, 24 h, 7 or 30 days, or after reading (⋮ › Lifetime). Share a note sealed: an encrypted file that only its code opens, sent by any app (⋮ › Share sealed). Quick drop: write a note without unlocking the app, from a quick settings tile or a long press on the icon, which also offers New note and Dictate. The keyboard is asked not to learn what you type (Settings). diff --git a/fastlane/metadata/android/en-US/changelogs/5072.txt b/fastlane/metadata/android/en-US/changelogs/5072.txt new file mode 100644 index 00000000..f4d4ccdd --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/5072.txt @@ -0,0 +1 @@ +New: notes that erase themselves — in 1 h, 24 h, 7 or 30 days, or after reading (⋮ › Lifetime). Share a note sealed: an encrypted file that only its code opens, sent by any app (⋮ › Share sealed). Quick drop: write a note without unlocking the app, from a quick settings tile or a long press on the icon, which also offers New note and Dictate. The keyboard is asked not to learn what you type (Settings). diff --git a/fastlane/metadata/android/en-US/changelogs/5073.txt b/fastlane/metadata/android/en-US/changelogs/5073.txt new file mode 100644 index 00000000..f4d4ccdd --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/5073.txt @@ -0,0 +1 @@ +New: notes that erase themselves — in 1 h, 24 h, 7 or 30 days, or after reading (⋮ › Lifetime). Share a note sealed: an encrypted file that only its code opens, sent by any app (⋮ › Share sealed). Quick drop: write a note without unlocking the app, from a quick settings tile or a long press on the icon, which also offers New note and Dictate. The keyboard is asked not to learn what you type (Settings). diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 9a1e9f3b..1e1e3226 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -22,6 +22,10 @@ Notes Tech: 100% local Markdown note-taking, encrypted at rest. Written in Kotli • Notes open to be read (Markdown: headings, lists, links); “Edit” to write, autosave • Long press to select notes, then archive, move, trash with Undo, pin, color or delete them at once • An Archive for the notes you keep out of your lists +• Notes that erase themselves: after 1 hour, 24 hours, 7 or 30 days, or after reading +• Share a note sealed: an encrypted file that only its code opens, sent by any app +• Quick drop: write a note without unlocking the app, from a quick settings tile or the icon's shortcuts +• The keyboard is asked not to learn what you type • A pastel colour per note in the lists (a vault note's only while its vault is open) • [[Title]] backlinks with autocomplete + "notes linking here" panel • Instant FTS5 search, find in note with highlighted matches, configurable sort diff --git a/fastlane/metadata/android/es-ES/changelogs/5071.txt b/fastlane/metadata/android/es-ES/changelogs/5071.txt new file mode 100644 index 00000000..80658d2d --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/5071.txt @@ -0,0 +1 @@ +Nuevo: notas que se borran solas — en 1 h, 24 h, 7 o 30 días, o tras la lectura (⋮ › Duración). Compartir una nota sellada: un archivo cifrado que solo su código abre, enviado con cualquier app. Depósito rápido: escribir una nota sin desbloquear la app, desde un mosaico de los ajustes rápidos o una pulsación larga en el icono (también Nueva nota, Dictar). Se pide al teclado que no aprenda nada (Ajustes). diff --git a/fastlane/metadata/android/es-ES/changelogs/5072.txt b/fastlane/metadata/android/es-ES/changelogs/5072.txt new file mode 100644 index 00000000..80658d2d --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/5072.txt @@ -0,0 +1 @@ +Nuevo: notas que se borran solas — en 1 h, 24 h, 7 o 30 días, o tras la lectura (⋮ › Duración). Compartir una nota sellada: un archivo cifrado que solo su código abre, enviado con cualquier app. Depósito rápido: escribir una nota sin desbloquear la app, desde un mosaico de los ajustes rápidos o una pulsación larga en el icono (también Nueva nota, Dictar). Se pide al teclado que no aprenda nada (Ajustes). diff --git a/fastlane/metadata/android/es-ES/changelogs/5073.txt b/fastlane/metadata/android/es-ES/changelogs/5073.txt new file mode 100644 index 00000000..80658d2d --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/5073.txt @@ -0,0 +1 @@ +Nuevo: notas que se borran solas — en 1 h, 24 h, 7 o 30 días, o tras la lectura (⋮ › Duración). Compartir una nota sellada: un archivo cifrado que solo su código abre, enviado con cualquier app. Depósito rápido: escribir una nota sin desbloquear la app, desde un mosaico de los ajustes rápidos o una pulsación larga en el icono (también Nueva nota, Dictar). Se pide al teclado que no aprenda nada (Ajustes). diff --git a/fastlane/metadata/android/es-ES/full_description.txt b/fastlane/metadata/android/es-ES/full_description.txt index 7a5b9d12..3db51049 100644 --- a/fastlane/metadata/android/es-ES/full_description.txt +++ b/fastlane/metadata/android/es-ES/full_description.txt @@ -22,6 +22,10 @@ Notes Tech: notas Markdown 100 % locales, cifradas en reposo. Escrita en Kotlin. • Las notas se abren para leer (Markdown: títulos, listas, enlaces); «Editar» para escribir, guardado automático • Pulsación larga para seleccionar notas y luego archivarlas, moverlas, enviarlas a la papelera con Deshacer, anclarlas, colorearlas o eliminarlas a la vez • Un Archivo para las notas que mantiene fuera de sus listas +• Notas que se borran solas: tras 1 hora, 24 horas, 7 o 30 días, o tras la lectura +• Compartir una nota sellada: un archivo cifrado que solo su código abre, enviado con cualquier app +• Depósito rápido: escribir una nota sin desbloquear la app, desde un mosaico de los ajustes rápidos o los accesos directos del icono +• Se pide al teclado que no aprenda nada de lo que escribe • Un color pastel por nota en las listas (el de una nota de caja fuerte, solo desbloqueada) • Retroenlaces [[Título]] con autocompletado y panel «notas que enlazan aquí» • Búsqueda FTS5 instantánea, búsqueda en la nota con coincidencias resaltadas, orden configurable diff --git a/fastlane/metadata/android/fr-FR/changelogs/5071.txt b/fastlane/metadata/android/fr-FR/changelogs/5071.txt new file mode 100644 index 00000000..4a64b4f7 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/5071.txt @@ -0,0 +1 @@ +Nouveau : des notes qui s'effacent seules — dans 1 h, 24 h, 7 ou 30 jours, ou après lecture (⋮ › Durée de vie). Partager une note scellée : un fichier chiffré que seul son code ouvre, envoyé par n'importe quelle appli. Dépôt rapide : écrire une note sans déverrouiller l'appli, depuis une tuile des réglages rapides ou un appui long sur l'icône (aussi Nouvelle note, Dicter). Le clavier est prié de ne rien apprendre (Réglages). diff --git a/fastlane/metadata/android/fr-FR/changelogs/5072.txt b/fastlane/metadata/android/fr-FR/changelogs/5072.txt new file mode 100644 index 00000000..4a64b4f7 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/5072.txt @@ -0,0 +1 @@ +Nouveau : des notes qui s'effacent seules — dans 1 h, 24 h, 7 ou 30 jours, ou après lecture (⋮ › Durée de vie). Partager une note scellée : un fichier chiffré que seul son code ouvre, envoyé par n'importe quelle appli. Dépôt rapide : écrire une note sans déverrouiller l'appli, depuis une tuile des réglages rapides ou un appui long sur l'icône (aussi Nouvelle note, Dicter). Le clavier est prié de ne rien apprendre (Réglages). diff --git a/fastlane/metadata/android/fr-FR/changelogs/5073.txt b/fastlane/metadata/android/fr-FR/changelogs/5073.txt new file mode 100644 index 00000000..4a64b4f7 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/5073.txt @@ -0,0 +1 @@ +Nouveau : des notes qui s'effacent seules — dans 1 h, 24 h, 7 ou 30 jours, ou après lecture (⋮ › Durée de vie). Partager une note scellée : un fichier chiffré que seul son code ouvre, envoyé par n'importe quelle appli. Dépôt rapide : écrire une note sans déverrouiller l'appli, depuis une tuile des réglages rapides ou un appui long sur l'icône (aussi Nouvelle note, Dicter). Le clavier est prié de ne rien apprendre (Réglages). diff --git a/fastlane/metadata/android/fr-FR/full_description.txt b/fastlane/metadata/android/fr-FR/full_description.txt index 8b2940a2..9f7876f9 100644 --- a/fastlane/metadata/android/fr-FR/full_description.txt +++ b/fastlane/metadata/android/fr-FR/full_description.txt @@ -22,6 +22,10 @@ Notes Tech : prise de notes Markdown 100 % locale, chiffrée au repos. Écrite e • Les notes s'ouvrent en lecture (Markdown : titres, listes, liens) ; « Éditer » pour écrire, enregistrement automatique • Appui long pour sélectionner des notes, puis les archiver, déplacer, mettre à la corbeille avec Annuler, épingler, colorer ou supprimer d'un coup • Des Archives pour les notes que vous gardez hors de vos listes +• Des notes qui s'effacent seules : après 1 heure, 24 heures, 7 ou 30 jours, ou après lecture +• Partager une note scellée : un fichier chiffré que seul son code ouvre, envoyé par n'importe quelle appli +• Dépôt rapide : écrire une note sans déverrouiller l'appli, depuis une tuile des réglages rapides ou les raccourcis de l'icône +• Le clavier est prié de ne rien apprendre de ce que vous tapez • Une couleur pastel par note dans les listes (celle d'une note de coffre, seulement coffre ouvert) • Rétroliens [[Titre]] avec autocomplétion + panneau « notes qui mentionnent celle-ci » • Recherche FTS5 instantanée, recherche dans la note avec occurrences surlignées, tri réglable diff --git a/fastlane/metadata/android/it-IT/changelogs/5071.txt b/fastlane/metadata/android/it-IT/changelogs/5071.txt new file mode 100644 index 00000000..c50832b1 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/5071.txt @@ -0,0 +1 @@ +Novità: note che si cancellano da sole — tra 1 h, 24 h, 7 o 30 giorni, o dopo la lettura (⋮ › Durata). Condividere una nota sigillata: un file cifrato che solo il suo codice apre, inviato con qualsiasi app. Deposito rapido: scrivere una nota senza sbloccare l'app, da un riquadro delle impostazioni rapide o tenendo premuta l'icona (anche Nuova nota, Dettare). Alla tastiera si chiede di non imparare nulla (Impostazioni). diff --git a/fastlane/metadata/android/it-IT/changelogs/5072.txt b/fastlane/metadata/android/it-IT/changelogs/5072.txt new file mode 100644 index 00000000..c50832b1 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/5072.txt @@ -0,0 +1 @@ +Novità: note che si cancellano da sole — tra 1 h, 24 h, 7 o 30 giorni, o dopo la lettura (⋮ › Durata). Condividere una nota sigillata: un file cifrato che solo il suo codice apre, inviato con qualsiasi app. Deposito rapido: scrivere una nota senza sbloccare l'app, da un riquadro delle impostazioni rapide o tenendo premuta l'icona (anche Nuova nota, Dettare). Alla tastiera si chiede di non imparare nulla (Impostazioni). diff --git a/fastlane/metadata/android/it-IT/changelogs/5073.txt b/fastlane/metadata/android/it-IT/changelogs/5073.txt new file mode 100644 index 00000000..c50832b1 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/5073.txt @@ -0,0 +1 @@ +Novità: note che si cancellano da sole — tra 1 h, 24 h, 7 o 30 giorni, o dopo la lettura (⋮ › Durata). Condividere una nota sigillata: un file cifrato che solo il suo codice apre, inviato con qualsiasi app. Deposito rapido: scrivere una nota senza sbloccare l'app, da un riquadro delle impostazioni rapide o tenendo premuta l'icona (anche Nuova nota, Dettare). Alla tastiera si chiede di non imparare nulla (Impostazioni). diff --git a/fastlane/metadata/android/it-IT/full_description.txt b/fastlane/metadata/android/it-IT/full_description.txt index 25de0eed..d564c40f 100644 --- a/fastlane/metadata/android/it-IT/full_description.txt +++ b/fastlane/metadata/android/it-IT/full_description.txt @@ -22,6 +22,10 @@ Notes Tech: note Markdown 100% locali, cifrate a riposo. Scritta in Kotlin. • Le note si aprono in lettura (Markdown: titoli, elenchi, link); «Modifica» per scrivere, salvataggio automatico • Pressione prolungata per selezionare le note, poi archiviarle, spostarle, metterle nel cestino con Annulla, fissarle, colorarle o eliminarle tutte insieme • Un Archivio per le note che tiene fuori dai suoi elenchi +• Note che si cancellano da sole: dopo 1 ora, 24 ore, 7 o 30 giorni, o dopo la lettura +• Condividere una nota sigillata: un file cifrato che solo il suo codice apre, inviato con qualsiasi app +• Deposito rapido: scrivere una nota senza sbloccare l'app, da un riquadro delle impostazioni rapide o dalle scorciatoie dell'icona +• Alla tastiera si chiede di non imparare nulla di ciò che digita • Un colore pastello per nota negli elenchi (per una nota in cassaforte, solo a cassaforte sbloccata) • Collegamenti inversi [[Titolo]] con completamento automatico e pannello «note che rimandano qui» • Ricerca FTS5 istantanea, ricerca nella nota con occorrenze evidenziate, ordinamento regolabile diff --git a/version.properties b/version.properties index b3337197..b20ef782 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=506 -versionName=3.3.0 +versionCode=507 +versionName=3.4.0