Commit abcb6cbf authored by Simon Redman's avatar Simon Redman

[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: kdeconnect-kde!101
parent 1db43273
* Copyright 2019 Simon Redman <>
* 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
* 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 <>.
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) {
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 {
// Single-sim case
// From
// Android added support for multi-sim devices in Lollypop v5.1 (api 22)
// See:
// There were vendor-specific implmentations before then, but those are very difficult to support
// S/O Reference:
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) {
return phoneNumbers;
* Try to get the phone number to which the TelephonyManager is pinned
public static @Nullable String getPhoneNumber(
@NonNull TelephonyManager telephonyManager)
throws SecurityException {
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, 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()) {
// 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
* // 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);
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 {
} 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[]{
// READ_PHONE_STATE should be optional, since we can just query the user, but that
// requires a GUI implementation for querying the user!
* 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"
public int getMinSdk() {
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment