Allow manual sending of scheduled messages

This commit is contained in:
Moez Bhatti
2018-06-27 12:25:19 -04:00
parent 61c8ca17e8
commit d3ceec01fd
13 changed files with 89 additions and 237 deletions
@@ -16,7 +16,6 @@
package com.klinker.android.send_message;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.ContentResolver;
import android.content.ContentValues;
@@ -26,11 +25,8 @@ import android.database.Cursor;
import android.graphics.Bitmap;
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.widget.Toast;
import com.android.mms.MmsConfig;
import com.android.mms.dom.smil.parser.SmilXmlSerializer;
import com.android.mms.util.DownloadManager;
@@ -96,8 +92,6 @@ public class Transaction {
public static final long DEFAULT_EXPIRY_TIME = 7 * 24 * 60 * 60;
public static final int DEFAULT_PRIORITY = PduHeaders.PRIORITY_NORMAL;
public static final long NO_THREAD_ID = 0;
/**
* Sets context and initializes settings to default values
*
@@ -135,208 +129,9 @@ public class Transaction {
public void sendNewMessage(Message message, long threadId) {
this.saveMessage = message.getSave();
// if message:
// 1) Has images attached
// or
// 1) is enabled to send long messages as mms
// 2) number of pages for that sms exceeds value stored in settings for when to send the mms by
// 3) prefer voice is disabled
// or
// 1) more than one address is attached
// 2) group messaging is enabled
//
// then, send as MMS, else send as Voice or SMS
if (checkMMS(message)) {
try {
Looper.prepare();
} catch (Exception e) {
}
RateController.init(context);
DownloadManager.init(context);
sendMmsMessage(message.getText(), message.getAddresses(), message.getImages(), message.getImageNames(), message.getParts(), message.getSubject());
} else {
sendSmsMessage(message.getText(), message.getAddresses(), threadId, message.getDelay());
}
}
private void sendSmsMessage(String text, String[] addresses, long threadId, int delay) {
Log.v("send_transaction", "message text: " + text);
Uri messageUri;
int messageId = 0;
if (saveMessage) {
Log.v("send_transaction", "saving message");
// add signature to original text to be saved in database (does not strip unicode for saving though)
if (!settings.getSignature().equals("")) {
text += "\n" + settings.getSignature();
}
// save the message for each of the addresses
for (String address : addresses) {
Calendar cal = Calendar.getInstance();
ContentValues values = new ContentValues();
values.put("address", address);
values.put("body", settings.getStripUnicode() ? StripAccents.stripAccents(text) : text);
values.put("date", cal.getTimeInMillis() + "");
values.put("read", 1);
values.put("type", 4);
// attempt to create correct thread id if one is not supplied
if (threadId == NO_THREAD_ID || addresses.length > 1) {
threadId = Utils.getOrCreateThreadId(context, address);
}
Log.v("send_transaction", "saving message with thread id: " + threadId);
values.put("thread_id", threadId);
messageUri = context.getContentResolver().insert(Uri.parse("content://sms/"), values);
Log.v("send_transaction", "inserted to uri: " + messageUri);
Cursor query = context.getContentResolver().query(messageUri, new String[]{"_id"}, null, null, null);
if (query != null && query.moveToFirst()) {
messageId = query.getInt(0);
query.close();
}
Log.v("send_transaction", "message id: " + messageId);
// set up sent and delivered pending intents to be used with message request
Intent sentIntent = new Intent(SMS_SENT);
BroadcastUtils.addClassName(context, sentIntent, SMS_SENT);
sentIntent.putExtra("message_uri", messageUri.toString());
PendingIntent sentPI = PendingIntent.getBroadcast(
context, messageId, sentIntent, PendingIntent.FLAG_UPDATE_CURRENT);
Intent deliveredIntent = new Intent(SMS_DELIVERED);
BroadcastUtils.addClassName(context, deliveredIntent, SMS_DELIVERED);
deliveredIntent.putExtra("message_uri", messageUri.toString());
PendingIntent deliveredPI = PendingIntent.getBroadcast(
context, messageId, deliveredIntent, PendingIntent.FLAG_UPDATE_CURRENT);
ArrayList<PendingIntent> sPI = new ArrayList<>();
ArrayList<PendingIntent> dPI = new ArrayList<>();
String body = text;
// edit the body of the text if unicode needs to be stripped
if (settings.getStripUnicode()) {
body = StripAccents.stripAccents(body);
}
if (!settings.getPreText().equals("")) {
body = settings.getPreText() + " " + body;
}
SmsManager smsManager = SmsManagerFactory.INSTANCE.createSmsManager(settings);
Log.v("send_transaction", "found sms manager");
if (settings.getSplit()) {
Log.v("send_transaction", "splitting message");
// figure out the length of supported message
int[] 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
int length = (body.length() + splitData[2]) / splitData[0];
Log.v("send_transaction", "length: " + length);
boolean counter = false;
if (settings.getSplitCounter() && body.length() > length) {
counter = true;
length -= 6;
}
// get the split messages
String[] textToSend = splitByLength(body, length, counter);
// send each message part to each recipient attached to message
for (String aTextToSend : textToSend) {
ArrayList<String> parts = smsManager.divideMessage(aTextToSend);
for (int k = 0; k < parts.size(); k++) {
sPI.add(saveMessage ? sentPI : null);
dPI.add(settings.getDeliveryReports() && saveMessage ? deliveredPI : null);
}
Log.v("send_transaction", "sending split message");
sendDelayedSms(smsManager, address, parts, sPI, dPI, delay, messageUri);
}
} else {
Log.v("send_transaction", "sending without splitting");
// send the message normally without forcing anything to be split
ArrayList<String> parts = smsManager.divideMessage(body);
for (int j = 0; j < parts.size(); j++) {
sPI.add(saveMessage ? sentPI : null);
dPI.add(settings.getDeliveryReports() && saveMessage ? deliveredPI : null);
}
if (Utils.isDefaultSmsApp(context)) {
try {
Log.v("send_transaction", "sent message");
sendDelayedSms(smsManager, address, parts, sPI, dPI, delay, messageUri);
} catch (Exception e) {
// whoops...
Log.v("send_transaction", "error sending message");
Log.e(TAG, "exception thrown", e);
try {
((Activity) context).getWindow().getDecorView().findViewById(android.R.id.content).post(new Runnable() {
@Override
public void run() {
Toast.makeText(context, "Message could not be sent", Toast.LENGTH_LONG).show();
}
});
} catch (Exception f) {
}
}
} else {
// not default app, so just fire it off right away for the hell of it
smsManager.sendMultipartTextMessage(address, null, parts, sPI, dPI);
}
}
}
}
}
private void sendDelayedSms(final SmsManager smsManager, final String address,
final ArrayList<String> parts, final ArrayList<PendingIntent> sPI,
final ArrayList<PendingIntent> dPI, final int delay, final Uri messageUri) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(delay);
} catch (Exception e) {
}
if (checkIfMessageExistsAfterDelay(messageUri)) {
Log.v("send_transaction", "message sent after delay");
try {
smsManager.sendMultipartTextMessage(address, null, parts, sPI, dPI);
} catch (Exception e) {
Log.e(TAG, "exception thrown", e);
}
} else {
Log.v("send_transaction", "message not sent after delay, no longer exists");
}
}
}).start();
}
private boolean checkIfMessageExistsAfterDelay(Uri messageUti) {
Cursor query = context.getContentResolver().query(messageUti, new String[]{"_id"}, null, null, null);
if (query != null && query.moveToFirst()) {
query.close();
return true;
} else {
return false;
}
RateController.init(context);
DownloadManager.init(context);
sendMmsMessage(message.getText(), message.getAddresses(), message.getImages(), message.getImageNames(), message.getParts(), message.getSubject());
}
private void sendMmsMessage(String text, String[] addresses, Bitmap[] image, String[] imageNames, List<Message.Part> parts, String subject) {
@@ -651,30 +446,6 @@ public class Transaction {
public byte[] bytes;
}
// splits text and adds split counter when applicable
private String[] splitByLength(String s, int chunkSize, boolean counter) {
int arraySize = (int) Math.ceil((double) s.length() / chunkSize);
String[] returnArray = new String[arraySize];
int index = 0;
for (int i = 0; i < s.length(); i = i + chunkSize) {
if (s.length() - i < chunkSize) {
returnArray[index++] = s.substring(i);
} else {
returnArray[index++] = s.substring(i, i + chunkSize);
}
}
if (counter && returnArray.length > 1) {
for (int i = 0; i < returnArray.length; i++) {
returnArray[i] = "(" + (i + 1) + "/" + returnArray.length + ") " + returnArray[i];
}
}
return returnArray;
}
private static Uri insert(Context context, String[] to, MMSPart[] parts, String subject) {
try {
Uri destUri = Uri.parse("content://mms");
+25
View File
@@ -0,0 +1,25 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 28
defaultConfig {
minSdkVersion 21
targetSdkVersion 28
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
}
dependencies {
implementation "androidx.core:core-ktx:$ktx_version"
implementation "com.jakewharton.timber:timber:$timber_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation project(":android-smsmms")
}
repositories {
mavenCentral()
}
+2
View File
@@ -0,0 +1,2 @@
<manifest package="com.moez.QKSMS"
xmlns:android="http://schemas.android.com/apk/res/android" />
+1
View File
@@ -55,6 +55,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "me.leolin:ShortcutBadger:1.1.21"
implementation project(":android-smsmms")
implementation project(':common')
implementation project(':domain')
withAnalyticsImplementation "com.amplitude:android-sdk:2.16.0"
withAnalyticsImplementation "com.mixpanel.android:mixpanel-android:5.2.1"
@@ -45,6 +45,7 @@ import com.moez.QKSMS.model.Conversation
import com.moez.QKSMS.model.Message
import com.moez.QKSMS.model.MmsPart
import com.moez.QKSMS.util.Preferences
import com.moez.QKSMS.util.tryOrNull
import io.realm.Case
import io.realm.Realm
import io.realm.RealmResults
@@ -276,7 +277,7 @@ class MessageRepositoryImpl @Inject constructor(
attachments
.filter { attachment -> !attachment.isGif(context) }
.mapNotNull { attachment -> attachment.getUri() }
.mapNotNull { uri -> imageRepository.loadImage(uri) }
.mapNotNull { uri -> tryOrNull { imageRepository.loadImage(uri) } }
.also { totalImageBytes = it.sumBy { it.allocationByteCount } }
.map { bitmap ->
val byteRatio = bitmap.allocationByteCount / totalImageBytes.toFloat()
@@ -18,10 +18,13 @@
*/
package com.moez.QKSMS.util
import timber.log.Timber
fun <T> tryOrNull(body: () -> T?): T? {
return try {
body()
} catch (e: Exception) {
Timber.w(e)
null
}
}
+1
View File
@@ -31,6 +31,7 @@ dependencies {
implementation "androidx.core:core-ktx:$ktx_version"
implementation "com.jakewharton.timber:timber:$timber_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation project(":common")
}
repositories {
@@ -0,0 +1,43 @@
package com.moez.QKSMS.interactor
import android.content.Context
import android.net.Uri
import com.moez.QKSMS.compat.TelephonyCompat
import com.moez.QKSMS.extensions.mapNotNull
import com.moez.QKSMS.model.Attachment
import com.moez.QKSMS.repository.ConversationRepository
import com.moez.QKSMS.repository.MessageRepository
import com.moez.QKSMS.repository.ScheduledMessageRepository
import io.reactivex.Flowable
import javax.inject.Inject
class SendScheduledMessage @Inject constructor(
private val context: Context,
private val conversationRepo: ConversationRepository,
private val messageRepo: MessageRepository,
private val scheduledMessageRepo: ScheduledMessageRepository
) : Interactor<Long>() {
override fun buildObservable(params: Long): Flowable<*> {
return Flowable.just(params)
.mapNotNull(scheduledMessageRepo::getScheduledMessage)
.map { message ->
TelephonyCompat.getOrCreateThreadId(context, message.recipients).also { threadId ->
if ((message.recipients.size == 1 || !message.sendAsGroup) && message.attachments.isEmpty()) {
message.recipients.forEach { address ->
messageRepo.sendSmsAndPersist(message.subId, threadId, address, message.body)
}
} else {
val attachments = message.attachments.mapNotNull(Uri::parse).map { Attachment(it) }
messageRepo.sendMms(message.subId, threadId, message.recipients, message.body, attachments)
}
}
}
.doOnNext { scheduledMessageRepo.deleteScheduledMessage(params) }
// If this was the first message sent in the conversation, the conversation might not exist yet
.doOnNext { conversationRepo.getOrCreateConversation(it) }
.doOnNext { conversationRepo.updateConversations(it) }
.doOnNext { conversationRepo.markUnarchived(it) }
}
}
+1
View File
@@ -156,6 +156,7 @@ dependencies {
implementation "com.jakewharton.timber:timber:$timber_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation project(":android-smsmms")
implementation project(":common")
implementation project(':data')
implementation project(':domain')
noAnalyticsDebug project(path: ':data', configuration: 'noAnalyticsDebug')
@@ -22,9 +22,9 @@ import android.app.Activity
import android.app.Application
import android.app.Service
import android.content.BroadcastReceiver
import androidx.core.provider.FontRequest
import androidx.emoji.text.EmojiCompat
import androidx.emoji.text.FontRequestEmojiCompatConfig
import androidx.core.provider.FontRequest
import com.akaita.java.rxjava2debug.RxJava2Debug
import com.bugsnag.android.Bugsnag
import com.bugsnag.android.Configuration
@@ -69,7 +69,7 @@ class QKApplication : Application(), HasActivityInjector, HasBroadcastReceiverIn
projectPackages = packages
})
RxJava2Debug.enableRxJava2AssemblyTracking(packages)
RxJava2Debug.enableRxJava2AssemblyTracking()
Realm.init(this)
Realm.setDefaultConfiguration(RealmConfiguration.Builder()
@@ -7,6 +7,7 @@ import com.moez.QKSMS.R
import com.moez.QKSMS.common.base.QkRealmAdapter
import com.moez.QKSMS.common.base.QkViewHolder
import com.moez.QKSMS.common.util.DateFormatter
import com.moez.QKSMS.interactor.SendScheduledMessage
import com.moez.QKSMS.model.Contact
import com.moez.QKSMS.model.PhoneNumber
import com.moez.QKSMS.model.Recipient
@@ -18,7 +19,8 @@ import javax.inject.Inject
class ScheduledMessageAdapter @Inject constructor(
private val contactRepo: ContactRepository,
private val dateFormatter: DateFormatter
private val dateFormatter: DateFormatter,
private val sendScheduledMessage: SendScheduledMessage
) : QkRealmAdapter<ScheduledMessage>() {
private val contacts by lazy { contactRepo.getContacts() }
@@ -32,6 +34,8 @@ class ScheduledMessageAdapter @Inject constructor(
val message = getItem(position)!!
val view = holder.itemView
view.setOnClickListener { sendScheduledMessage.execute(message.id) }
message.recipients.forEach { address ->
if (!contactMap.containsKey(address)) {
contactMap[address] = contacts
+1 -1
View File
@@ -1 +1 @@
include ':presentation', ':android-smsmms', ':domain', ':data'
include ':presentation', ':android-smsmms', ':domain', ':data', ':common'