Files
file-manager/docs/CODE_MAP.md
T
SoftwareApp 17078bf613 A copy into a picked folder says where it went, and Show takes you there
The last acceptance criterion M16 had left open. After Copy to..., the snackbar reads 'Copied to AADst' with a Show button; after a move to a picked folder it keeps the undoable message instead, because the sources are gone and Show would lead nowhere.

Two things needed care rather than a string. The action button was hard-wired to undo() - the label was a parameter beside it, which let the two disagree - so UiMessage now carries a MessageAction, and the label follows from it: Undo for an undo, Show for a jump. And a message cannot name a folder from the path alone: a volume root has no name of its own, and internal storage's is 0. The message carries the picked PickerFolder, and the screen resolves its label the way breadcrumbs do - so copying into the root reads 'Copied to Internal storage', which is the case a plain path.name would have got wrong.

Four tests: the message names the destination and offers Show; Show opens it; a paste into the folder on screen still names nothing and offers nothing (its destination is the screen); a move to a picked folder offers Undo. All four driven through the view model, and both snackbar variants checked on the phone, including the volume-root one.

detekt's SpreadOperator baseline entries moved with the expression, so the message building is now a small helper with a deliberate suppression and a reason, and the baseline was regenerated: three obsolete entries removed, none added.
2026-10-17 23:19:07 +02:00

702 lines
36 KiB
Markdown

