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 }