Skip to content

Commit 6911322

Browse files
giovannivelludoCogno-Marcomatteocarnelos
committed
Enable sending of SMSMessages longer than 160 characters, with sent and delivery confirmation (#20)
* added files to .gitignore * removed modules.xml, as it was added to .gitignore * added to .gitignore all patterns present in default .gitignore generated by Android Studio * checkSetup() is not performed for messages sent without listeners * messages too long to fit into a single SMS are split into multiple messages and sent (still have to implement passing of PendingIntents) * implemented sent confirmation for multipart text message, code needs to be polished and optimized * removed duplicate `for` cycle * inglobated setState() method into setReceived() * removed useless check for null listener, as it cannot be null * fixed specification, added TODO * removed unused import * changed HIDDEN_CHARACTER from a Unicode character to a GSM character; removed useless intentAction local variable; fixed import in a test. * added a TODO; now when SMSSentBroadcastReceiver calls a listener because message sending failed, it passes to the listener the SentState with most occurrences * improved comments and formatting * removed duplicate TODO * fixed comments and TODOs * added method reconstructMessage() * fixed TODOs and authors * removed usage of WeakReference<> * improved performance of method onReceive(), now sentState given to the listener is not the one with most occurrences, but the first one to contain an error. * added a timestamp to Message to distinguish messages with equal Peer and content but sent (or received) in different moments, also added methods equals() and hashCode() to allow comparison of messages; bumped minimum SDK version to 19 (Android 4.4 KitKat, released in 2013) to allow usage of Objects.hash(). * modified SMSReceivedBroadcastReceiver to handle multi-part text messages; SMSPeer accepts null or empty Strings in its constructors, as messages can be received by unknown numbers. * added author * added check for GSM characters, for now if the message contains other characters it won't be considered valid * removed timestamp, as it's not actually useful * removed sentState field from SMSPart, as it's now useless * use binary search on ordered array messageParts instead of linear search * fixed TODOs; formatting * modified SMSDeliveredBroadcastReceiver to handle multi-part text messages; SMSHandler now handles all messages as multi-part text messages (composed by only 1 part if they fit a single SMS); removed handling of single SMS messages from SMSSentBroadcastReceiver * fixed documentation * removed useless multiplication in SMSPeer.hashCode() * added TODOs * added zeroes padding to messageCounter when converted to a String, to make binary searches on intent actions' names work correctly; converted messageCounter to a long, to avoid overflows when intents are being created too quickly. * fixed GSM characters regex * split GSM_CHARACTERS_REGEX into two regex, to avoid doing identical checks multiple times when looking for characters from the GSM extension set; checkMessageText() correctly parses messages to find if they fit MAX_MSG_TEXT_LEN * moved SMSPeer.telephoneNumber validity check to when messages are sent, because we might receive SMS messages from peers who don't have a valid phone number (such as carriers) * fixed a comparison in SMSManager; added TODOs to remind me of classes to test; started writing instrumented test for SMS reception. * first successful attempt at receiving a dummy SMS message, however this method relies on SMSManager.getInstance().sendMessage() working correctly. * marked tests in SMSPeerTest which need to be moved to SMSManager tests; used Mockito to verify if SMSReceivedBroadcastReceiver receives messages, but it doesn't work at the moment. * removed an hardwiring * Implemented Google Library Still everything to test * adapted test to new SMSManager public interface * Rewritten tests, fixed one class, added emulator exception * Codacy improvements * Applied suggested changes: + Added author tag (and standardized specs) * Modified parseMessage() structure * Extracted exception messages into static final fields * Extracted RegEx string into static final field * Used String.format() - Removed fail() assertions * updated Mockito; added a TODO; fixed a test. * renamed test, to be the same as the class it's testing * updated TODOs * skeleton of class SMSManagerInstrumentedTest; formatting * formatting; tried to mock SmsManager in SMSManagerInstrumentedTest, but I can't because final classes cannot be mocked. * deleted all instrumented tests because I won't be able to get them to work before the review * removal of code used for instrumented tests * removal of code used for instrumented tests * imported powermock, removed methods used only in tests * used PowerMock in test to mock static method SmsManager.getDefault() * added TODO; added author; formatting; moved tests on Peer's number validity to SMSManagerTest, because the check happens when the SMSMessage is sent. * added tests for messages containing Unicode characters and characters from the GSM extension table. * Update README.md * Changes: + Added exception types - Removed throws clause on unchecked exceptions * Enhanced tests * Updated gradle * added tests for messages containing Unicode characters and characters from the GSM extension table. * added test for addPadding() * improved performance of checkMessageTest() * improved code after automated inspection * formatting; added author. * fixed imports * improved performance of checkMessageText by avoiding multiple calls of String.matches() when the message contains Unicode characters. * fixed documentation of SMSPeer * improved performance of checkMessageText() by removing a useless check * removed a useless variable, improved documentation * fixes requested by Mattia Fanan in his code review: #20 * removed unused import * removed `equals()` and `hashCode()` from interface `Message` * Test Fixes: Major fixes: * Overridden prefix auto-detection Small fixes: * Syntax and typo fixes * changes requested by Marco Mariotto in his code review: #20 * fixed errors resulting from merge * removed useless imports * added missing specifications. * added message to exception thrown in SMSManager * removed SMSPart and SMSPartTest, since BroadcastReceivers now use a counter; removed method addPadding() in SMSManager since its only use was for binary searches on SMSPart arrays * combined nested if statements * renamed all occurrences of UTF-16 to UCS-2, since that's the name of the encoding used in SMS messages * removed Map used to keep track of different messages coming from different addresses, since that can't happen; reorganized methods and added try-catch statements for possible exceptions, as in AOSP's Messaging app. * formatting * removed duplicate import * fixed specifications Co-authored-by: Cogno-Marco <marco.cognolato.98@gmail.com> Co-authored-by: Matteo Carnelos <matteo.carnelos@studenti.unipd.it>
1 parent 5ec03a4 commit 6911322

15 files changed

+536
-209
lines changed

app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ android {
55
buildToolsVersion "29.0.2"
66
defaultConfig {
77
applicationId 'com.eis.smslibproject'
8-
minSdkVersion 15
8+
minSdkVersion 19
99
targetSdkVersion 29
1010
versionCode 1
1111
versionName "1.0"

smslibrary/build.gradle

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ android {
66

77

88
defaultConfig {
9-
minSdkVersion 15
9+
minSdkVersion 19
1010
targetSdkVersion 29
1111
versionCode 1
1212
versionName "1.0"
@@ -37,5 +37,7 @@ dependencies {
3737
testImplementation 'org.mockito:mockito-core:1.10.19'
3838
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
3939
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
40-
implementation 'com.github.CremaLuca:android-preferences:2.3'
40+
testImplementation 'org.powermock:powermock-core:1.6.6'
41+
testImplementation 'org.powermock:powermock-module-junit4:1.6.6'
42+
testImplementation 'org.powermock:powermock-api-mockito:1.6.6'
4143
}

smslibrary/src/main/java/com/eis/communication/Message.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,23 @@
33
import java.io.Serializable;
44

55
/**
6-
* Encapsulation of a message in the network
6+
* Encapsulates data to be sent (or received) to (from) a certain peer.
7+
*
78
* @param <D> Data to be transmitted
89
* @param <P> Peer type of users of the network
910
*/
10-
public interface Message<D, P extends Peer> extends Serializable {
11+
public interface Message<D, P extends Peer> extends Serializable {
1112

1213
/**
13-
* Retrieves the data sent or to be sent in the network
14+
* Retrieves the data received or to be sent in the network
15+
*
1416
* @return data contained in this message
1517
*/
1618
D getData();
1719

1820
/**
19-
* Retrieves the sender or the destination
21+
* Retrieves the sender or the destination of this message
22+
*
2023
* @return Peer associated with this message
2124
*/
2225
P getPeer();

smslibrary/src/main/java/com/eis/smslibrary/SMSCore.java

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,10 @@
1212
* Wrapper for the {@link android.telephony} library.
1313
* This class is only used to interface with the core sms library of Android.
1414
*
15-
* @author Luca Crema, Marco Cognolato
15+
* @author Luca Crema, Marco Cognolato, Giovanni Velludo
1616
*/
1717
final class SMSCore {
1818

19-
static SmsManager manager;
20-
21-
/**
22-
* Sets up a given valid custom manager
23-
* @param manager The custom manager to set up to send messages
24-
*
25-
* @author Marco Cognolato
26-
*/
27-
static void setManager(SmsManager manager){
28-
SMSCore.manager = manager;
29-
}
30-
31-
/**
32-
* @return Returns the pre-set up manager if not null, else returns the default manager
33-
*
34-
* @author Marco Cognolato
35-
*/
36-
private static SmsManager getManager(){
37-
return manager == null ? SmsManager.getDefault() : manager;
38-
}
39-
4019
/**
4120
* Calls the library method to send a single message
4221
*
@@ -46,7 +25,7 @@ private static SmsManager getManager(){
4625
* @param deliveredPI {@link PendingIntent} for a broadcast on message received, can be null
4726
*/
4827
static void sendMessage(@NonNull final String message, @NonNull final String phoneNumber, @Nullable final PendingIntent sentPI, @Nullable final PendingIntent deliveredPI) {
49-
getManager().sendTextMessage(phoneNumber, null, message, sentPI, deliveredPI);
28+
SmsManager.getDefault().sendTextMessage(phoneNumber, null, message, sentPI, deliveredPI);
5029
}
5130

5231
/**
@@ -58,7 +37,7 @@ static void sendMessage(@NonNull final String message, @NonNull final String pho
5837
* @param deliveredPIs {@link ArrayList} of pending intents for a broadcast on message delivered, can be null
5938
*/
6039
static void sendMessages(@NonNull final ArrayList<String> messages, @NonNull final String phoneNumber, @Nullable final ArrayList<PendingIntent> sentPIs, @Nullable final ArrayList<PendingIntent> deliveredPIs) {
61-
getManager().sendMultipartTextMessage(phoneNumber, null, messages, sentPIs, deliveredPIs);
40+
SmsManager.getDefault().sendMultipartTextMessage(phoneNumber, null, messages, sentPIs, deliveredPIs);
6241
}
6342

6443
}

smslibrary/src/main/java/com/eis/smslibrary/SMSDeliveredBroadcastReceiver.java

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,44 @@
99

1010
import com.eis.smslibrary.listeners.SMSDeliveredListener;
1111

12+
import java.util.ArrayList;
13+
1214
/**
1315
* Broadcast receiver for delivered messages, called by Android Library.
1416
* Must be instantiated and set as receiver with context.registerReceiver(...).
1517
* There has to be one different DeliveredBroadcastReceiver per message sent,
1618
* so every IntentFilter name has to be different
1719
*
18-
* @author Marco Cognolato
20+
* @author Marco Cognolato, Giovanni Velludo
1921
*/
2022
public class SMSDeliveredBroadcastReceiver extends BroadcastReceiver {
21-
private SMSDeliveredListener listener;
22-
private SMSMessage message;
23+
24+
private final SMSDeliveredListener listener;
25+
private final SMSMessage message;
26+
private short partsToDeliverCounter;
2327

2428
/**
2529
* Constructor for the custom {@link BroadcastReceiver}.
2630
*
27-
* @param message that will be sent.
28-
* @param listener to be called when the operation is completed.
31+
* @param parts the parts of the message to be delivered.
32+
* @param listener the listener to be called when the operation is completed
33+
* @param peer the peer to whom the message will be delivered.
2934
*/
30-
SMSDeliveredBroadcastReceiver(@NonNull final SMSMessage message, @NonNull final SMSDeliveredListener listener) {
35+
SMSDeliveredBroadcastReceiver(@NonNull final ArrayList<String> parts,
36+
@NonNull final SMSDeliveredListener listener,
37+
@NonNull final SMSPeer peer) {
38+
StringBuilder fullMessageText = new StringBuilder();
39+
for (String part : parts) {
40+
fullMessageText.append(part);
41+
}
3142
this.listener = listener;
32-
this.message = message;
43+
this.message = new SMSMessage(peer, fullMessageText.toString());
44+
this.partsToDeliverCounter = (short) parts.size(); // they can't be more than 255
3345
}
3446

3547
/**
3648
* This method is subscribed to the intent of a message delivered, and will be called whenever a message is delivered using this library.
37-
* It interprets the state of the message delivering: {@link SMSMessage.DeliveredState#MESSAGE_DELIVERED} if it has been correctly sent,
49+
* It interprets the state of the message delivering: {@link SMSMessage.DeliveredState#MESSAGE_DELIVERED} if it has been correctly delivered,
3850
* some other state otherwise; then calls the listener and unregisters itself.
3951
*/
4052
@Override
@@ -53,9 +65,9 @@ public void onReceive(Context context, Intent intent) {
5365
break;
5466
}
5567

56-
if (listener != null) //extra check, even though listener should never be null
57-
listener.onSMSDelivered(message, deliveredState);
58-
68+
if (deliveredState == SMSMessage.DeliveredState.MESSAGE_DELIVERED &&
69+
--partsToDeliverCounter > 0) return;
70+
listener.onSMSDelivered(message, deliveredState);
5971
context.unregisterReceiver(this);
6072
}
6173
}

smslibrary/src/main/java/com/eis/smslibrary/SMSManager.java

Lines changed: 72 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,27 @@
44
import android.content.Context;
55
import android.content.Intent;
66
import android.content.IntentFilter;
7+
import android.telephony.SmsManager;
8+
9+
import androidx.annotation.NonNull;
10+
import androidx.annotation.Nullable;
711

812
import com.eis.communication.CommunicationManager;
13+
import com.eis.smslibrary.exceptions.InvalidTelephoneNumberException;
914
import com.eis.smslibrary.listeners.SMSDeliveredListener;
1015
import com.eis.smslibrary.listeners.SMSReceivedServiceListener;
1116
import com.eis.smslibrary.listeners.SMSSentListener;
1217

13-
import androidx.annotation.NonNull;
14-
import androidx.annotation.Nullable;
18+
import java.util.ArrayList;
19+
1520
import it.lucacrema.preferences.PreferencesManager;
1621

1722
/**
1823
* Communication handler for SMSs. It's a Singleton, you should
1924
* access it with {@link #getInstance}
2025
*
21-
* @author Luca Crema, Marco Mariotto, Alberto Ursino, Marco Tommasini, Marco Cognolato
26+
* @author Luca Crema, Marco Mariotto, Alberto Ursino, Marco Tommasini, Marco Cognolato, Giovanni Velludo
27+
* @since 29/11/2019
2228
*/
2329
@SuppressWarnings({"WeakerAccess", "unused"})
2430
public class SMSManager implements CommunicationManager<SMSMessage> {
@@ -43,7 +49,7 @@ public class SMSManager implements CommunicationManager<SMSMessage> {
4349
* same action name for every message we would have a conflict and we wouldn't
4450
* know what message has been sent
4551
*/
46-
private int messageCounter;
52+
private long messageCounter;
4753

4854
/**
4955
* Private constructor for Singleton
@@ -67,37 +73,41 @@ public static SMSManager getInstance() {
6773
* Requires {@link android.Manifest.permission#SEND_SMS}
6874
*
6975
* @param message to be sent in the channel to a peer
76+
* @throws InvalidTelephoneNumberException If message.peer.invalidityReason is not null.
7077
*/
7178
@Override
72-
public void sendMessage(final @NonNull SMSMessage message) {
79+
public void sendMessage(final @NonNull SMSMessage message)
80+
throws InvalidTelephoneNumberException {
7381
sendMessage(message, null, null, null);
7482
}
7583

7684
/**
77-
* Sends a message to a destination peer via SMS then
78-
* calls the listener.
85+
* Sends a message to a destination peer via SMS then calls the listener.
86+
* Requires {@link android.Manifest.permission#SEND_SMS}
7987
*
8088
* @param message to be sent in the channel to a peer
8189
* @param sentListener called on message sent or on error, can be null
8290
* @param context The context of the application used to setup the listener
91+
* @throws InvalidTelephoneNumberException If message.peer.invalidityReason is not null.
8392
*/
8493
public void sendMessage(final @NonNull SMSMessage message,
8594
final @Nullable SMSSentListener sentListener,
86-
Context context) {
95+
Context context) throws InvalidTelephoneNumberException {
8796
sendMessage(message, sentListener, null, context);
8897
}
8998

9099
/**
91-
* Sends a message to a destination peer via SMS then
92-
* calls the listener.
100+
* Sends a message to a destination peer via SMS then calls the listener.
101+
* Requires {@link android.Manifest.permission#SEND_SMS}
93102
*
94103
* @param message to be sent in the channel to a peer
95104
* @param deliveredListener called on message delivered or on error, can be null
96105
* @param context The context of the application used to setup the listener
106+
* @throws InvalidTelephoneNumberException If message.peer.invalidityReason is not null.
97107
*/
98108
public void sendMessage(final @NonNull SMSMessage message,
99109
final @Nullable SMSDeliveredListener deliveredListener,
100-
Context context) {
110+
Context context) throws InvalidTelephoneNumberException {
101111
sendMessage(message, null, deliveredListener, context);
102112
}
103113

@@ -109,56 +119,78 @@ public void sendMessage(final @NonNull SMSMessage message,
109119
* @param sentListener called on message sent or on error, can be null
110120
* @param deliveredListener called on message delivered or on error, can be null
111121
* @param context The context of the application used to setup the listener
122+
* @throws InvalidTelephoneNumberException If message.peer.invalidityReason is not null.
112123
*/
113124
public void sendMessage(final @NonNull SMSMessage message,
114125
final @Nullable SMSSentListener sentListener,
115126
final @Nullable SMSDeliveredListener deliveredListener,
116-
Context context) {
117-
PendingIntent sentPI = setupNewSentReceiver(message, sentListener, context);
118-
PendingIntent deliveredPI = setupNewDeliverReceiver(message, deliveredListener, context);
119-
SMSCore.sendMessage(getSMSContent(message), message.getPeer().getAddress(), sentPI, deliveredPI);
127+
Context context) throws InvalidTelephoneNumberException {
128+
if (message.getPeer().getInvalidityReason() != null) {
129+
InvalidTelephoneNumberException.Type invReason =
130+
message.getPeer().getInvalidityReason();
131+
String invMessage = message.getPeer().getInvalidityMessage();
132+
throw new InvalidTelephoneNumberException(invReason, invMessage);
133+
}
134+
ArrayList<String> texts = SmsManager.getDefault().divideMessage(getSMSContent(message));
135+
ArrayList<PendingIntent> sentPIs =
136+
setupNewSentReceiver(texts, sentListener, message.getPeer(), context);
137+
ArrayList<PendingIntent> deliveredPIs =
138+
setupNewDeliverReceiver(texts, deliveredListener, message.getPeer(), context);
139+
SMSCore.sendMessages(texts, message.getPeer().getAddress(), sentPIs, deliveredPIs);
120140
}
121141

122142
/**
123143
* Creates a new {@link SMSSentBroadcastReceiver} and registers it to receive broadcasts
124-
* with action {@value SENT_MESSAGE_INTENT_ACTION}
144+
* with actions {@value SENT_MESSAGE_INTENT_ACTION}
125145
*
126-
* @param message that will be sent
127-
* @param listener to call on broadcast received
128-
* @param context The context of the application used to setup the listener
129-
* @return a {@link PendingIntent} to be passed to SMSCore
146+
* @param texts the parts of the message to be sent.
147+
* @param listener the listener to call on broadcast received.
148+
* @param context the context of the application used to setup the listener
149+
* @return an {@link ArrayList} of {@link PendingIntent} to be passed to SMSCore.
130150
*/
131-
private PendingIntent setupNewSentReceiver(final @NonNull SMSMessage message,
132-
final @Nullable SMSSentListener listener,
133-
Context context) {
151+
private ArrayList<PendingIntent> setupNewSentReceiver(
152+
final @NonNull ArrayList<String> texts, final @Nullable SMSSentListener listener,
153+
final @NonNull SMSPeer peer, Context context) {
134154
if (listener == null || context == null)
135-
return null; //Doesn't make any sense to have a BroadcastReceiver if there is no listener or context
155+
return null; //Doesn't make any sense to have a BroadcastReceiver if there is no listener
136156

137-
SMSSentBroadcastReceiver onSentReceiver = new SMSSentBroadcastReceiver(message, listener);
138-
String actionName = SENT_MESSAGE_INTENT_ACTION + (messageCounter++);
139-
context.registerReceiver(onSentReceiver, new IntentFilter(actionName));
140-
return PendingIntent.getBroadcast(context, 0, new Intent(actionName), 0);
157+
ArrayList<PendingIntent> intents = new ArrayList<>();
158+
IntentFilter intentFilter = new IntentFilter();
159+
for (String text : texts) {
160+
String actionName = SENT_MESSAGE_INTENT_ACTION + messageCounter++;
161+
intents.add(PendingIntent.getBroadcast(context, 0, new Intent(actionName), 0));
162+
intentFilter.addAction(actionName);
163+
}
164+
SMSSentBroadcastReceiver onSentReceiver = new SMSSentBroadcastReceiver(texts, listener, peer);
165+
context.registerReceiver(onSentReceiver, intentFilter);
166+
return intents;
141167
}
142168

143169
/**
144170
* Creates a new {@link SMSDeliveredBroadcastReceiver} and registers it to receive broadcasts
145-
* with action {@value DELIVERED_MESSAGE_INTENT_ACTION}
171+
* with actions {@value DELIVERED_MESSAGE_INTENT_ACTION}
146172
*
147-
* @param message that will be sent
148-
* @param listener to call on broadcast received
149-
* @param context The context of the application used to setup the listener
150-
* @return a {@link PendingIntent} to be passed to SMSCore
173+
* @param texts the parts of the message to be delivered.
174+
* @param listener the listener to call on broadcast received.
175+
* @param context the context of the application used to setup the listener
176+
* @return an {@link ArrayList} of {@link PendingIntent} to be passed to SMSCore.
151177
*/
152-
private PendingIntent setupNewDeliverReceiver(final @NonNull SMSMessage message,
153-
final @Nullable SMSDeliveredListener listener,
154-
Context context) {
178+
private ArrayList<PendingIntent> setupNewDeliverReceiver(
179+
final @NonNull ArrayList<String> texts, final @Nullable SMSDeliveredListener listener,
180+
final @NonNull SMSPeer peer, Context context) {
155181
if (listener == null || context == null)
156182
return null; //Doesn't make any sense to have a BroadcastReceiver if there is no listener
157183

158-
SMSDeliveredBroadcastReceiver onDeliveredReceiver = new SMSDeliveredBroadcastReceiver(message, listener);
159-
String actionName = DELIVERED_MESSAGE_INTENT_ACTION + (messageCounter++);
160-
context.registerReceiver(onDeliveredReceiver, new IntentFilter(actionName));
161-
return PendingIntent.getBroadcast(context, 0, new Intent(actionName), 0);
184+
ArrayList<PendingIntent> intents = new ArrayList<>();
185+
IntentFilter intentFilter = new IntentFilter();
186+
for (String text : texts) {
187+
String actionName = DELIVERED_MESSAGE_INTENT_ACTION + messageCounter++;
188+
intents.add(PendingIntent.getBroadcast(context, 0, new Intent(actionName), 0));
189+
intentFilter.addAction(actionName);
190+
}
191+
SMSDeliveredBroadcastReceiver onDeliverReceiver = new SMSDeliveredBroadcastReceiver(texts, listener, peer);
192+
context.registerReceiver(onDeliverReceiver, intentFilter);
193+
return intents;
162194
}
163195

164196
/**

0 commit comments

Comments
 (0)