mirror of
https://github.com/KDE/kdeconnect-android
synced 2025-08-31 14:15:14 +00:00
[SMS App] Allow sending MMS while not the default messaging app
## Summary Use [SmsManager.SendMultimediaMessage](https://developer.android.com/reference/android/telephony/SmsManager#sendMultimediaMessage(android.content.Context,%20android.net.Uri,%20java.lang.String,%20android.os.Bundle,%20android.app.PendingIntent)) for sending MMS This allows us to remove the handling for being the default MMS app, which the Google Play Store is not allowing us to claim Additionally, rip out the parts of the app necessary for being the default handler (notifications, receivers, etc.) and and make the handling for changes in the message database less fragile (handle more messages at a time and handle MMS) This is a collaboration between myself and Albert Vaca ## Test Plan Sent a few plain-text and group MMS. It "works for me".
This commit is contained in:
@@ -53,22 +53,6 @@
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:theme="@style/KdeConnectTheme"
|
||||
android:name="org.kde.kdeconnect.MyApplication">
|
||||
<receiver
|
||||
android:name="org.kde.kdeconnect.Plugins.SMSPlugin.SmsReceiver"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BROADCAST_SMS">
|
||||
<intent-filter>
|
||||
<action android:name="android.provider.Telephony.SMS_DELIVER" />
|
||||
</intent-filter>
|
||||
<intent-filter android:priority="999">
|
||||
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name="org.kde.kdeconnect.Plugins.SMSPlugin.SmsSentReceiver"
|
||||
android:exported="true"
|
||||
android:taskAffinity="${applicationId}.SMS_SENT" />
|
||||
|
||||
<receiver
|
||||
android:name="com.android.mms.transaction.PushReceiver"
|
||||
@@ -81,39 +65,6 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name="org.kde.kdeconnect.Plugins.SMSPlugin.DelegatingMmsReceivedReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:taskAffinity="com.klinker.android.messaging.MMS_RECEIVED" />
|
||||
|
||||
<receiver
|
||||
android:name="org.kde.kdeconnect.Plugins.SMSPlugin.MmsSentReceiverImpl"
|
||||
android:exported="true"
|
||||
android:taskAffinity="com.klinker.android.messaging.MMS_SENT" />
|
||||
|
||||
<receiver
|
||||
android:name="org.kde.kdeconnect.Plugins.SMSPlugin.NotificationReplyReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name="org.kde.kdeconnect.Plugins.SMSPlugin.HeadlessSmsSendService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:scheme="sms" />
|
||||
<data android:scheme="smsto" />
|
||||
<data android:scheme="mms" />
|
||||
<data android:scheme="mmsto" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="com.android.mms.transaction.TransactionService"
|
||||
android:enabled="true"
|
||||
@@ -139,23 +90,10 @@
|
||||
android:name="org.kde.kdeconnect.UserInterface.MainActivity"
|
||||
android:label="KDE Connect"
|
||||
android:theme="@style/KdeConnectTheme.NoActionBar">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.SENDTO" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="sms" />
|
||||
<data android:scheme="smsto" />
|
||||
<data android:scheme="mms" />
|
||||
<data android:scheme="mmsto" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.app.role.SMS"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
|
@@ -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<Message> 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<String> smsSelectionArgs = new ArrayList<String>(2);
|
||||
smsSelectionArgs.add(startTimestamp.toString());
|
||||
|
||||
List<String> mmsSelectionArgs = new ArrayList<String>(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<Message> allMessages = getMessages(smsUri, context, allSmsColumns, selection, smsSelectionArgs, sortOrder, numberToGet);
|
||||
allMessages.addAll(getMessages(mmsUri, context, allMmsColumns, selection, mmsSelectionArgs, sortOrder, numberToGet));
|
||||
List<Message> 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<Message> 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<String> 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<TelephonyHelper.LocalPhoneNumber> 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<String, String> messageInfo,
|
||||
@NonNull List<String> userPhoneNumbers
|
||||
@NonNull List<TelephonyHelper.LocalPhoneNumber> userPhoneNumbers
|
||||
) {
|
||||
int event = Message.EVENT_UNKNOWN;
|
||||
|
||||
@@ -720,14 +705,18 @@ public class SMSHelper {
|
||||
|
||||
List<Address> 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
|
||||
|
@@ -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<String> getAllPhoneNumbers(
|
||||
public static @NonNull List<LocalPhoneNumber> 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<String> phoneNumbers = new ArrayList<>(subscriptionInfos.size());
|
||||
List<LocalPhoneNumber> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -1,24 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Aniket Kumar <anikketkumar786@gmail.com>
|
||||
*
|
||||
* 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);
|
||||
}
|
||||
}
|
@@ -1,24 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Aniket Kumar <anikketkumar786@gmail.com>
|
||||
*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
@@ -1,163 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Aniket Kumar <anikketkumar786@gmail.com>
|
||||
*
|
||||
* 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<String> 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());
|
||||
}
|
||||
}
|
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Aniket Kumar <anikketkumar786@gmail.com>
|
||||
*
|
||||
* 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<String> 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);
|
||||
}
|
||||
}
|
@@ -1,289 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Aniket Kumar <anikketkumar786@gmail.com>
|
||||
*
|
||||
* 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<String> 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<SMSHelper.Address> 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<String> 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<String, String> 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<String, String> 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<String> 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<String> 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;
|
||||
}
|
||||
}
|
@@ -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<SMSHelper.Message> 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
|
||||
|
@@ -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<TelephonyHelper.LocalPhoneNumber> allPhoneNumbers = TelephonyHelper.getAllPhoneNumbers(context);
|
||||
|
||||
Optional<TelephonyHelper.LocalPhoneNumber> 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<String> 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<MMSPart> 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<MMSPart> 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
|
||||
|
@@ -1,150 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Aniket Kumar <anikketkumar786@gmail.com>
|
||||
*
|
||||
* 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<String> 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());
|
||||
}
|
||||
}
|
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Aniket Kumar <anikketkumar786@gmail.com>
|
||||
*
|
||||
* 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<String> 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);
|
||||
}
|
||||
}
|
@@ -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<Integer> 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);
|
||||
|
Reference in New Issue
Block a user