channel multiple result crashfix, channel error reporting, crashlytics abstraction

This commit is contained in:
Thibault Deckers 2021-08-02 19:15:03 +09:00
parent 9b90c7ba84
commit 2a82aef354
37 changed files with 482 additions and 249 deletions

View file

@ -47,6 +47,7 @@ class MainActivity : FlutterActivity() {
val messenger = flutterEngine!!.dartExecutor.binaryMessenger
// dart -> platform -> dart
MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(AppAdapterHandler(this))
MethodChannel(messenger, AppShortcutHandler.CHANNEL).setMethodCallHandler(AppShortcutHandler(this))
MethodChannel(messenger, DebugHandler.CHANNEL).setMethodCallHandler(DebugHandler(this))
@ -61,12 +62,13 @@ class MainActivity : FlutterActivity() {
MethodChannel(messenger, TimeHandler.CHANNEL).setMethodCallHandler(TimeHandler())
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(WindowHandler(this))
// result streaming: dart -> platform ->->-> dart
StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(this, args) }
StreamsChannel(messenger, ImageOpStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageOpStreamHandler(this, args) }
StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(this, args) }
StreamsChannel(messenger, StorageAccessStreamHandler.CHANNEL).setStreamHandlerFactory { args -> StorageAccessStreamHandler(this, args) }
// Media Store change monitoring
// change monitoring: platform -> dart
mediaStoreChangeStreamHandler = MediaStoreChangeStreamHandler(this).apply {
EventChannel(messenger, MediaStoreChangeStreamHandler.CHANNEL).setStreamHandler(this)
}
@ -75,9 +77,11 @@ class MainActivity : FlutterActivity() {
}
// intent handling
// notification: platform -> dart
intentStreamHandler = IntentStreamHandler().apply {
EventChannel(messenger, IntentStreamHandler.CHANNEL).setStreamHandler(this)
}
// detail fetch: dart -> platform
intentDataMap = extractIntentData(intent)
MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
@ -89,6 +93,11 @@ class MainActivity : FlutterActivity() {
}
}
// notification: platform -> dart
errorStreamHandler = ErrorStreamHandler().apply {
EventChannel(messenger, ErrorStreamHandler.CHANNEL).setStreamHandler(this)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
setupShortcuts()
}
@ -243,6 +252,10 @@ class MainActivity : FlutterActivity() {
handler.onDenied()
}
}
var errorStreamHandler: ErrorStreamHandler? = null
fun notifyError(error: String) = errorStreamHandler?.notifyError(error)
}
}

View file

