2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-22 09:58:08 +00:00

Modifying SMSHelper.Message class and PACKET_TYPE_SMS_MESSAGE to contain list of attachment objects.

This commit is contained in:
Aniket Kumar 2020-08-12 17:08:02 +05:30
parent 31dd2cbd5f
commit f3d683e2c5
6 changed files with 300 additions and 79 deletions

View File

@ -26,6 +26,9 @@ import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.graphics.Bitmap;
import android.media.MediaMetadataRetriever;
import android.media.ThumbnailUtils;
import android.net.Uri;
import android.os.Build;
import android.os.Looper;
@ -46,6 +49,8 @@ import org.apache.commons.lang3.math.NumberUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.kde.kdeconnect.Plugins.SMSPlugin.MimeType;
import org.kde.kdeconnect.Plugins.SMSPlugin.SmsMmsUtils;
import java.io.IOException;
import java.io.InputStream;
@ -56,7 +61,6 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -71,6 +75,9 @@ import kotlin.text.Charsets;
@SuppressLint("InlinedApi")
public class SMSHelper {
private static final int THUMBNAIL_HEIGHT = 100;
private static final int THUMBNAIL_WIDTH = 100;
/**
* Get a URI for querying SMS messages
*/
@ -86,7 +93,7 @@ public class SMSHelper {
return Telephony.Mms.CONTENT_URI;
}
private static Uri getMMSPartUri() {
public static Uri getMMSPartUri() {
// Android says we should have Telephony.Mms.Part.CONTENT_URI. Alas, we do not.
return Uri.parse("content://mms/part/");
}
@ -606,7 +613,8 @@ public class SMSHelper {
threadID,
uID,
event,
subscriptionID
subscriptionID,
null
);
}
@ -628,6 +636,7 @@ public class SMSHelper {
@NonNull ThreadID threadID = new ThreadID(Long.parseLong(messageInfo.get(Message.THREAD_ID)));
long uID = Long.parseLong(messageInfo.get(Message.U_ID));
int subscriptionID = NumberUtils.toInt(messageInfo.get(Message.SUBSCRIPTION_ID));
List<Attachment> attachments = new ArrayList<>();
String[] columns = {
Telephony.Mms.Part._ID, // The content ID of this part
@ -660,7 +669,7 @@ public class SMSHelper {
long partID = cursor.getLong(partIDColumn);
String contentType = cursor.getString(contentTypeColumn);
String data = cursor.getString(dataColumn);
if ("text/plain".equals(contentType)) {
if (MimeType.isTypeText(contentType)) {
if (data != null) {
// data != null means the data is on disk. Go get it.
body = getMmsText(context, partID);
@ -668,10 +677,41 @@ public class SMSHelper {
body = cursor.getString(textColumn);
}
event = addEventFlag(event, Message.EVENT_TEXT_MESSAGE);
} //TODO: Parse more content types (photos and other attachments) here
} else if (MimeType.isTypeImage(contentType)) {
String mimeType = contentType;
String fileName = data.substring(data.lastIndexOf('/') + 1);
// Get the actual image from the mms database convert it into thumbnail and encode to Base64
Bitmap image = SmsMmsUtils.getMmsImage(context, partID);
Bitmap thumbnailImage = ThumbnailUtils.extractThumbnail(image, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
String encodedThumbnail = SmsMmsUtils.bitMapToBase64(thumbnailImage);
attachments.add(new Attachment(partID, mimeType, encodedThumbnail, fileName));
} else if (MimeType.isTypeVideo(contentType)) {
String mimeType = contentType;
String fileName = data.substring(data.lastIndexOf('/') + 1);
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(context, ContentUris.withAppendedId(getMMSPartUri(), partID));
Bitmap videoThumbnail = retriever.getFrameAtTime();
String encodedThumbnail = SmsMmsUtils.bitMapToBase64(
Bitmap.createScaledBitmap(videoThumbnail, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, true)
);
attachments.add(new Attachment(partID, mimeType, encodedThumbnail, fileName));
} else if (MimeType.isTypeAudio(contentType)) {
String mimeType = contentType;
String fileName = data.substring(data.lastIndexOf('/') + 1);
attachments.add(new Attachment(partID, mimeType, null, fileName));
} else {
Log.v("SMSHelper", "Unsupported attachment type: " + contentType);
}
} while (cursor.moveToNext());
}
} catch (Exception e) {
e.printStackTrace();
}
// Determine whether the message was in- our out- bound
@ -689,7 +729,26 @@ public class SMSHelper {
}
// Get address(es) of the message
List<Address> addresses = getMmsAddresses(context, Long.parseLong(mmsID), userPhoneNumbers);
Uri uri = ContentUris.appendId(getMMSUri().buildUpon(), uID).build();
Address from = SmsMmsUtils.getMmsFrom(context, uri);
List<Address> to = SmsMmsUtils.getMmsTo(context, uri);
List<Address> addresses = new ArrayList<>();
if (from != null) {
if (!userPhoneNumbers.contains(from.toString()) && !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")) {
addresses.add(address);
}
}
}
// It looks like addresses[0] is always the sender of the message and
// following addresses are recipient(s)
// This usually means the addresses list is at least 2 long, but there are cases (special
@ -713,78 +772,11 @@ public class SMSHelper {
threadID,
uID,
event,
subscriptionID
subscriptionID,
attachments
);
}
/**
* Get the address(es) of an MMS message
* Original implementation from https://stackoverflow.com/a/6446831/3723163
*
* The message at the first position of the list should be the sender of the message
*
* @param messageID ID of this message in the MMS database for looking up the remaining info
* @param userPhoneNumbers List of phone numbers which should be removed from the list of addresses
*/
private static @NonNull List<Address> getMmsAddresses(
@NonNull Context context,
@NonNull Long messageID,
@NonNull List<String> userPhoneNumbers
) {
Uri uri = ContentUris.appendId(getMMSUri().buildUpon(), messageID).appendPath("addr").build();
String[] columns = {
Telephony.Mms.Addr.MSG_ID, // ID of the message for which we are fetching addresses
Telephony.Mms.Addr.ADDRESS, // Address of this part
Telephony.Mms.Addr.CHARSET, // Charset of the returned address (where relevant) //TODO: Handle
};
String selection = Telephony.Mms.Addr.MSG_ID + " = ?";
String[] selectionArgs = {messageID.toString()};
// Keep an ordered set rather than a list because Android sometimes throws duplicates at us
Set<Address> addresses = new LinkedHashSet<>();
try (Cursor addrCursor = context.getContentResolver().query(
uri,
columns,
selection,
selectionArgs,
null
)) {
if (addrCursor != null && addrCursor.moveToFirst()) {
int addressIndex = addrCursor.getColumnIndex(Telephony.Mms.Addr.ADDRESS);
do {
String address = addrCursor.getString(addressIndex);
addresses.add(new Address(address));
} while (addrCursor.moveToNext());
}
}
// Prune the user's phone numbers from the list of addresses
List<Address> prunedAddresses = new ArrayList<>(addresses);
prunedAddresses.removeAll(userPhoneNumbers);
if (prunedAddresses.size() == 0) {
// If it turns out that we have pruned away everything, prune away nothing
// (The user is allowed to talk to themself)
// Remove duplicate entries, since the user knows if a conversation says "Me" on it,
// it is the conversation with themself. (We don't need to say "Me, Me")
// This leaves the multi-sim case alone, so the returned address list might say
// "Me1, Me2"
prunedAddresses = new ArrayList<>(addresses.size()); // The old one was empty too, but just to be clear...
for (Address address : addresses) {
if (!prunedAddresses.contains(address)) {
prunedAddresses.add(address);
}
}
}
return prunedAddresses;
}
/**
* Get a text part of an MMS message
* Original implementation from https://stackoverflow.com/a/6446831/3723163
@ -846,6 +838,46 @@ public class SMSHelper {
}
}
public static class Attachment {
final long partID;
final String mimeType;
final String base64EncodedFile;
final String uniqueIdentifier;
/**
* Attachment object field names
*/
public static final String PART_ID = "part_id";
public static final String MIME_TYPE = "mime_type";
public static final String ENCODED_THUMBNAIL = "encoded_thumbnail";
public static final String UNIQUE_IDENTIFIER = "unique_identifier";
public Attachment(long partID,
String mimeType,
@Nullable String base64EncodedFile,
String uniqueIdentifier
) {
this.partID = partID;
this.mimeType = mimeType;
this.base64EncodedFile = base64EncodedFile;
this.uniqueIdentifier = uniqueIdentifier;
}
public JSONObject toJson() throws JSONException {
JSONObject json = new JSONObject();
json.put(Attachment.PART_ID, this.partID);
json.put(Attachment.MIME_TYPE, this.mimeType);
if (this.base64EncodedFile != null) {
json.put(Attachment.ENCODED_THUMBNAIL, this.base64EncodedFile);
}
json.put(Attachment.UNIQUE_IDENTIFIER, this.uniqueIdentifier);
return json;
}
}
public static class Address {
final String address;
@ -905,7 +937,6 @@ public class SMSHelper {
JSONObject jsonObject = jsonArray.getJSONObject(i);
String address = jsonObject.getString("address");
addresses.add(new Address(address));
Log.e("address", address);
}
} catch (Exception e) {
e.printStackTrace();
@ -951,6 +982,7 @@ public class SMSHelper {
public final long uID;
public final int event;
public final int subscriptionID;
public final List<Attachment> attachments;
/**
* Named constants which are used to construct a Message
@ -965,6 +997,7 @@ public class SMSHelper {
static final String U_ID = Telephony.Sms._ID; // Something which uniquely identifies this message
static final String EVENT = "event";
static final String SUBSCRIPTION_ID = Telephony.Sms.SUBSCRIPTION_ID; // An ID which appears to identify a SIM card
static final String ATTACHMENTS = "attachments"; // List of files attached in an MMS
/**
* Event flags
@ -1016,7 +1049,8 @@ public class SMSHelper {
@NonNull ThreadID threadID,
long uID,
int event,
int subscriptionID
int subscriptionID,
@Nullable List<Attachment> attachments
) {
this.addresses = addresses;
this.body = body;
@ -1035,6 +1069,7 @@ public class SMSHelper {
this.uID = uID;
this.subscriptionID = subscriptionID;
this.event = event;
this.attachments = attachments;
}
public JSONObject toJSONObject() throws JSONException {
@ -1055,6 +1090,14 @@ public class SMSHelper {
json.put(Message.SUBSCRIPTION_ID, subscriptionID);
json.put(Message.EVENT, event);
if (this.attachments != null) {
JSONArray jsonAttachments = new JSONArray();
for (Attachment attachment : this.attachments) {
jsonAttachments.put(attachment.toJson());
}
json.put(Message.ATTACHMENTS, jsonAttachments);
}
return json;
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2020 Aniket Kumar <anikketkumar786@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.kde.kdeconnect.Plugins.SMSPlugin;
public final class MimeType {
public static final String TYPE_TEXT = "text/plain";
public static final String TYPE_IMAGE = "image";
public static final String TYPE_VIDEO = "video";
public static final String TYPE_AUDIO = "audio";
public static boolean isTypeText(String mimeType) { return mimeType.startsWith(TYPE_TEXT); }
public static boolean isTypeImage(String mimeType) {
return mimeType.startsWith(TYPE_IMAGE);
}
public static boolean isTypeVideo(String mimeType) { return mimeType.startsWith(TYPE_VIDEO); }
public static boolean isTypeAudio(String mimeType) { return mimeType.startsWith(TYPE_AUDIO); }
public static String postfixOf(String mimeType) { return mimeType.substring(mimeType.lastIndexOf('/')+1); }
}

View File

@ -113,6 +113,16 @@ public class SMSPlugin extends Plugin {
* // If this value is not defined or if it does not match a valid subscriber_id known by
* // Android, we will use whatever subscriber ID Android gives us as the default
*
* "attachments": <List<Attachment>> // List of Attachment objects, one for each attached file in the message.
*
* An Attachment object looks like:
* {
* "part_id": <long> // part_id of the attachment used to read the file from MMS database
* "mime_type": <int> // contains the mime type of the file (image, video, audio, etc.)
* "encoded_thumbnail": <String> // Optional base64-encoded thumbnail preview of the content for types which support it
* "unique_identifier": <String> // Unique name of te file
* }
*
* An Address object looks like:
* {
* "address": <String> // Address (phone number, email address, etc.) of this object

View File

@ -26,6 +26,10 @@ import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager;
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.RetrieveConf;
import com.klinker.android.send_message.Message;
import com.klinker.android.send_message.MmsSentReceiver;
import com.klinker.android.send_message.Settings;
@ -34,8 +38,11 @@ import com.klinker.android.send_message.Utils;
import android.content.ContentUris;
import android.content.ContentValues;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.provider.Telephony;
import android.net.Uri;
import android.util.Base64;
import android.util.Log;
import androidx.annotation.RequiresApi;
@ -45,6 +52,9 @@ import org.kde.kdeconnect.Helpers.SMSHelper;
import org.kde.kdeconnect.Helpers.TelephonyHelper;
import org.kde.kdeconnect_tp.R;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@ -129,6 +139,123 @@ public class SmsMmsUtils {
}
}
/**
* Returns the Address of the sender of the MMS message.
* @param uri content://mms/msgId/addr
* @param context context in which the method is called.
* @return sender's Address
*/
public static SMSHelper.Address getMmsFrom(Context context, Uri uri) {
MultimediaMessagePdu msg;
try {
msg = (MultimediaMessagePdu) PduPersister.getPduPersister(context).load(uri);
} catch (Exception e) {
return null;
}
EncodedStringValue encodedStringValue = msg.getFrom();
SMSHelper.Address from = new SMSHelper.Address(encodedStringValue.getString());
return from;
}
/**
* returns a List of Addresses of all the recipients of a MMS message.
* @param uri content://mms/part_id
* @param context Context in which the method is called.
* @return List of Addresses of all recipients of an MMS message
*/
public static List<SMSHelper.Address> getMmsTo(Context context, Uri uri) {
MultimediaMessagePdu msg;
try {
msg = (MultimediaMessagePdu) PduPersister.getPduPersister(context).load(uri);
} catch (Exception e) {
e.printStackTrace();
return null;
}
StringBuilder toBuilder = new StringBuilder();
EncodedStringValue to[] = msg.getTo();
if (to != null) {
toBuilder.append(EncodedStringValue.concat(to));
}
if (msg instanceof RetrieveConf) {
EncodedStringValue cc[] = ((RetrieveConf) msg).getCc();
if (cc != null && cc.length == 0) {
toBuilder.append(";");
toBuilder.append(EncodedStringValue.concat(cc));
}
}
String built = toBuilder.toString().replace(";", ", ");
if (built.startsWith(", ")) {
built = built.substring(2);
}
return stripDuplicatePhoneNumbers(built);
}
/**
* Removes duplicate addresses from the string and returns List of Addresses
*/
public static List<SMSHelper.Address> stripDuplicatePhoneNumbers(String phoneNumbers) {
if (phoneNumbers == null) {
return null;
}
String numbers[] = phoneNumbers.split(", ");
List<SMSHelper.Address> uniqueNumbers = new ArrayList<>();
for (String number : numbers) {
if (!uniqueNumbers.contains(number.trim())) {
uniqueNumbers.add(new SMSHelper.Address(number.trim()));
}
}
return uniqueNumbers;
}
/**
* Converts a given bitmap to an encoded Base64 string for sending to desktop
* @param bitmap bitmap to be encoded into string*
* @return Returns the Base64 encoded string
*/
public static String bitMapToBase64(Bitmap bitmap) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
// The below line is not really compressing to PNG so much as encoding as PNG, since PNG is lossless
boolean isCompressed = bitmap.compress(Bitmap.CompressFormat.PNG,100, byteArrayOutputStream);
if (isCompressed) {
byte[] b = byteArrayOutputStream.toByteArray();
String encodedString = Base64.encodeToString(b, Base64.DEFAULT);
return encodedString;
}
return null;
}
/**
* Reads the image files attached with an MMS from MMS database
* @param context Context in which the method is called
* @param id part ID of the image file attached with an MMS message
* @return Returns the image as a bitmap
*/
public static Bitmap getMmsImage(Context context, long id) {
Uri partURI = ContentUris.withAppendedId(SMSHelper.getMMSPartUri(), id);
Bitmap bitmap = null;
try (InputStream inputStream = context.getContentResolver().openInputStream(partURI)) {
bitmap = BitmapFactory.decodeStream(inputStream);
} catch (IOException e) {
Log.e("SmsMmsUtils", "Exception", e);
}
return bitmap;
}
/**
* Marks a conversation as read in the database.
*

View File

@ -31,7 +31,6 @@ import android.telephony.SmsMessage;
import android.provider.Telephony.Sms;
import android.net.Uri;
import android.content.ContentValues;
import android.util.Log;
import androidx.core.app.NotificationCompat;
import androidx.core.app.Person;