2
0
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:
Simon Redman
2019-07-19 17:46:54 +00:00
parent 1db4327370
commit abcb6cbf33
3 changed files with 403 additions and 92 deletions

View File

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

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

View File

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