@ -10,7 +10,7 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safesus
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.LogUtils
@ -29,35 +29,13 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getPackages" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getPackages) }
"getAppIcon" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getAppIcon) }
"getAppIcon" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::getAppIcon) }
"copyToClipboard" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::copyToClipboard) }
"edit" -> {
val title = call.argument<String>("title")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val mimeType = call.argument<String>("mimeType")
result.success(edit(title, uri, mimeType))
}
"open" -> {
val title = call.argument<String>("title")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val mimeType = call.argument<String>("mimeType")
result.success(open(title, uri, mimeType))
}
"openMap" -> {
val geoUri = call.argument<String>("geoUri")?.let { Uri.parse(it) }
result.success(openMap(geoUri))
}
"setAs" -> {
val title = call.argument<String>("title")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val mimeType = call.argument<String>("mimeType")
result.success(setAs(title, uri, mimeType))
}
"share" -> {
val title = call.argument<String>("title")
val urisByMimeType = call.argument<Map<String, List<String>>>("urisByMimeType")!!
result.success(shareMultiple(title, urisByMimeType))
}
"edit" -> safe(call, result, ::edit)
"open" -> safe(call, result, ::open)
"openMap" -> safe(call, result, ::openMap)
"setAs" -> safe(call, result, ::setAs)
"share" -> safe(call, result, ::share)
else -> result.notImplemented()
}
}
@ -173,77 +151,113 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
}
}
private fun edit(title: String?, uri: Uri?, mimeType: String?): Boolean {
uri ?: return false
private fun edit(call: MethodCall, result: MethodChannel.Result) {
val title = call.argument<String>("title")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val mimeType = call.argument<String>("mimeType")
if (uri == null) {
result.error("edit-args", "failed because of missing arguments", null)
return
}
val intent = Intent(Intent.ACTION_EDIT)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
.setDataAndType(getShareableUri(uri), mimeType)
return safeStartActivityChooser(title, intent)
val started = safeStartActivityChooser(title, intent)
result.success(started)
}
private fun open(title: String?, uri: Uri?, mimeType: String?): Boolean {
uri ?: return false
private fun open(call: MethodCall, result: MethodChannel.Result) {
val title = call.argument<String>("title")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val mimeType = call.argument<String>("mimeType")
if (uri == null) {
result.error("open-args", "failed because of missing arguments", null)
return
}
val intent = Intent(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.setDataAndType(getShareableUri(uri), mimeType)
return safeStartActivityChooser(title, intent)
val started = safeStartActivityChooser(title, intent)
result.success(started)
}
private fun openMap(geoUri: Uri?): Boolean {
geoUri ?: return false
private fun openMap(call: MethodCall, result: MethodChannel.Result) {
val geoUri = call.argument<String>("geoUri")?.let { Uri.parse(it) }
if (geoUri == null) {
result.error("openMap-args", "failed because of missing arguments", null)
return
}
val intent = Intent(Intent.ACTION_VIEW, geoUri)
return safeStartActivity(intent)
val started = safeStartActivity(intent)
result.success(started)
}
private fun setAs(title: String?, uri: Uri?, mimeType: String?): Boolean {
uri ?: return false
private fun setAs(call: MethodCall, result: MethodChannel.Result) {
val title = call.argument<String>("title")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val mimeType = call.argument<String>("mimeType")
if (uri == null) {
result.error("setAs-args", "failed because of missing arguments", null)
return
}
val intent = Intent(Intent.ACTION_ATTACH_DATA)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.setDataAndType(getShareableUri(uri), mimeType)
return safeStartActivityChooser(title, intent)
val started = safeStartActivityChooser(title, intent)
result.success(started)
}
private fun shareSingle(title: String?, uri: Uri, mimeType: String): Boolean {
val intent = Intent(Intent.ACTION_SEND)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.setType(mimeType)
.putExtra(Intent.EXTRA_STREAM, getShareableUri(uri))
return safeStartActivityChooser(title, intent)
}
private fun shareMultiple(title: String?, urisByMimeType: Map<String, List<String>>?): Boolean {
urisByMimeType ?: return false
private fun share(call: MethodCall, result: MethodChannel.Result) {
val title = call.argument<String>("title")
val urisByMimeType = call.argument<Map<String, List<String>>>("urisByMimeType")
if (urisByMimeType == null) {
result.error("setAs-args", "failed because of missing arguments", null)
return
}
val uriList = ArrayList(urisByMimeType.values.flatten().mapNotNull { Uri.parse(it) })
val mimeTypes = urisByMimeType.keys.toTypedArray()
// simplify share intent for a single item, as some apps can handle one item but not more
if (uriList.size == 1) {
return shareSingle(title, uriList.first(), mimeTypes.first())
}
val started = if (uriList.size == 1) {
val uri = uriList.first()
val mimeType = mimeTypes.first()
var mimeType = "*/*"
if (mimeTypes.size == 1) {
// items have the same mime type & subtype
mimeType = mimeTypes.first()
val intent = Intent(Intent.ACTION_SEND)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.setType(mimeType)
.putExtra(Intent.EXTRA_STREAM, getShareableUri(uri))
safeStartActivityChooser(title, intent)
} else {
// items have different subtypes
val mimeTypeTypes = mimeTypes.map { it.split("/") }.distinct()
if (mimeTypeTypes.size == 1) {
// items have the same mime type
mimeType = "${mimeTypeTypes.first()}/*"
var mimeType = "*/*"
if (mimeTypes.size == 1) {
// items have the same mime type & subtype
mimeType = mimeTypes.first()
} else {
// items have different subtypes
val mimeTypeTypes = mimeTypes.map { it.split("/") }.distinct()
if (mimeTypeTypes.size == 1) {
// items have the same mime type
mimeType = "${mimeTypeTypes.first()}/*"
}
}
val intent = Intent(Intent.ACTION_SEND_MULTIPLE)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList)
.setType(mimeType)
safeStartActivityChooser(title, intent)
}
val intent = Intent(Intent.ACTION_SEND_MULTIPLE)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList)
.setType(mimeType)
return safeStartActivityChooser(title, intent)
result.success(started)
}
private fun safeStartActivity(intent: Intent): Boolean {

View file

@ -8,6 +8,7 @@ import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.R
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.utils.BitmapUtils.centerSquareCrop
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
@ -20,23 +21,31 @@ import java.util.*
class AppShortcutHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"canPin" -> result.success(canPin())
"pin" -> {
GlobalScope.launch(Dispatchers.IO) { pin(call) }
result.success(null)
}
"canPin" -> safe(call, result, ::canPin)
"pin" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::pin) }
else -> result.notImplemented()
}
}
private fun canPin() = ShortcutManagerCompat.isRequestPinShortcutSupported(context)
private fun isSupported() = ShortcutManagerCompat.isRequestPinShortcutSupported(context)
private fun pin(call: MethodCall) {
if (!canPin()) return
private fun canPin(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
result.success(isSupported())
}
val label = call.argument<String>("label") ?: return
private fun pin(call: MethodCall, result: MethodChannel.Result) {
val label = call.argument<String>("label")
val iconBytes = call.argument<ByteArray>("iconBytes")
val filters = call.argument<List<String>>("filters") ?: return
val filters = call.argument<List<String>>("filters")
if (label == null || filters == null) {
result.error("pin-args", "failed because of missing arguments", null)
return
}
if (!isSupported()) {
result.error("pin-unsupported", "failed because the launcher does not support pinning shortcuts", null)
return
}
var icon: IconCompat? = null
if (iconBytes?.isNotEmpty() == true) {
@ -62,6 +71,8 @@ class AppShortcutHandler(private val context: Context) : MethodCallHandler {
.setIntent(intent)
.build()
ShortcutManagerCompat.requestPinShortcut(context, shortcut, null)
result.success(true)
}
companion object {

View file

@ -1,5 +1,6 @@
package deckers.thibault.aves.channel.calls
import deckers.thibault.aves.MainActivity
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CoroutineScope
@ -8,24 +9,42 @@ import kotlinx.coroutines.launch
import kotlin.reflect.KSuspendFunction2
// ensure `result` methods are called on the main looper thread
class Coresult internal constructor(private val methodResult: MethodChannel.Result) : MethodChannel.Result {
class Coresult internal constructor(private val call: MethodCall, private val methodResult: MethodChannel.Result) : MethodChannel.Result {
private val mainScope = CoroutineScope(Dispatchers.Main)
override fun success(result: Any?) {
mainScope.launch { methodResult.success(result) }
mainScope.launch {
try {
methodResult.success(result)
} catch (e: Exception) {
MainActivity.notifyError("failed to reply success for method=${call.method}, result=$result, exception=$e")
}
}
}
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
mainScope.launch { methodResult.error(errorCode, errorMessage, errorDetails) }
mainScope.launch {
try {
methodResult.error(errorCode, errorMessage, errorDetails)
} catch (e: Exception) {
MainActivity.notifyError("failed to reply error for method=${call.method}, errorCode=$errorCode, errorMessage=$errorMessage, errorDetails=$errorDetails, exception=$e")
}
}
}
override fun notImplemented() {
mainScope.launch { methodResult.notImplemented() }
mainScope.launch {
try {
methodResult.notImplemented()
} catch (e: Exception) {
MainActivity.notifyError("failed to reply notImplemented for method=${call.method}, exception=$e")
}
}
}
companion object {
fun safe(call: MethodCall, result: MethodChannel.Result, function: (call: MethodCall, result: MethodChannel.Result) -> Unit) {
val res = Coresult(result)
val res = Coresult(call, result)
try {
function(call, res)
} catch (e: Exception) {
@ -33,12 +52,12 @@ class Coresult internal constructor(private val methodResult: MethodChannel.Resu
}
}
suspend fun safesus(call: MethodCall, result: MethodChannel.Result, function: KSuspendFunction2<MethodCall, MethodChannel.Result, Unit>) {
val res = Coresult(result)
suspend fun safeSuspend(call: MethodCall, result: MethodChannel.Result, function: KSuspendFunction2<MethodCall, MethodChannel.Result, Unit>) {
val res = Coresult(call, result)
try {
function(call, res)
} catch (e: Exception) {
res.error("safe-exception", e.message, e.stackTraceToString())
res.error("safeSuspend-exception", e.message, e.stackTraceToString())
}
}
}

View file

@ -6,6 +6,8 @@ import android.database.Cursor
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.provider.MediaStore
import android.util.Log
import androidx.exifinterface.media.ExifInterface
@ -36,8 +38,15 @@ import java.util.*
class DebugHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"crash" -> Handler(Looper.getMainLooper()).postDelayed({ throw TestException() }, 50)
"exception" -> throw TestException()
"safeException" -> safe(call, result) { _, _ -> throw TestException() }
"exceptionInCoroutine" -> GlobalScope.launch(Dispatchers.IO) { throw TestException() }
"safeExceptionInCoroutine" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result) { _, _ -> throw TestException() } }
"getContextDirs" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getContextDirs) }
"getEnv" -> result.success(System.getenv())
"getEnv" -> safe(call, result, ::getEnv)
"getBitmapFactoryInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getBitmapFactoryInfo) }
"getContentResolverMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getContentResolverMetadata) }
"getExifInterfaceMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getExifInterfaceMetadata) }
@ -71,6 +80,10 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
result.success(dirs)
}
private fun getEnv(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
result.success(System.getenv())
}
private fun getBitmapFactoryInfo(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
if (uri == null) {
@ -320,4 +333,6 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
private val LOG_TAG = LogUtils.createTag<DebugHandler>()
const val CHANNEL = "deckers.thibault/aves/debug"
}
class TestException internal constructor() : RuntimeException("oops")
}

View file

@ -1,6 +1,7 @@
package deckers.thibault.aves.channel.calls
import android.os.Build
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
@ -8,17 +9,18 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler
class DeviceHandler : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getPerformanceClass" -> result.success(getPerformanceClass())
"getPerformanceClass" -> safe(call, result, ::getPerformanceClass)
else -> result.notImplemented()
}
}
private fun getPerformanceClass(): Int {
private fun getPerformanceClass(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
// TODO TLAD uncomment when the future is here
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// return Build.VERSION.MEDIA_PERFORMANCE_CLASS
// result.success(Build.VERSION.MEDIA_PERFORMANCE_CLASS)
// return
// }
return Build.VERSION.SDK_INT
result.success(Build.VERSION.SDK_INT)
}
companion object {

View file

@ -12,7 +12,7 @@ import com.drew.imaging.ImageMetadataReader
import com.drew.metadata.file.FileTypeDirectory
import com.drew.metadata.xmp.XmpDirectory
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safesus
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
import deckers.thibault.aves.metadata.Metadata
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString
import deckers.thibault.aves.metadata.MultiPage
@ -44,7 +44,7 @@ import java.util.*
class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getExifThumbnails" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getExifThumbnails) }
"getExifThumbnails" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::getExifThumbnails) }
"extractMotionPhotoVideo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractMotionPhotoVideo) }
"extractVideoEmbeddedPicture" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractVideoEmbeddedPicture) }
"extractXmpDataProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractXmpDataProp) }

View file

@ -2,6 +2,7 @@ package deckers.thibault.aves.channel.calls
import android.content.Context
import android.location.Geocoder
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
@ -18,7 +19,7 @@ class GeocodingHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getAddress" -> GlobalScope.launch(Dispatchers.IO) { Coresult.safe(call, result, ::getAddress) }
"getAddress" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getAddress) }
else -> result.notImplemented()
}
}

