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.
38 KiB
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