mirror of
https://github.com/KDE/kdeconnect-android
synced 2025-08-28 20:57:42 +00:00
[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!)
This commit is contained in:
parent
ec43336153
commit
51e957d822
@ -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/<threadID> 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<Message> getMessagesInThread(Context context, ThreadID threadID) {
|
||||
final String selection = ThreadID.lookupColumn + " == ?";
|
||||
final String[] selectionArgs = new String[] { threadID.toString() };
|
||||
public static @NonNull List<Message> 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<Message> 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<Message> 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<ThreadID, List<Message>>, 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<Message> of all messages in the return set, either in the order of sortOrder or in an unspecified order
|
||||
*/
|
||||
private static HashMap<ThreadID, List<Message>> getMessages(Uri Uri,
|
||||
Context context,
|
||||
String selection,
|
||||
String[] selectionArgs) {
|
||||
HashMap<ThreadID, List<Message>> toReturn = new HashMap<>();
|
||||
try (Cursor myCursor = context.getContentResolver().query(
|
||||
Uri,
|
||||
Message.smsColumns,
|
||||
private static @NonNull List<Message> getMessages(
|
||||
@NonNull Uri uri,
|
||||
@NonNull Context context,
|
||||
@Nullable String selection,
|
||||
@Nullable String[] selectionArgs,
|
||||
@Nullable String sortOrder,
|
||||
@Nullable Long numberToGet
|
||||
) {
|
||||
List<Message> toReturn = new ArrayList<>();
|
||||
|
||||
Set<String> 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<String, String> messageInfo = new HashMap<>();
|
||||
for (int columnIdx = 0; columnIdx < myCursor.getColumnCount(); columnIdx++) {
|
||||
String colName = myCursor.getColumnName(columnIdx);
|
||||
@ -147,17 +249,19 @@ 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;
|
||||
}
|
||||
@ -168,16 +272,18 @@ public class SMSHelper {
|
||||
* @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<Message> getMessagesWithFilter(Context context, String selection, String[] selectionArgs) {
|
||||
HashMap<ThreadID, List<Message>> result = getMessages(SMSHelper.getSMSUri(), context, selection, selectionArgs);
|
||||
List<Message> toReturn = new ArrayList<>();
|
||||
private static List<Message> getMessagesWithFilter(
|
||||
@NonNull Context context,
|
||||
@Nullable String selection,
|
||||
@Nullable String[] selectionArgs,
|
||||
@Nullable Long numberToGet
|
||||
) {
|
||||
String sortOrder = Message.DATE + " DESC";
|
||||
|
||||
for(Map.Entry<ThreadID, List<Message>> 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<ThreadID, Message> getConversations(Context context) {
|
||||
HashMap<ThreadID, List<Message>> result = getMessages(SMSHelper.getConversationUri(), context, null, null);
|
||||
HashMap<ThreadID, Message> toReturn = new HashMap<>();
|
||||
public static Map<ThreadID, Message> getConversations(
|
||||
@NonNull Context context
|
||||
) {
|
||||
Uri uri = SMSHelper.getConversationUri();
|
||||
|
||||
for(Map.Entry<ThreadID, List<Message>> entry : result.entrySet()) {
|
||||
ThreadID returnThreadID = entry.getKey();
|
||||
List<Message> messages = entry.getValue();
|
||||
List<Message> unthreadedMessages = getMessages(uri, context, null, null, null, null);
|
||||
|
||||
toReturn.put(returnThreadID, messages.get(0));
|
||||
Map<ThreadID, Message> 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<String, String> 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<String, String> 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<String, String> 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<String> 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<String> 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<String> 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<String, String> 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;
|
||||
}
|
||||
|
@ -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<SMSHelper.Message> 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");
|
||||
|
Loading…
x
Reference in New Issue
Block a user