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 val messenger = flutterEngine!!.dartExecutor.binaryMessenger
// dart -> platform -> dart
MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(AppAdapterHandler(this)) MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(AppAdapterHandler(this))
MethodChannel(messenger, AppShortcutHandler.CHANNEL).setMethodCallHandler(AppShortcutHandler(this)) MethodChannel(messenger, AppShortcutHandler.CHANNEL).setMethodCallHandler(AppShortcutHandler(this))
MethodChannel(messenger, DebugHandler.CHANNEL).setMethodCallHandler(DebugHandler(this)) MethodChannel(messenger, DebugHandler.CHANNEL).setMethodCallHandler(DebugHandler(this))
@ -61,12 +62,13 @@ class MainActivity : FlutterActivity() {
MethodChannel(messenger, TimeHandler.CHANNEL).setMethodCallHandler(TimeHandler()) MethodChannel(messenger, TimeHandler.CHANNEL).setMethodCallHandler(TimeHandler())
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(WindowHandler(this)) MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(WindowHandler(this))
// result streaming: dart -> platform ->->-> dart
StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(this, args) } StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(this, args) }
StreamsChannel(messenger, ImageOpStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageOpStreamHandler(this, args) } StreamsChannel(messenger, ImageOpStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageOpStreamHandler(this, args) }
StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(this, args) } StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(this, args) }
StreamsChannel(messenger, StorageAccessStreamHandler.CHANNEL).setStreamHandlerFactory { args -> StorageAccessStreamHandler(this, args) } StreamsChannel(messenger, StorageAccessStreamHandler.CHANNEL).setStreamHandlerFactory { args -> StorageAccessStreamHandler(this, args) }
// Media Store change monitoring // change monitoring: platform -> dart
mediaStoreChangeStreamHandler = MediaStoreChangeStreamHandler(this).apply { mediaStoreChangeStreamHandler = MediaStoreChangeStreamHandler(this).apply {
EventChannel(messenger, MediaStoreChangeStreamHandler.CHANNEL).setStreamHandler(this) EventChannel(messenger, MediaStoreChangeStreamHandler.CHANNEL).setStreamHandler(this)
} }
@ -75,9 +77,11 @@ class MainActivity : FlutterActivity() {
} }
// intent handling // intent handling
// notification: platform -> dart
intentStreamHandler = IntentStreamHandler().apply { intentStreamHandler = IntentStreamHandler().apply {
EventChannel(messenger, IntentStreamHandler.CHANNEL).setStreamHandler(this) EventChannel(messenger, IntentStreamHandler.CHANNEL).setStreamHandler(this)
} }
// detail fetch: dart -> platform
intentDataMap = extractIntentData(intent) intentDataMap = extractIntentData(intent)
MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler { call, result -> MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) { 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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
setupShortcuts() setupShortcuts()
} }
@ -243,6 +252,10 @@ class MainActivity : FlutterActivity() {
handler.onDenied() 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.load.DecodeFormat
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe 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.model.FieldMap
import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.LogUtils 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) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"getPackages" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getPackages) } "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) } "copyToClipboard" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::copyToClipboard) }
"edit" -> { "edit" -> safe(call, result, ::edit)
val title = call.argument<String>("title") "open" -> safe(call, result, ::open)
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } "openMap" -> safe(call, result, ::openMap)
val mimeType = call.argument<String>("mimeType") "setAs" -> safe(call, result, ::setAs)
result.success(edit(title, uri, mimeType)) "share" -> safe(call, result, ::share)
}
"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))
}
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
@ -173,77 +151,113 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
} }
} }
private fun edit(title: String?, uri: Uri?, mimeType: String?): Boolean { private fun edit(call: MethodCall, result: MethodChannel.Result) {
uri ?: return false 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) val intent = Intent(Intent.ACTION_EDIT)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
.setDataAndType(getShareableUri(uri), mimeType) .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 { private fun open(call: MethodCall, result: MethodChannel.Result) {
uri ?: return false 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) val intent = Intent(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.setDataAndType(getShareableUri(uri), mimeType) .setDataAndType(getShareableUri(uri), mimeType)
return safeStartActivityChooser(title, intent) val started = safeStartActivityChooser(title, intent)
result.success(started)
} }
private fun openMap(geoUri: Uri?): Boolean { private fun openMap(call: MethodCall, result: MethodChannel.Result) {
geoUri ?: return false 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) 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 { private fun setAs(call: MethodCall, result: MethodChannel.Result) {
uri ?: return false 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) val intent = Intent(Intent.ACTION_ATTACH_DATA)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.setDataAndType(getShareableUri(uri), mimeType) .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 { private fun share(call: MethodCall, result: MethodChannel.Result) {
val intent = Intent(Intent.ACTION_SEND) val title = call.argument<String>("title")
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) val urisByMimeType = call.argument<Map<String, List<String>>>("urisByMimeType")
.setType(mimeType) if (urisByMimeType == null) {
.putExtra(Intent.EXTRA_STREAM, getShareableUri(uri)) result.error("setAs-args", "failed because of missing arguments", null)
return safeStartActivityChooser(title, intent) return
} }
private fun shareMultiple(title: String?, urisByMimeType: Map<String, List<String>>?): Boolean {
urisByMimeType ?: return false
val uriList = ArrayList(urisByMimeType.values.flatten().mapNotNull { Uri.parse(it) }) val uriList = ArrayList(urisByMimeType.values.flatten().mapNotNull { Uri.parse(it) })
val mimeTypes = urisByMimeType.keys.toTypedArray() val mimeTypes = urisByMimeType.keys.toTypedArray()
// simplify share intent for a single item, as some apps can handle one item but not more // simplify share intent for a single item, as some apps can handle one item but not more
if (uriList.size == 1) { val started = if (uriList.size == 1) {
return shareSingle(title, uriList.first(), mimeTypes.first()) val uri = uriList.first()
} val mimeType = mimeTypes.first()
var mimeType = "*/*" val intent = Intent(Intent.ACTION_SEND)
if (mimeTypes.size == 1) { .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
// items have the same mime type & subtype .setType(mimeType)
mimeType = mimeTypes.first() .putExtra(Intent.EXTRA_STREAM, getShareableUri(uri))
safeStartActivityChooser(title, intent)
} else { } else {
// items have different subtypes var mimeType = "*/*"
val mimeTypeTypes = mimeTypes.map { it.split("/") }.distinct() if (mimeTypes.size == 1) {
if (mimeTypeTypes.size == 1) { // items have the same mime type & subtype
// items have the same mime type mimeType = mimeTypes.first()
mimeType = "${mimeTypeTypes.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) result.success(started)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList)
.setType(mimeType)
return safeStartActivityChooser(title, intent)
} }
private fun safeStartActivity(intent: Intent): Boolean { 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 androidx.core.graphics.drawable.IconCompat
import deckers.thibault.aves.MainActivity import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.R import deckers.thibault.aves.R
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.utils.BitmapUtils.centerSquareCrop import deckers.thibault.aves.utils.BitmapUtils.centerSquareCrop
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
@ -20,23 +21,31 @@ import java.util.*
class AppShortcutHandler(private val context: Context) : MethodCallHandler { class AppShortcutHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"canPin" -> result.success(canPin()) "canPin" -> safe(call, result, ::canPin)
"pin" -> { "pin" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::pin) }
GlobalScope.launch(Dispatchers.IO) { pin(call) }
result.success(null)
}
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
private fun canPin() = ShortcutManagerCompat.isRequestPinShortcutSupported(context) private fun isSupported() = ShortcutManagerCompat.isRequestPinShortcutSupported(context)
private fun pin(call: MethodCall) { private fun canPin(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
if (!canPin()) return 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 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 var icon: IconCompat? = null
if (iconBytes?.isNotEmpty() == true) { if (iconBytes?.isNotEmpty() == true) {
@ -62,6 +71,8 @@ class AppShortcutHandler(private val context: Context) : MethodCallHandler {
.setIntent(intent) .setIntent(intent)
.build() .build()
ShortcutManagerCompat.requestPinShortcut(context, shortcut, null) ShortcutManagerCompat.requestPinShortcut(context, shortcut, null)
result.success(true)
} }
companion object { companion object {

View file

@ -1,5 +1,6 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import deckers.thibault.aves.MainActivity
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -8,24 +9,42 @@ import kotlinx.coroutines.launch
import kotlin.reflect.KSuspendFunction2 import kotlin.reflect.KSuspendFunction2
// ensure `result` methods are called on the main looper thread // 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) private val mainScope = CoroutineScope(Dispatchers.Main)
override fun success(result: Any?) { 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?) { 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() { 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 { companion object {
fun safe(call: MethodCall, result: MethodChannel.Result, function: (call: MethodCall, result: MethodChannel.Result) -> Unit) { fun safe(call: MethodCall, result: MethodChannel.Result, function: (call: MethodCall, result: MethodChannel.Result) -> Unit) {
val res = Coresult(result) val res = Coresult(call, result)
try { try {
function(call, res) function(call, res)
} catch (e: Exception) { } 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>) { suspend fun safeSuspend(call: MethodCall, result: MethodChannel.Result, function: KSuspendFunction2<MethodCall, MethodChannel.Result, Unit>) {
val res = Coresult(result) val res = Coresult(call, result)
try { try {
function(call, res) function(call, res)
} catch (e: Exception) { } 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.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Handler
import android.os.Looper
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import android.util.Log
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
@ -36,8 +38,15 @@ import java.util.*
class DebugHandler(private val context: Context) : MethodCallHandler { class DebugHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { 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) } "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) } "getBitmapFactoryInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getBitmapFactoryInfo) }
"getContentResolverMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getContentResolverMetadata) } "getContentResolverMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getContentResolverMetadata) }
"getExifInterfaceMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getExifInterfaceMetadata) } "getExifInterfaceMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getExifInterfaceMetadata) }
@ -71,6 +80,10 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
result.success(dirs) 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) { private fun getBitmapFactoryInfo(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
if (uri == null) { if (uri == null) {
@ -320,4 +333,6 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
private val LOG_TAG = LogUtils.createTag<DebugHandler>() private val LOG_TAG = LogUtils.createTag<DebugHandler>()
const val CHANNEL = "deckers.thibault/aves/debug" 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 package deckers.thibault.aves.channel.calls
import android.os.Build import android.os.Build
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
@ -8,17 +9,18 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler
class DeviceHandler : MethodCallHandler { class DeviceHandler : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"getPerformanceClass" -> result.success(getPerformanceClass()) "getPerformanceClass" -> safe(call, result, ::getPerformanceClass)
else -> result.notImplemented() 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 // TODO TLAD uncomment when the future is here
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // 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 { companion object {

View file

@ -12,7 +12,7 @@ import com.drew.imaging.ImageMetadataReader
import com.drew.metadata.file.FileTypeDirectory import com.drew.metadata.file.FileTypeDirectory
import com.drew.metadata.xmp.XmpDirectory import com.drew.metadata.xmp.XmpDirectory
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe 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.Metadata
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString
import deckers.thibault.aves.metadata.MultiPage import deckers.thibault.aves.metadata.MultiPage
@ -44,7 +44,7 @@ import java.util.*
class EmbeddedDataHandler(private val context: Context) : MethodCallHandler { class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { 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) } "extractMotionPhotoVideo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractMotionPhotoVideo) }
"extractVideoEmbeddedPicture" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractVideoEmbeddedPicture) } "extractVideoEmbeddedPicture" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractVideoEmbeddedPicture) }
"extractXmpDataProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractXmpDataProp) } "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.content.Context
import android.location.Geocoder import android.location.Geocoder
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler 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) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { 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() else -> result.notImplemented()
} }
} }

View file

@ -5,7 +5,7 @@ import android.graphics.Rect
import android.net.Uri import android.net.Uri
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe 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.RegionFetcher
import deckers.thibault.aves.channel.calls.fetchers.SvgRegionFetcher import deckers.thibault.aves.channel.calls.fetchers.SvgRegionFetcher
import deckers.thibault.aves.channel.calls.fetchers.ThumbnailFetcher 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) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"getEntry" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getEntry) } "getEntry" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getEntry) }
"getThumbnail" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getThumbnail) } "getThumbnail" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::getThumbnail) }
"getRegion" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getRegion) } "getRegion" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::getRegion) }
"captureFrame" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::captureFrame) } "captureFrame" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::captureFrame) }
"rename" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::rename) } "rename" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::rename) }
"rotate" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::rotate) } "rotate" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::rotate) }
"flip" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::flip) } "flip" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::flip) }
"clearSizedThumbnailDiskCache" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::clearSizedThumbnailDiskCache) } "clearSizedThumbnailDiskCache" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::clearSizedThumbnailDiskCache) }

