chore(android,base,interface): Move notification stuff into Moxxy
This commit is contained in:
@@ -8,16 +8,6 @@
|
||||
|
||||
|
||||
<application>
|
||||
<provider
|
||||
android:name="me.polynom.moxplatform_android.MoxplatformFileProvider"
|
||||
android:authorities="me.polynom.moxplatform_android.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<service
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
@@ -38,7 +28,5 @@
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".NotificationReceiver" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
||||
package me.polynom.moxplatform_android
|
||||
|
||||
import androidx.core.content.FileProvider
|
||||
|
||||
class MoxplatformFileProvider : FileProvider(R.xml.file_paths) {
|
||||
}
|
||||
@@ -6,13 +6,12 @@ import static androidx.core.content.ContextCompat.startActivity;
|
||||
import static me.polynom.moxplatform_android.ConstantsKt.MOXPLATFORM_FILEPROVIDER_ID;
|
||||
import static me.polynom.moxplatform_android.ConstantsKt.SHARED_PREFERENCES_KEY;
|
||||
import static me.polynom.moxplatform_android.CryptoKt.*;
|
||||
import static me.polynom.moxplatform_android.NotificationsKt.createNotificationChannelsImpl;
|
||||
import static me.polynom.moxplatform_android.NotificationsKt.createNotificationGroupsImpl;
|
||||
import static me.polynom.moxplatform_android.RecordSentMessageKt.*;
|
||||
import static me.polynom.moxplatform_android.ThumbnailsKt.generateVideoThumbnailImplementation;
|
||||
|
||||
import me.polynom.moxplatform_android.Api.*;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
@@ -60,7 +59,7 @@ import io.flutter.plugin.common.JSONMethodCodec;
|
||||
import kotlin.Unit;
|
||||
import kotlin.jvm.functions.Function1;
|
||||
|
||||
public class MoxplatformAndroidPlugin extends BroadcastReceiver implements FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler, ServiceAware, MoxplatformApi {
|
||||
public class MoxplatformAndroidPlugin extends BroadcastReceiver implements FlutterPlugin, MethodCallHandler, ServiceAware, MoxplatformApi {
|
||||
public static final String entrypointKey = "entrypoint_handle";
|
||||
public static final String extraDataKey = "extra_data";
|
||||
private static final String autoStartAtBootKey = "auto_start_at_boot";
|
||||
@@ -71,8 +70,8 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt
|
||||
private static final List<MoxplatformAndroidPlugin> _instances = new ArrayList<>();
|
||||
private BackgroundService service;
|
||||
private MethodChannel channel;
|
||||
private static EventChannel notificationChannel;
|
||||
public static EventSink notificationSink;
|
||||
|
||||
public static Activity activity;
|
||||
|
||||
private Context context;
|
||||
|
||||
@@ -86,10 +85,6 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt
|
||||
channel.setMethodCallHandler(this);
|
||||
context = flutterPluginBinding.getApplicationContext();
|
||||
|
||||
notificationChannel = new EventChannel(flutterPluginBinding.getBinaryMessenger(), "me.polynom/notification_stream");
|
||||
notificationChannel.setStreamHandler(this);
|
||||
|
||||
|
||||
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(this.context);
|
||||
localBroadcastManager.registerReceiver(this, new IntentFilter(methodChannelKey));
|
||||
|
||||
@@ -102,6 +97,7 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt
|
||||
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(registrar.context());
|
||||
final MoxplatformAndroidPlugin plugin = new MoxplatformAndroidPlugin();
|
||||
localBroadcastManager.registerReceiver(plugin, new IntentFilter(methodChannelKey));
|
||||
activity = registrar.activity();
|
||||
|
||||
final MethodChannel channel = new MethodChannel(registrar.messenger(), "me.polynom/background_service_android", JSONMethodCodec.INSTANCE);
|
||||
channel.setMethodCallHandler(plugin);
|
||||
@@ -110,18 +106,6 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt
|
||||
Log.d(TAG, "Registered against registrar");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel(Object arguments) {
|
||||
Log.d(TAG, "Removed listener");
|
||||
notificationSink = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListen(Object arguments, EventChannel.EventSink eventSink) {
|
||||
Log.d(TAG, "Attached listener");
|
||||
notificationSink = eventSink;
|
||||
}
|
||||
|
||||
/// Store the entrypoint handle and extra data for the background service.
|
||||
private void configure(long entrypointHandle, String extraData) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE);
|
||||
@@ -226,60 +210,6 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt
|
||||
this.service = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createNotificationGroups(@NonNull List<NotificationGroup> groups) {
|
||||
createNotificationGroupsImpl(context, groups);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteNotificationGroups(@NonNull List<String> ids) {
|
||||
final NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||
for (final String id : ids) {
|
||||
notificationManager.deleteNotificationChannelGroup(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createNotificationChannels(@NonNull List<Api.NotificationChannel> channels) {
|
||||
createNotificationChannelsImpl(context, channels);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteNotificationChannels(@NonNull List<String> ids) {
|
||||
final NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||
for (final String id : ids) {
|
||||
notificationManager.deleteNotificationChannel(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showMessagingNotification(@NonNull MessagingNotification notification) {
|
||||
NotificationsKt.showMessagingNotification(context, notification);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showNotification(@NonNull RegularNotification notification) {
|
||||
NotificationsKt.showNotification(context, notification);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dismissNotification(@NonNull Long id) {
|
||||
NotificationManagerCompat.from(context).cancel(id.intValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNotificationSelfAvatar(@NonNull String path) {
|
||||
NotificationDataManager.INSTANCE.setAvatarPath(context, path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNotificationI18n(@NonNull NotificationI18nData data) {
|
||||
// Configure i18n
|
||||
NotificationDataManager.INSTANCE.setYou(context, data.getYou());
|
||||
NotificationDataManager.INSTANCE.setReply(context, data.getReply());
|
||||
NotificationDataManager.INSTANCE.setMarkAsRead(context, data.getMarkAsRead());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getPersistentDataPath() {
|
||||
@@ -349,9 +279,4 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt
|
||||
public Boolean generateVideoThumbnail(@NonNull String src, @NonNull String dest, @NonNull Long maxWidth) {
|
||||
return generateVideoThumbnailImplementation(src, dest, maxWidth);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eventStub(@NonNull NotificationEvent event) {
|
||||
// Stub to trick pigeon into
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
package me.polynom.moxplatform_android
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.Person
|
||||
import androidx.core.app.RemoteInput
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import me.polynom.moxplatform_android.Api.NotificationEvent
|
||||
import java.io.File
|
||||
import java.time.Instant
|
||||
|
||||
class NotificationReceiver : BroadcastReceiver() {
|
||||
/*
|
||||
* Dismisses the notification through which we received @intent.
|
||||
* */
|
||||
private fun dismissNotification(context: Context, intent: Intent) {
|
||||
// Dismiss the notification
|
||||
val notificationId = intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1).toInt()
|
||||
if (notificationId != -1) {
|
||||
NotificationManagerCompat.from(context).cancel(
|
||||
notificationId,
|
||||
)
|
||||
} else {
|
||||
Log.e("NotificationReceiver", "No id specified. Cannot dismiss notification")
|
||||
}
|
||||
}
|
||||
|
||||
private fun findActiveNotification(context: Context, id: Int): Notification? {
|
||||
return (context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
|
||||
.activeNotifications
|
||||
.find { it.id == id }?.notification
|
||||
}
|
||||
|
||||
private fun extractPayloadMapFromIntent(intent: Intent): Map<String?, String?> {
|
||||
val extras = mutableMapOf<String?, String?>()
|
||||
intent.extras?.keySet()!!.forEach {
|
||||
Log.d(TAG, "Checking $it -> ${intent.extras!!.get(it)}")
|
||||
if (it.startsWith("payload_")) {
|
||||
Log.d(TAG, "Adding $it")
|
||||
extras[it.substring(8)] = intent.extras!!.getString(it)
|
||||
}
|
||||
}
|
||||
|
||||
return extras
|
||||
}
|
||||
|
||||
private fun handleMarkAsRead(context: Context, intent: Intent) {
|
||||
MoxplatformAndroidPlugin.notificationSink?.success(
|
||||
NotificationEvent().apply {
|
||||
id = intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1)
|
||||
jid = intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!
|
||||
type = Api.NotificationEventType.MARK_AS_READ
|
||||
payload = null
|
||||
extra = extractPayloadMapFromIntent(intent)
|
||||
}.toList()
|
||||
)
|
||||
|
||||
NotificationManagerCompat.from(context).cancel(intent.getLongExtra(MARK_AS_READ_ID_KEY, -1).toInt())
|
||||
dismissNotification(context, intent);
|
||||
}
|
||||
|
||||
private fun handleReply(context: Context, intent: Intent) {
|
||||
val remoteInput = RemoteInput.getResultsFromIntent(intent) ?: return
|
||||
val replyPayload = remoteInput.getCharSequence(REPLY_TEXT_KEY)
|
||||
MoxplatformAndroidPlugin.notificationSink?.success(
|
||||
NotificationEvent().apply {
|
||||
id = intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1)
|
||||
jid = intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!
|
||||
type = Api.NotificationEventType.REPLY
|
||||
payload = replyPayload.toString()
|
||||
extra = extractPayloadMapFromIntent(intent)
|
||||
}.toList()
|
||||
)
|
||||
|
||||
val id = intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1).toInt()
|
||||
if (id == -1) {
|
||||
Log.e(TAG, "Failed to find notification id for reply")
|
||||
return;
|
||||
}
|
||||
|
||||
val notification = findActiveNotification(context, id)
|
||||
if (notification == null) {
|
||||
Log.e(TAG, "Failed to find notification for id $id")
|
||||
return
|
||||
}
|
||||
|
||||
// Thanks https://medium.com/@sidorovroman3/android-how-to-use-messagingstyle-for-notifications-without-caching-messages-c414ef2b816c
|
||||
val recoveredStyle = NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(notification)!!
|
||||
val newStyle = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||
Notification.MessagingStyle(
|
||||
android.app.Person.Builder().apply {
|
||||
setName(NotificationDataManager.getYou(context))
|
||||
|
||||
// Set an avatar, if we have one
|
||||
val avatarPath = NotificationDataManager.getAvatarPath(context)
|
||||
if (avatarPath != null) {
|
||||
setIcon(
|
||||
Icon.createWithAdaptiveBitmap(
|
||||
BitmapFactory.decodeFile(avatarPath)
|
||||
)
|
||||
)
|
||||
}
|
||||
}.build()
|
||||
)
|
||||
else Notification.MessagingStyle(NotificationDataManager.getYou(context))
|
||||
|
||||
newStyle.apply {
|
||||
conversationTitle = recoveredStyle.conversationTitle
|
||||
recoveredStyle.messages.forEach {
|
||||
// Check if we have to request (or refresh) the content URI to be able to still
|
||||
// see the embedded image.
|
||||
val mime = it.extras.getString(NOTIFICATION_MESSAGE_EXTRA_MIME)
|
||||
val path = it.extras.getString(NOTIFICATION_MESSAGE_EXTRA_PATH)
|
||||
val message = Notification.MessagingStyle.Message(it.text, it.timestamp, it.sender)
|
||||
if (mime != null && path != null) {
|
||||
// Request a new URI from the file provider to ensure we can still see the image
|
||||
// in the notification
|
||||
val fileUri = FileProvider.getUriForFile(
|
||||
context,
|
||||
MOXPLATFORM_FILEPROVIDER_ID,
|
||||
File(path),
|
||||
)
|
||||
message.setData(
|
||||
mime,
|
||||
fileUri,
|
||||
)
|
||||
|
||||
// As we're creating a new message, also recreate the additional metadata
|
||||
message.extras.apply {
|
||||
putString(NOTIFICATION_MESSAGE_EXTRA_MIME, mime)
|
||||
putString(NOTIFICATION_MESSAGE_EXTRA_PATH, path)
|
||||
}
|
||||
}
|
||||
|
||||
// Append the old message
|
||||
addMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
// Append our new message
|
||||
newStyle.addMessage(
|
||||
Notification.MessagingStyle.Message(
|
||||
replyPayload!!,
|
||||
Instant.now().toEpochMilli(),
|
||||
null as CharSequence?
|
||||
)
|
||||
)
|
||||
|
||||
// Post the new notification
|
||||
val recoveredBuilder = Notification.Builder.recoverBuilder(context, notification).apply {
|
||||
style = newStyle
|
||||
setOnlyAlertOnce(true)
|
||||
}
|
||||
NotificationManagerCompat.from(context).notify(id, recoveredBuilder.build())
|
||||
}
|
||||
|
||||
private fun handleTap(context: Context, intent: Intent) {
|
||||
MoxplatformAndroidPlugin.notificationSink?.success(
|
||||
NotificationEvent().apply {
|
||||
id = intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1)
|
||||
jid = intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!
|
||||
type = Api.NotificationEventType.OPEN
|
||||
payload = null
|
||||
extra = extractPayloadMapFromIntent(intent)
|
||||
}.toList()
|
||||
)
|
||||
|
||||
// Bring the app into the foreground
|
||||
val tapIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)!!
|
||||
context.startActivity(tapIntent)
|
||||
|
||||
// Dismiss the notification
|
||||
dismissNotification(context, intent)
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
// TODO: We need to be careful to ensure that the Flutter engine is running.
|
||||
// If it's not, we have to start it. However, that's only an issue when we expect to
|
||||
// receive notifications while not running, i.e. Push Notifications.
|
||||
when (intent.action) {
|
||||
MARK_AS_READ_ACTION -> handleMarkAsRead(context, intent)
|
||||
REPLY_ACTION -> handleReply(context, intent)
|
||||
TAP_ACTION -> handleTap(context, intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
package me.polynom.moxplatform_android
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationChannelGroup
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.Person
|
||||
import androidx.core.app.RemoteInput
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import java.io.File
|
||||
|
||||
/*
|
||||
* Holds "persistent" data for notifications, like i18n strings. While not useful now, this is
|
||||
* useful for when the app is dead and we receive a notification.
|
||||
* */
|
||||
object NotificationDataManager {
|
||||
private var you: String? = null
|
||||
private var markAsRead: String? = null
|
||||
private var reply: String? = null
|
||||
|
||||
private var fetchedAvatarPath = false
|
||||
private var avatarPath: String? = null
|
||||
|
||||
private fun getString(context: Context, key: String, fallback: String): String {
|
||||
return context.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)!!.getString(key, fallback)!!
|
||||
}
|
||||
|
||||
private fun setString(context: Context, key: String, value: String) {
|
||||
val prefs = context.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
|
||||
prefs.edit()
|
||||
.putString(key, value)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun getYou(context: Context): String {
|
||||
if (you == null) you = getString(context, SHARED_PREFERENCES_YOU_KEY, "You")
|
||||
return you!!
|
||||
}
|
||||
|
||||
fun setYou(context: Context, value: String) {
|
||||
setString(context, SHARED_PREFERENCES_YOU_KEY, value)
|
||||
you = value
|
||||
}
|
||||
|
||||
fun getMarkAsRead(context: Context): String {
|
||||
if (markAsRead == null) markAsRead = getString(context, SHARED_PREFERENCES_MARK_AS_READ_KEY, "Mark as read")
|
||||
return markAsRead!!
|
||||
}
|
||||
|
||||
fun setMarkAsRead(context: Context, value: String) {
|
||||
setString(context, SHARED_PREFERENCES_MARK_AS_READ_KEY, value)
|
||||
markAsRead = value
|
||||
}
|
||||
|
||||
fun getReply(context: Context): String {
|
||||
if (reply != null) reply = getString(context, SHARED_PREFERENCES_REPLY_KEY, "Reply")
|
||||
return reply!!
|
||||
}
|
||||
|
||||
fun setReply(context: Context, value: String) {
|
||||
setString(context, SHARED_PREFERENCES_REPLY_KEY, value)
|
||||
reply = value
|
||||
}
|
||||
|
||||
fun getAvatarPath(context: Context): String? {
|
||||
if (avatarPath == null && !fetchedAvatarPath) {
|
||||
val path = getString(context, SHARED_PREFERENCES_AVATAR_KEY, "")
|
||||
if (path.isNotEmpty()) {
|
||||
avatarPath = path
|
||||
}
|
||||
}
|
||||
|
||||
return avatarPath
|
||||
}
|
||||
|
||||
fun setAvatarPath(context: Context, value: String) {
|
||||
setString(context, SHARED_PREFERENCES_AVATAR_KEY, value)
|
||||
fetchedAvatarPath = true
|
||||
avatarPath = value
|
||||
}
|
||||
}
|
||||
|
||||
fun createNotificationGroupsImpl(context: Context, groups: List<Api.NotificationGroup>) {
|
||||
val notificationManager = context.getSystemService(NotificationManager::class.java)
|
||||
for (group in groups) {
|
||||
notificationManager.createNotificationChannelGroup(
|
||||
NotificationChannelGroup(group.id, group.description),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun createNotificationChannelsImpl(context: Context, channels: List<Api.NotificationChannel>) {
|
||||
val notificationManager = context.getSystemService(NotificationManager::class.java)
|
||||
for (channel in channels) {
|
||||
val importance = when(channel.importance) {
|
||||
Api.NotificationChannelImportance.DEFAULT -> NotificationManager.IMPORTANCE_DEFAULT
|
||||
Api.NotificationChannelImportance.MIN -> NotificationManager.IMPORTANCE_MIN
|
||||
Api.NotificationChannelImportance.HIGH -> NotificationManager.IMPORTANCE_HIGH
|
||||
}
|
||||
val notificationChannel = NotificationChannel(channel.id, channel.title, importance).apply {
|
||||
description = channel.description
|
||||
|
||||
enableVibration(channel.vibration)
|
||||
enableLights(channel.enableLights)
|
||||
setShowBadge(channel.showBadge)
|
||||
if (channel.groupId != null) {
|
||||
group = channel.groupId
|
||||
}
|
||||
}
|
||||
notificationManager.createNotificationChannel(notificationChannel)
|
||||
}
|
||||
}
|
||||
|
||||
/// Show a messaging style notification described by @notification.
|
||||
fun showMessagingNotification(context: Context, notification: Api.MessagingNotification) {
|
||||
// Build the actions
|
||||
// -> Reply action
|
||||
val remoteInput = RemoteInput.Builder(REPLY_TEXT_KEY).apply {
|
||||
setLabel(NotificationDataManager.getReply(context))
|
||||
}.build()
|
||||
val replyIntent = Intent(context, NotificationReceiver::class.java).apply {
|
||||
action = REPLY_ACTION
|
||||
putExtra(NOTIFICATION_EXTRA_JID_KEY, notification.jid)
|
||||
putExtra(NOTIFICATION_EXTRA_ID_KEY, notification.id)
|
||||
|
||||
notification.extra?.forEach {
|
||||
putExtra("payload_${it.key}", it.value)
|
||||
}
|
||||
}
|
||||
val replyPendingIntent = PendingIntent.getBroadcast(
|
||||
context.applicationContext,
|
||||
0,
|
||||
replyIntent,
|
||||
PendingIntent.FLAG_MUTABLE,
|
||||
)
|
||||
val replyAction = NotificationCompat.Action.Builder(
|
||||
R.drawable.reply,
|
||||
NotificationDataManager.getReply(context),
|
||||
replyPendingIntent,
|
||||
).apply {
|
||||
addRemoteInput(remoteInput)
|
||||
setAllowGeneratedReplies(true)
|
||||
}.build()
|
||||
|
||||
// -> Mark as read action
|
||||
val markAsReadIntent = Intent(context, NotificationReceiver::class.java).apply {
|
||||
action = MARK_AS_READ_ACTION
|
||||
putExtra(NOTIFICATION_EXTRA_JID_KEY, notification.jid)
|
||||
putExtra(NOTIFICATION_EXTRA_ID_KEY, notification.id)
|
||||
|
||||
notification.extra?.forEach {
|
||||
putExtra("payload_${it.key}", it.value)
|
||||
}
|
||||
}
|
||||
val markAsReadPendingIntent = PendingIntent.getBroadcast(
|
||||
context.applicationContext,
|
||||
0,
|
||||
markAsReadIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
val markAsReadAction = NotificationCompat.Action.Builder(
|
||||
R.drawable.mark_as_read,
|
||||
NotificationDataManager.getMarkAsRead(context),
|
||||
markAsReadPendingIntent,
|
||||
).build()
|
||||
|
||||
// -> Tap action
|
||||
// Thanks https://github.com/MaikuB/flutter_local_notifications/blob/master/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java#L246
|
||||
val tapIntent = Intent(context, NotificationReceiver::class.java).apply {
|
||||
action = TAP_ACTION
|
||||
putExtra(NOTIFICATION_EXTRA_JID_KEY, notification.jid)
|
||||
putExtra(NOTIFICATION_EXTRA_ID_KEY, notification.id)
|
||||
|
||||
notification.extra?.forEach {
|
||||
putExtra("payload_${it.key}", it.value)
|
||||
}
|
||||
}
|
||||
val tapPendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
notification.id.toInt(),
|
||||
tapIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
// Build the notification
|
||||
val selfPerson = Person.Builder().apply {
|
||||
setName(NotificationDataManager.getYou(context))
|
||||
|
||||
// Set an avatar, if we have one
|
||||
val avatarPath = NotificationDataManager.getAvatarPath(context)
|
||||
if (avatarPath != null) {
|
||||
setIcon(
|
||||
IconCompat.createWithAdaptiveBitmap(
|
||||
BitmapFactory.decodeFile(avatarPath),
|
||||
),
|
||||
)
|
||||
}
|
||||
}.build()
|
||||
val style = NotificationCompat.MessagingStyle(selfPerson);
|
||||
style.isGroupConversation = notification.isGroupchat
|
||||
if (notification.isGroupchat) {
|
||||
style.conversationTitle = notification.title
|
||||
}
|
||||
|
||||
for (i in notification.messages.indices) {
|
||||
val message = notification.messages[i]
|
||||
|
||||
// Build the sender
|
||||
// NOTE: Note that we set it to null if message.sender == null because otherwise this results in
|
||||
// a bogus Person object which messes with the "self-message" display as Android expects
|
||||
// null in that case.
|
||||
val sender = if (message.sender == null)
|
||||
null
|
||||
else Person.Builder().apply {
|
||||
setName(message.sender)
|
||||
setKey(message.jid)
|
||||
|
||||
// Set the avatar, if available
|
||||
if (message.avatarPath != null) {
|
||||
try {
|
||||
setIcon(
|
||||
IconCompat.createWithAdaptiveBitmap(
|
||||
BitmapFactory.decodeFile(message.avatarPath),
|
||||
),
|
||||
)
|
||||
} catch (ex: Throwable) {
|
||||
Log.w(TAG, "Failed to open avatar at ${message.avatarPath}")
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
|
||||
// Build the message
|
||||
val body = message.content.body ?: ""
|
||||
val msg = NotificationCompat.MessagingStyle.Message(
|
||||
body,
|
||||
message.timestamp,
|
||||
sender,
|
||||
)
|
||||
// If we got an image, turn it into a content URI and set it
|
||||
if (message.content.mime != null && message.content.path != null) {
|
||||
val fileUri = FileProvider.getUriForFile(
|
||||
context,
|
||||
MOXPLATFORM_FILEPROVIDER_ID,
|
||||
File(message.content.path),
|
||||
)
|
||||
msg.apply {
|
||||
setData(message.content.mime, fileUri)
|
||||
|
||||
extras.apply {
|
||||
putString(NOTIFICATION_MESSAGE_EXTRA_MIME, message.content.mime)
|
||||
putString(NOTIFICATION_MESSAGE_EXTRA_PATH, message.content.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append the message
|
||||
style.addMessage(msg)
|
||||
}
|
||||
|
||||
// Assemble the notification
|
||||
val finalNotification = NotificationCompat.Builder(context, notification.channelId).apply {
|
||||
setStyle(style)
|
||||
// NOTE: It's okay to use the service icon here as I cannot get Android to display the
|
||||
// actual logo. So we'll have to make do with the silhouette and the color purple.
|
||||
setSmallIcon(R.drawable.ic_service_icon)
|
||||
color = Color.argb(255, 207, 74, 255)
|
||||
setColorized(true)
|
||||
|
||||
// Tap action
|
||||
setContentIntent(tapPendingIntent)
|
||||
|
||||
// Notification actions
|
||||
addAction(replyAction)
|
||||
addAction(markAsReadAction)
|
||||
|
||||
// Groupchat title
|
||||
if (notification.isGroupchat) {
|
||||
setContentTitle(notification.title)
|
||||
}
|
||||
|
||||
// Prevent grouping with the foreground service
|
||||
if (notification.groupId != null) {
|
||||
setGroup(notification.groupId)
|
||||
}
|
||||
|
||||
setAllowSystemGeneratedContextualActions(true)
|
||||
setCategory(Notification.CATEGORY_MESSAGE)
|
||||
|
||||
// Prevent no notification when we replied before
|
||||
setOnlyAlertOnce(false)
|
||||
}.build()
|
||||
|
||||
// Post the notification
|
||||
NotificationManagerCompat.from(context).notify(
|
||||
notification.id.toInt(),
|
||||
finalNotification,
|
||||
)
|
||||
}
|
||||
|
||||
fun showNotification(context: Context, notification: Api.RegularNotification) {
|
||||
val builtNotification = NotificationCompat.Builder(context, notification.channelId).apply {
|
||||
setContentTitle(notification.title)
|
||||
setContentText(notification.body)
|
||||
|
||||
when (notification.icon) {
|
||||
Api.NotificationIcon.ERROR -> setSmallIcon(R.drawable.error)
|
||||
Api.NotificationIcon.WARNING -> setSmallIcon(R.drawable.warning)
|
||||
Api.NotificationIcon.NONE -> {}
|
||||
}
|
||||
|
||||
if (notification.groupId != null) {
|
||||
setGroup(notification.groupId)
|
||||
}
|
||||
}.build()
|
||||
|
||||
// Post the notification
|
||||
NotificationManagerCompat.from(context).notify(notification.id.toInt(), builtNotification)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- For testing -->
|
||||
<cache-path name="file_picker" path="file_picker/"/>
|
||||
|
||||
<!-- Moxxy -->
|
||||
<files-path name="media" path="media/" />
|
||||
</paths>
|
||||
@@ -1,64 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart';
|
||||
|
||||
class AndroidNotificationsImplementation extends NotificationsImplementation {
|
||||
final MoxplatformApi _api = MoxplatformApi();
|
||||
|
||||
final EventChannel _channel =
|
||||
const EventChannel('me.polynom/notification_stream');
|
||||
|
||||
@override
|
||||
Future<void> createNotificationChannels(
|
||||
List<NotificationChannel> channels) async {
|
||||
return _api.createNotificationChannels(channels);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteNotificationChannels(List<String> ids) {
|
||||
return _api.deleteNotificationChannels(ids);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> createNotificationGroups(List<NotificationGroup> groups) async {
|
||||
return _api.createNotificationGroups(groups);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteNotificationGroups(List<String> ids) {
|
||||
return _api.deleteNotificationGroups(ids);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> showMessagingNotification(
|
||||
MessagingNotification notification,
|
||||
) async {
|
||||
return _api.showMessagingNotification(notification);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> showNotification(RegularNotification notification) async {
|
||||
return _api.showNotification(notification);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dismissNotification(int id) async {
|
||||
return _api.dismissNotification(id);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setNotificationSelfAvatar(String path) async {
|
||||
return _api.setNotificationSelfAvatar(path);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setI18n(NotificationI18nData data) {
|
||||
return _api.setNotificationI18n(data);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<NotificationEvent> getEventStream() => _channel
|
||||
.receiveBroadcastStream()
|
||||
.cast<Object>()
|
||||
.map(NotificationEvent.decode);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:moxplatform_android/src/contacts_android.dart';
|
||||
import 'package:moxplatform_android/src/crypto_android.dart';
|
||||
import 'package:moxplatform_android/src/isolate_android.dart';
|
||||
import 'package:moxplatform_android/src/notifications_android.dart';
|
||||
import 'package:moxplatform_android/src/platform_android.dart';
|
||||
import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart';
|
||||
|
||||
@@ -12,7 +11,6 @@ class MoxplatformAndroidPlugin extends MoxplatformInterface {
|
||||
MoxplatformInterface.contacts = AndroidContactsImplementation();
|
||||
MoxplatformInterface.crypto = AndroidCryptographyImplementation();
|
||||
MoxplatformInterface.handler = AndroidIsolateHandler();
|
||||
MoxplatformInterface.notifications = AndroidNotificationsImplementation();
|
||||
MoxplatformInterface.platform = AndroidPlatformImplementation();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user