diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 3e21a813..37cf9acc 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -53,22 +53,6 @@
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/KdeConnectTheme"
android:name="org.kde.kdeconnect.MyApplication">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/org/kde/kdeconnect/Helpers/SMSHelper.java b/src/org/kde/kdeconnect/Helpers/SMSHelper.java
index 2092f51e..59f66e56 100644
--- a/src/org/kde/kdeconnect/Helpers/SMSHelper.java
+++ b/src/org/kde/kdeconnect/Helpers/SMSHelper.java
@@ -28,7 +28,6 @@ import androidx.annotation.RequiresApi;
import com.google.android.mms.pdu_alt.MultimediaMessagePdu;
import com.google.android.mms.pdu_alt.PduPersister;
-import com.klinker.android.send_message.Utils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
@@ -137,24 +136,26 @@ public class SMSHelper {
@NonNull ThreadID threadID,
@Nullable Long numberToGet
) {
- return getMessagesInRange(context, threadID, Long.MAX_VALUE, numberToGet);
+ return getMessagesInRange(context, threadID, Long.MAX_VALUE, numberToGet, true);
}
/**
- * Get some messages in the given thread which have timestamp equal to or after the given timestamp
+ * Get some messages in the given thread based on a start timestamp and an optional count
*
* @param context android.content.Context running the request
- * @param threadID Thread to look up
+ * @param threadID Optional ThreadID to look up. If not included, this method will return the latest messages from all threads.
* @param startTimestamp Beginning of the range to return
* @param numberToGet Number of messages to return. Pass null for "all"
+ * @param getMessagesOlderStartTime If true, get messages with timestamps before the startTimestamp. If false, get newer messages
* @return Some messages in the requested conversation
*/
@SuppressLint("NewApi")
public static @NonNull List getMessagesInRange(
@NonNull Context context,
- @NonNull ThreadID threadID,
+ @Nullable ThreadID threadID,
@NonNull Long startTimestamp,
- @Nullable Long numberToGet
+ @Nullable Long numberToGet,
+ @NonNull Boolean getMessagesOlderStartTime
) {
// The stickiness with this is that Android's MMS database has its timestamp in epoch *seconds*
// while the SMS database uses epoch *milliseconds*.
@@ -174,15 +175,30 @@ public class SMSHelper {
allMmsColumns.addAll(Arrays.asList(Message.multiSIMColumns));
}
- String selection = Message.THREAD_ID + " = ? AND ? >= " + Message.DATE;
+ String selection;
- String[] smsSelectionArgs = new String[] { threadID.toString(), startTimestamp.toString() };
- String[] mmsSelectionArgs = new String[] { threadID.toString(), Long.toString(startTimestamp / 1000) };
+ if (getMessagesOlderStartTime) {
+ selection = Message.DATE + " <= ?";
+ } else {
+ selection = Message.DATE + " >= ?";
+ }
+
+ List smsSelectionArgs = new ArrayList(2);
+ smsSelectionArgs.add(startTimestamp.toString());
+
+ List mmsSelectionArgs = new ArrayList(2);
+ mmsSelectionArgs.add(Long.toString(startTimestamp / 1000));
+
+ if (threadID != null) {
+ selection += " AND " + Message.THREAD_ID + " = ?";
+ smsSelectionArgs.add(threadID.toString());
+ mmsSelectionArgs.add(threadID.toString());
+ }
String sortOrder = Message.DATE + " DESC";
- List allMessages = getMessages(smsUri, context, allSmsColumns, selection, smsSelectionArgs, sortOrder, numberToGet);
- allMessages.addAll(getMessages(mmsUri, context, allMmsColumns, selection, mmsSelectionArgs, sortOrder, numberToGet));
+ List allMessages = getMessages(smsUri, context, allSmsColumns, selection, smsSelectionArgs.toArray(new String[0]), sortOrder, numberToGet);
+ allMessages.addAll(getMessages(mmsUri, context, allMmsColumns, selection, mmsSelectionArgs.toArray(new String[0]), sortOrder, numberToGet));
// Need to now only return the requested number of messages:
// Suppose we were requested to return N values and suppose a user sends only one MMS per
@@ -207,30 +223,6 @@ public class SMSHelper {
return toReturn;
}
- /**
- * Get the newest sent or received message
- *
- * This might have some potential for race conditions if many messages are received in a short
- * timespan, but my target use-case is humans sending and receiving messages, so I don't think
- * it will be an issue
- *
- * @return null if no matching message is found, otherwise return a Message
- */
- public static @Nullable Message getNewestMessage(
- @NonNull Context context
- ) {
- List messages = getMessagesWithFilter(context, null, null, 1L);
-
- if (messages.size() > 1) {
- Log.w("SMSHelper", "getNewestMessage asked for one message but got " + messages.size());
- }
- if (messages.size() < 1) {
- return null;
- } else {
- return messages.get(0);
- }
- }
-
/**
* Checks if device supports `Telephony.Sms.SUBSCRIPTION_ID` column in database with URI `uri`
*
@@ -284,14 +276,7 @@ public class SMSHelper {
// Get all the active phone numbers so we can filter the user out of the list of targets
// of any MMSes
- List userPhoneNumbers = TelephonyHelper.getAllPhoneNumbers(context);
-
- if (Utils.isDefaultSmsApp(context)) {
- // Due to some reason, which I'm not able to find out yet, when message sending fails, no sent receiver
- // gets invoked to mark the message as failed to send. This is the reason we have to delete the failed
- // messages pending in the outbox before fetching new messages from the database
- deleteFailedMessages(uri, context, fetchColumns, selection, selectionArgs, sortOrder);
- }
+ List userPhoneNumbers = TelephonyHelper.getAllPhoneNumbers(context);
try (Cursor myCursor = context.getContentResolver().query(
uri,
@@ -610,7 +595,7 @@ public class SMSHelper {
private static @NonNull Message parseMMS(
@NonNull Context context,
@NonNull Map messageInfo,
- @NonNull List userPhoneNumbers
+ @NonNull List userPhoneNumbers
) {
int event = Message.EVENT_UNKNOWN;
@@ -720,14 +705,18 @@ public class SMSHelper {
List addresses = new ArrayList<>();
if (from != null) {
- if (!userPhoneNumbers.contains(from.toString()) && !from.toString().equals("insert-address-token")) {
+ boolean isLocalPhoneNumber = userPhoneNumbers.stream().anyMatch(localPhoneNumber -> localPhoneNumber.isMatchingPhoneNumber(from.address));
+
+ if (!isLocalPhoneNumber && !from.toString().equals("insert-address-token")) {
addresses.add(from);
}
}
if (to != null) {
for (Address address : to) {
- if (!userPhoneNumbers.contains(address.toString()) && !address.toString().equals("insert-address-token")) {
+ boolean isLocalPhoneNumber = userPhoneNumbers.stream().anyMatch(localPhoneNumber -> localPhoneNumber.isMatchingPhoneNumber(address.address));
+
+ if (!isLocalPhoneNumber && !from.toString().equals("insert-address-token")) {
addresses.add(address);
}
}
@@ -874,7 +863,7 @@ public class SMSHelper {
}
public static class Address {
- final String address;
+ public final String address;
/**
* Address object field names
diff --git a/src/org/kde/kdeconnect/Helpers/TelephonyHelper.java b/src/org/kde/kdeconnect/Helpers/TelephonyHelper.java
index abb231c1..79fbe225 100644
--- a/src/org/kde/kdeconnect/Helpers/TelephonyHelper.java
+++ b/src/org/kde/kdeconnect/Helpers/TelephonyHelper.java
@@ -25,6 +25,7 @@ import androidx.core.content.ContextCompat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.stream.Collectors;
public class TelephonyHelper {
@@ -65,7 +66,7 @@ public class TelephonyHelper {
*
* Note that entries of the returned list might return null if the phone number is not known by the device
*/
- public static @NonNull List getAllPhoneNumbers(
+ public static @NonNull List getAllPhoneNumbers(
@NonNull Context context)
throws SecurityException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
@@ -82,7 +83,7 @@ public class TelephonyHelper {
Log.w(LOGGING_TAG, "Could not get TelephonyManager");
return Collections.emptyList();
}
- String phoneNumber = getPhoneNumber(telephonyManager);
+ LocalPhoneNumber phoneNumber = getPhoneNumber(telephonyManager);
return Collections.singletonList(phoneNumber);
} else {
// Potentially multi-sim case
@@ -99,18 +100,19 @@ public class TelephonyHelper {
Log.w(LOGGING_TAG, "Could not get SubscriptionInfos");
return Collections.emptyList();
}
- List phoneNumbers = new ArrayList<>(subscriptionInfos.size());
+ List phoneNumbers = new ArrayList<>(subscriptionInfos.size());
for (SubscriptionInfo info : subscriptionInfos) {
- phoneNumbers.add(info.getNumber());
+ LocalPhoneNumber thisPhoneNumber = new LocalPhoneNumber(info.getNumber(), info.getSubscriptionId());
+ phoneNumbers.add(thisPhoneNumber);
}
- return phoneNumbers;
+ return phoneNumbers.stream().filter(localPhoneNumber -> localPhoneNumber.number != null).collect(Collectors.toList());
}
}
/**
* Try to get the phone number to which the TelephonyManager is pinned
*/
- public static @Nullable String getPhoneNumber(
+ public static @Nullable LocalPhoneNumber getPhoneNumber(
@NonNull TelephonyManager telephonyManager)
throws SecurityException {
@SuppressLint("HardwareIds")
@@ -138,7 +140,7 @@ public class TelephonyHelper {
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;
+ return new LocalPhoneNumber(maybeNumber, -1);
}
}
@@ -261,6 +263,31 @@ public class TelephonyHelper {
return false;
}
+ /**
+ * Canonicalize a phone number by removing all (valid) non-digit characters
+ *
+ * Should be equivalent to SmsHelper::canonicalizePhoneNumber in the C++ implementation
+ *
+ * @param phoneNumber The phone number to canonicalize
+ * @return The canonicalized version of the input phone number
+ */
+ public static String canonicalizePhoneNumber(String phoneNumber)
+ {
+ String toReturn = phoneNumber;
+ toReturn = toReturn.replace(" ", "");
+ toReturn = toReturn.replace("-", "");
+ toReturn = toReturn.replace("(", "");
+ toReturn = toReturn.replace(")", "");
+ toReturn = toReturn.replace("+", "");
+ toReturn = toReturn.replaceFirst("^0*", "");
+
+ if (toReturn.isEmpty()) {
+ // If we have stripped away everything, assume this is a special number (and already canonicalized)
+ return phoneNumber;
+ }
+ return toReturn;
+ }
+
/**
* Light copy of https://developer.android.com/reference/android/telephony/data/ApnSetting so
* that we can support older API versions. Delete this when API 28 becomes our supported version.
@@ -312,4 +339,63 @@ public class TelephonyHelper {
return mmsProxyPort;
}
}
+
+ /**
+ * Class representing a phone number which is assigned to the current device
+ */
+ public static class LocalPhoneNumber {
+ /**
+ * The phone number
+ */
+ public final String number;
+
+ /**
+ * The subscription ID to which this phone number belongs
+ */
+ public final int subscriptionID;
+
+ public LocalPhoneNumber(String number, int subscriptionID) {
+ this.number = number;
+ this.subscriptionID = subscriptionID;
+ }
+
+ @Override
+ public String toString() {
+ return number;
+ }
+
+ /**
+ * Do some basic fuzzy matching on two phone numbers to determine whether they match
+ *
+ * This is roughly equivalent to SmsHelper::isPhoneNumberMatch, but might produce more false negatives
+ *
+ * @param potentialMatchingPhoneNumber The phone number to compare to this phone number
+ * @return True if the phone numbers appear to be the same, false otherwise
+ */
+ public boolean isMatchingPhoneNumber(String potentialMatchingPhoneNumber) {
+ String mPhoneNumber = canonicalizePhoneNumber(this.number);
+ String oPhoneNumber = canonicalizePhoneNumber(potentialMatchingPhoneNumber);
+
+ if (mPhoneNumber.isEmpty() || oPhoneNumber.isEmpty()) {
+ // The empty string is not a valid phone number so does not match anything
+ return false;
+ }
+
+ // To decide if a phone number matches:
+ // 1. Are they similar lengths? If two numbers are very different, probably one is junk data and should be ignored
+ // 2. Is one a superset of the other? Phone number digits get more specific the further towards the end of the string,
+ // so if one phone number ends with the other, it is probably just a more-complete version of the same thing
+ String longerNumber = mPhoneNumber.length() >= oPhoneNumber.length() ? mPhoneNumber : oPhoneNumber;
+ String shorterNumber = mPhoneNumber.length() < oPhoneNumber.length() ? mPhoneNumber : oPhoneNumber;
+
+ // If the numbers are vastly different in length, assume they are not the same
+ if (shorterNumber.length() < 0.75 * longerNumber.length()) {
+ return false;
+ }
+
+ boolean matchingPhoneNumber = longerNumber.endsWith(shorterNumber);
+
+ return matchingPhoneNumber;
+ }
+ }
}
diff --git a/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java
index f1a0bcd9..870943cf 100644
--- a/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java
+++ b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java
@@ -44,7 +44,6 @@ import org.kde.kdeconnect.Helpers.AppsHelper;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.Plugins.PluginFactory;
-import org.kde.kdeconnect.Plugins.SMSPlugin.NotificationReplyReceiver;
import org.kde.kdeconnect.UserInterface.MainActivity;
import org.kde.kdeconnect.UserInterface.PluginSettingsFragment;
import org.kde.kdeconnect.UserInterface.StartActivityAlertDialogFragment;
@@ -213,10 +212,6 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
groupKey = statusBarNotification.getGroupKey();
}
-
- if (!groupKey.contains(NotificationReplyReceiver.SMS_NOTIFICATION_GROUP_KEY)) {
- return;
- }
}
NetworkPacket np = new NetworkPacket(PACKET_TYPE_NOTIFICATION);
diff --git a/src/org/kde/kdeconnect/Plugins/SMSPlugin/DelegatingMmsReceivedReceiver.java b/src/org/kde/kdeconnect/Plugins/SMSPlugin/DelegatingMmsReceivedReceiver.java
deleted file mode 100644
index f16b45ee..00000000
--- a/src/org/kde/kdeconnect/Plugins/SMSPlugin/DelegatingMmsReceivedReceiver.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2020 Aniket Kumar
- *
- * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
- */
-
-package org.kde.kdeconnect.Plugins.SMSPlugin;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-
-/**
- * A small BroadcastReceiver wrapper for MMSReceivedReceiver to load user preferences
- */
-public class DelegatingMmsReceivedReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- MmsReceivedReceiver delegate = new MmsReceivedReceiver();
-
- delegate.getPreferredApn(context, intent);
- delegate.onReceive(context, intent);
- }
-}
diff --git a/src/org/kde/kdeconnect/Plugins/SMSPlugin/HeadlessSmsSendService.java b/src/org/kde/kdeconnect/Plugins/SMSPlugin/HeadlessSmsSendService.java
deleted file mode 100644
index 222dacdc..00000000
--- a/src/org/kde/kdeconnect/Plugins/SMSPlugin/HeadlessSmsSendService.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2020 Aniket Kumar
- *
- * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
- */
-
-package org.kde.kdeconnect.Plugins.SMSPlugin;
-
-import android.content.Intent;
-import android.app.Service;
-import android.os.IBinder;
-
-/**
- * Service for sending messages to a conversation without a UI present. These messages could come
- * from something like Phone, needed to make default sms app
- */
-public class HeadlessSmsSendService extends Service {
-
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
-
-}
\ No newline at end of file
diff --git a/src/org/kde/kdeconnect/Plugins/SMSPlugin/MmsReceivedReceiver.java b/src/org/kde/kdeconnect/Plugins/SMSPlugin/MmsReceivedReceiver.java
deleted file mode 100644
index c0cbb56e..00000000
--- a/src/org/kde/kdeconnect/Plugins/SMSPlugin/MmsReceivedReceiver.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2020 Aniket Kumar
- *
- * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
- */
-
-package org.kde.kdeconnect.Plugins.SMSPlugin;
-
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Build;
-import android.text.TextUtils;
-import android.util.Log;
-
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.Person;
-import androidx.core.app.RemoteInput;
-import androidx.core.content.ContextCompat;
-
-import com.klinker.android.send_message.Transaction;
-import com.klinker.android.send_message.Utils;
-
-import org.kde.kdeconnect.Helpers.TelephonyHelper;
-import org.kde.kdeconnect_tp.R;
-import org.kde.kdeconnect.Helpers.NotificationHelper;
-import org.kde.kdeconnect.Helpers.SMSHelper;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-
-/**
- * Receiver for notifying user when a new MMS has been received by the device. By default it will
- * persist the message to the internal database and it will also show a notification in the status bar.
- */
-public class MmsReceivedReceiver extends com.klinker.android.send_message.MmsReceivedReceiver {
-
- private TelephonyHelper.ApnSetting apnSetting = null;
-
- @Override
- public void onMessageReceived(Context context, Uri messageUri) {
- Log.v("MmsReceived", "message received: " + messageUri.toString());
-
- // Notify messageUpdateReceiver about the arrival of the new MMS message
- Intent refreshIntent = new Intent(Transaction.REFRESH);
- context.sendBroadcast(refreshIntent);
-
- // Fetch the latest message from the database
- SMSHelper.Message message = SMSHelper.getNewestMessage(context);
-
- // Notify the user about the received mms message
- createMmsNotification(context, message);
- }
-
- @Override
- public void onError(Context context, String error) {
- Log.v("MmsReceived", "error: " + error);
-
- // Fetch the latest message from the database
- SMSHelper.Message message = SMSHelper.getNewestMessage(context);
-
- // Notify the user about the received mms message
- createMmsNotification(context, message);
- }
-
- public void getPreferredApn(Context context, Intent intent) {
- int subscriptionId = intent.getIntExtra(SUBSCRIPTION_ID, Utils.getDefaultSubscriptionId());
- apnSetting = TelephonyHelper.getPreferredApn(context, subscriptionId);
- }
-
- /**
- * some carriers will download duplicate MMS messages without this ACK. When using the
- * system sending method, apparently Android does not do this for us. Not sure why.
- * We might have to have users manually enter their APN settings if we cannot get them
- * from the system somehow.
- */
- @Override
- public MmscInformation getMmscInfoForReceptionAck() {
- if (apnSetting != null) {
- String mmscUrl = apnSetting.getMmsc().toString();
- String mmsProxy = apnSetting.getMmsProxyAddressAsString();
- int mmsPort = apnSetting.getMmsProxyPort();
-
- try {
- return new MmscInformation(mmscUrl, mmsProxy, mmsPort);
- } catch (Exception e) {
- Log.e("MmsReceivedReceiver", "Exception", e);
- }
- }
- return null;
- }
-
- private void createMmsNotification(Context context, SMSHelper.Message mmsMessage) {
- ArrayList addressList = new ArrayList<>();
- for (SMSHelper.Address address : mmsMessage.addresses) {
- addressList.add(address.toString());
- }
-
- Person sender = NotificationReplyReceiver.getMessageSender(context, addressList.get(0));
-
- int notificationId;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- notificationId = (int) Utils.getOrCreateThreadId(context, new HashSet<>(addressList));
- } else {
- notificationId = (int) System.currentTimeMillis();
- }
-
- // Todo: When SMSHelper.Message class will be modified to contain thumbnail of the image or video attachment, add them here to display.
-
- // Create pending intent for reply action through notification
- PendingIntent replyPendingIntent = NotificationReplyReceiver.createReplyPendingIntent(
- context,
- addressList,
- notificationId,
- true
- );
-
- RemoteInput remoteReplyInput = new RemoteInput.Builder(NotificationReplyReceiver.KEY_TEXT_REPLY)
- .setLabel(context.getString(R.string.message_reply_label))
- .build();
-
- NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(0, context.getString(R.string.message_reply_label), replyPendingIntent)
- .addRemoteInput(remoteReplyInput)
- .setAllowGeneratedReplies(true)
- .build();
-
- // Create pending intent for marking the message as read in database through mark as read action
- PendingIntent markAsReadPendingIntent = NotificationReplyReceiver.createMarkAsReadPendingIntent(
- context,
- addressList,
- notificationId
- );
-
- NotificationCompat.Action markAsReadAction = new NotificationCompat.Action.Builder(0, context.getString(R.string.mark_as_read_label), markAsReadPendingIntent)
- .build();
-
- NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- NotificationCompat.MessagingStyle messagingStyle = NotificationReplyReceiver.createMessagingStyle(
- context,
- notificationId,
- mmsMessage.body,
- TextUtils.join(",", addressList),
- mmsMessage.date,
- sender,
- notificationManager
- );
-
- NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NotificationReplyReceiver.CHANNEL_ID)
- .setSmallIcon(R.drawable.ic_baseline_sms_24)
- .setColor(ContextCompat.getColor(context, R.color.primary))
- .setPriority(NotificationCompat.PRIORITY_DEFAULT)
- .setStyle(messagingStyle)
- .setAutoCancel(true)
- .addAction(replyAction)
- .addAction(markAsReadAction)
- .setGroup(NotificationReplyReceiver.SMS_NOTIFICATION_GROUP_KEY);
-
- NotificationHelper.notifyCompat(notificationManager, notificationId, builder.build());
- }
-}
diff --git a/src/org/kde/kdeconnect/Plugins/SMSPlugin/MmsSentReceiverImpl.java b/src/org/kde/kdeconnect/Plugins/SMSPlugin/MmsSentReceiverImpl.java
deleted file mode 100644
index 6c8e46c6..00000000
--- a/src/org/kde/kdeconnect/Plugins/SMSPlugin/MmsSentReceiverImpl.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2020 Aniket Kumar
- *
- * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
- */
-
-package org.kde.kdeconnect.Plugins.SMSPlugin;
-
-import android.content.Context;
-import android.content.Intent;
-
-import com.klinker.android.send_message.Transaction;
-import com.klinker.android.send_message.Utils;
-
-import org.kde.kdeconnect.Helpers.SMSHelper;
-
-import java.util.ArrayList;
-
-public class MmsSentReceiverImpl extends com.klinker.android.send_message.MmsSentReceiver {
-
- @Override
- public void updateInInternalDatabase(Context context, Intent intent, int resultCode) {
- super.updateInInternalDatabase(context, intent, resultCode);
-
- if (Utils.isDefaultSmsApp(context)) {
- // Notify messageUpdateReceiver about the successful sending of the mms message
- Intent refreshIntent = new Intent(Transaction.REFRESH);
- context.sendBroadcast(refreshIntent);
- }
- }
-
- @Override
- public void onMessageStatusUpdated(Context context, Intent intent, int resultCode) {
- SMSHelper.Message message = SMSHelper.getNewestMessage(context);
-
- ArrayList addressList = new ArrayList<>();
- for (SMSHelper.Address address : message.addresses) {
- addressList.add(address.toString());
- }
-
- Intent repliedNotification = new Intent(context, NotificationReplyReceiver.class);
- repliedNotification.setAction(NotificationReplyReceiver.SMS_MMS_REPLY_ACTION);
- repliedNotification.putExtra(NotificationReplyReceiver.TEXT_BODY, message.body);
- repliedNotification.putExtra(NotificationReplyReceiver.NOTIFICATION_ID, Integer.parseInt(message.threadID.toString()));
- repliedNotification.putExtra(NotificationReplyReceiver.ADDRESS_LIST, addressList);
-
- // SEND_ACTION value is required to differentiate between the intents sent from reply action or
- // SentReceivers inorder to avoid posting duplicate notifications
- repliedNotification.putExtra(NotificationReplyReceiver.SEND_ACTION, false);
-
- context.sendBroadcast(repliedNotification);
- }
-}
diff --git a/src/org/kde/kdeconnect/Plugins/SMSPlugin/NotificationReplyReceiver.java b/src/org/kde/kdeconnect/Plugins/SMSPlugin/NotificationReplyReceiver.java
deleted file mode 100644
index 169ce5c2..00000000
--- a/src/org/kde/kdeconnect/Plugins/SMSPlugin/NotificationReplyReceiver.java
+++ /dev/null
@@ -1,289 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2020 Aniket Kumar
- *
- * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
- */
-
-package org.kde.kdeconnect.Plugins.SMSPlugin;
-
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.os.Build;
-import android.os.Bundle;
-import android.service.notification.StatusBarNotification;
-import android.text.TextUtils;
-import android.util.Base64;
-import android.util.Log;
-
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.Person;
-import androidx.core.app.RemoteInput;
-import androidx.core.content.ContextCompat;
-import androidx.core.graphics.drawable.IconCompat;
-
-import com.klinker.android.send_message.Utils;
-
-import org.kde.kdeconnect.Helpers.ContactsHelper;
-import org.kde.kdeconnect.Helpers.NotificationHelper;
-import org.kde.kdeconnect.Helpers.SMSHelper;
-import org.kde.kdeconnect_tp.R;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.Map;
-
-public class NotificationReplyReceiver extends BroadcastReceiver {
- public static final String SMS_MMS_REPLY_ACTION = "org.kde.kdeconnect.Plugins.SMSPlugin.sms_mms_reply_action";
- public static final String SMS_MMS_MARK_ACTION = "org.kde.kdeconnect.Plugins.SMSPlugin.sms_mms_mark_action";
- public static final String ADDRESS_LIST = "address_list";
- public static final String CHANNEL_ID = NotificationHelper.Channels.SMS_MMS;
- public static final String KEY_TEXT_REPLY = "key_text_reply";
- public static final String NOTIFICATION_ID = "notification_id";
- public static final String SEND_ACTION = "send_action";
- public static final String TEXT_BODY = "text_body";
- public static final String SMS_NOTIFICATION_GROUP_KEY = "Plugins.SMSPlugin.sms_notification_group_key";
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (!Utils.isDefaultSmsApp(context) || intent == null) {
- return;
- }
-
- Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
-
- final int notificationId = intent.getIntExtra(NotificationReplyReceiver.NOTIFICATION_ID, 0);
- final ArrayList addressList = intent.getStringArrayListExtra(ADDRESS_LIST);
- final boolean sentUsingReplyButton = intent.getBooleanExtra(SEND_ACTION, false);
-
- if (intent.getAction().equals(SMS_MMS_REPLY_ACTION)) {
- String inputString = null;
-
- if (sentUsingReplyButton) {
- inputString = remoteInput.getCharSequence(NotificationReplyReceiver.KEY_TEXT_REPLY).toString();
-
- ArrayList addresses = new ArrayList<>();
- for (String address : addressList) {
- addresses.add(new SMSHelper.Address(address));
- }
- SmsMmsUtils.sendMessage(context, inputString, addresses, -1);
- } else {
- inputString = intent.getStringExtra(TEXT_BODY);
- repliedMessageNotification(context, notificationId, inputString, addressList);
-
- }
- }
-
- // Mark the conversation as read
- if (intent.getAction().equals(SMS_MMS_MARK_ACTION)) {
- NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- notificationManager.cancel(notificationId);
- SmsMmsUtils.markConversationRead(context, new HashSet<>(addressList));
- }
- }
-
- /**
- * Updates the active notification with the newly replied message
- */
- private void repliedMessageNotification(Context context, int notificationId, String inputString, ArrayList addressList) {
- Person sender = new Person.Builder()
- .setName(context.getString(R.string.user_display_name))
- .build();
-
- // Create pending intent for reply action through notification
- PendingIntent replyPendingIntent = createReplyPendingIntent(context, addressList, notificationId, true);
-
- RemoteInput remoteReplyInput = new RemoteInput.Builder(NotificationReplyReceiver.KEY_TEXT_REPLY)
- .setLabel(context.getString(R.string.message_reply_label))
- .build();
-
- NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(0, context.getString(R.string.message_reply_label), replyPendingIntent)
- .addRemoteInput(remoteReplyInput)
- .setAllowGeneratedReplies(true)
- .build();
-
- // Create pending intent for marking the message as read in database through mark as read action
- PendingIntent markAsReadPendingIntent = createMarkAsReadPendingIntent(context, addressList, notificationId);
-
- NotificationCompat.Action markAsReadAction = new NotificationCompat.Action.Builder(0, context.getString(R.string.mark_as_read_label), markAsReadPendingIntent)
- .build();
-
- NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- NotificationCompat.MessagingStyle.Message message = new NotificationCompat.MessagingStyle.Message(
- inputString,
- System.currentTimeMillis(),
- sender
- );
-
- NotificationCompat.MessagingStyle messagingStyle = restoreActiveMessagingStyle(notificationId, notificationManager);
-
- if (messagingStyle == null) {
- // Return when there is no active notification in the statusBar with the above notificationId
- return;
- }
-
- messagingStyle.addMessage(message);
-
- NotificationCompat.Builder repliedNotification = new NotificationCompat.Builder(context, NotificationReplyReceiver.CHANNEL_ID)
- .setSmallIcon(R.drawable.ic_baseline_sms_24)
- .setColor(ContextCompat.getColor(context, R.color.primary))
- .setOnlyAlertOnce(true)
- .setPriority(NotificationCompat.PRIORITY_DEFAULT)
- .setStyle(messagingStyle)
- .setAutoCancel(true)
- .addAction(replyAction)
- .addAction(markAsReadAction)
- .setGroup(NotificationReplyReceiver.SMS_NOTIFICATION_GROUP_KEY);
-
- NotificationHelper.notifyCompat(notificationManager, notificationId, repliedNotification.build());
- }
-
- /**
- * This method creates a new messaging style for newer conversations and if there is already an active notification
- * of the same id, it just adds to the previous and returns the modified messagingStyle object.
- */
- public static NotificationCompat.MessagingStyle createMessagingStyle(
- Context context,
- int notificationId,
- String textMessage,
- String phoneNumbers,
- long date,
- Person sender,
- NotificationManager notificationManager
- ) {
- NotificationCompat.MessagingStyle messageStyle = NotificationReplyReceiver.restoreActiveMessagingStyle(
- notificationId,
- notificationManager
- );
-
- NotificationCompat.MessagingStyle.Message message = new NotificationCompat.MessagingStyle.Message(
- textMessage,
- date,
- sender
- );
-
- if (messageStyle == null) {
- // When no active notification is found for matching conversation create a new one
- String senderName = phoneNumbers;
- Map contactInfo = ContactsHelper.phoneNumberLookup(context, phoneNumbers);
-
- if (contactInfo.containsKey("name")) {
- senderName = contactInfo.get("name");
- }
-
- messageStyle = new NotificationCompat.MessagingStyle(sender)
- .setConversationTitle(senderName);
- }
- messageStyle.addMessage(message);
-
- return messageStyle;
- }
-
- /**
- * This method is responsible for searching the notification for same conversation ID and if there is an active notification
- * of save ID found in the status menu it extracts and returns the messagingStyle object of that notification
- */
- public static NotificationCompat.MessagingStyle restoreActiveMessagingStyle(
- int notificationId,
- NotificationManager notificationManager
- ) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- StatusBarNotification notifications[] = notificationManager.getActiveNotifications();
- for (StatusBarNotification notification : notifications) {
- if (notification.getId() == notificationId) {
- return NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(notification.getNotification());
- }
- }
- }
- return null;
- }
-
- /**
- * returns the sender of the message as a Person object
- */
- public static Person getMessageSender(Context context, String address) {
- Map contactInfo = ContactsHelper.phoneNumberLookup(context, address);
- String senderName = address;
-
- if (contactInfo.containsKey("name")) {
- senderName = contactInfo.get("name");
- }
-
- Bitmap contactPhoto = null;
- if (contactInfo.containsKey("photoID")) {
- String photoUri = contactInfo.get("photoID");
- if (photoUri != null) {
- try {
- String base64photo = ContactsHelper.photoId64Encoded(context, photoUri);
- if (!TextUtils.isEmpty(base64photo)) {
- byte[] decodedString = Base64.decode(base64photo, Base64.DEFAULT);
- contactPhoto = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length);
- }
- } catch (Exception e) {
- Log.e("SMS Notification", "Failed to get contact photo");
- }
- }
- }
-
- Person.Builder personBuilder = new Person.Builder()
- .setName(senderName);
-
- if (contactPhoto != null) {
- personBuilder.setIcon(IconCompat.createWithBitmap(contactPhoto));
- }
-
- return personBuilder.build();
- }
-
- /**
- * Create pending intent for reply action through notification
- */
- public static PendingIntent createReplyPendingIntent(
- Context context,
- ArrayList addressList,
- int notificationId,
- boolean isFromSendAction
- ) {
- Intent replyIntent = new Intent(context, NotificationReplyReceiver.class);
- replyIntent.setAction(NotificationReplyReceiver.SMS_MMS_REPLY_ACTION);
- replyIntent.putExtra(NotificationReplyReceiver.NOTIFICATION_ID, notificationId);
- replyIntent.putExtra(NotificationReplyReceiver.ADDRESS_LIST, addressList);
- replyIntent.putExtra(NotificationReplyReceiver.SEND_ACTION, isFromSendAction);
-
- PendingIntent replyPendingIntent = PendingIntent.getBroadcast(
- context,
- notificationId,
- replyIntent,
- PendingIntent.FLAG_UPDATE_CURRENT
- );
-
- return replyPendingIntent;
- }
-
- /**
- * Create pending intent for marking the message as read in database through mark as read action
- */
- public static PendingIntent createMarkAsReadPendingIntent(
- Context context,
- ArrayList addressList,
- int notificationId
- ) {
- Intent markAsReadIntent = new Intent(context, NotificationReplyReceiver.class);
- markAsReadIntent.setAction(NotificationReplyReceiver.SMS_MMS_MARK_ACTION);
- markAsReadIntent.putExtra(NotificationReplyReceiver.NOTIFICATION_ID, notificationId);
- markAsReadIntent.putExtra(NotificationReplyReceiver.ADDRESS_LIST, addressList);
-
- PendingIntent markAsReadPendingIntent = PendingIntent.getBroadcast(
- context,
- notificationId,
- markAsReadIntent,
- PendingIntent.FLAG_CANCEL_CURRENT
- );
-
- return markAsReadPendingIntent;
- }
-}
diff --git a/src/org/kde/kdeconnect/Plugins/SMSPlugin/SMSPlugin.java b/src/org/kde/kdeconnect/Plugins/SMSPlugin/SMSPlugin.java
index 4f355bd4..f06ed215 100644
--- a/src/org/kde/kdeconnect/Plugins/SMSPlugin/SMSPlugin.java
+++ b/src/org/kde/kdeconnect/Plugins/SMSPlugin/SMSPlugin.java
@@ -40,7 +40,6 @@ import org.kde.kdeconnect_tp.R;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
@@ -240,12 +239,6 @@ public class SMSPlugin extends Plugin {
*/
@Override
public void onChange(boolean selfChange) {
- // If the KDE Connect is set as default Sms app
- // prevent from reading the latest message in the database before the sentReceivers mark it as sent
- if (Utils.isDefaultSmsApp(context)) {
- return;
- }
-
sendLatestMessage();
}
@@ -283,20 +276,21 @@ public class SMSPlugin extends Plugin {
mostRecentTimestampLock.unlock();
return;
}
- SMSHelper.Message message = SMSHelper.getNewestMessage(context);
+ List messages = SMSHelper.getMessagesInRange(context, null, mostRecentTimestamp, null, false);
- if (message == null || message.date <= mostRecentTimestamp) {
- // onChange can trigger many times for a single message. Don't make unnecessary noise
- mostRecentTimestampLock.unlock();
- return;
+ long newMostRecentTimestamp = mostRecentTimestamp;
+ for (SMSHelper.Message message : messages) {
+ if (message == null || message.date <= newMostRecentTimestamp) {
+ newMostRecentTimestamp = message.date;
+ }
}
// Update the most recent counter
- mostRecentTimestamp = message.date;
+ mostRecentTimestamp = newMostRecentTimestamp;
mostRecentTimestampLock.unlock();
// Send the alert about the update
- device.sendPacket(constructBulkMessagePacket(Collections.singleton(message)));
+ device.sendPacket(constructBulkMessagePacket(messages));
}
/**
@@ -529,7 +523,7 @@ public class SMSPlugin extends Plugin {
if (rangeStartTimestamp < 0) {
conversation = SMSHelper.getMessagesInThread(this.context, threadID, numberToGet);
} else {
- conversation = SMSHelper.getMessagesInRange(this.context, threadID, rangeStartTimestamp, numberToGet);
+ conversation = SMSHelper.getMessagesInRange(this.context, threadID, rangeStartTimestamp, numberToGet, true);
}
// Sometimes when desktop app is kept open while android app is restarted for any reason
diff --git a/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsMmsUtils.java b/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsMmsUtils.java
index 229d0fc8..4bc8f8fa 100644
--- a/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsMmsUtils.java
+++ b/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsMmsUtils.java
@@ -6,16 +6,27 @@
package org.kde.kdeconnect.Plugins.SMSPlugin;
+import android.content.ContentResolver;
import android.content.Context;
-import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
+import android.os.Bundle;
import android.preference.PreferenceManager;
+import com.android.mms.dom.smil.parser.SmilXmlSerializer;
+import com.google.android.mms.ContentType;
+import com.google.android.mms.InvalidHeaderValueException;
+import com.google.android.mms.MMSPart;
+import com.google.android.mms.pdu_alt.CharacterSets;
import com.google.android.mms.pdu_alt.EncodedStringValue;
import com.google.android.mms.pdu_alt.MultimediaMessagePdu;
-import com.google.android.mms.pdu_alt.PduPersister;
+import com.google.android.mms.pdu_alt.PduBody;
+import com.google.android.mms.pdu_alt.PduComposer;
+import com.google.android.mms.pdu_alt.PduHeaders;
+import com.google.android.mms.pdu_alt.PduPart;
import com.google.android.mms.pdu_alt.RetrieveConf;
+import com.google.android.mms.pdu_alt.SendReq;
+import com.google.android.mms.smil.SmilHelper;
import com.klinker.android.send_message.Message;
import com.klinker.android.send_message.Settings;
import com.klinker.android.send_message.Transaction;
@@ -27,6 +38,9 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.provider.Telephony;
import android.net.Uri;
+import android.telephony.SmsManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
@@ -41,11 +55,15 @@ import org.kde.kdeconnect_tp.R;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
+import java.util.Optional;
+import java.util.Random;
public class SmsMmsUtils {
@@ -68,6 +86,29 @@ public class SmsMmsUtils {
prefs.getString(context.getString(R.string.convert_to_mms_after),
context.getString(R.string.convert_to_mms_after_default)));
+ TelephonyHelper.LocalPhoneNumber sendingPhoneNumber;
+ List allPhoneNumbers = TelephonyHelper.getAllPhoneNumbers(context);
+
+ Optional maybeSendingPhoneNumber = allPhoneNumbers.stream()
+ .filter(localPhoneNumber -> localPhoneNumber.subscriptionID == subID)
+ .findAny();
+
+ if (maybeSendingPhoneNumber.isPresent()) {
+ sendingPhoneNumber = maybeSendingPhoneNumber.get();
+ } else {
+ if (allPhoneNumbers.isEmpty()) {
+ sendingPhoneNumber = null;
+ } else {
+ sendingPhoneNumber = allPhoneNumbers.get(0);
+ }
+ Log.w(SENDING_MESSAGE, "Unable to get outgoing address for sub ID " + subID + " using " + sendingPhoneNumber);
+ }
+
+ if (sendingPhoneNumber != null) {
+ // Remove the user's phone number if present in the list of recipients
+ addressList.removeIf(address -> sendingPhoneNumber.isMatchingPhoneNumber(address.address));
+ }
+
try {
Settings settings = new Settings();
TelephonyHelper.ApnSetting apnSettings = TelephonyHelper.getPreferredApn(context, subID);
@@ -80,10 +121,8 @@ public class SmsMmsUtils {
settings.setUseSystemSending(true);
}
- if (Utils.isDefaultSmsApp(context)) {
- settings.setSendLongAsMms(longTextAsMms);
- settings.setSendLongAsMmsAfter(sendLongAsMmsAfter);
- }
+ settings.setSendLongAsMms(longTextAsMms);
+ settings.setSendLongAsMmsAfter(sendLongAsMmsAfter);
settings.setGroup(groupMessageAsMms);
@@ -92,8 +131,6 @@ public class SmsMmsUtils {
}
Transaction transaction = new Transaction(context, settings);
- transaction.setExplicitBroadcastForSentSms(new Intent(context, SmsSentReceiver.class));
- transaction.setExplicitBroadcastForSentMms(new Intent(context, MmsSentReceiverImpl.class));
List addresses = new ArrayList<>();
for (SMSHelper.Address address : addressList) {
@@ -101,32 +138,182 @@ public class SmsMmsUtils {
}
Message message = new Message(textMessage, addresses.toArray(ArrayUtils.EMPTY_STRING_ARRAY));
+ message.setFromAddress(sendingPhoneNumber.number);
message.setSave(true);
// Sending MMS on android requires the app to be set as the default SMS app,
// but sending SMS doesn't needs the app to be set as the default app.
// This is the reason why there are separate branch handling for SMS and MMS.
if (transaction.checkMMS(message)) {
- if (Utils.isDefaultSmsApp(context)) {
- if (Utils.isMobileDataEnabled(context)) {
- com.klinker.android.logger.Log.v("", "Sending new MMS");
- transaction.sendNewMessage(message, Transaction.NO_THREAD_ID);
- }
+ Log.v("", "Sending new MMS");
+ //transaction.sendNewMessage(message, Transaction.NO_THREAD_ID);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
+ sendMmsMessageNative(context, message, settings);
} else {
- com.klinker.android.logger.Log.v(SENDING_MESSAGE, "KDE Connect is not set to default SMS app.");
- //TODO: Notify other end that they need to enable the mobile data in order to send MMS
+ // Cross fingers and hope Klinker's library works for this case
+ transaction.sendNewMessage(message, Transaction.NO_THREAD_ID);
}
} else {
- com.klinker.android.logger.Log.v(SENDING_MESSAGE, "Sending new SMS");
+ Log.v(SENDING_MESSAGE, "Sending new SMS");
transaction.sendNewMessage(message, Transaction.NO_THREAD_ID);
}
//TODO: Notify other end
} catch (Exception e) {
//TODO: Notify other end
- com.klinker.android.logger.Log.e(SENDING_MESSAGE, "Exception", e);
+ Log.e(SENDING_MESSAGE, "Exception", e);
}
}
+ /**
+ * Send an MMS message using SmsManager.sendMultimediaMessage
+ *
+ * @param context
+ * @param message
+ * @param klinkerSettings
+ */
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
+ protected static void sendMmsMessageNative(Context context, Message message, Settings klinkerSettings) {
+ ArrayList data = new ArrayList<>();
+
+ for (Message.Part p : message.getParts()) {
+ MMSPart part = new MMSPart();
+ if (p.getName() != null) {
+ part.Name = p.getName();
+ } else {
+ part.Name = p.getContentType().split("/")[0];
+ }
+ part.MimeType = p.getContentType();
+ part.Data = p.getMedia();
+ data.add(part);
+ }
+
+ if (message.getText() != null && !message.getText().equals("")) {
+ // add text to the end of the part and send
+ MMSPart part = new MMSPart();
+ part.Name = "text";
+ part.MimeType = "text/plain";
+ part.Data = message.getText().getBytes();
+ data.add(part);
+ }
+
+ SendReq sendReq = buildPdu(context, message.getFromAddress(), message.getAddresses(), message.getSubject(), data, klinkerSettings);
+
+ Bundle configOverrides = new Bundle();
+ configOverrides.putBoolean(SmsManager.MMS_CONFIG_GROUP_MMS_ENABLED, klinkerSettings.getGroup());
+
+ // Write the PDUs to disk so that we can pass them to the SmsManager
+ final String fileName = "send." + String.valueOf(Math.abs(new Random().nextLong())) + ".dat";
+ File mSendFile = new File(context.getCacheDir(), fileName);
+
+ Uri contentUri = (new Uri.Builder())
+ .authority(context.getPackageName() + ".MmsFileProvider")
+ .path(fileName)
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .build();
+
+ try (FileOutputStream writer = new FileOutputStream(mSendFile)) {
+ writer.write(new PduComposer(context, sendReq).make());
+ } catch (final IOException e)
+ {
+ android.util.Log.e(SENDING_MESSAGE, "Error while writing temporary PDU file: ", e);
+ }
+
+ SmsManager mSmsManager;
+
+ if (klinkerSettings.getSubscriptionId() < 0)
+ {
+ mSmsManager = SmsManager.getDefault();
+ } else {
+ mSmsManager = SmsManager.getSmsManagerForSubscriptionId(klinkerSettings.getSubscriptionId());
+ }
+
+ mSmsManager.sendMultimediaMessage(context, contentUri, null, null, null);
+ }
+
+ public static final long DEFAULT_EXPIRY_TIME = 7 * 24 * 60 * 60;
+ public static final int DEFAULT_PRIORITY = PduHeaders.PRIORITY_NORMAL;
+
+ /**
+ * Copy of the same-name method from https://github.com/klinker41/android-smsmms
+ */
+ private static SendReq buildPdu(Context context, String fromAddress, String[] recipients, String subject,
+ List parts, Settings settings) {
+ final SendReq req = new SendReq();
+ // From, per spec
+ req.prepareFromAddress(context, fromAddress, settings.getSubscriptionId());
+ // To
+ for (String recipient : recipients) {
+ req.addTo(new EncodedStringValue(recipient));
+ }
+ // Subject
+ if (!TextUtils.isEmpty(subject)) {
+ req.setSubject(new EncodedStringValue(subject));
+ }
+ // Date
+ req.setDate(System.currentTimeMillis() / 1000);
+ // Body
+ PduBody body = new PduBody();
+ // Add text part. Always add a smil part for compatibility, without it there
+ // may be issues on some carriers/client apps
+ int size = 0;
+ for (int i = 0; i < parts.size(); i++) {
+ MMSPart part = parts.get(i);
+ size += addTextPart(body, part, i);
+ }
+
+ // add a SMIL document for compatibility
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ SmilXmlSerializer.serialize(SmilHelper.createSmilDocument(body), out);
+ PduPart smilPart = new PduPart();
+ smilPart.setContentId("smil".getBytes());
+ smilPart.setContentLocation("smil.xml".getBytes());
+ smilPart.setContentType(ContentType.APP_SMIL.getBytes());
+ smilPart.setData(out.toByteArray());
+ body.addPart(0, smilPart);
+
+ req.setBody(body);
+ // Message size
+ req.setMessageSize(size);
+ // Message class
+ req.setMessageClass(PduHeaders.MESSAGE_CLASS_PERSONAL_STR.getBytes());
+ // Expiry
+ req.setExpiry(DEFAULT_EXPIRY_TIME);
+ try {
+ // Priority
+ req.setPriority(DEFAULT_PRIORITY);
+ // Delivery report
+ req.setDeliveryReport(PduHeaders.VALUE_NO);
+ // Read report
+ req.setReadReport(PduHeaders.VALUE_NO);
+ } catch (InvalidHeaderValueException e) {}
+
+ return req;
+ }
+
+ /**
+ * Copy of the same-name method from https://github.com/klinker41/android-smsmms
+ */
+ private static int addTextPart(PduBody pb, MMSPart p, int id) {
+ String filename = p.Name;
+ final PduPart part = new PduPart();
+ // Set Charset if it's a text media.
+ if (p.MimeType.startsWith("text")) {
+ part.setCharset(CharacterSets.UTF_8);
+ }
+ // Set Content-Type.
+ part.setContentType(p.MimeType.getBytes());
+ // Set Content-Location.
+ part.setContentLocation(filename.getBytes());
+ int index = filename.lastIndexOf(".");
+ String contentId = (index == -1) ? filename
+ : filename.substring(0, index);
+ part.setContentId(contentId.getBytes());
+ part.setData(p.Data);
+ pb.addPart(part);
+
+ return part.getData().length;
+ }
+
/**
* Returns the Address of the sender of the MMS message.
* @return sender's Address
diff --git a/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsReceiver.java b/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsReceiver.java
deleted file mode 100644
index 65f600ec..00000000
--- a/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsReceiver.java
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2020 Aniket Kumar
- *
- * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
- */
-
-package org.kde.kdeconnect.Plugins.SMSPlugin;
-
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build;
-import android.os.Bundle;
-import android.telephony.SmsMessage;
-import android.provider.Telephony.Sms;
-import android.net.Uri;
-import android.content.ContentValues;
-
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.Person;
-import androidx.core.app.RemoteInput;
-import androidx.core.content.ContextCompat;
-
-import com.klinker.android.send_message.Transaction;
-import com.klinker.android.send_message.Utils;
-
-import org.kde.kdeconnect.Helpers.NotificationHelper;
-import org.kde.kdeconnect_tp.R;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-
-public class SmsReceiver extends BroadcastReceiver {
-
- private static final String SMS_RECEIVED = "android.provider.Telephony.SMS_RECEIVED";
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (!Utils.isDefaultSmsApp(context)) {
- return;
- }
-
- if (intent != null && intent.getAction().equals(SMS_RECEIVED)) {
- Bundle dataBundle = intent.getExtras();
-
- if (dataBundle != null) {
- Object[] smsExtra = (Object[]) dataBundle.get("pdus");
- final SmsMessage[] message = new SmsMessage[smsExtra.length];
-
- for (int i = 0; i < smsExtra.length; ++i) {
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- String format = dataBundle.getString("format");
- message[i] = SmsMessage.createFromPdu((byte[]) smsExtra[i], format);
- } else {
- message[i] = SmsMessage.createFromPdu((byte[]) smsExtra[i]);
- }
-
- // Write the received sms to the sms provider
- for (SmsMessage msg : message) {
- ContentValues values = new ContentValues();
- values.put(Sms.ADDRESS, msg.getDisplayOriginatingAddress());
- values.put(Sms.BODY, msg.getMessageBody());
- values.put(Sms.DATE, System.currentTimeMillis()+"");
- values.put(Sms.TYPE, Sms.MESSAGE_TYPE_INBOX);
- values.put(Sms.STATUS, msg.getStatus());
- values.put(Sms.READ, 0);
- values.put(Sms.SEEN, 0);
- context.getApplicationContext().getContentResolver().insert(Uri.parse("content://sms/"), values);
-
- // Notify messageUpdateReceiver about the arrival of the new sms message
- Intent refreshIntent = new Intent(Transaction.REFRESH);
- context.sendBroadcast(refreshIntent);
- }
-
- String body = message[i].getMessageBody();
- String phoneNo = message[i].getOriginatingAddress();
- long date = message[i].getTimestampMillis();
-
- createSmsNotification(context, body, phoneNo, date);
- }
- }
- }
- }
-
- private void createSmsNotification(Context context, String body, String phoneNo, long date) {
- int notificationId;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- notificationId = (int) Utils.getOrCreateThreadId(context, phoneNo);
- } else {
- notificationId = (int) System.currentTimeMillis();
- }
-
- Person sender = NotificationReplyReceiver.getMessageSender(context, phoneNo);
-
- ArrayList addressList = new ArrayList<>(Arrays.asList(phoneNo));
-
- // Create pending intent for reply action through notification
- PendingIntent replyPendingIntent = NotificationReplyReceiver.createReplyPendingIntent(
- context,
- addressList,
- notificationId,
- true
- );
-
- RemoteInput remoteReplyInput = new RemoteInput.Builder(NotificationReplyReceiver.KEY_TEXT_REPLY)
- .setLabel(context.getString(R.string.message_reply_label))
- .build();
-
- NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(0, context.getString(R.string.message_reply_label), replyPendingIntent)
- .addRemoteInput(remoteReplyInput)
- .setAllowGeneratedReplies(true)
- .build();
-
- // Create pending intent for marking the message as read in database through mark as read action
- PendingIntent markAsReadPendingIntent = NotificationReplyReceiver.createMarkAsReadPendingIntent(
- context,
- addressList,
- notificationId
- );
-
- NotificationCompat.Action markAsReadAction = new NotificationCompat.Action.Builder(0, context.getString(R.string.mark_as_read_label), markAsReadPendingIntent)
- .build();
-
- NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- NotificationCompat.MessagingStyle messagingStyle = NotificationReplyReceiver.createMessagingStyle(
- context,
- notificationId,
- body,
- phoneNo,
- date,
- sender,
- notificationManager
- );
-
- NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NotificationReplyReceiver.CHANNEL_ID)
- .setSmallIcon(R.drawable.ic_baseline_sms_24)
- .setColor(ContextCompat.getColor(context, R.color.primary))
- .setPriority(NotificationCompat.PRIORITY_DEFAULT)
- .setStyle(messagingStyle)
- .setAutoCancel(true)
- .addAction(replyAction)
- .addAction(markAsReadAction)
- .setGroup(NotificationReplyReceiver.SMS_NOTIFICATION_GROUP_KEY);
-
- NotificationHelper.notifyCompat(notificationManager, notificationId, builder.build());
- }
-}
diff --git a/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsSentReceiver.java b/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsSentReceiver.java
deleted file mode 100644
index b8af1668..00000000
--- a/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsSentReceiver.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2020 Aniket Kumar
- *
- * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
- */
-
-package org.kde.kdeconnect.Plugins.SMSPlugin;
-
-import android.content.Context;
-import android.content.Intent;
-
-import com.klinker.android.send_message.SentReceiver;
-import com.klinker.android.send_message.Transaction;
-import com.klinker.android.send_message.Utils;
-
-import org.kde.kdeconnect.Helpers.SMSHelper;
-
-import java.util.ArrayList;
-
-public class SmsSentReceiver extends SentReceiver {
-
- @Override
- public void updateInInternalDatabase(Context context, Intent intent, int receiverResultCode) {
- super.updateInInternalDatabase(context, intent, receiverResultCode);
-
- if (Utils.isDefaultSmsApp(context)) {
- // Notify messageUpdateReceiver about the successful sending of the sms message
- Intent refreshIntent = new Intent(Transaction.REFRESH);
- context.sendBroadcast(refreshIntent);
- }
- }
-
- @Override
- public void onMessageStatusUpdated(Context context, Intent intent, int receiverResultCode) {
- SMSHelper.Message message = SMSHelper.getNewestMessage(context);
-
- ArrayList addressList = new ArrayList<>();
- for (SMSHelper.Address address : message.addresses) {
- addressList.add(address.toString());
- }
-
- Intent repliedNotification = new Intent(context, NotificationReplyReceiver.class);
- repliedNotification.setAction(NotificationReplyReceiver.SMS_MMS_REPLY_ACTION);
- repliedNotification.putExtra(NotificationReplyReceiver.TEXT_BODY, message.body);
- repliedNotification.putExtra(NotificationReplyReceiver.NOTIFICATION_ID, Integer.parseInt(message.threadID.toString()));
- repliedNotification.putExtra(NotificationReplyReceiver.ADDRESS_LIST, addressList);
-
- // SEND_ACTION value is required to differentiate between the intents sent from reply action or
- // SentReceivers inorder to avoid posting duplicate notifications
- repliedNotification.putExtra(NotificationReplyReceiver.SEND_ACTION, false);
-
- context.sendBroadcast(repliedNotification);
- }
-}
diff --git a/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java b/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java
index fc783d20..5f0f61fb 100644
--- a/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java
+++ b/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java
@@ -296,44 +296,6 @@ public class DeviceFragment extends Fragment {
dialog.show(getChildFragmentManager(), null);
}
});
-
- // Add a button to the pluginList for setting KDE Connect as default sms app for allowing it to send mms
- if (!Utils.isDefaultSmsApp(mActivity)) {
- // Check if there are any preferred APN settings available on the device, if not then disable the MMS support
- boolean hasApnSettings = false;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
- List subIds = TelephonyHelper.getActiveSubscriptionIDs(mActivity);
- for (final int subId : subIds) {
- if (TelephonyHelper.getPreferredApn(mActivity, subId) != null) {
- hasApnSettings = true;
- break;
- }
- }
- } else {
- if (TelephonyHelper.getPreferredApn(mActivity, 0) != null) {
- hasApnSettings = true;
- }
- }
-
- for (Plugin p : plugins) {
- if (p.getPluginKey().equals("SMSPlugin") && hasApnSettings) {
- pluginListItems.add(new SetDefaultAppPluginListItem(p, mActivity.getResources().getString(R.string.pref_plugin_telepathy_mms), (action) -> {
- DialogFragment dialog = new DefaultSmsAppAlertDialogFragment.Builder()
- .setTitle(R.string.set_default_sms_app_title)
- .setMessage(R.string.pref_plugin_telepathy_mms_desc)
- .setPositiveButton(R.string.ok)
- .setNegativeButton(R.string.cancel)
- .setPermissions(SMSPlugin.getMmsPermissions())
- .setRequestCode(MainActivity.RESULT_NEEDS_RELOAD)
- .create();
-
- if (dialog != null) {
- dialog.show(getChildFragmentManager(), null);
- }
- }));
- }
- }
- }
}
ListAdapter adapter = new ListAdapter(mActivity, pluginListItems);