Skip to content

Commit b3b64c4

Browse files
authored
Merge pull request #36 from ItzNotABug/observer
Add SAF directory observation
2 parents 73a3b84 + 26aaa92 commit b3b64c4

27 files changed

Lines changed: 3626 additions & 67 deletions

File tree

‎.github/workflows/build.yml‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,45 @@
11
name: Build source
22

3+
permissions:
4+
contents: read
5+
36
on:
47
workflow_dispatch:
58
pull_request:
69
types: [ opened, synchronize, reopened ]
710
paths:
811
- "dfc/**"
12+
- "app/**"
13+
- ".github/workflows/build.yml"
14+
- "*.gradle"
15+
- "gradle/**"
16+
- "gradle.properties"
917
push:
1018
branches: [ master ]
1119
paths:
1220
- "dfc/**"
21+
- "app/**"
22+
- ".github/workflows/build.yml"
23+
- "*.gradle"
24+
- "gradle/**"
25+
- "gradle.properties"
1326

1427
jobs:
1528
build:
29+
name: Build
1630
runs-on: ubuntu-latest
1731
steps:
1832
- name: Checkout
19-
uses: actions/checkout@v4
33+
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
34+
with:
35+
persist-credentials: false
2036

2137
- name: Set up JDK
22-
uses: actions/setup-java@v4
38+
uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4
2339
with:
2440
distribution: "temurin"
2541
java-version: "17"
2642
cache: "gradle"
2743

28-
- name: Build DFC
29-
run: ./gradlew :dfc:assemble
44+
- name: Build project
45+
run: ./gradlew :dfc:assemble :dfc:assembleDebugAndroidTest :app:assembleDebug

‎README.md‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ do not keep paying for the same queries again and again.
2121
- Raw `File` access via `fromFile(...)`.
2222
- Common `DocumentFile`-style methods and getters.
2323
- Faster directory listing and metadata access.
24+
- Event-driven observation of a directory's direct children.
2425
- Custom projections for lighter queries.
2526
- Convenience APIs like `count()`, `copyTo(destination)`, and `copyFrom(source)`.
2627

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

74+
### Observe a directory
75+
76+
`observe()` watches the direct children of a SAF tree directory without polling:
77+
78+
```kotlin
79+
val observer = directory.observe { event, document ->
80+
println("$event: ${document.name}")
81+
}
82+
83+
observer.startWatching(
84+
onError = { error -> println("Observation stopped: $error") },
85+
onReady = { println("Initial snapshot is ready") },
86+
)
87+
88+
// Later, from the owning lifecycle:
89+
observer.close()
90+
```
91+
92+
Files found during the initial scan aren't emitted. `onReady` runs after that scan, and `onError`
93+
reports why watching stopped. Callbacks run on a worker thread; close the observer with its
94+
lifecycle.
95+
96+
Observation depends on provider change notifications. Each refresh scans all direct children, and
97+
rapid changes may be combined. Move events require stable document IDs; otherwise renames arrive as
98+
delete and create events, which rename tracking should also handle.
99+
73100
## Performance
74101

75102
The sample app includes simple comparisons against AndroidX `DocumentFile`. Results depend on the

‎app/src/main/AndroidManifest.xml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818
android:theme="@style/Theme.FileCompat"
1919
tools:ignore="AllowBackup">
2020

21+
<activity
22+
android:name=".ObserverActivity"
23+
android:exported="false"
24+
android:label="@string/observer_title"
25+
android:parentActivityName=".MainActivity" />
26+
2127
<activity
2228
android:name=".MainActivity"
2329
android:exported="true"

‎app/src/main/java/com/lazygeniouz/filecompat/example/MainActivity.kt‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) {
2626
private lateinit var buttonDir: Button
2727
private lateinit var buttonFile: Button
2828
private lateinit var buttonProjections: Button
29+
private lateinit var buttonObserver: Button
2930

3031
private lateinit var textView: TextView
3132
private lateinit var progress: ProgressBar
@@ -44,6 +45,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) {
4445
buttonDir.isVisible = false
4546
buttonFile.isVisible = false
4647
buttonProjections.isVisible = false
48+
buttonObserver.isVisible = false
4749

4850
val generationResult = TestFileGenerator.generateTestFiles(
4951
this@MainActivity, documentUri, selectedFileCount
@@ -53,6 +55,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) {
5355
buttonDir.isVisible = true
5456
buttonFile.isVisible = true
5557
buttonProjections.isVisible = true
58+
buttonObserver.isVisible = true
5659
textView.text = generationResult
5760
}
5861
}
@@ -71,6 +74,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) {
7174
buttonDir.isVisible = false
7275
buttonFile.isVisible = false
7376
buttonProjections.isVisible = false
77+
buttonObserver.isVisible = false
7478
val performanceResult = withContext(Dispatchers.IO) {
7579
Performance.calculateDirectoryPerformance(
7680
this@MainActivity, documentUri
@@ -81,6 +85,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) {
8185
buttonDir.isVisible = true
8286
buttonFile.isVisible = true
8387
buttonProjections.isVisible = true
88+
buttonObserver.isVisible = true
8489
textView.text = performanceResult
8590
}
8691
}
@@ -120,6 +125,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) {
120125
buttonDir.isVisible = false
121126
buttonFile.isVisible = false
122127
buttonProjections.isVisible = false
128+
buttonObserver.isVisible = false
123129

124130
val performanceResult = withContext(Dispatchers.IO) {
125131
Performance.calculateProjectionPerformance(
@@ -131,6 +137,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) {
131137
buttonDir.isVisible = true
132138
buttonFile.isVisible = true
133139
buttonProjections.isVisible = true
140+
buttonObserver.isVisible = true
134141
textView.text = performanceResult
135142
}
136143
}
@@ -143,6 +150,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) {
143150
buttonDir = findViewById(R.id.buttonDir)
144151
buttonFile = findViewById(R.id.buttonFile)
145152
buttonProjections = findViewById(R.id.buttonProjections)
153+
buttonObserver = findViewById(R.id.buttonObserver)
146154
textView = findViewById(R.id.fileNames)
147155
progress = findViewById(R.id.progress)
148156

@@ -159,6 +167,10 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) {
159167
buttonProjections.setOnClickListener {
160168
projectionResultLauncher.launch(getStorageIntent())
161169
}
170+
171+
buttonObserver.setOnClickListener {
172+
startActivity(Intent(this, ObserverActivity::class.java))
173+
}
162174
}
163175

164176
override fun onCreateOptionsMenu(menu: Menu): Boolean {

0 commit comments

Comments
 (0)