Compare commits

..

9 Commits

10 changed files with 90 additions and 36 deletions

View File

@@ -3,7 +3,10 @@ package org.moxxy.moxxy_native
const val TAG = "moxxy_native" const val TAG = "moxxy_native"
// The event channel name for the keyboard height // The event channel name for the keyboard height
const val KEYBOARD_HEIGHT_EVENT_CHANNEL_NAME = "org.moxxy.moxxyv2/notification_stream" const val KEYBOARD_HEIGHT_EVENT_CHANNEL_NAME = "org.moxxy.moxxyv2/keyboard_stream"
// The event channel name for notification events
const val NOTIFICATION_EVENT_CHANNEL_NAME = "org.moxxy.moxxyv2/notification_stream"
// The size of buffers to use for various operations // The size of buffers to use for various operations
const val BUFFER_SIZE = 4096 const val BUFFER_SIZE = 4096

View File

@@ -25,6 +25,7 @@ import org.moxxy.moxxy_native.media.MediaImplementation
import org.moxxy.moxxy_native.media.MoxxyMediaApi import org.moxxy.moxxy_native.media.MoxxyMediaApi
import org.moxxy.moxxy_native.notifications.MoxxyNotificationsApi import org.moxxy.moxxy_native.notifications.MoxxyNotificationsApi
import org.moxxy.moxxy_native.notifications.NotificationEvent import org.moxxy.moxxy_native.notifications.NotificationEvent
import org.moxxy.moxxy_native.notifications.NotificationStreamHandler
import org.moxxy.moxxy_native.notifications.NotificationsImplementation import org.moxxy.moxxy_native.notifications.NotificationsImplementation
import org.moxxy.moxxy_native.picker.FilePickerType import org.moxxy.moxxy_native.picker.FilePickerType
import org.moxxy.moxxy_native.picker.MoxxyPickerApi import org.moxxy.moxxy_native.picker.MoxxyPickerApi
@@ -37,23 +38,6 @@ import org.moxxy.moxxy_native.service.MoxxyServiceApi
import org.moxxy.moxxy_native.service.PluginTracker import org.moxxy.moxxy_native.service.PluginTracker
import org.moxxy.moxxy_native.service.ServiceImplementation import org.moxxy.moxxy_native.service.ServiceImplementation
object MoxxyEventChannels {
var notificationChannel: EventChannel? = null
var notificationEventSink: EventChannel.EventSink? = null
}
object NotificationStreamHandler : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
Log.d(TAG, "NotificationStreamHandler: Attached stream")
MoxxyEventChannels.notificationEventSink = events
}
override fun onCancel(arguments: Any?) {
Log.d(TAG, "NotificationStreamHandler: Detached stream")
MoxxyEventChannels.notificationEventSink = null
}
}
/* /*
* Hold the last notification event in case we did a cold start. * Hold the last notification event in case we did a cold start.
*/ */
@@ -107,6 +91,10 @@ class MoxxyNativePlugin : FlutterPlugin, ActivityAware, ServiceAware, BroadcastR
val keyboardChannel = EventChannel(flutterPluginBinding.getBinaryMessenger(), KEYBOARD_HEIGHT_EVENT_CHANNEL_NAME) val keyboardChannel = EventChannel(flutterPluginBinding.getBinaryMessenger(), KEYBOARD_HEIGHT_EVENT_CHANNEL_NAME)
keyboardChannel?.setStreamHandler(KeyboardStreamHandler) keyboardChannel?.setStreamHandler(KeyboardStreamHandler)
// Special handling from notification events
val notificationChannel = EventChannel(flutterPluginBinding.getBinaryMessenger(), NOTIFICATION_EVENT_CHANNEL_NAME)
notificationChannel?.setStreamHandler(NotificationStreamHandler)
// Register the picker handler // Register the picker handler
pickerListener = PickerResultListener(context!!) pickerListener = PickerResultListener(context!!)
Log.d(TAG, "Attached to engine") Log.d(TAG, "Attached to engine")

View File

