diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt index eab4536ff..3437e977d 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt @@ -12,8 +12,10 @@ import android.os.Bundle import android.util.Log import android.widget.RemoteViews import app.loup.streams_channel.StreamsChannel +import deckers.thibault.aves.channel.AvesByteSendingMethodCodec import deckers.thibault.aves.channel.calls.DeviceHandler -import deckers.thibault.aves.channel.calls.MediaFetchHandler +import deckers.thibault.aves.channel.calls.MediaFetchBytesHandler +import deckers.thibault.aves.channel.calls.MediaFetchObjectHandler import deckers.thibault.aves.channel.calls.MediaStoreHandler import deckers.thibault.aves.channel.streams.ImageByteStreamHandler import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler @@ -190,7 +192,8 @@ class HomeWidgetProvider : AppWidgetProvider() { // - need Context MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(context)) MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(context)) - MethodChannel(messenger, MediaFetchHandler.CHANNEL).setMethodCallHandler(MediaFetchHandler(context)) + MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(context)) + MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(context)) // result streaming: dart -> platform ->->-> dart // - need Context diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt index ad2196561..d2cade1c9 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt @@ -16,6 +16,7 @@ import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat import app.loup.streams_channel.StreamsChannel +import deckers.thibault.aves.channel.AvesByteSendingMethodCodec import deckers.thibault.aves.channel.calls.* import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler import deckers.thibault.aves.channel.calls.window.WindowHandler @@ -70,7 +71,8 @@ open class MainActivity : FlutterActivity() { MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this)) MethodChannel(messenger, GlobalSearchHandler.CHANNEL).setMethodCallHandler(GlobalSearchHandler(this)) MethodChannel(messenger, HomeWidgetHandler.CHANNEL).setMethodCallHandler(HomeWidgetHandler(this)) - MethodChannel(messenger, MediaFetchHandler.CHANNEL).setMethodCallHandler(MediaFetchHandler(this)) + MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(this)) + MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(this)) MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this)) MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this)) MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this)) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/ScreenSaverService.kt b/android/app/src/main/kotlin/deckers/thibault/aves/ScreenSaverService.kt index 18847b2ea..be332d36f 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/ScreenSaverService.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/ScreenSaverService.kt @@ -4,6 +4,7 @@ import android.service.dreams.DreamService import android.util.Log import android.view.View import app.loup.streams_channel.StreamsChannel +import deckers.thibault.aves.channel.AvesByteSendingMethodCodec import deckers.thibault.aves.channel.calls.* import deckers.thibault.aves.channel.calls.window.ServiceWindowHandler import deckers.thibault.aves.channel.calls.window.WindowHandler @@ -99,7 +100,8 @@ class ScreenSaverService : DreamService() { // - need Context MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this)) MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this)) - MethodChannel(messenger, MediaFetchHandler.CHANNEL).setMethodCallHandler(MediaFetchHandler(this)) + MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(this)) + MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(this)) MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this)) MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this)) MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this)) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/WallpaperActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/WallpaperActivity.kt index ccee5b904..60c37d9ec 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/WallpaperActivity.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/WallpaperActivity.kt @@ -8,6 +8,7 @@ import android.os.Handler import android.os.Looper import android.util.Log import app.loup.streams_channel.StreamsChannel +import deckers.thibault.aves.channel.AvesByteSendingMethodCodec import deckers.thibault.aves.channel.calls.* import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler import deckers.thibault.aves.channel.calls.window.WindowHandler @@ -34,7 +35,8 @@ class WallpaperActivity : FlutterActivity() { // - need Context MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this)) MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this)) - MethodChannel(messenger, MediaFetchHandler.CHANNEL).setMethodCallHandler(MediaFetchHandler(this)) + MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(context)) + MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(this)) MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this)) MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this)) // - need ContextWrapper diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/AvesByteSendingMethodCodec.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/AvesByteSendingMethodCodec.kt new file mode 100644 index 000000000..9ae286aff --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/AvesByteSendingMethodCodec.kt @@ -0,0 +1,52 @@ +package deckers.thibault.aves.channel + +import android.util.Log +import deckers.thibault.aves.utils.LogUtils +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodCodec +import io.flutter.plugin.common.StandardMessageCodec +import io.flutter.plugin.common.StandardMethodCodec +import java.nio.ByteBuffer + +class AvesByteSendingMethodCodec private constructor() : MethodCodec { + override fun decodeMethodCall(methodCall: ByteBuffer): MethodCall { + return STANDARD.decodeMethodCall(methodCall) + } + + override fun decodeEnvelope(envelope: ByteBuffer): Any { + return STANDARD.decodeEnvelope(envelope) + } + + override fun encodeMethodCall(methodCall: MethodCall): ByteBuffer { + return STANDARD.encodeMethodCall(methodCall) + } + + override fun encodeSuccessEnvelope(result: Any?): ByteBuffer { + if (result is ByteArray) { + val size = result.size + return ByteBuffer.allocateDirect(4 + size).apply { + put(0) + put(result) + } + } + + Log.e(LOG_TAG, "encodeSuccessEnvelope failed with result=$result") + return ByteBuffer.allocateDirect(0) + } + + override fun encodeErrorEnvelope(errorCode: String, errorMessage: String?, errorDetails: Any?): ByteBuffer { + Log.e(LOG_TAG, "encodeErrorEnvelope failed with errorCode=$errorCode, errorMessage=$errorMessage, errorDetails=$errorDetails") + return ByteBuffer.allocateDirect(0) + } + + override fun encodeErrorEnvelopeWithStacktrace(errorCode: String, errorMessage: String?, errorDetails: Any?, errorStacktrace: String?): ByteBuffer { + Log.e(LOG_TAG, "encodeErrorEnvelopeWithStacktrace failed with errorCode=$errorCode, errorMessage=$errorMessage, errorDetails=$errorDetails, errorStacktrace=$errorStacktrace") + return ByteBuffer.allocateDirect(0) + } + + companion object { + private val LOG_TAG = LogUtils.createTag() + val INSTANCE = AvesByteSendingMethodCodec() + private val STANDARD = StandardMethodCodec(StandardMessageCodec.INSTANCE) + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFetchHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFetchBytesHandler.kt similarity index 73% rename from android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFetchHandler.kt rename to android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFetchBytesHandler.kt index ba032ec2b..1306b2868 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFetchHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFetchBytesHandler.kt @@ -3,16 +3,11 @@ package deckers.thibault.aves.channel.calls import android.content.Context import android.graphics.Rect import android.net.Uri -import com.bumptech.glide.Glide -import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend import deckers.thibault.aves.channel.calls.fetchers.RegionFetcher import deckers.thibault.aves.channel.calls.fetchers.SvgRegionFetcher import deckers.thibault.aves.channel.calls.fetchers.ThumbnailFetcher import deckers.thibault.aves.channel.calls.fetchers.TiffRegionFetcher -import deckers.thibault.aves.model.FieldMap -import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback -import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider import deckers.thibault.aves.utils.MimeTypes import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel @@ -23,7 +18,7 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlin.math.roundToInt -class MediaFetchHandler(private val context: Context) : MethodCallHandler { +class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler { private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val density = context.resources.displayMetrics.density @@ -31,34 +26,12 @@ class MediaFetchHandler(private val context: Context) : MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - "getEntry" -> ioScope.launch { safe(call, result, ::getEntry) } "getThumbnail" -> ioScope.launch { safeSuspend(call, result, ::getThumbnail) } "getRegion" -> ioScope.launch { safeSuspend(call, result, ::getRegion) } - "clearSizedThumbnailDiskCache" -> ioScope.launch { safe(call, result, ::clearSizedThumbnailDiskCache) } else -> result.notImplemented() } } - private fun getEntry(call: MethodCall, result: MethodChannel.Result) { - val mimeType = call.argument("mimeType") // MIME type is optional - val uri = call.argument("uri")?.let { Uri.parse(it) } - if (uri == null) { - result.error("getEntry-args", "missing arguments", null) - return - } - - val provider = getProvider(uri) - if (provider == null) { - result.error("getEntry-provider", "failed to find provider for uri=$uri", null) - return - } - - provider.fetchSingle(context, uri, mimeType, object : ImageOpCallback { - override fun onSuccess(fields: FieldMap) = result.success(fields) - override fun onFailure(throwable: Throwable) = result.error("getEntry-failure", "failed to get entry for uri=$uri", throwable.message) - }) - } - private suspend fun getThumbnail(call: MethodCall, result: MethodChannel.Result) { val uri = call.argument("uri") val mimeType = call.argument("mimeType") @@ -137,12 +110,7 @@ class MediaFetchHandler(private val context: Context) : MethodCallHandler { } } - private fun clearSizedThumbnailDiskCache(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { - Glide.get(context).clearDiskCache() - result.success(null) - } - companion object { - const val CHANNEL = "deckers.thibault/aves/media_fetch" + const val CHANNEL = "deckers.thibault/aves/media_fetch_bytes" } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFetchObjectHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFetchObjectHandler.kt new file mode 100644 index 000000000..665d17a17 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFetchObjectHandler.kt @@ -0,0 +1,57 @@ +package deckers.thibault.aves.channel.calls + +import android.content.Context +import android.net.Uri +import com.bumptech.glide.Glide +import deckers.thibault.aves.channel.calls.Coresult.Companion.safe +import deckers.thibault.aves.model.FieldMap +import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback +import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch + +class MediaFetchObjectHandler(private val context: Context) : MethodCallHandler { + private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "getEntry" -> ioScope.launch { safe(call, result, ::getEntry) } + "clearSizedThumbnailDiskCache" -> ioScope.launch { safe(call, result, ::clearSizedThumbnailDiskCache) } + else -> result.notImplemented() + } + } + + private fun getEntry(call: MethodCall, result: MethodChannel.Result) { + val mimeType = call.argument("mimeType") // MIME type is optional + val uri = call.argument("uri")?.let { Uri.parse(it) } + if (uri == null) { + result.error("getEntry-args", "missing arguments", null) + return + } + + val provider = getProvider(uri) + if (provider == null) { + result.error("getEntry-provider", "failed to find provider for uri=$uri", null) + return + } + + provider.fetchSingle(context, uri, mimeType, object : ImageOpCallback { + override fun onSuccess(fields: FieldMap) = result.success(fields) + override fun onFailure(throwable: Throwable) = result.error("getEntry-failure", "failed to get entry for uri=$uri", throwable.message) + }) + } + + private fun clearSizedThumbnailDiskCache(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + Glide.get(context).clearDiskCache() + result.success(null) + } + + companion object { + const val CHANNEL = "deckers.thibault/aves/media_fetch_object" + } +} \ No newline at end of file diff --git a/lib/geo/topojson.dart b/lib/geo/topojson.dart index a6a499c73..c61dc4b73 100644 --- a/lib/geo/topojson.dart +++ b/lib/geo/topojson.dart @@ -12,7 +12,7 @@ class TopoJson { static Topology? _isoParse(String jsonData) { try { - final data = json.decode(jsonData) as Map; + final data = jsonDecode(jsonData) as Map; return Topology.parse(data); } catch (error, stack) { // an unhandled error in a spawn isolate would make the app crash diff --git a/lib/services/media/byte_receiving_codec.dart b/lib/services/media/byte_receiving_codec.dart new file mode 100644 index 000000000..ea8f82eb2 --- /dev/null +++ b/lib/services/media/byte_receiving_codec.dart @@ -0,0 +1,27 @@ +import 'package:flutter/services.dart'; + +class AvesByteReceivingMethodCodec extends StandardMethodCodec { + const AvesByteReceivingMethodCodec() : super(); + + @override + dynamic decodeEnvelope(ByteData envelope) { + // First byte is zero in success case, and non-zero otherwise. + if (envelope.lengthInBytes == 0) { + throw const FormatException('Expected envelope, got nothing'); + } + final ReadBuffer buffer = ReadBuffer(envelope); + if (buffer.getUint8() == 0) { + return envelope.buffer.asUint8List(envelope.offsetInBytes + 1, envelope.lengthInBytes - 1); + } + + final Object? errorCode = messageCodec.readValue(buffer); + final Object? errorMessage = messageCodec.readValue(buffer); + final Object? errorDetails = messageCodec.readValue(buffer); + final String? errorStacktrace = (buffer.hasRemaining) ? messageCodec.readValue(buffer) as String? : null; + if (errorCode is String && (errorMessage == null || errorMessage is String) && !buffer.hasRemaining) { + throw PlatformException(code: errorCode, message: errorMessage as String?, details: errorDetails, stacktrace: errorStacktrace); + } else { + throw const FormatException('Invalid envelope'); + } + } +} diff --git a/lib/services/media/media_fetch_service.dart b/lib/services/media/media_fetch_service.dart index b5a3f6862..7bf81e98b 100644 --- a/lib/services/media/media_fetch_service.dart +++ b/lib/services/media/media_fetch_service.dart @@ -6,6 +6,7 @@ import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/common/output_buffer.dart'; import 'package:aves/services/common/service_policy.dart'; import 'package:aves/services/common/services.dart'; +import 'package:aves/services/media/byte_receiving_codec.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:streams_channel/streams_channel.dart'; @@ -66,14 +67,15 @@ abstract class MediaFetchService { } class PlatformMediaFetchService implements MediaFetchService { - static const _platform = MethodChannel('deckers.thibault/aves/media_fetch'); + static const _platformObject = MethodChannel('deckers.thibault/aves/media_fetch_object'); + static const _platformBytes = MethodChannel('deckers.thibault/aves/media_fetch_bytes', AvesByteReceivingMethodCodec()); static final _byteStream = StreamsChannel('deckers.thibault/aves/media_byte_stream'); static const double _thumbnailDefaultSize = 64.0; @override Future getEntry(String uri, String? mimeType) async { try { - final result = await _platform.invokeMethod('getEntry', { + final result = await _platformObject.invokeMethod('getEntry', { 'uri': uri, 'mimeType': mimeType, }) as Map; @@ -171,7 +173,7 @@ class PlatformMediaFetchService implements MediaFetchService { return servicePolicy.call( () async { try { - final result = await _platform.invokeMethod('getRegion', { + final result = await _platformBytes.invokeMethod('getRegion', { 'uri': uri, 'mimeType': mimeType, 'pageId': pageId, @@ -211,7 +213,7 @@ class PlatformMediaFetchService implements MediaFetchService { return servicePolicy.call( () async { try { - final result = await _platform.invokeMethod('getThumbnail', { + final result = await _platformBytes.invokeMethod('getThumbnail', { 'uri': uri, 'mimeType': mimeType, 'dateModifiedSecs': dateModifiedSecs, @@ -238,7 +240,7 @@ class PlatformMediaFetchService implements MediaFetchService { @override Future clearSizedThumbnailDiskCache() async { try { - return _platform.invokeMethod('clearSizedThumbnailDiskCache'); + return _platformObject.invokeMethod('clearSizedThumbnailDiskCache'); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } diff --git a/lib/widgets/viewer/info/metadata/xmp_tile.dart b/lib/widgets/viewer/info/metadata/xmp_tile.dart index 0a7a4748d..3d0ac7a23 100644 --- a/lib/widgets/viewer/info/metadata/xmp_tile.dart +++ b/lib/widgets/viewer/info/metadata/xmp_tile.dart @@ -43,7 +43,7 @@ class _XmpDirTileState extends State { super.initState(); _tags = Map.from(widget.tags)..remove(schemaRegistryPrefixesKey); final prefixesJson = widget.allTags[schemaRegistryPrefixesKey]; - final Map prefixesDecoded = prefixesJson != null ? json.decode(prefixesJson) : {}; + final Map prefixesDecoded = prefixesJson != null ? jsonDecode(prefixesJson) : {}; _schemaRegistryPrefixes = Map.fromEntries(prefixesDecoded.entries.map((kv) => MapEntry(kv.key, kv.value as String))); }