Conversation
My Notes gets a successor, Encly: an encrypted, offline notes app. A new "Meet Encly" page (drawer, next to the what's-new entry, plus a one-time dialog after the update) explains what Encly is and what does not move. With a trusted Encly installed, "Move my notes to Encly" writes a ZIP with handoff.json (format mynotes-handoff, schema 1) to the cache and hands it to com.pasich.encly.action.IMPORT_FROM_MY_NOTES through the FileProvider with a read grant. Before any plaintext leaves, the receiver must resolve the action and carry the pinned Encly signing certificate; API 26-27 fall back to GET_SIGNATURES with exactly one signer. The payload is raw My Notes data with stable ids from sync_metadata (or mynotes:<type>:<id>); attachment files, reminders and pins are not sent, and the page counts what stays behind. The ZIP is deleted after the result, and leftovers are swept on the next start. Nothing is deleted automatically. After Encly confirms the move, "Clear My Notes" is offered as a separate step with its own confirmation; it cancels reminders, deletes the records with sync tombstones so a Drive copy follows, and removes attachment files. Refs: #167
After a Google Drive sync the main screen could go blank or show an empty category until the app was restarted, and the list animations often froze or drew one card on top of another. The selected category was a snapshot filtered by name, so a tag renamed, merged or deleted by sync emptied the page. The presenter now re-resolves the selection against every fresh tags list (by id, then by name, else All notes), stores the result and marks the chip from it. Tags and notes emissions are folded with a 50 ms debounce, and the view state compares by content, so a sync that rewrites rows without changing them no longer re-renders the list. The list, the empty state and the tags row are now driven toward a target state that cancels whatever animation is running, so an empty-then-filled update inside the hide animation can no longer leave the list invisible. States that arrive in selection mode are held and applied when it ends (dropping selected notes that sync removed), and states that arrive right after returning from the editor wait for the return transition. Card overlap came from change cross-fades and per-update span reassignment in the staggered grid. Change animations are off, spans are only rebuilt while the list is hidden, and a reorder (or an insert/remove in a multi-column grid) is shown as one short cross-fade of the list instead of cards sliding across columns. Item animations are skipped when the screen is not visible or system animations are off. Refs: #170
Every sync applies the whole merged snapshot, and the apply re-wrote every known record: notes with a REPLACE insert, tags, tasks and task lists with an update, plus two sync_metadata updates each. Room invalidates a query on any write to its tables, so even a sync with nothing new re-emitted the notes, tags and tasks lists and the main screen redrew and re-animated. The apply now builds the row a version would produce and writes it only when it differs from the stored row. The comparison is made on the row, not on the version hash, because a note whose attachment references are moved to this device's files on apply hashes the same before and after; that first normalising apply is still written. A note with no attachments counts as unchanged whether it stores null or "[]". Version bookkeeping is re-read inside the transaction and only the columns that differ are written, for live records and tombstones alike. Changed records, revivals and deletions are applied exactly as before, and the wire format is unchanged. New Robolectric tests count row writes with SQLite triggers on the watched tables: a repeated sync of an unchanged library writes no rows and does not re-emit the notes Flowable, while a changed note or task and a tombstone are still applied and written. An instrumented case covers a note with an attachment. Refs: #170
Search only knew one rule: a title equal to the query came first and everything else followed by date. The check compared the untrimmed query, so a trailing space defeated it, tags were never searched, accents and repeated spaces made otherwise equal text miss, and queries shorter than two characters returned nothing at all. A new pure-Java NoteSearchRanker folds every note once per notes list (NFKD, diacritics removed, locale-independent lower case, whitespace collapsed) and sorts matches into buckets: exact title, title prefix, whole word in the title, title contains, tag name, then note text. Inside a bucket pinned notes come first, then the newest. A one-character query searches titles only; tags and text are searched from two characters. The ranking now runs after the debounce on a worker thread instead of on the main thread for every keystroke, and the view learns from the result whether an empty list should ask the user to keep typing. Search results highlight the matched part of the title and preview with the theme's primary container colours, mapped back through the folding so accented and decomposed text is marked at the right characters. SearchFilterTest now exercises the production ranker instead of a copy of the filter: every bucket in order, diacritics, Cyrillic, trailing and inner spaces, one-character queries, the pinned tie-break, the tag filter and a 5,000-note timing smoke test. MainPresenterTest covers the debounced search stream end to end. Refs: #175
The main search bar carried two separate actions, Sort and Format, with vague labels and a layout icon that showed the opposite layout at launch because FormatListTool.init() was never called. They are now one View options action that opens a bottom sheet with the sort order (radio group) and a List/Grid segmented control. Choices apply at once and the sheet stays open; the activity saves and applies them, so the sheet survives recreation. The action's icon and TalkBack label show the current layout. Switching the layout now goes through the list's fade swap, so cards no longer slide over one another when the column count changes. The search hint lost its trailing space and falls back to a short "Search" when the full hint does not fit, so large text and long languages no longer cut it off. The drawer lists Tags and Trash first, then Tasks, and the stats card no longer duplicates the Support button; its decorative icons are hidden from TalkBack so rows are read once. In the note editors the toolbar keeps Back, the save status and More. The reminder bell, which duplicated Reminder in More, is replaced by a chip at the top of the note showing the upcoming reminder's day and time; tapping it opens the reminder picker and it is hidden when there is no reminder. The extended editor's reading-mode action has a localized title that follows its state. Strings are added in all shipped locales and the help texts describe the new places for sorting, layout and reminders. Refs: #180
The simple editor saved the scroll position when the keyboard opened and jumped back to it when the keyboard closed, so text edited in between was scrolled away. That restore is gone: opening the keyboard brings the caret into view and closing it moves nothing. Tapping Edit no longer forces the caret to the end of the note; it goes to the first visible line of a long note, or to the end when the end is on screen, and a selection restored after a rotation wins over both. The body field used a LinkMovementMethod subclass, which clears the selection whenever the field takes focus, so the caret fell back to offset 0 and typing landed in the first line. It is replaced by an ArrowKeyMovementMethod that opens a link only on a short tap on it, and not when the caret already sits inside the link. Copying the body, counting words and comparing it with the note ran on every keystroke; they now run once typing pauses for 250 ms. The extended editor no longer parses the whole Editor.js document on the UI thread for every change: the presenter stores the JSON and derives the plain text and attachment list when the note is saved or read. Both editors call the new NotePresenter.flushPending() from onStop, so the edits still inside the two-second autosave debounce are written when the screen goes away; the extended editor also asks the page for its document without waiting for Editor.js change batching. The write is not tied to the presenter's CompositeDisposable, so a destroy right after onStop cannot cancel it. A save of a note with no meaningful content no longer reports success, which used to run the attachment cleanup against a list that was never written. The extended editor pads its root by the keyboard or navigation bar, as it draws edge to edge and was not resized by the keyboard, and the page keeps the caret visible when its height changes. Both editors keep their reading position across a rotation (a text offset for the simple editor, the top visible block for the extended one), plus the selection, edit mode and any draft whose save is still in flight. Refs: #171
The image tool uploaded through a synchronous bridge call that carried the whole file as base64 and returned only after Android had decoded, optimized and written it, so the editor's JS thread was blocked for the length of the save and the page jumped when it caught up. Uploads are now asynchronous: the page sends a request id, Android stores the file on a background thread and resolves the page's promise through evaluateJavascript. For a file from the system picker Android keeps the content URI from the chooser result and reads it itself, so the bytes never cross the bridge; pasted or dropped files still send base64, but the call returns at once. Attachments use the same path. The stored image's width and height are returned with the upload and kept in the block data. The image tool reserves the picture's aspect ratio from the preview while it uploads, from the stored size once it is saved, and on every later render, so inserting or loading an image no longer changes the height of the note around the caret. The uploading placeholder used a hard-coded white background; it and the page background now follow the theme, and the WebView paints the theme surface before the page loads, so the editor no longer flashes white in the dark theme. AttachmentCleaner deleted any file not referenced by the saved note, including an image whose block had not been saved yet. Files uploaded in this process are now protected until a saved note references them, and files younger than ten minutes are never treated as orphans, which covers a process that died before the save. When the screen is recreated while the picker is open, the result reached a page that no longer existed and the picked file was dropped. The picker's kind and the index of the block that opened it are kept in the saved state; a result that comes back without a waiting page is stored and its block is inserted at that index once the note has rendered again. Recent edits are already written on onStop by the previous change. Refs: #172
Both editors now remember, per note and on this device only, where the note was being read or edited, and go back there the next time it is opened, including after an app restart. The simple editor keeps the selection (when the note was left while editing), the offset at the top of the viewport, the scroll position, and the length and a hash of the text, plus a short piece of text on each side of the caret and of the top line. PositionRestorer, a pure Java class with its own tests, uses the exact offsets when the text is unchanged, finds them again by that context when the note was edited here or on another device, maps them proportionally after a modest change with no recognisable context left, and opens at the top when the note was emptied or largely rewritten. Every offset it returns is inside the current text, so no selection can go out of range. The restored caret is used when editing starts while it is still on screen. The extended editor reports the block at the top of the viewport and the caret's block, input and character offset once scrolling or the caret settles. Editor.js keeps block ids across saves, so the position is found by id even after blocks moved; when every saved block is gone only the block index is kept, clamped. After render the page puts the caret back through editor.caret.setToBlock and scrolls the saved block into place, and autofocus is turned off when a position is about to be restored so the first block does not take the caret first. Positions live in their own preferences file (NoteViewStateStore), not on the Note entity and not in backups or sync, limited to the 200 most recently used notes. They are written in onPause and removed when a note is deleted for good: from the trash, by clearing the trash, by a sync deletion from another device, or by clearing the library after the move to Encly. A rotation keeps using the existing in-memory anchor. Refs: #176
Settings > Interaction gains three options, all off-sync and kept in a preferences file of their own (NoteOpeningPreferences) rather than in PreferencesBackup, so an older app version restoring or syncing settings cannot reset them. Open notes in: Automatic (the default, and the behaviour so far: reading in the simple editor, editing in the extended one), Reading mode or Editing mode. New notes always open for editing. Opening an existing note in editing mode waits for the first layout, applies the saved position, and then places the caret by the rules editing already uses: the restored caret if it is on screen, else the first visible line, or the end of a note that fits on screen. The keyboard opens in both editors; Automatic keeps the extended editor's keyboard closed as before. Open notes at: Where you left off (the default, using the positions from the previous commit) or The beginning. Double-tap to edit, off by default: in reading mode a double tap on the text starts editing there. The simple editor watches the gesture in dispatchTouchEvent, since the body is disabled while reading, and maps the point with getOffsetForPosition. The extended editor handles dblclick in the page, finds the block, input and offset with caretRangeFromPoint, leaves reading mode and puts the caret there. The extended editor now starts read-only through Editor.js's own readOnly option instead of toggling after load, and its reading mode also locks the title, which stayed editable before. Switching modes keeps the block at the top of the viewport in place, the page reports every switch to Android, and the toolbar action shows the right icon from the start and after a double tap. The mode survives a rotation. Reminder notifications always opened NoteActivity, so notes with attachments, or every note with the extended editor switched on, opened in the wrong editor. The receiver now looks the note up off the main thread and both it and ReminderTapActivity build the intent through NoteNavigator. The two choices use a new settings choice dialog in the style of the theme mode dialog, with a line explaining each option and the current one announced as selected. The forward chevron used by settings rows now mirrors in right-to-left layouts. Refs: #177
… or years The reminder picker gets a Custom chip next to Daily, Weekly and Monthly. It opens a dialog for a number from 1 to 999 and a unit, and the chosen rule is shown as "Every 3 hours" on the chip, the note card, the reminder chip at the top of the note and in More, with plurals in every shipped language. The rule is a RepeatRule kept in the existing reminderRepeat column and sync field, so there is no migration. Daily, weekly and monthly are still written as DAILY, WEEKLY and MONTHLY whenever an older version can follow them; other rules are stored as EVERY:<n>:<UNIT>@<anchor>, which an older version reads as a one-time reminder. Every occurrence is computed from the anchor instead of from "now", so reminders no longer drift later, a monthly reminder on the 31st returns to the 31st after a short month, a yearly one on 29 February falls on 28 February in other years, hours are real elapsed time and days keep the wall-clock time across daylight-saving changes. Scheduling was reworked so the schedule survives everything around it. The "repeat notification" nag and snooze get their own alarms and no longer overwrite the next occurrence; snoozes are remembered on the device. A ReminderRescheduler re-arms reminders at boot, after an app update, when the exact-alarm permission is granted, at app start and after a sync applies changed or deleted notes, so a reminder edited on another device is armed or cancelled here at once. A missed occurrence fires once and then rolls forward, instead of being dropped. Without the exact-alarm permission reminders are armed inexactly rather than skipped, and the picker and the main screen offer to turn the permission on. Task reminders get the same inexact fallback. Refs: #179
Autosave overwrote a note in place, so text removed by accident and then saved was gone for good. Each note now keeps up to 20 earlier versions in a new note_versions table on this device. The editor's save keeps the text it replaces at most once every ten minutes, or at once when it removes at least 200 characters; a version applied by a sync, the side picked in a conflict and a restore always keep the text they replace first. Duplicates of the latest version are skipped and the oldest versions are pruned in the same transaction. The table has no foreign key on purpose: notes are written with REPLACE inserts, which SQLite performs as delete plus insert, so a cascade would wipe the history on every sync. History is deleted explicitly when a note is deleted for good, when the trash is emptied, when all data is cleared and when another device deletes the note, and a sweep after each sync removes anything left without its note. It stays while the note is in the trash, never syncs and is not part of backups. More -> Version history, in the editor and on the notes list, opens a new screen listing the versions with time, reason and the start of the text. A version opens in a sheet next to the current note with the differing part highlighted in both, using the comparison extracted from the sync conflict presentation. Restoring asks for confirmation, keeps the current text as a version, and writes the version as an ordinary edit that syncs like any other. Only text comes back: attachment blocks whose files the note no longer holds are left out, and the sheet says so. The editor saves pending edits before opening the history and reloads the note after a restore. Migration 22 -> 23 also adds notes.customPosition for the custom order in #178. It is seeded newest edit first, rides in local JSON backups as "n", is stripped from sync payloads so hashes and older clients are unaffected, and is preserved when a sync rewrites the note row. Refs: #174
View options gains a Custom order next to the two date orders. In it, press and hold a note and drag it, in the list or in the two-column grid; letting go without moving opens the note's menu as before, and TalkBack users get Move up and Move down actions on every card instead. Pinned notes keep their own section above the others and are arranged among themselves. Order is notes.customPosition, larger first, handed out 1024 apart so a drop takes the midpoint between its neighbours and writes one row; only when two neighbours touch is the whole order renumbered. A move updates that column alone, without the note's date or sync timestamp, so it is not an edit and is never published. New notes, copies, notes from another device and notes restored from a backup that predates the order start at the top; a local JSON backup keeps the position. The drag runs on the adapter's own working copy of the list, and the dropped order is handed to the differ with its updates muted, since the cards are already where it says they are; dispatching the moves a second time is what makes cards slide over each other. States that arrive mid-drag, such as a sync, are held until the drop has settled. NoteAdapter now wraps an AsyncListDiffer directly to allow this. A dragged card is no longer faded by the swipe fade, which only applies to swipes now. The Custom choice is stored in a device-local flag. The synced and backed-up sort setting keeps DataSort or DataReserve, which older versions read correctly, and applying synced settings leaves the flag alone. Refs: #178
Undo and Redo appear in the note toolbar while a note is being edited, after the save status so its coming and going never moves them, and are dimmed and disabled when there is nothing to take back or bring back. All toolbar actions are now shown as "always": with "ifRoom" a phone narrower than 500dp shows at most three actions and More would fall into an overflow menu. Ctrl+Z, Ctrl+Shift+Z and Ctrl+Y work on a hardware keyboard. The simple editor records every change to the title and the body as a diff in TextEditHistory, a plain Java class. Letters typed in a row, a keyboard rewriting the word it composes, and backspace through a typo join one step while they follow closely in time and place; a new word, a paste and typing after a deletion start new steps. Each step keeps the selection it replaced. Steps are bounded in count and characters, changes the app makes itself (loading the note, applying an undo) are not recorded, and an undo is applied as an ordinary edit so it is autosaved. The title's line-break handling moved from rewriting the title after the fact to an input filter, so the history only sees the title as it is. The most recent steps, up to 32K characters, survive a recreation and are put back only for the same note holding exactly the same text. The extended editor keeps its history in the page: whole document states with the caret, batched on a pause, a finished word or line, a block added, removed or moved, or after a few seconds of typing. Undo applies a state with editor.blocks.update for blocks whose content changed and renders again only when the list of blocks changed, then puts the caret and scroll back with the existing view-state helpers. The result is sent to Android like any edit. The browser's own undo is replaced, a block deleted together with its file is taken out of every state, and files an undoable step removed are kept from the orphan cleanup so an undo never brings back a block without its file. The presenter starts the history again whenever a note is put into the editor (load, copy, a restored version) and never on save, so autosave leaves it intact and no undo reaches into another note. Refs: #173
A full list swap fades the notes out, then submits the new list from a callback. A second swap requested while the first was still fading replaced that callback, and a plain update arriving in the same window submitted its list straight away while the older swap's list was still pending, so the older list could land last. A sync finishing right after a category switch, or a layout change made mid-fade, could leave the screen on stale notes or keep the previous column count. The swap now reads what it applies when it runs: the newest notes, their category, the saved layout and whether to start at the top are held on the activity and committed together, and any state that arrives while a swap is in progress joins it instead of racing it. Refs: #170
The page now opens on a clear hero: Encly's real icon, a "from the author of My Notes" label, the title and a one-sentence promise. Four benefit rows with tonal icons follow (encrypted, offline with no account, PIN and fingerprint lock, free and open source), then two cards that say what moves to Encly and what stays in My Notes, each as a label with its count. Rows with nothing to move or leave behind are hidden, and when nothing stays behind the card says so. Each state has one full-width primary action and its own helper text; the update and wrong-certificate states explain themselves in a tinted card, and the button reads "Preparing your notes" while the hand-off is being built. The result is a card with a success or failure badge, the counts Encly reported, and clearing My Notes as a secondary, red text action that still asks for confirmation. Content keeps a 600 dp maximum width on tablets, count rows are read by TalkBack as one item, and headings are marked as such. The one-time bottom sheet uses the same hero and three benefit rows with a filled primary button and "Later". Opening the page now also marks that introduction as seen, so it can never appear after the user has already visited the page; a Robolectric test pins the shown-once rule across restarts and later updates. All meet_encly strings are rewritten to be shorter and plainer, in all eleven locales. Refs: #167
Switching from a tag with no notes back to All notes (or to another tag) left the list blank: the notes were there, but none was laid out until the app was restarted. On a device the list had all its items and no child views, and a forced layout pass did not help. When a swap brought in an empty list, the grid's columns were reset and it was scrolled to the top while it had no items. That left the StaggeredGridLayoutManager anchored wrong, so the next non-empty list was laid out below the screen. The column reset and the jump to the top now happen only when there are notes to show, also when the layout switches between list and grid on an empty tag. Refs: #170
Undo and Redo took two of the few places in the note toolbar, and in the extended editor they crowded Back, the title, the save status, Read and More. They now sit on a slim bar docked on top of the soft keyboard, the same EditorKeyboardBar in both editors: Undo and Redo at the start, Hide keyboard at the end, and room between them for formatting actions later. The toolbar is back to Back, the title, the save status, Read in the extended editor, and More. The bar is shown only while a note is edited and the on-screen keyboard is up, so it is absent in reading mode and with a hardware keyboard, where Ctrl+Z, Ctrl+Shift+Z and Ctrl+Y keep working. It is a 48dp row of 48dp icon buttons under a 1dp hairline in colorOutlineVariant, on colorSurfaceContainer; icons are colorOnSurfaceVariant and colorOnSurface at 38% when there is nothing to undo or redo. The buttons carry content descriptions and tooltips, and the arrows are auto-mirrored. The bar is laid out where the keyboard ends up and translated frame by frame with a WindowInsetsAnimationCompat callback, so it rides on the keyboard as it slides in and out instead of jumping ahead of it; with animations off the keyboard and the bar simply appear. Both editors end their content above the keyboard plus the bar: the simple editor's scroll view and caret scrolling, and the extended editor's WebView, which is now sized by a bottom margin instead of padding the root, so the page's own resize handling keeps the caret line above the bar. Side system bars and cutouts in landscape keep the buttons clear of them. Without the keyboard, Undo and Redo are in More while a note is edited, dimmed and disabled like the bar; the sheet stays open and follows the history, so several steps can be taken back in a row. Refs: #173
Moving the top note to the trash left All notes with an empty list until another tag was chosen, and after editing, pinning or restoring a note, creating one or switching to the custom order the list often stayed scrolled down, with the new top note out of sight. The jump to the top was requested from the commit callback of the new list, before the list had laid out the change. StaggeredGridLayoutManager resolves a pending scroll against the children of the previous layout and takes the position of its first child as the anchor. When that child was the removed top note the anchor was -1 and nothing was laid out; when the new top note was a visible card moved up from below, the offset was measured from where that card used to be and the list landed at a wrong place. The jump now waits until the change has been laid out and is applied before the next frame is drawn, and it expands the search bar and tags again so the top note is fully visible. A note created while the editor was in front jumps to the top instead of starting a smooth scroll on a list that is not on screen. Refs: #170
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Why
Encly replaces My Notes, and users need a safe one-tap move to it. The other changes close issues #170–#180.
Behaviour changes
note_versions,notes.customPosition).Testing
./gradlew :app:assembleDebug :app:test :app:lintDebug :app:spotlessCheck: pass on the branch head.Not verified:
MigrationTest22→23 (instrumented, not run); RTL (the app hassupportsRtloff).References
Closes #167, closes #170, closes #171, closes #172, closes #173, closes #174, closes #175, closes #176, closes #177, closes #178, closes #179, closes #180 · pasichDev/Encly#46 · pasichDev/Encly#56