View file

@ -5,7 +5,7 @@ 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.safesus
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
@ -32,10 +32,10 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getEntry" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getEntry) }
"getThumbnail" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getThumbnail) }
"getRegion" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getRegion) }
"captureFrame" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::captureFrame) }
"rename" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::rename) }
"getThumbnail" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::getThumbnail) }
"getRegion" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::getRegion) }
"captureFrame" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::captureFrame) }
"rename" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::rename) }
"rotate" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::rotate) }
"flip" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::flip) }
"clearSizedThumbnailDiskCache" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::clearSizedThumbnailDiskCache) }

View file

@ -1,5 +1,6 @@
package deckers.thibault.aves.channel.calls
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
@ -8,11 +9,15 @@ import java.util.*
class TimeHandler : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getDefaultTimeZone" -> result.success(TimeZone.getDefault().id)
"getDefaultTimeZone" -> safe(call, result, ::getDefaultTimeZone)
else -> result.notImplemented()
}
}
private fun getDefaultTimeZone(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
result.success(TimeZone.getDefault().id)
}
companion object {
const val CHANNEL = "deckers.thibault/aves/time"
}

View file

@ -17,7 +17,7 @@ class WindowHandler(private val activity: Activity) : MethodCallHandler {
"keepScreenOn" -> safe(call, result, ::keepScreenOn)
"isRotationLocked" -> safe(call, result, ::isRotationLocked)
"requestOrientation" -> safe(call, result, ::requestOrientation)
"canSetCutoutMode" -> result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
"canSetCutoutMode" -> safe(call, result, ::canSetCutoutMode)
"setCutoutMode" -> safe(call, result, ::setCutoutMode)
else -> result.notImplemented()
}
@ -60,6 +60,10 @@ class WindowHandler(private val activity: Activity) : MethodCallHandler {
result.success(true)
}
private fun canSetCutoutMode(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
}
private fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) {
val use = call.argument<Boolean>("use")
if (use == null) {

View file

@ -0,0 +1,25 @@
package deckers.thibault.aves.channel.streams
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink
class ErrorStreamHandler : EventChannel.StreamHandler {
// cannot use `lateinit` because we cannot guarantee
// its initialization in `onListen` at the right time
// e.g. when resuming the app after the activity got destroyed
private var eventSink: EventSink? = null
override fun onListen(arguments: Any?, eventSink: EventSink) {
this.eventSink = eventSink
}
override fun onCancel(arguments: Any?) {}
fun notifyError(error: String) {
eventSink?.success(error)
}
companion object {
const val CHANNEL = "deckers.thibault/aves/error"
}
}

View file

@ -1,7 +1,7 @@
import 'dart:isolate';
import 'package:aves/services/services.dart';
import 'package:aves/widgets/aves_app.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';
void main() {
@ -20,12 +20,11 @@ void main() {
//
// flutter run --profile --trace-skia
Isolate.current.addErrorListener(RawReceivePort((pair) async {
initPlatformServices();
Isolate.current.addErrorListener(RawReceivePort((pair) {
final List<dynamic> errorAndStacktrace = pair;
await FirebaseCrashlytics.instance.recordError(
errorAndStacktrace.first,
errorAndStacktrace.last,
);
reportService.recordError(errorAndStacktrace.first, errorAndStacktrace.last);
}).sendPort);
runApp(const AvesApp());

View file

@ -13,7 +13,6 @@ import 'package:aves/services/services.dart';
import 'package:aves/utils/pedantic.dart';
import 'package:collection/collection.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
@ -120,7 +119,7 @@ class Settings extends ChangeNotifier {
// to allow settings customization without Firebase context (e.g. before a Flutter Driver test)
Future<void> initFirebase() async {
await Firebase.app().setAutomaticDataCollectionEnabled(isCrashlyticsEnabled);
await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(isCrashlyticsEnabled);
await reportService.setCollectionEnabled(isCrashlyticsEnabled);
}
Future<void> reset({required bool includeInternalKeys}) async {

View file

@ -1,9 +1,9 @@
import 'dart:typed_data';
import 'package:aves/model/entry.dart';
import 'package:aves/services/services.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
class AndroidAppService {
@ -20,7 +20,7 @@ class AndroidAppService {
}
return packages;
} on PlatformException catch (e) {
debugPrint('getPackages failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
await reportService.recordChannelError('getPackages', e);
}
return {};
}
@ -33,7 +33,7 @@ class AndroidAppService {
});
if (result != null) return result as Uint8List;
} on PlatformException catch (e) {
debugPrint('getAppIcon failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('getAppIcon', e);
}
return Uint8List(0);
}
@ -46,7 +46,7 @@ class AndroidAppService {
});
if (result != null) return result as bool;
} on PlatformException catch (e) {
debugPrint('copyToClipboard failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('copyToClipboard', e);
}
return false;
}
@ -59,7 +59,7 @@ class AndroidAppService {
});
if (result != null) return result as bool;
} on PlatformException catch (e) {
debugPrint('edit failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('edit', e);
}
return false;
}
@ -72,7 +72,7 @@ class AndroidAppService {
});
if (result != null) return result as bool;
} on PlatformException catch (e) {
debugPrint('open failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('open', e);
}
return false;
}
@ -84,7 +84,7 @@ class AndroidAppService {
});
if (result != null) return result as bool;
} on PlatformException catch (e) {
debugPrint('openMap failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('openMap', e);
}
return false;
}
@ -97,7 +97,7 @@ class AndroidAppService {
});
if (result != null) return result as bool;
} on PlatformException catch (e) {
debugPrint('setAs failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('setAs', e);
}
return false;
}
@ -112,7 +112,7 @@ class AndroidAppService {
});
if (result != null) return result as bool;
} on PlatformException catch (e) {
debugPrint('shareEntries failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('shareEntries', e);
}
return false;
}
@ -126,7 +126,7 @@ class AndroidAppService {
});
if (result != null) return result as bool;
} on PlatformException catch (e) {
debugPrint('shareSingle failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('shareSingle', e);
}
return false;
}

