Files
file-manager/docs/CODE_MAP.md
T
SoftwareApp 1371407435 Harden the thumbnail path: versioned cache keys, the icon as the placeholder, and a decided cache
Three things, each a failure the thumbnail path could have caused. First, correctness: Coil keys caches by the request, and a request built from a File is keyed by its path, so a picture replaced or edited outside the app kept showing its previous self - in a file manager that reads as data loss. core/ThumbnailKey now carries path, size and modification time, is built from the numbers the listing already has (no stat, so it stays off the main thread's disk work), and has four tests that name the two ways a file changes. The request carries it as the memory and disk cache key.

Second, the placeholder: the row drew an empty square while a thumbnail decoded, which a thousand-row folder makes very visible. The kind icon is now drawn first and the picture paints over it, so every row shows what it is immediately and the thumbnail replaces it without a flicker.

Third, the cache is decided rather than inherited: memory only, at 15% of the heap, and no disk cache at all - the sources are local files that decode cheaply, and a disk copy is a second copy that survives the file changing. Coil's default would have kept up to 2% of free space of stale thumbnails.

Measured on the phone and recorded in EDGE_CASE_TESTING: a 6200-file folder holding 300 real JPEGs opened and scrolled five times with no crash, no OOM and text rows keeping their icons (PSS 86.6 MB), and a folder of 300 real JPEGs where every row drew its own picture and eight swipes down and one back left PSS at 149.6 MB with the cache full and stable. Stated as not measured: jank on this screen (this device reports no frame durations) and a 20000-image folder.
2026-10-17 18:29:59 +02:00

35 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.

78 source files, 12856 lines - 34 test files, 390 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)
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 125 The "All files access" backend: plain java.io.File over absolute paths. AllFilesSource, File() AllFilesSourceTest (13)
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 96 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 279 Renaming several files at once, with the numbering worked out before anything moves. 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 591 BrowserScreen() -
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 311 One immutable description of everything the browser shows. ViewMode, SearchScope, Clipboard, UiMessage, ConflictPrompt, RootItem, Bookmark, DetailFocus, Undo, UndoMove, Overlay, BrowserUiState -
BrowserViewModel.kt 2152 Drives the browser screen. BrowserViewModel, ArchivePreview BrowserViewModelTest (89)
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 176 The contextual action bar shown while entries are selected. StorageAccessPrompt(), SelectionBar() SelectionBarTest (2)
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/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 (13)

  • 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 (89)

  • 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
  • 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 (2)

  • 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 77): 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