From b119de8e76e24c6dd4ce58210a0f1d13a482b2a9 Mon Sep 17 00:00:00 2001 From: Aniket Kumar Date: Sun, 5 Jul 2020 13:32:44 +0530 Subject: [PATCH] Added MMS support to the SMSPlugin using Klinker library. --- AndroidManifest.xml | 90 ++++++++++- build.gradle | 2 + proguard-rules.pro | 4 + res/values/strings.xml | 34 ++++ res/xml/smsplugin_preferences.xml | 51 ++++++ src/org/kde/kdeconnect/Helpers/SMSHelper.java | 109 +++++++++++++ .../DelegatingMmsReceivedReceiver.java | 38 +++++ .../SMSPlugin/HeadlessSmsSendService.java | 38 +++++ .../SMSPlugin/MmsReceivedReceiver.java | 86 ++++++++++ .../Plugins/SMSPlugin/MmsSentReceiver.java | 45 ++++++ .../Plugins/SMSPlugin/SMSPlugin.java | 147 ++++++++++++++---- .../Plugins/SMSPlugin/SmsMmsUtils.java | 121 ++++++++++++++ .../Plugins/SMSPlugin/SmsReceiver.java | 82 ++++++++++ .../Plugins/SMSPlugin/SmsSentReceiver.java | 46 ++++++ .../DefaultSmsAppAlertDialogFragment.java | 101 ++++++++++++ .../UserInterface/DeviceFragment.java | 27 ++++ .../List/SetDefaultAppPluginListItem.java | 33 ++++ 17 files changed, 1023 insertions(+), 31 deletions(-) create mode 100644 res/xml/smsplugin_preferences.xml create mode 100644 src/org/kde/kdeconnect/Plugins/SMSPlugin/DelegatingMmsReceivedReceiver.java create mode 100644 src/org/kde/kdeconnect/Plugins/SMSPlugin/HeadlessSmsSendService.java create mode 100644 src/org/kde/kdeconnect/Plugins/SMSPlugin/MmsReceivedReceiver.java create mode 100644 src/org/kde/kdeconnect/Plugins/SMSPlugin/MmsSentReceiver.java create mode 100644 src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsMmsUtils.java create mode 100644 src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsReceiver.java create mode 100644 src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsSentReceiver.java create mode 100644 src/org/kde/kdeconnect/UserInterface/DefaultSmsAppAlertDialogFragment.java create mode 100644 src/org/kde/kdeconnect/UserInterface/List/SetDefaultAppPluginListItem.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 16cdf2b3..8266ff0e 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -22,6 +22,8 @@ + + @@ -31,9 +33,13 @@ - + + + + + @@ -49,6 +55,67 @@ android:networkSecurityConfig="@xml/network_security_config" android:theme="@style/KdeConnectTheme" android:name="org.kde.kdeconnect.MyApplication"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -69,10 +136,29 @@ android:name="org.kde.kdeconnect.UserInterface.MainActivity" android:label="KDE Connect" android:theme="@style/KdeConnectTheme.NoActionBar"> + + + + + + + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/build.gradle b/build.gradle index a6880aa2..56b2558e 100644 --- a/build.gradle +++ b/build.gradle @@ -165,6 +165,8 @@ dependencies { implementation 'org.atteo.classindex:classindex:3.6' annotationProcessor 'org.atteo.classindex:classindex:3.6' + implementation 'com.klinkerapps:android-smsmms:5.2.6' //For SMS and MMS purposes + // Testing testImplementation 'junit:junit:4.12' testImplementation 'org.powermock:powermock-core:2.0.0' diff --git a/proguard-rules.pro b/proguard-rules.pro index 838aab66..c643b628 100644 --- a/proguard-rules.pro +++ b/proguard-rules.pro @@ -44,3 +44,7 @@ -dontwarn android.test.** -dontwarn java.lang.management.** -dontwarn javax.** + +-dontwarn android.net.ConnectivityManager +-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement +-dontwarn android.net.LinkProperties diff --git a/res/values/strings.xml b/res/values/strings.xml index 73f09b7a..c938291c 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -265,6 +265,8 @@ There are no file browsers installed. Send SMS Send text messages from your desktop + Send MMS + To be able to send MMS from KDE Connect you need to set it as the default SMS app. Find my phone Find my tablet Find my TV @@ -372,6 +374,38 @@ To share microphone input from your phone you need to give access to the phone\'s audio input Speech + Send MMS + Send group MMS + set_group_message_as_mms + Send long text as MMS + set_long_text_as_mms + Convert to MMS + convert_to_mms_after + Set MMSC + MMSC + sms_pref_set_mmsc + Set MMS proxy + MMS proxy + sms_pref_set_mms_proxy + Set MMS port + MMS port + sms_pref_set_mms_port + 3 + + After one message + After two messages + After three messages + After four messages + After five messages + + + 1 + 2 + 3 + 4 + 5 + + Choose theme Set by Battery Saver diff --git a/res/xml/smsplugin_preferences.xml b/res/xml/smsplugin_preferences.xml new file mode 100644 index 00000000..77fb5ce9 --- /dev/null +++ b/res/xml/smsplugin_preferences.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + diff --git a/src/org/kde/kdeconnect/Helpers/SMSHelper.java b/src/org/kde/kdeconnect/Helpers/SMSHelper.java index 899fc267..8d251526 100644 --- a/src/org/kde/kdeconnect/Helpers/SMSHelper.java +++ b/src/org/kde/kdeconnect/Helpers/SMSHelper.java @@ -61,6 +61,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import com.klinker.android.send_message.Utils; + @SuppressLint("InlinedApi") public class SMSHelper { @@ -295,6 +297,13 @@ public class SMSHelper { // 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); + } + try (Cursor myCursor = context.getContentResolver().query( uri, fetchColumns.toArray(new String[]{}), @@ -376,6 +385,83 @@ public class SMSHelper { return toReturn; } + /** + * Deletes messages which are failed to send due to some reason + * + * @param uri Uri indicating the messages database to read + * @param context android.content.Context running the request. + * @param fetchColumns List of columns to fetch + * @param selection Parameterizable filter to use with the ContentResolver query. May be null. + * @param selectionArgs Parameters for selection. May be null. + * @param sortOrder Sort ordering passed to Android's content resolver. May be null for unspecified + */ + private static void deleteFailedMessages( + @NonNull Uri uri, + @NonNull Context context, + @NonNull Collection fetchColumns, + @Nullable String selection, + @Nullable String[] selectionArgs, + @Nullable String sortOrder + ) { + try (Cursor myCursor = context.getContentResolver().query( + uri, + fetchColumns.toArray(new String[]{}), + selection, + selectionArgs, + sortOrder) + ) { + if (myCursor != null && myCursor.moveToFirst()) { + do { + String id = null; + String type = null; + String msgBox = null; + + for (int columnIdx = 0; columnIdx < myCursor.getColumnCount(); columnIdx++) { + String colName = myCursor.getColumnName(columnIdx); + + if (colName.equals("_id")) { + id = myCursor.getString(columnIdx); + } + + if(colName.equals("type")) { + type = myCursor.getString(columnIdx); + } + + if (colName.equals("msg_box")) { + msgBox = myCursor.getString(columnIdx); + } + } + + if (type != null && id != null) { + if (type.equals(Telephony.Sms.MESSAGE_TYPE_OUTBOX) || type.equals(Telephony.Sms.MESSAGE_TYPE_FAILED)) { + Log.v("Deleting sms", "content://sms/" + id); + context.getContentResolver().delete(Uri.parse("content://sms/" + id), null, null); + } + } + + if (msgBox != null && id != null) { + if (msgBox.equals(Telephony.Mms.MESSAGE_BOX_OUTBOX) || msgBox.equals(Telephony.Mms.MESSAGE_BOX_FAILED)) { + Log.v("Deleting mms", "content://mms/" + id); + context.getContentResolver().delete(Uri.parse("content://mms/" + id), null, null); + } + } + } while (myCursor.moveToNext()); + } + } catch (SQLiteException e) { + String[] unfilteredColumns = {}; + try (Cursor unfilteredColumnsCursor = context.getContentResolver().query(uri, null, null, null, null)) { + if (unfilteredColumnsCursor != null) { + unfilteredColumns = unfilteredColumnsCursor.getColumnNames(); + } + } + if (unfilteredColumns.length == 0) { + throw new MessageAccessException(uri, e); + } else { + throw new MessageAccessException(unfilteredColumns, uri, e); + } + } + } + /** * Gets messages which match the selection * @@ -818,6 +904,29 @@ public class SMSHelper { } } + /** + * converts a given JSONArray into List
+ */ + public static List
jsonArrayToAddressList(JSONArray jsonArray) { + if (jsonArray == null) { + return null; + } + + List
addresses = new ArrayList<>(); + try { + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject jsonObject = jsonArray.getJSONObject(i); + String address = jsonObject.getString("address"); + addresses.add(new Address(address)); + Log.e("address", address); + } + } catch (Exception e) { + e.printStackTrace(); + } + + return addresses; + } + /** * Indicate that some error has occurred while reading a message. * More useful for logging than catching and handling diff --git a/src/org/kde/kdeconnect/Plugins/SMSPlugin/DelegatingMmsReceivedReceiver.java b/src/org/kde/kdeconnect/Plugins/SMSPlugin/DelegatingMmsReceivedReceiver.java new file mode 100644 index 00000000..8e6c959c --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/SMSPlugin/DelegatingMmsReceivedReceiver.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020 Aniket Kumar + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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.loadFromPreferences(context); + delegate.onReceive(context, intent); + } +} diff --git a/src/org/kde/kdeconnect/Plugins/SMSPlugin/HeadlessSmsSendService.java b/src/org/kde/kdeconnect/Plugins/SMSPlugin/HeadlessSmsSendService.java new file mode 100644 index 00000000..29202c86 --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/SMSPlugin/HeadlessSmsSendService.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020 Aniket Kumar + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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 new file mode 100644 index 00000000..be817587 --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/SMSPlugin/MmsReceivedReceiver.java @@ -0,0 +1,86 @@ +/* + * Copyright 2020 Aniket Kumar + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kde.kdeconnect.Plugins.SMSPlugin; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.preference.PreferenceManager; +import android.util.Log; + +import com.klinker.android.send_message.Transaction; + +import org.kde.kdeconnect_tp.R; + +/** + * 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 notification service to notify the users will be + * implemented later. + */ +public class MmsReceivedReceiver extends com.klinker.android.send_message.MmsReceivedReceiver { + + private String mmscUrl = null; + private String mmsProxy = null; + private String mmsPort = 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); + } + + @Override + public void onError(Context context, String error) { + Log.v("MmsReceived", "error: " + error); + } + + public void loadFromPreferences(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + mmscUrl = prefs.getString(context.getString(R.string.sms_pref_set_mmsc), ""); + mmsProxy = prefs.getString(context.getString(R.string.sms_pref_set_mms_proxy), ""); + mmsPort = prefs.getString(context.getString(R.string.sms_pref_set_mms_port), ""); + } + + /** + * 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 (mmscUrl != null || mmsProxy != null || mmsPort != null) { + try { + return new MmscInformation(mmscUrl, mmsProxy, Integer.parseInt(mmsPort)); + } catch (Exception e) { + Log.e("MmsReceivedReceiver", "Exception", e); + return null; + } + } else { + return null; + } + } +} diff --git a/src/org/kde/kdeconnect/Plugins/SMSPlugin/MmsSentReceiver.java b/src/org/kde/kdeconnect/Plugins/SMSPlugin/MmsSentReceiver.java new file mode 100644 index 00000000..3bf40b97 --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/SMSPlugin/MmsSentReceiver.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020 Aniket Kumar + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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; + +public class MmsSentReceiver 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) { + } +} diff --git a/src/org/kde/kdeconnect/Plugins/SMSPlugin/SMSPlugin.java b/src/org/kde/kdeconnect/Plugins/SMSPlugin/SMSPlugin.java index 0a49288d..71daecaa 100644 --- a/src/org/kde/kdeconnect/Plugins/SMSPlugin/SMSPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/SMSPlugin/SMSPlugin.java @@ -39,7 +39,6 @@ import android.provider.Telephony; import android.telephony.PhoneNumberUtils; import android.telephony.SmsManager; import android.telephony.SmsMessage; -import android.util.Log; import org.json.JSONArray; import org.json.JSONException; @@ -63,6 +62,11 @@ import java.util.concurrent.locks.ReentrantLock; import androidx.core.content.ContextCompat; +import com.klinker.android.send_message.ApnUtils; +import com.klinker.android.send_message.Transaction; +import com.klinker.android.send_message.Utils; +import com.klinker.android.logger.Log; + import static org.kde.kdeconnect.Plugins.TelephonyPlugin.TelephonyPlugin.PACKET_TYPE_TELEPHONY; @PluginFactory.LoadablePlugin @@ -125,9 +129,10 @@ public class SMSPlugin extends Plugin { *

* The body should look like so: * { "sendSms": true, - * "phoneNumber": "542904563213", - * "messageBody": "Hi mom!", - * "sub_id": "3859358340534" + * "phoneNumber": "542904563213" // For older desktop versions of SMS app this packet carries phoneNumber field + * "addresses": // For newer desktop versions of SMS app it contains addresses field instead of phoneNumber field + * "messageBody": "Hi mom!", + * "sub_id": "3859358340534" * } */ private final static String PACKET_TYPE_SMS_REQUEST = "kdeconnect.sms.request"; @@ -210,33 +215,64 @@ public class SMSPlugin extends Plugin { */ @Override public void onChange(boolean selfChange) { - // Lock so no one uses the mostRecentTimestamp between the moment we read it and the - // moment we update it. This is because reading the Messages DB can take long. - mostRecentTimestampLock.lock(); - - if (mostRecentTimestamp == 0) { - // Since the timestamp has not been initialized, we know that nobody else - // has requested a message. That being the case, there is most likely - // nobody listening for message updates, so just drop them - mostRecentTimestampLock.unlock(); + // 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; } - SMSHelper.Message message = SMSHelper.getNewestMessage(context); - - if (message == null || message.date <= mostRecentTimestamp) { - // onChange can trigger many times for a single message. Don't make unnecessary noise - mostRecentTimestampLock.unlock(); - return; - } - - // Update the most recent counter - mostRecentTimestamp = message.date; - mostRecentTimestampLock.unlock(); - - // Send the alert about the update - device.sendPacket(constructBulkMessagePacket(Collections.singleton(message))); + sendLatestMessage(); } + + } + + /** + * This receiver will be invoked only when the app will be set as the default sms app + * Whenever the app will be set as the default, the database update alert will be sent + * using messageUpdateReceiver and not the contentObserver class + */ + private final BroadcastReceiver messagesUpdateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + + String action = intent.getAction(); + + if (Transaction.REFRESH.equals(action)) { + sendLatestMessage(); + } + } + }; + + /** + * Helper method to read the latest message from the sms-mms database and sends it to the desktop + */ + private void sendLatestMessage() { + // Lock so no one uses the mostRecentTimestamp between the moment we read it and the + // moment we update it. This is because reading the Messages DB can take long. + mostRecentTimestampLock.lock(); + + if (mostRecentTimestamp == 0) { + // Since the timestamp has not been initialized, we know that nobody else + // has requested a message. That being the case, there is most likely + // nobody listening for message updates, so just drop them + mostRecentTimestampLock.unlock(); + return; + } + SMSHelper.Message message = SMSHelper.getNewestMessage(context); + + if (message == null || message.date <= mostRecentTimestamp) { + // onChange can trigger many times for a single message. Don't make unnecessary noise + mostRecentTimestampLock.unlock(); + return; + } + + // Update the most recent counter + mostRecentTimestamp = message.date; + mostRecentTimestampLock.unlock(); + + // Send the alert about the update + device.sendPacket(constructBulkMessagePacket(Collections.singleton(message))); + Log.e("sent", "update"); } /** @@ -304,6 +340,10 @@ public class SMSPlugin extends Plugin { filter.setPriority(500); context.registerReceiver(receiver, filter); + IntentFilter refreshFilter = new IntentFilter(Transaction.REFRESH); + refreshFilter.setPriority(500); + context.registerReceiver(messagesUpdateReceiver, refreshFilter); + Looper helperLooper = SMSHelper.MessageLooper.getLooper(); ContentObserver messageObserver = new MessageContentObserver(new Handler(helperLooper)); SMSHelper.registerObserver(messageObserver, context); @@ -312,6 +352,13 @@ public class SMSPlugin extends Plugin { Log.w("SMSPlugin", "This is a very old version of Android. The SMS Plugin might not function as intended."); } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + ApnUtils.initDefaultApns(context, null); + } + + // To see debug messages for Klinker library, uncomment the below line + //Log.setDebug(true); + return true; } @@ -334,8 +381,22 @@ public class SMSPlugin extends Plugin { case PACKET_TYPE_SMS_REQUEST_CONVERSATION: return this.handleRequestConversation(np); case PACKET_TYPE_SMS_REQUEST: - // Fall through to old-style handling - // This space may be filled in differently once MMS support is implemented + if (np.getBoolean("sendSms")) { + String textMessage = np.getString("messageBody"); + long subID = np.getLong("subID", -1); + + List addressList = SMSHelper.jsonArrayToAddressList(np.getJSONArray("addresses")); + if (addressList == null) { + // If the List of Address is null, then the SMS_REQUEST packet is + // most probably from the older version of the desktop app. + addressList = new ArrayList<>(); + addressList.add(new SMSHelper.Address(np.getString("phoneNumber"))); + } + + SmsMmsUtils.sendMessage(context, textMessage, addressList, (int) subID); + } + break; + case TelephonyPlugin.PACKET_TYPE_TELEPHONY_REQUEST: if (np.getBoolean("sendSms")) { String phoneNo = np.getString("phoneNumber"); @@ -432,6 +493,17 @@ public class SMSPlugin extends Plugin { conversation = SMSHelper.getMessagesInRange(this.context, threadID, rangeStartTimestamp, numberToGet); } + // Sometimes when desktop app is kept open while android app is restarted for any reason + // mostRecentTimeStamp must be updated in that scenario too if a user request for a + // single conversation and not the entire conversation list + mostRecentTimestampLock.lock(); + for (SMSHelper.Message message : conversation) { + if (message.date > mostRecentTimestamp) { + mostRecentTimestamp = message.date; + } + } + mostRecentTimestampLock.unlock(); + NetworkPacket reply = constructBulkMessagePacket(conversation); device.sendPacket(reply); @@ -451,6 +523,10 @@ public class SMSPlugin extends Plugin { return false; } + @Override + public boolean hasSettings() { + return true; + } @Override public String[] getSupportedPacketTypes() { @@ -478,6 +554,19 @@ public class SMSPlugin extends Plugin { }; } + /** + * Permissions required for sending and receiving MMs messages + */ + public static String[] getMmsPermissions() { + return new String[]{ + Manifest.permission.RECEIVE_SMS, + Manifest.permission.RECEIVE_MMS, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.CHANGE_NETWORK_STATE, + Manifest.permission.WAKE_LOCK, + }; + } + /** * With versions older than KITKAT, lots of the content providers used in SMSHelper become * un-documented. Most manufacturers *did* do things the same way as was done in mainline diff --git a/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsMmsUtils.java b/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsMmsUtils.java new file mode 100644 index 00000000..ee60270a --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsMmsUtils.java @@ -0,0 +1,121 @@ +/* + * Copyright 2020 Aniket Kumar + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kde.kdeconnect.Plugins.SMSPlugin; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Build; +import android.preference.PreferenceManager; + +import com.klinker.android.send_message.Message; +import com.klinker.android.send_message.MmsSentReceiver; +import com.klinker.android.send_message.Settings; +import com.klinker.android.send_message.Transaction; +import com.klinker.android.send_message.Utils; + +import org.kde.kdeconnect.Helpers.SMSHelper; +import org.kde.kdeconnect_tp.R; + +import java.util.ArrayList; +import java.util.List; + +public class SmsMmsUtils { + + private static final String SENDING_MESSAGE = "Sending message"; + + /** + * Sends SMS or MMS message. + * + * @param context context in which the method is called. + * @param textMessage text body of the message to be sent. + * @param addressList List of addresses. + * @param subID Note that here subID is of type int and not long because klinker library requires it as int + * I don't really know the exact reason why they implemented it as int instead of long + */ + public static void sendMessage(Context context, String textMessage, List addressList, int subID) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + boolean longTextAsMms = prefs.getBoolean(context.getString(R.string.set_long_text_as_mms), false); + boolean groupMessageAsMms = prefs.getBoolean(context.getString(R.string.set_group_message_as_mms), true); + int sendLongAsMmsAfter = Integer.parseInt( + prefs.getString(context.getString(R.string.convert_to_mms_after), + context.getString(R.string.convert_to_mms_after_default))); + + try { + Settings settings = new Settings(); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + // If the build version is less than lollipop then we have to manually take the APN settings + // from the user in order to be able to send MMS. + settings.setMmsc(prefs.getString(context.getString(R.string.sms_pref_set_mmsc), "")); + settings.setProxy(prefs.getString(context.getString(R.string.sms_pref_set_mms_proxy), "")); + settings.setPort(prefs.getString(context.getString(R.string.sms_pref_set_mms_port), "")); + } + + settings.setUseSystemSending(true); + + if (Utils.isDefaultSmsApp(context)) { + settings.setSendLongAsMms(longTextAsMms); + settings.setSendLongAsMmsAfter(sendLongAsMmsAfter); + } + + settings.setGroup(groupMessageAsMms); + + if (subID != -1) { + settings.setSubscriptionId(subID); + } + + Transaction transaction = new Transaction(context, settings); + transaction.setExplicitBroadcastForSentSms(new Intent(context, SmsSentReceiver.class)); + transaction.setExplicitBroadcastForSentMms(new Intent(context, MmsSentReceiver.class)); + + List addresses = new ArrayList<>(); + for (SMSHelper.Address address : addressList) { + addresses.add(address.toString()); + } + + Message message = new Message(textMessage, addresses.toArray(new String[0])); + 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); + } + } 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 + } + } else { + com.klinker.android.logger.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); + } + } +} diff --git a/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsReceiver.java b/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsReceiver.java new file mode 100644 index 00000000..63a97de1 --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsReceiver.java @@ -0,0 +1,82 @@ +/* + * Copyright 2020 Aniket Kumar + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kde.kdeconnect.Plugins.SMSPlugin; + +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 com.klinker.android.send_message.Transaction; +import com.klinker.android.send_message.Utils; + +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); + } + } + } + } + } +} diff --git a/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsSentReceiver.java b/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsSentReceiver.java new file mode 100644 index 00000000..b942ca81 --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/SMSPlugin/SmsSentReceiver.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020 Aniket Kumar + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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; + +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) { + } +} diff --git a/src/org/kde/kdeconnect/UserInterface/DefaultSmsAppAlertDialogFragment.java b/src/org/kde/kdeconnect/UserInterface/DefaultSmsAppAlertDialogFragment.java new file mode 100644 index 00000000..77f445d4 --- /dev/null +++ b/src/org/kde/kdeconnect/UserInterface/DefaultSmsAppAlertDialogFragment.java @@ -0,0 +1,101 @@ +/* + * Copyright 2019 Erik Duisters + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kde.kdeconnect.UserInterface; + +import android.app.Activity; +import android.app.role.RoleManager; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.provider.Telephony; + +import androidx.annotation.Nullable; +import androidx.core.app.ActivityCompat; + +public class DefaultSmsAppAlertDialogFragment extends AlertDialogFragment { + private static final String KEY_PERMISSIONS = "Permissions"; + private static final String KEY_REQUEST_CODE = "RequestCode"; + + private String[] permissions; + private int requestCode; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle args = getArguments(); + + if (args == null) { + return; + } + + permissions = args.getStringArray(KEY_PERMISSIONS); + requestCode = args.getInt(KEY_REQUEST_CODE, 0); + + setCallback(new Callback() { + @Override + public void onPositiveButtonClicked() { + Activity host = requireActivity(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + RoleManager roleManager = host.getSystemService(RoleManager.class); + + if (roleManager.isRoleAvailable(RoleManager.ROLE_SMS)) { + if (!roleManager.isRoleHeld(RoleManager.ROLE_SMS)) { + Intent roleRequestIntent = roleManager.createRequestRoleIntent(RoleManager.ROLE_SMS); + host.startActivityForResult(roleRequestIntent, requestCode); + } + } + } else { + Intent intent = new Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT); + intent.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, getActivity().getPackageName()); + host.startActivityForResult(intent, requestCode); + } + + ActivityCompat.requestPermissions(requireActivity(), permissions, requestCode); + } + }); + } + + public static class Builder extends AlertDialogFragment.AbstractBuilder { + + @Override + public Builder getThis() { + return this; + } + + public Builder setPermissions(String[] permissions) { + args.putStringArray(KEY_PERMISSIONS, permissions); + + return getThis(); + } + + public Builder setRequestCode(int requestCode) { + args.putInt(KEY_REQUEST_CODE, requestCode); + + return getThis(); + } + + @Override + protected DefaultSmsAppAlertDialogFragment createFragment() { + return new DefaultSmsAppAlertDialogFragment(); + } + } +} diff --git a/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java b/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java index f4e34c59..b9cf9af3 100644 --- a/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java +++ b/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java @@ -40,14 +40,18 @@ import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; +import com.klinker.android.send_message.Utils; + import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; import org.kde.kdeconnect.Plugins.Plugin; +import org.kde.kdeconnect.Plugins.SMSPlugin.SMSPlugin; import org.kde.kdeconnect.UserInterface.List.PluginListHeaderItem; import org.kde.kdeconnect.UserInterface.List.FailedPluginListItem; import org.kde.kdeconnect.UserInterface.List.ListAdapter; import org.kde.kdeconnect.UserInterface.List.PluginItem; +import org.kde.kdeconnect.UserInterface.List.SetDefaultAppPluginListItem; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; @@ -337,6 +341,29 @@ 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 + // for now I'm not able to integrate it with other plugin list, but this needs to be reimplemented in a better way. + if (!Utils.isDefaultSmsApp(mActivity)) { + for (Plugin p : plugins) { + if (p.getPluginKey().equals("SMSPlugin")) { + 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); diff --git a/src/org/kde/kdeconnect/UserInterface/List/SetDefaultAppPluginListItem.java b/src/org/kde/kdeconnect/UserInterface/List/SetDefaultAppPluginListItem.java new file mode 100644 index 00000000..10ca8933 --- /dev/null +++ b/src/org/kde/kdeconnect/UserInterface/List/SetDefaultAppPluginListItem.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020 Aniket Kumar + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.kde.kdeconnect.UserInterface.List; + +import org.kde.kdeconnect.Plugins.Plugin; + +public class SetDefaultAppPluginListItem extends SmallEntryItem { + + public interface Action { + void action(Plugin plugin); + } + + public SetDefaultAppPluginListItem(Plugin plugin, String displayName, SetDefaultAppPluginListItem.Action action) { + super(displayName, (view) -> action.action(plugin)); + } +} \ No newline at end of file