diff --git a/README.md b/README.md index 9604a2e..e4cb39c 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ const appendFileToFormData = async () => { +* [`pickDirectory()`](#pickdirectory) * [`pickFiles(...)`](#pickfiles) * [`pickImages(...)`](#pickimages) * [`pickMedia(...)`](#pickmedia) @@ -104,6 +105,25 @@ const appendFileToFormData = async () => { +### pickDirectory() + +```typescript +pickDirectory() => Promise +``` + +Pick a directory. + +Returns a security-scoped URL for the directory that permits your app to access content outside its container. + +Only available on Android and iOS. + +**Returns:** Promise<PickDirectoryResult> + +**Since:** 0.5.7 + +-------------------- + + ### pickFiles(...) ```typescript @@ -193,6 +213,13 @@ Only available on Android and iOS. ### Interfaces +#### PickDirectoryResult + +| Prop | Type | Description | Since | +| ---------- | ------------------- | -------------------------- | ----- | +| **`path`** | string | The path of the directory. | 0.5.7 | + + #### PickFilesResult | Prop | Type | diff --git a/android/src/main/java/io/capawesome/capacitorjs/plugins/filepicker/FilePickerHelper.java b/android/src/main/java/io/capawesome/capacitorjs/plugins/filepicker/FilePickerHelper.java new file mode 100644 index 0000000..7f95439 --- /dev/null +++ b/android/src/main/java/io/capawesome/capacitorjs/plugins/filepicker/FilePickerHelper.java @@ -0,0 +1,67 @@ +package io.capawesome.capacitorjs.plugins.filepicker; + +import android.content.ContentResolver; +import android.database.Cursor; +import android.net.Uri; +import android.provider.DocumentsContract; +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.getcapacitor.JSArray; +import com.getcapacitor.Logger; + +import org.json.JSONException; + +import java.util.LinkedList; +import java.util.List; + +public class FilePickerHelper { + + @Nullable + public static String[] parseTypesOption(@Nullable JSArray types) { + if (types == null) { + return null; + } + try { + List typesList = types.toList(); + if (typesList.contains("text/csv")) { + typesList.add("text/comma-separated-values"); + } + return typesList.toArray(new String[0]); + } catch (JSONException exception) { + Logger.error("parseTypesOption failed.", exception); + return null; + } + } + + public static void traverseDirectoryEntries(ContentResolver contentResolver, Uri rootUri) { + Uri childrenUri; + try { + //for childs and sub child dirs + childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, DocumentsContract.getDocumentId(rootUri)); + } catch (Exception e) { + // for parent dir + childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, DocumentsContract.getTreeDocumentId(rootUri)); + } + + List dirNodes = new LinkedList<>(); + dirNodes.add(childrenUri); + + while(!dirNodes.isEmpty()) { + // Get the item from the top + childrenUri = dirNodes.remove(0); + Cursor c = contentResolver.query(childrenUri, new String[]{DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_MIME_TYPE}, null, null, null); + while (c.moveToNext()) { + final String docId = c.getString(0); + final String name = c.getString(1); + final String mime = c.getString(2); + Log.d("sss", "childrenUri: " + childrenUri + ", docId: " + docId + ", name: " + name + ", mime: " + mime); + //if(isDirectory(mime)) { + // final Uri newNode = DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, docId); + // dirNodes.add(newNode); + //} + } + } + } +} diff --git a/android/src/main/java/io/capawesome/capacitorjs/plugins/filepicker/FilePickerPlugin.java b/android/src/main/java/io/capawesome/capacitorjs/plugins/filepicker/FilePickerPlugin.java index 685a018..0213fdb 100644 --- a/android/src/main/java/io/capawesome/capacitorjs/plugins/filepicker/FilePickerPlugin.java +++ b/android/src/main/java/io/capawesome/capacitorjs/plugins/filepicker/FilePickerPlugin.java @@ -3,6 +3,7 @@ import android.app.Activity; import android.content.Intent; import android.net.Uri; +import android.provider.DocumentsContract; import android.util.Log; import androidx.activity.result.ActivityResult; import androidx.annotation.Nullable; @@ -14,6 +15,8 @@ import com.getcapacitor.PluginMethod; import com.getcapacitor.annotation.ActivityCallback; import com.getcapacitor.annotation.CapacitorPlugin; + +import java.io.File; import java.util.ArrayList; import java.util.List; import org.json.JSONException; @@ -23,6 +26,8 @@ public class FilePickerPlugin extends Plugin { public static final String TAG = "FilePickerPlugin"; + public static final String ERROR_PICK_DIRECTORY_FAILED = "pickDirectory failed."; + public static final String ERROR_PICK_DIRECTORY_CANCELED = "pickDirectory canceled."; public static final String ERROR_PICK_FILE_FAILED = "pickFiles failed."; public static final String ERROR_PICK_FILE_CANCELED = "pickFiles canceled."; private FilePicker implementation; @@ -31,12 +36,30 @@ public void load() { implementation = new FilePicker(this.getBridge()); } + @PluginMethod + public void pickDirectory(PluginCall call) { + try { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + intent.addFlags( + Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); + + startActivityForResult(call, intent, "pickDirectoryResult"); + } catch (Exception ex) { + String message = ex.getMessage(); + Log.e(TAG, message); + call.reject(message); + } + } + @PluginMethod public void pickFiles(PluginCall call) { try { JSArray types = call.getArray("types", null); boolean multiple = call.getBoolean("multiple", false); - String[] parsedTypes = parseTypesOption(types); + String[] parsedTypes = FilePickerHelper.parseTypesOption(types); Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("*/*"); @@ -111,20 +134,28 @@ public void pickVideos(PluginCall call) { } } - @Nullable - private String[] parseTypesOption(@Nullable JSArray types) { - if (types == null) { - return null; - } + @ActivityCallback + private void pickDirectoryResult(PluginCall call, ActivityResult result) { try { - List typesList = types.toList(); - if (typesList.contains("text/csv")) { - typesList.add("text/comma-separated-values"); + if (call == null) { + return; } - return typesList.toArray(new String[0]); - } catch (JSONException exception) { - Logger.error("parseTypesOption failed.", exception); - return null; + int resultCode = result.getResultCode(); + switch (resultCode) { + case Activity.RESULT_OK: + JSObject callResult = createPickDirectoryResult(result.getData()); + call.resolve(callResult); + break; + case Activity.RESULT_CANCELED: + call.reject(ERROR_PICK_DIRECTORY_CANCELED); + break; + default: + call.reject(ERROR_PICK_DIRECTORY_FAILED); + } + } catch (Exception ex) { + String message = ex.getMessage(); + Log.e(TAG, message); + call.reject(message); } } @@ -154,6 +185,14 @@ private void pickFilesResult(PluginCall call, ActivityResult result) { } } + private JSObject createPickDirectoryResult(Intent data) { + Uri uri = data.getData(); + JSObject result = new JSObject(); + FilePickerHelper.traverseDirectoryEntries(getActivity().getContentResolver(), uri); + result.put("path", implementation.getPathFromUri(uri)); + return result; + } + private JSObject createPickFilesResult(@Nullable Intent data, boolean readData) { JSObject callResult = new JSObject(); List filesResultList = new ArrayList<>(); diff --git a/src/definitions.ts b/src/definitions.ts index 1d743c5..ca36f6f 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -1,4 +1,14 @@ export interface FilePickerPlugin { + /** + * Pick a directory. + * + * Returns a security-scoped URL for the directory that permits your app to access content outside its container. + * + * Only available on Android and iOS. + * + * @since 0.5.7 + */ + pickDirectory(): Promise; /** * Open the file picker that allows the user to select one or more files. */ @@ -35,6 +45,18 @@ export interface FilePickerPlugin { pickVideos(options?: PickVideosOptions): Promise; } +/** + * @since 0.5.7 + */ +export interface PickDirectoryResult { + /** + * The path of the directory. + * + * @since 0.5.7 + */ + path: string; +} + export interface PickFilesOptions { /** * List of accepted file types. diff --git a/src/web.ts b/src/web.ts index 00a5f76..23114a8 100644 --- a/src/web.ts +++ b/src/web.ts @@ -3,6 +3,7 @@ import { WebPlugin } from '@capacitor/core'; import type { File as FileModel, FilePickerPlugin, + PickDirectoryResult, PickFilesOptions, PickFilesResult, PickImagesOptions, @@ -16,6 +17,10 @@ import type { export class FilePickerWeb extends WebPlugin implements FilePickerPlugin { public readonly ERROR_PICK_FILE_CANCELED = 'pickFiles canceled.'; + public async pickDirectory(): Promise { + throw this.unimplemented('Not implemented on web.'); + } + public async pickFiles(options?: PickFilesOptions): Promise { const pickedFiles = await this.openFilePicker(options); if (!pickedFiles) {