norn-talk/app/src/main/java/xyz/johnny/norntalk/messages/NornTransaction.kt

744 lines
29 KiB
Kotlin

/*
* Copyright 2013 Jacob Klinker
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.johnny.norntalk.messages
import android.app.Activity
import android.app.PendingIntent
import android.content.*
import android.net.Uri
import android.os.Bundle
import android.os.Looper
import android.telephony.SmsManager
import android.telephony.SmsMessage
import android.text.TextUtils
import android.view.View
import android.widget.Toast
import com.android.mms.MmsConfig
import com.android.mms.dom.smil.parser.SmilXmlSerializer
import com.android.mms.util.DownloadManager
import com.android.mms.util.RateController
import com.google.android.mms.ContentType
import com.google.android.mms.InvalidHeaderValueException
import com.google.android.mms.MMSPart
import com.google.android.mms.MmsException
import com.google.android.mms.pdu_alt.*
import com.google.android.mms.smil.SmilHelper
import com.klinker.android.logger.Log
import com.klinker.android.send_message.*
import java.io.*
import java.util.*
/**
* Class to process transaction requests for sending
*
* @author Jake Klinker
*
* Sets context and settings
*
* @param context is the context of the activity or service
*/
class NornTransaction constructor(private val context: Context) {
private var explicitSentSmsReceiver: Intent? = null
private var explicitSentMmsReceiver: Intent? = null
private var explicitDeliveredSmsReceiver: Intent? = null
private var saveMessage = true
var SMS_SENT = ".SMS_SENT"
var SMS_DELIVERED = ".SMS_DELIVERED"
init {
SMS_SENT = context.packageName + SMS_SENT
SMS_DELIVERED = context.packageName + SMS_DELIVERED
if (NOTIFY_SMS_FAILURE == ".NOTIFY_SMS_FAILURE") {
NOTIFY_SMS_FAILURE = context.packageName + NOTIFY_SMS_FAILURE
}
}
/**
* Evoyer un message à partir d'une instance de [NornMessage]
*
* @param message Message à envoyer
* @param insert Si Vrai le message doit être inséré dans la base de données, Faux sinon
* cela permet de cacher les messages d'échange de clés à l'utilisateur
* @param raw Si Vrai le message sera envoyé en clair, sinon le message sera chiffré en fonction
* des propriétés de la conversation
*/
fun sendNewMessage(message: NornMessage, insert: Boolean, raw: Boolean) {
if (message.medias.isEmpty()) {
this.sendSmsMessage(message, insert, raw)
} else {
try {
Looper.prepare()
} catch (e: Exception) {}
RateController.init(context)
DownloadManager.init(context)
this.sendMmsMessage(message)
}
}
/**
* Optional: define a [BroadcastReceiver] that will get started when Android notifies us that the SMS has
* been marked as "sent". If you do not define a receiver here, it will look for the .SMS_SENT receiver
* that was defined in the AndroidManifest, as discussed in the README.md.
*
* @param intent the receiver that you want to start when the message gets marked as sent.
*/
fun setExplicitBroadcastForSentSms(intent: Intent): NornTransaction {
explicitSentSmsReceiver = intent
return this
}
/**
* Optional: define a [BroadcastReceiver] that will get started when Android notifies us that the MMS has
* been marked as "sent". If you do not define a receiver here, it will look for the .MMS_SENT receiver
* that was defined in the AndroidManifest, as discussed in the README.md.
*
* @param intent the receiver that you want to start when the message gets marked as sent.
*/
fun setExplicitBroadcastForSentMms(intent: Intent): NornTransaction {
explicitSentMmsReceiver = intent
return this
}
/**
* Optional: define a [BroadcastReceiver] that will get started when Android notifies us that the SMS has
* been marked as "delivered". If you do not define a receiver here, it will look for the .SMS_DELIVERED
* receiver that was defined in the AndroidManifest, as discussed in the README.md.
*
*
* Providing a receiver here does not guarantee that it will ever get started. If the [Settings]
* object does not have delivery reports turned on, this receiver will never get called.
*
* @param intent the receiver that you want to start when the message gets marked as sent.
*/
fun setExplicitBroadcastForDeliveredSms(intent: Intent): NornTransaction {
explicitDeliveredSmsReceiver = intent
return this
}
private fun sendSmsMessage(message: NornMessage, insert: Boolean, raw: Boolean) {
// envoyer le message chiffré si la conversation est sécurisée
val body = message.messageBody(raw)
// save the message for each of the addresses
for (i in message.addresses.indices) {
var sentPI : PendingIntent? = null
var deliveredPI : PendingIntent? = null
if (insert) {
message.insertMessage(context).get()
Log.v("send_transaction", "message id: " + message.id)
// set up sent and delivered pending intents to be used with message request
val sentIntent: Intent?
if (explicitSentSmsReceiver == null) {
sentIntent = Intent(SMS_SENT)
BroadcastUtils.addClassName(context, sentIntent, SMS_SENT)
} else {
sentIntent = explicitSentSmsReceiver
}
sentIntent!!.putExtra("message_id", message.id)
sentPI = PendingIntent.getBroadcast(context, message.id.toInt(), sentIntent,
PendingIntent.FLAG_UPDATE_CURRENT)
val deliveredIntent: Intent?
if (explicitDeliveredSmsReceiver == null) {
deliveredIntent = Intent(SMS_DELIVERED)
BroadcastUtils.addClassName(context, deliveredIntent, SMS_DELIVERED)
} else {
deliveredIntent = explicitDeliveredSmsReceiver
}
deliveredIntent!!.putExtra("message_id", message.id)
deliveredPI = PendingIntent.getBroadcast(context, message.id.toInt(),
deliveredIntent, PendingIntent.FLAG_UPDATE_CURRENT)
}
val sPI = ArrayList<PendingIntent?>()
val dPI = ArrayList<PendingIntent?>()
val smsManager = SmsManagerFactory.createSmsManager(settings)
Log.v("send_transaction", "found sms manager")
if (settings.split) {
Log.v("send_transaction", "splitting message")
// figure out the length of supported message
val splitData = SmsMessage.calculateLength(body, false)
// we take the current length + the remaining length to get the total number of characters
// that message set can support, and then divide by the number of message that will require
// to get the length supported by a single message
var length = (body.length + splitData[2]) / splitData[0]
Log.v("send_transaction", "length: $length")
var counter = false
if (settings.splitCounter && body.length > length) {
counter = true
length -= 6
}
// get the split messages
val textToSend = splitByLength(body, length, counter)
// send each message part to each recipient attached to message
for (j in textToSend.indices) {
val parts = smsManager.divideMessage(textToSend[j])
for (k in parts.indices) {
sPI.add(sentPI)
dPI.add(if (settings.deliveryReports) deliveredPI else null)
}
Log.v("send_transaction", "sending split message")
smsManager.sendMultipartTextMessage(message.addresses[i], null, parts, sPI, dPI)
}
} else {
Log.v("send_transaction", "sending without splitting")
// send the message normally without forcing anything to be split
val parts = smsManager.divideMessage(body)
for (j in parts.indices) {
sPI.add(sentPI)
dPI.add(if (settings.deliveryReports) deliveredPI else null)
}
try {
Log.v("send_transaction", "sent message")
smsManager.sendMultipartTextMessage(message.addresses[i], null, parts, sPI, dPI)
} catch (e: Exception) {
// whoops...
Log.v("send_transaction", "error sending message")
Log.e(TAG, "exception thrown", e)
try {
(context as Activity).window.decorView.findViewById<View>(android.R.id.content).post({ Toast.makeText(context, "Message could not be sent", Toast.LENGTH_LONG).show() })
} catch (f: Exception) {}
}
}
}
}
private fun sendMmsMessage(message: NornMessage) {
// create the medias to send
val data = ArrayList<MMSPart>()
// add any extra medias according to their mimeType set in the message
// eg. videos, audio, contact cards, location maybe?
for (m in message.medias) {
val part = MMSPart()
part.Name = m.name
part.MimeType = m.mimeType
part.Data = m.getContent(context, message)
data.add(part)
}
val text = message.messageBody(false)
if (message.text != "") {
// add text to the end of the part and send
val part = MMSPart()
part.Name = "text"
part.MimeType = "text/plain"
part.Data = text.toByteArray()
data.add(part)
}
message.insertMessage(context).get()
Log.v(TAG, "using system method for sending")
sendMmsThroughSystem(context, message.subject, data, message.addresses, explicitSentMmsReceiver)
}
class MessageInfo {
var token: Long = 0
var location: Uri? = null
var bytes: ByteArray? = null
}
// splits text and adds split counter when applicable
private fun splitByLength(s: String, chunkSize: Int, counter: Boolean): Array<String?> {
val arraySize = Math.ceil(s.length.toDouble() / chunkSize).toInt()
val returnArray = arrayOfNulls<String>(arraySize)
var index = 0
run {
var i = 0
while (i < s.length) {
if (s.length - i < chunkSize) {
returnArray[index++] = s.substring(i)
} else {
returnArray[index++] = s.substring(i, i + chunkSize)
}
i = i + chunkSize
}
}
if (counter && returnArray.size > 1) {
for (i in returnArray.indices) {
returnArray[i] = "(" + (i + 1) + "/" + returnArray.size + ") " + returnArray[i]
}
}
return returnArray
}
/**
* A method for checking whether or not a certain message will be sent as mms depending on its contents and the settings
*
* @param message is the message that you are checking against
* @return true if the message will be mms, otherwise false
*/
fun checkMMS(message: Message): Boolean {
return message.images.size != 0 ||
message.parts.size != 0 ||
settings.sendLongAsMms && Utils.getNumPages(settings, message.text) > settings.sendLongAsMmsAfter ||
message.addresses.size > 1 && settings.group ||
message.subject != null
}
companion object {
private val TAG = "NornTransaction"
val settings by lazy { Settings() }
var NOTIFY_SMS_FAILURE = ".NOTIFY_SMS_FAILURE"
val MMS_ERROR = "com.klinker.android.send_message.MMS_ERROR"
val REFRESH = "com.klinker.android.send_message.REFRESH"
val MMS_PROGRESS = "com.klinker.android.send_message.MMS_PROGRESS"
val NOTIFY_OF_DELIVERY = "com.klinker.android.send_message.NOTIFY_DELIVERY"
val NOTIFY_OF_MMS = "com.klinker.android.messaging.NEW_MMS_DOWNLOADED"
val NO_THREAD_ID: Long = 0
@Throws(MmsException::class)
fun getBytes(context: Context, saveMessage: Boolean, recipients: Array<String>,
parts: Array<MMSPart>?, subject: String?): MessageInfo {
val sendRequest = SendReq()
// create send request addresses
for (i in recipients.indices) {
val phoneNumbers = EncodedStringValue.extract(recipients[i])
if (phoneNumbers != null && phoneNumbers.size > 0) {
sendRequest.addTo(phoneNumbers[0])
}
}
if (subject != null) {
sendRequest.subject = EncodedStringValue(subject)
}
sendRequest.date = Calendar.getInstance().timeInMillis / 1000L
try {
sendRequest.from = EncodedStringValue(Utils.getMyPhoneNumber(context))
} catch (e: Exception) {
Log.e(TAG, "error getting from address", e)
}
val pduBody = PduBody()
// assign parts to the pdu body which contains sending data
var size: Long = 0
if (parts != null) {
for (i in parts.indices) {
val part = parts[i]
if (part != null) {
try {
val partPdu = PduPart()
partPdu.name = part.Name.toByteArray()
partPdu.contentType = part.MimeType.toByteArray()
if (part.MimeType.startsWith("text")) {
partPdu.charset = CharacterSets.UTF_8
}
// Set Content-Location.
partPdu.contentLocation = part.Name.toByteArray()
val index = part.Name.lastIndexOf(".")
val contentId = if (index == -1)
part.Name
else
part.Name.substring(0, index)
partPdu.contentId = contentId.toByteArray()
partPdu.data = part.Data
pduBody.addPart(partPdu)
size += (2 * part.Name.toByteArray().size + part.MimeType.toByteArray().size + part.Data.size + contentId.toByteArray().size).toLong()
} catch (e: Exception) {
}
}
}
}
val out = ByteArrayOutputStream()
SmilXmlSerializer.serialize(SmilHelper.createSmilDocument(pduBody), out)
val smilPart = PduPart()
smilPart.contentId = "smil".toByteArray()
smilPart.contentLocation = "smil.xml".toByteArray()
smilPart.contentType = ContentType.APP_SMIL.toByteArray()
smilPart.data = out.toByteArray()
pduBody.addPart(0, smilPart)
sendRequest.body = pduBody
Log.v(TAG, "setting message size to $size bytes")
sendRequest.messageSize = size
// add everything else that could be set
sendRequest.priority = PduHeaders.PRIORITY_NORMAL
sendRequest.deliveryReport = PduHeaders.VALUE_NO
sendRequest.expiry = (1000 * 60 * 60 * 24 * 7).toLong()
sendRequest.messageClass = PduHeaders.MESSAGE_CLASS_PERSONAL_STR.toByteArray()
sendRequest.readReport = PduHeaders.VALUE_NO
// create byte array which will actually be sent
val composer = PduComposer(context, sendRequest)
val bytesToSend: ByteArray
try {
bytesToSend = composer.make()
} catch (e: OutOfMemoryError) {
throw MmsException("Out of memory!")
}
val info = MessageInfo()
info.bytes = bytesToSend
if (saveMessage) {
try {
val persister = PduPersister.getPduPersister(context)
info.location = persister.persist(sendRequest, Uri.parse("content://mms/outbox"), true, settings.group, null)
} catch (e: Exception) {
Log.v("sending_mms_library", "error saving mms message")
Log.e(TAG, "exception thrown", e)
// use the old way if something goes wrong with the persister
insert(context, recipients, parts, subject)
}
}
try {
val query = context.contentResolver.query(info.location!!, arrayOf("thread_id"), null, null, null)
if (query != null && query.moveToFirst()) {
info.token = query.getLong(query.getColumnIndex("thread_id"))
query.close()
} else {
// just default sending token for what I had before
info.token = 4444L
}
} catch (e: Exception) {
Log.e(TAG, "exception thrown", e)
info.token = 4444L
}
return info
}
val DEFAULT_EXPIRY_TIME = (7 * 24 * 60 * 60).toLong()
val DEFAULT_PRIORITY = PduHeaders.PRIORITY_NORMAL
private fun sendMmsThroughSystem(context: Context, subject: String, parts: List<MMSPart>,
addresses: Array<String>, explicitSentMmsReceiver: Intent?) {
try {
val fileName = "send." + Math.abs(Random().nextLong()).toString() + ".dat"
val mSendFile = File(context.cacheDir, fileName)
val sendReq = buildPdu(context, addresses, subject, parts)
val persister = PduPersister.getPduPersister(context)
val messageUri = persister.persist(sendReq, Uri.parse("content://mms/outbox"),
true, settings.group, null)
val intent: Intent
if (explicitSentMmsReceiver == null) {
intent = Intent(MmsSentReceiver.MMS_SENT)
BroadcastUtils.addClassName(context, intent, MmsSentReceiver.MMS_SENT)
} else {
intent = explicitSentMmsReceiver
}
intent.putExtra(MmsSentReceiver.EXTRA_CONTENT_URI, messageUri.toString())
intent.putExtra(MmsSentReceiver.EXTRA_FILE_PATH, mSendFile.path)
val pendingIntent = PendingIntent.getBroadcast(
context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT)
val writerUri = Uri.Builder()
.authority(context.packageName + ".MmsFileProvider")
.path(fileName)
.scheme(ContentResolver.SCHEME_CONTENT)
.build()
var writer: FileOutputStream? = null
var contentUri: Uri? = null
try {
writer = FileOutputStream(mSendFile)
writer.write(PduComposer(context, sendReq).make())
contentUri = writerUri
} catch (e: IOException) {
Log.e(TAG, "Error writing send file", e)
} finally {
if (writer != null) {
try {
writer.close()
} catch (e: IOException) {
}
}
}
val configOverrides = Bundle()
configOverrides.putBoolean(SmsManager.MMS_CONFIG_GROUP_MMS_ENABLED, settings.group)
val httpParams = MmsConfig.getHttpParams()
if (!TextUtils.isEmpty(httpParams)) {
configOverrides.putString(SmsManager.MMS_CONFIG_HTTP_PARAMS, httpParams)
}
configOverrides.putInt(SmsManager.MMS_CONFIG_MAX_MESSAGE_SIZE, MmsConfig.getMaxMessageSize())
if (contentUri != null) {
SmsManagerFactory.createSmsManager(settings).sendMultimediaMessage(context,
contentUri, null, configOverrides, pendingIntent)
} else {
Log.e(TAG, "Error writing sending Mms")
try {
pendingIntent.send(SmsManager.MMS_ERROR_IO_ERROR)
} catch (ex: PendingIntent.CanceledException) {
Log.e(TAG, "Mms pending intent cancelled?", ex)
}
}
} catch (e: Exception) {
Log.e(TAG, "error using system sending method", e)
}
}
private fun buildPdu(context: Context, recipients: Array<String>, subject: String,
parts: List<MMSPart>): SendReq {
val req = SendReq()
// From, per spec
val lineNumber = Utils.getMyPhoneNumber(context)
if (!TextUtils.isEmpty(lineNumber)) {
req.from = EncodedStringValue(lineNumber)
}
// To
for (recipient in recipients) {
req.addTo(EncodedStringValue(recipient))
}
// Subject
if (!TextUtils.isEmpty(subject)) {
req.subject = EncodedStringValue(subject)
}
// Date
req.date = System.currentTimeMillis() / 1000
// Body
val body = PduBody()
// Add text part. Always add a smil part for compatibility, without it there
// may be issues on some carriers/client apps
var size = 0
for (i in parts.indices) {
val part = parts[i]
size += addTextPart(body, part, i)
}
// add a SMIL document for compatibility
val out = ByteArrayOutputStream()
SmilXmlSerializer.serialize(SmilHelper.createSmilDocument(body), out)
val smilPart = PduPart()
smilPart.contentId = "smil".toByteArray()
smilPart.contentLocation = "smil.xml".toByteArray()
smilPart.contentType = ContentType.APP_SMIL.toByteArray()
smilPart.data = out.toByteArray()
body.addPart(0, smilPart)
req.body = body
// Message size
req.messageSize = size.toLong()
// Message class
req.messageClass = PduHeaders.MESSAGE_CLASS_PERSONAL_STR.toByteArray()
// Expiry
req.expiry = DEFAULT_EXPIRY_TIME
try {
// Priority
req.priority = DEFAULT_PRIORITY
// Delivery report
req.deliveryReport = PduHeaders.VALUE_NO
// Read report
req.readReport = PduHeaders.VALUE_NO
} catch (e: InvalidHeaderValueException) {
}
return req
}
private fun addTextPart(pb: PduBody, p: MMSPart, id: Int): Int {
val filename = p.Name
val part = PduPart()
// Set Charset if it's a text medias.
if (p.MimeType.startsWith("text")) {
part.charset = CharacterSets.UTF_8
}
// Set Content-Type.
part.contentType = p.MimeType.toByteArray()
// Set Content-Location.
part.contentLocation = filename.toByteArray()
val index = filename.lastIndexOf(".")
val contentId = if (index == -1)
filename
else
filename.substring(0, index)
part.contentId = contentId.toByteArray()
part.data = p.Data
pb.addPart(part)
return part.data.size
}
private fun insert(context: Context, to: Array<String>, parts: Array<MMSPart>?, subject: String?): Uri? {
try {
val destUri = Uri.parse("content://mms")
val recipients = HashSet<String>()
recipients.addAll(Arrays.asList(*to))
val thread_id = Utils.getOrCreateThreadId(context, recipients)
// Create a dummy sms
val dummyValues = ContentValues()
dummyValues.put("thread_id", thread_id)
dummyValues.put("body", " ")
val dummySms = context.contentResolver.insert(Uri.parse("content://sms/sent"), dummyValues)
// Create a new message entry
val now = System.currentTimeMillis()
val mmsValues = ContentValues()
mmsValues.put("thread_id", thread_id)
mmsValues.put("date", now / 1000L)
mmsValues.put("msg_box", 4)
//mmsValues.put("m_id", System.currentTimeMillis());
mmsValues.put("read", true)
mmsValues.put("sub", subject ?: "")
mmsValues.put("sub_cs", 106)
mmsValues.put("ct_t", "application/vnd.wap.multipart.related")
var imageBytes: Long = 0
for (part in parts!!) {
imageBytes += part.Data.size.toLong()
}
mmsValues.put("exp", imageBytes)
mmsValues.put("m_cls", "personal")
mmsValues.put("m_type", 128) // 132 (RETRIEVE CONF) 130 (NOTIF IND) 128 (SEND REQ)
mmsValues.put("v", 19)
mmsValues.put("pri", 129)
mmsValues.put("tr_id", "T" + java.lang.Long.toHexString(now))
mmsValues.put("resp_st", 128)
// Insert message
val res = context.contentResolver.insert(destUri, mmsValues)
val messageId = res!!.lastPathSegment.trim { it <= ' ' }
// Create part
for (part in parts) {
if (part.MimeType.startsWith("image")) {
createPartImage(context, messageId, part.Data, part.MimeType)
} else if (part.MimeType.startsWith("text")) {
createPartText(context, messageId, String(part.Data, Charsets.UTF_8))
}
}
// Create addresses
for (addr in to) {
createAddr(context, messageId, addr)
}
//res = Uri.parse(destUri + "/" + messageId);
// Delete dummy sms
context.contentResolver.delete(dummySms!!, null, null)
return res
} catch (e: Exception) {
Log.v("sending_mms_library", "still an error saving... :(")
Log.e(TAG, "exception thrown", e)
}
return null
}
// create the image part to be stored in database
@Throws(Exception::class)
private fun createPartImage(context: Context, id: String, imageBytes: ByteArray, mimeType: String): Uri {
val mmsPartValue = ContentValues()
mmsPartValue.put("mid", id)
mmsPartValue.put("ct", mimeType)
mmsPartValue.put("cid", "<" + System.currentTimeMillis() + ">")
val partUri = Uri.parse("content://mms/$id/part")
val res = context.contentResolver.insert(partUri, mmsPartValue)
// Add data to part
val os = context.contentResolver.openOutputStream(res!!)
val `is` = ByteArrayInputStream(imageBytes)
val buffer = ByteArray(256)
var len = `is`.read(buffer)
while (len != -1) {
os!!.write(buffer, 0, len)
len = `is`.read(buffer)
}
os!!.close()
`is`.close()
return res
}
// create the text part to be stored in database
@Throws(Exception::class)
private fun createPartText(context: Context, id: String, text: String): Uri? {
val mmsPartValue = ContentValues()
mmsPartValue.put("mid", id)
mmsPartValue.put("ct", "text/plain")
mmsPartValue.put("cid", "<" + System.currentTimeMillis() + ">")
mmsPartValue.put("text", text)
val partUri = Uri.parse("content://mms/$id/part")
return context.contentResolver.insert(partUri, mmsPartValue)
}
// add address to the request
@Throws(Exception::class)
private fun createAddr(context: Context, id: String, addr: String): Uri? {
val addrValues = ContentValues()
addrValues.put("address", addr)
addrValues.put("charset", "106")
addrValues.put("type", 151) // TO
val addrUri = Uri.parse("content://mms/$id/addr")
return context.contentResolver.insert(addrUri, addrValues)
}
}
}