From 51e957d82290bc5d07c01bcd55e03c17868ca14c Mon Sep 17 00:00:00 2001 From: Simon Redman Date: Mon, 10 Jun 2019 05:48:28 +0000 Subject: [PATCH] [SMSApp] Support plain-text MMS ## Summary Not having support for MMS caused some minor problems, like in https://bugs.kde.org/show_bug.cgi?id=398889 . This patch adds basic MMS support for plain-text MMS, including multi-target messages. Android companion to https://invent.kde.org/kde/kdeconnect-kde/merge_requests/97 Currently there are several rough areas: - Multi-target messages do not have the full list of recipients (I am planning to work on this in another patch, because this one is already quite large enough) - Parsing MMS is significantly slower than parsing SMS. This makes sense, since we need to make significantly many more content:// calls for MMS. The only solution I can think of here is to add the ability to request a range of messages, which I need to do anyway, but which should not be part of this patch. - The desktop app is totally busted with regard to multi-target MMS, but that will also be fixed in another MR BUG: 398889 ## Test Plan ### Before: Open SMS app on desktop, scroll through conversations, notice: - Any single-target message which had the most-recent message as an MMS does not appear - Any multi-target MMS conversations do not appear ### After: Open SMS app on desktop, notice: - Conversations which have an MMS as their most-recent message appear - MMS which consisted of only text are rendered correctly - Multi-target conversations are shown (though pretty busted, as said before. Do not attempt to reply to one!) --- src/org/kde/kdeconnect/Helpers/SMSHelper.java | 466 +++++++++++++++--- .../Plugins/SMSPlugin/SMSPlugin.java | 16 +- 2 files changed, 412 insertions(+), 70 deletions(-) diff --git a/src/org/kde/kdeconnect/Helpers/SMSHelper.java b/src/org/kde/kdeconnect/Helpers/SMSHelper.java index 7f1f5047..49c909fc 100644 --- a/src/org/kde/kdeconnect/Helpers/SMSHelper.java +++ b/src/org/kde/kdeconnect/Helpers/SMSHelper.java @@ -21,9 +21,11 @@ package org.kde.kdeconnect.Helpers; import android.annotation.SuppressLint; +import android.content.ContentUris; import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; +import android.database.sqlite.SQLiteException; import android.net.Uri; import android.os.Build; import android.os.Looper; @@ -33,15 +35,23 @@ import android.util.Log; import org.json.JSONException; import org.json.JSONObject; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @SuppressLint("InlinedApi") @@ -64,7 +74,6 @@ public class SMSHelper { */ @RequiresApi(Build.VERSION_CODES.KITKAT) private static Uri getSMSURIGood() { - // TODO: Why not use Telephony.MmsSms.CONTENT_URI? return Telephony.Sms.CONTENT_URI; } @@ -76,6 +85,21 @@ public class SMSHelper { } } + private static Uri getMMSUri() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + return Telephony.Mms.CONTENT_URI; + } else { + // Same as with getSMSUriBad, this is unsafe if the manufacturer did their own thing + // before this was part of the API + return Uri.parse("content://mms/"); + } + } + + private static Uri getMMSPartUri() { + // Android says we should have Telephony.Mms.Part.CONTENT_URI. Alas, we do not. + return Uri.parse("content://mms/part/"); + } + /** * Get the base address for all message conversations */ @@ -89,6 +113,26 @@ public class SMSHelper { } } + @RequiresApi(api = Build.VERSION_CODES.FROYO) + private static Uri getCompleteConversationsUri() { + // This glorious - but completely undocumented - content URI gives us all messages, both MMS and SMS, + // in all conversations + // See https://stackoverflow.com/a/36439630/3723163 + return Uri.parse("content://mms-sms/complete-conversations"); + } + + /** + * Column used to discriminate between SMS and MMS messages + * Unfortunately, this column is not defined for Telephony.MmsSms.CONTENT_CONVERSATIONS_URI + * (aka. content://mms-sms/conversations) + * which gives us the first message in every conversation, but it *is* defined for + * content://mms-sms/conversations/ which gives us the complete conversation matching + * that threadID, so at least it's partially useful to us. + */ + private static String getTransportTypeDiscriminatorColumn() { + return Telephony.MmsSms.TYPE_DISCRIMINATOR_COLUMN; + } + /** * Get all the messages in a requested thread * @@ -96,50 +140,108 @@ public class SMSHelper { * @param threadID Thread to look up * @return List of all messages in the thread */ - public static List getMessagesInThread(Context context, ThreadID threadID) { - final String selection = ThreadID.lookupColumn + " == ?"; - final String[] selectionArgs = new String[] { threadID.toString() }; + public static @NonNull List getMessagesInThread( + @NonNull Context context, + @NonNull ThreadID threadID + ) { + Uri uri = Uri.withAppendedPath(getConversationUri(), threadID.toString()); - return getMessagesWithFilter(context, selection, selectionArgs); + return getMessages(uri, context, null, null, null, null); } /** - * Get all messages which have a timestamp after the requested timestamp + * Get the newest sent or received message + * + * This might have some potential for race conditions if many messages are received in a short + * timespan, but my target use-case is humans sending and receiving messages, so I don't think + * it will be an issue * - * @param timestamp epoch in millis matching the timestamp to return * @return null if no matching message is found, otherwise return a Message */ - public static List getMessagesSinceTimestamp(Context context, long timestamp) { - final String selection = Message.DATE + " > ?"; - final String[] selectionArgs = new String[] {Long.toString(timestamp)}; + public static @Nullable Message getNewestMessage( + @NonNull Context context + ) { + List messages = getMessagesWithFilter(context, null, null, 1L); - return getMessagesWithFilter(context, selection, selectionArgs); + if (messages.size() > 1) { + Log.w("SMSHelper", "getNewestMessage asked for one message but got " + messages.size()); + } + if (messages.size() < 1) { + return null; + } else { + return messages.get(0); + } } /** - * Gets Messages for caller functions, such as: getMessagesWithFilter() and getConversations() + * Gets messages which match the selection * - * @param Uri Uri indicating the messages database to read + * @param uri Uri indicating the messages database to read * @param context android.content.Context running the request. * @param selection Parameterizable filter to use with the ContentResolver query. May be null. * @param selectionArgs Parameters for selection. May be null. - * @return Returns HashMap>, which is transformed in caller functions into other classes. + * @param sortOrder Sort ordering passed to Android's content resolver. May be null for unspecified + * @param numberToGet Number of things to get from the result. Pass null to get all + * @return Returns List of all messages in the return set, either in the order of sortOrder or in an unspecified order */ - private static HashMap> getMessages(Uri Uri, - Context context, - String selection, - String[] selectionArgs) { - HashMap> toReturn = new HashMap<>(); - try (Cursor myCursor = context.getContentResolver().query( - Uri, - Message.smsColumns, + private static @NonNull List getMessages( + @NonNull Uri uri, + @NonNull Context context, + @Nullable String selection, + @Nullable String[] selectionArgs, + @Nullable String sortOrder, + @Nullable Long numberToGet + ) { + List toReturn = new ArrayList<>(); + + Set allColumns = new HashSet<>(); + allColumns.addAll(Arrays.asList(Message.smsColumns)); + allColumns.addAll(Arrays.asList(Message.mmsColumns)); + + if (uri != getConversationUri()) { + // See https://issuetracker.google.com/issues/134592631 + allColumns.add(getTransportTypeDiscriminatorColumn()); + } + + String[] fetchColumns = {}; + fetchColumns = allColumns.toArray(fetchColumns); + try (Cursor myCursor = context.getContentResolver().query( + uri, + fetchColumns, selection, selectionArgs, - null) + sortOrder) ) { if (myCursor != null && myCursor.moveToFirst()) { - int threadColumn = myCursor.getColumnIndexOrThrow(ThreadID.lookupColumn); do { + int transportTypeColumn = myCursor.getColumnIndex(getTransportTypeDiscriminatorColumn()); + + TransportType transportType; + if (transportTypeColumn < 0) { + // The column didn't actually exist. See https://issuetracker.google.com/issues/134592631 + // Try to determine using other information + int messageBoxColumn = myCursor.getColumnIndex(Telephony.Mms.MESSAGE_BOX); + // MessageBoxColumn is defined for MMS only + boolean messageBoxExists = !myCursor.isNull(messageBoxColumn); + if (messageBoxExists) { + transportType = TransportType.MMS; + } else { + // There is room here for me to have made an assumption and we'll guess wrong + // The penalty is the user will potentially get some garbled data, so that's not too bad. + transportType = TransportType.SMS; + } + } else { + String transportTypeString = myCursor.getString(transportTypeColumn); + if ("mms".equals(transportTypeString)) { + transportType = TransportType.MMS; + } else if ("sms".equals(transportTypeString)) { + transportType = TransportType.SMS; + } else { + Log.w("SMSHelper", "Skipping message with unknown TransportType: " + transportTypeString); + continue; + } + } + HashMap messageInfo = new HashMap<>(); for (int columnIdx = 0; columnIdx < myCursor.getColumnCount(); columnIdx++) { String colName = myCursor.getColumnName(columnIdx); @@ -147,37 +249,41 @@ public class SMSHelper { messageInfo.put(colName, body); } - Message message = new Message(messageInfo); - ThreadID threadID = new ThreadID(message.threadID); - - if (!toReturn.containsKey(threadID)) { - toReturn.put(threadID, new ArrayList<>()); + if (transportType == TransportType.SMS) { + parseSMS(context, messageInfo); + } else if (transportType == TransportType.MMS) { + parseMMS(context, messageInfo); } - toReturn.get(threadID).add(message); - } while (myCursor.moveToNext()); - } else { - // No conversations or SMSes available? + + Message message = new Message(messageInfo); + + toReturn.add(message); + } while ((numberToGet == null || toReturn.size() != numberToGet) && myCursor.moveToNext()); } + } catch (SQLiteException e) { + throw new MessageAccessException(fetchColumns, uri, e); } return toReturn; } - + /** * Get all messages matching the passed filter. See documentation for Android's ContentResolver * * @param context android.content.Context running the request * @param selection Parameterizable filter to use with the ContentResolver query. May be null. * @param selectionArgs Parameters for selection. May be null. - * @return List of messages matching the filter + * @param numberToGet Number of things to return. Pass null to get all + * @return List of messages matching the filter, from newest to oldest */ - private static List getMessagesWithFilter(Context context, String selection, String[] selectionArgs) { - HashMap> result = getMessages(SMSHelper.getSMSUri(), context, selection, selectionArgs); - List toReturn = new ArrayList<>(); + private static List getMessagesWithFilter( + @NonNull Context context, + @Nullable String selection, + @Nullable String[] selectionArgs, + @Nullable Long numberToGet + ) { + String sortOrder = Message.DATE + " DESC"; - for(Map.Entry> entry : result.entrySet()) { - toReturn.addAll(entry.getValue()); - } - return toReturn; + return getMessages(getCompleteConversationsUri(), context, selection, selectionArgs, sortOrder, numberToGet); } /** @@ -187,27 +293,226 @@ public class SMSHelper { * @param context android.content.Context running the request * @return Mapping of thread_id to the first message in each thread */ - public static Map getConversations(Context context) { - HashMap> result = getMessages(SMSHelper.getConversationUri(), context, null, null); - HashMap toReturn = new HashMap<>(); + public static Map getConversations( + @NonNull Context context + ) { + Uri uri = SMSHelper.getConversationUri(); - for(Map.Entry> entry : result.entrySet()) { - ThreadID returnThreadID = entry.getKey(); - List messages = entry.getValue(); + List unthreadedMessages = getMessages(uri, context, null, null, null, null); - toReturn.put(returnThreadID, messages.get(0)); + Map toReturn = new HashMap<>(); + + for (Message message : unthreadedMessages) { + ThreadID tID = message.threadID; + + if (toReturn.containsKey(tID)) { + Log.w("SMSHelper", "getConversations got two messages for the same ThreadID: " + tID); + } + + toReturn.put(tID, message); } return toReturn; } + private static void addEventFlag( + @NonNull Map messageInfo, + @NonNull int eventFlag + ) { + int oldEvent = Integer.parseInt(messageInfo.getOrDefault(Message.EVENT, "0")); + messageInfo.put(Message.EVENT, Integer.toString(oldEvent | eventFlag)); + } + + /** + * Do any parsing of an SMS message which still needs to be done + */ + private static void parseSMS( + @NonNull Context context, + @NonNull Map messageInfo + ) { + addEventFlag(messageInfo, Message.EVENT_TEXT_MESSAGE); + } + + /** + * Parse all parts of the MMS message into the messageInfo format + * Original implementation from https://stackoverflow.com/a/6446831/3723163 + */ + private static void parseMMS( + @NonNull Context context, + @NonNull Map messageInfo + ) { + addEventFlag(messageInfo, Message.EVENT_UNKNOWN); + + String[] columns = { + Telephony.Mms.Part._ID, // The content ID of this part + Telephony.Mms.Part._DATA, // The location in the filesystem of the data + Telephony.Mms.Part.CONTENT_TYPE, // The mime type of the data + Telephony.Mms.Part.TEXT, // The plain text body of this MMS + Telephony.Mms.Part.CHARSET, // Charset of the plain text body + }; + + String mmsID = messageInfo.get(Message.U_ID); + String selection = Telephony.Mms.Part.MSG_ID + " = ?"; + String[] selectionArgs = {mmsID}; + + // Get text body and attachments of the message + try (Cursor cursor = context.getContentResolver().query( + getMMSPartUri(), + columns, + selection, + selectionArgs, + null + )) { + if (cursor != null && cursor.moveToFirst()) { + int partIDColumn = cursor.getColumnIndexOrThrow(Telephony.Mms.Part._ID); + int contentTypeColumn = cursor.getColumnIndexOrThrow(Telephony.Mms.Part.CONTENT_TYPE); + int dataColumn = cursor.getColumnIndexOrThrow(Telephony.Mms.Part._DATA); + int textColumn = cursor.getColumnIndexOrThrow(Telephony.Mms.Part.TEXT); + // TODO: Parse charset (As usual, it is skimpily documented) (Possibly refer to MMS spec) + + do { + Long partID = cursor.getLong(partIDColumn); + String contentType = cursor.getString(contentTypeColumn); + String data = cursor.getString(dataColumn); + if ("text/plain".equals(contentType)) { + String body; + if (data != null) { + // data != null means the data is on disk. Go get it. + body = getMmsText(context, partID); + } else { + body = cursor.getString(textColumn); + } + messageInfo.put(Message.BODY, body); + addEventFlag(messageInfo, Message.EVENT_TEXT_MESSAGE); + } //TODO: Parse more content types (photos and other attachments) here + + } while (cursor.moveToNext()); + } + } + + // Determine whether the message was in- our out- bound + long messageBox = Long.parseLong(messageInfo.get(Telephony.Mms.MESSAGE_BOX)); + if (messageBox == Telephony.Mms.MESSAGE_BOX_INBOX) { + messageInfo.put(Message.TYPE, Integer.toString(Telephony.Sms.MESSAGE_TYPE_INBOX)); + } else if (messageBox == Telephony.Mms.MESSAGE_BOX_SENT) { + messageInfo.put(Message.TYPE, Integer.toString(Telephony.Sms.MESSAGE_TYPE_SENT)); + } else { + // As an undocumented feature, it looks like the values of Mms.MESSAGE_BOX_* + // are the same as Sms.MESSAGE_TYPE_* of the same type. So by default let's just use + // the value we've got. + // This includes things like drafts, which are a far-distant plan to support + messageInfo.put(Message.TYPE, messageInfo.get(Telephony.Mms.MESSAGE_BOX)); + } + + // Get address(es) of the message + List addresses = getMmsAddresses(context, Long.parseLong(mmsID)); + // It looks like addresses[0] is always the sender of the message and + // following addresses are recipient(s) + // This usually means the addresses list is at least 2 long, but there are cases (special + // telco service messages) where it is not (only 1 long in that case, just the "sender") + + // The address field which will get written to the message. + // Remember that this is always the address of the other side of the conversation + String address = ""; + + if (addresses.size() > 2) { + // TODO: Collect addresses for multi-target MMS + // Probably we will need to figure out the user's address at this point and strip it out of the list + addEventFlag(messageInfo, Message.EVENT_MULTI_TARGET); + } else { + if (messageBox == Telephony.Mms.MESSAGE_BOX_INBOX) { + address = addresses.get(0); + } else if (messageBox == Telephony.Mms.MESSAGE_BOX_SENT) { + address = addresses.get(1); + } else { + Log.w("SMSHelper", "Unknown message type " + messageBox + " while parsing addresses."); + // Not much smart to do here. Just leave as default. + } + } + messageInfo.put(Message.ADDRESS, address); + + // Canonicalize the date field + // SMS uses epoch milliseconds, MMS uses epoch seconds. Standardize on milliseconds. + long rawDate = Long.parseLong(messageInfo.get(Message.DATE)); + messageInfo.put(Message.DATE, Long.toString(rawDate * 1000)); + } + + /** + * Get the address(es) of an MMS message + * Original implementation from https://stackoverflow.com/a/6446831/3723163 + */ + private static @NonNull List getMmsAddresses( + @NonNull Context context, + @NonNull Long messageID + ) { + Uri uri = ContentUris.appendId(getMMSUri().buildUpon(), messageID).appendPath("addr").build(); + + String[] columns = { + Telephony.Mms.Addr.MSG_ID, // ID of the message for which we are fetching addresses + Telephony.Mms.Addr.ADDRESS, // Address of this part + Telephony.Mms.Addr.CHARSET, // Charset of the returned address (where relevant) //TODO: Handle + }; + + String selection = Telephony.Mms.Addr.MSG_ID + " = ?"; + String[] selectionArgs = {messageID.toString()}; + + List addresses = new ArrayList<>(); + + try (Cursor addrCursor = context.getContentResolver().query( + uri, + columns, + selection, + selectionArgs, + null + )) { + if (addrCursor != null && addrCursor.moveToFirst()) { + int addressIndex = addrCursor.getColumnIndex(Telephony.Mms.Addr.ADDRESS); + + do { + String address = addrCursor.getString(addressIndex); + addresses.add(address); + } while (addrCursor.moveToNext()); + } + } + return addresses; + } + + /** + * Get a text part of an MMS message + * Original implementation from https://stackoverflow.com/a/6446831/3723163 + */ + private static String getMmsText( + @NonNull Context context, + @NonNull Long id + ) { + Uri partURI = ContentUris.withAppendedId(getMMSPartUri(), id); + StringBuilder body = new StringBuilder(); + try (InputStream is = context.getContentResolver().openInputStream(partURI)) { + if (is != null) { + InputStreamReader isr = new InputStreamReader(is, "UTF-8"); + BufferedReader reader = new BufferedReader(isr); + String temp = reader.readLine(); + while (temp != null) { + body.append(temp); + temp = reader.readLine(); + } + } + } catch (IOException e) { + throw new SMSHelper.MessageAccessException(partURI, e); + } + return body.toString(); + } + /** * Register a ContentObserver for the Messages database * * @param observer ContentObserver to alert on Message changes */ - public static void registerObserver(ContentObserver observer, Context context) { + public static void registerObserver( + @NonNull ContentObserver observer, + @NonNull Context context + ) { context.getContentResolver().registerContentObserver( - SMSHelper.getSMSUri(), + SMSHelper.getConversationUri(), true, observer ); @@ -240,6 +545,29 @@ public class SMSHelper { } } + /** + * Indicate that some error has occurred while reading a message. + * More useful for logging than catching and handling + */ + public static class MessageAccessException extends RuntimeException { + MessageAccessException(Uri uri, Throwable cause) { + super("Error getting messages from " + uri.toString(), cause); + } + + MessageAccessException(String[] availableColumns, Uri uri, Throwable cause) { + super("Error getting messages from " + uri.toString() + " . Available columns were: " + Arrays.toString(availableColumns), cause); + } + } + + /** + * Represent all known transport types + */ + public enum TransportType { + SMS, + MMS, + // Maybe in the future there will be more TransportType, but for now these are all I know about + } + /** * Represent a message and all of its interesting data columns */ @@ -250,8 +578,10 @@ public class SMSHelper { public final long date; final int type; final int read; - final long threadID; // ThreadID is *int* for SMS messages but *long* for MMS - final int uID; + final ThreadID threadID; // ThreadID is *int* for SMS messages but *long* for MMS + final long uID; + final int event; + final int subscriptionID; /** * Named constants which are used to construct a Message @@ -263,15 +593,19 @@ public class SMSHelper { static final String TYPE = Telephony.Sms.TYPE; // Compare with Telephony.TextBasedSmsColumns.MESSAGE_TYPE_* static final String READ = Telephony.Sms.READ; // Whether we have received a read report for this message (int) static final String THREAD_ID = ThreadID.lookupColumn; // Magic number which binds (message) threads - static final String U_ID = Telephony.Sms._ID; // Something which uniquely identifies this message + static final String U_ID = Telephony.Sms._ID; // Something which uniquely identifies this message + static final String EVENT = "event"; + static final String SUBSCRIPTION_ID = Telephony.Sms.SUBSCRIPTION_ID; // An ID which appears to identify a SIM card /** * Event flags * A message should have a bitwise-or of event flags before delivering the packet * Any events not supported by the receiving device should be ignored */ - public static final int TEXT_MESSAGE = 0x1; // This message has a "body" field which contains - // pure, human-readable text + public static final int EVENT_UNKNOWN = 0x0; // The message was of some type we did not understand + public static final int EVENT_TEXT_MESSAGE = 0x1; // This message has a "body" field which contains + // pure, human-readable text + public static final int EVENT_MULTI_TARGET = 0x2; // Indicates that this message has multiple recipients /** * Define the columns which are to be extracted from the Android SMS database @@ -284,6 +618,16 @@ public class SMSHelper { Message.READ, Message.THREAD_ID, Message.U_ID, + Message.SUBSCRIPTION_ID, + }; + + static final String[] mmsColumns = new String[]{ + Message.U_ID, + Message.THREAD_ID, + Message.DATE, + Message.READ, + Telephony.Mms.TEXT_ONLY, + Telephony.Mms.MESSAGE_BOX, // Compare with Telephony.BaseMmsColumns.MESSAGE_BOX_* }; Message(final HashMap messageInfo) { @@ -293,15 +637,17 @@ public class SMSHelper { if (messageInfo.get(Message.TYPE) == null) { // To be honest, I have no idea why this happens. The docs say the TYPE field is mandatory. - // Just stick some junk in here and hope we can figure it out later. - // Quick investigation suggests that these are multi-target MMSes + Log.w("SMSHelper", "Encountered undefined message type"); type = -1; + // Proceed anyway, maybe this is not an important problem. } else { type = Integer.parseInt(messageInfo.get(Message.TYPE)); } read = Integer.parseInt(messageInfo.get(Message.READ)); - threadID = Long.parseLong(messageInfo.get(Message.THREAD_ID)); + threadID = new ThreadID(Long.parseLong(messageInfo.get(Message.THREAD_ID))); uID = Integer.parseInt(messageInfo.get(Message.U_ID)); + subscriptionID = Integer.parseInt(messageInfo.get(Message.SUBSCRIPTION_ID)); + event = Integer.parseInt(messageInfo.get(Message.EVENT)); } public JSONObject toJSONObject() throws JSONException { @@ -314,6 +660,8 @@ public class SMSHelper { json.put(Message.READ, read); json.put(Message.THREAD_ID, threadID); json.put(Message.U_ID, uID); + json.put(Message.SUBSCRIPTION_ID, subscriptionID); + json.put(Message.EVENT, event); return json; } diff --git a/src/org/kde/kdeconnect/Plugins/SMSPlugin/SMSPlugin.java b/src/org/kde/kdeconnect/Plugins/SMSPlugin/SMSPlugin.java index 92e87130..4fa8392a 100644 --- a/src/org/kde/kdeconnect/Plugins/SMSPlugin/SMSPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/SMSPlugin/SMSPlugin.java @@ -55,12 +55,12 @@ import org.kde.kdeconnect_tp.R; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import androidx.annotation.RequiresApi; import androidx.core.content.ContextCompat; import static org.kde.kdeconnect.Plugins.TelephonyPlugin.TelephonyPlugin.PACKET_TYPE_TELEPHONY; @@ -197,9 +197,9 @@ public class SMSPlugin extends Plugin { long mostRecentTimestamp = mPlugin.mostRecentTimestamp; mostRecentTimestampLock.unlock(); - List messages = SMSHelper.getMessagesSinceTimestamp(mPlugin.context, mostRecentTimestamp); + SMSHelper.Message message = SMSHelper.getNewestMessage(mPlugin.context); - if (messages.size() == 0) { + if (message.date <= mostRecentTimestamp) { // Our onChange often gets called many times for a single message. Don't make unnecessary // noise return; @@ -207,15 +207,11 @@ public class SMSPlugin extends Plugin { // Update the most recent counter mostRecentTimestampLock.lock(); - for (SMSHelper.Message message : messages) { - if (message.date > mostRecentTimestamp) { - mPlugin.mostRecentTimestamp = message.date; - } - } + mPlugin.mostRecentTimestamp = message.date; mostRecentTimestampLock.unlock(); // Send the alert about the update - device.sendPacket(constructBulkMessagePacket(messages)); + device.sendPacket(constructBulkMessagePacket(Collections.singleton(message))); } } @@ -352,8 +348,6 @@ public class SMSPlugin extends Plugin { try { JSONObject json = message.toJSONObject(); - json.put("event", SMSHelper.Message.TEXT_MESSAGE); - body.put(json); } catch (JSONException e) { Log.e("Conversations", "Error serializing message");