mirror of
https://github.com/KDE/kdeconnect-android
synced 2025-09-01 06:35:09 +00:00
[SMS App] Export all addresses of multitarget messages
## Summary Export the complete list of remote addresses of a multitarget message Note that this changes format of the returned Message object, replacing the string "address" field with a string list "addresses" field, so it is not backwards-compatible with old desktop applications ## Test Plan See Test Plan of the desktop-side patch: https://invent.kde.org/kde/kdeconnect-kde/merge_requests/101
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2018 Simon Redman <simon@ergotech.com>
|
* Copyright 2019 Simon Redman <simon@ergotech.com>
|
||||||
*
|
*
|
||||||
* This program is free software; you can redistribute it and/or
|
* This program is free software; you can redistribute it and/or
|
||||||
* modify it under the terms of the GNU General Public License as
|
* modify it under the terms of the GNU General Public License as
|
||||||
@@ -30,8 +30,10 @@ import android.net.Uri;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.provider.Telephony;
|
import android.provider.Telephony;
|
||||||
|
import android.telephony.PhoneNumberUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
@@ -41,6 +43,7 @@ import java.io.InputStream;
|
|||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -205,11 +208,18 @@ public class SMSHelper {
|
|||||||
) {
|
) {
|
||||||
List<Message> toReturn = new ArrayList<>();
|
List<Message> toReturn = new ArrayList<>();
|
||||||
|
|
||||||
|
// Get all the active phone numbers so we can filter the user out of the list of targets
|
||||||
|
// of any MMSes
|
||||||
|
List<String> userPhoneNumbers = TelephonyHelper.getAllPhoneNumbers(context);
|
||||||
|
|
||||||
Set<String> allColumns = new HashSet<>();
|
Set<String> allColumns = new HashSet<>();
|
||||||
allColumns.addAll(Arrays.asList(Message.smsColumns));
|
allColumns.addAll(Arrays.asList(Message.smsColumns));
|
||||||
allColumns.addAll(Arrays.asList(Message.mmsColumns));
|
allColumns.addAll(Arrays.asList(Message.mmsColumns));
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||||
|
allColumns.addAll(Arrays.asList(Message.multiSIMColumns));
|
||||||
|
}
|
||||||
|
|
||||||
if (uri != getConversationUri()) {
|
if (!uri.equals(getConversationUri())) {
|
||||||
// See https://issuetracker.google.com/issues/134592631
|
// See https://issuetracker.google.com/issues/134592631
|
||||||
allColumns.add(getTransportTypeDiscriminatorColumn());
|
allColumns.add(getTransportTypeDiscriminatorColumn());
|
||||||
}
|
}
|
||||||
@@ -260,19 +270,32 @@ public class SMSHelper {
|
|||||||
messageInfo.put(colName, body);
|
messageInfo.put(colName, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Message message;
|
||||||
if (transportType == TransportType.SMS) {
|
if (transportType == TransportType.SMS) {
|
||||||
parseSMS(context, messageInfo);
|
message = parseSMS(context, messageInfo);
|
||||||
} else if (transportType == TransportType.MMS) {
|
} else if (transportType == TransportType.MMS) {
|
||||||
parseMMS(context, messageInfo);
|
message = parseMMS(context, messageInfo, userPhoneNumbers);
|
||||||
|
} else {
|
||||||
|
// As we can see, all possible transportTypes are covered, but the compiler
|
||||||
|
// requires this line anyway
|
||||||
|
throw new UnsupportedOperationException("Unknown TransportType encountered");
|
||||||
}
|
}
|
||||||
|
|
||||||
Message message = new Message(messageInfo);
|
|
||||||
|
|
||||||
toReturn.add(message);
|
toReturn.add(message);
|
||||||
} while ((numberToGet == null || toReturn.size() != numberToGet) && myCursor.moveToNext());
|
} while ((numberToGet == null || toReturn.size() != numberToGet) && myCursor.moveToNext());
|
||||||
}
|
}
|
||||||
} catch (SQLiteException e) {
|
} catch (SQLiteException e) {
|
||||||
throw new MessageAccessException(fetchColumns, uri, e);
|
String[] unfilteredColumns = {};
|
||||||
|
try (Cursor unfilteredColumnsCursor = context.getContentResolver().query(uri, null, null, null, null)) {
|
||||||
|
if (unfilteredColumnsCursor != null) {
|
||||||
|
unfilteredColumns = unfilteredColumnsCursor.getColumnNames();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (unfilteredColumns.length == 0) {
|
||||||
|
throw new MessageAccessException(uri, e);
|
||||||
|
} else {
|
||||||
|
throw new MessageAccessException(unfilteredColumns, uri, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return toReturn;
|
return toReturn;
|
||||||
}
|
}
|
||||||
@@ -325,37 +348,63 @@ public class SMSHelper {
|
|||||||
return toReturn;
|
return toReturn;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addEventFlag(
|
private static int addEventFlag(
|
||||||
@NonNull Map<String, String> messageInfo,
|
int oldEvent,
|
||||||
@NonNull int eventFlag
|
int eventFlag
|
||||||
) {
|
) {
|
||||||
int oldEvent = 0; //Default value
|
return oldEvent | eventFlag;
|
||||||
String oldEventString = messageInfo.get(Message.EVENT);
|
|
||||||
if (oldEventString != null) {
|
|
||||||
oldEvent = Integer.parseInt(oldEventString);
|
|
||||||
}
|
|
||||||
messageInfo.put(Message.EVENT, Integer.toString(oldEvent | eventFlag));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do any parsing of an SMS message which still needs to be done
|
* Parse all parts of an SMS into a Message
|
||||||
*/
|
*/
|
||||||
private static void parseSMS(
|
private static @NonNull Message parseSMS(
|
||||||
@NonNull Context context,
|
@NonNull Context context,
|
||||||
@NonNull Map<String, String> messageInfo
|
@NonNull Map<String, String> messageInfo
|
||||||
) {
|
) {
|
||||||
addEventFlag(messageInfo, Message.EVENT_TEXT_MESSAGE);
|
int event = Message.EVENT_UNKNOWN;
|
||||||
|
event = addEventFlag(event, Message.EVENT_TEXT_MESSAGE);
|
||||||
|
|
||||||
|
@NonNull List<Address> address = Collections.singletonList(new Address(messageInfo.get(Telephony.Sms.ADDRESS)));
|
||||||
|
@NonNull String body = messageInfo.get(Message.BODY);
|
||||||
|
long date = Long.parseLong(messageInfo.get(Message.DATE));
|
||||||
|
int type = Integer.parseInt(messageInfo.get(Message.TYPE));
|
||||||
|
int read = Integer.parseInt(messageInfo.get(Message.READ));
|
||||||
|
@NonNull ThreadID threadID = new ThreadID(Long.parseLong(messageInfo.get(Message.THREAD_ID)));
|
||||||
|
long uID = Long.parseLong(messageInfo.get(Message.U_ID));
|
||||||
|
int subscriptionID = Integer.parseInt(messageInfo.get(Message.SUBSCRIPTION_ID));
|
||||||
|
|
||||||
|
return new Message(
|
||||||
|
address,
|
||||||
|
body,
|
||||||
|
date,
|
||||||
|
type,
|
||||||
|
read,
|
||||||
|
threadID,
|
||||||
|
uID,
|
||||||
|
event,
|
||||||
|
subscriptionID
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse all parts of the MMS message into the messageInfo format
|
* Parse all parts of the MMS message into a message
|
||||||
* Original implementation from https://stackoverflow.com/a/6446831/3723163
|
* Original implementation from https://stackoverflow.com/a/6446831/3723163
|
||||||
*/
|
*/
|
||||||
private static void parseMMS(
|
private static @NonNull Message parseMMS(
|
||||||
@NonNull Context context,
|
@NonNull Context context,
|
||||||
@NonNull Map<String, String> messageInfo
|
@NonNull Map<String, String> messageInfo,
|
||||||
|
@NonNull List<String> userPhoneNumbers
|
||||||
) {
|
) {
|
||||||
addEventFlag(messageInfo, Message.EVENT_UNKNOWN);
|
int event = Message.EVENT_UNKNOWN;
|
||||||
|
|
||||||
|
@NonNull String body = "";
|
||||||
|
long date;
|
||||||
|
int type;
|
||||||
|
int read = Integer.parseInt(messageInfo.get(Message.READ));
|
||||||
|
@NonNull ThreadID threadID = new ThreadID(Long.parseLong(messageInfo.get(Message.THREAD_ID)));
|
||||||
|
long uID = Long.parseLong(messageInfo.get(Message.U_ID));
|
||||||
|
int subscriptionID = Integer.parseInt(messageInfo.get(Message.SUBSCRIPTION_ID));
|
||||||
|
|
||||||
String[] columns = {
|
String[] columns = {
|
||||||
Telephony.Mms.Part._ID, // The content ID of this part
|
Telephony.Mms.Part._ID, // The content ID of this part
|
||||||
@@ -389,15 +438,13 @@ public class SMSHelper {
|
|||||||
String contentType = cursor.getString(contentTypeColumn);
|
String contentType = cursor.getString(contentTypeColumn);
|
||||||
String data = cursor.getString(dataColumn);
|
String data = cursor.getString(dataColumn);
|
||||||
if ("text/plain".equals(contentType)) {
|
if ("text/plain".equals(contentType)) {
|
||||||
String body;
|
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
// data != null means the data is on disk. Go get it.
|
// data != null means the data is on disk. Go get it.
|
||||||
body = getMmsText(context, partID);
|
body = getMmsText(context, partID);
|
||||||
} else {
|
} else {
|
||||||
body = cursor.getString(textColumn);
|
body = cursor.getString(textColumn);
|
||||||
}
|
}
|
||||||
messageInfo.put(Message.BODY, body);
|
event = addEventFlag(event, Message.EVENT_TEXT_MESSAGE);
|
||||||
addEventFlag(messageInfo, Message.EVENT_TEXT_MESSAGE);
|
|
||||||
} //TODO: Parse more content types (photos and other attachments) here
|
} //TODO: Parse more content types (photos and other attachments) here
|
||||||
|
|
||||||
} while (cursor.moveToNext());
|
} while (cursor.moveToNext());
|
||||||
@@ -407,57 +454,59 @@ public class SMSHelper {
|
|||||||
// Determine whether the message was in- our out- bound
|
// Determine whether the message was in- our out- bound
|
||||||
long messageBox = Long.parseLong(messageInfo.get(Telephony.Mms.MESSAGE_BOX));
|
long messageBox = Long.parseLong(messageInfo.get(Telephony.Mms.MESSAGE_BOX));
|
||||||
if (messageBox == Telephony.Mms.MESSAGE_BOX_INBOX) {
|
if (messageBox == Telephony.Mms.MESSAGE_BOX_INBOX) {
|
||||||
messageInfo.put(Message.TYPE, Integer.toString(Telephony.Sms.MESSAGE_TYPE_INBOX));
|
type = Telephony.Sms.MESSAGE_TYPE_INBOX;
|
||||||
} else if (messageBox == Telephony.Mms.MESSAGE_BOX_SENT) {
|
} else if (messageBox == Telephony.Mms.MESSAGE_BOX_SENT) {
|
||||||
messageInfo.put(Message.TYPE, Integer.toString(Telephony.Sms.MESSAGE_TYPE_SENT));
|
type = Telephony.Sms.MESSAGE_TYPE_SENT;
|
||||||
} else {
|
} else {
|
||||||
// As an undocumented feature, it looks like the values of Mms.MESSAGE_BOX_*
|
// 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
|
// are the same as Sms.MESSAGE_TYPE_* of the same type. So by default let's just use
|
||||||
// the value we've got.
|
// the value we've got.
|
||||||
// This includes things like drafts, which are a far-distant plan to support
|
// This includes things like drafts, which are a far-distant plan to support
|
||||||
messageInfo.put(Message.TYPE, messageInfo.get(Telephony.Mms.MESSAGE_BOX));
|
type = Integer.parseInt(messageInfo.get(Telephony.Mms.MESSAGE_BOX));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get address(es) of the message
|
// Get address(es) of the message
|
||||||
List<String> addresses = getMmsAddresses(context, Long.parseLong(mmsID));
|
List<Address> addresses = getMmsAddresses(context, Long.parseLong(mmsID), userPhoneNumbers);
|
||||||
// It looks like addresses[0] is always the sender of the message and
|
// It looks like addresses[0] is always the sender of the message and
|
||||||
// following addresses are recipient(s)
|
// following addresses are recipient(s)
|
||||||
// This usually means the addresses list is at least 2 long, but there are cases (special
|
// 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")
|
// 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) {
|
if (addresses.size() > 2) {
|
||||||
// TODO: Collect addresses for multi-target MMS
|
// TODO: Handle 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
|
// 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);
|
event = addEventFlag(event, 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
|
// Canonicalize the date field
|
||||||
// SMS uses epoch milliseconds, MMS uses epoch seconds. Standardize on milliseconds.
|
// SMS uses epoch milliseconds, MMS uses epoch seconds. Standardize on milliseconds.
|
||||||
long rawDate = Long.parseLong(messageInfo.get(Message.DATE));
|
long rawDate = Long.parseLong(messageInfo.get(Message.DATE));
|
||||||
messageInfo.put(Message.DATE, Long.toString(rawDate * 1000));
|
date = rawDate * 1000;
|
||||||
|
|
||||||
|
return new Message(
|
||||||
|
addresses,
|
||||||
|
body,
|
||||||
|
date,
|
||||||
|
type,
|
||||||
|
read,
|
||||||
|
threadID,
|
||||||
|
uID,
|
||||||
|
event,
|
||||||
|
subscriptionID
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the address(es) of an MMS message
|
* Get the address(es) of an MMS message
|
||||||
* Original implementation from https://stackoverflow.com/a/6446831/3723163
|
* Original implementation from https://stackoverflow.com/a/6446831/3723163
|
||||||
|
*
|
||||||
|
* @param messageID ID of this message in the MMS database for looking up the remaining info
|
||||||
|
* @param userPhoneNumbers List of phone numbers which should be removed from the list of addresses
|
||||||
*/
|
*/
|
||||||
private static @NonNull List<String> getMmsAddresses(
|
private static @NonNull List<Address> getMmsAddresses(
|
||||||
@NonNull Context context,
|
@NonNull Context context,
|
||||||
@NonNull Long messageID
|
@NonNull Long messageID,
|
||||||
|
@NonNull List<String> userPhoneNumbers
|
||||||
) {
|
) {
|
||||||
Uri uri = ContentUris.appendId(getMMSUri().buildUpon(), messageID).appendPath("addr").build();
|
Uri uri = ContentUris.appendId(getMMSUri().buildUpon(), messageID).appendPath("addr").build();
|
||||||
|
|
||||||
@@ -470,7 +519,7 @@ public class SMSHelper {
|
|||||||
String selection = Telephony.Mms.Addr.MSG_ID + " = ?";
|
String selection = Telephony.Mms.Addr.MSG_ID + " = ?";
|
||||||
String[] selectionArgs = {messageID.toString()};
|
String[] selectionArgs = {messageID.toString()};
|
||||||
|
|
||||||
List<String> addresses = new ArrayList<>();
|
List<Address> addresses = new ArrayList<>();
|
||||||
|
|
||||||
try (Cursor addrCursor = context.getContentResolver().query(
|
try (Cursor addrCursor = context.getContentResolver().query(
|
||||||
uri,
|
uri,
|
||||||
@@ -484,11 +533,32 @@ public class SMSHelper {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
String address = addrCursor.getString(addressIndex);
|
String address = addrCursor.getString(addressIndex);
|
||||||
addresses.add(address);
|
addresses.add(new Address(address));
|
||||||
} while (addrCursor.moveToNext());
|
} while (addrCursor.moveToNext());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return addresses;
|
|
||||||
|
// Prune the user's phone numbers from the list of addresses
|
||||||
|
List<Address> prunedAddresses = new ArrayList<>(addresses);
|
||||||
|
prunedAddresses.removeAll(userPhoneNumbers);
|
||||||
|
|
||||||
|
if (prunedAddresses.size() == 0) {
|
||||||
|
// If it turns out that we have pruned away everything, prune away nothing
|
||||||
|
// (The user is allowed to talk to themself)
|
||||||
|
|
||||||
|
// Remove duplicate entries, since the user knows if a conversation says "Me" on it,
|
||||||
|
// it is the conversation with themself. (We don't need to say "Me, Me")
|
||||||
|
// This leaves the multi-sim case alone, so the returned address list might say
|
||||||
|
// "Me1, Me2"
|
||||||
|
|
||||||
|
prunedAddresses = new ArrayList<>(addresses.size()); // The old one was empty too, but just to be clear...
|
||||||
|
for (Address address : addresses) {
|
||||||
|
if (!prunedAddresses.contains(address)) {
|
||||||
|
prunedAddresses.add(address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prunedAddresses;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -560,6 +630,46 @@ public class SMSHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class Address {
|
||||||
|
final String address;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address object field names
|
||||||
|
*/
|
||||||
|
public static final String ADDRESS = "address";
|
||||||
|
|
||||||
|
public Address(String address) {
|
||||||
|
this.address = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JSONObject toJson() throws JSONException {
|
||||||
|
JSONObject json = new JSONObject();
|
||||||
|
|
||||||
|
json.put(Address.ADDRESS, this.address);
|
||||||
|
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other){
|
||||||
|
if (other == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (other.getClass().isAssignableFrom(Address.class)) {
|
||||||
|
return PhoneNumberUtils.compare(this.address, ((Address)other).address);
|
||||||
|
}
|
||||||
|
if (other.getClass().isAssignableFrom(String.class)) {
|
||||||
|
return PhoneNumberUtils.compare(this.address, (String)other);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicate that some error has occurred while reading a message.
|
* Indicate that some error has occurred while reading a message.
|
||||||
* More useful for logging than catching and handling
|
* More useful for logging than catching and handling
|
||||||
@@ -588,21 +698,21 @@ public class SMSHelper {
|
|||||||
*/
|
*/
|
||||||
public static class Message {
|
public static class Message {
|
||||||
|
|
||||||
final String address;
|
public final List<Address> addresses;
|
||||||
final String body;
|
public final String body;
|
||||||
public final long date;
|
public final long date;
|
||||||
final int type;
|
public final int type;
|
||||||
final int read;
|
public final int read;
|
||||||
final ThreadID threadID; // ThreadID is *int* for SMS messages but *long* for MMS
|
public final ThreadID threadID;
|
||||||
final long uID;
|
public final long uID;
|
||||||
final int event;
|
public final int event;
|
||||||
final int subscriptionID;
|
public final int subscriptionID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Named constants which are used to construct a Message
|
* Named constants which are used to construct a Message
|
||||||
* See: https://developer.android.com/reference/android/provider/Telephony.TextBasedSmsColumns.html for full documentation
|
* See: https://developer.android.com/reference/android/provider/Telephony.TextBasedSmsColumns.html for full documentation
|
||||||
*/
|
*/
|
||||||
static final String ADDRESS = Telephony.Sms.ADDRESS; // Contact information (phone number or otherwise) of the remote
|
static final String ADDRESSES = "addresses"; // Contact information (phone number or otherwise) of the remote
|
||||||
static final String BODY = Telephony.Sms.BODY; // Body of the message
|
static final String BODY = Telephony.Sms.BODY; // Body of the message
|
||||||
static final String DATE = Telephony.Sms.DATE; // Date (Unix epoch millis) associated with the message
|
static final String DATE = Telephony.Sms.DATE; // Date (Unix epoch millis) associated with the message
|
||||||
static final String TYPE = Telephony.Sms.TYPE; // Compare with Telephony.TextBasedSmsColumns.MESSAGE_TYPE_*
|
static final String TYPE = Telephony.Sms.TYPE; // Compare with Telephony.TextBasedSmsColumns.MESSAGE_TYPE_*
|
||||||
@@ -626,54 +736,77 @@ public class SMSHelper {
|
|||||||
* Define the columns which are to be extracted from the Android SMS database
|
* Define the columns which are to be extracted from the Android SMS database
|
||||||
*/
|
*/
|
||||||
static final String[] smsColumns = new String[]{
|
static final String[] smsColumns = new String[]{
|
||||||
Message.ADDRESS,
|
Telephony.Sms.ADDRESS,
|
||||||
Message.BODY,
|
Telephony.Sms.BODY,
|
||||||
Message.DATE,
|
Telephony.Sms.DATE,
|
||||||
Message.TYPE,
|
Telephony.Sms.TYPE,
|
||||||
Message.READ,
|
Telephony.Sms.READ,
|
||||||
Message.THREAD_ID,
|
Telephony.Sms.THREAD_ID,
|
||||||
Message.U_ID,
|
Message.U_ID,
|
||||||
Message.SUBSCRIPTION_ID,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static final String[] mmsColumns = new String[]{
|
static final String[] mmsColumns = new String[]{
|
||||||
Message.U_ID,
|
Message.U_ID,
|
||||||
Message.THREAD_ID,
|
Telephony.Mms.THREAD_ID,
|
||||||
Message.DATE,
|
Telephony.Mms.DATE,
|
||||||
Message.READ,
|
Telephony.Mms.READ,
|
||||||
Telephony.Mms.TEXT_ONLY,
|
Telephony.Mms.TEXT_ONLY,
|
||||||
Telephony.Mms.MESSAGE_BOX, // Compare with Telephony.BaseMmsColumns.MESSAGE_BOX_*
|
Telephony.Mms.MESSAGE_BOX, // Compare with Telephony.BaseMmsColumns.MESSAGE_BOX_*
|
||||||
};
|
};
|
||||||
|
|
||||||
Message(final HashMap<String, String> messageInfo) {
|
/**
|
||||||
address = messageInfo.get(Message.ADDRESS);
|
* These columns are for determining what SIM card the message belongs to, and therefore
|
||||||
body = messageInfo.get(Message.BODY);
|
* are only defined on Android versions with multi-sim capabilities
|
||||||
date = Long.parseLong(messageInfo.get(Message.DATE));
|
*/
|
||||||
if (messageInfo.get(Message.TYPE) == null)
|
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
|
||||||
|
static final String[] multiSIMColumns = new String[]{
|
||||||
|
Telephony.Sms.SUBSCRIPTION_ID,
|
||||||
|
};
|
||||||
|
|
||||||
|
Message(
|
||||||
|
@NonNull List<Address> addresses,
|
||||||
|
@NonNull String body,
|
||||||
|
long date,
|
||||||
|
@NonNull Integer type,
|
||||||
|
int read,
|
||||||
|
@NonNull ThreadID threadID,
|
||||||
|
long uID,
|
||||||
|
int event,
|
||||||
|
int subscriptionID
|
||||||
|
) {
|
||||||
|
this.addresses = addresses;
|
||||||
|
this.body = body;
|
||||||
|
this.date = date;
|
||||||
|
if (type == null)
|
||||||
{
|
{
|
||||||
// To be honest, I have no idea why this happens. The docs say the TYPE field is mandatory.
|
// To be honest, I have no idea why this happens. The docs say the TYPE field is mandatory.
|
||||||
Log.w("SMSHelper", "Encountered undefined message type");
|
Log.w("SMSHelper", "Encountered undefined message type");
|
||||||
type = -1;
|
this.type = -1;
|
||||||
// Proceed anyway, maybe this is not an important problem.
|
// Proceed anyway, maybe this is not an important problem.
|
||||||
} else {
|
} else {
|
||||||
type = Integer.parseInt(messageInfo.get(Message.TYPE));
|
this.type = type;
|
||||||
}
|
}
|
||||||
read = Integer.parseInt(messageInfo.get(Message.READ));
|
this.read = read;
|
||||||
threadID = new ThreadID(Long.parseLong(messageInfo.get(Message.THREAD_ID)));
|
this.threadID = threadID;
|
||||||
uID = Integer.parseInt(messageInfo.get(Message.U_ID));
|
this.uID = uID;
|
||||||
subscriptionID = Integer.parseInt(messageInfo.get(Message.SUBSCRIPTION_ID));
|
this.subscriptionID = subscriptionID;
|
||||||
event = Integer.parseInt(messageInfo.get(Message.EVENT));
|
this.event = event;
|
||||||
}
|
}
|
||||||
|
|
||||||
public JSONObject toJSONObject() throws JSONException {
|
public JSONObject toJSONObject() throws JSONException {
|
||||||
JSONObject json = new JSONObject();
|
JSONObject json = new JSONObject();
|
||||||
|
|
||||||
json.put(Message.ADDRESS, address);
|
JSONArray jsonAddresses = new JSONArray();
|
||||||
|
for (Address address : this.addresses) {
|
||||||
|
jsonAddresses.put(address.toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
json.put(Message.ADDRESSES, jsonAddresses);
|
||||||
json.put(Message.BODY, body);
|
json.put(Message.BODY, body);
|
||||||
json.put(Message.DATE, date);
|
json.put(Message.DATE, date);
|
||||||
json.put(Message.TYPE, type);
|
json.put(Message.TYPE, type);
|
||||||
json.put(Message.READ, read);
|
json.put(Message.READ, read);
|
||||||
json.put(Message.THREAD_ID, threadID);
|
json.put(Message.THREAD_ID, threadID.threadID);
|
||||||
json.put(Message.U_ID, uID);
|
json.put(Message.U_ID, uID);
|
||||||
json.put(Message.SUBSCRIPTION_ID, subscriptionID);
|
json.put(Message.SUBSCRIPTION_ID, subscriptionID);
|
||||||
json.put(Message.EVENT, event);
|
json.put(Message.EVENT, event);
|
||||||
|
141
src/org/kde/kdeconnect/Helpers/TelephonyHelper.java
Normal file
141
src/org/kde/kdeconnect/Helpers/TelephonyHelper.java
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Simon Redman <simon@ergotech.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.kde.kdeconnect.Helpers;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.telephony.SubscriptionInfo;
|
||||||
|
import android.telephony.SubscriptionManager;
|
||||||
|
import android.telephony.TelephonyManager;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TelephonyHelper {
|
||||||
|
|
||||||
|
public static final String LOGGING_TAG = "TelephonyHelper";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all subscriptionIDs of the device
|
||||||
|
* As far as I can tell, this is essentially a way of identifying particular SIM cards
|
||||||
|
*/
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
|
||||||
|
public static List<Integer> getActiveSubscriptionIDs(
|
||||||
|
@NonNull Context context)
|
||||||
|
throws SecurityException {
|
||||||
|
SubscriptionManager subscriptionManager = (SubscriptionManager) context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
|
||||||
|
if (subscriptionManager == null) {
|
||||||
|
// I don't know why or when this happens...
|
||||||
|
Log.w(LOGGING_TAG, "Could not get SubscriptionManager");
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<SubscriptionInfo> subscriptionInfos = subscriptionManager.getActiveSubscriptionInfoList();
|
||||||
|
List<Integer> subscriptionIDs = new ArrayList<>(subscriptionInfos.size());
|
||||||
|
for (SubscriptionInfo info : subscriptionInfos) {
|
||||||
|
subscriptionIDs.add(info.getSubscriptionId());
|
||||||
|
}
|
||||||
|
return subscriptionIDs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to get the phone number currently active on the phone
|
||||||
|
*
|
||||||
|
* Make sure that you have the READ_PHONE_STATE permission!
|
||||||
|
*
|
||||||
|
* Note that entries of the returned list might return null if the phone number is not known by the device
|
||||||
|
*/
|
||||||
|
public static @NonNull List<String> getAllPhoneNumbers(
|
||||||
|
@NonNull Context context)
|
||||||
|
throws SecurityException {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||||
|
// Single-sim case
|
||||||
|
// From https://stackoverflow.com/a/25131061/3723163
|
||||||
|
// Android added support for multi-sim devices in Lollypop v5.1 (api 22)
|
||||||
|
// See: https://developer.android.com/about/versions/android-5.1.html#multisim
|
||||||
|
// There were vendor-specific implmentations before then, but those are very difficult to support
|
||||||
|
// S/O Reference: https://stackoverflow.com/a/28571835/3723163
|
||||||
|
TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
|
||||||
|
if (telephonyManager == null) {
|
||||||
|
// I don't know why or when this happens...
|
||||||
|
Log.w(LOGGING_TAG, "Could not get TelephonyManager");
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
String phoneNumber = getPhoneNumber(telephonyManager);
|
||||||
|
return Collections.singletonList(phoneNumber);
|
||||||
|
} else {
|
||||||
|
// Potentially multi-sim case
|
||||||
|
SubscriptionManager subscriptionManager = (SubscriptionManager)context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
|
||||||
|
if (subscriptionManager == null) {
|
||||||
|
// I don't know why or when this happens...
|
||||||
|
Log.w(LOGGING_TAG, "Could not get SubscriptionManager");
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<SubscriptionInfo> subscriptionInfos = subscriptionManager.getActiveSubscriptionInfoList();
|
||||||
|
List<String> phoneNumbers = new ArrayList<>(subscriptionInfos.size());
|
||||||
|
for (SubscriptionInfo info : subscriptionInfos) {
|
||||||
|
phoneNumbers.add(info.getNumber());
|
||||||
|
}
|
||||||
|
return phoneNumbers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to get the phone number to which the TelephonyManager is pinned
|
||||||
|
*/
|
||||||
|
public static @Nullable String getPhoneNumber(
|
||||||
|
@NonNull TelephonyManager telephonyManager)
|
||||||
|
throws SecurityException {
|
||||||
|
@SuppressLint("HardwareIds")
|
||||||
|
String maybeNumber = telephonyManager.getLine1Number();
|
||||||
|
|
||||||
|
if (maybeNumber == null) {
|
||||||
|
Log.d(LOGGING_TAG, "Got 'null' instead of a phone number");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Sometimes we will get some garbage like "Unknown" or "?????" or a variety of other things
|
||||||
|
// Per https://stackoverflow.com/a/25131061/3723163, the only real solution to this is to
|
||||||
|
// query the user for the proper phone number
|
||||||
|
// As a quick possible check, I say if a "number" is not at least 25% digits, it is not actually
|
||||||
|
// a number
|
||||||
|
int digitCount = 0;
|
||||||
|
for (char digit : "0123456789".toCharArray()) {
|
||||||
|
// https://stackoverflow.com/a/8910767/3723163
|
||||||
|
// The number of occurrences of a particular character can be counted by looking at the
|
||||||
|
// total length of the string and subtracting the length of the string without the
|
||||||
|
// target digit
|
||||||
|
int count = maybeNumber.length() - maybeNumber.replace("" + digit, "").length();
|
||||||
|
digitCount += count;
|
||||||
|
}
|
||||||
|
if (maybeNumber.length() > digitCount*4) {
|
||||||
|
Log.d(LOGGING_TAG, "Discarding " + maybeNumber + " because it does not contain a high enough digit ratio to be a real phone number");
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return maybeNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -75,23 +75,47 @@ public class SMSPlugin extends Plugin {
|
|||||||
* The body should contain the key "messages" mapping to an array of messages
|
* The body should contain the key "messages" mapping to an array of messages
|
||||||
* <p>
|
* <p>
|
||||||
* For example:
|
* For example:
|
||||||
* { "messages" : [
|
* {
|
||||||
|
* "version": 2 // This is the second version of this packet type and
|
||||||
|
* // version 1 packets (which did not carry this flag)
|
||||||
|
* // are incompatible with the new format
|
||||||
|
* "messages" : [
|
||||||
* { "event" : 1, // 32-bit field containing a bitwise-or of event flags
|
* { "event" : 1, // 32-bit field containing a bitwise-or of event flags
|
||||||
* // See constants declared in SMSHelper.Message for defined
|
* // See constants declared in SMSHelper.Message for defined
|
||||||
* // values and explanations
|
* // values and explanations
|
||||||
* "body" : "Hello", // Text message body
|
* "body" : "Hello", // Text message body
|
||||||
* "address" : "2021234567", // Sending or receiving address of the message
|
* "addresses": <List<Address>> // List of Address objects, one for each participant of the conversation
|
||||||
|
* // The user's Address is excluded so:
|
||||||
|
* // If this is a single-target messsage, there will only be one
|
||||||
|
* // Address (the other party)
|
||||||
|
* // If this is an incoming multi-target message, the first Address is the
|
||||||
|
* // sender and all other addresses are other parties to the conversation
|
||||||
|
* // If this is an outgoing multi-target message, the sender is implicit
|
||||||
|
* // (the user's phone number) and all Addresses are recipients
|
||||||
* "date" : "1518846484880", // Timestamp of the message
|
* "date" : "1518846484880", // Timestamp of the message
|
||||||
* "type" : "2", // Compare with Android's
|
* "type" : "2", // Compare with Android's
|
||||||
* // Telephony.TextBasedSmsColumns.MESSAGE_TYPE_*
|
* // Telephony.TextBasedSmsColumns.MESSAGE_TYPE_*
|
||||||
* "thread_id" : "132" // Thread to which the message belongs
|
* "thread_id" : 132 // Thread to which the message belongs
|
||||||
* "read" : true // Boolean representing whether a message is read or unread
|
* "read" : true // Boolean representing whether a message is read or unread
|
||||||
* },
|
* },
|
||||||
* { ... },
|
* { ... },
|
||||||
* ...
|
* ...
|
||||||
* ]
|
* ]
|
||||||
|
*
|
||||||
|
* The following optional fields of a message object may be defined
|
||||||
|
* "sub_id": <int> // Android's subscriber ID, which is basically used to determine which SIM card the message
|
||||||
|
* // belongs to. This is mostly useful when attempting to reply to an SMS with the correct
|
||||||
|
* // SIM card using PACKET_TYPE_SMS_REQUEST.
|
||||||
|
* // If this value is not defined or if it does not match a valid subscriber_id known by
|
||||||
|
* // Android, we will use whatever subscriber ID Android gives us as the default
|
||||||
|
*
|
||||||
|
* An Address object looks like:
|
||||||
|
* {
|
||||||
|
* "address": <String> // Address (phone number, email address, etc.) of this object
|
||||||
|
* }
|
||||||
*/
|
*/
|
||||||
private final static String PACKET_TYPE_SMS_MESSAGE = "kdeconnect.sms.messages";
|
private final static String PACKET_TYPE_SMS_MESSAGE = "kdeconnect.sms.messages";
|
||||||
|
private final static int SMS_MESSAGE_PACKET_VERSION = 2; // We *send* packets of this version
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Packet sent to request a message be sent
|
* Packet sent to request a message be sent
|
||||||
@@ -280,6 +304,10 @@ public class SMSPlugin extends Plugin {
|
|||||||
ContentObserver messageObserver = new MessageContentObserver(new Handler(helperLooper));
|
ContentObserver messageObserver = new MessageContentObserver(new Handler(helperLooper));
|
||||||
SMSHelper.registerObserver(messageObserver, context);
|
SMSHelper.registerObserver(messageObserver, context);
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||||
|
Log.w("SMSPlugin", "This is a very old version of Android. The SMS Plugin might not function as intended.");
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,12 +374,12 @@ public class SMSPlugin extends Plugin {
|
|||||||
|
|
||||||
body.put(json);
|
body.put(json);
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
Log.e("Conversations", "Error serializing message");
|
Log.e("Conversations", "Error serializing message", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reply.set("messages", body);
|
reply.set("messages", body);
|
||||||
reply.set("event", "batch_messages");
|
reply.set("version", SMS_MESSAGE_PACKET_VERSION);
|
||||||
|
|
||||||
return reply;
|
return reply;
|
||||||
}
|
}
|
||||||
@@ -426,14 +454,23 @@ public class SMSPlugin extends Plugin {
|
|||||||
return new String[]{
|
return new String[]{
|
||||||
Manifest.permission.SEND_SMS,
|
Manifest.permission.SEND_SMS,
|
||||||
Manifest.permission.READ_SMS,
|
Manifest.permission.READ_SMS,
|
||||||
|
// READ_PHONE_STATE should be optional, since we can just query the user, but that
|
||||||
|
// requires a GUI implementation for querying the user!
|
||||||
|
Manifest.permission.READ_PHONE_STATE,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* I suspect we can actually go lower than this, but it might get unstable
|
* With versions older than KITKAT, lots of the content providers used in SMSHelper become
|
||||||
|
* un-documented. Most manufacturers *did* do things the same way as was done in mainline
|
||||||
|
* Android at that time, but some did not. If the manufacturer followed the default route,
|
||||||
|
* everything will be fine. If not, the plugin will crash. But, since we have a global catch-all
|
||||||
|
* in Device.onPacketReceived, it will not crash catastrophically.
|
||||||
|
* The onCreated method of this SMSPlugin complains if a version older than KitKat is loaded,
|
||||||
|
* but it still allowed in the optimistic hope that things will "just work"
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public int getMinSdk() {
|
public int getMinSdk() {
|
||||||
return Build.VERSION_CODES.KITKAT;
|
return Build.VERSION_CODES.FROYO;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user