@@ -1,8 +1,6 @@
package org.moxxy.moxxy_native.notifications package org.moxxy.moxxy_native.notifications
import android.content.Intent import android.content.Intent
import android.util.Log
import org.moxxy.moxxy_native.TAG
/* /*
* Extract all user-added extra key-value pairs from @intent. * Extract all user-added extra key-value pairs from @intent.
@@ -10,9 +8,7 @@ import org.moxxy.moxxy_native.TAG
fun extractPayloadMapFromIntent(intent: Intent): Map<String?, String?> { fun extractPayloadMapFromIntent(intent: Intent): Map<String?, String?> {
val extras = mutableMapOf<String?, String?>() val extras = mutableMapOf<String?, String?>()
intent.extras?.keySet()!!.forEach { intent.extras?.keySet()!!.forEach {
Log.d(TAG, "Checking $it -> ${intent.extras!!.get(it)}")
if (it.startsWith("payload_")) { if (it.startsWith("payload_")) {
Log.d(TAG, "Adding $it")
extras[it.substring(8)] = intent.extras!!.getString(it) extras[it.substring(8)] = intent.extras!!.getString(it)
} }
} }

View File

@@ -15,7 +15,6 @@ import androidx.core.app.RemoteInput
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import org.moxxy.moxxy_native.MARK_AS_READ_ACTION import org.moxxy.moxxy_native.MARK_AS_READ_ACTION
import org.moxxy.moxxy_native.MOXXY_FILEPROVIDER_ID import org.moxxy.moxxy_native.MOXXY_FILEPROVIDER_ID
import org.moxxy.moxxy_native.MoxxyEventChannels
import org.moxxy.moxxy_native.NOTIFICATION_EXTRA_ID_KEY import org.moxxy.moxxy_native.NOTIFICATION_EXTRA_ID_KEY
import org.moxxy.moxxy_native.NOTIFICATION_EXTRA_JID_KEY import org.moxxy.moxxy_native.NOTIFICATION_EXTRA_JID_KEY
import org.moxxy.moxxy_native.NOTIFICATION_MESSAGE_EXTRA_MIME import org.moxxy.moxxy_native.NOTIFICATION_MESSAGE_EXTRA_MIME
@@ -50,7 +49,7 @@ class NotificationReceiver : BroadcastReceiver() {
} }
private fun handleMarkAsRead(context: Context, intent: Intent) { private fun handleMarkAsRead(context: Context, intent: Intent) {
MoxxyEventChannels.notificationEventSink?.success( NotificationStreamHandler.sink?.success(
NotificationEvent( NotificationEvent(
intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1), intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1),
intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!, intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!,
@@ -65,7 +64,7 @@ class NotificationReceiver : BroadcastReceiver() {
private fun handleReply(context: Context, intent: Intent) { private fun handleReply(context: Context, intent: Intent) {
val remoteInput = RemoteInput.getResultsFromIntent(intent) ?: return val remoteInput = RemoteInput.getResultsFromIntent(intent) ?: return
val replyPayload = remoteInput.getCharSequence(REPLY_TEXT_KEY) val replyPayload = remoteInput.getCharSequence(REPLY_TEXT_KEY)
MoxxyEventChannels.notificationEventSink?.success( NotificationStreamHandler.sink?.success(
NotificationEvent( NotificationEvent(
intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1), intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1),
intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!, intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!,
@@ -165,7 +164,7 @@ class NotificationReceiver : BroadcastReceiver() {
} }
private fun handleTap(context: Context, intent: Intent) { private fun handleTap(context: Context, intent: Intent) {
MoxxyEventChannels.notificationEventSink?.success( NotificationStreamHandler.sink?.success(
NotificationEvent( NotificationEvent(
intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1), intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1),
intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!, intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!,

View File

@@ -0,0 +1,22 @@
package org.moxxy.moxxy_native.notifications
import android.util.Log
import io.flutter.plugin.common.EventChannel
import org.moxxy.moxxy_native.TAG
object NotificationStreamHandler : EventChannel.StreamHandler {
// The event sink to use for sending notification events to the service.
var sink: EventChannel.EventSink? = null
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
// "register" the event sink
sink = events
Log.d(TAG, "NotificationStreamHandler: Attached stream")
}
override fun onCancel(arguments: Any?) {
sink = null
Log.d(TAG, "NotificationStreamHandler: Detached stream")
}
}

View File

@@ -94,7 +94,7 @@ class NotificationsImplementation(private val context: Context) : MoxxyNotificat
context.applicationContext, context.applicationContext,
0, 0,
replyIntent, replyIntent,
PendingIntent.FLAG_MUTABLE, PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
) )
val replyAction = NotificationCompat.Action.Builder( val replyAction = NotificationCompat.Action.Builder(
R.drawable.reply, R.drawable.reply,
@@ -113,13 +113,14 @@ class NotificationsImplementation(private val context: Context) : MoxxyNotificat
notification.extra?.forEach { notification.extra?.forEach {
putExtra("payload_${it.key}", it.value) putExtra("payload_${it.key}", it.value)
Log.d(TAG, "Adding payload_${it.key} -> ${it.value}")
} }
} }
val markAsReadPendingIntent = PendingIntent.getBroadcast( val markAsReadPendingIntent = PendingIntent.getBroadcast(
context.applicationContext, context.applicationContext,
0, 0,
markAsReadIntent, markAsReadIntent,
PendingIntent.FLAG_IMMUTABLE, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
) )
val markAsReadAction = NotificationCompat.Action.Builder( val markAsReadAction = NotificationCompat.Action.Builder(
R.drawable.mark_as_read, R.drawable.mark_as_read,
@@ -250,7 +251,7 @@ class NotificationsImplementation(private val context: Context) : MoxxyNotificat
setCategory(Notification.CATEGORY_MESSAGE) setCategory(Notification.CATEGORY_MESSAGE)
// Prevent no notification when we replied before // Prevent no notification when we replied before
setOnlyAlertOnce(false) setOnlyAlertOnce(true)
// Automatically dismiss the notification on tap // Automatically dismiss the notification on tap
setAutoCancel(true) setAutoCancel(true)

View File

@@ -0,0 +1,13 @@
package org.moxxy.moxxy_native.picker
object MimeUtils {
// A reverse-mapping of image mime types to their commonly used file extension.
val imageMimeTypesToFileExtension = mapOf(
"image/png" to ".png",
"image/apng" to ".apng",
"image/avif" to ".avif",
"image/gif" to ".gif",
"image/jpeg" to ".jpg",
"image/webp" to ".webp",
)
}

View File

@@ -6,10 +6,11 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.OpenableColumns import android.provider.MediaStore.Images
import android.util.Log import android.util.Log
import io.flutter.plugin.common.PluginRegistry.ActivityResultListener import io.flutter.plugin.common.PluginRegistry.ActivityResultListener
import org.moxxy.moxxy_native.AsyncRequestTracker import org.moxxy.moxxy_native.AsyncRequestTracker
import org.moxxy.moxxy_native.BUFFER_SIZE
import org.moxxy.moxxy_native.PICK_FILES_REQUEST import org.moxxy.moxxy_native.PICK_FILES_REQUEST
import org.moxxy.moxxy_native.PICK_FILE_REQUEST import org.moxxy.moxxy_native.PICK_FILE_REQUEST
import org.moxxy.moxxy_native.PICK_FILE_WITH_DATA_REQUEST import org.moxxy.moxxy_native.PICK_FILE_WITH_DATA_REQUEST
@@ -20,6 +21,26 @@ import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
/*
* Attempt to replace the file extension in @fileName with @newExtension. If @newExtension is null,
* then @fileName is returned verbatim.
* */
private fun maybeReplaceExtension(fileName: String, newExtension: String?): String {
if (newExtension == null) {
return fileName
}
assert(newExtension[0] == '.')
val parts = fileName.split(".")
return if (parts.size == 1) {
"$fileName$newExtension"
} else {
// Split at the ".", join all but the list end together and append the new extension
val fileNameWithoutExtension = parts.subList(0, parts.size - 1).joinToString(".")
"$fileNameWithoutExtension$newExtension"
}
}
class PickerResultListener(private val context: Context) : ActivityResultListener { class PickerResultListener(private val context: Context) : ActivityResultListener {
/* /*
* Attempt to deduce the filename for the URI @uri. * Attempt to deduce the filename for the URI @uri.
@@ -29,10 +50,22 @@ class PickerResultListener(private val context: Context) : ActivityResultListene
private fun queryFileName(context: Context, uri: Uri): String { private fun queryFileName(context: Context, uri: Uri): String {
var result: String? = null var result: String? = null
if (uri.scheme == "content") { if (uri.scheme == "content") {
val projection = arrayOf(
Images.Media._ID,
Images.Media.MIME_TYPE,
Images.Media.DISPLAY_NAME,
)
val cursor = context.contentResolver.query(uri, null, null, null, null) val cursor = context.contentResolver.query(uri, null, null, null, null)
cursor.use { cursor -> cursor.use { cursor ->
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)) val mimeType = cursor.getString(cursor.getColumnIndex(Images.Media.MIME_TYPE))
val displayName = cursor.getString(cursor.getColumnIndex(Images.Media.DISPLAY_NAME))
val fileExtension = MimeUtils.imageMimeTypesToFileExtension[mimeType]
// Note: This is a workaround for the Dart image library failing to parse the file
// because displayName somehow is always ".jpg", which confuses image.
result = maybeReplaceExtension(displayName, fileExtension)
Log.d(TAG, "Returning $result as filename (MIME: $mimeType)")
} }
} }
} }
@@ -51,7 +84,7 @@ class PickerResultListener(private val context: Context) : ActivityResultListene
if (Build.VERSION.SDK_INT >= 33) { if (Build.VERSION.SDK_INT >= 33) {
android.os.FileUtils.copy(input, output) android.os.FileUtils.copy(input, output)
} else { } else {
val buffer = ByteArray(4096) val buffer = ByteArray(BUFFER_SIZE)
while (input.read(buffer).also {} != -1) { while (input.read(buffer).also {} != -1) {
output.write(buffer) output.write(buffer)
} }
@@ -94,7 +127,7 @@ class PickerResultListener(private val context: Context) : ActivityResultListene
} }
val returnBuffer = mutableListOf<Byte>() val returnBuffer = mutableListOf<Byte>()
val readBuffer = ByteArray(4096) val readBuffer = ByteArray(BUFFER_SIZE)
try { try {
val inputStream = context.contentResolver.openInputStream(data!!.data!!)!! val inputStream = context.contentResolver.openInputStream(data!!.data!!)!!
while (inputStream.read(readBuffer).also {} != -1) { while (inputStream.read(readBuffer).also {} != -1) {

View File

@@ -8,7 +8,6 @@ import android.view.ViewTreeObserver
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel
import org.moxxy.moxxy_native.MoxxyEventChannels
import org.moxxy.moxxy_native.TAG import org.moxxy.moxxy_native.TAG
object KeyboardStreamHandler : EventChannel.StreamHandler { object KeyboardStreamHandler : EventChannel.StreamHandler {
@@ -67,7 +66,7 @@ object KeyboardStreamHandler : EventChannel.StreamHandler {
} }
override fun onCancel(arguments: Any?) { override fun onCancel(arguments: Any?) {
MoxxyEventChannels.notificationEventSink = null sink = null
Log.d(TAG, "KeyboardStreamHandler: Detached stream") Log.d(TAG, "KeyboardStreamHandler: Detached stream")
} }
} }

View File

@@ -1,6 +1,6 @@
name: moxxy_native name: moxxy_native
description: Interactions with the system for Moxxy description: Interactions with the system for Moxxy
version: 0.3.0 version: 0.3.2
publish_to: https://git.polynom.me/api/packages/Moxxy/pub publish_to: https://git.polynom.me/api/packages/Moxxy/pub
homepage: homepage: