Skip to content

Commit 14b718e

Browse files
committed
Add ability to receive MMS attachments
1 parent 52a6b11 commit 14b718e

File tree

5 files changed

+168
-43
lines changed

5 files changed

+168
-43
lines changed

android/app/src/main/AndroidManifest.xml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@
4848
<activity android:name=".SettingsActivity" android:screenOrientation="portrait"
4949
tools:ignore="LockedOrientationActivity" />
5050
<activity
51-
android:name="com.journeyapps.barcodescanner.CaptureActivity"
52-
android:screenOrientation="fullSensor"
53-
tools:replace="screenOrientation"
54-
tools:ignore="DiscouragedApi" />
51+
android:name="com.journeyapps.barcodescanner.CaptureActivity"
52+
android:screenOrientation="fullSensor"
53+
tools:replace="screenOrientation"
54+
tools:ignore="DiscouragedApi" />
5555

5656
<service
5757
android:name=".services.StickyNotificationService"
@@ -74,6 +74,10 @@
7474
<intent-filter android:priority="999">
7575
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
7676
</intent-filter>
77+
<intent-filter android:priority="999">
78+
<action android:name="android.provider.Telephony.WAP_PUSH_RECEIVED" />
79+
<data android:mimeType="application/vnd.wap.mms-message" />
80+
</intent-filter>
7781
</receiver>
7882

7983
<receiver android:enabled="true" android:exported="true" android:name=".receivers.PhoneStateReceiver" android:permission="android.permission.READ_PHONE_STATE">