View file

@ -1,17 +1,57 @@
import 'package:aves/model/entry.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:flutter/foundation.dart';
import 'package:aves/services/services.dart';
import 'package:flutter/services.dart';
class AndroidDebugService {
static const platform = MethodChannel('deckers.thibault/aves/debug');
static Future<void> crash() async {
try {
await platform.invokeMethod('crash');
} on PlatformException catch (e) {
await reportService.recordChannelError('crash', e);
}
}
static Future<void> exception() async {
try {
await platform.invokeMethod('exception');
} on PlatformException catch (e) {
await reportService.recordChannelError('exception', e);
}
}
static Future<void> safeException() async {
try {
await platform.invokeMethod('safeException');
} on PlatformException catch (e) {
await reportService.recordChannelError('safeException', e);
}
}
static Future<void> exceptionInCoroutine() async {
try {
await platform.invokeMethod('exceptionInCoroutine');
} on PlatformException catch (e) {
await reportService.recordChannelError('exceptionInCoroutine', e);
}
}
static Future<void> safeExceptionInCoroutine() async {
try {
await platform.invokeMethod('safeExceptionInCoroutine');
} on PlatformException catch (e) {
await reportService.recordChannelError('safeExceptionInCoroutine', e);
}
}
static Future<Map> getContextDirs() async {
try {
final result = await platform.invokeMethod('getContextDirs');
if (result != null) return result as Map;
} on PlatformException catch (e) {
debugPrint('getContextDirs failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
await reportService.recordChannelError('getContextDirs', e);
}
return {};
}
@ -21,7 +61,7 @@ class AndroidDebugService {
final result = await platform.invokeMethod('getEnv');
if (result != null) return result as Map;
} on PlatformException catch (e) {
debugPrint('getEnv failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
await reportService.recordChannelError('getEnv', e);
}
return {};
}
@ -34,7 +74,7 @@ class AndroidDebugService {
});
if (result != null) return result as Map;
} on PlatformException catch (e) {
debugPrint('getBitmapFactoryInfo failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('getBitmapFactoryInfo', e);
}
return {};
}
@ -48,7 +88,7 @@ class AndroidDebugService {
});
if (result != null) return result as Map;
} on PlatformException catch (e) {
debugPrint('getContentResolverMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('getContentResolverMetadata', e);
}
return {};
}
@ -63,7 +103,7 @@ class AndroidDebugService {
});
if (result != null) return result as Map;
} on PlatformException catch (e) {
debugPrint('getExifInterfaceMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('getExifInterfaceMetadata', e);
}
return {};
}
@ -76,7 +116,7 @@ class AndroidDebugService {
});
if (result != null) return result as Map;
} on PlatformException catch (e) {
debugPrint('getMediaMetadataRetrieverMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('getMediaMetadataRetrieverMetadata', e);
}
return {};
}
@ -91,7 +131,7 @@ class AndroidDebugService {
});
if (result != null) return result as Map;
} on PlatformException catch (e) {
debugPrint('getMetadataExtractorSummary failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('getMetadataExtractorSummary', e);
}
return {};
}
@ -105,7 +145,7 @@ class AndroidDebugService {
});
if (result != null) return result as Map;
} on PlatformException catch (e) {
debugPrint('getTiffStructure failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('getTiffStructure', e);
}
return {};
}

View file

@ -24,7 +24,7 @@ class AppShortcutService {
return result;
}
} on PlatformException catch (e) {
debugPrint('canPin failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
await reportService.recordChannelError('canPin', e);
}
return false;
}
@ -50,7 +50,7 @@ class AppShortcutService {
'filters': filters.map((filter) => filter.toJson()).toList(),
});
} on PlatformException catch (e) {
debugPrint('pin failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('pin', e);
}
}
}

View file

