Initial commit

This commit is contained in:
2023-09-07 18:51:22 +02:00
commit 2eed6345fd
51 changed files with 2009 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.moxxy.moxxy_native">
</manifest>

View File

@@ -0,0 +1,5 @@
package org.moxxy.moxxy_native
object AsyncRequestTracker {
val requestTracker: MutableMap<Int, (Result<Any>) -> Unit> = mutableMapOf()
}

View File

@@ -0,0 +1,8 @@
package org.moxxy.moxxy_native
const val TAG = "moxxy_native"
// Request codes
const val PICK_FILE_REQUEST = 42
const val PICK_FILES_REQUEST = 43
const val PICK_FILE_WITH_DATA_REQUEST = 44

View File

@@ -0,0 +1,110 @@
package org.moxxy.moxxy_native
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import org.moxxy.moxxy_native.generated.FilePickerType
import org.moxxy.moxxy_native.generated.MoxxyPickerApi
import org.moxxy.moxxy_native.picker.PickerResultListener
class MoxxyNativePlugin: FlutterPlugin, ActivityAware, MoxxyPickerApi {
private var context: Context? = null
private var activity: Activity? = null
private lateinit var pickerListener: PickerResultListener
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
MoxxyPickerApi.setUp(flutterPluginBinding.binaryMessenger, this)
pickerListener = PickerResultListener(context!!)
Log.d(TAG, "Attached to engine")
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
Log.d(TAG, "Detached from engine")
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
binding.addActivityResultListener(pickerListener)
Log.d(TAG, "Attached to activity")
}
override fun onDetachedFromActivityForConfigChanges() {
activity = null
Log.d(TAG, "Detached from activity")
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
}
override fun onDetachedFromActivity() {
activity = null
Log.d(TAG, "Detached from activity")
}
override fun pickFiles(
type: FilePickerType,
multiple: Boolean,
callback: (Result<List<String>>) -> Unit
) {
val requestCode = if (multiple) PICK_FILES_REQUEST else PICK_FILE_REQUEST
AsyncRequestTracker.requestTracker[requestCode] = callback as (Result<Any>) -> Unit
if (type == FilePickerType.GENERIC) {
val pickIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
this.type = "*/*"
// Allow/disallow picking multiple files
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple)
}
activity?.startActivityForResult(pickIntent, requestCode)
return
}
val contract = when (multiple) {
false -> ActivityResultContracts.PickVisualMedia()
true -> ActivityResultContracts.PickMultipleVisualMedia()
}
val pickType = when (type) {
// We keep FilePickerType.GENERIC here, even though we know that @type will never be
// GENERIC to make Kotlin happy.
FilePickerType.GENERIC, FilePickerType.IMAGE -> ActivityResultContracts.PickVisualMedia.ImageOnly
FilePickerType.VIDEO -> ActivityResultContracts.PickVisualMedia.VideoOnly
FilePickerType.IMAGEANDVIDEO -> ActivityResultContracts.PickVisualMedia.ImageAndVideo
}
val pickIntent = contract.createIntent(context!!, PickVisualMediaRequest(pickType))
activity?.startActivityForResult(pickIntent, requestCode)
}
override fun pickFileWithData(type: FilePickerType, callback: (Result<ByteArray?>) -> Unit) {
AsyncRequestTracker.requestTracker[PICK_FILE_WITH_DATA_REQUEST] = callback as (Result<Any>) -> Unit
if (type == FilePickerType.GENERIC) {
val pickIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
this.type = "*/*"
}
activity?.startActivityForResult(pickIntent, PICK_FILE_WITH_DATA_REQUEST)
return
}
val pickType = when (type) {
// We keep FilePickerType.GENERIC here, even though we know that @type will never be
// GENERIC to make Kotlin happy.
FilePickerType.GENERIC, FilePickerType.IMAGE -> ActivityResultContracts.PickVisualMedia.ImageOnly
FilePickerType.VIDEO -> ActivityResultContracts.PickVisualMedia.VideoOnly
FilePickerType.IMAGEANDVIDEO -> ActivityResultContracts.PickVisualMedia.ImageAndVideo
}
val contract = ActivityResultContracts.PickVisualMedia()
val pickIntent = contract.createIntent(context!!, PickVisualMediaRequest(pickType))
activity?.startActivityForResult(pickIntent, PICK_FILE_WITH_DATA_REQUEST)
}
}

