File indexing completed on 2024-12-22 04:41:41

0001 /*
0002  * SPDX-FileCopyrightText: 2014 Albert Vaca Cintora <albertvaka@gmail.com>
0003  * SPDX-FileCopyrightText: 2021 Simon Redman <simon@ergotech.com>
0004  * SPDX-FileCopyrightText: 2020 Aniket Kumar <anikketkumar786@gmail.com>
0005  *
0006  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0007  */
0008 
0009 package org.kde.kdeconnect.Plugins.SMSPlugin;
0010 
0011 import static org.kde.kdeconnect.Plugins.TelephonyPlugin.TelephonyPlugin.PACKET_TYPE_TELEPHONY;
0012 
0013 import android.Manifest;
0014 import android.annotation.SuppressLint;
0015 import android.app.Activity;
0016 import android.content.BroadcastReceiver;
0017 import android.content.Context;
0018 import android.content.Intent;
0019 import android.content.IntentFilter;
0020 import android.content.SharedPreferences;
0021 import android.content.pm.PackageManager;
0022 import android.database.ContentObserver;
0023 import android.os.Build;
0024 import android.os.Bundle;
0025 import android.os.Handler;
0026 import android.os.Looper;
0027 import android.preference.PreferenceManager;
0028 import android.provider.Telephony;
0029 import android.telephony.PhoneNumberUtils;
0030 import android.telephony.SmsManager;
0031 import android.telephony.SmsMessage;
0032 
0033 import androidx.annotation.NonNull;
0034 import androidx.core.content.ContextCompat;
0035 
0036 import com.klinker.android.logger.Log;
0037 import com.klinker.android.send_message.Transaction;
0038 
0039 import org.json.JSONArray;
0040 import org.json.JSONException;
0041 import org.json.JSONObject;
0042 import org.kde.kdeconnect.Helpers.ContactsHelper;
0043 import org.kde.kdeconnect.Helpers.SMSHelper;
0044 import org.kde.kdeconnect.NetworkPacket;
0045 import org.kde.kdeconnect.Plugins.Plugin;
0046 import org.kde.kdeconnect.Plugins.PluginFactory;
0047 import org.kde.kdeconnect.Plugins.TelephonyPlugin.TelephonyPlugin;
0048 import org.kde.kdeconnect.UserInterface.PluginSettingsFragment;
0049 import org.kde.kdeconnect_tp.BuildConfig;
0050 import org.kde.kdeconnect_tp.R;
0051 
0052 import java.util.ArrayList;
0053 import java.util.Collections;
0054 import java.util.List;
0055 import java.util.Map;
0056 import java.util.concurrent.locks.Lock;
0057 import java.util.concurrent.locks.ReentrantLock;
0058 
0059 @PluginFactory.LoadablePlugin
0060 @SuppressLint("InlinedApi")
0061 public class SMSPlugin extends Plugin {
0062 
0063     /**
0064      * Packet used to indicate a batch of messages has been pushed from the remote device
0065      * <p>
0066      * The body should contain the key "messages" mapping to an array of messages
0067      * <p>
0068      * For example:
0069      * {
0070      *   "version": 2                     // This is the second version of this packet type and
0071      *                                    // version 1 packets (which did not carry this flag)
0072      *                                    // are incompatible with the new format
0073      *   "messages" : [
0074      *   { "event"     : 1,               // 32-bit field containing a bitwise-or of event flags
0075      *                                    // See constants declared in SMSHelper.Message for defined
0076      *                                    // values and explanations
0077      *     "body"      : "Hello",         // Text message body
0078      *     "addresses": <List<Address>>   // List of Address objects, one for each participant of the conversation
0079      *                                    // The user's Address is excluded so:
0080      *                                    // If this is a single-target messsage, there will only be one
0081      *                                    // Address (the other party)
0082      *                                    // If this is an incoming multi-target message, the first Address is the
0083      *                                    // sender and all other addresses are other parties to the conversation
0084      *                                    // If this is an outgoing multi-target message, the sender is implicit
0085      *                                    // (the user's phone number) and all Addresses are recipients
0086      *     "date"      : "1518846484880", // Timestamp of the message
0087      *     "type"      : "2",   // Compare with Android's
0088      *                          // Telephony.TextBasedSmsColumns.MESSAGE_TYPE_*
0089      *     "thread_id" : 132    // Thread to which the message belongs
0090      *     "read"      : true   // Boolean representing whether a message is read or unread
0091      *   },
0092      *   { ... },
0093      *   ...
0094      * ]
0095      *
0096      * The following optional fields of a message object may be defined
0097      * "sub_id": <int> // Android's subscriber ID, which is basically used to determine which SIM card the message
0098      *                 // belongs to. This is mostly useful when attempting to reply to an SMS with the correct
0099      *                 // SIM card using PACKET_TYPE_SMS_REQUEST.
0100      *                 // If this value is not defined or if it does not match a valid subscriber_id known by
0101      *                 // Android, we will use whatever subscriber ID Android gives us as the default
0102      *
0103      * "attachments": <List<Attachment>>    // List of Attachment objects, one for each attached file in the message.
0104      *
0105      * An Attachment object looks like:
0106      * {
0107      *     "part_id": <long>                // part_id of the attachment used to read the file from MMS database
0108      *     "mime_type": <String>            // contains the mime type of the file (eg: image/jpg, video/mp4 etc.)
0109      *     "encoded_thumbnail": <String>    // Optional base64-encoded thumbnail preview of the content for types which support it
0110      *     "unique_identifier": <String>    // Unique name of te file
0111      * }
0112      *
0113      * An Address object looks like:
0114      * {
0115      *     "address": <String> // Address (phone number, email address, etc.) of this object
0116      * }
0117      */
0118     private final static String PACKET_TYPE_SMS_MESSAGE = "kdeconnect.sms.messages";
0119     private final static int SMS_MESSAGE_PACKET_VERSION = 2; // We *send* packets of this version
0120 
0121     /**
0122      * Packet sent to request a message be sent
0123      *
0124      * The body should look like so:
0125      * {
0126      *   "version": 2,                     // The version of the packet being sent. Compare to SMS_REQUEST_PACKET_VERSION before attempting to handle.
0127      *   "sendSms": true,                  // (Depreciated, ignored) Old versions of the desktop app used to mix phone calls, SMS, etc. in the same packet type and used this field to differentiate.
0128      *   "phoneNumber": "542904563213",    // (Depreciated) Retained for backwards-compatibility. Old versions of the desktop app send a single phoneNumber. Use the Addresses field instead.
0129      *   "addresses": <List of Addresses>, // The one or many targets of this message
0130      *   "messageBody": "Hi mom!",         // Plain-text string to be sent as the body of the message (Optional if sending an attachment)
0131      *   "attachments": <List of Attached files>,
0132      *   "sub_id": 3859358340534           // Some magic number which tells Android which SIM card to use (Optional, if omitted, sends with the default SIM card)
0133      * }
0134      *
0135      * An AttachmentContainer object looks like:
0136      * {
0137      *   "fileName": <String>             // Name of the file
0138      *   "base64EncodedFile": <String>    // Base64 encoded file
0139      *   "mimeType": <String>             // File type (eg: image/jpg, video/mp4 etc.)
0140      * }
0141      */
0142     private final static String PACKET_TYPE_SMS_REQUEST = "kdeconnect.sms.request";
0143     private final static int SMS_REQUEST_PACKET_VERSION = 2; // We *handle* packets of this version or lower. Update this number only if future packets break backwards-compatibility.
0144 
0145     /**
0146      * Packet sent to request the most-recent message in each conversations on the device
0147      * <p>
0148      * The request packet shall contain no body
0149      */
0150     private final static String PACKET_TYPE_SMS_REQUEST_CONVERSATIONS = "kdeconnect.sms.request_conversations";
0151 
0152     /**
0153      * Packet sent to request all the messages in a particular conversation
0154      * <p>
0155      * The following fields are available:
0156      * "threadID": <long>            // (Required) ThreadID to request
0157      * "rangeStartTimestamp": <long> // (Optional) Millisecond epoch timestamp indicating the start of the range from which to return messages
0158      * "numberToRequest": <long>     // (Optional) Number of messages to return, starting from rangeStartTimestamp.
0159      *                               // May return fewer than expected if there are not enough or more than expected if many
0160      *                               // messages have the same timestamp.
0161      */
0162     private final static String PACKET_TYPE_SMS_REQUEST_CONVERSATION = "kdeconnect.sms.request_conversation";
0163 
0164     /**
0165      * Packet sent to request an attachment file in a particular message of a conversation
0166      * <p>
0167      * The body should look like so:
0168      * "part_id": <long>                // Part id of the attachment
0169      * "unique_identifier": <String>    // This unique_identifier should come from a previous message packet's attachment field
0170      */
0171     private final static String PACKET_TYPE_SMS_REQUEST_ATTACHMENT = "kdeconnect.sms.request_attachment";
0172 
0173     /**
0174      * Packet used to send original attachment file from mms database to desktop
0175      * <p>
0176      * The following fields are available:
0177      * "filename": <String>     // Name of the attachment file in the database
0178      * "payload":               // Actual attachment file to be transferred
0179      */
0180     private final static String PACKET_TYPE_SMS_ATTACHMENT_FILE = "kdeconnect.sms.attachment_file";
0181 
0182     private static final String KEY_PREF_BLOCKED_NUMBERS = "telephony_blocked_numbers";
0183 
0184     private final BroadcastReceiver receiver = new BroadcastReceiver() {
0185         @Override
0186         public void onReceive(Context context, Intent intent) {
0187 
0188             String action = intent.getAction();
0189 
0190             //Log.e("TelephonyPlugin","Telephony event: " + action);
0191 
0192             if (Telephony.Sms.Intents.SMS_RECEIVED_ACTION.equals(action)) {
0193 
0194                 final Bundle bundle = intent.getExtras();
0195                 if (bundle == null) return;
0196                 final Object[] pdus = (Object[]) bundle.get("pdus");
0197                 ArrayList<SmsMessage> messages = new ArrayList<>();
0198 
0199                 for (Object pdu : pdus) {
0200                     // I hope, but am not sure, that the pdus array is in the order that the parts
0201                     // of the SMS message should be
0202                     // If it is not, I believe the pdu contains the information necessary to put it
0203                     // in order, but in my testing the order seems to be correct, so I won't worry
0204                     // about it now.
0205                     messages.add(SmsMessage.createFromPdu((byte[]) pdu));
0206                 }
0207 
0208                 smsBroadcastReceivedDeprecated(messages);
0209             }
0210         }
0211     };
0212 
0213     /**
0214      * Keep track of the most-recently-seen message so that we can query for later ones as they arrive
0215      */
0216     private long mostRecentTimestamp = 0;
0217     // Since the mostRecentTimestamp is accessed both from the plugin's thread and the ContentObserver
0218     // thread, make sure that access is coherent
0219     private final Lock mostRecentTimestampLock = new ReentrantLock();
0220 
0221     private class MessageContentObserver extends ContentObserver {
0222 
0223         /**
0224          * Create a ContentObserver to watch the Messages database. onChange is called for
0225          * every subscribed change
0226          *
0227          * @param handler Handler object used to make the callback
0228          */
0229         MessageContentObserver(Handler handler) {
0230             super(handler);
0231         }
0232 
0233         /**
0234          * The onChange method is called whenever the subscribed-to database changes
0235          *
0236          * In this case, this onChange expects to be called whenever *anything* in the Messages
0237          * database changes and simply reports those updated messages to anyone who might be listening
0238          */
0239         @Override
0240         public void onChange(boolean selfChange) {
0241             sendLatestMessage();
0242         }
0243 
0244     }
0245 
0246     /**
0247      * This receiver will be invoked only when the app will be set as the default sms app
0248      * Whenever the app will be set as the default, the database update alert will be sent
0249      * using messageUpdateReceiver and not the contentObserver class
0250      */
0251     private final BroadcastReceiver messagesUpdateReceiver = new BroadcastReceiver() {
0252         @Override
0253         public void onReceive(Context context, Intent intent) {
0254 
0255             String action = intent.getAction();
0256 
0257             if (Transaction.REFRESH.equals(action)) {
0258                 sendLatestMessage();
0259             }
0260         }
0261     };
0262 
0263     /**
0264      * Helper method to read the latest message from the sms-mms database and sends it to the desktop
0265      */
0266     private void sendLatestMessage() {
0267         // Lock so no one uses the mostRecentTimestamp between the moment we read it and the
0268         // moment we update it. This is because reading the Messages DB can take long.
0269         mostRecentTimestampLock.lock();
0270 
0271         if (mostRecentTimestamp == 0) {
0272             // Since the timestamp has not been initialized, we know that nobody else
0273             // has requested a message. That being the case, there is most likely
0274             // nobody listening for message updates, so just drop them
0275             mostRecentTimestampLock.unlock();
0276             return;
0277         }
0278         List<SMSHelper.Message> messages = SMSHelper.getMessagesInRange(context, null, mostRecentTimestamp, null, false);
0279 
0280         long newMostRecentTimestamp = mostRecentTimestamp;
0281         for (SMSHelper.Message message : messages) {
0282             if (message == null || message.date >= newMostRecentTimestamp) {
0283                   newMostRecentTimestamp = message.date;
0284             }
0285         }
0286 
0287         // Update the most recent counter
0288         mostRecentTimestamp = newMostRecentTimestamp;
0289         mostRecentTimestampLock.unlock();
0290 
0291         // Send the alert about the update
0292         device.sendPacket(constructBulkMessagePacket(messages));
0293     }
0294 
0295     /**
0296      * Deliver an old-style SMS packet in response to a new message arriving
0297      *
0298      * For backwards-compatibility with long-lived distro packages, this method needs to exist in
0299      * order to support older desktop apps. However, note that it should no longer be used
0300      *
0301      * This comment is being written 30 August 2018. Distros will likely be running old versions for
0302      * many years to come...
0303      *
0304      * @param messages Ordered list of parts of the message body which should be combined into a single message
0305      */
0306     @Deprecated
0307     private void smsBroadcastReceivedDeprecated(ArrayList<SmsMessage> messages) {
0308 
0309         if (BuildConfig.DEBUG) {
0310             if (!(messages.size() > 0)) {
0311                 throw new AssertionError("This method requires at least one message");
0312             }
0313         }
0314 
0315         NetworkPacket np = new NetworkPacket(PACKET_TYPE_TELEPHONY);
0316 
0317         np.set("event", "sms");
0318 
0319         StringBuilder messageBody = new StringBuilder();
0320         for (int index = 0; index < messages.size(); index++) {
0321             messageBody.append(messages.get(index).getMessageBody());
0322         }
0323         np.set("messageBody", messageBody.toString());
0324 
0325         String phoneNumber = messages.get(0).getOriginatingAddress();
0326 
0327         if (isNumberBlocked(phoneNumber))
0328             return;
0329 
0330         int permissionCheck = ContextCompat.checkSelfPermission(context,
0331                 Manifest.permission.READ_CONTACTS);
0332 
0333         if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
0334             Map<String, String> contactInfo = ContactsHelper.phoneNumberLookup(context, phoneNumber);
0335 
0336             if (contactInfo.containsKey("name")) {
0337                 np.set("contactName", contactInfo.get("name"));
0338             }
0339 
0340             if (contactInfo.containsKey("photoID")) {
0341                 np.set("phoneThumbnail", ContactsHelper.photoId64Encoded(context, contactInfo.get("photoID")));
0342             }
0343         }
0344         if (phoneNumber != null) {
0345             np.set("phoneNumber", phoneNumber);
0346         }
0347 
0348 
0349         device.sendPacket(np);
0350     }
0351 
0352     @Override
0353     public int getPermissionExplanation() {
0354         return R.string.telepathy_permission_explanation;
0355     }
0356 
0357     @Override
0358     public boolean onCreate() {
0359         IntentFilter filter = new IntentFilter(Telephony.Sms.Intents.SMS_RECEIVED_ACTION);
0360         filter.setPriority(500);
0361         context.registerReceiver(receiver, filter);
0362 
0363         IntentFilter refreshFilter = new IntentFilter(Transaction.REFRESH);
0364         refreshFilter.setPriority(500);
0365         context.registerReceiver(messagesUpdateReceiver, refreshFilter);
0366 
0367         Looper helperLooper = SMSHelper.MessageLooper.getLooper();
0368         ContentObserver messageObserver = new MessageContentObserver(new Handler(helperLooper));
0369         SMSHelper.registerObserver(messageObserver, context);
0370 
0371         // To see debug messages for Klinker library, uncomment the below line
0372         //Log.setDebug(true);
0373 
0374         return true;
0375     }
0376 
0377     @Override
0378     public @NonNull String getDisplayName() {
0379         return context.getResources().getString(R.string.pref_plugin_telepathy);
0380     }
0381 
0382     @Override
0383     public @NonNull String getDescription() {
0384         return context.getResources().getString(R.string.pref_plugin_telepathy_desc);
0385     }
0386 
0387     @Override
0388     public boolean onPacketReceived(@NonNull NetworkPacket np) {
0389         long subID;
0390 
0391         switch (np.getType()) {
0392             case PACKET_TYPE_SMS_REQUEST_CONVERSATIONS:
0393                 return this.handleRequestAllConversations(np);
0394             case PACKET_TYPE_SMS_REQUEST_CONVERSATION:
0395                 return this.handleRequestSingleConversation(np);
0396             case PACKET_TYPE_SMS_REQUEST:
0397                 String textMessage = np.getString("messageBody");
0398                 subID = np.getLong("subID", -1);
0399 
0400                 List<SMSHelper.Address> addressList = SMSHelper.jsonArrayToAddressList(np.getJSONArray("addresses"));
0401                 if (addressList == null) {
0402                     // If the List of Address is null, then the SMS_REQUEST packet is
0403                     // most probably from the older version of the desktop app.
0404                     addressList = new ArrayList<>();
0405                     addressList.add(new SMSHelper.Address(np.getString("phoneNumber")));
0406                 }
0407                 List<SMSHelper.Attachment> attachedFiles = SMSHelper.jsonArrayToAttachmentsList(np.getJSONArray("attachments"));
0408 
0409                 SmsMmsUtils.sendMessage(context, textMessage, attachedFiles, addressList, (int) subID);
0410                 break;
0411 
0412             case TelephonyPlugin.PACKET_TYPE_TELEPHONY_REQUEST:
0413                 if (np.getBoolean("sendSms")) {
0414                     String phoneNo = np.getString("phoneNumber");
0415                     String sms = np.getString("messageBody");
0416                     subID = np.getLong("subID", -1);
0417 
0418                     try {
0419                         SmsManager smsManager = subID == -1? SmsManager.getDefault() :
0420                             SmsManager.getSmsManagerForSubscriptionId((int) subID);
0421                         ArrayList<String> parts = smsManager.divideMessage(sms);
0422 
0423                         // If this message turns out to fit in a single SMS, sendMultipartTextMessage
0424                         // properly handles that case
0425                         smsManager.sendMultipartTextMessage(phoneNo, null, parts, null, null);
0426 
0427                         //TODO: Notify other end
0428                     } catch (Exception e) {
0429                         //TODO: Notify other end
0430                         Log.e("SMSPlugin", "Exception", e);
0431                     }
0432                 }
0433                 break;
0434 
0435             case PACKET_TYPE_SMS_REQUEST_ATTACHMENT:
0436                 long partID = np.getLong("part_id");
0437                 String uniqueIdentifier = np.getString("unique_identifier");
0438 
0439                 NetworkPacket networkPacket = SmsMmsUtils.partIdToMessageAttachmentPacket(
0440                         context,
0441                         partID,
0442                         uniqueIdentifier,
0443                         PACKET_TYPE_SMS_ATTACHMENT_FILE
0444                 );
0445 
0446                 if (networkPacket != null) {
0447                     device.sendPacket(networkPacket);
0448                 }
0449                 break;
0450         }
0451 
0452         return true;
0453     }
0454 
0455     /**
0456      * Construct a proper packet of PACKET_TYPE_SMS_MESSAGE from the passed messages
0457      *
0458      * @param messages Messages to include in the packet
0459      * @return NetworkPacket of type PACKET_TYPE_SMS_MESSAGE
0460      */
0461     private static NetworkPacket constructBulkMessagePacket(Iterable<SMSHelper.Message> messages) {
0462         NetworkPacket reply = new NetworkPacket(PACKET_TYPE_SMS_MESSAGE);
0463 
0464         JSONArray body = new JSONArray();
0465 
0466         for (SMSHelper.Message message : messages) {
0467             try {
0468                 JSONObject json = message.toJSONObject();
0469 
0470                 body.put(json);
0471             } catch (JSONException e) {
0472                 Log.e("Conversations", "Error serializing message", e);
0473             }
0474         }
0475 
0476         reply.set("messages", body);
0477         reply.set("version", SMS_MESSAGE_PACKET_VERSION);
0478 
0479         return reply;
0480     }
0481 
0482     /**
0483      * Respond to a request for all conversations
0484      * <p>
0485      * Send one packet of type PACKET_TYPE_SMS_MESSAGE with the first message in all conversations
0486      */
0487     private boolean handleRequestAllConversations(NetworkPacket packet) {
0488         Iterable<SMSHelper.Message> conversations = SMSHelper.getConversations(this.context);
0489 
0490         // Prepare the mostRecentTimestamp counter based on these messages, since they are the most
0491         // recent in every conversation
0492         mostRecentTimestampLock.lock();
0493         for (SMSHelper.Message message : conversations) {
0494             if (message.date > mostRecentTimestamp) {
0495                 mostRecentTimestamp = message.date;
0496             }
0497             NetworkPacket partialReply = constructBulkMessagePacket(Collections.singleton(message));
0498             device.sendPacket(partialReply);
0499         }
0500         mostRecentTimestampLock.unlock();
0501 
0502         return true;
0503     }
0504 
0505     private boolean handleRequestSingleConversation(NetworkPacket packet) {
0506         SMSHelper.ThreadID threadID = new SMSHelper.ThreadID(packet.getLong("threadID"));
0507 
0508         long rangeStartTimestamp = packet.getLong("rangeStartTimestamp", -1);
0509         Long numberToGet = packet.getLong("numberToRequest", -1);
0510 
0511         if (numberToGet < 0) {
0512             numberToGet = null;
0513         }
0514 
0515         List<SMSHelper.Message> conversation;
0516         if (rangeStartTimestamp < 0) {
0517             conversation = SMSHelper.getMessagesInThread(this.context, threadID, numberToGet);
0518         } else {
0519             conversation = SMSHelper.getMessagesInRange(this.context, threadID, rangeStartTimestamp, numberToGet, true);
0520         }
0521 
0522         // Sometimes when desktop app is kept open while android app is restarted for any reason
0523         // mostRecentTimeStamp must be updated in that scenario too if a user request for a
0524         // single conversation and not the entire conversation list
0525         mostRecentTimestampLock.lock();
0526         for (SMSHelper.Message message : conversation) {
0527             if (message.date > mostRecentTimestamp) {
0528                 mostRecentTimestamp = message.date;
0529             }
0530         }
0531         mostRecentTimestampLock.unlock();
0532 
0533         NetworkPacket reply = constructBulkMessagePacket(conversation);
0534 
0535         device.sendPacket(reply);
0536 
0537         return true;
0538     }
0539 
0540     private boolean isNumberBlocked(String number) {
0541         SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
0542         String[] blockedNumbers = sharedPref.getString(KEY_PREF_BLOCKED_NUMBERS, "").split("\n");
0543 
0544         for (String s : blockedNumbers) {
0545             if (PhoneNumberUtils.compare(number, s))
0546                 return true;
0547         }
0548 
0549         return false;
0550     }
0551 
0552     @Override
0553     public boolean hasSettings() {
0554         return true;
0555     }
0556 
0557     @Override
0558     public PluginSettingsFragment getSettingsFragment(Activity activity) {
0559         return PluginSettingsFragment.newInstance(getPluginKey(), R.xml.smsplugin_preferences);
0560     }
0561 
0562     @Override
0563     public @NonNull String[] getSupportedPacketTypes() {
0564         return new String[]{
0565                 PACKET_TYPE_SMS_REQUEST,
0566                 TelephonyPlugin.PACKET_TYPE_TELEPHONY_REQUEST,
0567                 PACKET_TYPE_SMS_REQUEST_CONVERSATIONS,
0568                 PACKET_TYPE_SMS_REQUEST_CONVERSATION,
0569                 PACKET_TYPE_SMS_REQUEST_ATTACHMENT
0570         };
0571     }
0572 
0573     @Override
0574     public @NonNull String[] getOutgoingPacketTypes() {
0575         return new String[]{
0576                 PACKET_TYPE_SMS_MESSAGE,
0577                 PACKET_TYPE_SMS_ATTACHMENT_FILE
0578         };
0579     }
0580 
0581     @Override
0582     public @NonNull String[] getRequiredPermissions() {
0583         return new String[]{
0584                 Manifest.permission.SEND_SMS,
0585                 Manifest.permission.READ_SMS,
0586                 // READ_PHONE_STATE should be optional, since we can just query the user, but that
0587                 // requires a GUI implementation for querying the user!
0588                 Manifest.permission.READ_PHONE_STATE,
0589         };
0590     }
0591 
0592     /**
0593      * Permissions required for sending and receiving MMs messages
0594      */
0595     public static String[] getMmsPermissions() {
0596         return new String[]{
0597                 Manifest.permission.RECEIVE_SMS,
0598                 Manifest.permission.RECEIVE_MMS,
0599                 Manifest.permission.WRITE_EXTERNAL_STORAGE,
0600                 Manifest.permission.CHANGE_NETWORK_STATE,
0601                 Manifest.permission.WAKE_LOCK,
0602         };
0603     }
0604 
0605     /**
0606      * With versions older than KITKAT, lots of the content providers used in SMSHelper become
0607      * un-documented. Most manufacturers *did* do things the same way as was done in mainline
0608      * Android at that time, but some did not. If the manufacturer followed the default route,
0609      * everything will be fine. If not, the plugin will crash. But, since we have a global catch-all
0610      * in Device.onPacketReceived, it will not crash catastrophically.
0611      * The onCreated method of this SMSPlugin complains if a version older than KitKat is loaded,
0612      * but it still allowed in the optimistic hope that things will "just work"
0613      */
0614     @Override
0615     public int getMinSdk() {
0616         return Build.VERSION_CODES.FROYO;
0617     }
0618 }