From 0528de7fde25d52c9d530ef0c6518c1eb38d5593 Mon Sep 17 00:00:00 2001 From: Aniket Kumar Date: Mon, 3 Aug 2020 18:00:19 +0530 Subject: [PATCH] Implemented notification support to notify the user about the newly received SMS/MMS when KDE Connect will be set as the default SMS app. --- AndroidManifest.xml | 6 + res/drawable/ic_baseline_sms_24.xml | 5 + res/values/strings.xml | 4 + .../Helpers/NotificationHelper.java | 7 + .../NotificationsPlugin.java | 16 +- .../SMSPlugin/MmsReceivedReceiver.java | 98 +++++- .../Plugins/SMSPlugin/MmsSentReceiver.java | 22 ++ .../SMSPlugin/NotificationReplyReceiver.java | 303 ++++++++++++++++++ .../Plugins/SMSPlugin/SmsMmsUtils.java | 43 +++ .../Plugins/SMSPlugin/SmsReceiver.java | 83 +++++ .../Plugins/SMSPlugin/SmsSentReceiver.java | 22 ++ 11 files changed, 604 insertions(+), 5 deletions(-) create mode 100644 res/drawable/ic_baseline_sms_24.xml create mode 100644 src/org/kde/kdeconnect/Plugins/SMSPlugin/NotificationReplyReceiver.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 8266ff0e..775a5a5c 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -95,6 +95,12 @@ android:enabled="true" android:taskAffinity="com.klinker.android.messaging.MMS_SENT" /> + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 7b54d361..0955467d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -308,6 +308,7 @@ Media control File transfer High priority + New Message Stop the current player Copy URL to clipboard @@ -374,6 +375,9 @@ To share microphone input from your phone you need to give access to the phone\'s audio input Speech + REPLY + MARK AS READ + You Send MMS Send group MMS set_group_message_as_mms diff --git a/src/org/kde/kdeconnect/Helpers/NotificationHelper.java b/src/org/kde/kdeconnect/Helpers/NotificationHelper.java index a3603d1b..46a866e0 100644 --- a/src/org/kde/kdeconnect/Helpers/NotificationHelper.java +++ b/src/org/kde/kdeconnect/Helpers/NotificationHelper.java @@ -19,6 +19,7 @@ public class NotificationHelper { public final static String DEFAULT = "default"; public final static String MEDIA_CONTROL = "media_control"; public final static String FILETRANSFER = "filetransfer"; + public final static String SMS_MMS = "sms_mms"; public final static String RECEIVENOTIFICATION = "receive"; public final static String HIGHPRIORITY = "highpriority"; } @@ -83,6 +84,12 @@ public class NotificationHelper { NotificationManager.IMPORTANCE_DEFAULT) ); + manager.createNotificationChannel(new NotificationChannel( + Channels.SMS_MMS, + context.getString(R.string.notification_channel_sms_mms), + NotificationManager.IMPORTANCE_DEFAULT) + ); + NotificationChannel highPriority = new NotificationChannel(Channels.HIGHPRIORITY, context.getString(R.string.notification_channel_high_priority), NotificationManager.IMPORTANCE_HIGH); manager.createNotificationChannel(highPriority); } diff --git a/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java index 5db80691..65ee7da2 100644 --- a/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java @@ -58,6 +58,7 @@ import org.kde.kdeconnect.Helpers.AppsHelper; import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.Plugins.PluginFactory; +import org.kde.kdeconnect.Plugins.SMSPlugin.NotificationReplyReceiver; import org.kde.kdeconnect.UserInterface.MainActivity; import org.kde.kdeconnect.UserInterface.PluginSettingsFragment; import org.kde.kdeconnect.UserInterface.StartActivityAlertDialogFragment; @@ -203,7 +204,6 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver. String packageName = statusBarNotification.getPackageName(); String appName = AppsHelper.appNameLookup(context, packageName); - if ("com.facebook.orca".equals(packageName) && (statusBarNotification.getId() == 10012) && "Messenger".equals(appName) && @@ -219,8 +219,18 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver. } if ("org.kde.kdeconnect_tp".equals(packageName)) { - // Don't send our own notifications - return; + // Don't send our own notifications except notifications posted by SMSPlugin + String groupKey = ""; + + // SMS Notifications on devices running API's lower than Lollipop are not supported + // as groupKey's are not supported on API's older than Lollipop + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + groupKey = statusBarNotification.getGroupKey(); + } + + if (!groupKey.contains(NotificationReplyReceiver.SMS_NOTIFICATION_GROUP_KEY)) { + return; + } } NetworkPacket np = new NetworkPacket(PACKET_TYPE_NOTIFICATION); diff --git a/src/org/kde/kdeconnect/Plugins/SMSPlugin/MmsReceivedReceiver.java b/src/org/kde/kdeconnect/Plugins/SMSPlugin/MmsReceivedReceiver.java index 496f4659..396bc19e 100644 --- a/src/org/kde/kdeconnect/Plugins/SMSPlugin/MmsReceivedReceiver.java +++ b/src/org/kde/kdeconnect/Plugins/SMSPlugin/MmsReceivedReceiver.java @@ -24,17 +24,31 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Build; +import android.text.TextUtils; import android.util.Log; +import android.app.NotificationManager; +import android.app.PendingIntent; + +import androidx.core.app.NotificationCompat; +import androidx.core.app.Person; +import androidx.core.app.RemoteInput; +import androidx.core.content.ContextCompat; + import com.klinker.android.send_message.Transaction; import com.klinker.android.send_message.Utils; import org.kde.kdeconnect.Helpers.TelephonyHelper; +import org.kde.kdeconnect_tp.R; +import org.kde.kdeconnect.Helpers.NotificationHelper; +import org.kde.kdeconnect.Helpers.SMSHelper; + +import java.util.ArrayList; +import java.util.HashSet; /** * Receiver for notifying user when a new MMS has been received by the device. By default it will - * persist the message to the internal database and notification service to notify the users will be - * implemented later. + * persist the message to the internal database and it will also show a notification in the status bar. */ public class MmsReceivedReceiver extends com.klinker.android.send_message.MmsReceivedReceiver { @@ -47,11 +61,23 @@ public class MmsReceivedReceiver extends com.klinker.android.send_message.MmsRec // Notify messageUpdateReceiver about the arrival of the new MMS message Intent refreshIntent = new Intent(Transaction.REFRESH); context.sendBroadcast(refreshIntent); + + // Fetch the latest message from the database + SMSHelper.Message message = SMSHelper.getNewestMessage(context); + + // Notify the user about the received mms message + createMmsNotification(context, message); } @Override public void onError(Context context, String error) { Log.v("MmsReceived", "error: " + error); + + // Fetch the latest message from the database + SMSHelper.Message message = SMSHelper.getNewestMessage(context); + + // Notify the user about the received mms message + createMmsNotification(context, message); } public void getPreferredApn(Context context, Intent intent) { @@ -80,4 +106,72 @@ public class MmsReceivedReceiver extends com.klinker.android.send_message.MmsRec } return null; } + + private void createMmsNotification(Context context, SMSHelper.Message mmsMessage) { + ArrayList addressList = new ArrayList<>(); + for (SMSHelper.Address address : mmsMessage.addresses) { + addressList.add(address.toString()); + } + + Person sender = NotificationReplyReceiver.getMessageSender(context, addressList.get(0)); + + int notificationId; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + notificationId = (int) Utils.getOrCreateThreadId(context, new HashSet<>(addressList)); + } else { + notificationId = (int) System.currentTimeMillis(); + } + + // Todo: When SMSHelper.Message class will be modified to contain thumbnail of the image or video attachment, add them here to display. + + // Create pending intent for reply action through notification + PendingIntent replyPendingIntent = NotificationReplyReceiver.createReplyPendingIntent( + context, + addressList, + notificationId, + true + ); + + RemoteInput remoteReplyInput = new RemoteInput.Builder(NotificationReplyReceiver.KEY_TEXT_REPLY) + .setLabel(context.getString(R.string.message_reply_label)) + .build(); + + NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(0, context.getString(R.string.message_reply_label), replyPendingIntent) + .addRemoteInput(remoteReplyInput) + .setAllowGeneratedReplies(true) + .build(); + + // Create pending intent for marking the message as read in database through mark as read action + PendingIntent markAsReadPendingIntent = NotificationReplyReceiver.createMarkAsReadPendingIntent( + context, + addressList, + notificationId + ); + + NotificationCompat.Action markAsReadAction = new NotificationCompat.Action.Builder(0, context.getString(R.string.mark_as_read_label), markAsReadPendingIntent) + .build(); + + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + NotificationCompat.MessagingStyle messagingStyle = NotificationReplyReceiver.createMessagingStyle( + context, + notificationId, + mmsMessage.body, + TextUtils.join(",", addressList), + mmsMessage.date, + sender, + notificationManager + ); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NotificationReplyReceiver.CHANNEL_ID) + .setSmallIcon(R.drawable.ic_baseline_sms_24) + .setColor(ContextCompat.getColor(context, R.color.primary)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setStyle(messagingStyle) + .setAutoCancel(true) + .addAction(replyAction) + .addAction(markAsReadAction) + .setGroup(NotificationReplyReceiver.SMS_NOTIFICATION_GROUP_KEY); + + NotificationHelper.notifyCompat(notificationManager, notificationId, builder.build()); + } } diff --git a/src/org/kde/kdeconnect/Plugins/SMSPlugin/MmsSentReceiver.java b/src/org/kde/kdeconnect/Plugins/SMSPlugin/MmsSentReceiver.java index 3bf40b97..61879317 100644 --- a/src/org/kde/kdeconnect/Plugins/SMSPlugin/MmsSentReceiver.java +++ b/src/org/kde/kdeconnect/Plugins/SMSPlugin/MmsSentReceiver.java @@ -26,6 +26,10 @@ import android.content.Intent; import com.klinker.android.send_message.Transaction; import com.klinker.android.send_message.Utils; +import org.kde.kdeconnect.Helpers.SMSHelper; + +import java.util.ArrayList; + public class MmsSentReceiver extends com.klinker.android.send_message.MmsSentReceiver { @Override @@ -41,5 +45,23 @@ public class MmsSentReceiver extends com.klinker.android.send_message.MmsSentRec @Override public void onMessageStatusUpdated(Context context, Intent intent, int resultCode) { + SMSHelper.Message message = SMSHelper.getNewestMessage(context); + + ArrayList addressList = new ArrayList<>(); + for (SMSHelper.Address address : message.addresses) { + addressList.add(address.toString()); + } + + Intent repliedNotification = new Intent(context, NotificationReplyReceiver.class); + repliedNotification.setAction(NotificationReplyReceiver.SMS_MMS_REPLY_ACTION); + repliedNotification.putExtra(NotificationReplyReceiver.TEXT_BODY, message.body); + repliedNotification.putExtra(NotificationReplyReceiver.NOTIFICATION_ID, Integer.parseInt(message.threadID.toString())); + repliedNotification.putExtra(NotificationReplyReceiver.ADDRESS_LIST, addressList); + + // SEND_ACTION value is required to differentiate between the intents sent from reply action or + // SentReceivers inorder to avoid posting duplicate notifications + repliedNotification.putExtra(NotificationReplyReceiver.SEND_ACTION, false); + + context.sendBroadcast(repliedNotification); } } diff --git a/src/org/kde/kdeconnect/Plugins/SMSPlugin/NotificationReplyReceiver.java b/src/org/kde/kdeconnect/Plugins/SMSPlugin/NotificationReplyReceiver.java new file mode 100644 index 00000000..966d2175 --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/SMSPlugin/NotificationReplyReceiver.java @@ -0,0 +1,303 @@ +/* + * Copyright 2020 Aniket Kumar + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kde.kdeconnect.Plugins.SMSPlugin; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Build; +import android.os.Bundle; +import android.service.notification.StatusBarNotification; +import android.text.TextUtils; +import android.util.Base64; +import android.util.Log; + +import androidx.core.app.NotificationCompat; +import androidx.core.app.Person; +import androidx.core.app.RemoteInput; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.IconCompat; + +import com.klinker.android.send_message.Utils; + +import org.kde.kdeconnect.Helpers.ContactsHelper; +import org.kde.kdeconnect.Helpers.NotificationHelper; +import org.kde.kdeconnect.Helpers.SMSHelper; +import org.kde.kdeconnect_tp.R; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Map; + +public class NotificationReplyReceiver extends BroadcastReceiver { + public static final String SMS_MMS_REPLY_ACTION = "org.kde.kdeconnect.Plugins.SMSPlugin.sms_mms_reply_action"; + public static final String SMS_MMS_MARK_ACTION = "org.kde.kdeconnect.Plugins.SMSPlugin.sms_mms_mark_action"; + public static final String ADDRESS_LIST = "address_list"; + public static final String CHANNEL_ID = NotificationHelper.Channels.SMS_MMS; + public static final String KEY_TEXT_REPLY = "key_text_reply"; + public static final String NOTIFICATION_ID = "notification_id"; + public static final String SEND_ACTION = "send_action"; + public static final String TEXT_BODY = "text_body"; + public static final String SMS_NOTIFICATION_GROUP_KEY = "Plugins.SMSPlugin.sms_notification_group_key"; + + @Override + public void onReceive(Context context, Intent intent) { + if (!Utils.isDefaultSmsApp(context) || intent == null) { + return; + } + + Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); + + final int notificationId = intent.getIntExtra(NotificationReplyReceiver.NOTIFICATION_ID, 0); + final ArrayList addressList = intent.getStringArrayListExtra(ADDRESS_LIST); + final boolean sentUsingReplyButton = intent.getBooleanExtra(SEND_ACTION, false); + + if (intent.getAction().equals(SMS_MMS_REPLY_ACTION)) { + String inputString = null; + + if (sentUsingReplyButton) { + inputString = remoteInput.getCharSequence(NotificationReplyReceiver.KEY_TEXT_REPLY).toString(); + + ArrayList addresses = new ArrayList<>(); + for (String address : addressList) { + addresses.add(new SMSHelper.Address(address)); + } + SmsMmsUtils.sendMessage(context, inputString, addresses, -1); + } else { + inputString = intent.getStringExtra(TEXT_BODY); + repliedMessageNotification(context, notificationId, inputString, addressList); + + } + } + + // Mark the conversation as read + if (intent.getAction().equals(SMS_MMS_MARK_ACTION)) { + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(notificationId); + SmsMmsUtils.markConversationRead(context, new HashSet<>(addressList)); + } + } + + /** + * Updates the active notification with the newly replied message + */ + private void repliedMessageNotification(Context context, int notificationId, String inputString, ArrayList addressList) { + Person sender = new Person.Builder() + .setName(context.getString(R.string.user_display_name)) + .build(); + + // Create pending intent for reply action through notification + PendingIntent replyPendingIntent = createReplyPendingIntent(context, addressList, notificationId, true); + + RemoteInput remoteReplyInput = new RemoteInput.Builder(NotificationReplyReceiver.KEY_TEXT_REPLY) + .setLabel(context.getString(R.string.message_reply_label)) + .build(); + + NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(0, context.getString(R.string.message_reply_label), replyPendingIntent) + .addRemoteInput(remoteReplyInput) + .setAllowGeneratedReplies(true) + .build(); + + // Create pending intent for marking the message as read in database through mark as read action + PendingIntent markAsReadPendingIntent = createMarkAsReadPendingIntent(context, addressList, notificationId); + + NotificationCompat.Action markAsReadAction = new NotificationCompat.Action.Builder(0, context.getString(R.string.mark_as_read_label), markAsReadPendingIntent) + .build(); + + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + NotificationCompat.MessagingStyle.Message message = new NotificationCompat.MessagingStyle.Message( + inputString, + System.currentTimeMillis(), + sender + ); + + NotificationCompat.MessagingStyle messagingStyle = restoreActiveMessagingStyle(notificationId, notificationManager); + + if (messagingStyle == null) { + // Return when there is no active notification in the statusBar with the above notificationId + return; + } + + messagingStyle.addMessage(message); + + NotificationCompat.Builder repliedNotification = new NotificationCompat.Builder(context, NotificationReplyReceiver.CHANNEL_ID) + .setSmallIcon(R.drawable.ic_baseline_sms_24) + .setColor(ContextCompat.getColor(context, R.color.primary)) + .setOnlyAlertOnce(true) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setStyle(messagingStyle) + .setAutoCancel(true) + .addAction(replyAction) + .addAction(markAsReadAction) + .setGroup(NotificationReplyReceiver.SMS_NOTIFICATION_GROUP_KEY); + + NotificationHelper.notifyCompat(notificationManager, notificationId, repliedNotification.build()); + } + + /** + * This method creates a new messaging style for newer conversations and if there is already an active notification + * of the same id, it just adds to the previous and returns the modified messagingStyle object. + */ + public static NotificationCompat.MessagingStyle createMessagingStyle( + Context context, + int notificationId, + String textMessage, + String phoneNumbers, + long date, + Person sender, + NotificationManager notificationManager + ) { + NotificationCompat.MessagingStyle messageStyle = NotificationReplyReceiver.restoreActiveMessagingStyle( + notificationId, + notificationManager + ); + + NotificationCompat.MessagingStyle.Message message = new NotificationCompat.MessagingStyle.Message( + textMessage, + date, + sender + ); + + if (messageStyle == null) { + // When no active notification is found for matching conversation create a new one + String senderName = phoneNumbers; + Map contactInfo = ContactsHelper.phoneNumberLookup(context, phoneNumbers); + + if (contactInfo.containsKey("name")) { + senderName = contactInfo.get("name"); + } + + messageStyle = new NotificationCompat.MessagingStyle(sender) + .setConversationTitle(senderName); + } + messageStyle.addMessage(message); + + return messageStyle; + } + + /** + * This method is responsible for searching the notification for same conversation ID and if there is an active notification + * of save ID found in the status menu it extracts and returns the messagingStyle object of that notification + */ + public static NotificationCompat.MessagingStyle restoreActiveMessagingStyle( + int notificationId, + NotificationManager notificationManager + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + StatusBarNotification notifications[] = notificationManager.getActiveNotifications(); + for (StatusBarNotification notification : notifications) { + if (notification.getId() == notificationId) { + return NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(notification.getNotification()); + } + } + } + return null; + } + + /** + * returns the sender of the message as a Person object + */ + public static Person getMessageSender(Context context, String address) { + Map contactInfo = ContactsHelper.phoneNumberLookup(context, address); + String senderName = address; + + if (contactInfo.containsKey("name")) { + senderName = contactInfo.get("name"); + } + + Bitmap contactPhoto = null; + if (contactInfo.containsKey("photoID")) { + String photoUri = contactInfo.get("photoID"); + if (photoUri != null) { + try { + String base64photo = ContactsHelper.photoId64Encoded(context, photoUri); + if (!TextUtils.isEmpty(base64photo)) { + byte[] decodedString = Base64.decode(base64photo, Base64.DEFAULT); + contactPhoto = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length); + } + } catch (Exception e) { + Log.e("SMS Notification", "Failed to get contact photo"); + } + } + } + + Person.Builder personBuilder = new Person.Builder() + .setName(senderName); + + if (contactPhoto != null) { + personBuilder.setIcon(IconCompat.createWithBitmap(contactPhoto)); + } + + return personBuilder.build(); + } + + /** + * Create pending intent for reply action through notification + */ + public static PendingIntent createReplyPendingIntent( + Context context, + ArrayList addressList, + int notificationId, + boolean isFromSendAction + ) { + Intent replyIntent = new Intent(context, NotificationReplyReceiver.class); + replyIntent.setAction(NotificationReplyReceiver.SMS_MMS_REPLY_ACTION); + replyIntent.putExtra(NotificationReplyReceiver.NOTIFICATION_ID, notificationId); + replyIntent.putExtra(NotificationReplyReceiver.ADDRESS_LIST, addressList); + replyIntent.putExtra(NotificationReplyReceiver.SEND_ACTION, isFromSendAction); + + PendingIntent replyPendingIntent = PendingIntent.getBroadcast( + context, + notificationId, + replyIntent, + PendingIntent.FLAG_UPDATE_CURRENT + ); + + return replyPendingIntent; + } + + /** + * Create pending intent for marking the message as read in database through mark as read action + */ + public static PendingIntent createMarkAsReadPendingIntent( + Context context, + ArrayList addressList, + int notificationId + ) { + Intent markAsReadIntent = new Intent(context, NotificationReplyReceiver.class); + markAsReadIntent.setAction(NotificationReplyReceiver.SMS_MMS_MARK_ACTION); + markAsReadIntent.putExtra(NotificationReplyReceiver.NOTIFICATION_ID, notificationId); + markAsReadIntent.putExtra(NotificationReplyReceiver.ADDRESS_LIST, addressList); + + PendingIntent markAsReadPendingIntent = PendingIntent.getBroadcast( + context, + notificationId, + markAsReadIntent, + PendingIntent.FLAG_CANCEL_CURRENT + ); + + return markAsReadPendingIntent; + } +} diff --git a/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsMmsUtils.java b/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsMmsUtils.java index 3ed53ac6..badff29b 100644 --- a/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsMmsUtils.java +++ b/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsMmsUtils.java @@ -32,12 +32,21 @@ import com.klinker.android.send_message.Settings; import com.klinker.android.send_message.Transaction; import com.klinker.android.send_message.Utils; +import android.content.ContentUris; +import android.content.ContentValues; +import android.provider.Telephony; +import android.net.Uri; +import android.util.Log; + +import androidx.annotation.RequiresApi; + import org.apache.commons.lang3.ArrayUtils; import org.kde.kdeconnect.Helpers.SMSHelper; import org.kde.kdeconnect.Helpers.TelephonyHelper; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; public class SmsMmsUtils { @@ -119,4 +128,38 @@ public class SmsMmsUtils { com.klinker.android.logger.Log.e(SENDING_MESSAGE, "Exception", e); } } + + /** + * Marks a conversation as read in the database. + * + * @param context the context to get the content provider with. + * @param recipients the phone numbers to find the conversation with. + */ + public static void markConversationRead(Context context, HashSet recipients) { + new Thread() { + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + @Override + public void run() { + try { + long threadId = Utils.getOrCreateThreadId(context, recipients); + markAsRead(context, ContentUris.withAppendedId(Telephony.Threads.CONTENT_URI, threadId), threadId); + } catch (Exception e) { + // the conversation doesn't exist + e.printStackTrace(); + } + } + }.start(); + } + + private static void markAsRead(Context context, Uri uri, long threadId) { + Log.v("SMSPlugin", "marking thread with threadId " + threadId + " as read at Uri" + uri); + + if (uri != null && context != null) { + ContentValues values = new ContentValues(2); + values.put("read", 1); + values.put("seen", 1); + + context.getContentResolver().update(uri, values, "(read=0 OR seen=0)", null); + } + } } diff --git a/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsReceiver.java b/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsReceiver.java index 63a97de1..57821175 100644 --- a/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsReceiver.java +++ b/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsReceiver.java @@ -20,6 +20,8 @@ package org.kde.kdeconnect.Plugins.SMSPlugin; +import android.app.NotificationManager; +import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -29,10 +31,22 @@ import android.telephony.SmsMessage; import android.provider.Telephony.Sms; import android.net.Uri; import android.content.ContentValues; +import android.util.Log; + +import androidx.core.app.NotificationCompat; +import androidx.core.app.Person; +import androidx.core.app.RemoteInput; +import androidx.core.content.ContextCompat; import com.klinker.android.send_message.Transaction; import com.klinker.android.send_message.Utils; +import org.kde.kdeconnect.Helpers.NotificationHelper; +import org.kde.kdeconnect_tp.R; + +import java.util.ArrayList; +import java.util.Arrays; + public class SmsReceiver extends BroadcastReceiver { private static final String SMS_RECEIVED = "android.provider.Telephony.SMS_RECEIVED"; @@ -75,8 +89,77 @@ public class SmsReceiver extends BroadcastReceiver { Intent refreshIntent = new Intent(Transaction.REFRESH); context.sendBroadcast(refreshIntent); } + + String body = message[i].getMessageBody(); + String phoneNo = message[i].getOriginatingAddress(); + long date = message[i].getTimestampMillis(); + + createSmsNotification(context, body, phoneNo, date); } } } } + + private void createSmsNotification(Context context, String body, String phoneNo, long date) { + int notificationId; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + notificationId = (int) Utils.getOrCreateThreadId(context, phoneNo); + } else { + notificationId = (int) System.currentTimeMillis(); + } + + Person sender = NotificationReplyReceiver.getMessageSender(context, phoneNo); + + ArrayList addressList = new ArrayList<>(Arrays.asList(phoneNo)); + + // Create pending intent for reply action through notification + PendingIntent replyPendingIntent = NotificationReplyReceiver.createReplyPendingIntent( + context, + addressList, + notificationId, + true + ); + + RemoteInput remoteReplyInput = new RemoteInput.Builder(NotificationReplyReceiver.KEY_TEXT_REPLY) + .setLabel(context.getString(R.string.message_reply_label)) + .build(); + + NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(0, context.getString(R.string.message_reply_label), replyPendingIntent) + .addRemoteInput(remoteReplyInput) + .setAllowGeneratedReplies(true) + .build(); + + // Create pending intent for marking the message as read in database through mark as read action + PendingIntent markAsReadPendingIntent = NotificationReplyReceiver.createMarkAsReadPendingIntent( + context, + addressList, + notificationId + ); + + NotificationCompat.Action markAsReadAction = new NotificationCompat.Action.Builder(0, context.getString(R.string.mark_as_read_label), markAsReadPendingIntent) + .build(); + + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + NotificationCompat.MessagingStyle messagingStyle = NotificationReplyReceiver.createMessagingStyle( + context, + notificationId, + body, + phoneNo, + date, + sender, + notificationManager + ); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NotificationReplyReceiver.CHANNEL_ID) + .setSmallIcon(R.drawable.ic_baseline_sms_24) + .setColor(ContextCompat.getColor(context, R.color.primary)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setStyle(messagingStyle) + .setAutoCancel(true) + .addAction(replyAction) + .addAction(markAsReadAction) + .setGroup(NotificationReplyReceiver.SMS_NOTIFICATION_GROUP_KEY); + + NotificationHelper.notifyCompat(notificationManager, notificationId, builder.build()); + } } diff --git a/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsSentReceiver.java b/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsSentReceiver.java index b942ca81..c3628c9f 100644 --- a/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsSentReceiver.java +++ b/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsSentReceiver.java @@ -27,6 +27,10 @@ import com.klinker.android.send_message.SentReceiver; import com.klinker.android.send_message.Transaction; import com.klinker.android.send_message.Utils; +import org.kde.kdeconnect.Helpers.SMSHelper; + +import java.util.ArrayList; + public class SmsSentReceiver extends SentReceiver { @Override @@ -42,5 +46,23 @@ public class SmsSentReceiver extends SentReceiver { @Override public void onMessageStatusUpdated(Context context, Intent intent, int receiverResultCode) { + SMSHelper.Message message = SMSHelper.getNewestMessage(context); + + ArrayList addressList = new ArrayList<>(); + for (SMSHelper.Address address : message.addresses) { + addressList.add(address.toString()); + } + + Intent repliedNotification = new Intent(context, NotificationReplyReceiver.class); + repliedNotification.setAction(NotificationReplyReceiver.SMS_MMS_REPLY_ACTION); + repliedNotification.putExtra(NotificationReplyReceiver.TEXT_BODY, message.body); + repliedNotification.putExtra(NotificationReplyReceiver.NOTIFICATION_ID, Integer.parseInt(message.threadID.toString())); + repliedNotification.putExtra(NotificationReplyReceiver.ADDRESS_LIST, addressList); + + // SEND_ACTION value is required to differentiate between the intents sent from reply action or + // SentReceivers inorder to avoid posting duplicate notifications + repliedNotification.putExtra(NotificationReplyReceiver.SEND_ACTION, false); + + context.sendBroadcast(repliedNotification); } }