f4ca5f22ae
The phone set was two days old and predated M16-M20: nothing in it showed the copy-to picker, the trash's window or the compress dialog. It is now eight shots (Play's maximum) taken from the signed release build over the demo library: home, folder list, photo grid, storage usage, sort sheet, Copy to..., the trash with its 24-hour line and Empty trash, and the compress dialog with its switch. The shots have to stay inside the demo library, so two things were deliberately not captured: the home screen (its library counts are the real phone's - the old AVD-shot file is kept) and the picker one level down (it would name real folders). The picker is shot at the volume list instead. Taking the picker shot exposed a real bug: it listed '.plainfiles-trash' among the destinations, because unlike every other listing it did not filter hidden entries. Hidden folders are now offered only when the listing shows hidden files, with a test for both halves. The tablet sets are unchanged. The tablet AVDs cannot boot on this machine (no KVM) and the phone-resized path needs more navigation work than this session had left; the checklist says so and records what to regenerate and where.
731 lines
38 KiB
Markdown
731 lines
38 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.
|
|
|
|
80 source files, 13902 lines - 37 test files, 431 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) |
|
|
| `ShortcutRoute.kt` | 27 | Where a launcher shortcut asks the app to open. ShortcutRoute | `ShortcutRouteTest` (2) |
|
|
| `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` | 667 | Creates and extracts archives, on top of the storage backends. ArchiveEntry, ArchiveEngine | `ArchiveEngineTest` (31) |
|
|
| `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` | 360 | A staging folder that makes a delete undoable. TrashedEntry, TrashBatch, Trash | `TrashTest` (20) |
|
|
| `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` | 365 | The thumbnail an entry's leading visual gets in a list row. ListThumbnailSize, GridThumbnailSize, GridCellMinSize, EntryIcon(), FileEntry(), FileEntry(), VIDEO_FRAME_MILLIS, iconFor(), iconFor(), MarkOfChange(), EntryRow(), EntryGridCell(), FileEntry() | `BrowserComponentsTest` (3) |
|
|
| `BrowserContent.kt` | 465 | 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` | 444 | Compressing something, with the one choice that changes what comes out. CompressDialog(), 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` | 661 | The browser, and everything that can be on top of it. 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` | 391 | One immutable description of everything the browser shows. ViewMode, SearchScope, Clipboard, CompressRequest, PickerFolder, TransferPicker, MessageAction, UiMessage, ConflictPrompt, RootItem, Bookmark, DetailFocus, Undo, UndoMove, Overlay, BrowserUiState | - |
|
|
| `BrowserViewModel.kt` | 2601 | Drives the browser screen. BrowserViewModel, ArchivePreview | `BrowserViewModelTest` (117) |
|
|
| `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` | 220 | 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` | 69 | 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/ShortcutRouteTest.kt` (2)
|
|
|
|
- each shortcut key maps to its route
|
|
- anything else is ignored rather than thrown
|
|
|
|
### `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` (31)
|
|
|
|
- a zip round-trips folders and files
|
|
- an uncompressed zip round-trips and is larger than a squeezed one
|
|
- 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` (20)
|
|
|
|
- emptying removes every batch and leaves what is not one
|
|
- 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/BrowserComponentsTest.kt` (3)
|
|
|
|
- a row an operation produced carries the mark
|
|
- an ordinary row carries no mark
|
|
- a grid cell an operation produced carries the mark too
|
|
|
|
### `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` (117)
|
|
|
|
- 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 leaves hidden folders out until they are asked for
|
|
- 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
|
|
- a copy into the folder on screen marks the copy
|
|
- a copy into another folder marks it there, not here
|
|
- reading the folder again spends its marks
|
|
- a rename marks the name it produced
|
|
- compressing asks for a name and a choice before writing
|
|
- a confirmed compress writes the name that was typed, and marks it
|
|
- cancelling the compress dialog writes nothing
|
|
- opening the trash sweeps what has expired
|
|
- opening the trash keeps what is still inside the window
|
|
- emptying the trash removes what is in it
|
|
- a shortcut opens the view it names
|
|
- typing while the index is being built does not start it again
|
|
- switching library while the index is being built does not start it again
|
|
- a search is answered again when the pass finds what it was looking for
|
|
- 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 (14 of 79): `MainActivity.kt`, `ArchiveFormats.kt`, `FileNames.kt`, `ApkInspector.kt`, `OperationModels.kt`, `TextRes.kt`, `BrowserComponents.kt`, `BrowserDetailPane.kt`, `BrowserDialogs.kt`, `BrowserRoots.kt`, `EntryIntents.kt`, `Color.kt`, `Theme.kt`, `Type.kt`
|