Skip to content

Commit 61cbaa1

Browse files
committed
Implement getDirectory, resolveRelativePath
1 parent 1015ba8 commit 61cbaa1

File tree

5 files changed

+458
-0
lines changed

5 files changed

+458
-0
lines changed

android/src/main/kotlin/codeux/design/filepicker/file_picker_writable/FilePickerWritableImpl.kt

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,70 @@ class FilePickerWritableImpl(
261261
copyContentUriAndReturn(result, Uri.parse(identifier))
262262
}
263263

264+
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
265+
@MainThread
266+
suspend fun getDirectory(
267+
result: MethodChannel.Result,
268+
rootUri: String,
269+
fileUri: String
270+
) {
271+
val activity = requireActivity()
272+
273+
val root = Uri.parse(rootUri)
274+
val leaf = Uri.parse(fileUri)
275+
val leafUnderRoot = DocumentsContract.buildDocumentUriUsingTree(
276+
root,
277+
DocumentsContract.getDocumentId(leaf)
278+
)
279+
280+
if (!fileExists(leafUnderRoot, activity.applicationContext.contentResolver)) {
281+
result.error(
282+
"InvalidArguments",
283+
"The supplied fileUri $fileUri is not a child of $rootUri",
284+
null
285+
)
286+
return
287+
}
288+
289+
val ret = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
290+
getParent(leafUnderRoot, activity.applicationContext)
291+
} else {
292+
null
293+
} ?: findParent(root, leaf, activity.applicationContext)
294+
295+
296+
result.success(mapOf(
297+
"identifier" to ret.toString(),
298+
"persistable" to "true",
299+
"uri" to ret.toString()
300+
))
301+
}
302+
303+
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
304+
@MainThread
305+
suspend fun resolveRelativePath(
306+
result: MethodChannel.Result,
307+
parentIdentifier: String,
308+
relativePath: String
309+
) {
310+
val activity = requireActivity()
311+
312+
val resolvedUri = resolveRelativePath(Uri.parse(parentIdentifier), relativePath, activity.applicationContext)
313+
if (resolvedUri != null) {
314+
val displayName = getDisplayName(resolvedUri, activity.applicationContext.contentResolver)
315+
val isDirectory = isDirectory(resolvedUri, activity.applicationContext.contentResolver)
316+
result.success(mapOf(
317+
"identifier" to resolvedUri.toString(),
318+
"persistable" to "true",
319+
"fileName" to displayName,
320+
"uri" to resolvedUri.toString(),
321+
"isDirectory" to isDirectory.toString()
322+
))
323+
} else {
324+
result.error("FileNotFound", "$relativePath could not be located relative to $parentIdentifier", null)
325+
}
326+
}
327+
264328
@MainThread
265329
private suspend fun copyContentUriAndReturn(
266330
result: MethodChannel.Result,

android/src/main/kotlin/codeux/design/filepicker/file_picker_writable/FilePickerWritablePlugin.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,28 @@ class FilePickerWritablePlugin : FlutterPlugin, MethodCallHandler,
128128
?: throw FilePickerException("Expected argument 'identifier'")
129129
impl.readFileWithIdentifier(result, identifier)
130130
}
131+
"getDirectory" -> {
132+
val rootIdentifier = call.argument<String>("rootIdentifier")
133+
?: throw FilePickerException("Expected argument 'rootIdentifier'")
134+
val fileIdentifier = call.argument<String>("fileIdentifier")
135+
?: throw FilePickerException("Expected argument 'fileIdentifier'")
136+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
137+
impl.getDirectory(result, rootIdentifier, fileIdentifier)
138+
} else {
139+
throw FilePickerException("${call.method} is not supported on Android ${Build.VERSION.RELEASE}")
140+
}
141+
}
142+
"resolveRelativePath" -> {
143+
val directoryIdentifier = call.argument<String>("directoryIdentifier")
144+
?: throw FilePickerException("Expected argument 'directoryIdentifier'")
145+
val relativePath = call.argument<String>("relativePath")
146+
?: throw FilePickerException("Expected argument 'relativePath'")
147+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
148+
impl.resolveRelativePath(result, directoryIdentifier, relativePath)
149+
} else {
150+
throw FilePickerException("${call.method} is not supported on Android ${Build.VERSION.RELEASE}")
151+
}
152+
}
131153
"writeFileWithIdentifier" -> {
132154
val identifier = call.argument<String>("identifier")
133155
?: throw FilePickerException("Expected argument 'identifier'")

android/src/main/kotlin/codeux/design/filepicker/file_picker_writable/Query.kt

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package codeux.design.filepicker.file_picker_writable
22

33
import android.content.ContentResolver
4+
import android.content.Context
45
import android.database.Cursor
56
import android.net.Uri
7+
import android.os.Build
8+
import android.provider.DocumentsContract
69
import android.provider.OpenableColumns
10+
import androidx.annotation.RequiresApi
711
import kotlinx.coroutines.Dispatchers
812
import kotlinx.coroutines.withContext
913

@@ -36,3 +40,257 @@ suspend fun getDisplayName(
3640

3741
} ?: throw FilePickerException("Unable to load file info from $uri")
3842
}
43+
44+
/**
45+
* Determine whether [uri] is a directory.
46+
*
47+
* - Expects: {Tree+}document URI
48+
*/
49+
suspend fun isDirectory(
50+
uri: Uri,
51+
contentResolver: ContentResolver
52+
): Boolean = withContext(Dispatchers.IO) {
53+
// Like DocumentsContractApi19#isDirectory
54+
contentResolver.query(
55+
uri, arrayOf(DocumentsContract.Document.COLUMN_MIME_TYPE), null, null, null, null
56+
)?.use {
57+
if (!it.moveToFirst()) {
58+
throw FilePickerException("Cursor returned empty while trying to read info for $uri")
59+
}
60+
val typeColumn = it.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE)
61+
val childType = it.getString(typeColumn)
62+
DocumentsContract.Document.MIME_TYPE_DIR == childType
63+
} ?: throw FilePickerException("Unable to query info for $uri")
64+
}
65+
66+
67+
/**
68+
* Directly compute the URI of the parent directory of the supplied child URI.
69+
* Efficient, but only available on Android O or later.
70+
*
71+
* - Expects: Tree{+document} URI
72+
* - Returns: Tree{+document} URI
73+
*/
74+
@RequiresApi(Build.VERSION_CODES.O)
75+
suspend fun getParent(
76+
child: Uri,
77+
context: Context
78+
): Uri? = withContext(Dispatchers.IO) {
79+
val uri = when {
80+
DocumentsContract.isDocumentUri(context, child) -> {
81+
// Tree+document URI (probably from getDirectory)
82+
child
83+
}
84+
DocumentsContract.isTreeUri(child) -> {
85+
// Just a tree URI (probably from pickDirectory)
86+
DocumentsContract.buildDocumentUriUsingTree(child, DocumentsContract.getTreeDocumentId(child))
87+
}
88+
else -> {
89+
throw Exception("Unknown URI type")
90+
}
91+
}
92+
val path = DocumentsContract.findDocumentPath(context.contentResolver, uri)
93+
?: return@withContext null
94+
val parents = path.path
95+
if (parents.size < 2) {
96+
return@withContext null
97+
}
98+
// Last item is the child itself, so get second-to-last item
99+
val parent = parents[parents.lastIndex - 1]
100+
when {
101+
DocumentsContract.isTreeUri(child) -> {
102+
DocumentsContract.buildDocumentUriUsingTree(child, parent)
103+
}
104+
else -> {
105+
DocumentsContract.buildTreeDocumentUri(child.authority, parent)
106+
}
107+
}
108+
}
109+
110+
/**
111+
* Starting at [root], perform a breadth-wise search through all children to
112+
* locate the immediate parent of [leaf].
113+
*
114+
* This is extremely inefficient compared to [getParent], but it is available on
115+
* older systems.
116+
*
117+
* - Expects: [root] is Tree{+document} URI; [leaf] is {tree+}document URI
118+
* - Returns: Tree+document URI
119+
*/
120+
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
121+
suspend fun findParent(
122+
root: Uri,
123+
leaf: Uri,
124+
context: Context
125+
): Uri? {
126+
val leafDocId = DocumentsContract.getDocumentId(leaf)
127+
val children = getChildren(root, context)
128+
// Do breadth-first search because hopefully the leaf is not too deep
129+
// relative to the root
130+
for (child in children) {
131+
if (DocumentsContract.getDocumentId(child) == leafDocId) {
132+
return root
133+
}
134+
}
135+
for (child in children) {
136+
if (isDirectory(child, context.contentResolver)) {
137+
val result = findParent(child, leaf, context)
138+
if (result != null) {
139+
return result
140+
}
141+
}
142+
}
143+
return null
144+
}
145+
146+
/**
147+
* Return URIs of all children of [uri].
148+
*
149+
* - Expects: Tree{+document} or tree URI
150+
* - Returns: Tree+document URI
151+
*/
152+
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
153+
suspend fun getChildren(
154+
uri: Uri,
155+
context: Context
156+
): List<Uri> = withContext(Dispatchers.IO) {
157+
// Like TreeDocumentFile#listFiles
158+
val docId = when {
159+
DocumentsContract.isDocumentUri(context, uri) -> {
160+
DocumentsContract.getDocumentId(uri)
161+
}
162+
else -> {
163+
DocumentsContract.getTreeDocumentId(uri)
164+
}
165+
}
166+
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, docId)
167+
context.contentResolver.query(
168+
childrenUri,
169+
arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID), null, null, null, null
170+
)?.use {
171+
val results = mutableListOf<Uri>()
172+
val idColumn = it.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
173+
while (it.moveToNext()) {
174+
val childDocId = it.getString(idColumn)
175+
val childUri = DocumentsContract.buildDocumentUriUsingTree(uri, childDocId)
176+
results.add(childUri)
177+
}
178+
results
179+
} ?: throw FilePickerException("Unable to query info for $uri")
180+
}
181+
182+
/**
183+
* Check whether the file pointed to by [uri] exists.
184+
*
185+
* - Expects: {Tree+}document URI
186+
*/
187+
suspend fun fileExists(
188+
uri: Uri,
189+
contentResolver: ContentResolver
190+
): Boolean = withContext(Dispatchers.IO) {
191+
// Like DocumentsContractApi19#exists
192+
contentResolver.query(
193+
uri, arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID), null, null, null, null
194+
)?.use {
195+
it.count > 0
196+
} ?: throw FilePickerException("Unable to query info for $uri")
197+
}
198+
199+
/**
200+
* From the [start] point, compute the URI of the entity pointed to by
201+
* [relativePath].
202+
*
203+
* - Expects: Tree{+document} URI
204+
* - Returns: Tree{+document} URI
205+
*/
206+
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
207+
suspend fun resolveRelativePath(
208+
start: Uri,
209+
relativePath: String,
210+
context: Context
211+
): Uri? = withContext(Dispatchers.IO) {
212+
val stack = mutableListOf(start)
213+
for (segment in relativePath.split('/', '\\')) {
214+
when (segment) {
215+
"" -> {
216+
}
217+
"." -> {
218+
}
219+
".." -> {
220+
val last = stack.removeAt(stack.lastIndex)
221+
if (stack.isEmpty()) {
222+
val parent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
223+
getParent(last, context)
224+
} else {
225+
null
226+
}
227+
if (parent != null) {
228+
stack.add(parent)
229+
} else {
230+
return@withContext null
231+
}
232+
}
233+
}
234+
else -> {
235+
val next = getChildByDisplayName(stack.last(), segment, context)
236+
if (next == null) {
237+
return@withContext null
238+
} else {
239+
stack.add(next)
240+
}
241+
}
242+
}
243+
}
244+
stack.last()
245+
}
246+
247+
/**
248+
* Compute the URI of the named [child] under [parent].
249+
*
250+
* - Expects: Tree{+document} URI
251+
* - Returns: Tree+document URI
252+
*/
253+
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
254+
suspend fun getChildByDisplayName(
255+
parent: Uri,
256+
child: String,
257+
context: Context
258+
): Uri? = withContext(Dispatchers.IO) {
259+
val parentDocumentId = when {
260+
DocumentsContract.isDocumentUri(context, parent) -> {
261+
// Tree+document URI (probably from getDirectory)
262+
DocumentsContract.getDocumentId(parent)
263+
}
264+
else -> {
265+
// Just a tree URI (probably from pickDirectory)
266+
DocumentsContract.getTreeDocumentId(parent)
267+
}
268+
}
269+
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(parent, parentDocumentId)
270+
context.contentResolver.query(
271+
childrenUri,
272+
arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME),
273+
"${DocumentsContract.Document.COLUMN_DISPLAY_NAME} = ?",
274+
arrayOf(child),
275+
null
276+
)?.use {
277+
val idColumn = it.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
278+
val nameColumn = it.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
279+
var documentId: String? = null
280+
while (it.moveToNext()) {
281+
val name = it.getString(nameColumn)
282+
// FileSystemProvider doesn't respect our selection so we have to
283+
// manually filter here to be safe
284+
if (name == child) {
285+
documentId = it.getString(idColumn)
286+
break
287+
}
288+
}
289+
290+
if (documentId != null) {
291+
DocumentsContract.buildDocumentUriUsingTree(parent, documentId)
292+
} else {
293+
null
294+
}
295+
}
296+
}

0 commit comments

Comments
 (0)