View File

@@ -0,0 +1,130 @@
// Autogenerated from Pigeon (v11.0.1), do not edit directly.
// See also: https://pub.dev/packages/pigeon
package org.moxxy.moxxy_native.generated
import android.util.Log
import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MessageCodec
import io.flutter.plugin.common.StandardMessageCodec
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
private fun wrapResult(result: Any?): List<Any?> {
return listOf(result)
}
private fun wrapError(exception: Throwable): List<Any?> {
if (exception is FlutterError) {
return listOf(
exception.code,
exception.message,
exception.details
)
} else {
return listOf(
exception.javaClass.simpleName,
exception.toString(),
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
)
}
}
/**
* Error class for passing custom error details to Flutter via a thrown PlatformException.
* @property code The error code.
* @property message The error message.
* @property details The error details. Must be a datatype supported by the api codec.
*/
class FlutterError (
val code: String,
override val message: String? = null,
val details: Any? = null
) : Throwable()
enum class FilePickerType(val raw: Int) {
/** Pick only image(s) */
IMAGE(0),
/** Pick only video(s) */
VIDEO(1),
/** Pick image(s) and video(s) */
IMAGEANDVIDEO(2),
/** Pick any kind of file(s) */
GENERIC(3);
companion object {
fun ofRaw(raw: Int): FilePickerType? {
return values().firstOrNull { it.raw == raw }
}
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface MoxxyPickerApi {
/**
* Open either the photo picker or the generic file picker to get a list of paths that were
* selected and are accessable. If the list is empty, then the user dismissed the picker without
* selecting anything.
*
* [type] specifies what kind of file(s) should be picked.
*
* [multiple] controls whether multiple files can be picked (true) or just a single file
* is enough (false).
*/
fun pickFiles(type: FilePickerType, multiple: Boolean, callback: (Result<List<String>>) -> Unit)
/** Like [pickFiles] but sets multiple to false and returns the raw binary data from the file. */
fun pickFileWithData(type: FilePickerType, callback: (Result<ByteArray?>) -> Unit)
companion object {
/** The codec used by MoxxyPickerApi. */
val codec: MessageCodec<Any?> by lazy {
StandardMessageCodec()
}
/** Sets up an instance of `MoxxyPickerApi` to handle messages through the `binaryMessenger`. */
@Suppress("UNCHECKED_CAST")
fun setUp(binaryMessenger: BinaryMessenger, api: MoxxyPickerApi?) {
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyPickerApi.pickFiles", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val typeArg = FilePickerType.ofRaw(args[0] as Int)!!
val multipleArg = args[1] as Boolean
api.pickFiles(typeArg, multipleArg) { result: Result<List<String>> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyPickerApi.pickFileWithData", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val typeArg = FilePickerType.ofRaw(args[0] as Int)!!
api.pickFileWithData(typeArg) { result: Result<ByteArray?> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}

View File

@@ -0,0 +1,177 @@
package org.moxxy.moxxy_native.picker
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.OpenableColumns
import android.util.Log
import io.flutter.plugin.common.PluginRegistry.ActivityResultListener
import org.moxxy.moxxy_native.AsyncRequestTracker
import org.moxxy.moxxy_native.PICK_FILES_REQUEST
import org.moxxy.moxxy_native.PICK_FILE_REQUEST
import org.moxxy.moxxy_native.PICK_FILE_WITH_DATA_REQUEST
import org.moxxy.moxxy_native.TAG
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
class PickerResultListener(private val context: Context) : ActivityResultListener {
/*
* Attempt to deduce the filename for the URI @uri.
* Based on https://stackoverflow.com/a/25005243
*/
@SuppressLint("Range")
private fun queryFileName(context: Context, uri: Uri): String {
var result: String? = null
if (uri.scheme == "content") {
val cursor = context.contentResolver.query(uri, null, null, null, null)
cursor.use { cursor ->
if (cursor != null && cursor.moveToFirst()) {
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
}
}
}
return result ?: uri.path!!.split("/").last()
}
/*
* Copy from the input stream @input to the output stream @output.
* On Android >= 13 uses Android's own copy method. Below, reads the stream in 4096 byte
* segments and write them back.
*
* Based on https://github.com/flutter/packages/blob/b8b84b2304f00a3f93ce585cc7a30e1235bde7a0/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java#L130
*/
private fun copy(input: InputStream, output: OutputStream) {
if (Build.VERSION.SDK_INT >= 33) {
android.os.FileUtils.copy(input, output)
} else {
val buffer = ByteArray(4096)
while (input.read(buffer).also {} != -1) {
output.write(buffer)
}
output.flush()
}
}
/*
* Copy the content of the file @uri is pointing to into the cache directory for access from
* within Flutter.
*
* Based on https://github.com/flutter/packages/blob/b8b84b2304f00a3f93ce585cc7a30e1235bde7a0/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java#L54C64-L54C64
*/
private fun resolveContentUri(context: Context, uri: Uri): String? {
try {
val inputStream = context.contentResolver.openInputStream(uri)
val cacheDir = File(context.cacheDir, "cache").apply {
mkdir()
deleteOnExit()
}
val cacheFile = File(cacheDir, queryFileName(context, uri))
val outputStream = FileOutputStream(cacheFile)
copy(inputStream!!, outputStream)
return cacheFile.path
} catch (ex: IOException) {
Log.d(TAG, "IO exception while resolving URI $uri: ${ex.message}")
return null
} catch (ex: SecurityException) {
Log.d(TAG, "Security exception while resolving URI $uri: ${ex.message}")
return null
}
}
private fun handlePickWithData(context: Context, resultCode: Int, data: Intent?, result: (Result<ByteArray?>) -> Unit) {
// Handle not picking anything
if (resultCode != Activity.RESULT_OK) {
result(Result.success(null))
return
}
val returnBuffer = mutableListOf<Byte>()
val readBuffer = ByteArray(4096)
try {
val inputStream = context.contentResolver.openInputStream(data!!.data!!)!!
while (inputStream.read(readBuffer).also {} != -1) {
returnBuffer.addAll(readBuffer.asList())
}
inputStream.close()
result(
Result.success(returnBuffer.toByteArray()),
)
} catch (ex: IOException) {
Log.w(TAG, "IO exception while reading URI ${data!!.data}: ${ex.message}")
result(Result.success(null))
} catch (ex: SecurityException) {
Log.w(TAG, "Security exception while reading URI ${data!!.data}: ${ex.message}")
result(Result.success(null))
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
Log.d(TAG, "Got result for $requestCode")
if (requestCode != PICK_FILE_REQUEST && requestCode != PICK_FILES_REQUEST && requestCode != PICK_FILE_WITH_DATA_REQUEST) {
Log.d(TAG, "Unknown result code")
return false
}
// Check if we have a request pending
val result = AsyncRequestTracker.requestTracker.remove(requestCode)
if (result == null) {
Log.w(TAG, "Received result for $requestCode but we have no tracked request")
return true
}
if (requestCode == PICK_FILE_WITH_DATA_REQUEST) {
handlePickWithData(context, resultCode, data, result as (Result<ByteArray?>) -> Unit)
return true
}
// No file(s) picked
if (resultCode != Activity.RESULT_OK) {
Log.d(TAG, "resultCode $resultCode != ${Activity.RESULT_OK}")
result!!(Result.success(listOf<String>()))
return true
}
val pickedMultiple = requestCode == PICK_FILES_REQUEST
val pickedFiles = mutableListOf<String>()
if (pickedMultiple) {
if (data!!.clipData != null) {
Log.w(TAG, "Files shared: ${data!!.clipData!!.itemCount}")
for (i in 0 until data!!.clipData!!.itemCount) {
val path = resolveContentUri(context, data!!.clipData!!.getItemAt(i).uri)
if (path != null) {
pickedFiles.add(path)
}
}
} else if (data!!.data != null) {
// Handle the generic file picker with multiple=true returning only one file
val path = resolveContentUri(context, data!!.data!!)
if (path != null) {
pickedFiles.add(path)
}
} else {
Log.w(TAG, "Multi-file intent has no clipData and data")
}
} else {
if (data!!.data != null) {
val path = resolveContentUri(context, data!!.data!!)
if (path != null) {
pickedFiles.add(path)
}
} else {
Log.w(TAG, "Single-file intent has no data")
}
}
result!!(Result.success(pickedFiles))
return true
}
}