@ -1,6 +1,5 @@
import 'package:flutter/foundation.dart';
import 'package:aves/services/services.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
class DeviceService {
static const platform = MethodChannel('deckers.thibault/aves/device');
@ -11,7 +10,7 @@ class DeviceService {
final result = await platform.invokeMethod('getPerformanceClass');
if (result != null) return result as int;
} on PlatformException catch (e) {
debugPrint('getPerformanceClass failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('getPerformanceClass', e);
}
return 0;
}

View file

@ -1,7 +1,7 @@
import 'dart:typed_data';
import 'package:aves/model/entry.dart';
import 'package:flutter/foundation.dart';
import 'package:aves/services/services.dart';
import 'package:flutter/services.dart';
abstract class EmbeddedDataService {
@ -27,7 +27,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService {
});
if (result != null) return (result as List).cast<Uint8List>();
} on PlatformException catch (e) {
debugPrint('getExifThumbnail failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('getExifThumbnail', e);
}
return [];
}
@ -43,7 +43,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService {
});
if (result != null) return result as Map;
} on PlatformException catch (e) {
debugPrint('extractMotionPhotoVideo failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('extractMotionPhotoVideo', e);
}
return {};
}
@ -57,7 +57,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService {
});
if (result != null) return result as Map;
} on PlatformException catch (e) {
debugPrint('extractVideoEmbeddedPicture failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('extractVideoEmbeddedPicture', e);
}
return {};
}
@ -75,7 +75,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService {
});
if (result != null) return result as Map;
} on PlatformException catch (e) {
debugPrint('extractXmpDataProp failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('extractXmpDataProp', e);
}
return {};
}

View file

@ -1,8 +1,8 @@
import 'dart:async';
import 'package:aves/services/services.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:latlong2/latlong.dart';
class GeocodingService {
@ -21,7 +21,7 @@ class GeocodingService {
});
return (result as List).cast<Map>().map((map) => Address.fromMap(map)).toList();
} on PlatformException catch (e) {
debugPrint('getAddress failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
await reportService.recordChannelError('getAddress', e);
}
return [];
}

View file

@ -1,7 +1,6 @@
import 'dart:ui';
import 'package:aves/services/services.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:intl/date_symbol_data_local.dart';
@ -16,7 +15,7 @@ class GlobalSearch {
'callbackHandle': PluginUtilities.getCallbackHandle(_init)?.toRawHandle(),
});
} on PlatformException catch (e) {
debugPrint('registerCallback failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('registerCallback', e);
}
}
}

View file

@ -1,14 +1,15 @@
import 'dart:async';
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui';
import 'package:aves/model/entry.dart';
import 'package:aves/services/image_op_events.dart';
import 'package:aves/services/output_buffer.dart';
import 'package:aves/services/service_policy.dart';
import 'package:aves/services/services.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:streams_channel/streams_channel.dart';
abstract class ImageFileService {
@ -124,7 +125,7 @@ class PlatformImageFileService implements ImageFileService {
}) as Map;
return AvesEntry.fromMap(result);
} on PlatformException catch (e) {
debugPrint('getEntry failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('getEntry', e);
}
return null;
}
@ -188,7 +189,7 @@ class PlatformImageFileService implements ImageFileService {
);
return completer.future;
} on PlatformException catch (e) {
debugPrint('getImage failed with code=${e.code}, exception=${e.message}, details=${e.details}');
reportService.recordChannelError('getImage', e);
}
return Future.sync(() => Uint8List(0));
}
@ -223,7 +224,7 @@ class PlatformImageFileService implements ImageFileService {
});
if (result != null) return result as Uint8List;
} on PlatformException catch (e) {
debugPrint('getRegion failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('getRegion', e);
}
return Uint8List(0);
},
@ -260,7 +261,7 @@ class PlatformImageFileService implements ImageFileService {
});
if (result != null) return result as Uint8List;
} on PlatformException catch (e) {
debugPrint('getThumbnail failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('getThumbnail', e);
}
return Uint8List(0);
},
@ -274,7 +275,7 @@ class PlatformImageFileService implements ImageFileService {
try {
return platform.invokeMethod('clearSizedThumbnailDiskCache');
} on PlatformException catch (e) {
debugPrint('clearSizedThumbnailDiskCache failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('clearSizedThumbnailDiskCache', e);
}
}
@ -295,7 +296,7 @@ class PlatformImageFileService implements ImageFileService {
'entries': entries.map(_toPlatformEntryMap).toList(),
}).map((event) => ImageOpEvent.fromMap(event));
} on PlatformException catch (e) {
debugPrint('delete failed with code=${e.code}, exception=${e.message}, details=${e.details}');
reportService.recordChannelError('delete', e);
return Stream.error(e);
}
}
@ -314,7 +315,7 @@ class PlatformImageFileService implements ImageFileService {
'destinationPath': destinationAlbum,
}).map((event) => MoveOpEvent.fromMap(event));
} on PlatformException catch (e) {
debugPrint('move failed with code=${e.code}, exception=${e.message}, details=${e.details}');
reportService.recordChannelError('move', e);
return Stream.error(e);
}
}
@ -333,7 +334,7 @@ class PlatformImageFileService implements ImageFileService {
'destinationPath': destinationAlbum,
}).map((event) => ExportOpEvent.fromMap(event));
} on PlatformException catch (e) {
debugPrint('export failed with code=${e.code}, exception=${e.message}, details=${e.details}');
reportService.recordChannelError('export', e);
return Stream.error(e);
}
}
@ -356,7 +357,7 @@ class PlatformImageFileService implements ImageFileService {
});
if (result != null) return (result as Map).cast<String, dynamic>();
} on PlatformException catch (e) {
debugPrint('captureFrame failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('captureFrame', e);
}
return {};
}
@ -371,7 +372,7 @@ class PlatformImageFileService implements ImageFileService {
});
if (result != null) return (result as Map).cast<String, dynamic>();
} on PlatformException catch (e) {
debugPrint('rename failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('rename', e);
}
return {};
}
@ -386,7 +387,7 @@ class PlatformImageFileService implements ImageFileService {
});
if (result != null) return (result as Map).cast<String, dynamic>();
} on PlatformException catch (e) {
debugPrint('rotate failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('rotate', e);
}
return {};
}
@ -400,7 +401,7 @@ class PlatformImageFileService implements ImageFileService {
});
if (result != null) return (result as Map).cast<String, dynamic>();
} on PlatformException catch (e) {
debugPrint('flip failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('flip', e);
}
return {};
}

View file

