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