mirror of
https://github.com/KDE/kdeconnect-android
synced 2025-08-30 13:47:41 +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
|
||||
* 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.Looper;
|
||||
import android.provider.Telephony;
|
||||
import android.telephony.PhoneNumberUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
@@ -41,6 +43,7 @@ import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -205,11 +208,18 @@ public class SMSHelper {
|
||||
) {
|
||||
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<>();
|
||||
allColumns.addAll(Arrays.asList(Message.smsColumns));
|
||||
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
|
||||
allColumns.add(getTransportTypeDiscriminatorColumn());
|
||||
}
|
||||
@@ -260,19 +270,32 @@ public class SMSHelper {
|
||||
messageInfo.put(colName, body);
|
||||
}
|
||||
|
||||
Message message;
|
||||
if (transportType == TransportType.SMS) {
|
||||
parseSMS(context, messageInfo);
|
||||
message = parseSMS(context, messageInfo);
|
||||
} 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);
|
||||
} while ((numberToGet == null || toReturn.size() != numberToGet) && myCursor.moveToNext());
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
@@ -325,37 +348,63 @@ public class SMSHelper {
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
private static void addEventFlag(
|
||||
@NonNull Map<String, String> messageInfo,
|
||||
@NonNull int eventFlag
|
||||
private static int addEventFlag(
|
||||
int oldEvent,
|
||||
int eventFlag
|
||||
) {
|
||||
int oldEvent = 0; //Default value
|
||||
String oldEventString = messageInfo.get(Message.EVENT);
|
||||
if (oldEventString != null) {
|
||||
oldEvent = Integer.parseInt(oldEventString);
|
||||
}
|
||||
messageInfo.put(Message.EVENT, Integer.toString(oldEvent | eventFlag));
|
||||
return 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 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
|
||||
*/
|
||||
private static void parseMMS(
|
||||
private static @NonNull Message parseMMS(
|
||||
@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 = {
|
||||
Telephony.Mms.Part._ID, // The content ID of this part
|
||||
@@ -389,15 +438,13 @@ public class SMSHelper {
|
||||
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);
|
||||
event = addEventFlag(event, Message.EVENT_TEXT_MESSAGE);
|
||||
} //TODO: Parse more content types (photos and other attachments) here
|
||||
|
||||
} while (cursor.moveToNext());
|
||||
@@ -407,57 +454,59 @@ public class SMSHelper {
|
||||
// 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));
|
||||
type = Telephony.Sms.MESSAGE_TYPE_INBOX;
|
||||
} 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 {
|
||||
// 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));
|
||||
type = Integer.parseInt(messageInfo.get(Telephony.Mms.MESSAGE_BOX));
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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
|
||||
// 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
|
||||
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.
|
||||
}
|
||||
event = addEventFlag(event, Message.EVENT_MULTI_TARGET);
|
||||
}
|
||||
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));
|
||||
date = rawDate * 1000;
|
||||
|
||||
return new Message(
|
||||
addresses,
|
||||
body,
|
||||
date,
|
||||
type,
|
||||
read,
|
||||
threadID,
|
||||
uID,
|
||||
event,
|
||||
subscriptionID
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the address(es) of an MMS message
|
||||
* 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 Long messageID
|
||||
@NonNull Long messageID,
|
||||
@NonNull List<String> userPhoneNumbers
|
||||
) {
|
||||
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[] selectionArgs = {messageID.toString()};
|
||||
|
||||
List<String> addresses = new ArrayList<>();
|
||||
List<Address> addresses = new ArrayList<>();
|
||||
|
||||
try (Cursor addrCursor = context.getContentResolver().query(
|
||||
uri,
|
||||
@@ -484,11 +533,32 @@ public class SMSHelper {
|
||||
|
||||
do {
|
||||
String address = addrCursor.getString(addressIndex);
|
||||
addresses.add(address);
|
||||
addresses.add(new Address(address));
|
||||
} 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.
|
||||
* More useful for logging than catching and handling
|
||||
@@ -588,21 +698,21 @@ public class SMSHelper {
|
||||
*/
|
||||
public static class Message {
|
||||
|
||||
final String address;
|
||||
final String body;
|
||||
public final List<Address> addresses;
|
||||
public final String body;
|
||||
public final long date;
|
||||
final int type;
|
||||
final int read;
|
||||
final ThreadID threadID; // ThreadID is *int* for SMS messages but *long* for MMS
|
||||
final long uID;
|
||||
final int event;
|
||||
final int subscriptionID;
|
||||
public final int type;
|
||||
public final int read;
|
||||
public final ThreadID threadID;
|
||||
public final long uID;
|
||||
public final int event;
|
||||
public final int subscriptionID;
|
||||
|
||||
/**
|
||||
* Named constants which are used to construct a Message
|
||||
* 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 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_*
|
||||
@@ -626,54 +736,77 @@ public class SMSHelper {
|
||||
* Define the columns which are to be extracted from the Android SMS database
|
||||
*/
|
||||
static final String[] smsColumns = new String[]{
|
||||
Message.ADDRESS,
|
||||
Message.BODY,
|
||||
Message.DATE,
|
||||
Message.TYPE,
|
||||
Message.READ,
|
||||
Message.THREAD_ID,
|
||||
Telephony.Sms.ADDRESS,
|
||||
Telephony.Sms.BODY,
|
||||
Telephony.Sms.DATE,
|
||||
Telephony.Sms.TYPE,
|
||||
Telephony.Sms.READ,
|
||||
Telephony.Sms.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.THREAD_ID,
|
||||
Telephony.Mms.DATE,
|
||||
Telephony.Mms.READ,
|
||||
Telephony.Mms.TEXT_ONLY,
|
||||
Telephony.Mms.MESSAGE_BOX, // Compare with Telephony.BaseMmsColumns.MESSAGE_BOX_*
|
||||
};
|
||||
|
||||
Message(final HashMap<String, String> messageInfo) {
|
||||
address = messageInfo.get(Message.ADDRESS);
|
||||
body = messageInfo.get(Message.BODY);
|
||||
date = Long.parseLong(messageInfo.get(Message.DATE));
|
||||
if (messageInfo.get(Message.TYPE) == null)
|
||||
/**
|
||||
* These columns are for determining what SIM card the message belongs to, and therefore
|
||||
* are only defined on Android versions with multi-sim capabilities
|
||||
*/
|
||||
@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.
|
||||
Log.w("SMSHelper", "Encountered undefined message type");
|
||||
type = -1;
|
||||
this.type = -1;
|
||||
// Proceed anyway, maybe this is not an important problem.
|
||||
} else {
|
||||
type = Integer.parseInt(messageInfo.get(Message.TYPE));
|
||||
this.type = type;
|
||||
}
|
||||
read = Integer.parseInt(messageInfo.get(Message.READ));
|
||||
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));
|
||||
this.read = read;
|
||||
this.threadID = threadID;
|
||||
this.uID = uID;
|
||||
this.subscriptionID = subscriptionID;
|
||||
this.event = event;
|
||||
}
|
||||
|
||||
public JSONObject toJSONObject() throws JSONException {
|
||||
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.DATE, date);
|
||||
json.put(Message.TYPE, type);
|
||||
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.SUBSCRIPTION_ID, subscriptionID);
|
||||
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
|
||||
* <p>
|
||||
* 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
|
||||
* // See constants declared in SMSHelper.Message for defined
|
||||
* // values and explanations
|
||||
* "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
|
||||
* "type" : "2", // Compare with Android's
|
||||
* // 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
|
||||
* },
|
||||
* { ... },
|
||||
* ...
|
||||
* ]
|
||||
*
|
||||
* 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 int SMS_MESSAGE_PACKET_VERSION = 2; // We *send* packets of this version
|
||||
|
||||
/**
|
||||
* Packet sent to request a message be sent
|
||||
@@ -280,6 +304,10 @@ public class SMSPlugin extends Plugin {
|
||||
ContentObserver messageObserver = new MessageContentObserver(new Handler(helperLooper));
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -346,12 +374,12 @@ public class SMSPlugin extends Plugin {
|
||||
|
||||
body.put(json);
|
||||
} catch (JSONException e) {
|
||||
Log.e("Conversations", "Error serializing message");
|
||||
Log.e("Conversations", "Error serializing message", e);
|
||||
}
|
||||
}
|
||||
|
||||
reply.set("messages", body);
|
||||
reply.set("event", "batch_messages");
|
||||
reply.set("version", SMS_MESSAGE_PACKET_VERSION);
|
||||
|
||||
return reply;
|
||||
}
|
||||
@@ -426,14 +454,23 @@ public class SMSPlugin extends Plugin {
|
||||
return new String[]{
|
||||
Manifest.permission.SEND_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
|
||||
public int getMinSdk() {
|
||||
return Build.VERSION_CODES.KITKAT;
|
||||
return Build.VERSION_CODES.FROYO;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user