@ -1,6 +1,5 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
@immutable
class ImageOpEvent extends Equatable {

View file

@ -1,8 +1,8 @@
import 'dart:async';
import 'package:aves/model/entry.dart';
import 'package:aves/services/services.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:streams_channel/streams_channel.dart';
abstract class MediaStoreService {
@ -26,7 +26,7 @@ class PlatformMediaStoreService implements MediaStoreService {
});
return (result as List).cast<int>();
} on PlatformException catch (e) {
debugPrint('checkObsoleteContentIds failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('checkObsoleteContentIds', e);
}
return [];
}
@ -39,7 +39,7 @@ class PlatformMediaStoreService implements MediaStoreService {
});
return (result as List).cast<int>();
} on PlatformException catch (e) {
debugPrint('checkObsoletePaths failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('checkObsoletePaths', e);
}
return [];
}
@ -51,7 +51,7 @@ class PlatformMediaStoreService implements MediaStoreService {
'knownEntries': knownEntries,
}).map((event) => AvesEntry.fromMap(event));
} on PlatformException catch (e) {
debugPrint('getEntries failed with code=${e.code}, exception=${e.message}, details=${e.details}');
reportService.recordChannelError('getEntries', e);
return Stream.error(e);
}
}

View file

@ -3,7 +3,7 @@ import 'package:aves/model/metadata.dart';
import 'package:aves/model/multipage.dart';
import 'package:aves/model/panorama.dart';
import 'package:aves/services/service_policy.dart';
import 'package:flutter/foundation.dart';
import 'package:aves/services/services.dart';
import 'package:flutter/services.dart';
abstract class MetadataService {
@ -36,7 +36,7 @@ class PlatformMetadataService implements MetadataService {
});
if (result != null) return result as Map;
} on PlatformException catch (e) {
debugPrint('getAllMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('getAllMetadata', e);
}
return {};
}
@ -66,7 +66,7 @@ class PlatformMetadataService implements MetadataService {
result['contentId'] = entry.contentId;
return CatalogMetadata.fromMap(result);
} on PlatformException catch (e) {
debugPrint('getCatalogMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('getCatalogMetadata', e);
}
return null;
}
@ -92,7 +92,7 @@ class PlatformMetadataService implements MetadataService {
}) as Map;
return OverlayMetadata.fromMap(result);
} on PlatformException catch (e) {
debugPrint('getOverlayMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('getOverlayMetadata', e);
}
return null;
}
@ -114,7 +114,7 @@ class PlatformMetadataService implements MetadataService {
}
return MultiPageInfo.fromPageMaps(entry, pageMaps);
} on PlatformException catch (e) {
debugPrint('getMultiPageInfo failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('getMultiPageInfo', e);
}
return null;
}
@ -132,7 +132,7 @@ class PlatformMetadataService implements MetadataService {
}) as Map;
return PanoramaInfo.fromMap(result);
} on PlatformException catch (e) {
debugPrint('PanoramaInfo failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('PanoramaInfo', e);
}
return null;
}
@ -146,7 +146,7 @@ class PlatformMetadataService implements MetadataService {
'prop': prop,
});
} on PlatformException catch (e) {
debugPrint('getContentResolverProp failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('getContentResolverProp', e);
}
return null;
}

View file

@ -0,0 +1,55 @@
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
abstract class ReportService {
bool get isCollectionEnabled;
Future<void> setCollectionEnabled(bool enabled);
Future<void> log(String message);
Future<void> setCustomKey(String key, Object value);
Future<void> setCustomKeys(Map<String, Object> map);
Future<void> recordError(dynamic exception, StackTrace? stack);
Future<void> recordFlutterError(FlutterErrorDetails flutterErrorDetails);
Future<void> recordChannelError(String method, PlatformException e) {
return recordError('$method failed with code=${e.code}, exception=${e.message}, details=${e.details}}', null);
}
}
class CrashlyticsReportService extends ReportService {
FirebaseCrashlytics get instance => FirebaseCrashlytics.instance;
@override
bool get isCollectionEnabled => instance.isCrashlyticsCollectionEnabled;
@override
Future<void> setCollectionEnabled(bool enabled) => instance.setCrashlyticsCollectionEnabled(enabled);
@override
Future<void> log(String message) => instance.log(message);
@override
Future<void> setCustomKey(String key, Object value) => instance.setCustomKey(key, value);
@override
Future<void> setCustomKeys(Map<String, Object> map) {
final _instance = instance;
return Future.forEach<MapEntry<String, Object>>(map.entries, (kv) => _instance.setCustomKey(kv.key, kv.value));
}
@override
Future<void> recordError(dynamic exception, StackTrace? stack) {
return instance.recordError(exception, stack);
}
@override
Future<void> recordFlutterError(FlutterErrorDetails flutterErrorDetails) {
return instance.recordFlutterError(flutterErrorDetails);
}
}

View file

