|
| 1 | +/**************************************************************************/ |
| 2 | +/* FilePicker.kt */ |
| 3 | +/**************************************************************************/ |
| 4 | +/* This file is part of: */ |
| 5 | +/* GODOT ENGINE */ |
| 6 | +/* https://godotengine.org */ |
| 7 | +/**************************************************************************/ |
| 8 | +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ |
| 9 | +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ |
| 10 | +/* */ |
| 11 | +/* Permission is hereby granted, free of charge, to any person obtaining */ |
| 12 | +/* a copy of this software and associated documentation files (the */ |
| 13 | +/* "Software"), to deal in the Software without restriction, including */ |
| 14 | +/* without limitation the rights to use, copy, modify, merge, publish, */ |
| 15 | +/* distribute, sublicense, and/or sell copies of the Software, and to */ |
| 16 | +/* permit persons to whom the Software is furnished to do so, subject to */ |
| 17 | +/* the following conditions: */ |
| 18 | +/* */ |
| 19 | +/* The above copyright notice and this permission notice shall be */ |
| 20 | +/* included in all copies or substantial portions of the Software. */ |
| 21 | +/* */ |
| 22 | +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ |
| 23 | +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ |
| 24 | +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ |
| 25 | +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ |
| 26 | +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ |
| 27 | +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ |
| 28 | +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ |
| 29 | +/**************************************************************************/ |
| 30 | + |
| 31 | +package org.godotengine.godot.io |
| 32 | + |
| 33 | +import android.app.Activity |
| 34 | +import android.content.Context |
| 35 | +import android.content.Intent |
| 36 | +import android.net.Uri |
| 37 | +import android.os.Build |
| 38 | +import android.provider.DocumentsContract |
| 39 | +import android.util.Log |
| 40 | +import androidx.annotation.RequiresApi |
| 41 | +import org.godotengine.godot.GodotLib |
| 42 | +import org.godotengine.godot.io.file.MediaStoreData |
| 43 | + |
| 44 | +/** |
| 45 | + * Utility class for managing file selection and file picker activities. |
| 46 | + * |
| 47 | + * It provides methods to launch a file picker and handle the result, supporting various file modes, |
| 48 | + * including opening files, directories, and saving files. |
| 49 | + */ |
| 50 | +internal class FilePicker { |
| 51 | + companion object { |
| 52 | + private const val FILE_PICKER_REQUEST = 1000 |
| 53 | + private val TAG = FilePicker::class.java.simpleName |
| 54 | + |
| 55 | + // Constants for fileMode values |
| 56 | + private const val FILE_MODE_OPEN_FILE = 0 |
| 57 | + private const val FILE_MODE_OPEN_FILES = 1 |
| 58 | + private const val FILE_MODE_OPEN_DIR = 2 |
| 59 | + private const val FILE_MODE_OPEN_ANY = 3 |
| 60 | + private const val FILE_MODE_SAVE_FILE = 4 |
| 61 | + |
| 62 | + /** |
| 63 | + * Handles the result from a file picker activity and processes the selected file(s) or directory. |
| 64 | + * |
| 65 | + * @param context The context from which the file picker was launched. |
| 66 | + * @param requestCode The request code used when starting the file picker activity. |
| 67 | + * @param resultCode The result code returned by the activity. |
| 68 | + * @param data The intent data containing the selected file(s) or directory. |
| 69 | + */ |
| 70 | + @RequiresApi(Build.VERSION_CODES.Q) |
| 71 | + fun handleActivityResult(context: Context, requestCode: Int, resultCode: Int, data: Intent?) { |
| 72 | + if (requestCode == FILE_PICKER_REQUEST) { |
| 73 | + if (resultCode == Activity.RESULT_CANCELED) { |
| 74 | + Log.d(TAG, "File picker canceled") |
| 75 | + GodotLib.filePickerCallback(false, emptyArray()) |
| 76 | + return |
| 77 | + } |
| 78 | + if (resultCode == Activity.RESULT_OK) { |
| 79 | + val selectedPaths: MutableList<String> = mutableListOf() |
| 80 | + // Handle multiple file selection. |
| 81 | + val clipData = data?.clipData |
| 82 | + if (clipData != null) { |
| 83 | + for (i in 0 until clipData.itemCount) { |
| 84 | + val uri = clipData.getItemAt(i).uri |
| 85 | + uri?.let { |
| 86 | + val filepath = MediaStoreData.getFilePathFromUri(context, uri) |
| 87 | + if (filepath != null) { |
| 88 | + selectedPaths.add(filepath) |
| 89 | + } else { |
| 90 | + Log.d(TAG, "null filepath URI: $it") |
| 91 | + } |
| 92 | + } |
| 93 | + } |
| 94 | + } else { |
| 95 | + val uri: Uri? = data?.data |
| 96 | + uri?.let { |
| 97 | + val filepath = MediaStoreData.getFilePathFromUri(context, uri) |
| 98 | + if (filepath != null) { |
| 99 | + selectedPaths.add(filepath) |
| 100 | + } else { |
| 101 | + Log.d(TAG, "null filepath URI: $it") |
| 102 | + } |
| 103 | + } |
| 104 | + } |
| 105 | + |
| 106 | + if (selectedPaths.isNotEmpty()) { |
| 107 | + GodotLib.filePickerCallback(true, selectedPaths.toTypedArray()) |
| 108 | + } else { |
| 109 | + GodotLib.filePickerCallback(false, emptyArray()) |
| 110 | + } |
| 111 | + } |
| 112 | + } |
| 113 | + } |
| 114 | + |
| 115 | + /** |
| 116 | + * Launches a file picker activity with specified settings based on the mode, initial directory, |
| 117 | + * file type filters, and other parameters. |
| 118 | + * |
| 119 | + * @param context The context from which to start the file picker. |
| 120 | + * @param activity The activity instance used to initiate the picker. Required for activity results. |
| 121 | + * @param currentDirectory The directory path to start the file picker in. |
| 122 | + * @param filename The name of the file when using save mode. |
| 123 | + * @param fileMode The mode to operate in, specifying open, save, or directory select. |
| 124 | + * @param filters Array of MIME types to filter file selection. |
| 125 | + */ |
| 126 | + @RequiresApi(Build.VERSION_CODES.Q) |
| 127 | + fun showFilePicker(context: Context, activity: Activity?, currentDirectory: String, filename: String, fileMode: Int, filters: Array<String>) { |
| 128 | + val intent = when (fileMode) { |
| 129 | + FILE_MODE_OPEN_DIR -> Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) |
| 130 | + FILE_MODE_SAVE_FILE -> Intent(Intent.ACTION_CREATE_DOCUMENT) |
| 131 | + else -> Intent(Intent.ACTION_OPEN_DOCUMENT) |
| 132 | + } |
| 133 | + val initialDirectory = MediaStoreData.getUriFromDirectoryPath(context, currentDirectory) |
| 134 | + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && initialDirectory != null) { |
| 135 | + intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialDirectory) |
| 136 | + } else { |
| 137 | + Log.d(TAG, "Error cannot set initial directory") |
| 138 | + } |
| 139 | + if (fileMode == FILE_MODE_OPEN_FILES) { |
| 140 | + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) // Set multi select for FILE_MODE_OPEN_FILES |
| 141 | + } else if (fileMode == FILE_MODE_SAVE_FILE) { |
| 142 | + intent.putExtra(Intent.EXTRA_TITLE, filename) // Set filename for FILE_MODE_SAVE_FILE |
| 143 | + } |
| 144 | + // ACTION_OPEN_DOCUMENT_TREE does not support intent type |
| 145 | + if (fileMode != FILE_MODE_OPEN_DIR) { |
| 146 | + intent.type = "*/*" |
| 147 | + if (filters.isNotEmpty()) { |
| 148 | + if (filters.size == 1) { |
| 149 | + intent.type = filters[0] |
| 150 | + } else { |
| 151 | + intent.putExtra(Intent.EXTRA_MIME_TYPES, filters) |
| 152 | + } |
| 153 | + } |
| 154 | + intent.addCategory(Intent.CATEGORY_OPENABLE) |
| 155 | + } |
| 156 | + intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true) |
| 157 | + activity?.startActivityForResult(intent, FILE_PICKER_REQUEST) |
| 158 | + } |
| 159 | + } |
| 160 | +} |
0 commit comments