android/app/src/main/java/com/httpsms/Constants.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class Constants {
1010
const val KEY_MESSAGE_TIMESTAMP = "KEY_MESSAGE_TIMESTAMP"
1111
const val KEY_MESSAGE_REASON = "KEY_MESSAGE_REASON"
1212
const val KEY_MESSAGE_ENCRYPTED = "KEY_MESSAGE_ENCRYPTED"
13+
const val KEY_MESSAGE_ATTACHMENTS = "KEY_MESSAGE_ATTACHMENTS"
1314

1415

1516
const val KEY_HEARTBEAT_ID = "KEY_HEARTBEAT_ID"

android/app/src/main/java/com/httpsms/HttpSmsApiService.kt

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import okhttp3.MediaType.Companion.toMediaType
77
import okhttp3.OkHttpClient
88
import okhttp3.Request
99
import okhttp3.RequestBody.Companion.toRequestBody
10-
import org.apache.commons.text.StringEscapeUtils
1110
import timber.log.Timber
1211
import java.io.File
1312
import java.io.FileOutputStream
@@ -75,17 +74,8 @@ class HttpSmsApiService(private val apiKey: String, private val baseURL: URI) {
7574
return sendEvent(messageId, "FAILED", timestamp, reason)
7675
}
7776

78-
fun receive(sim: String, from: String, to: String, content: String, encrypted: Boolean, timestamp: String): Boolean {
79-
val body = """
80-
{
81-
"content": "${StringEscapeUtils.escapeJson(content)}",
82-
"sim": "$sim",
83-
"from": "$from",
84-
"timestamp": "$timestamp",
85-
"encrypted": $encrypted,
86-
"to": "$to"
87-
}
88-
""".trimIndent()
77+
fun receive(requestPayload: ReceivedMessageRequest): Boolean {
78+
val body = com.beust.klaxon.Klaxon().toJsonString(requestPayload)
8979

9080
val request: Request = Request.Builder()
9181
.url(resolveURL("/v1/messages/receive"))
@@ -94,16 +84,21 @@ class HttpSmsApiService(private val apiKey: String, private val baseURL: URI) {
9484
.header(clientVersionHeader, BuildConfig.VERSION_NAME)
9585
.build()
9686

97-
val response = client.newCall(request).execute()
87+
val response = try {
88+
client.newCall(request).execute()
89+
} catch (e: Exception) {
90+
Timber.e(e, "Exception while sending received message request")
91+
return false
92+
}
93+
9894
if (!response.isSuccessful) {
99-
Timber.e("error response [${response.body?.string()}] with code [${response.code}] while receiving message [${body}]")
95+
Timber.e("error response [${response.body?.string()}] with code [${response.code}] while receiving message")
10096
response.close()
10197
return response.code in 400..499
10298
}
10399

104-
val message = ResponseMessage.fromJson(response.body!!.string())
105100
response.close()
106-
Timber.i("received message stored successfully for message with ID [${message?.data?.id}]" )
101+
Timber.i("received message stored successfully")
107102
return true
108103
}
109104

@@ -211,14 +206,14 @@ class HttpSmsApiService(private val apiKey: String, private val baseURL: URI) {
211206
val inputStream = body.byteStream()
212207
FileOutputStream(tempFile).use { outputStream ->
213208
inputStream.use { input ->
214-
input.copyToWithLimit(outputStream, MAX_MMS_ATTACHMENT_SIZE.toLong())
209+
input.copyToWithLimit(outputStream, MAX_MMS_ATTACHMENT_SIZE)
215210
}
216211
}
217212

218213
return Pair(tempFile, body.contentType())
219214
}
220215
} catch (e: Exception) {
221-
Timber.e(e, "Exception while downloading attachment")
216+
Timber.e(e, "Exception while download attachment")
222217
return Pair(null, null)
223218
}
224219
}

android/app/src/main/java/com/httpsms/Models.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,20 @@ data class Message (
7272

7373
val attachments: List<String>? = null
7474
)
75+
76+
data class ReceivedAttachment(
77+
val name: String,
78+
@Json(name = "content_type")
79+
val contentType: String,
80+
val content: String
81+
)
82+
83+
data class ReceivedMessageRequest(
84+
val sim: String,
85+
val from: String,
86+
val to: String,
87+
val content: String,
88+
val encrypted: Boolean,
89+
val timestamp: String,
90+
val attachments: List<ReceivedAttachment>? = null
91+
)

android/app/src/main/java/com/httpsms/ReceivedReceiver.kt

Lines changed: 129 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import android.content.BroadcastReceiver
44
import android.content.Context
55
import android.content.Intent
66
import android.provider.Telephony
7-
import androidx.work.BackoffPolicy
7+
import android.util.Base64
88
import androidx.work.Constraints
99
import androidx.work.Data
1010
import androidx.work.NetworkType
@@ -13,20 +13,30 @@ import androidx.work.WorkManager
1313
import androidx.work.Worker
1414
import androidx.work.WorkerParameters
1515
import androidx.work.workDataOf
16+
import com.google.android.mms.pdu_alt.CharacterSets
17+
import com.google.android.mms.pdu_alt.MultimediaMessagePdu
18+
import com.google.android.mms.pdu_alt.PduParser
19+
import com.google.android.mms.pdu_alt.RetrieveConf
1620
import timber.log.Timber
21+
import java.io.File
22+
import java.io.FileOutputStream
1723
import java.time.ZoneOffset
1824
import java.time.ZonedDateTime
1925
import java.time.format.DateTimeFormatter
20-
import java.util.concurrent.TimeUnit
2126

2227
class ReceivedReceiver: BroadcastReceiver()
2328
{
24-
override fun onReceive(context: Context,intent: Intent) {
25-
if (intent.action != Telephony.Sms.Intents.SMS_RECEIVED_ACTION) {
29+
override fun onReceive(context: Context, intent: Intent) {
30+
if (intent.action == Telephony.Sms.Intents.SMS_RECEIVED_ACTION) {
31+
handleSmsReceived(context, intent)
32+
} else if (intent.action == Telephony.Sms.Intents.WAP_PUSH_RECEIVED_ACTION) {
33+
handleMmsReceived(context, intent)
34+
} else {
2635
Timber.e("received invalid intent with action [${intent.action}]")
27-
return
2836
}
37+
}
2938

39+
private fun handleSmsReceived(context: Context, intent: Intent) {
3040
var smsSender = ""
3141
var smsBody = ""
3242

@@ -35,12 +45,7 @@ class ReceivedReceiver: BroadcastReceiver()
3545
smsBody += smsMessage.messageBody
3646
}
3747

38-
var sim = Constants.SIM1
39-
var owner = Settings.getSIM1PhoneNumber(context)
40-
if (intent.getIntExtra("android.telephony.extra.SLOT_INDEX", 0) > 0 && Settings.isDualSIM(context)) {
41-
owner = Settings.getSIM2PhoneNumber(context)
42-
sim = Constants.SIM2
43-
}
48+
val (sim, owner) = getSimAndOwner(context, intent)
4449

4550
if (!Settings.isIncomingMessageEnabled(context, sim)) {
4651
Timber.w("[${sim}] is not active for incoming messages")
@@ -56,7 +61,71 @@ class ReceivedReceiver: BroadcastReceiver()
5661
)
5762
}
5863

59-
private fun handleMessageReceived(context: Context, sim: String, from: String, to : String, content: String) {
64+
private fun handleMmsReceived(context: Context, intent: Intent) {
65+
val pushData = intent.getByteArrayExtra("data") ?: return
66+
val pdu = PduParser(pushData, true).parse() ?: return
67+
68+
if (pdu !is MultimediaMessagePdu) {
69+
Timber.d("Received PDU is not a MultimediaMessagePdu, ignoring.")
70+
return
71+
}
72+
73+
val from = pdu.from?.string ?: ""
74+
var content = ""
75+
val attachmentFiles = mutableListOf<String>()
76+
77+
// Check if it's a RetrieveConf (which contains the actual message body)
78+
if (pdu is RetrieveConf) {
79+
val body = pdu.body
80+
if (body != null) {
81+
for (i in 0 until body.partsNum) {
82+
val part = body.getPart(i)
83+
val partData = part.data ?: continue
84+
val contentType = String(part.contentType ?: "application/octet-stream".toByteArray())
85+
86+
if (contentType.startsWith("text/plain")) {
87+
content += String(partData, charset(CharacterSets.getMimeName(part.charset)))
88+
} else {
89+
// Save attachment to a temporary file
90+
val fileName = String(part.name ?: part.contentLocation ?: part.contentId ?: "attachment_$i".toByteArray())
91+
val tempFile = File(context.cacheDir, "received_mms_${System.currentTimeMillis()}_$i")
92+
FileOutputStream(tempFile).use { it.write(partData) }
93+
attachmentFiles.add("${tempFile.absolutePath}|${contentType}|${fileName}")
94+
}
95+
}
96+
}
97+
} else {
98+
Timber.d("Received PDU is of type [${pdu.javaClass.simpleName}], body extraction not implemented.")
99+
}
100+
101+
val (sim, owner) = getSimAndOwner(context, intent)
102+
103+
if (!Settings.isIncomingMessageEnabled(context, sim)) {
104+
Timber.w("[${sim}] is not active for incoming messages")
105+
return
106+
}
107+
108+
handleMessageReceived(
109+
context,
110+
sim,
111+
from,
112+
owner,
113+
content,
114+
attachmentFiles.toTypedArray()
115+
)
116+
}
117+
118+
private fun getSimAndOwner(context: Context, intent: Intent): Pair<String, String> {
119+
var sim = Constants.SIM1
120+
var owner = Settings.getSIM1PhoneNumber(context)
121+
if (intent.getIntExtra("android.telephony.extra.SLOT_INDEX", 0) > 0 && Settings.isDualSIM(context)) {
122+
owner = Settings.getSIM2PhoneNumber(context)
123+
sim = Constants.SIM2
124+
}
125+
return Pair(sim, owner)
126+
}
127+
128+
private fun handleMessageReceived(context: Context, sim: String, from: String, to : String, content: String, attachments: Array<String>? = null) {
60129
val timestamp = ZonedDateTime.now(ZoneOffset.UTC)
61130

62131
if (!Settings.isLoggedIn(context)) {
@@ -84,7 +153,8 @@ class ReceivedReceiver: BroadcastReceiver()
84153
Constants.KEY_MESSAGE_SIM to sim,
85154
Constants.KEY_MESSAGE_CONTENT to body,
86155
Constants.KEY_MESSAGE_ENCRYPTED to Settings.encryptReceivedMessages(context),
87-
Constants.KEY_MESSAGE_TIMESTAMP to DateTimeFormatter.ofPattern(Constants.TIMESTAMP_PATTERN).format(timestamp).replace("+", "Z")
156+
Constants.KEY_MESSAGE_TIMESTAMP to DateTimeFormatter.ofPattern(Constants.TIMESTAMP_PATTERN).format(timestamp).replace("+", "Z"),
157+
Constants.KEY_MESSAGE_ATTACHMENTS to attachments
88158
)
89159

90160
val work = OneTimeWorkRequest
@@ -104,14 +174,52 @@ class ReceivedReceiver: BroadcastReceiver()
104174
override fun doWork(): Result {
105175
Timber.i("[${this.inputData.getString(Constants.KEY_MESSAGE_SIM)}] forwarding received message from [${this.inputData.getString(Constants.KEY_MESSAGE_FROM)}] to [${this.inputData.getString(Constants.KEY_MESSAGE_TO)}]")
106176

107-
if (HttpSmsApiService.create(applicationContext).receive(
108-
this.inputData.getString(Constants.KEY_MESSAGE_SIM)!!,
109-
this.inputData.getString(Constants.KEY_MESSAGE_FROM)!!,
110-
this.inputData.getString(Constants.KEY_MESSAGE_TO)!!,
111-
this.inputData.getString(Constants.KEY_MESSAGE_CONTENT)!!,
112-
this.inputData.getBoolean(Constants.KEY_MESSAGE_ENCRYPTED, false),
113-
this.inputData.getString(Constants.KEY_MESSAGE_TIMESTAMP)!!,
114-
)) {
177+
val sim = this.inputData.getString(Constants.KEY_MESSAGE_SIM)!!
178+
val from = this.inputData.getString(Constants.KEY_MESSAGE_FROM)!!
179+
val to = this.inputData.getString(Constants.KEY_MESSAGE_TO)!!
180+
val content = this.inputData.getString(Constants.KEY_MESSAGE_CONTENT)!!
181+
val encrypted = this.inputData.getBoolean(Constants.KEY_MESSAGE_ENCRYPTED, false)
182+
val timestamp = this.inputData.getString(Constants.KEY_MESSAGE_TIMESTAMP)!!
183+
184+
val attachmentsData = inputData.getStringArray(Constants.KEY_MESSAGE_ATTACHMENTS)
185+
val attachments = attachmentsData?.mapNotNull {
186+
val parts = it.split("|")
187+
val file = File(parts[0])
188+
if (file.exists()) {
189+
val bytes = file.readBytes()
190+
val base64Content = Base64.encodeToString(bytes, Base64.NO_WRAP)
191+
ReceivedAttachment(
192+
name = parts[2],
193+
contentType = parts[1],
194+
content = base64Content
195+
)
196+
} else {
197+
null
198+
}
199+
}
200+
201+
val request = ReceivedMessageRequest(
202+
sim = sim,
203+
from = from,
204+
to = to,
205+
content = content,
206+
encrypted = encrypted,
207+
timestamp = timestamp,
208+
attachments = attachments
209+
)
210+
211+
val success = HttpSmsApiService.create(applicationContext).receive(request)
212+
213+
// Cleanup temp files
214+
attachmentsData?.forEach {
215+
val path = it.split("|")[0]
216+
val file = File(path)
217+
if (file.exists()) {
218+
file.delete()
219+
}
220+
}
221+
222+
if (success) {
115223
return Result.success()
116224
}
117225

0 commit comments

Comments
 (0)