# Code map
Generated from the source by `tools/make-code-map.py` - do not edit by hand. Regenerate it after a change (`python3 tools/make-code-map.py`); `--check` exits non-zero when this file is stale.
79 source files, 13481 lines - 35 test files, 409 tests. The behaviour index at the end lists every test name, and in this project a test name is a sentence about what the app does, so it doubles as the specification.
Start at `AGENTS.md`. What is done, and why: `docs/ROADMAP.md` and `CHANGELOG.md`. What was verified by hand rather than by a test: `docs/EDGE_CASE_TESTING.md`.
## core/
Pure Kotlin, **no Android imports**: paths, file names, kinds, sorting, formatting, matching, the batch-rename planner
| file | lines | what it holds | tests |
|---|---|---|---|
| `ArchiveFormats.kt` | 102 | Which archive formats the app can handle, by file name. ArchiveFormat, ArchiveFormats | `ArchivePathTest` (18) |
| `ArchivePath.kt` | 43 | Guards archive extraction against writing outside the destination folder. ArchivePath | `ArchivePathTest` (18) |
| `BatchRename.kt` | 125 | Renames a batch of files to one name with a number after each: Holiday (1).jpg, Holiday (2).jpg, and so on. BatchRenameMove, BatchRenamePlan, BatchRename | `BatchRenameTest` (16) |
| `FileCategory.kt` | 53 | A library the home screen can open: everything on the device of a given kind, rather than everything in a given folder. FileCategory | `FileCategoryTest` (6) |
| `FileEntry.kt` | 29 | One row in a directory listing. FileEntry, isHiddenName() | `FileEntryTest` (8) |
| `FileKind.kt` | 73 | The coarse category a file belongs to, derived from its name. FileKind | `FileKindTest` (6) |
| `FileNames.kt` | 13 | Whether name can be the name of a file in a folder. isUsableName() | `FileNamesTest` (4) |
| `FileSize.kt` | 27 | Human-readable byte sizes for the file list ("1.5 MB"). FileSize | `FileSizeTest` (2) |
| `MimeTypes.kt` | 110 | Best-effort MIME lookup for the "open with" intent. MimeTypes | `FileEntryTest` (8) |
| `NameAllocator.kt` | 30 | Picks a name that is not already in use, in the familiar "name (1)" style. NameAllocator | `NameMatcherTest` (10) |
| `NameMatcher.kt` | 63 | The in-folder search matcher. NameMatcher | `NameMatcherTest` (10) |
| `SortSpec.kt` | 82 | A complete, serialisable description of the ordering of a directory listing. SortKey, SortDirection, SortSpec, NaturalOrder | `SortSpecTest` (8) |
| `SortedMerge.kt` | 42 | Adds a freshly read batch to an already sorted list, in one pass. SortedMerge | `SortedMergeTest` (4) |
| `StoragePath.kt` | 89 | A normalized, absolute, backend-agnostic storage path. StoragePath | `StoragePathTest` (8) |
| `ThumbnailKey.kt` | 22 | The identity of a thumbnail: which file it came from, and which version of that file. ThumbnailKey | `ThumbnailKeyTest` (4) |
| `Timestamps.kt` | 20 | Timestamps for the details sheet. Timestamps | - |
| `TransferPlanner.kt` | 105 | Turns "copy/paste these items into that folder" into a per-item decision, before a single byte is written. ConflictPolicy, TransferAction, TransferSource, TransferItem, TransferPlanner | `TransferPlannerTest` (5) |
## data/
Storage backends (all-files access and SAF), volumes, incoming shares, the SQLite search index, trash index and favourite list
| file | lines | what it holds | tests |
|---|---|---|---|
| `AllFilesSource.kt` | 154 | The "All files access" backend: plain java.io.File over absolute paths. AllFilesSource, File() | `AllFilesSourceTest` (14) |
| `ApkInspector.kt` | 100 | What an APK says about itself, read without installing it. ApkInfo, interface(), PackageManagerApkInspector | - |
| `BookmarkStore.kt` | 34 | The folders the user starred. BookmarkStore | - |
| `IncomingSource.kt` | 48 | Something another app has handed to this one. IncomingSource, ContentResolverIncomingSource | - |
| `MediaStoreDeleter.kt` | 73 | A best-effort DeleteRequestFactory backed by MediaStore. interface(), MediaStoreDeleter | - |
| `SafDocumentIds.kt` | 40 | Maps a Storage Access Framework tree onto StoragePaths. SafDocumentIds | `SafDocumentIdsTest` (6) |
| `SafGrant.kt` | 41 | Picking and keeping a folder tree for the SAF fallback. SafGrant | - |
| `SafSource.kt` | 187 | The Storage Access Framework fallback backend. SafSource | `SafSourceTest` (5) |
| `SearchIndex.kt` | 196 | A by-name index of what is on the volumes, so a search does not have to walk them. IndexedFile, VolumeStamp, SearchIndex | - |
| `SqliteBookmarkStore.kt` | 118 | The starred folders, on SQLite. SqliteBookmarkStore | `SqliteBookmarkStoreTest` (9) |
| `SqliteSearchIndex.kt` | 438 | The search index, on SQLite. SqliteSearchIndex | `SqliteSearchIndexTest` (27) |
| `SqliteTrashIndex.kt` | 163 | The trash index, on SQLite. SqliteTrashIndex | `SqliteTrashIndexTest` (7) |
| `StorageAccess.kt` | 80 | The Android-facing side of the storage-access decision. StorageAccess | - |
| `StorageAccessPolicy.kt` | 68 | The decision rules behind StorageAccess. StorageAccessMode, StorageAccessPolicy | `StorageAccessPolicyTest` (6) |
| `StorageLocation.kt` | 87 | Everything the browser needs to know about where files live and what it may touch. StorageLocation, StorageProvider | - |
| `StorageSource.kt` | 124 | The primitive file operations a storage backend must provide. StorageException, StorageSource | - |
| `TrashIndex.kt` | 61 | The durable memory of what is in the trash. TrashRecord, TrashIndex | - |
| `VolumeRepository.kt` | 121 | Discovers the device's storage volumes and reports their space. VolumeInfo, SpaceInfo, VolumeRepository | `VolumeRepositoryTest` (6) |
## domain/
Operations over a source: copy/move/delete/rename, archives, search walking, duplicate finding, trash, usage analysis
| file | lines | what it holds | tests |
|---|---|---|---|
| `ArchiveEngine.kt` | 658 | Creates and extracts archives, on top of the storage backends. ArchiveEntry, ArchiveEngine | `ArchiveEngineTest` (30) |
| `DuplicateFinder.kt` | 202 | Finds files that hold the same bytes. DuplicateGroup, DuplicateProgress, DuplicateScan, DuplicateFinder | `DuplicateFinderTest` (8) |
| `FileOperations.kt` | 272 | Copy, move and delete, assembled from the StorageSource primitives. FileOperations | `FileOperationsTest` (19) |
| `FileSearch.kt` | 79 | Recursive name search. FileSearch | `FileSearchTest` (8) |
| `OperationModels.kt` | 42 | The kind of long-running operation a progress notification can describe. OperationKind, OperationProgress, OperationFailure, OperationResult | - |
| `OperationNotifier.kt` | 25 | Keeps the foreground service in step with a running operation. OperationNotifier | - |
| `SearchIndexer.kt` | 173 | Fills a SearchIndex by walking a volume. SearchIndexer | `SearchIndexerTest` (14) |
| `Trash.kt` | 338 | A staging folder that makes a delete undoable. TrashedEntry, TrashBatch, Trash | `TrashTest` (19) |
| `UsageAnalysis.kt` | 95 | Works out where the space went. UsageNode, UsageAnalysis | `UsageAnalysisTest` (6) |
## service/
The foreground service that keeps a long operation alive and reports it
| file | lines | what it holds | tests |
|---|---|---|---|
| `FileOpNotifications.kt` | 64 | Builds the progress notification for a running file operation. FileOpNotifications | - |
| `FileOpService.kt` | 107 | Keeps a long copy, move or delete alive while the app is backgrounded, and mirrors the engine's progress into an ongoing notification. FileOpService | `FileOpServiceTest` (4) |
| `ServiceOperationNotifier.kt` | 19 | The production OperationNotifier: forwards to FileOpService, which owns the foreground notification and keeps the process alive. ServiceOperationNotifier | - |
## text/
Domain enum to string-resource mapping
| file | lines | what it holds | tests |
|---|---|---|---|
| `TextRes.kt` | 58 | Maps domain enums onto their resource ids. OperationKind(), FileKind(), FileCategory() | - |
## ui/browser/
The browser screen: view model, state, list and grid, dialogs, selection bar
| file | lines | what it holds | tests |
|---|---|---|---|
| `BrowserComponents.kt` | 317 | The thumbnail an entry's leading visual gets in a list row. ListThumbnailSize, GridThumbnailSize, GridCellMinSize, EntryIcon(), FileEntry(), FileEntry(), VIDEO_FRAME_MILLIS, iconFor(), iconFor(), EntryRow(), EntryGridCell(), FileEntry() | - |
| `BrowserContent.kt` | 463 | Everything below the app bar: the permission prompt, the roots, or a directory. WIDE_LAYOUT_MIN_WIDTH, MasterPaneWidth, BrowserContent(), LazyListScope(), LIBRARY_SECTION_KEY, LazyListScope(), BOOKMARK_SECTION_KEY, DirectoryView(), FileEntry(), IndexStatusRow(), FileEntry() | - |
| `BrowserDetailPane.kt` | 131 | The second pane of the wide layout: what is inside the folder the user picked, or a preview and the details of the file they picked. DetailPane(), FolderPreview(), ImagePreview() | - |
| `BrowserDialogs.kt` | 385 | The Copy to… / Move to… picker. TransferToDialog(), titleFor(), hereFor(), BatchRenameDialog(), PREVIEW_NAMES, batchRenameNote(), NameDialog(), ConfirmDialog(), ConflictDialog() | `BrowserDialogsTest` (5) |
| `BrowserRoots.kt` | 270 | A row on the root screen: a volume, with its free space when the platform reports it. RootRow(), itemCountLabel(), EmptyState(), LibrarySectionHeader(), BookmarkSectionHeader(), SectionHeader(), BookmarkRow(), LibraryRow(), Breadcrumbs() | `BrowserRootsTest` (4) |
| `BrowserScreen.kt` | 622 | The text of a snackbar message. BrowserScreen(), messageText() | - |
| `BrowserTopBar.kt` | 238 | The browser's app bar: title, up, the favourite star, and the search / sort / overflow actions. BrowserTopBar(), SEARCH_FIELD_WIDTH | `BrowserTopBarTest` (4) |
| `BrowserUiState.kt` | 379 | One immutable description of everything the browser shows. ViewMode, SearchScope, Clipboard, PickerFolder, TransferPicker, MessageAction, UiMessage, ConflictPrompt, RootItem, Bookmark, DetailFocus, Undo, UndoMove, Overlay, BrowserUiState | - |
| `BrowserViewModel.kt` | 2430 | Drives the browser screen. BrowserViewModel, ArchivePreview | `BrowserViewModelTest` (102) |
| `DetailsSheet.kt` | 109 | Name, location, size, type and modified date for one entry. DetailsSheet(), DetailLines(), DetailLine() | - |
| `EntryIntents.kt` | 58 | Wraps an entry's path in a content:// URI another app may read. contentUriFor(), viewIntent(), shareIntent() | - |
| `IncomingBar.kt` | 60 | The bar shown while another app is waiting on this one. IncomingBar() | - |
| `IncomingRequest.kt` | 46 | What another app asked this one to do. IncomingRequest | - |
| `SelectionBar.kt` | 219 | The contextual action bar shown while entries are selected. StorageAccessPrompt(), SelectionBar() | `SelectionBarTest` (3) |
| `SortSheet.kt` | 128 | Sort key, direction, hidden files and list/grid — all in one sheet. sortKeyLabel(), SortSheet() | - |
## ui/viewer/
The built-in viewers (text, image, PDF, media, markup, archive contents)
| file | lines | what it holds | tests |
|---|---|---|---|
| `ApkInfoScreen.kt` | 201 | What an APK says about itself, read from its own manifest without installing it. ApkInfoScreen(), ApkDetails(), ApkLine() | - |
| `ArchivePreviewScreen.kt` | 198 | What an archive holds, read from its index without unpacking any of it. ArchivePreviewScreen(), ArchiveEntryRow(), INDENT_DP | - |
| `ImageViewerScreen.kt` | 64 | The built-in image viewer. ImageViewerScreen() | - |
| `MarkupPreviewScreen.kt` | 116 | Markup drawn by the platform's own web engine: rendered HTML, and the SVG that the raster image viewer cannot decode at all. MarkupPreviewScreen(), ENCODING | - |
| `MediaPlayerScreen.kt` | 250 | Audio and video, played from the file itself. MediaPlayerScreen(), VideoPreview(), AudioPreview(), Unplayable(), timeLabel(), POSITION_POLL_MILLIS | - |
| `PdfViewerScreen.kt` | 276 | A PDF, drawn a page at a time by the platform's own renderer. OpenPdf, PdfViewerScreen(), PdfPages(), PdfPage(), PageBar() | - |
| `TextEditorScreen.kt` | 79 | A plain text editor for the text formats the browser classifies as FileKind.TEXT. TextEditorScreen() | - |
## ui/usage/
The storage-usage screen
| file | lines | what it holds | tests |
|---|---|---|---|
| `UsageScreen.kt` | 146 | Where the space went: each child of the current folder with its recursive size, biggest first, and a bar showing its share of the folder. UsageScreen(), UsageRow() | - |
## ui/trash/
The trash screen
| file | lines | what it holds | tests |
|---|---|---|---|
| `TrashScreen.kt` | 192 | What the trash still holds, and the way to put it back. TrashScreen(), BatchHeader(), TrashEntryRow() | - |
## ui/duplicates/
The duplicate-finder screen
| file | lines | what it holds | tests |
|---|---|---|---|
| `DuplicateScreen.kt` | 246 | The duplicate scan: what was compared, what matched, and which copies to remove. DuplicateScreen(), ScanRow(), LazyListScope(), GroupHeader(), EmptyDuplicates() | - |
## ui/theme/
Compose theme, colours, type
| file | lines | what it holds | tests |
|---|---|---|---|
| `Color.kt` | 27 | BluePrimaryLight, BlueOnPrimaryLight, BlueContainerLight, BlueOnContainerLight, TealSecondaryLight, SurfaceLight, SurfaceVariantLight, OnSurfaceLight, ErrorLight, BluePrimaryDark, BlueOnPrimaryDark, BlueContainerDark, BlueOnContainerDark, TealSecondaryDark, SurfaceDark, SurfaceVariantDark, OnSurfaceDark, ErrorDark | - |
| `Theme.kt` | 64 | App theme. LightColors, DarkColors, FileManagerTheme() | - |
| `Type.kt` | 9 | The app sticks with the Material 3 default type scale; a dedicated object keeps a single place to tune typography later. FileManagerTypography | - |
## ./
| file | lines | what it holds | tests |
|---|---|---|---|
| `FileManagerApplication.kt` | 151 | Application entry point and the app's tiny object graph. FileManagerApplication | - |
| `MainActivity.kt` | 65 | MainActivity | - |
## benchmark/
The macrobenchmark test APK: four measurements over a fixture folder, run on a device
| file | lines | what it holds | tests |
|---|---|---|---|
| `BrowserBenchmark.kt` | 249 | The four numbers docs/PERFORMANCE.md promises, measured on a benchmark build of the app. BrowserBenchmark, Fixture | - |
## Behaviour index
Every test name in the suite, by file: the fastest way to see whether something is already covered, and what the app claims to do.
### `ProjectInvariantsTest.kt` (4)
- core is pure Kotlin, with no Android imports
- user-visible text comes from string resources, not from literals
- the app asks for no permission that could reach the network
- a name is one path segment, and that rule lives in one place
### `core/ArchivePathTest.kt` (18)
- plain names are kept as they are
- redundant segments are normalized away
- absolute paths are rejected
- windows drive letters are rejected
- climbing out of the destination is rejected
- backslashes are treated as separators
- empty and dot-only names are rejected
- a climb that stays inside the destination is allowed
- an unsafe name never yields a path with a parent segment
- detects the formats it can extract
- detects 7z, which is readable without native code
- recognises but refuses formats that would need native code
- an ordinary file is not an archive
- detection is case-insensitive and tgz wins over gz
- single-file wrappers lose their own suffix
- a compressed tar is recognised as a tar, not as a bare compressed file
- every compressed tar names the tar inside it
- only zip can be created
### `core/BatchRenameTest.kt` (16)
- a second batch carries on where the first stopped
- a file named exactly like the base counts as number zero
- it continues from the highest number rather than from the first gap
- the series is counted across extensions
- names that merely resemble the pattern are ignored
- a base that contains regex characters is matched literally
- an empty folder starts at one
- a plan keeps each file's own extension and numbers it in the order shown
- a plan can continue the folder's series
- a name that is already in the folder blocks the whole batch
- a chain through a selected file is refused rather than half-done
- files that already have the pattern's names are left alone
- a file that already has its target name is counted and skipped, not renamed to itself
- a start below one is treated as one, because there is no zeroth copy
- a file with no extension gains none
- a base has to be a name, not a path
### `core/FileCategoryTest.kt` (6)
- the kinds that belong to no library are exactly the three that were decided
- documents covers text as well as documents
- every kind maps back to the library that claims it
- an uncollected kind answers null
- recent is the only library that is not about a kind
- no kind is claimed by two libraries
### `core/FileEntryTest.kt` (8)
- derives name and kind from the path
- hidden follows the leading-dot convention
- maps known extensions
- falls back to the binary type
- directories report the directory type
- an intent takes the type its items share
- a mixed batch falls back to the wildcard rather than hiding half the apps
- an unknown extension is still shared as a file
### `core/FileKindTest.kt` (6)
- a directory wins over its extension
- classifies common extensions case-insensitively
- only the last extension counts
- apk is recognised separately from archives
- unknown and hidden names are other
- extensionOf ignores dotfiles and trailing dots
### `core/FileNamesTest.kt` (4)
- an ordinary name is usable
- a separator is not part of a name
- a name has to say something
- a null byte cannot be in a name
### `core/FileSizeTest.kt` (2)
- bytes are shown without decimals
- kilobytes and above use one decimal
### `core/NameMatcherTest.kt` (10)
- an empty query matches everything
- a plain query is a case-insensitive substring
- a query with wildcards is treated as a glob
- glob is case-insensitive
- glob handles stars against empty and partial text
- keeps the desired name when it is free
- appends a counter before the extension
- handles names without an extension
- treats a dotfile as having no extension
- preserves the base of multi-dot names
### `core/SortSpecTest.kt` (8)
- directories come first in both directions
- name sort is natural, so file2 precedes file10
- size sorts ascending and descending
- date descending puts the newest first
- ties break by name ascending
- digit runs compare numerically
- comparison ignores case
- a prefix sorts before a longer name
### `core/SortedMergeTest.kt` (4)
- a later batch slots into the order already on screen
- the first batch into an empty list is sorted on its own
- an empty batch changes nothing
- an entry that ties on name does not jump ahead of one that arrived earlier
### `core/StoragePathTest.kt` (8)
- normalizes separators and dot segments
- adds a leading slash
- the root is its own path
- cannot escape above the root
- exposes segments, name and parent
- resolve handles relative and absolute input
- ancestor and descendant are strict
- is a value type
### `core/ThumbnailKeyTest.kt` (4)
- the same file with the same size and timestamp is the same cache entry
- a replaced file is a different cache entry even though its path is unchanged
- a file edited in place is a different cache entry even though its size is unchanged
- two files with the same size and timestamp do not share an entry
### `core/TransferPlannerTest.kt` (5)
- no conflict copies straight in
- conflicts follow the chosen policy
- two identical names in one batch do not collide
- a folder cannot be moved into itself or a descendant
- a move into itself is skipped in the plan
### `data/AllFilesSourceTest.kt` (14)
- reads a large folder in batches, covering every entry exactly once
- lists the children of a directory
- names lists a folder without reading metadata
- names of a folder that cannot be read is null, not empty
- names of a file is null too, because a file holds no names
- listing a missing directory yields nothing
- stat and exists agree
- creates missing directories and empty files
- delete removes a tree recursively
- delete reports false for something that is not there
- rename moves within the source and refuses to overwrite
- writing then reading round-trips, and append works
- free space comes from the injected probe
- reports its backend name
### `data/SafDocumentIdsTest.kt` (6)
- the root maps onto the tree document id
- children append their relative path
- every path round-trips
- documents outside the tree are rejected
- labels are derived from the tree id
- trees are not confused with similarly prefixed ones
### `data/SafSourceTest.kt` (5)
- lists the children of the tree root
- stats a single document
- a document the provider does not know does not exist
- exposes a label and exactly one root
- reports no free space, since the framework cannot
### `data/SqliteBookmarkStoreTest.kt` (9)
- a starred folder reads back whole
- the path comes back as the same value it was stored with
- the newest favourite comes first
- starring the same folder twice neither duplicates it nor moves it
- removing a favourite takes only that one out
- removing something that was never starred is not an error
- an empty store reads back empty rather than throwing
- a schema bump with no migration reports itself instead of emptying the list
- an already current schema is left alone
### `data/SqliteSearchIndexTest.kt` (27)
- a plain query is a substring of the name, case-insensitively
- a query with a wildcard is a glob over the whole name
- wildcards the user did not type stay literal
- results are capped and ordered by name
- a volume is replaced wholesale, and the stalest one decides the age
- a half-finished pass leaves the old timestamp in place
- forgetting a row takes it out of the results
- clearing empties the rows and the timestamps
- nothing indexed yet reports no age at all
- a schema bump with no migration reports itself
- children returns one folder's rows and nothing deeper
- forgetting a folder takes its subtree and leaves its neighbours
- forgetting a folder whose name contains a wildcard takes only that subtree
- a catch-up keeps the time of the last full walk
- the stamp remembers which hidden-files preference built it
- a library query returns only the kinds it was asked for
- a library query is newest first, so the cap keeps the newest
- a library of two kinds holds both of them
- a library that collects no kind answers nothing rather than everything
- recent is ordered by date and leaves folders out
- counts are grouped by kind
- an index that has never been filled counts nothing
- migrating from version 1 keeps the rows and adds the new columns
- migrating from version 2 rebuilds the files table and forgets the stamps
- duplicate candidates share a size, largest first
- folders are never candidates, however their size falls out
- the floor and the cap bound the candidate list
### `data/SqliteTrashIndexTest.kt` (7)
- a recorded item reads back whole
- recording the same staged path twice replaces instead of duplicating
- records come back newest batch first
- forgetting one batch leaves the others alone
- an empty index reads back empty rather than throwing
- a schema bump with no migration reports itself instead of emptying the index
- an already current schema is left alone
### `data/StorageAccessPolicyTest.kt` (6)
- android 11 and up key off the all-files special permission
- android 10 and below key off the legacy write grant
- from android 13 notifications are the only grant left to ask for
- storage grants are asked for up to android 12
- already granted permissions are not requested again
- the permission constants match the platform strings
### `data/VolumeRepositoryTest.kt` (6)
- volumes are listed primary first
- roots are built as directories in order
- space is summed across volumes
- an unknown volume reports no space
- a volume without a space reading contributes nothing
- used bytes never go negative
### `domain/ArchiveEngineTest.kt` (30)
- a zip round-trips folders and files
- createZip reports progress over the files it will write
- a zip-slip entry cannot escape the destination
- a single-file gzip is unwrapped to its inner name
- a single-file xz is unwrapped, which needs the optional xz library
- a plain tar round-trips its files
- a gzip tar round-trips its files
- a bzip2 tar round-trips its files
- an xz tar round-trips its files
- an unsupported format is refused rather than guessed at
- a 7z round-trips its files
- a 7z-slip entry cannot escape the destination
- 7z is refused with a readable reason when the backend cannot seek
- extracting something that is not there is reported
- listing reads names, sizes and nesting without extracting anything
- a listing is the same when the backend cannot seek
- a 7z is listed from its index
- a compressed file is listed as the one name inside it
- an unsupported archive cannot be listed either
- one entry can be extracted without unpacking the rest
- one entry can be extracted when the backend cannot seek
- one entry can be extracted from a 7z
- extracting a single hostile entry is refused
- an entry can be read without extracting anything
- an entry can be read when the backend cannot seek
- an entry can be read from a 7z, which has no stream per entry
- an entry inside a compressed wrapper is read under the name its archive implies
- reading refuses to hand out more than the limit
- an entry can be written straight to a file outside the backend
- reading an entry the archive does not hold is null
### `domain/DuplicateFinderTest.kt` (8)
- files holding the same bytes are one group, and the group says what it would give back
- three copies are one group of three
- files of the same length but different contents are never a group
- files that share a beginning but differ later are caught by the full pass
- a file that cannot be read is counted rather than guessed at
- groups are ordered by the space they would give back
- a file whose length nothing else shares is never read
- nothing to compare is nothing to report
### `domain/FileOperationsTest.kt` (19)
- copy brings a file across with its content
- copy recurses into folders
- copy creates the target folder when it does not exist
- copy reports progress that reaches the total
- keep both renames the incoming copy
- skip leaves the existing file untouched
- replace overwrites the existing file
- ask is surfaced to the caller and skipped by the engine
- move renames within the backend
- move falls back to copy and delete when rename is unavailable
- move keeps the source when the copy failed
- a failing file is reported and the batch continues
- an item that vanished is reported
- cancellation stops between entries and reports partial work
- delete removes files and folders
- delete reports an item that is already gone
- rename falls back to a copy and delete
- rename refuses to clobber an existing target
- createDirectory makes the folder
### `domain/FileSearchTest.kt` (8)
- finds a match however deep it is
- a hidden folder is skipped whole, not just hidden from the results
- hidden entries are searched when the preference asks for them
- a matching folder is still descended into
- matches stream out as they are found
- the walk stops at the limit
- globs work as they do everywhere else
- a query that matches nothing finds nothing
### `domain/SearchIndexerTest.kt` (14)
- a walk records every file under the volume
- a hidden folder is skipped whole when the preference says so
- rows are handed over in batches rather than one write per file
- progress is reported before the first batch is written
- a catch-up reads names and the metadata of nothing it already knows
- a catch-up finds a file that arrived and leaves the rest alone
- a catch-up drops a file that has gone
- a catch-up takes a folder that has gone with its whole subtree
- a folder that cannot be read is left exactly as it was
- a catch-up records that it was a catch-up and not a walk
- a catch-up with the other hidden-files preference forgets what it should not show
- a walk and a catch-up over a real tree keep the index level with it
- the timestamp is stamped from the clock when the pass finishes
- a second pass replaces the first rather than doubling it
### `domain/TrashTest.kt` (19)
- staging moves an item aside and records where it came from
- restoring puts an item back exactly where it was
- a folder comes back whole
- two items sharing a name stage without one overwriting the other
- restoring never overwrites a name that was taken again meanwhile
- expiry drops only the batches that are old enough
- staging nothing yields no undo handle
- restoring a batch that is already gone is reported, not thrown
- a batch folder is recognised by its name, and nothing else is
- the trash folder is hidden, the way Android hides dotfiles
- staging records where the item came from
- a staged delete is still recoverable after the process forgets everything
- the index is emptied once a restore has succeeded
- a row whose file has gone is not offered back
- purging forgets the rows it deletes
- purging drops rows whose folder has gone, so the index cannot grow forever
- what is offered back carries the name, the type, the size and where it came from
- one item can be put back without disturbing the rest of its batch
- a batch is forgotten once its last item is back
### `domain/UsageAnalysisTest.kt` (6)
- folder sizes are recursive and sorted largest first
- rows are reported as they are measured
- the largest files are found anywhere below the folder
- the total rolls up the whole tree
- an empty folder measures zero
- a folder that is gone measures nothing rather than blowing up
### `service/FileOpServiceTest.kt` (4)
- the channel is created once and reused
- an indeterminate operation says it is preparing
- a determinate operation shows the counter
- starting the service puts it in the foreground with that title
### `ui/browser/BrowserDialogsTest.kt` (5)
- the batch dialog spells the names out before anything moves
- a name the folder already holds is named, and the button is off
- files that already hold the names are said to be done
- a name with a slash is refused while it is typed
- a plain name is accepted
### `ui/browser/BrowserRootsTest.kt` (4)
- a starred folder shows its label, where it is, and how to remove it
- a favourite that has gone says so instead of its path
- a library the index knows nothing about shows no count
- a library with an index behind it shows what it holds
### `ui/browser/BrowserTopBarTest.kt` (4)
- inside a folder the star invites the folder to be starred
- a starred folder says so instead
- a library or the root list has nothing to star
- the star gives way to the search field
### `ui/browser/BrowserViewModelTest.kt` (102)
- starts on the root list
- opening a folder lists it with directories first and natural order
- hidden files are filtered out until asked for
- search narrows the listing
- sorting by size reorders the listing
- a folder of six hundred files is still listed in natural order
- copy to a chosen folder leaves the user where they are and keeps the selection
- move to a chosen folder takes the rows with it
- the picker offers folders and nothing else
- the picker names a volume the way the browser does
- the picker walks up to the volumes and back down
- a conflicting destination asks before overwriting, naming that folder
- cancelling the picker copies nothing
- a copy says where it went and offers to show it
- show takes the user to the folder the copy went to
- a paste into the folder on screen names no destination and offers no action
- the message's undo action is the undo
- a move to a chosen folder offers undo rather than show
- navigating up goes to the parent then to the roots
- selection survives toggling and clears on navigation
- a file with no preview of its own is handed to the system
- an image still opens in the built-in viewer, but can also be handed on
- the SAF fallback will not hand out a file, and says why
- a zip opens its contents instead of unpacking them
- an archive that cannot be read closes again and says so
- documents and media open in their own viewers
- markup is drawn, and the text editor stays one action away
- an apk is inspected rather than being handed to another app
- a text entry inside an archive is previewed without writing anything
- markup inside an archive is rendered, and not offered for editing
- an image entry is copied into the scratch space, and the copy goes with the preview
- a preview that would be too large is refused before anything is read
- an entry with no viewer is offered an extraction instead
- closing a preview of an entry returns to the archive it came from
- searching everywhere answers from the index, across folders
- switching scope re-answers the search rather than leaving the old results
- the index is rebuilt when it has never been built
- a fresh index is neither walked nor caught up
- a stale index is caught up, and the answer includes what the catch-up found
- a changed hidden-files preference asks for a walk rather than a catch-up
- a search from the volume list is answered everywhere, with no folder open
- starring the folder on screen adds it to the favourites
- starring it again takes it back out
- a favourite outlives the view model that added it
- the newest favourite comes first
- a favourite whose folder has gone is kept and marked
- opening a favourite that has gone says so instead of showing it empty
- a library cannot be starred
- opening a library lists the index's files of that kind and no folders
- a library brings the index up to date behind the answer
- a library row whose file has gone is dropped and forgotten
- returning to the app keeps the library open rather than dropping to the roots
- the home counts come from the index, and there are none without one
- finding duplicates compares the index's candidates and reports what matched
- a scan with nothing to compare says so rather than reporting a clean device
- deleting the ticked copies closes the scan rather than patching it
- ticking a copy does not tick every copy
- a search from inside a library is answered everywhere, because a library is not a folder
- a preview that needs a real file is refused in the SAF fallback
- createFolder makes a directory in the current folder
- createFolder refuses a name that is a path
- rename moves the entry to the new name
- delete removes the selected entries directly when the platform has no dialog
- delete prefers the platform dialog and leaves the files alone until it returns
- copy then paste duplicates into the current folder
- cut then paste moves and clears the clipboard
- a pasted name that already exists asks before overwriting
- without any grant the browser asks for a folder
- a granted tree becomes the only root
- focusing a folder in the master pane lists it for the detail pane
- focusing a file keeps the entry and previews no children
- the detail pane honours the hidden-files preference
- navigating clears the focus so the pane cannot show a stale folder
- a focused volume previews the contents of that volume
- the browsed folder and view settings survive a restart
- a search query is not restored, so the folder is not hidden on launch
- a selection and a clipboard do not survive a restart
- restarting on the roots screen stays on the roots screen
- search reaches into a subfolder, not just the one being browsed
- clearing the query brings the folder back
- navigating abandons a search, because its results belonged to the old folder
- the search honours the hidden-files preference
- a delete only moves the item aside, so it can be taken back
- undoing a delete puts the file back
- batch rename numbers the files in the order they are listed
- batch rename continues a series the folder already has
- a batch rename is a single undo step
- batch rename refuses a batch whose names are already taken
- a filename cannot be a path
- a batch rename cannot be given a path either
- undoing a rename puts the old name back
- undoing a cut-paste moves the item back
- a copy is not undoable, because nothing was changed
- the undo handle is cleared once it has been used
- a shared file is saved into the folder the user chose
- a shared file never overwrites one already in the folder
- a file the sender cannot name still lands under a usable one
- a share that cannot be read leaves nothing behind
- cancelling a share leaves an ordinary browser
- with no reader wired up, a share is not silently pretended
- while providing, tapping a file chooses it instead of opening it
- while providing, a folder still navigates so the file can actually be reached
### `ui/browser/SelectionBarTest.kt` (3)
- the destination verbs live in the overflow menu
- several files are offered a rename and not the details
- a single file keeps both
### Test files with no source file of the same name
- `ProjectInvariantsTest.kt` (4 tests) - ProjectInvariants, ProjectInvariants
---
Purpose taken from the first KDoc rather than from a declaration sharing the file's name (15 of 78): `MainActivity.kt`, `ArchiveFormats.kt`, `FileNames.kt`, `ApkInspector.kt`, `OperationModels.kt`, `TextRes.kt`, `BrowserComponents.kt`, `BrowserDetailPane.kt`, `BrowserDialogs.kt`, `BrowserRoots.kt`, `BrowserScreen.kt`, `EntryIntents.kt`, `Color.kt`, `Theme.kt`, `Type.kt`