@ -4,6 +4,7 @@ import 'package:aves/services/embedded_data_service.dart';
import 'package:aves/services/image_file_service.dart';
import 'package:aves/services/media_store_service.dart';
import 'package:aves/services/metadata_service.dart';
import 'package:aves/services/report_service.dart';
import 'package:aves/services/storage_service.dart';
import 'package:aves/services/time_service.dart';
import 'package:aves/services/window_service.dart';
@ -20,6 +21,7 @@ final EmbeddedDataService embeddedDataService = getIt<EmbeddedDataService>();
final ImageFileService imageFileService = getIt<ImageFileService>();
final MediaStoreService mediaStoreService = getIt<MediaStoreService>();
final MetadataService metadataService = getIt<MetadataService>();
final ReportService reportService = getIt<ReportService>();
final StorageService storageService = getIt<StorageService>();
final TimeService timeService = getIt<TimeService>();
final WindowService windowService = getIt<WindowService>();
@ -33,6 +35,7 @@ void initPlatformServices() {
getIt.registerLazySingleton<ImageFileService>(() => PlatformImageFileService());
getIt.registerLazySingleton<MediaStoreService>(() => PlatformMediaStoreService());
getIt.registerLazySingleton<MetadataService>(() => PlatformMetadataService());
getIt.registerLazySingleton<ReportService>(() => CrashlyticsReportService());
getIt.registerLazySingleton<StorageService>(() => PlatformStorageService());
getIt.registerLazySingleton<TimeService>(() => PlatformTimeService());
getIt.registerLazySingleton<WindowService>(() => PlatformWindowService());

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:typed_data';
import 'package:aves/services/output_buffer.dart';
import 'package:aves/services/services.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
@ -47,7 +48,7 @@ class PlatformStorageService implements StorageService {
final result = await platform.invokeMethod('getStorageVolumes');
return (result as List).cast<Map>().map((map) => StorageVolume.fromMap(map)).toSet();
} on PlatformException catch (e) {
debugPrint('getStorageVolumes failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
await reportService.recordChannelError('getStorageVolumes', e);
}
return {};
}
@ -60,7 +61,7 @@ class PlatformStorageService implements StorageService {
});
return result as int?;
} on PlatformException catch (e) {
debugPrint('getFreeSpace failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
await reportService.recordChannelError('getFreeSpace', e);
}
return null;
}
@ -71,7 +72,7 @@ class PlatformStorageService implements StorageService {
final result = await platform.invokeMethod('getGrantedDirectories');
return (result as List).cast<String>();
} on PlatformException catch (e) {
debugPrint('getGrantedDirectories failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
await reportService.recordChannelError('getGrantedDirectories', e);
}
return [];
}
@ -83,7 +84,7 @@ class PlatformStorageService implements StorageService {
'path': path,
});
} on PlatformException catch (e) {
debugPrint('revokeDirectoryAccess failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
await reportService.recordChannelError('revokeDirectoryAccess', e);
}
return;
}
@ -98,7 +99,7 @@ class PlatformStorageService implements StorageService {
return (result as List).cast<Map>().map(VolumeRelativeDirectory.fromMap).toSet();
}
} on PlatformException catch (e) {
debugPrint('getInaccessibleDirectories failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
await reportService.recordChannelError('getInaccessibleDirectories', e);
}
return {};
}
@ -111,7 +112,7 @@ class PlatformStorageService implements StorageService {
return (result as List).cast<Map>().map(VolumeRelativeDirectory.fromMap).toSet();
}
} on PlatformException catch (e) {
debugPrint('getRestrictedDirectories failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
await reportService.recordChannelError('getRestrictedDirectories', e);
}
return {};
}
@ -134,7 +135,7 @@ class PlatformStorageService implements StorageService {
);
return completer.future;
} on PlatformException catch (e) {
debugPrint('requestVolumeAccess failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
await reportService.recordChannelError('requestVolumeAccess', e);
}
return false;
}
@ -148,7 +149,7 @@ class PlatformStorageService implements StorageService {
});
if (result != null) return result as int;
} on PlatformException catch (e) {
debugPrint('deleteEmptyDirectories failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
await reportService.recordChannelError('deleteEmptyDirectories', e);
}
return 0;
}
@ -164,7 +165,7 @@ class PlatformStorageService implements StorageService {
});
if (result != null) return Uri.tryParse(result);
} on PlatformException catch (e) {
debugPrint('scanFile failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
await reportService.recordChannelError('scanFile', e);
}
return null;
}
@ -172,7 +173,7 @@ class PlatformStorageService implements StorageService {
@override
Future<bool?> createFile(String name, String mimeType, Uint8List bytes) async {
try {
final completer = Completer<bool>();
final completer = Completer<bool?>();
storageAccessChannel.receiveBroadcastStream(<String, dynamic>{
'op': 'createFile',
'name': name,
@ -188,7 +189,7 @@ class PlatformStorageService implements StorageService {
);
return completer.future;
} on PlatformException catch (e) {
debugPrint('createFile failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
await reportService.recordChannelError('createFile', e);
}
return false;
}
@ -215,7 +216,7 @@ class PlatformStorageService implements StorageService {
);
return completer.future;
} on PlatformException catch (e) {
debugPrint('openFile failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
await reportService.recordChannelError('openFile', e);
}
return Uint8List(0);
}
@ -223,7 +224,7 @@ class PlatformStorageService implements StorageService {
@override
Future<String?> selectDirectory() async {
try {
final completer = Completer<String>();
final completer = Completer<String?>();
storageAccessChannel.receiveBroadcastStream(<String, dynamic>{
'op': 'selectDirectory',
}).listen(
@ -236,7 +237,7 @@ class PlatformStorageService implements StorageService {
);
return completer.future;
} on PlatformException catch (e) {
debugPrint('selectDirectory failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
await reportService.recordChannelError('selectDirectory', e);
}
return null;
}

View file

@ -1,4 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:aves/services/services.dart';
import 'package:flutter/services.dart';
abstract class TimeService {
@ -13,7 +13,7 @@ class PlatformTimeService implements TimeService {
try {
return await platform.invokeMethod('getDefaultTimeZone');
} on PlatformException catch (e) {
debugPrint('getDefaultTimeZone failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('getDefaultTimeZone', e);
}
return null;
}

View file

@ -1,4 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:aves/services/services.dart';
import 'package:flutter/services.dart';
class ViewerService {
@ -10,7 +10,7 @@ class ViewerService {
final result = await platform.invokeMethod('getIntentData');
if (result != null) return (result as Map).cast<String, dynamic>();
} on PlatformException catch (e) {
debugPrint('getIntentData failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('getIntentData', e);
}
return {};
}
@ -21,7 +21,7 @@ class ViewerService {
'uri': uri,
});
} on PlatformException catch (e) {
debugPrint('pick failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('pick', e);
}
}
}

View file

@ -1,4 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:aves/services/services.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
@ -24,7 +24,7 @@ class PlatformWindowService implements WindowService {
'on': on,
});
} on PlatformException catch (e) {
debugPrint('keepScreenOn failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('keepScreenOn', e);
}
}
@ -34,7 +34,7 @@ class PlatformWindowService implements WindowService {
final result = await platform.invokeMethod('isRotationLocked');
if (result != null) return result as bool;
} on PlatformException catch (e) {
debugPrint('isRotationLocked failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('isRotationLocked', e);
}
return false;
}
@ -62,7 +62,7 @@ class PlatformWindowService implements WindowService {
'orientation': orientationCode,
});
} on PlatformException catch (e) {
debugPrint('requestOrientation failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('requestOrientation', e);
}
}
@ -72,7 +72,7 @@ class PlatformWindowService implements WindowService {
final result = await platform.invokeMethod('canSetCutoutMode');
if (result != null) return result as bool;
} on PlatformException catch (e) {
debugPrint('canSetCutoutMode failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('canSetCutoutMode', e);
}
return false;
}
@ -84,7 +84,7 @@ class PlatformWindowService implements WindowService {
'use': use,
});
} on PlatformException catch (e) {
debugPrint('setCutoutMode failed with code=${e.code}, exception=${e.message}, details=${e.details}');
await reportService.recordChannelError('setCutoutMode', e);
}
}
}

View file