View file

@ -1,5 +1,6 @@
package deckers.thibault.aves.channel.calls 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.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
@ -8,11 +9,15 @@ import java.util.*
class TimeHandler : MethodCallHandler { class TimeHandler : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"getDefaultTimeZone" -> result.success(TimeZone.getDefault().id) "getDefaultTimeZone" -> safe(call, result, ::getDefaultTimeZone)
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
private fun getDefaultTimeZone(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
result.success(TimeZone.getDefault().id)
}
companion object { companion object {
const val CHANNEL = "deckers.thibault/aves/time" 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) "keepScreenOn" -> safe(call, result, ::keepScreenOn)
"isRotationLocked" -> safe(call, result, ::isRotationLocked) "isRotationLocked" -> safe(call, result, ::isRotationLocked)
"requestOrientation" -> safe(call, result, ::requestOrientation) "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) "setCutoutMode" -> safe(call, result, ::setCutoutMode)
else -> result.notImplemented() else -> result.notImplemented()
} }
@ -60,6 +60,10 @@ class WindowHandler(private val activity: Activity) : MethodCallHandler {
result.success(true) 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) { private fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) {
val use = call.argument<Boolean>("use") val use = call.argument<Boolean>("use")
if (use == null) { 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 'dart:isolate';
import 'package:aves/services/services.dart';
import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/aves_app.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
void main() { void main() {
@ -20,12 +20,11 @@ void main() {
// //
// flutter run --profile --trace-skia // flutter run --profile --trace-skia
Isolate.current.addErrorListener(RawReceivePort((pair) async { initPlatformServices();
Isolate.current.addErrorListener(RawReceivePort((pair) {
final List<dynamic> errorAndStacktrace = pair; final List<dynamic> errorAndStacktrace = pair;
await FirebaseCrashlytics.instance.recordError( reportService.recordError(errorAndStacktrace.first, errorAndStacktrace.last);
errorAndStacktrace.first,
errorAndStacktrace.last,
);
}).sendPort); }).sendPort);
runApp(const AvesApp()); runApp(const AvesApp());

View file

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

View file

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

View file

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

View file

@ -24,7 +24,7 @@ class AppShortcutService {
return result; return result;
} }
} on PlatformException catch (e) { } on PlatformException catch (e) {
debugPrint('canPin failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); await reportService.recordChannelError('canPin', e);
} }
return false; return false;
} }
@ -50,7 +50,7 @@ class AppShortcutService {
'filters': filters.map((filter) => filter.toJson()).toList(), 'filters': filters.map((filter) => filter.toJson()).toList(),
}); });
} on PlatformException catch (e) { } 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/services.dart';
import 'package:flutter/widgets.dart';
class DeviceService { class DeviceService {
static const platform = MethodChannel('deckers.thibault/aves/device'); static const platform = MethodChannel('deckers.thibault/aves/device');
@ -11,7 +10,7 @@ class DeviceService {
final result = await platform.invokeMethod('getPerformanceClass'); final result = await platform.invokeMethod('getPerformanceClass');
if (result != null) return result as int; if (result != null) return result as int;
} on PlatformException catch (e) { } on PlatformException catch (e) {
debugPrint('getPerformanceClass failed with code=${e.code}, exception=${e.message}, details=${e.details}'); await reportService.recordChannelError('getPerformanceClass', e);
} }
return 0; return 0;
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/services/services.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:streams_channel/streams_channel.dart'; import 'package:streams_channel/streams_channel.dart';
abstract class MediaStoreService { abstract class MediaStoreService {
@ -26,7 +26,7 @@ class PlatformMediaStoreService implements MediaStoreService {
}); });
return (result as List).cast<int>(); return (result as List).cast<int>();
} on PlatformException catch (e) { } on PlatformException catch (e) {
debugPrint('checkObsoleteContentIds failed with code=${e.code}, exception=${e.message}, details=${e.details}'); await reportService.recordChannelError('checkObsoleteContentIds', e);
} }
return []; return [];
} }
@ -39,7 +39,7 @@ class PlatformMediaStoreService implements MediaStoreService {
}); });
return (result as List).cast<int>(); return (result as List).cast<int>();
} on PlatformException catch (e) { } on PlatformException catch (e) {
debugPrint('checkObsoletePaths failed with code=${e.code}, exception=${e.message}, details=${e.details}'); await reportService.recordChannelError('checkObsoletePaths', e);
} }
return []; return [];
} }
@ -51,7 +51,7 @@ class PlatformMediaStoreService implements MediaStoreService {
'knownEntries': knownEntries, 'knownEntries': knownEntries,
}).map((event) => AvesEntry.fromMap(event)); }).map((event) => AvesEntry.fromMap(event));
} on PlatformException catch (e) { } 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); 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/multipage.dart';
import 'package:aves/model/panorama.dart'; import 'package:aves/model/panorama.dart';
import 'package:aves/services/service_policy.dart'; import 'package:aves/services/service_policy.dart';
import 'package:flutter/foundation.dart'; import 'package:aves/services/services.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
abstract class MetadataService { abstract class MetadataService {
@ -36,7 +36,7 @@ class PlatformMetadataService implements MetadataService {
}); });
if (result != null) return result as Map; if (result != null) return result as Map;
} on PlatformException catch (e) { } on PlatformException catch (e) {
debugPrint('getAllMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}'); await reportService.recordChannelError('getAllMetadata', e);
} }
return {}; return {};
} }
@ -66,7 +66,7 @@ class PlatformMetadataService implements MetadataService {
result['contentId'] = entry.contentId; result['contentId'] = entry.contentId;
return CatalogMetadata.fromMap(result); return CatalogMetadata.fromMap(result);
} on PlatformException catch (e) { } on PlatformException catch (e) {
debugPrint('getCatalogMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}'); await reportService.recordChannelError('getCatalogMetadata', e);
} }
return null; return null;
} }
@ -92,7 +92,7 @@ class PlatformMetadataService implements MetadataService {
}) as Map; }) as Map;
return OverlayMetadata.fromMap(result); return OverlayMetadata.fromMap(result);
} on PlatformException catch (e) { } on PlatformException catch (e) {
debugPrint('getOverlayMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}'); await reportService.recordChannelError('getOverlayMetadata', e);
} }
return null; return null;
} }
@ -114,7 +114,7 @@ class PlatformMetadataService implements MetadataService {
} }
return MultiPageInfo.fromPageMaps(entry, pageMaps); return MultiPageInfo.fromPageMaps(entry, pageMaps);
} on PlatformException catch (e) { } on PlatformException catch (e) {
debugPrint('getMultiPageInfo failed with code=${e.code}, exception=${e.message}, details=${e.details}'); await reportService.recordChannelError('getMultiPageInfo', e);
} }
return null; return null;
} }
@ -132,7 +132,7 @@ class PlatformMetadataService implements MetadataService {
}) as Map; }) as Map;
return PanoramaInfo.fromMap(result); return PanoramaInfo.fromMap(result);
} on PlatformException catch (e) { } on PlatformException catch (e) {
debugPrint('PanoramaInfo failed with code=${e.code}, exception=${e.message}, details=${e.details}'); await reportService.recordChannelError('PanoramaInfo', e);
} }
return null; return null;
} }
@ -146,7 +146,7 @@ class PlatformMetadataService implements MetadataService {
'prop': prop, 'prop': prop,
}); });
} on PlatformException catch (e) { } on PlatformException catch (e) {
debugPrint('getContentResolverProp failed with code=${e.code}, exception=${e.message}, details=${e.details}'); await reportService.recordChannelError('getContentResolverProp', e);
} }
return null; 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/image_file_service.dart';
import 'package:aves/services/media_store_service.dart'; import 'package:aves/services/media_store_service.dart';
import 'package:aves/services/metadata_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/storage_service.dart';
import 'package:aves/services/time_service.dart'; import 'package:aves/services/time_service.dart';
import 'package:aves/services/window_service.dart'; import 'package:aves/services/window_service.dart';
@ -20,6 +21,7 @@ final EmbeddedDataService embeddedDataService = getIt<EmbeddedDataService>();
final ImageFileService imageFileService = getIt<ImageFileService>(); final ImageFileService imageFileService = getIt<ImageFileService>();
final MediaStoreService mediaStoreService = getIt<MediaStoreService>(); final MediaStoreService mediaStoreService = getIt<MediaStoreService>();
final MetadataService metadataService = getIt<MetadataService>(); final MetadataService metadataService = getIt<MetadataService>();
final ReportService reportService = getIt<ReportService>();
final StorageService storageService = getIt<StorageService>(); final StorageService storageService = getIt<StorageService>();
final TimeService timeService = getIt<TimeService>(); final TimeService timeService = getIt<TimeService>();
final WindowService windowService = getIt<WindowService>(); final WindowService windowService = getIt<WindowService>();
@ -33,6 +35,7 @@ void initPlatformServices() {
getIt.registerLazySingleton<ImageFileService>(() => PlatformImageFileService()); getIt.registerLazySingleton<ImageFileService>(() => PlatformImageFileService());
getIt.registerLazySingleton<MediaStoreService>(() => PlatformMediaStoreService()); getIt.registerLazySingleton<MediaStoreService>(() => PlatformMediaStoreService());
getIt.registerLazySingleton<MetadataService>(() => PlatformMetadataService()); getIt.registerLazySingleton<MetadataService>(() => PlatformMetadataService());
getIt.registerLazySingleton<ReportService>(() => CrashlyticsReportService());
getIt.registerLazySingleton<StorageService>(() => PlatformStorageService()); getIt.registerLazySingleton<StorageService>(() => PlatformStorageService());
getIt.registerLazySingleton<TimeService>(() => PlatformTimeService()); getIt.registerLazySingleton<TimeService>(() => PlatformTimeService());
getIt.registerLazySingleton<WindowService>(() => PlatformWindowService()); getIt.registerLazySingleton<WindowService>(() => PlatformWindowService());

View file

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

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/services.dart';
abstract class TimeService { abstract class TimeService {
@ -13,7 +13,7 @@ class PlatformTimeService implements TimeService {
try { try {
return await platform.invokeMethod('getDefaultTimeZone'); return await platform.invokeMethod('getDefaultTimeZone');
} on PlatformException catch (e) { } on PlatformException catch (e) {
debugPrint('getDefaultTimeZone failed with code=${e.code}, exception=${e.message}, details=${e.details}'); await reportService.recordChannelError('getDefaultTimeZone', e);
} }
return null; return null;
} }

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/services.dart';
class ViewerService { class ViewerService {
@ -10,7 +10,7 @@ class ViewerService {
final result = await platform.invokeMethod('getIntentData'); final result = await platform.invokeMethod('getIntentData');
if (result != null) return (result as Map).cast<String, dynamic>(); if (result != null) return (result as Map).cast<String, dynamic>();
} on PlatformException catch (e) { } on PlatformException catch (e) {
debugPrint('getIntentData failed with code=${e.code}, exception=${e.message}, details=${e.details}'); await reportService.recordChannelError('getIntentData', e);
} }
return {}; return {};
} }
@ -21,7 +21,7 @@ class ViewerService {
'uri': uri, 'uri': uri,
}); });
} on PlatformException catch (e) { } 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/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -24,7 +24,7 @@ class PlatformWindowService implements WindowService {
'on': on, 'on': on,
}); });
} on PlatformException catch (e) { } 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'); final result = await platform.invokeMethod('isRotationLocked');
if (result != null) return result as bool; if (result != null) return result as bool;
} on PlatformException catch (e) { } on PlatformException catch (e) {
debugPrint('isRotationLocked failed with code=${e.code}, exception=${e.message}, details=${e.details}'); await reportService.recordChannelError('isRotationLocked', e);
} }
return false; return false;
} }
@ -62,7 +62,7 @@ class PlatformWindowService implements WindowService {
'orientation': orientationCode, 'orientation': orientationCode,
}); });
} on PlatformException catch (e) { } 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'); final result = await platform.invokeMethod('canSetCutoutMode');
if (result != null) return result as bool; if (result != null) return result as bool;
} on PlatformException catch (e) { } on PlatformException catch (e) {
debugPrint('canSetCutoutMode failed with code=${e.code}, exception=${e.message}, details=${e.details}'); await reportService.recordChannelError('canSetCutoutMode', e);
} }
return false; return false;
} }
@ -84,7 +84,7 @@ class PlatformWindowService implements WindowService {
'use': use, 'use': use,
}); });
} on PlatformException catch (e) { } 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:aves/widgets/welcome_page.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -44,6 +43,7 @@ class _AvesAppState extends State<AvesApp> {
List<NavigatorObserver> _navigatorObservers = []; List<NavigatorObserver> _navigatorObservers = [];
final EventChannel _mediaStoreChangeChannel = const EventChannel('deckers.thibault/aves/mediastorechange'); final EventChannel _mediaStoreChangeChannel = const EventChannel('deckers.thibault/aves/mediastorechange');
final EventChannel _newIntentChannel = const EventChannel('deckers.thibault/aves/intent'); 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'); final GlobalKey<NavigatorState> _navigatorKey = GlobalKey(debugLabel: 'app-navigator');
Widget getFirstPage({Map? intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : const WelcomePage(); Widget getFirstPage({Map? intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : const WelcomePage();
@ -52,10 +52,10 @@ class _AvesAppState extends State<AvesApp> {
void initState() { void initState() {
super.initState(); super.initState();
EquatableConfig.stringify = true; EquatableConfig.stringify = true;
initPlatformServices();
_appSetup = _setup(); _appSetup = _setup();
_mediaStoreChangeChannel.receiveBroadcastStream().listen((event) => _onMediaStoreChange(event as String?)); _mediaStoreChangeChannel.receiveBroadcastStream().listen((event) => _onMediaStoreChange(event as String?));
_newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map?)); _newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map?));
_errorChannel.receiveBroadcastStream().listen((event) => _onError(event as String?));
} }
@override @override
@ -124,18 +124,17 @@ class _AvesAppState extends State<AvesApp> {
Future<void> _setup() async { Future<void> _setup() async {
await Firebase.initializeApp().then((app) { await Firebase.initializeApp().then((app) {
final crashlytics = FirebaseCrashlytics.instance; FlutterError.onError = reportService.recordFlutterError;
FlutterError.onError = crashlytics.recordFlutterError;
crashlytics.setCustomKey('locales', window.locales.join(', '));
final now = DateTime.now(); final now = DateTime.now();
crashlytics.setCustomKey('timezone', '${now.timeZoneName} (${now.timeZoneOffset})'); reportService.setCustomKeys({
crashlytics.setCustomKey( 'locales': window.locales.join(', '),
'build_mode', 'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})',
kReleaseMode 'build_mode': kReleaseMode
? 'release' ? 'release'
: kProfileMode : kProfileMode
? 'profile' ? 'profile'
: 'debug'); : 'debug',
});
}); });
await settings.init(); await settings.init();
await settings.initFirebase(); await settings.initFirebase();
@ -150,7 +149,7 @@ class _AvesAppState extends State<AvesApp> {
// do not reset when relaunching the app // do not reset when relaunching the app
if (appModeNotifier.value == AppMode.main && (intentData == null || intentData.isEmpty == true)) return; if (appModeNotifier.value == AppMode.main && (intentData == null || intentData.isEmpty == true)) return;
FirebaseCrashlytics.instance.log('New intent'); reportService.log('New intent');
_navigatorKey.currentState!.pushReplacement(DirectMaterialPageRoute( _navigatorKey.currentState!.pushReplacement(DirectMaterialPageRoute(
settings: const RouteSettings(name: HomePage.routeName), settings: const RouteSettings(name: HomePage.routeName),
builder: (_) => getFirstPage(intentData: intentData), 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'; import 'package:flutter/material.dart';
class CrashlyticsRouteTracker extends NavigatorObserver { class CrashlyticsRouteTracker extends NavigatorObserver {
@override @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 @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 @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 @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}'; 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/android_env.dart';
import 'package:aves/widgets/debug/cache.dart'; import 'package:aves/widgets/debug/cache.dart';
import 'package:aves/widgets/debug/database.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/overlay.dart';
import 'package:aves/widgets/debug/report.dart';
import 'package:aves/widgets/debug/settings.dart'; import 'package:aves/widgets/debug/settings.dart';
import 'package:aves/widgets/debug/storage.dart'; import 'package:aves/widgets/debug/storage.dart';
import 'package:aves/widgets/viewer/info/common.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_delegate.dart';
import 'package:aves/widgets/search/search_page.dart'; import 'package:aves/widgets/search/search_page.dart';
import 'package:aves/widgets/viewer/entry_viewer_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/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
@ -106,7 +105,7 @@ class _HomePageState extends State<HomePage> {
} }
} }
context.read<ValueNotifier<AppMode>>().value = appMode; 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) { if (appMode != AppMode.view) {
final source = context.read<CollectionSource>(); final source = context.read<CollectionSource>();