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);
}
}