Files
file-manager/docs/CODE_MAP.md
T
SoftwareApp 0743bda29e Let a folder be starred, and put the starred ones on the home screen
The app bar of any folder now carries a star, and a starred folder appears in a Favourites section
on the home screen, above the libraries. Above, because it is the user's own list - and absent
entirely while empty, so someone opening the app for the first time still meets the libraries first,
which is the order they were given for a reason. The star in the row is the removal, which is what a
star means everywhere else: the mark that says "this is a favourite" is the thing you tap to stop it
being one. That avoids a long-press as well, and with it a destructive gesture nobody can see.

A favourite is a reference, not a copy: data/BookmarkStore keeps one path per row and nothing about
the folder's contents, so reading the whole list is one query and no filesystem work, which is what
makes it affordable on every visit to the home screen. It has its own SQLite database rather than a
table in the trash index - one table and three queries do not justify Room in a build that has no
annotation processor, and the two stores have nothing to say to each other. Two behaviours there are
decisions rather than defaults. A folder that has gone is kept and marked (Folder not found) instead
of being deleted: a card out of its slot and a folder deleted in another app are indistinguishable
from here, and silently forgetting someone's list because their SD card was out is the worse of the
two mistakes. And opening the row re-checks existence at that moment, because the flag is as old as
the screen - so the answer is a message rather than an empty listing, which would read as an empty
folder instead of a missing one.

Two smaller refusals came out of the same question. There is no star in a library or on the root
screen, because neither is a folder and there would be nothing to come back to: the distinction that
governs the new-folder button, and one the view model enforces too rather than trusting a stale
screen. And a bookmark has no undo, because Undo means "the last change to the filesystem the app
made" and a favourite is not one - the star is one tap to put back. The row's label comes from the
same place the breadcrumbs do, which is why a starred volume root reads as "Internal storage" rather
than as "0".

Sixteen tests: nine against real SQLite through Robolectric (round trip, ordering, starring twice
neither duplicating nor moving a row, and the same no-drop migration guard the trash index carries),
and seven through the view model (toggling, outliving the view model that added it, newest first,
the missing folder kept and marked, the missing folder refusing to open, and a library refusing to be
starred). 371 JVM tests, lint clean. Verified on the phone: the star appears only inside a folder and
flips from "Add to favourites" to "Remove from favourites" without leaving the screen; the home
screen lists the volume root above the libraries with its own label and its path underneath; the row
opens it with the star already filled; and the row's star takes the whole section away with a
snackbar. The documentation moved with it - CHANGELOG, ROADMAP (M15), README, COMPETITIVE_ANALYSIS
(that gap closes instead of joining the candidates), EDGE_CASE_TESTING, and the generated code map.
2026-10-17 07:48:26 +02:00

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

76 source files, 12462 lines - 29 test files, 371 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)
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 117 The starred folders, on SQLite. SqliteBookmarkStore SqliteBookmarkStoreTest (9)
SqliteSearchIndex.kt 438 The search index, on SQLite. SqliteSearchIndex SqliteSearchIndexTest (27)
SqliteTrashIndex.kt 162 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 301 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() -
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() -
BrowserScreen.kt 592 BrowserScreen() -
BrowserTopBar.kt 238 The browser's app bar: title, up, the favourite star, and the search / sort / overflow actions. BrowserTopBar(), SEARCH_FIELD_WIDTH -
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 2100 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() -
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 97 Application entry point and the app's tiny object graph. FileManagerApplication -
MainActivity.kt 65 MainActivity -

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/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/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

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 76): 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