channel multiple result crashfix, channel error reporting, crashlytics abstraction
This commit is contained in:
parent
9b90c7ba84
commit
2a82aef354
37 changed files with 482 additions and 249 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import com.bumptech.glide.Glide
|
|||
import com.bumptech.glide.load.DecodeFormat
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safesus
|
||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
|
||||
import deckers.thibault.aves.model.FieldMap
|
||||
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
|
@ -29,35 +29,13 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
|||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"getPackages" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getPackages) }
|
||||
"getAppIcon" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getAppIcon) }
|
||||
"getAppIcon" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::getAppIcon) }
|
||||
"copyToClipboard" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::copyToClipboard) }
|
||||
"edit" -> {
|
||||
val title = call.argument<String>("title")
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val mimeType = call.argument<String>("mimeType")
|
||||
result.success(edit(title, uri, mimeType))
|
||||
}
|
||||
"open" -> {
|
||||
val title = call.argument<String>("title")
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val mimeType = call.argument<String>("mimeType")
|
||||
result.success(open(title, uri, mimeType))
|
||||
}
|
||||
"openMap" -> {
|
||||
val geoUri = call.argument<String>("geoUri")?.let { Uri.parse(it) }
|
||||
result.success(openMap(geoUri))
|
||||
}
|
||||
"setAs" -> {
|
||||
val title = call.argument<String>("title")
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val mimeType = call.argument<String>("mimeType")
|
||||
result.success(setAs(title, uri, mimeType))
|
||||
}
|
||||
"share" -> {
|
||||
val title = call.argument<String>("title")
|
||||
val urisByMimeType = call.argument<Map<String, List<String>>>("urisByMimeType")!!
|
||||
result.success(shareMultiple(title, urisByMimeType))
|
||||
}
|
||||
"edit" -> safe(call, result, ::edit)
|
||||
"open" -> safe(call, result, ::open)
|
||||
"openMap" -> safe(call, result, ::openMap)
|
||||
"setAs" -> safe(call, result, ::setAs)
|
||||
"share" -> safe(call, result, ::share)
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
@ -173,77 +151,113 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private fun edit(title: String?, uri: Uri?, mimeType: String?): Boolean {
|
||||
uri ?: return false
|
||||
private fun edit(call: MethodCall, result: MethodChannel.Result) {
|
||||
val title = call.argument<String>("title")
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val mimeType = call.argument<String>("mimeType")
|
||||
if (uri == null) {
|
||||
result.error("edit-args", "failed because of missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
val intent = Intent(Intent.ACTION_EDIT)
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
.setDataAndType(getShareableUri(uri), mimeType)
|
||||
return safeStartActivityChooser(title, intent)
|
||||
val started = safeStartActivityChooser(title, intent)
|
||||
|
||||
result.success(started)
|
||||
}
|
||||
|
||||
private fun open(title: String?, uri: Uri?, mimeType: String?): Boolean {
|
||||
uri ?: return false
|
||||
private fun open(call: MethodCall, result: MethodChannel.Result) {
|
||||
val title = call.argument<String>("title")
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val mimeType = call.argument<String>("mimeType")
|
||||
if (uri == null) {
|
||||
result.error("open-args", "failed because of missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.setDataAndType(getShareableUri(uri), mimeType)
|
||||
return safeStartActivityChooser(title, intent)
|
||||
val started = safeStartActivityChooser(title, intent)
|
||||
|
||||
result.success(started)
|
||||
}
|
||||
|
||||
private fun openMap(geoUri: Uri?): Boolean {
|
||||
geoUri ?: return false
|
||||
private fun openMap(call: MethodCall, result: MethodChannel.Result) {
|
||||
val geoUri = call.argument<String>("geoUri")?.let { Uri.parse(it) }
|
||||
if (geoUri == null) {
|
||||
result.error("openMap-args", "failed because of missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW, geoUri)
|
||||
return safeStartActivity(intent)
|
||||
val started = safeStartActivity(intent)
|
||||
|
||||
result.success(started)
|
||||
}
|
||||
|
||||
private fun setAs(title: String?, uri: Uri?, mimeType: String?): Boolean {
|
||||
uri ?: return false
|
||||
private fun setAs(call: MethodCall, result: MethodChannel.Result) {
|
||||
val title = call.argument<String>("title")
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val mimeType = call.argument<String>("mimeType")
|
||||
if (uri == null) {
|
||||
result.error("setAs-args", "failed because of missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
val intent = Intent(Intent.ACTION_ATTACH_DATA)
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.setDataAndType(getShareableUri(uri), mimeType)
|
||||
return safeStartActivityChooser(title, intent)
|
||||
val started = safeStartActivityChooser(title, intent)
|
||||
|
||||
result.success(started)
|
||||
}
|
||||
|
||||
private fun shareSingle(title: String?, uri: Uri, mimeType: String): Boolean {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.setType(mimeType)
|
||||
.putExtra(Intent.EXTRA_STREAM, getShareableUri(uri))
|
||||
return safeStartActivityChooser(title, intent)
|
||||
}
|
||||
|
||||
private fun shareMultiple(title: String?, urisByMimeType: Map<String, List<String>>?): Boolean {
|
||||
urisByMimeType ?: return false
|
||||
private fun share(call: MethodCall, result: MethodChannel.Result) {
|
||||
val title = call.argument<String>("title")
|
||||
val urisByMimeType = call.argument<Map<String, List<String>>>("urisByMimeType")
|
||||
if (urisByMimeType == null) {
|
||||
result.error("setAs-args", "failed because of missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
val uriList = ArrayList(urisByMimeType.values.flatten().mapNotNull { Uri.parse(it) })
|
||||
val mimeTypes = urisByMimeType.keys.toTypedArray()
|
||||
|
||||
// simplify share intent for a single item, as some apps can handle one item but not more
|
||||
if (uriList.size == 1) {
|
||||
return shareSingle(title, uriList.first(), mimeTypes.first())
|
||||
}
|
||||
val started = if (uriList.size == 1) {
|
||||
val uri = uriList.first()
|
||||
val mimeType = mimeTypes.first()
|
||||
|
||||
var mimeType = "*/*"
|
||||
if (mimeTypes.size == 1) {
|
||||
// items have the same mime type & subtype
|
||||
mimeType = mimeTypes.first()
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.setType(mimeType)
|
||||
.putExtra(Intent.EXTRA_STREAM, getShareableUri(uri))
|
||||
safeStartActivityChooser(title, intent)
|
||||
} else {
|
||||
// items have different subtypes
|
||||
val mimeTypeTypes = mimeTypes.map { it.split("/") }.distinct()
|
||||
if (mimeTypeTypes.size == 1) {
|
||||
// items have the same mime type
|
||||
mimeType = "${mimeTypeTypes.first()}/*"
|
||||
var mimeType = "*/*"
|
||||
if (mimeTypes.size == 1) {
|
||||
// items have the same mime type & subtype
|
||||
mimeType = mimeTypes.first()
|
||||
} else {
|
||||
// items have different subtypes
|
||||
val mimeTypeTypes = mimeTypes.map { it.split("/") }.distinct()
|
||||
if (mimeTypeTypes.size == 1) {
|
||||
// items have the same mime type
|
||||
mimeType = "${mimeTypeTypes.first()}/*"
|
||||
}
|
||||
}
|
||||
|
||||
val intent = Intent(Intent.ACTION_SEND_MULTIPLE)
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList)
|
||||
.setType(mimeType)
|
||||
safeStartActivityChooser(title, intent)
|
||||
}
|
||||
|
||||
val intent = Intent(Intent.ACTION_SEND_MULTIPLE)
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList)
|
||||
.setType(mimeType)
|
||||
return safeStartActivityChooser(title, intent)
|
||||
result.success(started)
|
||||
}
|
||||
|
||||
private fun safeStartActivity(intent: Intent): Boolean {
|
||||
|
|
|
@ -8,6 +8,7 @@ import androidx.core.content.pm.ShortcutManagerCompat
|
|||
import androidx.core.graphics.drawable.IconCompat
|
||||
import deckers.thibault.aves.MainActivity
|
||||
import deckers.thibault.aves.R
|
||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||
import deckers.thibault.aves.utils.BitmapUtils.centerSquareCrop
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
@ -20,23 +21,31 @@ import java.util.*
|
|||
class AppShortcutHandler(private val context: Context) : MethodCallHandler {
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"canPin" -> result.success(canPin())
|
||||
"pin" -> {
|
||||
GlobalScope.launch(Dispatchers.IO) { pin(call) }
|
||||
result.success(null)
|
||||
}
|
||||
"canPin" -> safe(call, result, ::canPin)
|
||||
"pin" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::pin) }
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
private fun canPin() = ShortcutManagerCompat.isRequestPinShortcutSupported(context)
|
||||
private fun isSupported() = ShortcutManagerCompat.isRequestPinShortcutSupported(context)
|
||||
|
||||
private fun pin(call: MethodCall) {
|
||||
if (!canPin()) return
|
||||
private fun canPin(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
|
||||
result.success(isSupported())
|
||||
}
|
||||
|
||||
val label = call.argument<String>("label") ?: return
|
||||
private fun pin(call: MethodCall, result: MethodChannel.Result) {
|
||||
val label = call.argument<String>("label")
|
||||
val iconBytes = call.argument<ByteArray>("iconBytes")
|
||||
val filters = call.argument<List<String>>("filters") ?: return
|
||||
val filters = call.argument<List<String>>("filters")
|
||||
if (label == null || filters == null) {
|
||||
result.error("pin-args", "failed because of missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
if (!isSupported()) {
|
||||
result.error("pin-unsupported", "failed because the launcher does not support pinning shortcuts", null)
|
||||
return
|
||||
}
|
||||
|
||||
var icon: IconCompat? = null
|
||||
if (iconBytes?.isNotEmpty() == true) {
|
||||
|
@ -62,6 +71,8 @@ class AppShortcutHandler(private val context: Context) : MethodCallHandler {
|
|||
.setIntent(intent)
|
||||
.build()
|
||||
ShortcutManagerCompat.requestPinShortcut(context, shortcut, null)
|
||||
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package deckers.thibault.aves.channel.calls
|
||||
|
||||
import deckers.thibault.aves.MainActivity
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -8,24 +9,42 @@ import kotlinx.coroutines.launch
|
|||
import kotlin.reflect.KSuspendFunction2
|
||||
|
||||
// ensure `result` methods are called on the main looper thread
|
||||
class Coresult internal constructor(private val methodResult: MethodChannel.Result) : MethodChannel.Result {
|
||||
class Coresult internal constructor(private val call: MethodCall, private val methodResult: MethodChannel.Result) : MethodChannel.Result {
|
||||
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
override fun success(result: Any?) {
|
||||
mainScope.launch { methodResult.success(result) }
|
||||
mainScope.launch {
|
||||
try {
|
||||
methodResult.success(result)
|
||||
} catch (e: Exception) {
|
||||
MainActivity.notifyError("failed to reply success for method=${call.method}, result=$result, exception=$e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
|
||||
mainScope.launch { methodResult.error(errorCode, errorMessage, errorDetails) }
|
||||
mainScope.launch {
|
||||
try {
|
||||
methodResult.error(errorCode, errorMessage, errorDetails)
|
||||
} catch (e: Exception) {
|
||||
MainActivity.notifyError("failed to reply error for method=${call.method}, errorCode=$errorCode, errorMessage=$errorMessage, errorDetails=$errorDetails, exception=$e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun notImplemented() {
|
||||
mainScope.launch { methodResult.notImplemented() }
|
||||
mainScope.launch {
|
||||
try {
|
||||
methodResult.notImplemented()
|
||||
} catch (e: Exception) {
|
||||
MainActivity.notifyError("failed to reply notImplemented for method=${call.method}, exception=$e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun safe(call: MethodCall, result: MethodChannel.Result, function: (call: MethodCall, result: MethodChannel.Result) -> Unit) {
|
||||
val res = Coresult(result)
|
||||
val res = Coresult(call, result)
|
||||
try {
|
||||
function(call, res)
|
||||
} catch (e: Exception) {
|
||||
|
@ -33,12 +52,12 @@ class Coresult internal constructor(private val methodResult: MethodChannel.Resu
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun safesus(call: MethodCall, result: MethodChannel.Result, function: KSuspendFunction2<MethodCall, MethodChannel.Result, Unit>) {
|
||||
val res = Coresult(result)
|
||||
suspend fun safeSuspend(call: MethodCall, result: MethodChannel.Result, function: KSuspendFunction2<MethodCall, MethodChannel.Result, Unit>) {
|
||||
val res = Coresult(call, result)
|
||||
try {
|
||||
function(call, res)
|
||||
} catch (e: Exception) {
|
||||
res.error("safe-exception", e.message, e.stackTraceToString())
|
||||
res.error("safeSuspend-exception", e.message, e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ import android.database.Cursor
|
|||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
|
@ -36,8 +38,15 @@ import java.util.*
|
|||
class DebugHandler(private val context: Context) : MethodCallHandler {
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"crash" -> Handler(Looper.getMainLooper()).postDelayed({ throw TestException() }, 50)
|
||||
"exception" -> throw TestException()
|
||||
"safeException" -> safe(call, result) { _, _ -> throw TestException() }
|
||||
"exceptionInCoroutine" -> GlobalScope.launch(Dispatchers.IO) { throw TestException() }
|
||||
"safeExceptionInCoroutine" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result) { _, _ -> throw TestException() } }
|
||||
|
||||
"getContextDirs" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getContextDirs) }
|
||||
"getEnv" -> result.success(System.getenv())
|
||||
"getEnv" -> safe(call, result, ::getEnv)
|
||||
|
||||
"getBitmapFactoryInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getBitmapFactoryInfo) }
|
||||
"getContentResolverMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getContentResolverMetadata) }
|
||||
"getExifInterfaceMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getExifInterfaceMetadata) }
|
||||
|
@ -71,6 +80,10 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
|
|||
result.success(dirs)
|
||||
}
|
||||
|
||||
private fun getEnv(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
|
||||
result.success(System.getenv())
|
||||
}
|
||||
|
||||
private fun getBitmapFactoryInfo(call: MethodCall, result: MethodChannel.Result) {
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
if (uri == null) {
|
||||
|
@ -320,4 +333,6 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
|
|||
private val LOG_TAG = LogUtils.createTag<DebugHandler>()
|
||||
const val CHANNEL = "deckers.thibault/aves/debug"
|
||||
}
|
||||
|
||||
class TestException internal constructor() : RuntimeException("oops")
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ class WindowHandler(private val activity: Activity) : MethodCallHandler {
|
|||
"keepScreenOn" -> safe(call, result, ::keepScreenOn)
|
||||
"isRotationLocked" -> safe(call, result, ::isRotationLocked)
|
||||
"requestOrientation" -> safe(call, result, ::requestOrientation)
|
||||
"canSetCutoutMode" -> result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||
"canSetCutoutMode" -> safe(call, result, ::canSetCutoutMode)
|
||||
"setCutoutMode" -> safe(call, result, ::setCutoutMode)
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
|
@ -60,6 +60,10 @@ class WindowHandler(private val activity: Activity) : MethodCallHandler {
|
|||
result.success(true)
|
||||
}
|
||||
|
||||
private fun canSetCutoutMode(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
|
||||
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||
}
|
||||
|
||||
private fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) {
|
||||
val use = call.argument<Boolean>("use")
|
||||
if (use == null) {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:isolate';
|
||||
|
||||
import 'package:aves/services/services.dart';
|
||||
import 'package:aves/widgets/aves_app.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() {
|
||||
|
@ -20,12 +20,11 @@ void main() {
|
|||
//
|
||||
// flutter run --profile --trace-skia
|
||||
|
||||
Isolate.current.addErrorListener(RawReceivePort((pair) async {
|
||||
initPlatformServices();
|
||||
|
||||
Isolate.current.addErrorListener(RawReceivePort((pair) {
|
||||
final List<dynamic> errorAndStacktrace = pair;
|
||||
await FirebaseCrashlytics.instance.recordError(
|
||||
errorAndStacktrace.first,
|
||||
errorAndStacktrace.last,
|
||||
);
|
||||
reportService.recordError(errorAndStacktrace.first, errorAndStacktrace.last);
|
||||
}).sendPort);
|
||||
|
||||
runApp(const AvesApp());
|
||||
|
|
|
@ -13,7 +13,6 @@ import 'package:aves/services/services.dart';
|
|||
import 'package:aves/utils/pedantic.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
@ -120,7 +119,7 @@ class Settings extends ChangeNotifier {
|
|||
// to allow settings customization without Firebase context (e.g. before a Flutter Driver test)
|
||||
Future<void> initFirebase() async {
|
||||
await Firebase.app().setAutomaticDataCollectionEnabled(isCrashlyticsEnabled);
|
||||
await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(isCrashlyticsEnabled);
|
||||
await reportService.setCollectionEnabled(isCrashlyticsEnabled);
|
||||
}
|
||||
|
||||
Future<void> reset({required bool includeInternalKeys}) async {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,17 +1,57 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:aves/services/services.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class AndroidDebugService {
|
||||
static const platform = MethodChannel('deckers.thibault/aves/debug');
|
||||
|
||||
static Future<void> crash() async {
|
||||
try {
|
||||
await platform.invokeMethod('crash');
|
||||
} on PlatformException catch (e) {
|
||||
await reportService.recordChannelError('crash', e);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> exception() async {
|
||||
try {
|
||||
await platform.invokeMethod('exception');
|
||||
} on PlatformException catch (e) {
|
||||
await reportService.recordChannelError('exception', e);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> safeException() async {
|
||||
try {
|
||||
await platform.invokeMethod('safeException');
|
||||
} on PlatformException catch (e) {
|
||||
await reportService.recordChannelError('safeException', e);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> exceptionInCoroutine() async {
|
||||
try {
|
||||
await platform.invokeMethod('exceptionInCoroutine');
|
||||
} on PlatformException catch (e) {
|
||||
await reportService.recordChannelError('exceptionInCoroutine', e);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> safeExceptionInCoroutine() async {
|
||||
try {
|
||||
await platform.invokeMethod('safeExceptionInCoroutine');
|
||||
} on PlatformException catch (e) {
|
||||
await reportService.recordChannelError('safeExceptionInCoroutine', e);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Map> getContextDirs() async {
|
||||
try {
|
||||
final result = await platform.invokeMethod('getContextDirs');
|
||||
if (result != null) return result as Map;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getContextDirs failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
|
||||
await reportService.recordChannelError('getContextDirs', e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
@ -21,7 +61,7 @@ class AndroidDebugService {
|
|||
final result = await platform.invokeMethod('getEnv');
|
||||
if (result != null) return result as Map;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getEnv failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
|
||||
await reportService.recordChannelError('getEnv', e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
@ -34,7 +74,7 @@ class AndroidDebugService {
|
|||
});
|
||||
if (result != null) return result as Map;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getBitmapFactoryInfo failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
await reportService.recordChannelError('getBitmapFactoryInfo', e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
@ -48,7 +88,7 @@ class AndroidDebugService {
|
|||
});
|
||||
if (result != null) return result as Map;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getContentResolverMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
await reportService.recordChannelError('getContentResolverMetadata', e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
@ -63,7 +103,7 @@ class AndroidDebugService {
|
|||
});
|
||||
if (result != null) return result as Map;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getExifInterfaceMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
await reportService.recordChannelError('getExifInterfaceMetadata', e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
@ -76,7 +116,7 @@ class AndroidDebugService {
|
|||
});
|
||||
if (result != null) return result as Map;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getMediaMetadataRetrieverMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
await reportService.recordChannelError('getMediaMetadataRetrieverMetadata', e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
@ -91,7 +131,7 @@ class AndroidDebugService {
|
|||
});
|
||||
if (result != null) return result as Map;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getMetadataExtractorSummary failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
await reportService.recordChannelError('getMetadataExtractorSummary', e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
@ -105,7 +145,7 @@ class AndroidDebugService {
|
|||
});
|
||||
if (result != null) return result as Map;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getTiffStructure failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
await reportService.recordChannelError('getTiffStructure', e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:aves/services/services.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
abstract class EmbeddedDataService {
|
||||
|
@ -27,7 +27,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService {
|
|||
});
|
||||
if (result != null) return (result as List).cast<Uint8List>();
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getExifThumbnail failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
await reportService.recordChannelError('getExifThumbnail', e);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService {
|
|||
});
|
||||
if (result != null) return result as Map;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('extractMotionPhotoVideo failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
await reportService.recordChannelError('extractMotionPhotoVideo', e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService {
|
|||
});
|
||||
if (result != null) return result as Map;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('extractVideoEmbeddedPicture failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
await reportService.recordChannelError('extractVideoEmbeddedPicture', e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
@ -75,7 +75,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService {
|
|||
});
|
||||
if (result != null) return result as Map;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('extractXmpDataProp failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
await reportService.recordChannelError('extractXmpDataProp', e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/services/services.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
class GeocodingService {
|
||||
|
@ -21,7 +21,7 @@ class GeocodingService {
|
|||
});
|
||||
return (result as List).cast<Map>().map((map) => Address.fromMap(map)).toList();
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getAddress failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
|
||||
await reportService.recordChannelError('getAddress', e);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/services/image_op_events.dart';
|
||||
import 'package:aves/services/output_buffer.dart';
|
||||
import 'package:aves/services/service_policy.dart';
|
||||
import 'package:aves/services/services.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:streams_channel/streams_channel.dart';
|
||||
|
||||
abstract class ImageFileService {
|
||||
|
@ -124,7 +125,7 @@ class PlatformImageFileService implements ImageFileService {
|
|||
}) as Map;
|
||||
return AvesEntry.fromMap(result);
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getEntry failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
await reportService.recordChannelError('getEntry', e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -188,7 +189,7 @@ class PlatformImageFileService implements ImageFileService {
|
|||
);
|
||||
return completer.future;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getImage failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
reportService.recordChannelError('getImage', e);
|
||||
}
|
||||
return Future.sync(() => Uint8List(0));
|
||||
}
|
||||
|
@ -223,7 +224,7 @@ class PlatformImageFileService implements ImageFileService {
|
|||
});
|
||||
if (result != null) return result as Uint8List;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getRegion failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
await reportService.recordChannelError('getRegion', e);
|
||||
}
|
||||
return Uint8List(0);
|
||||
},
|
||||
|
@ -260,7 +261,7 @@ class PlatformImageFileService implements ImageFileService {
|
|||
});
|
||||
if (result != null) return result as Uint8List;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getThumbnail failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
await reportService.recordChannelError('getThumbnail', e);
|
||||
}
|
||||
return Uint8List(0);
|
||||
},
|
||||
|
@ -274,7 +275,7 @@ class PlatformImageFileService implements ImageFileService {
|
|||
try {
|
||||
return platform.invokeMethod('clearSizedThumbnailDiskCache');
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('clearSizedThumbnailDiskCache failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
await reportService.recordChannelError('clearSizedThumbnailDiskCache', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -295,7 +296,7 @@ class PlatformImageFileService implements ImageFileService {
|
|||
'entries': entries.map(_toPlatformEntryMap).toList(),
|
||||
}).map((event) => ImageOpEvent.fromMap(event));
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('delete failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
reportService.recordChannelError('delete', e);
|
||||
return Stream.error(e);
|
||||
}
|
||||
}
|
||||
|
@ -314,7 +315,7 @@ class PlatformImageFileService implements ImageFileService {
|
|||
'destinationPath': destinationAlbum,
|
||||
}).map((event) => MoveOpEvent.fromMap(event));
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('move failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
reportService.recordChannelError('move', e);
|
||||
return Stream.error(e);
|
||||
}
|
||||
}
|
||||
|
@ -333,7 +334,7 @@ class PlatformImageFileService implements ImageFileService {
|
|||
'destinationPath': destinationAlbum,
|
||||
}).map((event) => ExportOpEvent.fromMap(event));
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('export failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
reportService.recordChannelError('export', e);
|
||||
return Stream.error(e);
|
||||
}
|
||||
}
|
||||
|
@ -356,7 +357,7 @@ class PlatformImageFileService implements ImageFileService {
|
|||
});
|
||||
if (result != null) return (result as Map).cast<String, dynamic>();
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('captureFrame failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
await reportService.recordChannelError('captureFrame', e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
@ -371,7 +372,7 @@ class PlatformImageFileService implements ImageFileService {
|
|||
});
|
||||
if (result != null) return (result as Map).cast<String, dynamic>();
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('rename failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
await reportService.recordChannelError('rename', e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
@ -386,7 +387,7 @@ class PlatformImageFileService implements ImageFileService {
|
|||
});
|
||||
if (result != null) return (result as Map).cast<String, dynamic>();
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('rotate failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
await reportService.recordChannelError('rotate', e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
@ -400,7 +401,7 @@ class PlatformImageFileService implements ImageFileService {
|
|||
});
|
||||
if (result != null) return (result as Map).cast<String, dynamic>();
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('flip failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
await reportService.recordChannelError('flip', e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
@immutable
|
||||
class ImageOpEvent extends Equatable {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/services/services.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:streams_channel/streams_channel.dart';
|
||||
|
||||
abstract class MediaStoreService {
|
||||
|
@ -26,7 +26,7 @@ class PlatformMediaStoreService implements MediaStoreService {
|
|||
});
|
||||
return (result as List).cast<int>();
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('checkObsoleteContentIds failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
await reportService.recordChannelError('checkObsoleteContentIds', e);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ class PlatformMediaStoreService implements MediaStoreService {
|
|||
});
|
||||
return (result as List).cast<int>();
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('checkObsoletePaths failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
await reportService.recordChannelError('checkObsoletePaths', e);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ class PlatformMediaStoreService implements MediaStoreService {
|
|||
'knownEntries': knownEntries,
|
||||
}).map((event) => AvesEntry.fromMap(event));
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getEntries failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
reportService.recordChannelError('getEntries', e);
|
||||
return Stream.error(e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
55
lib/services/report_service.dart
Normal file
55
lib/services/report_service.dart
Normal 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);
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import 'package:aves/services/embedded_data_service.dart';
|
|||
import 'package:aves/services/image_file_service.dart';
|
||||
import 'package:aves/services/media_store_service.dart';
|
||||
import 'package:aves/services/metadata_service.dart';
|
||||
import 'package:aves/services/report_service.dart';
|
||||
import 'package:aves/services/storage_service.dart';
|
||||
import 'package:aves/services/time_service.dart';
|
||||
import 'package:aves/services/window_service.dart';
|
||||
|
@ -20,6 +21,7 @@ final EmbeddedDataService embeddedDataService = getIt<EmbeddedDataService>();
|
|||
final ImageFileService imageFileService = getIt<ImageFileService>();
|
||||
final MediaStoreService mediaStoreService = getIt<MediaStoreService>();
|
||||
final MetadataService metadataService = getIt<MetadataService>();
|
||||
final ReportService reportService = getIt<ReportService>();
|
||||
final StorageService storageService = getIt<StorageService>();
|
||||
final TimeService timeService = getIt<TimeService>();
|
||||
final WindowService windowService = getIt<WindowService>();
|
||||
|
@ -33,6 +35,7 @@ void initPlatformServices() {
|
|||
getIt.registerLazySingleton<ImageFileService>(() => PlatformImageFileService());
|
||||
getIt.registerLazySingleton<MediaStoreService>(() => PlatformMediaStoreService());
|
||||
getIt.registerLazySingleton<MetadataService>(() => PlatformMetadataService());
|
||||
getIt.registerLazySingleton<ReportService>(() => CrashlyticsReportService());
|
||||
getIt.registerLazySingleton<StorageService>(() => PlatformStorageService());
|
||||
getIt.registerLazySingleton<TimeService>(() => PlatformTimeService());
|
||||
getIt.registerLazySingleton<WindowService>(() => PlatformWindowService());
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:aves/services/output_buffer.dart';
|
||||
import 'package:aves/services/services.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
@ -47,7 +48,7 @@ class PlatformStorageService implements StorageService {
|
|||
final result = await platform.invokeMethod('getStorageVolumes');
|
||||
return (result as List).cast<Map>().map((map) => StorageVolume.fromMap(map)).toSet();
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getStorageVolumes failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
|
||||
await reportService.recordChannelError('getStorageVolumes', e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
@ -60,7 +61,7 @@ class PlatformStorageService implements StorageService {
|
|||
});
|
||||
return result as int?;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getFreeSpace failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
|
||||
await reportService.recordChannelError('getFreeSpace', e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -71,7 +72,7 @@ class PlatformStorageService implements StorageService {
|
|||
final result = await platform.invokeMethod('getGrantedDirectories');
|
||||
return (result as List).cast<String>();
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getGrantedDirectories failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
|
||||
await reportService.recordChannelError('getGrantedDirectories', e);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
@ -83,7 +84,7 @@ class PlatformStorageService implements StorageService {
|
|||
'path': path,
|
||||
});
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('revokeDirectoryAccess failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
|
||||
await reportService.recordChannelError('revokeDirectoryAccess', e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -98,7 +99,7 @@ class PlatformStorageService implements StorageService {
|
|||
return (result as List).cast<Map>().map(VolumeRelativeDirectory.fromMap).toSet();
|
||||
}
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getInaccessibleDirectories failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
|
||||
await reportService.recordChannelError('getInaccessibleDirectories', e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
@ -111,7 +112,7 @@ class PlatformStorageService implements StorageService {
|
|||
return (result as List).cast<Map>().map(VolumeRelativeDirectory.fromMap).toSet();
|
||||
}
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getRestrictedDirectories failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
|
||||
await reportService.recordChannelError('getRestrictedDirectories', e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
@ -134,7 +135,7 @@ class PlatformStorageService implements StorageService {
|
|||
);
|
||||
return completer.future;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('requestVolumeAccess failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
|
||||
await reportService.recordChannelError('requestVolumeAccess', e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -148,7 +149,7 @@ class PlatformStorageService implements StorageService {
|
|||
});
|
||||
if (result != null) return result as int;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('deleteEmptyDirectories failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
|
||||
await reportService.recordChannelError('deleteEmptyDirectories', e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
@ -164,7 +165,7 @@ class PlatformStorageService implements StorageService {
|
|||
});
|
||||
if (result != null) return Uri.tryParse(result);
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('scanFile failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
|
||||
await reportService.recordChannelError('scanFile', e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -172,7 +173,7 @@ class PlatformStorageService implements StorageService {
|
|||
@override
|
||||
Future<bool?> createFile(String name, String mimeType, Uint8List bytes) async {
|
||||
try {
|
||||
final completer = Completer<bool>();
|
||||
final completer = Completer<bool?>();
|
||||
storageAccessChannel.receiveBroadcastStream(<String, dynamic>{
|
||||
'op': 'createFile',
|
||||
'name': name,
|
||||
|
@ -188,7 +189,7 @@ class PlatformStorageService implements StorageService {
|
|||
);
|
||||
return completer.future;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('createFile failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
|
||||
await reportService.recordChannelError('createFile', e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -215,7 +216,7 @@ class PlatformStorageService implements StorageService {
|
|||
);
|
||||
return completer.future;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('openFile failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
|
||||
await reportService.recordChannelError('openFile', e);
|
||||
}
|
||||
return Uint8List(0);
|
||||
}
|
||||
|
@ -223,7 +224,7 @@ class PlatformStorageService implements StorageService {
|
|||
@override
|
||||
Future<String?> selectDirectory() async {
|
||||
try {
|
||||
final completer = Completer<String>();
|
||||
final completer = Completer<String?>();
|
||||
storageAccessChannel.receiveBroadcastStream(<String, dynamic>{
|
||||
'op': 'selectDirectory',
|
||||
}).listen(
|
||||
|
@ -236,7 +237,7 @@ class PlatformStorageService implements StorageService {
|
|||
);
|
||||
return completer.future;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('selectDirectory failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
|
||||
await reportService.recordChannelError('selectDirectory', e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:aves/services/services.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class ViewerService {
|
||||
|
@ -10,7 +10,7 @@ class ViewerService {
|
|||
final result = await platform.invokeMethod('getIntentData');
|
||||
if (result != null) return (result as Map).cast<String, dynamic>();
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getIntentData failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
await reportService.recordChannelError('getIntentData', e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ class ViewerService {
|
|||
'uri': uri,
|
||||
});
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('pick failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
await reportService.recordChannelError('pick', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ import 'package:aves/widgets/home_page.dart';
|
|||
import 'package:aves/widgets/welcome_page.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
@ -44,6 +43,7 @@ class _AvesAppState extends State<AvesApp> {
|
|||
List<NavigatorObserver> _navigatorObservers = [];
|
||||
final EventChannel _mediaStoreChangeChannel = const EventChannel('deckers.thibault/aves/mediastorechange');
|
||||
final EventChannel _newIntentChannel = const EventChannel('deckers.thibault/aves/intent');
|
||||
final EventChannel _errorChannel = const EventChannel('deckers.thibault/aves/error');
|
||||
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey(debugLabel: 'app-navigator');
|
||||
|
||||
Widget getFirstPage({Map? intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : const WelcomePage();
|
||||
|
@ -52,10 +52,10 @@ class _AvesAppState extends State<AvesApp> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
EquatableConfig.stringify = true;
|
||||
initPlatformServices();
|
||||
_appSetup = _setup();
|
||||
_mediaStoreChangeChannel.receiveBroadcastStream().listen((event) => _onMediaStoreChange(event as String?));
|
||||
_newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map?));
|
||||
_errorChannel.receiveBroadcastStream().listen((event) => _onError(event as String?));
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -124,18 +124,17 @@ class _AvesAppState extends State<AvesApp> {
|
|||
|
||||
Future<void> _setup() async {
|
||||
await Firebase.initializeApp().then((app) {
|
||||
final crashlytics = FirebaseCrashlytics.instance;
|
||||
FlutterError.onError = crashlytics.recordFlutterError;
|
||||
crashlytics.setCustomKey('locales', window.locales.join(', '));
|
||||
FlutterError.onError = reportService.recordFlutterError;
|
||||
final now = DateTime.now();
|
||||
crashlytics.setCustomKey('timezone', '${now.timeZoneName} (${now.timeZoneOffset})');
|
||||
crashlytics.setCustomKey(
|
||||
'build_mode',
|
||||
kReleaseMode
|
||||
? 'release'
|
||||
: kProfileMode
|
||||
? 'profile'
|
||||
: 'debug');
|
||||
reportService.setCustomKeys({
|
||||
'locales': window.locales.join(', '),
|
||||
'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})',
|
||||
'build_mode': kReleaseMode
|
||||
? 'release'
|
||||
: kProfileMode
|
||||
? 'profile'
|
||||
: 'debug',
|
||||
});
|
||||
});
|
||||
await settings.init();
|
||||
await settings.initFirebase();
|
||||
|
@ -150,7 +149,7 @@ class _AvesAppState extends State<AvesApp> {
|
|||
// do not reset when relaunching the app
|
||||
if (appModeNotifier.value == AppMode.main && (intentData == null || intentData.isEmpty == true)) return;
|
||||
|
||||
FirebaseCrashlytics.instance.log('New intent');
|
||||
reportService.log('New intent');
|
||||
_navigatorKey.currentState!.pushReplacement(DirectMaterialPageRoute(
|
||||
settings: const RouteSettings(name: HomePage.routeName),
|
||||
builder: (_) => getFirstPage(intentData: intentData),
|
||||
|
@ -171,4 +170,6 @@ class _AvesAppState extends State<AvesApp> {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onError(String? error) => reportService.recordError(error, null);
|
||||
}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:aves/services/services.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CrashlyticsRouteTracker extends NavigatorObserver {
|
||||
@override
|
||||
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) => FirebaseCrashlytics.instance.log('Nav didPush to ${_name(route)}');
|
||||
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) => reportService.log('Nav didPush to ${_name(route)}');
|
||||
|
||||
@override
|
||||
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) => FirebaseCrashlytics.instance.log('Nav didPop to ${_name(previousRoute)}');
|
||||
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) => reportService.log('Nav didPop to ${_name(previousRoute)}');
|
||||
|
||||
@override
|
||||
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) => FirebaseCrashlytics.instance.log('Nav didRemove to ${_name(previousRoute)}');
|
||||
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) => reportService.log('Nav didRemove to ${_name(previousRoute)}');
|
||||
|
||||
@override
|
||||
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) => FirebaseCrashlytics.instance.log('Nav didReplace to ${_name(newRoute)}');
|
||||
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) => reportService.log('Nav didReplace to ${_name(newRoute)}');
|
||||
|
||||
String _name(Route<dynamic>? route) => route?.settings.name ?? 'unnamed ${route?.runtimeType}';
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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}',
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
63
lib/widgets/debug/report.dart
Normal file
63
lib/widgets/debug/report.dart
Normal 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}',
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -17,7 +17,6 @@ import 'package:aves/widgets/filter_grids/albums_page.dart';
|
|||
import 'package:aves/widgets/search/search_delegate.dart';
|
||||
import 'package:aves/widgets/search/search_page.dart';
|
||||
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
@ -106,7 +105,7 @@ class _HomePageState extends State<HomePage> {
|
|||
}
|
||||
}
|
||||
context.read<ValueNotifier<AppMode>>().value = appMode;
|
||||
unawaited(FirebaseCrashlytics.instance.setCustomKey('app_mode', appMode.toString()));
|
||||
unawaited(reportService.setCustomKey('app_mode', appMode.toString()));
|
||||
|
||||
if (appMode != AppMode.view) {
|
||||
final source = context.read<CollectionSource>();
|
||||
|
|
Loading…
Reference in a new issue