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