Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
339f495
feat: add directory observation
ItzNotABug Aug 31, 2026
0357028
test: cover directory observation
ItzNotABug Aug 31, 2026
0e0abb4
feat: add observer sample
ItzNotABug Aug 31, 2026
1804097
docs: document directory observation
ItzNotABug Aug 31, 2026
5bd2240
ci: run instrumented tests
ItzNotABug Aug 31, 2026
1743cc3
fix: harden observer lifecycle
ItzNotABug Aug 31, 2026
39a8d9d
test: cover observer failure races
ItzNotABug Aug 31, 2026
b4a772a
ci: align required checks
ItzNotABug Aug 31, 2026
a9747fc
docs: clarify observer restart timing
ItzNotABug Aug 31, 2026
29b1528
refactor: extract directory observer
ItzNotABug Sep 1, 2026
51a7acf
chore: align file endings
ItzNotABug Sep 1, 2026
c71f09c
chore: clean up minor code details
ItzNotABug Sep 1, 2026
3bdb0df
perf: reduce observer snapshot overhead
ItzNotABug Sep 1, 2026
dfdd72e
test: benchmark observer snapshots
ItzNotABug Sep 1, 2026
1e48588
fix: tolerate transient provider query failures
ItzNotABug Sep 1, 2026
43610ae
ci: remove hosted instrumentation tests
ItzNotABug Sep 1, 2026
4758835
docs: clarify observer behavior
ItzNotABug Sep 1, 2026
f7e7219
test: synchronize null cursor retries
ItzNotABug Sep 1, 2026
8350129
perf: reduce observer startup work
ItzNotABug Sep 2, 2026
10c18db
docs: simplify observer guidance
ItzNotABug Sep 2, 2026
72c742e
fix: preserve modify events across renames
ItzNotABug Sep 2, 2026
0fefb11
fix: harden observer stop and failure handling
ItzNotABug Sep 2, 2026
7cf94bc
docs: clarify observer lifecycle and rename behavior
ItzNotABug Sep 2, 2026
6aa9842
ci: compile instrumentation tests
ItzNotABug Sep 2, 2026
cb1a278
test: remove snapshot benchmark harness
ItzNotABug Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,29 +1,45 @@
name: Build source

permissions:
contents: read

on:
workflow_dispatch:
pull_request:
types: [ opened, synchronize, reopened ]
paths:
- "dfc/**"
- "app/**"
- ".github/workflows/build.yml"
- "*.gradle"
- "gradle/**"
- "gradle.properties"
push:
branches: [ master ]
paths:
- "dfc/**"
- "app/**"
- ".github/workflows/build.yml"
- "*.gradle"
- "gradle/**"
- "gradle.properties"

jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false

- name: Set up JDK
uses: actions/setup-java@v4
uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4
with:
distribution: "temurin"
java-version: "17"
cache: "gradle"

- name: Build DFC
run: ./gradlew :dfc:assemble
- name: Build project
run: ./gradlew :dfc:assemble :dfc:assembleDebugAndroidTest :app:assembleDebug
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ do not keep paying for the same queries again and again.
- Raw `File` access via `fromFile(...)`.
- Common `DocumentFile`-style methods and getters.
- Faster directory listing and metadata access.
- Event-driven observation of a directory's direct children.
- Custom projections for lighter queries.
- Convenience APIs like `count()`, `copyTo(destination)`, and `copyFrom(source)`.

Expand Down Expand Up @@ -70,6 +71,32 @@ Other entry points:
Additional helpers like `count()`, `copyTo(destination)`, `copyFrom(source)`, and
`listFiles(projection)` are available when you need them.

### Observe a directory

`observe()` watches the direct children of a SAF tree directory without polling:

```kotlin
val observer = directory.observe { event, document ->
println("$event: ${document.name}")
}

observer.startWatching(
onError = { error -> println("Observation stopped: $error") },
onReady = { println("Initial snapshot is ready") },
)

// Later, from the owning lifecycle:
observer.close()
```

Files found during the initial scan aren't emitted. `onReady` runs after that scan, and `onError`
reports why watching stopped. Callbacks run on a worker thread; close the observer with its
lifecycle.

Observation depends on provider change notifications. Each refresh scans all direct children, and
rapid changes may be combined. Move events require stable document IDs; otherwise renames arrive as
delete and create events, which rename tracking should also handle.

## Performance

The sample app includes simple comparisons against AndroidX `DocumentFile`. Results depend on the
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
android:theme="@style/Theme.FileCompat"
tools:ignore="AllowBackup">

<activity
android:name=".ObserverActivity"
android:exported="false"
android:label="@string/observer_title"
android:parentActivityName=".MainActivity" />

<activity
android:name=".MainActivity"
android:exported="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) {
private lateinit var buttonDir: Button
private lateinit var buttonFile: Button
private lateinit var buttonProjections: Button
private lateinit var buttonObserver: Button

private lateinit var textView: TextView
private lateinit var progress: ProgressBar
Expand All @@ -44,6 +45,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) {
buttonDir.isVisible = false
buttonFile.isVisible = false
buttonProjections.isVisible = false
buttonObserver.isVisible = false

val generationResult = TestFileGenerator.generateTestFiles(
this@MainActivity, documentUri, selectedFileCount
Expand All @@ -53,6 +55,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) {
buttonDir.isVisible = true
buttonFile.isVisible = true
buttonProjections.isVisible = true
buttonObserver.isVisible = true
textView.text = generationResult
}
}
Expand All @@ -71,6 +74,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) {
buttonDir.isVisible = false
buttonFile.isVisible = false
buttonProjections.isVisible = false
buttonObserver.isVisible = false
val performanceResult = withContext(Dispatchers.IO) {
Performance.calculateDirectoryPerformance(
this@MainActivity, documentUri
Expand All @@ -81,6 +85,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) {
buttonDir.isVisible = true
buttonFile.isVisible = true
buttonProjections.isVisible = true
buttonObserver.isVisible = true
textView.text = performanceResult
}
}
Expand Down Expand Up @@ -120,6 +125,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) {
buttonDir.isVisible = false
buttonFile.isVisible = false
buttonProjections.isVisible = false
buttonObserver.isVisible = false

val performanceResult = withContext(Dispatchers.IO) {
Performance.calculateProjectionPerformance(
Expand All @@ -131,6 +137,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) {
buttonDir.isVisible = true
buttonFile.isVisible = true
buttonProjections.isVisible = true
buttonObserver.isVisible = true
textView.text = performanceResult
}
}
Expand All @@ -143,6 +150,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) {
buttonDir = findViewById(R.id.buttonDir)
buttonFile = findViewById(R.id.buttonFile)
buttonProjections = findViewById(R.id.buttonProjections)
buttonObserver = findViewById(R.id.buttonObserver)
textView = findViewById(R.id.fileNames)
progress = findViewById(R.id.progress)

Expand All @@ -159,6 +167,10 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) {
buttonProjections.setOnClickListener {
projectionResultLauncher.launch(getStorageIntent())
}

buttonObserver.setOnClickListener {
startActivity(Intent(this, ObserverActivity::class.java))
}
}

override fun onCreateOptionsMenu(menu: Menu): Boolean {
Expand Down
Loading
Loading