Conversation
MainTheme was Theme.AppCompat.Light with five hardcoded hex values, copied in full into values-v29 and values-v35 so that the qualified variants had already drifted apart. Dark mode was the OS force-dark hack turned on with android:forceDarkAllowed, not a resource set of our own. The theme is now Theme.Material3.DayNight over named colour roles, with a values-night counterpart. Base.MainTheme holds everything and the qualified variants only add what is specific to them, so there is no copy to drift. The inverse and tertiary roles are set explicitly: leave them out and snackbars fall back to material's purple baseline. uiMode comes out of configChanges. Swallowing it meant the activity was never recreated on a dark mode toggle, so already-inflated views kept their day colours until the next launch - which would have made the whole migration look half-applied. Documents stay on the background they were authored against. Algorithmic darkening never fired under the old light theme, and at targetSdk 33 and up it starts the moment the app theme reports itself as dark; inverting an odt full of tables and coloured text is not a change to make silently. ProgressDialogFragment moves off the framework ProgressDialog, which resolves android:alertDialogTheme and would have stayed light. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The app opened on a static welcome screen: a title and three icon rows explaining that it can view, search and edit documents, none of which was actionable. The recently opened list existed but was buried as the last row of the file manager chooser, so almost nobody found it. The landing screen is now that list. LandingFragment is a sibling of DocumentFragment, with its state in a ViewModel so it survives the recreation a rotation or a dark mode toggle causes, and MainActivity keeps only the container swap it already did. RecyclerView and the two lifecycle artifacts already came in through material, so declaring them costs no apk size - it only stops a material bump from silently taking a compile dependency away. No coroutines: they are on the classpath transitively via the play libraries, and nothing in this project is written in them, so the list loads on an executor and publishes through LiveData like the loaders do. The fragment is hidden rather than stopped when a document opens, so nothing in the lifecycle fires when one is closed - hence the explicit setLandingVisible, without which the list would miss the document that was just added to it. The catch-all switch keeps its place, under a settings header. It is exactly the setting a stuck user has to be able to find. Its logic moves to CatchAllSetting, but applyOnLaunch stays in MainActivity.onCreate: the upgrade re-toggle has to run even when the app is launched straight into a document and the landing screen is never shown. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
findDocument queried every activity answering ACTION_OPEN_DOCUMENT and raised an "Open document via:" dialog to choose between them, before the picker it was going to launch anyway. That is a tap that buys nothing: the system picker already browses Drive, Downloads, a usb stick and every installed file manager, and it is the ui people know. Its last row used to be the recently opened documents, which is now the landing screen itself, so the dialog has nothing left that is its own. The <queries> entries stay. They were there for queryIntentActivities, but removing package visibility is exactly the kind of change that breaks on one OEM and nowhere else. MainActivityTests drove the picker through the dialog, so it loses that step; the intent stubbing is untouched, because Espresso-Intents intercepts at execStartActivity, below ActivityResultLauncher. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
"See the files on my phone" has no permission behind it any more. READ_EXTERNAL_STORAGE has not reached documents since scoped storage, READ_MEDIA_* covers only images, video and audio, and Play restricts MANAGE_EXTERNAL_STORAGE to file managers and backup apps - a document viewer asking for it gets the listing rejected. The manifest still declares nothing but INTERNET, and it stays that way. ACTION_OPEN_DOCUMENT_TREE does the same job with the user's consent instead: they pick a folder once, we persist a read only grant, and the landing screen lists and navigates it from then on. Read only - no write flag is ever requested. Listing goes through DocumentsContract rather than androidx's DocumentFile, which issues a fresh query per child the moment a name or type is read. One cursor covers a whole folder. SupportedDocumentTypes decides what is worth showing. It has to guess: MetadataLoader only knows what is supported after libmagic has run over the cached file, and a folder listing has nothing but a name and whatever mime type the provider volunteered - which is regularly application/octet-stream, hence the extension fallback. It mirrors the STRICT_CATCH intent filter and nothing keeps the two in step, so both sides now say so, as does CLAUDE.md. prune() learns that a document below a granted tree is covered by that tree's grant, which it can tell from the uri alone - so nothing has to record where a document was opened from. The tree grant is taken through PersistedUriPermissions rather than the resolver directly, so that it joins the in flight set the recent list's grants already use: addFolderTree writes the tree down on the view model's executor, and a prune in that window would find a grant nothing points at yet and hand it straight back. Same race, same fix, one folder instead of one document. That decision is now isReferenced(), which takes the strings rather than a context and so can be covered by a plain jvm test, with the release half lifted out beside it to keep prune() down to the loop it is. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The list of mime prefixes took the whole openxmlformats-officedocument family, but the STRICT_CATCH filter names two of them - docx and xlsx - and CoreLoader opens no others. A provider that reports a pptx by its standard mime type therefore had it listed as openable, and tapping it led into the unsupported/upload path instead. The extension side of the guess already left pptx out, so the two halves disagreed. Both types are now spelled out, and stay prefixes rather than exact matches so that they keep testing the same strings the same way CoreLoader does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgJyGjRBpjAJ2724aMNnEp
The empty state was shown instead of the list, which took the settings section with it: on a fresh install - and after the last recent document is removed - the catch-all switch was the one thing on the landing screen that could not be reached, and it is exactly what somebody whose file manager refuses to hand documents over needs to find. It is now the first row of the list rather than a screen of its own, so whatever sits below it stays where it is. It keeps both of its buttons, which is why the folders section is left out while it is up: the row already offers to add one, and an empty section under it would say the same thing twice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgJyGjRBpjAJ2724aMNnEp
Two things the list was missing. Each row now carries the time it was last opened, which is the thing that tells two similarly named documents apart, and a recent document can be swiped off the list with an undo. Undo puts the entry back at the index it came from rather than at the front, so the list ends up looking exactly as it did - that is the point of an undo, and it is why RecentDocumentList grows an insert() next to add() instead of reusing it. Nothing releases the uri permission on the way out. An undo needs it back, and prune() reclaims it on the next launch anyway - which is what reconciling against the stored lists, rather than bookkeeping every removal, was for. Only the recently opened documents can be swiped. A document inside a granted folder is simply there, and the app has no business removing anything from disk; the adapter tells the two apart by whether the row has a timestamp on it. A "dark documents" switch was built here too and taken back out: with the html odrcore generates, algorithmic darkening changes nothing visible, so the setting would have looked broken. Making it work needs a dark stylesheet from the core, not a toggle here. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The landing screen already offers to open a document twice - the fab, and the labelled button in the empty state - and both go straight to the system picker since the chooser dialog went away. The toolbar icon was a third copy of the same intent, sitting in the one place where it is least explained: an unlabelled folder glyph next to the title. Inside a document it was the odd one out too. Every other toolbar action acts on the document that is open; this one threw it away and started over, which is what back already does, and back lands on the list where the next document is picked anyway. The tests clicked the toolbar icon to reach the picker, so they now click whichever landing entry point is on screen. Which of the two it is depends on whether an earlier test already left something in the recent list, so they match either. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The subtitle carries the last opened time, and an entry written before that time was recorded has none - so reading "is this a recent" off it made exactly those legacy entries unswipeable, on the one install where it matters: an upgrade with a recent_documents.json already in place. They would have stayed that way until each was opened again. LandingItem.Document now carries the provenance itself, set where the row is built and defaulted off for the folder listing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgJyGjRBpjAJ2724aMNnEp
insert() skipped the eviction add() does, so an undo could take the list past MAX_ENTRIES: swipe one away with a full list, open another document while the snackbar is still up, then undo, and the list comes back one entry longer - with the extra entry holding a persisted uri permission of its own, against a per package cap the class exists to respect. Both paths share a cap() now, and insert() returns the same Update as add() so the entry that fell off the end can have its grant handed back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgJyGjRBpjAJ2724aMNnEp
It was the last item in the toolbar menu on the landing screen, and with the open action gone it was the only one - a screen whose entire content is a list kept an overflow menu alive for a single row that belongs in that list. It now sits under the settings header, next to the catch-all switch, and the landing screen has no menu at all. The row is only built when there is something to sell: never in pro, where the purchase is implied, and not once it has been bought. LandingFragment asks the activity at render time rather than reading it from the state, because billing is set up by initializeProprietaryLibraries - which runs again after the play services dialog - and is not something the ViewModel could read off disk itself. That takes it out of the document view too, where the overflow menu carried it. What is left there is the snackbar that offers the purchase on entering fullscreen, which is untouched. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The toolbar menu is gone. What can be done with the open document is now two buttons over its bottom right corner: search, which is what a reader reaches for most, and one that unfolds the other seven with their names next to them. The top right corner of a modern phone is the one place a thumb holding it cannot reach, and it is where every one of these lived - four of them behind "More options", which says nothing about what is inside. Naming them costs a tap that the overflow charged anyway. DocumentActions is a view rather than a menu, so the actions are set whenever a load finishes instead of whenever something happens to invalidate the menu. That is the reason for the odd rule the old code lived by: prepareMenu ran from onCreateMenu only, and a document that finished loading does not invalidate anything, so which items were visible depended on what else had asked the toolbar to rebuild. There is no menu provider left in either class, and menu_main.xml is deleted. The rows scroll: seven of them do not fit above the buttons on a phone held sideways. They hide while an action mode is up - find, tts and edit each bring their own controls - and while fullscreen has the screen, which back still leaves exactly as before. Back folds them up first. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing has been left in the options menu: the landing screen carries its own headers, the open action went away with the direct picker, the ad removal moved into the settings section and the document's actions moved down to the buttons by the thumb. What stayed behind is an empty strip of colour at the top of every screen. The bar cannot simply be dropped though - it is where the action modes appear. find, tts and edit are all raised with startSupportActionMode(), and appcompat puts them in the ActionBar's own container, in place of the toolbar. So rather than moving to a .NoActionBar theme, MainActivity hides the bar on start and lets appcompat bring the container back for as long as a mode is up: checkShowingFlags() shows it whenever an action mode is showing, whatever the app asked for, and takes it away again when the mode finishes. That also leaves the window insets alone. ActionBarOverlayLayout keeps the system bar insets off the content and reports the toolbar's height on to the app as an inset of its own, which is what MainActivity's listener pads main_root with - so the find bar pushes the page down by exactly its own height, and nothing pads anything while no mode is up. A NoActionBar theme would swap that decor for a FitWindowsLinearLayout, which pads itself by the system bars on top of the padding MainActivity already applies. The fullscreen hide()/show() pair goes with it: with the bar hidden for good there is nothing left for it to toggle, and show() would have put the empty bar back on leaving fullscreen. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Open every format the core renders, not just half of them odrcore v6 keeps reference html output for doc, ppt, xls and pptx next to the ones the app already claimed, so the "TODO: enable pptx too" that sat in CoreLoader was the only thing keeping presentations out - and the legacy binary formats reached the core only by accident, through the doc mime type that was listed before ppt and xls ever were. What the loader claims is now one list: opendocument, the whole ooxml family, the three legacy microsoft formats and pdf. Prefixes, so the templates and the macro enabled variants come along - the latter are spelled vnd.ms-word/vnd.ms-excel/vnd.ms-powerpoint rather than under the openxmlformats prefix, which is why those appear without their subtypes. Text, csv and images stay out: the core renders them too, but RawLoader is what gives them their player, and it only gets a turn when the core loader says no. The manifest filter and SupportedDocumentTypes follow, so the system offers us pptx/ppt/xls and the folder browser lists them. Two things fall out of it. The doOoxml flag is gone - it has been passed true since ooxml stopped being experimental and now gates nothing. And the ".doc-files are not real documents" special case becomes a check for all three legacy types: the core does parse them now, but declares them neither editable nor savable, so keeping one open only buys a second parse. pdf changes which viewer a failed upload falls back to, so that stays explicit: google docs still gets the pdf, the microsoft viewer keeps everything else the core knows. Verified on device - .pptx, .ppt, .doc and .xls all render through the core, with CoreTest covering each of them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Ask the core what can be edited instead of guessing by mime type Codex was right on #546: doc, ppt and xls now reach CoreLoader, and prepareActions offered Edit for every CORE result except ODS and PDF - so the legacy formats got a button that could only ever end in a failed save, because host() would not keep a document open for them. The list of exceptions was never ours to keep. odrcore answers this itself: Document.isEditable()/isSavable() is false for the three legacy binary formats, for ooxml spreadsheets and presentations, and for every spreadsheet including odf - the last being the core's own TODO, the same gap as #442 that the UI was spelling out by mime prefix. host() now keeps the document only when the core says yes, which makes "we have a document" the answer, and it travels to the fragment on Result. While in there, two more copies of the same idea: - CoreLoader.MIME_PREFIXES was a second SupportedDocumentTypes. It is gone; the loader defers to the table. What stays separate is the manifest, which XML keeps out of reach - so SupportedFormatsTest walks odrcore's FileType values, asks Odr.mimetypeByFileType for each, and asserts the table and the package manager agree. Adding a format to one side and forgetting the other is now a red test. The core cannot replace the table itself: its lookups are native, and know one canonical mime per format - no templates, no macroEnabled, no x-vnd.oasis, which is exactly what providers hand out. - toggleDarkMode's isDarkModeSupported was dead. Nothing in the body read it while DocumentFragment worked out that a pdf is not worth inverting. Renamed to disableDarkening(), which is what it does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b8becded88
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Codex on #547, all three of them real: - Adding a folder only pruned when the add evicted an older tree, but pruning is also what settles the pending grant takeRead() marked. A tree that never got settled stayed in the pending set, so removing it again in the same session found its grant spoken for and kept it alive until the next launch - against a per package cap. It prunes after every add now. - A LandingFragment recreated underneath an open document - a dark mode toggle, a rotation - starts out believing it is on screen, and its view model comes back with it. One that was inside a folder therefore went on eating back presses behind the document. MainActivity tells it it is hidden when it restores a DocumentFragment, the same way it does when it opens one. - SupportedDocumentTypes takes the ooxml family in two prefixes, so the templates, the slideshows and the macro enabled variants all count as supported - but an intent-filter matches a mime type exactly, and the manifest named only docx, xlsx and pptx. A .dotx therefore opened from the folder browser while the same file offered nothing from another app. The eleven variants are spelled out now. SupportedFormatsTest could not have caught that: it walks odrcore's FileType values, and the core names one canonical mime per format, so none of these ever appear in it. They get a case of their own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgJyGjRBpjAJ2724aMNnEp
Nothing here changes what the app does. It is the sweepings of the fourteen commits before it, found by walking the screens in the order a user meets them. Dead: - LandingViewModel.releaseUnusedGrants(), which nothing ever called. - MainActivity.editActionMode, written and nulled but never read. The tts one stays: onPause has to stop it. - DocumentTreeBrowser.Child.lastModified, along with the column it made every folder listing ask for. - FolderTreesUtil.FolderTree.addedAt, persisted, parsed and never consulted - the order the file has them in is the order they were added. - The nine strings the redesign orphaned - the welcome copy, the file manager chooser and the two toolbar actions - still translated into 31 languages. The ones main was already carrying are left alone; they are not this branch's doing. Shared: - RecentDocumentsUtil and FolderTreesUtil were the same json file twice over: open it or treat a missing one as empty, walk the array, skip what does not parse, write the whole thing back. That half is JsonFileStore now and only the fields are left to each of them. Read better: - prepareActions built seven actions with seven add() calls; it is one listOfNotNull with the edit entry as the only conditional, so the order they unfold in fits on a screen. - Its last line was pageView.disableDarkening(), which is a property of the page and not of the buttons over it. It moves to initializePageView, which every PageView passes through. - SnackbarHelper.show ended in two bare booleans at all ten call sites. Named, they say which is which. - loadUri looked the document fragment up twice around a second null check. Same lookup, same fallback, one expression. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgJyGjRBpjAJ2724aMNnEp
Three things the redesign left half done, found by walking the screens the way somebody using them does. **The folder browser said nothing about where it was.** The name went to `mainActivity.title`, which was written while the toolbar still existed; the bar has been hidden since, and an action mode shows its own title, so the only place it still surfaced was the recents switcher. Two levels into a tree looked exactly like one level in, with an "Up one level" row as the sole cue. The name is a header at the top of the list now, and the title write is gone - LandingItem.Header takes the text rather than a string resource, because this one is not a constant. **A recent could be removed two ways, with opposite manners.** A swipe asked nothing and offered an undo; a long press asked for confirmation and offered no undo, then pruned the grant straight away where the swipe left it for the next launch. Both are the same wish. Long press does what the swipe does now, the dialog and its two strings are gone, and the view model is down to one removeRecentDocument. The dialog on *folders* stays - it is the only way to hand a tree back, and it is a grant the user gave us rather than a row in a list. **Every navigation re-proved the whole recent list.** refresh() was both "reload everything" and "publish what changed", so entering a folder, leaving it or flipping the catch-all switch each queried the provider once per recent document - up to 32 binder round trips - on top of the folder listing. Nothing the screen does to itself can revoke a grant, unmount a card or delete a file; only being away can. So reload() checks and refresh() does not, and only onResume and coming back from a document reload. Falls out of it: removeWithUndo took a position it never used, and the back callback was enabled from two places against two different sources - the live view model in one and the published state in the other. One updateFolderBack(), off the view model. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgJyGjRBpjAJ2724aMNnEp
theCatchAllSettingIsOfferedBeforeAnythingWasOpened has never passed on CI. It asserts the switch is displayed straight after launch, and CI's emulator is 320x480 mdpi - where the empty state alone is 392dp tall, so the settings below it are not merely off screen but never bound at all, and no matcher can find them. Hence NoMatchingViewException rather than a failed isDisplayed. Reproduced on a 320x480 mdpi avd: the same test, the same exception, the same line. Scrolling the list first, the way somebody looking for a setting would, all nine LandingTests pass there. Scrolling is the answer rather than a smaller empty state: the switch being a row of the list rather than a screen of its own is what put it within reach in the first place, and no empty state worth having fits above a settings section on a 383dp tall screen. espresso-contrib comes in for RecyclerViewActions - androidTest only, so nothing ships. Not fixed here, because it is not this: MainActivityTests.testPDF and testODT fail intermittently on one api level per run, and have since landing/04-direct-picker - the sibling branches show the same. The hierarchy at the failure is a landing screen with an empty state and no document container, i.e. the app is back at the start rather than showing a document that failed to load. That matches the play services crash testPDF already carries a comment about. Both pass locally. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgJyGjRBpjAJ2724aMNnEp
225 lines to 153, without dropping a rule. Most of what went was said twice or derivable from the tree. "one canonical mime per format" was the point of three separate paragraphs in the supported-types section, and the manifest half of it explained itself twice over. The native section explained conan once under "Native Dependencies" and again under "Core Library Integration". The dependency list named androidx, play services, espresso and junit, which libs.versions.toml already does, and better. The component inventories - one line per loader, per service class, per ui class - are now the two paragraphs that say how a document actually gets from a uri onto the screen, which is what somebody arriving at this codebase needs and what a list of names does not give them. What stays is the part a reader cannot work out by looking: why the package names differ, why the jni jar and the .so come out of the same conan package, why there is no storage permission, why editability is the core's answer and not a list here. Those are shorter but none of them are gone, and the ones phrased as prohibitions still are - they are what the file is for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgJyGjRBpjAJ2724aMNnEp
testPDF and testODT have been failing on one api level per run since landing/04-direct-picker, and the emulator logcat says why. Sixteen milliseconds before the click, Gboard activates over our window with a 320x616 input view on a 320x640 screen. The click is dispatched, the ime logs onDeactivate/onActivate - it is the one that got the touch - and nothing else happens: no picker intent, not even the analytics line findDocument reports before launching it. The tap landed on the keyboard, which sits above our window, and espresso reported the click as performed because as far as it is concerned it was. Then the assertion fails somewhere else entirely. Nothing was queued, so the idling resource never went busy, so the next onView did not wait: it ran 267ms later against a landing screen that had never been asked to do anything. In the run where the same test passed, that gap is 428ms and there is no ime in the log at all. The keyboard is left over from an earlier test - the password dialog puts one up, and the ime is a system service that outlives both the activity and the flavor's process, so it re-shows on the next window of ours that takes focus. It only matters since the picker moved to the landing screen: the toolbar action it replaced was at the top of the screen, and both of the entry points that replaced it are in the lower half, which is where a keyboard is. So closeSoftKeyboard() leads every click that a keyboard can cover - both landing entry points, the rows, and the password dialog's buttons, which is the same failure a screen further on. Verified on a 320x480 avd, which reproduced it: testPasswordProtectedODT failed there before this and passes after, and the full suite now runs clean twice over, pro then lite, the way CI does it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgJyGjRBpjAJ2724aMNnEp
Only the recent documents could be swiped. A folder could be removed only by long pressing it - a gesture nothing on screen mentions - and then confirming in a dialog. So of the two lists on the landing screen, one had an obvious way out and the other had a hidden one. Both are swipeable now, and both offer an undo instead of asking first. The dialog was there because a tree is a permission the user granted rather than a row in a list, and taking it back means going through the system picker again. An undo answers that better than a confirmation does: it costs nothing when the swipe was meant, and it is there when it was not. The grant is not released on removal - the same bargain the recent documents already make - so there is something to put back until the next launch prunes it. Restoring puts the folder at the index it came from rather than at the end, so an undo leaves the list as it was. FolderTreesUtil grows an insertFolderTree() beside addFolderTree(), both on the one put() that dedupes and caps, and the cap still applies: a folder can have been added while the undo was on offer. What can be removed is now on the row. LandingItem.Folder carries `granted`, the way Document carries `recent`, so a sub directory of a tree is not swipeable and a long press on one does nothing - it is part of the tree above it, not something the app was given. The adapter's isRemovableDocument becomes isRemovable and answers for both. Covered by three tests around a seeded tree: it is listed, it can be removed, and the undo brings it back. They drive the long press rather than the swipe, because the gesture belongs to ItemTouchHelper while what both paths call is ours. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgJyGjRBpjAJ2724aMNnEp
260 lines across 23 files, none of them reachable. Six were still in values/strings.xml with nothing referring to them: back_to_documents, discard_changes, error, save_now, unsaved_changes and yes. The other fifteen had lost their default entry years ago and lived on only in translations, which is why every build printed "removing resource ... without required default value" for each of them. Most are the onboarding: intro_title_open and its six companions belong to the IntroActivity that was gated behind a "show_intro" remote config flag in 2021 and removed as unpopular in 2023 (bf8e410), along with the appintro dependency and four megabytes of onboard png. The rest went the same way - the file manager chooser dialog, the video-ad and monthly ad removal options, the print-unavailable and leave-fullscreen croutons, the storage permission toast from before scoped storage, and the edit help action. I left these alone while clearing the ones the redesign itself orphaned, on the grounds that they were not this branch's doing. They are not, but they are nobody else's either, and every one of them is still handed to translators. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgJyGjRBpjAJ2724aMNnEp
The fab was hidden whenever there was neither a recent document nor a granted folder, because the empty state offers the same thing with a label on it. That made the one button that is always in the same place the one button that comes and goes. openDocumentThroughPicker() matched either entry point with isDisplayed() for exactly that reason, and would now find both on an empty list and throw AmbiguousViewMatcherException, so it takes the fab alone. theFabOpensTheSystemPicker no longer seeds a document, which leaves it tapping the fab over the empty state - the case this changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016gkBduJr5WoJ6dPZ6Y3CtS
Nothing was clipping them: the column sits its 16dp above the padding MainActivity puts on for the system bars, shadow included. But 16dp is all that separates it from the navigation bar right below, so the lower button reads as running off the edge of the screen. 32dp under it, and the rows above follow the buttons up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016gkBduJr5WoJ6dPZ6Y3CtS
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.
The app opened on a static welcome screen — a title and three icon rows explaining
that it can view, search and edit documents, none of it actionable — over a
Theme.AppCompat.Lightwith five hardcoded hex values and the OS force-dark hackstanding in for a night mode. Everything you could do with an open document lived in
the top right corner, four of it behind "More options".
This branch replaces all of that: Material 3 with real night resources, a landing
screen that is the recently opened list plus the folders you have granted access to,
and the document's actions moved down to the thumb. It collects 14 commits, 12 of which
were reviewed as their own PRs (#524–#546).
Theme (#524)
Base.MainThemeisTheme.Material3.DayNightover named colour roles with avalues-nightcounterpart; the-v29/-v35variants now only add what is specific tothem, so there is no full copy left to drift. The inverse and tertiary roles are set
explicitly — leave them out and snackbars fall back to material's purple baseline.
uiModecomes out ofconfigChanges, so a dark mode toggle recreates the activityinstead of leaving already-inflated views on their day colours until the next launch.
Documents themselves stay on the background they were authored against: algorithmic
darkening starts firing on its own at targetSdk 33+ once the app theme reports itself
dark, and inverting an odt full of tables and coloured text is not a change to make
silently.
ProgressDialogFragmentmoves off the frameworkProgressDialog, whichresolves
android:alertDialogThemeand would have stayed light.Landing screen (#525, #527, #528, #530)
LandingFragmentis a sibling ofDocumentFragmentwith its state in a ViewModel, soit survives the recreation a rotation or a dark mode toggle causes. It loads on an
executor and publishes through LiveData like the loaders do — no coroutines, nothing in
this project is written in them.
dialog. Each row carries when it was last opened, and can be swiped away with an undo
that puts it back at the index it came from.
but
INTERNETand stays that way.ACTION_OPEN_DOCUMENT_TREEtakes a read-only grantthe user picks once, and the landing screen lists and navigates it from then on.
Listing goes through
DocumentsContractrather than androidx'sDocumentFile, whichissues a fresh query per child the moment a name or type is read.
screen's last remaining menu item — sit under a header in the list. The empty state is
the first row rather than a screen of its own, so the settings stay reachable on a
fresh install, which is exactly when somebody whose file manager refuses to hand
documents over needs to find them.
PersistedUriPermissionsgrowsisReferenced(), soprune()knows a document below agranted tree is covered by that tree's grant, and folder grants join the in-flight set
that already kept a prune from reclaiming a grant nothing points at yet.
Actions where the thumb is (#526, #529, #531, #542)
Downloads, a usb stick and every installed file manager, and it is the ui people know.
The
<queries>entries stay; removing package visibility is the kind of change thatbreaks on one OEM and nowhere else.
twice already, and inside a document it was the odd one out — every other action acts
on the open document, that one threw it away.
DocumentActionsis a view, not a menu: two buttons over the document's bottomright corner — search, and one that unfolds the other seven with their names next to
them. Being a view means the actions are set when a load finishes rather than when
something invalidates the menu, which is what the old
prepareMenu-from-onCreateMenurule was working around.
menu_main.xmlis deleted.still where action modes appear (find, tts and edit all use
startSupportActionMode),and
ActionBarOverlayLayoutis what keeps system bar insets off the content — socheckShowingFlags()brings the container back for as long as a mode is up. A.NoActionBartheme would have swapped that decor out and double-padded the content.Formats (#546)
odrcore v6 keeps reference html output for doc, ppt, xls and pptx too, so what
CoreLoaderclaims is now one list — opendocument, the whole ooxml family, the threelegacy microsoft formats and pdf — instead of a
TODO: enable pptx tooand a doc mimetype that let the legacy binaries through by accident. Prefixes, so templates and the
macro-enabled variants come along.
Two lists collapse into one.
CoreLoader.MIME_PREFIXESwas a secondSupportedDocumentTypesand is gone. And editability is asked of the core ratherthan guessed from a mime type:
host()keeps a document open only whenisEditable()/isSavable()says yes, so "we have a document" is the answer thattravels to the fragment — the UI's own list of exceptions had already gone wrong for
the legacy formats it was newly offering an Edit button to.
The manifest's
STRICT_CATCHfilter is the one declaration XML keeps out of reach, soSupportedFormatsTestwalks odrcore'sFileTypevalues and asserts the table and thepackage manager agree.
Tests
New:
LandingTests(252 lines, instrumented),SupportedFormatsTest,CoreLoaderTest,RecentDocumentListTest,SupportedDocumentTypesTest, andCoreTestcoverage for .doc/.ppt/.xls/.pptx against real fixtures. Verified on device.
🤖 Generated with Claude Code