diff --git a/bindings/dart/android/src/main/kotlin/ie/equalit/ouisync_plugin/OuisyncPlugin.kt b/bindings/dart/android/src/main/kotlin/ie/equalit/ouisync_plugin/OuisyncPlugin.kt index c6564a30e..3c98e9a3c 100644 --- a/bindings/dart/android/src/main/kotlin/ie/equalit/ouisync_plugin/OuisyncPlugin.kt +++ b/bindings/dart/android/src/main/kotlin/ie/equalit/ouisync_plugin/OuisyncPlugin.kt @@ -8,7 +8,6 @@ import android.net.Uri import android.util.Log import android.os.Environment import android.webkit.MimeTypeMap -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 @@ -24,38 +23,87 @@ class OuisyncPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { /// This local reference serves to register the plugin with the Flutter Engine and unregister it /// when the Flutter Engine is detached from the Activity var activity : Activity? = null - private lateinit var context : Context companion object { - lateinit var channel : MethodChannel + private val TAG = OuisyncPlugin::class.java.simpleName + + private const val CHANNEL_NAME = "ouisync_plugin" + + /// The channel needs to be static so we can access it from `PipeProvider` but we need to make + /// sure we only create/destroy it once even when there are multiple `OuisyncPlugin` instances. + /// That's why we use the explicit ref count. + private var channel : MethodChannel? = null + private val channelLock = Any() + private var channelRefCount = 0 + + fun invokeMethod(method: String, arguments: Any?, callback: MethodChannel.Result? = null) { + channel.let { + if (it != null) { + it.invokeMethod(method, arguments, callback) + } else { + callback?.error("not attached to engine", null, null) + } + } + } } - override fun onAttachedToActivity(@NonNull activityPluginBinding: ActivityPluginBinding) { - print("onAttachedToActivity") + override fun onAttachedToActivity(activityPluginBinding: ActivityPluginBinding) { + Log.d(TAG, "onAttachedToActivity"); activity = activityPluginBinding.activity } override fun onDetachedFromActivityForConfigChanges() { + Log.d(TAG, "onDetachedFromActivityForConfigChanges"); activity = null } - override fun onReattachedToActivityForConfigChanges(@NonNull activityPluginBinding: ActivityPluginBinding) { + override fun onReattachedToActivityForConfigChanges(activityPluginBinding: ActivityPluginBinding) { + Log.d(TAG, "onReattachedToActivityForConfigChanges"); activity = activityPluginBinding.activity } override fun onDetachedFromActivity() { + Log.d(TAG, "onDetachedFromActivity"); activity = null } - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - channel = MethodChannel(flutterPluginBinding.binaryMessenger, "ouisync_plugin") - channel.setMethodCallHandler(this) + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + Log.d(TAG, "onAttachedToEngine"); + + synchronized(channelLock) { + channelRefCount++ - context = flutterPluginBinding.getApplicationContext() + if (channelRefCount == 1) { + Log.d(TAG, "create method channel") + + channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL_NAME) + channel?.setMethodCallHandler(this) + } + } } - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) { + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + Log.d(TAG, "onDetachedFromEngine"); + // When the user requests for the app to manage it's own battery + // optimization permissions (e.g. when using the `flutter_background` + // plugin https://pub.dev/packages/flutter_background), then killing the + // app will not stop the native code execution and we have to do it + // manually. + invokeMethod("stopSession", null) + + synchronized(channelLock) { + channelRefCount-- + + if (channelRefCount == 0) { + Log.d(TAG, "destroy method channel") + + channel?.setMethodCallHandler(null) + } + } + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { "shareFile" -> { val arguments = call.arguments as HashMap @@ -131,6 +179,7 @@ class OuisyncPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { return MimeTypeMap.getFileExtensionFromUrl(path) } + private fun startFileShareAction(arguments: HashMap) { val authority = arguments["authority"] val path = arguments["path"] @@ -157,14 +206,4 @@ class OuisyncPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { putExtra(Intent.EXTRA_STREAM, intentData) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } - - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { - // When the user requests for the app to manage it's own battery - // optimization permissions (e.g. when using the `flutter_background` - // plugin https://pub.dev/packages/flutter_background), then killing the - // app will not stop the native code execution and we have to do it - // manually. - channel.invokeMethod("stopSession", null) - channel.setMethodCallHandler(null) - } } diff --git a/bindings/dart/android/src/main/kotlin/ie/equalit/ouisync_plugin/PipeProvider.kt b/bindings/dart/android/src/main/kotlin/ie/equalit/ouisync_plugin/PipeProvider.kt index 80631d7e3..5d2217980 100644 --- a/bindings/dart/android/src/main/kotlin/ie/equalit/ouisync_plugin/PipeProvider.kt +++ b/bindings/dart/android/src/main/kotlin/ie/equalit/ouisync_plugin/PipeProvider.kt @@ -1,244 +1,244 @@ -package ie.equalit.ouisync_plugin - -import android.content.Context -import android.content.res.AssetFileDescriptor -import android.net.Uri -import android.os.Handler -import android.os.HandlerThread -import android.os.Looper -import android.os.ParcelFileDescriptor -import android.os.storage.StorageManager; -import android.util.Log -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.Result -import java.io.FileNotFoundException -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream -import java.util.concurrent.Callable -import java.util.concurrent.FutureTask -import java.util.concurrent.Semaphore - -class PipeProvider: AbstractFileProvider() { - companion object { - private const val CHUNK_SIZE = 64000 - private val TAG = PipeProvider::class.java.simpleName - - private val supportsProxyFileDescriptor: Boolean - get() = android.os.Build.VERSION.SDK_INT >= 26 - } - - private var _handler: Handler? = null - - private val handler: Handler - @Synchronized get() { - if (_handler == null) { - Log.d(TAG, "Creating worker thread") - - val thread = HandlerThread("${javaClass.simpleName} worker thread") - thread.start(); - _handler = Handler(thread.getLooper()) - } - - return _handler!! - } - - override fun onCreate(): Boolean { - return true - } - - // TODO: Handle `mode` - @Throws(FileNotFoundException::class) - override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? { - Log.d(TAG, "Opening file '$uri' in mode '$mode'") - - val path = getPathFromUri(uri); - Log.d(TAG, "Pipe path=" + path); - if (supportsProxyFileDescriptor) { - var size = super.getDataLength(uri); - - if (size == AssetFileDescriptor.UNKNOWN_LENGTH) { - Log.d(TAG, "Using pipe because size is unknown"); - return openPipe(path); - } - - Log.d(TAG, "Using proxy file"); - return openProxyFile(path, size); - } else { - Log.d(TAG, "Using pipe because proxy file is not supported"); - return openPipe(path); - } - } - - @Throws(FileNotFoundException::class) - private fun openProxyFile(path: String, size: Long): ParcelFileDescriptor? { - var storage = context!!.getSystemService(Context.STORAGE_SERVICE) as StorageManager; - - // https://developer.android.google.cn/reference/android/os/storage/StorageManager - return storage.openProxyFileDescriptor( - ParcelFileDescriptor.MODE_READ_ONLY, - ProxyCallbacks(path, size), - handler - ) - } - - @Throws(FileNotFoundException::class) - private fun openPipe(path: String): ParcelFileDescriptor? { - var pipe: Array? - - try { - pipe = ParcelFileDescriptor.createPipe() - } catch (e: IOException) { - Log.e(TAG, "Exception opening pipe", e) - throw FileNotFoundException("Could not open pipe for: $path") - } - - var reader = pipe[0] - var writer = pipe[1] - var dstFd = writer!!.detachFd(); - - runInUiThread { - copyFileToRawFd(path, dstFd, object: MethodChannel.Result { - override fun success(a: Any?) { - writer.close() - } - - override fun error(code: String, message: String?, details: Any?) { - Log.e(TAG, channelMethodErrorMessage(code, message, details)) - writer.close() - } - - override fun notImplemented() {} - }) - } - - return reader - } - - private fun getPathFromUri(uri: Uri): String { - val segments = uri.pathSegments - var index = 0 - var path = "" - - for (segment in segments) { - if (index > 0) { - path += "/$segment" - } - index++ - } - - return path - } - - internal class ProxyCallbacks( - private val path: String, - private val size: Long - ) : android.os.ProxyFileDescriptorCallback() { - private var id: Int? = null - - override fun onGetSize() = size - - override fun onRead(offset: Long, chunkSize: Int, outData: ByteArray): Int { - var id = this.id - - if (id == null) { - id = invokeBlocking { result -> openFile(path, result) } - ?: throw FileNotFoundException("file not found: $path") - this.id = id - } - - val chunk = invokeBlocking { result -> - readFile(id, chunkSize, offset, result) - } - - if (chunk != null) { - chunk.copyInto(outData) - return chunk.size - } else { - return 0 - } - } - - override fun onRelease() { - val id = this.id - - if (id != null) { - invokeBlocking { result -> closeFile(id, result) } - } - } - } -} - -private fun openFile(path: String, result: MethodChannel.Result) { - val arguments = hashMapOf("path" to path) - OuisyncPlugin.channel.invokeMethod("openFile", arguments, result) -} - -private fun closeFile(id: Int, result: MethodChannel.Result) { - val arguments = hashMapOf("id" to id) - OuisyncPlugin.channel.invokeMethod("closeFile", arguments, result) -} - -private fun readFile(id: Int, chunkSize: Int, offset: Long, result: MethodChannel.Result) { - val arguments = hashMapOf("id" to id, "chunkSize" to chunkSize, "offset" to offset) - OuisyncPlugin.channel.invokeMethod("readFile", arguments, result) -} - -private fun copyFileToRawFd(srcPath: String, dstFd: Int, result: MethodChannel.Result) { - val arguments = hashMapOf("srcPath" to srcPath, "dstFd" to dstFd) - OuisyncPlugin.channel.invokeMethod("copyFileToRawFd", arguments, result) -} - -// Implementation of MethodChannel.Result which blocks until the result is available. -class BlockingResult: MethodChannel.Result { - private val semaphore = Semaphore(1) - private var result: Any? = null - - init { - semaphore.acquire(1) - } - - // Wait until the result is available and returns it. If the invoked method failed, throws an - // exception. - fun wait(): T? { - semaphore.acquire(1) - - try { - val result = this.result - - if (result is Throwable) { - throw result - } else { - return result as T? - } - } finally { - semaphore.release(1) - } - } - - override fun success(a: Any?) { - result = a - semaphore.release(1) - } - - override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { - result = Exception(channelMethodErrorMessage(errorCode, errorMessage, errorDetails)) - semaphore.release(1) - } - - override fun notImplemented() {} -} - -private fun invokeBlocking(f: (MethodChannel.Result) -> Unit): T? { - val result = BlockingResult() - runInUiThread { f(result) } - return result.wait() -} - -private fun runInUiThread(f: () -> Unit) { - Handler(Looper.getMainLooper()).post { f() } -} - -private fun channelMethodErrorMessage(code: String?, message: String?, details: Any?): String = - "error invoking channel method (code: $code, message: $message, details: $details)" - +package ie.equalit.ouisync_plugin + +import android.content.Context +import android.content.res.AssetFileDescriptor +import android.net.Uri +import android.os.Handler +import android.os.HandlerThread +import android.os.Looper +import android.os.ParcelFileDescriptor +import android.os.storage.StorageManager; +import android.util.Log +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.Result +import java.io.FileNotFoundException +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.util.concurrent.Callable +import java.util.concurrent.FutureTask +import java.util.concurrent.Semaphore + +class PipeProvider: AbstractFileProvider() { + companion object { + private const val CHUNK_SIZE = 64000 + private val TAG = PipeProvider::class.java.simpleName + + private val supportsProxyFileDescriptor: Boolean + get() = android.os.Build.VERSION.SDK_INT >= 26 + } + + private var _handler: Handler? = null + + private val handler: Handler + @Synchronized get() { + if (_handler == null) { + Log.d(TAG, "Creating worker thread") + + val thread = HandlerThread("${javaClass.simpleName} worker thread") + thread.start(); + _handler = Handler(thread.getLooper()) + } + + return _handler!! + } + + override fun onCreate(): Boolean { + return true + } + + // TODO: Handle `mode` + @Throws(FileNotFoundException::class) + override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? { + Log.d(TAG, "Opening file '$uri' in mode '$mode'") + + val path = getPathFromUri(uri); + Log.d(TAG, "Pipe path=" + path); + if (supportsProxyFileDescriptor) { + var size = super.getDataLength(uri); + + if (size == AssetFileDescriptor.UNKNOWN_LENGTH) { + Log.d(TAG, "Using pipe because size is unknown"); + return openPipe(path); + } + + Log.d(TAG, "Using proxy file"); + return openProxyFile(path, size); + } else { + Log.d(TAG, "Using pipe because proxy file is not supported"); + return openPipe(path); + } + } + + @Throws(FileNotFoundException::class) + private fun openProxyFile(path: String, size: Long): ParcelFileDescriptor? { + var storage = context!!.getSystemService(Context.STORAGE_SERVICE) as StorageManager; + + // https://developer.android.google.cn/reference/android/os/storage/StorageManager + return storage.openProxyFileDescriptor( + ParcelFileDescriptor.MODE_READ_ONLY, + ProxyCallbacks(path, size), + handler + ) + } + + @Throws(FileNotFoundException::class) + private fun openPipe(path: String): ParcelFileDescriptor? { + var pipe: Array? + + try { + pipe = ParcelFileDescriptor.createPipe() + } catch (e: IOException) { + Log.e(TAG, "Exception opening pipe", e) + throw FileNotFoundException("Could not open pipe for: $path") + } + + var reader = pipe[0] + var writer = pipe[1] + var dstFd = writer!!.detachFd(); + + runInUiThread { + copyFileToRawFd(path, dstFd, object: MethodChannel.Result { + override fun success(a: Any?) { + writer.close() + } + + override fun error(code: String, message: String?, details: Any?) { + Log.e(TAG, channelMethodErrorMessage(code, message, details)) + writer.close() + } + + override fun notImplemented() {} + }) + } + + return reader + } + + private fun getPathFromUri(uri: Uri): String { + val segments = uri.pathSegments + var index = 0 + var path = "" + + for (segment in segments) { + if (index > 0) { + path += "/$segment" + } + index++ + } + + return path + } + + internal class ProxyCallbacks( + private val path: String, + private val size: Long + ) : android.os.ProxyFileDescriptorCallback() { + private var id: Int? = null + + override fun onGetSize() = size + + override fun onRead(offset: Long, chunkSize: Int, outData: ByteArray): Int { + var id = this.id + + if (id == null) { + id = invokeBlocking { result -> openFile(path, result) } + ?: throw FileNotFoundException("file not found: $path") + this.id = id + } + + val chunk = invokeBlocking { result -> + readFile(id, chunkSize, offset, result) + } + + if (chunk != null) { + chunk.copyInto(outData) + return chunk.size + } else { + return 0 + } + } + + override fun onRelease() { + val id = this.id + + if (id != null) { + invokeBlocking { result -> closeFile(id, result) } + } + } + } +} + +private fun openFile(path: String, result: MethodChannel.Result) { + val arguments = hashMapOf("path" to path) + OuisyncPlugin.invokeMethod("openFile", arguments, result) +} + +private fun closeFile(id: Int, result: MethodChannel.Result) { + val arguments = hashMapOf("id" to id) + OuisyncPlugin.invokeMethod("closeFile", arguments, result) +} + +private fun readFile(id: Int, chunkSize: Int, offset: Long, result: MethodChannel.Result) { + val arguments = hashMapOf("id" to id, "chunkSize" to chunkSize, "offset" to offset) + OuisyncPlugin.invokeMethod("readFile", arguments, result) +} + +private fun copyFileToRawFd(srcPath: String, dstFd: Int, result: MethodChannel.Result) { + val arguments = hashMapOf("srcPath" to srcPath, "dstFd" to dstFd) + OuisyncPlugin.invokeMethod("copyFileToRawFd", arguments, result) +} + +// Implementation of MethodChannel.Result which blocks until the result is available. +class BlockingResult: MethodChannel.Result { + private val semaphore = Semaphore(1) + private var result: Any? = null + + init { + semaphore.acquire(1) + } + + // Wait until the result is available and returns it. If the invoked method failed, throws an + // exception. + fun wait(): T? { + semaphore.acquire(1) + + try { + val result = this.result + + if (result is Throwable) { + throw result + } else { + return result as T? + } + } finally { + semaphore.release(1) + } + } + + override fun success(a: Any?) { + result = a + semaphore.release(1) + } + + override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + result = Exception(channelMethodErrorMessage(errorCode, errorMessage, errorDetails)) + semaphore.release(1) + } + + override fun notImplemented() {} +} + +private fun invokeBlocking(f: (MethodChannel.Result) -> Unit): T? { + val result = BlockingResult() + runInUiThread { f(result) } + return result.wait() +} + +private fun runInUiThread(f: () -> Unit) { + Handler(Looper.getMainLooper()).post { f() } +} + +private fun channelMethodErrorMessage(code: String?, message: String?, details: Any?): String = + "error invoking channel method (code: $code, message: $message, details: $details)" +