From 2a82aef354391db5c3bb643df9a695af7542c2e0 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 2 Aug 2021 19:15:03 +0900 Subject: [PATCH] channel multiple result crashfix, channel error reporting, crashlytics abstraction --- .../deckers/thibault/aves/MainActivity.kt | 15 +- .../aves/channel/calls/AppAdapterHandler.kt | 150 ++++++++++-------- .../aves/channel/calls/AppShortcutHandler.kt | 31 ++-- .../thibault/aves/channel/calls/Coresult.kt | 35 +++- .../aves/channel/calls/DebugHandler.kt | 17 +- .../aves/channel/calls/DeviceHandler.kt | 10 +- .../aves/channel/calls/EmbeddedDataHandler.kt | 4 +- .../aves/channel/calls/GeocodingHandler.kt | 3 +- .../aves/channel/calls/ImageFileHandler.kt | 10 +- .../aves/channel/calls/TimeHandler.kt | 7 +- .../aves/channel/calls/WindowHandler.kt | 6 +- .../channel/streams/ErrorStreamHandler.kt | 25 +++ lib/main.dart | 11 +- lib/model/settings/settings.dart | 3 +- lib/services/android_app_service.dart | 20 +-- lib/services/android_debug_service.dart | 58 +++++-- lib/services/app_shortcut_service.dart | 4 +- lib/services/device_service.dart | 5 +- lib/services/embedded_data_service.dart | 10 +- lib/services/geocoding_service.dart | 4 +- lib/services/global_search.dart | 3 +- lib/services/image_file_service.dart | 27 ++-- lib/services/image_op_events.dart | 1 - lib/services/media_store_service.dart | 8 +- lib/services/metadata_service.dart | 14 +- lib/services/report_service.dart | 55 +++++++ lib/services/services.dart | 3 + lib/services/storage_service.dart | 29 ++-- lib/services/time_service.dart | 4 +- lib/services/viewer_service.dart | 6 +- lib/services/window_service.dart | 12 +- lib/widgets/aves_app.dart | 29 ++-- .../common/behaviour/route_tracker.dart | 10 +- lib/widgets/debug/app_debug_page.dart | 2 +- lib/widgets/debug/firebase.dart | 34 ---- lib/widgets/debug/report.dart | 63 ++++++++ lib/widgets/home_page.dart | 3 +- 37 files changed, 482 insertions(+), 249 deletions(-) create mode 100644 android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ErrorStreamHandler.kt create mode 100644 lib/services/report_service.dart delete mode 100644 lib/widgets/debug/firebase.dart create mode 100644 lib/widgets/debug/report.dart diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt index c21c51308..773f2ef19 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt @@ -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) } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt index 69e390997..04afd10b8 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt @@ -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("title") - val uri = call.argument("uri")?.let { Uri.parse(it) } - val mimeType = call.argument("mimeType") - result.success(edit(title, uri, mimeType)) - } - "open" -> { - val title = call.argument("title") - val uri = call.argument("uri")?.let { Uri.parse(it) } - val mimeType = call.argument("mimeType") - result.success(open(title, uri, mimeType)) - } - "openMap" -> { - val geoUri = call.argument("geoUri")?.let { Uri.parse(it) } - result.success(openMap(geoUri)) - } - "setAs" -> { - val title = call.argument("title") - val uri = call.argument("uri")?.let { Uri.parse(it) } - val mimeType = call.argument("mimeType") - result.success(setAs(title, uri, mimeType)) - } - "share" -> { - val title = call.argument("title") - val urisByMimeType = call.argument>>("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("title") + val uri = call.argument("uri")?.let { Uri.parse(it) } + val mimeType = call.argument("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("title") + val uri = call.argument("uri")?.let { Uri.parse(it) } + val mimeType = call.argument("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("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("title") + val uri = call.argument("uri")?.let { Uri.parse(it) } + val mimeType = call.argument("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>?): Boolean { - urisByMimeType ?: return false + private fun share(call: MethodCall, result: MethodChannel.Result) { + val title = call.argument("title") + val urisByMimeType = call.argument>>("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 { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppShortcutHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppShortcutHandler.kt index 684edfc12..24b81b739 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppShortcutHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppShortcutHandler.kt @@ -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("label") ?: return + private fun pin(call: MethodCall, result: MethodChannel.Result) { + val label = call.argument("label") val iconBytes = call.argument("iconBytes") - val filters = call.argument>("filters") ?: return + val filters = call.argument>("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 { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/Coresult.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/Coresult.kt index 5207bde7d..6e2f770c5 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/Coresult.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/Coresult.kt @@ -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) { - val res = Coresult(result) + suspend fun safeSuspend(call: MethodCall, result: MethodChannel.Result, function: KSuspendFunction2) { + 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()) } } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt index 1d5a9fd8e..70292b9ea 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt @@ -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("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() const val CHANNEL = "deckers.thibault/aves/debug" } + + class TestException internal constructor() : RuntimeException("oops") } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt index 287a0b87b..a99906901 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt @@ -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 { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt index ad877204b..9bc84c83f 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt @@ -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) } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GeocodingHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GeocodingHandler.kt index 874c32d97..17ced0c0b 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GeocodingHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GeocodingHandler.kt @@ -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() } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt index ac092169f..8b35b4d9a 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt @@ -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) } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/TimeHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/TimeHandler.kt index 0f09aff8f..6d4736dc6 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/TimeHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/TimeHandler.kt @@ -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" } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/WindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/WindowHandler.kt index c1b1cda0c..bad2bf6eb 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/WindowHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/WindowHandler.kt @@ -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("use") if (use == null) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ErrorStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ErrorStreamHandler.kt new file mode 100644 index 000000000..b5eab9563 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ErrorStreamHandler.kt @@ -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" + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 5758785c4..e1abba8bd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 errorAndStacktrace = pair; - await FirebaseCrashlytics.instance.recordError( - errorAndStacktrace.first, - errorAndStacktrace.last, - ); + reportService.recordError(errorAndStacktrace.first, errorAndStacktrace.last); }).sendPort); runApp(const AvesApp()); diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 7fea6d05b..229d292b5 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -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 initFirebase() async { await Firebase.app().setAutomaticDataCollectionEnabled(isCrashlyticsEnabled); - await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(isCrashlyticsEnabled); + await reportService.setCollectionEnabled(isCrashlyticsEnabled); } Future reset({required bool includeInternalKeys}) async { diff --git a/lib/services/android_app_service.dart b/lib/services/android_app_service.dart index 17c89058e..63b13c0c1 100644 --- a/lib/services/android_app_service.dart +++ b/lib/services/android_app_service.dart @@ -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; } diff --git a/lib/services/android_debug_service.dart b/lib/services/android_debug_service.dart index 257e79be2..ae063ac12 100644 --- a/lib/services/android_debug_service.dart +++ b/lib/services/android_debug_service.dart @@ -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 crash() async { + try { + await platform.invokeMethod('crash'); + } on PlatformException catch (e) { + await reportService.recordChannelError('crash', e); + } + } + + static Future exception() async { + try { + await platform.invokeMethod('exception'); + } on PlatformException catch (e) { + await reportService.recordChannelError('exception', e); + } + } + + static Future safeException() async { + try { + await platform.invokeMethod('safeException'); + } on PlatformException catch (e) { + await reportService.recordChannelError('safeException', e); + } + } + + static Future exceptionInCoroutine() async { + try { + await platform.invokeMethod('exceptionInCoroutine'); + } on PlatformException catch (e) { + await reportService.recordChannelError('exceptionInCoroutine', e); + } + } + + static Future safeExceptionInCoroutine() async { + try { + await platform.invokeMethod('safeExceptionInCoroutine'); + } on PlatformException catch (e) { + await reportService.recordChannelError('safeExceptionInCoroutine', e); + } + } + static Future 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 {}; } diff --git a/lib/services/app_shortcut_service.dart b/lib/services/app_shortcut_service.dart index 4ef9c5323..90fb1a102 100644 --- a/lib/services/app_shortcut_service.dart +++ b/lib/services/app_shortcut_service.dart @@ -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); } } } diff --git a/lib/services/device_service.dart b/lib/services/device_service.dart index 84a3fe8f1..82ba698f6 100644 --- a/lib/services/device_service.dart +++ b/lib/services/device_service.dart @@ -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; } diff --git a/lib/services/embedded_data_service.dart b/lib/services/embedded_data_service.dart index 50cd15adf..4a2276dfb 100644 --- a/lib/services/embedded_data_service.dart +++ b/lib/services/embedded_data_service.dart @@ -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(); } 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 {}; } diff --git a/lib/services/geocoding_service.dart b/lib/services/geocoding_service.dart index c87baddf2..e993e64a5 100644 --- a/lib/services/geocoding_service.dart +++ b/lib/services/geocoding_service.dart @@ -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) => 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 []; } diff --git a/lib/services/global_search.dart b/lib/services/global_search.dart index 2f1023314..055af0e5c 100644 --- a/lib/services/global_search.dart +++ b/lib/services/global_search.dart @@ -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); } } } diff --git a/lib/services/image_file_service.dart b/lib/services/image_file_service.dart index b5461c602..870f42468 100644 --- a/lib/services/image_file_service.dart +++ b/lib/services/image_file_service.dart @@ -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(); } 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(); } 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(); } 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(); } on PlatformException catch (e) { - debugPrint('flip failed with code=${e.code}, exception=${e.message}, details=${e.details}'); + await reportService.recordChannelError('flip', e); } return {}; } diff --git a/lib/services/image_op_events.dart b/lib/services/image_op_events.dart index 52bcde5a6..e65f8f8da 100644 --- a/lib/services/image_op_events.dart +++ b/lib/services/image_op_events.dart @@ -1,6 +1,5 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; @immutable class ImageOpEvent extends Equatable { diff --git a/lib/services/media_store_service.dart b/lib/services/media_store_service.dart index acfe6b35d..32f3acf4f 100644 --- a/lib/services/media_store_service.dart +++ b/lib/services/media_store_service.dart @@ -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(); } 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(); } 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); } } diff --git a/lib/services/metadata_service.dart b/lib/services/metadata_service.dart index e42fdbf64..258e34676 100644 --- a/lib/services/metadata_service.dart +++ b/lib/services/metadata_service.dart @@ -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; } diff --git a/lib/services/report_service.dart b/lib/services/report_service.dart new file mode 100644 index 000000000..feab952a2 --- /dev/null +++ b/lib/services/report_service.dart @@ -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 setCollectionEnabled(bool enabled); + + Future log(String message); + + Future setCustomKey(String key, Object value); + + Future setCustomKeys(Map map); + + Future recordError(dynamic exception, StackTrace? stack); + + Future recordFlutterError(FlutterErrorDetails flutterErrorDetails); + + Future 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 setCollectionEnabled(bool enabled) => instance.setCrashlyticsCollectionEnabled(enabled); + + @override + Future log(String message) => instance.log(message); + + @override + Future setCustomKey(String key, Object value) => instance.setCustomKey(key, value); + + @override + Future setCustomKeys(Map map) { + final _instance = instance; + return Future.forEach>(map.entries, (kv) => _instance.setCustomKey(kv.key, kv.value)); + } + + @override + Future recordError(dynamic exception, StackTrace? stack) { + return instance.recordError(exception, stack); + } + + @override + Future recordFlutterError(FlutterErrorDetails flutterErrorDetails) { + return instance.recordFlutterError(flutterErrorDetails); + } +} diff --git a/lib/services/services.dart b/lib/services/services.dart index 04c0feebf..52fbd7b2d 100644 --- a/lib/services/services.dart +++ b/lib/services/services.dart @@ -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(); final ImageFileService imageFileService = getIt(); final MediaStoreService mediaStoreService = getIt(); final MetadataService metadataService = getIt(); +final ReportService reportService = getIt(); final StorageService storageService = getIt(); final TimeService timeService = getIt(); final WindowService windowService = getIt(); @@ -33,6 +35,7 @@ void initPlatformServices() { getIt.registerLazySingleton(() => PlatformImageFileService()); getIt.registerLazySingleton(() => PlatformMediaStoreService()); getIt.registerLazySingleton(() => PlatformMetadataService()); + getIt.registerLazySingleton(() => CrashlyticsReportService()); getIt.registerLazySingleton(() => PlatformStorageService()); getIt.registerLazySingleton(() => PlatformTimeService()); getIt.registerLazySingleton(() => PlatformWindowService()); diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index 8e1574ccc..53d460f5f 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -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) => 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(); } 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(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(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 createFile(String name, String mimeType, Uint8List bytes) async { try { - final completer = Completer(); + final completer = Completer(); storageAccessChannel.receiveBroadcastStream({ '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 selectDirectory() async { try { - final completer = Completer(); + final completer = Completer(); storageAccessChannel.receiveBroadcastStream({ '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; } diff --git a/lib/services/time_service.dart b/lib/services/time_service.dart index af9b90613..bc0ea606c 100644 --- a/lib/services/time_service.dart +++ b/lib/services/time_service.dart @@ -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; } diff --git a/lib/services/viewer_service.dart b/lib/services/viewer_service.dart index 336ebd99b..0a3bfa089 100644 --- a/lib/services/viewer_service.dart +++ b/lib/services/viewer_service.dart @@ -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(); } 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); } } } diff --git a/lib/services/window_service.dart b/lib/services/window_service.dart index 6f461bcd2..cf204de4b 100644 --- a/lib/services/window_service.dart +++ b/lib/services/window_service.dart @@ -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); } } } diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index 6f2c93fea..6a142abe3 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -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 { List _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 _navigatorKey = GlobalKey(debugLabel: 'app-navigator'); Widget getFirstPage({Map? intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : const WelcomePage(); @@ -52,10 +52,10 @@ class _AvesAppState extends State { 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 { Future _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 { // 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 { }); } } + + void _onError(String? error) => reportService.recordError(error, null); } diff --git a/lib/widgets/common/behaviour/route_tracker.dart b/lib/widgets/common/behaviour/route_tracker.dart index c0d577919..2e3d53fd0 100644 --- a/lib/widgets/common/behaviour/route_tracker.dart +++ b/lib/widgets/common/behaviour/route_tracker.dart @@ -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 route, Route? previousRoute) => FirebaseCrashlytics.instance.log('Nav didPush to ${_name(route)}'); + void didPush(Route route, Route? previousRoute) => reportService.log('Nav didPush to ${_name(route)}'); @override - void didPop(Route route, Route? previousRoute) => FirebaseCrashlytics.instance.log('Nav didPop to ${_name(previousRoute)}'); + void didPop(Route route, Route? previousRoute) => reportService.log('Nav didPop to ${_name(previousRoute)}'); @override - void didRemove(Route route, Route? previousRoute) => FirebaseCrashlytics.instance.log('Nav didRemove to ${_name(previousRoute)}'); + void didRemove(Route route, Route? previousRoute) => reportService.log('Nav didRemove to ${_name(previousRoute)}'); @override - void didReplace({Route? newRoute, Route? oldRoute}) => FirebaseCrashlytics.instance.log('Nav didReplace to ${_name(newRoute)}'); + void didReplace({Route? newRoute, Route? oldRoute}) => reportService.log('Nav didReplace to ${_name(newRoute)}'); String _name(Route? route) => route?.settings.name ?? 'unnamed ${route?.runtimeType}'; } diff --git a/lib/widgets/debug/app_debug_page.dart b/lib/widgets/debug/app_debug_page.dart index 8ac9e89ea..fc5c9480a 100644 --- a/lib/widgets/debug/app_debug_page.dart +++ b/lib/widgets/debug/app_debug_page.dart @@ -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'; diff --git a/lib/widgets/debug/firebase.dart b/lib/widgets/debug/firebase.dart deleted file mode 100644 index be073a05e..000000000 --- a/lib/widgets/debug/firebase.dart +++ /dev/null @@ -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}', - }, - ), - ) - ], - ); - } -} diff --git a/lib/widgets/debug/report.dart b/lib/widgets/debug/report.dart new file mode 100644 index 000000000..fd5d842be --- /dev/null +++ b/lib/widgets/debug/report.dart @@ -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}', + }, + ), + ) + ], + ); + } +} diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index 25b184f61..86e15c017 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -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 { } } context.read>().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();