@ -17,7 +17,6 @@ import 'package:aves/widgets/home_page.dart';
import 'package:aves/widgets/welcome_page.dart';
import 'package:equatable/equatable.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -44,6 +43,7 @@ class _AvesAppState extends State<AvesApp> {
List<NavigatorObserver> _navigatorObservers = [];
final EventChannel _mediaStoreChangeChannel = const EventChannel('deckers.thibault/aves/mediastorechange');
final EventChannel _newIntentChannel = const EventChannel('deckers.thibault/aves/intent');
final EventChannel _errorChannel = const EventChannel('deckers.thibault/aves/error');
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey(debugLabel: 'app-navigator');
Widget getFirstPage({Map? intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : const WelcomePage();
@ -52,10 +52,10 @@ class _AvesAppState extends State<AvesApp> {
void initState() {
super.initState();
EquatableConfig.stringify = true;
initPlatformServices();
_appSetup = _setup();
_mediaStoreChangeChannel.receiveBroadcastStream().listen((event) => _onMediaStoreChange(event as String?));
_newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map?));
_errorChannel.receiveBroadcastStream().listen((event) => _onError(event as String?));
}
@override
@ -124,18 +124,17 @@ class _AvesAppState extends State<AvesApp> {
Future<void> _setup() async {
await Firebase.initializeApp().then((app) {
final crashlytics = FirebaseCrashlytics.instance;
FlutterError.onError = crashlytics.recordFlutterError;
crashlytics.setCustomKey('locales', window.locales.join(', '));
FlutterError.onError = reportService.recordFlutterError;
final now = DateTime.now();
crashlytics.setCustomKey('timezone', '${now.timeZoneName} (${now.timeZoneOffset})');
crashlytics.setCustomKey(
'build_mode',
kReleaseMode
? 'release'
: kProfileMode
? 'profile'
: 'debug');
reportService.setCustomKeys({
'locales': window.locales.join(', '),
'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})',
'build_mode': kReleaseMode
? 'release'
: kProfileMode
? 'profile'
: 'debug',
});
});
await settings.init();
await settings.initFirebase();
@ -150,7 +149,7 @@ class _AvesAppState extends State<AvesApp> {
// do not reset when relaunching the app
if (appModeNotifier.value == AppMode.main && (intentData == null || intentData.isEmpty == true)) return;
FirebaseCrashlytics.instance.log('New intent');
reportService.log('New intent');
_navigatorKey.currentState!.pushReplacement(DirectMaterialPageRoute(
settings: const RouteSettings(name: HomePage.routeName),
builder: (_) => getFirstPage(intentData: intentData),
@ -171,4 +170,6 @@ class _AvesAppState extends State<AvesApp> {
});
}
}
void _onError(String? error) => reportService.recordError(error, null);
}

View file

@ -1,18 +1,18 @@
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:aves/services/services.dart';
import 'package:flutter/material.dart';
class CrashlyticsRouteTracker extends NavigatorObserver {
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) => FirebaseCrashlytics.instance.log('Nav didPush to ${_name(route)}');
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) => reportService.log('Nav didPush to ${_name(route)}');
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) => FirebaseCrashlytics.instance.log('Nav didPop to ${_name(previousRoute)}');
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) => reportService.log('Nav didPop to ${_name(previousRoute)}');
@override
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) => FirebaseCrashlytics.instance.log('Nav didRemove to ${_name(previousRoute)}');
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) => reportService.log('Nav didRemove to ${_name(previousRoute)}');
@override
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) => FirebaseCrashlytics.instance.log('Nav didReplace to ${_name(newRoute)}');
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) => reportService.log('Nav didReplace to ${_name(newRoute)}');
String _name(Route<dynamic>? route) => route?.settings.name ?? 'unnamed ${route?.runtimeType}';
}

View file

@ -7,8 +7,8 @@ import 'package:aves/widgets/debug/android_dirs.dart';
import 'package:aves/widgets/debug/android_env.dart';
import 'package:aves/widgets/debug/cache.dart';
import 'package:aves/widgets/debug/database.dart';
import 'package:aves/widgets/debug/firebase.dart';
import 'package:aves/widgets/debug/overlay.dart';
import 'package:aves/widgets/debug/report.dart';
import 'package:aves/widgets/debug/settings.dart';
import 'package:aves/widgets/debug/storage.dart';
import 'package:aves/widgets/viewer/info/common.dart';

View file

@ -1,34 +0,0 @@
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
import 'package:aves/widgets/viewer/info/common.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';
class DebugFirebaseSection extends StatelessWidget {
const DebugFirebaseSection({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return AvesExpansionTile(
title: 'Firebase',
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: ElevatedButton(
onPressed: FirebaseCrashlytics.instance.crash,
child: const Text('Crash'),
),
),
Padding(
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
child: InfoRowGroup(
info: {
'Firebase data collection enabled': '${Firebase.app().isAutomaticDataCollectionEnabled}',
'Crashlytics collection enabled': '${FirebaseCrashlytics.instance.isCrashlyticsCollectionEnabled}',
},
),
)
],
);
}
}

View file

@ -0,0 +1,63 @@
import 'package:aves/services/android_debug_service.dart';
import 'package:aves/services/services.dart';
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
import 'package:aves/widgets/viewer/info/common.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
class DebugFirebaseSection extends StatelessWidget {
const DebugFirebaseSection({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return AvesExpansionTile(
title: 'Reporting',
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: ElevatedButton(
onPressed: AndroidDebugService.crash,
child: Text('Crash'),
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: ElevatedButton(
onPressed: AndroidDebugService.exception,
child: Text('Throw exception'),
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: ElevatedButton(
onPressed: AndroidDebugService.safeException,
child: Text('Throw exception (safe)'),
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: ElevatedButton(
onPressed: AndroidDebugService.exceptionInCoroutine,
child: Text('Throw exception in coroutine'),
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: ElevatedButton(
onPressed: AndroidDebugService.safeExceptionInCoroutine,
child: Text('Throw exception in coroutine (safe)'),
),
),
Padding(
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
child: InfoRowGroup(
info: {
'Firebase data collection enabled': '${Firebase.app().isAutomaticDataCollectionEnabled}',
'Crashlytics collection enabled': '${reportService.isCollectionEnabled}',
},
),
)
],
);
}
}

View file

@ -17,7 +17,6 @@ import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/search/search_delegate.dart';
import 'package:aves/widgets/search/search_page.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart';
@ -106,7 +105,7 @@ class _HomePageState extends State<HomePage> {
}
}
context.read<ValueNotifier<AppMode>>().value = appMode;
unawaited(FirebaseCrashlytics.instance.setCustomKey('app_mode', appMode.toString()));
unawaited(reportService.setCustomKey('app_mode', appMode.toString()));
if (appMode != AppMode.view) {
final source = context.read<CollectionSource>();