Add schedule send message creation ui

Move android-smsmms logic into a separate file
This commit is contained in:
Naveen 2022-09-26 03:02:07 +05:30
parent acefd0c0f4
commit 7624174cad
16 changed files with 485 additions and 60 deletions

View file

@ -25,6 +25,7 @@ import android.view.inputmethod.EditorInfo
import android.widget.LinearLayout
import android.widget.LinearLayout.LayoutParams
import android.widget.RelativeLayout
import androidx.core.content.res.ResourcesCompat
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.DiskCacheStrategy
@ -37,8 +38,6 @@ import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.Target
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.klinker.android.send_message.Transaction
import com.klinker.android.send_message.Utils.getNumPages
import com.simplemobiletools.commons.dialogs.ConfirmationDialog
import com.simplemobiletools.commons.dialogs.RadioGroupDialog
import com.simplemobiletools.commons.extensions.*
@ -50,17 +49,17 @@ import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.adapters.AutoCompleteTextViewAdapter
import com.simplemobiletools.smsmessenger.adapters.ThreadAdapter
import com.simplemobiletools.smsmessenger.dialogs.ScheduleSendDialog
import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.*
import com.simplemobiletools.smsmessenger.models.*
import com.simplemobiletools.smsmessenger.receivers.SmsStatusDeliveredReceiver
import com.simplemobiletools.smsmessenger.receivers.SmsStatusSentReceiver
import kotlinx.android.synthetic.main.activity_thread.*
import kotlinx.android.synthetic.main.item_attachment.view.*
import kotlinx.android.synthetic.main.item_selected_contact.view.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.joda.time.DateTime
import java.io.File
import java.io.InputStream
import java.io.OutputStream
@ -92,6 +91,9 @@ class ThreadActivity : SimpleActivity() {
private var allMessagesFetched = false
private var oldestMessageDate = -1
private var isScheduledMessage: Boolean = false
private lateinit var scheduledDateTime: DateTime
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_thread)
@ -425,6 +427,12 @@ class ThreadActivity : SimpleActivity() {
thread_send_message.setOnClickListener {
sendMessage()
}
thread_send_message.setOnLongClickListener {
if (!isScheduledMessage) {
launchScheduleSendDialog()
}
true
}
thread_send_message.isClickable = false
thread_type_message.onTextChangeListener {
@ -468,6 +476,8 @@ class ThreadActivity : SimpleActivity() {
addAttachment(it)
}
}
setupScheduleSendUi()
}
private fun setupAttachmentSizes() {
@ -909,9 +919,11 @@ class ThreadActivity : SimpleActivity() {
private fun checkSendMessageAvailability() {
if (thread_type_message.text!!.isNotEmpty() || (attachmentSelections.isNotEmpty() && !attachmentSelections.values.any { it.isPending })) {
thread_send_message.isEnabled = true
thread_send_message.isClickable = true
thread_send_message.alpha = 0.9f
} else {
thread_send_message.isEnabled = false
thread_send_message.isClickable = false
thread_send_message.alpha = 0.4f
}
@ -919,53 +931,25 @@ class ThreadActivity : SimpleActivity() {
}
private fun sendMessage() {
var msg = thread_type_message.value
if (msg.isEmpty() && attachmentSelections.isEmpty()) {
var text = thread_type_message.value
if (text.isEmpty() && attachmentSelections.isEmpty()) {
showErrorToast(getString(R.string.unknown_error_occurred))
return
}
msg = removeDiacriticsIfNeeded(msg)
text = removeDiacriticsIfNeeded(text)
val numbers = ArrayList<String>()
participants.forEach { contact ->
contact.phoneNumbers.forEach {
numbers.add(it.normalizedNumber)
}
}
val addresses = participants
.flatMap { it.phoneNumbers }
.map { it.normalizedNumber }
val settings = getSendMessageSettings()
val currentSubscriptionId = availableSIMCards.getOrNull(currentSIMCardIndex)?.subscriptionId
if (currentSubscriptionId != null) {
settings.subscriptionId = currentSubscriptionId
}
val transaction = Transaction(this, settings)
val message = com.klinker.android.send_message.Message(msg, numbers.toTypedArray())
if (attachmentSelections.isNotEmpty()) {
for (selection in attachmentSelections.values) {
try {
val byteArray = contentResolver.openInputStream(selection.uri)?.readBytes() ?: continue
val mimeType = contentResolver.getType(selection.uri) ?: continue
message.addMedia(byteArray, mimeType)
} catch (e: Exception) {
showErrorToast(e)
} catch (e: Error) {
showErrorToast(e.localizedMessage ?: getString(R.string.unknown_error_occurred))
}
}
}
val attachments = attachmentSelections.values.map { it.uri }
try {
val smsSentIntent = Intent(this, SmsStatusSentReceiver::class.java)
val deliveredIntent = Intent(this, SmsStatusDeliveredReceiver::class.java)
transaction.setExplicitBroadcastForSentSms(smsSentIntent)
transaction.setExplicitBroadcastForDeliveredSms(deliveredIntent)
refreshedSinceSent = false
transaction.sendNewMessage(message)
sendTransactionMessage(text, addresses, currentSubscriptionId, attachments)
thread_type_message.setText("")
attachmentSelections.clear()
thread_attachments_holder.beGone()
@ -1146,10 +1130,9 @@ class ThreadActivity : SimpleActivity() {
}
private fun updateMessageType() {
val settings = getSendMessageSettings()
val text = thread_type_message.text.toString()
val isGroupMms = participants.size > 1 && config.sendGroupMessageMMS
val isLongMmsMessage = getNumPages(settings, text) > settings.sendLongAsMmsAfter && config.sendLongMessageMMS
val isLongMmsMessage = isLongMmsMessage(text) && config.sendLongMessageMMS
val stringId = if (attachmentSelections.isNotEmpty() || isGroupMms || isLongMmsMessage) {
R.string.mms
} else {
@ -1166,4 +1149,58 @@ class ThreadActivity : SimpleActivity() {
}
return File.createTempFile("IMG_", ".jpg", outputDirectory)
}
private fun launchScheduleSendDialog(originalDt: DateTime? = null) {
ScheduleSendDialog(this, originalDt) { newDt ->
if (newDt != null) {
scheduledDateTime = newDt
showScheduleSendUi()
}
}
}
private fun setupScheduleSendUi() {
val textColor = getProperTextColor()
scheduled_message_holder.background.applyColorFilter(getProperBackgroundColor().getContrastColor())
scheduled_message_button.apply {
val clockDrawable = ResourcesCompat.getDrawable(resources, R.drawable.ic_clock_vector, theme)?.apply { applyColorFilter(textColor) }
setCompoundDrawablesWithIntrinsicBounds(clockDrawable, null, null, null)
setTextColor(textColor)
setOnClickListener {
launchScheduleSendDialog(scheduledDateTime)
}
}
discard_scheduled_message.apply {
applyColorFilter(textColor)
setOnClickListener {
hideScheduleSendUi()
}
}
}
private fun showScheduleSendUi() {
isScheduledMessage = true
updateSendButton()
scheduled_message_holder.beVisible()
scheduled_message_button.text = scheduledDateTime.humanize(this)
}
private fun hideScheduleSendUi() {
isScheduledMessage = false
scheduled_message_holder.beGone()
updateSendButton()
}
private fun updateSendButton() {
val drawableResId = if (isScheduledMessage) {
R.drawable.ic_schedule_send_vector
} else {
R.drawable.ic_send_vector
}
ResourcesCompat.getDrawable(resources, drawableResId, theme)?.apply {
applyColorFilter(getProperTextColor())
thread_send_message.setCompoundDrawablesWithIntrinsicBounds(null, this, null, null)
}
}
}

View file

@ -0,0 +1,161 @@
package com.simplemobiletools.smsmessenger.dialogs
import android.app.DatePickerDialog
import android.app.DatePickerDialog.OnDateSetListener
import android.app.TimePickerDialog
import android.app.TimePickerDialog.OnTimeSetListener
import android.text.format.DateFormat
import android.text.style.RelativeSizeSpan
import androidx.appcompat.app.AlertDialog
import androidx.core.text.toSpannable
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.TIME_FORMAT_12
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.extensions.round
import com.simplemobiletools.smsmessenger.helpers.DATE_FORMAT_PATTERN
import kotlinx.android.synthetic.main.schedule_message_dialog.view.*
import org.joda.time.DateTime
import java.util.*
class ScheduleSendDialog(private val activity: BaseSimpleActivity, private var dateTime: DateTime? = null, private val callback: (dt: DateTime?) -> Unit) {
private val view = activity.layoutInflater.inflate(R.layout.schedule_message_dialog, null)
private val textColor = activity.getProperTextColor()
private var previewDialog: AlertDialog? = null
private var previewShown = false
private var isNewMessage = dateTime == null
private val calendar = Calendar.getInstance()
init {
arrayOf(view.subtitle, view.edit_time, view.edit_date).forEach { it.setTextColor(textColor) }
arrayOf(view.dateIcon, view.timeIcon).forEach { it.applyColorFilter(textColor) }
view.edit_date.setOnClickListener { showDatePicker() }
view.edit_time.setOnClickListener { showTimePicker() }
updateTexts(dateTime ?: DateTime.now().plusHours(1))
if (isNewMessage) {
showDatePicker()
} else {
showPreview()
}
}
private fun updateTexts(dt: DateTime) {
val dateText = dt.toString(DATE_FORMAT_PATTERN).toSpannable()
dateText.setSpan(RelativeSizeSpan(0.6f), 2, dateText.length, 0)
val timeText = dt.toString(TIME_FORMAT_12).toSpannable()
timeText.setSpan(RelativeSizeSpan(0.6f), timeText.lastIndex - 2, timeText.length, 0)
view.edit_date.text = dateText
view.edit_time.text = timeText
}
private fun showPreview() {
if (previewShown) return
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.apply {
previewShown = true
activity.setupDialogStuff(view, this, R.string.schedule_send) { dialog ->
previewDialog = dialog
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
if (validateDateTime()) {
callback(dateTime)
dialog.dismiss()
}
}
dialog.setOnDismissListener {
previewShown = false
previewDialog = null
}
}
}
}
private fun showDatePicker() {
val year = dateTime?.year ?: calendar.get(Calendar.YEAR)
val monthOfYear = dateTime?.monthOfYear?.minus(1) ?: calendar.get(Calendar.MONTH)
val dayOfMonth = dateTime?.dayOfMonth ?: calendar.get(Calendar.DAY_OF_MONTH)
val dateSetListener = OnDateSetListener { _, y, m, d -> dateSet(y, m, d) }
DatePickerDialog(
activity, activity.getDatePickerDialogTheme(), dateSetListener, year, monthOfYear, dayOfMonth
).apply {
datePicker.minDate = System.currentTimeMillis()
show()
getButton(AlertDialog.BUTTON_NEGATIVE).apply {
text = activity.getString(R.string.back)
setOnClickListener {
showPreview()
dismiss()
}
}
}
}
private fun showTimePicker() {
val hourOfDay = dateTime?.hourOfDay ?: getNextHour()
val minute = dateTime?.minuteOfHour ?: getNextMinute()
val timeSetListener = OnTimeSetListener { _, h, m -> timeSet(h, m) }
TimePickerDialog(
activity, activity.getDatePickerDialogTheme(), timeSetListener, hourOfDay, minute, DateFormat.is24HourFormat(activity)
).apply {
show()
getButton(AlertDialog.BUTTON_NEGATIVE).apply {
text = activity.getString(R.string.back)
setOnClickListener {
showPreview()
dismiss()
}
}
}
}
private fun dateSet(year: Int, monthOfYear: Int, dayOfMonth: Int) {
if (isNewMessage) {
showTimePicker()
}
dateTime = DateTime.now()
.withDate(year, monthOfYear + 1, dayOfMonth)
.run {
if (dateTime != null) {
withTime(dateTime!!.hourOfDay, dateTime!!.minuteOfHour, 0, 0)
} else {
withTime(getNextHour(), getNextMinute(), 0, 0)
}
}
if (!isNewMessage) {
validateDateTime()
}
isNewMessage = false
updateTexts(dateTime!!)
}
private fun timeSet(hourOfDay: Int, minute: Int) {
dateTime = dateTime?.withHourOfDay(hourOfDay)?.withMinuteOfHour(minute)
if (validateDateTime()) {
updateTexts(dateTime!!)
showPreview()
} else {
showTimePicker()
}
}
private fun validateDateTime(): Boolean {
return if (dateTime?.isAfterNow == false) {
activity.toast(R.string.must_pick_time_in_the_future)
false
} else {
true
}
}
private fun getNextHour() = calendar.get(Calendar.HOUR_OF_DAY) + 1
private fun getNextMinute() = (calendar.get(Calendar.MINUTE) + 5).round(5).coerceIn(0, 59)
}

View file

@ -26,7 +26,6 @@ import android.telephony.SubscriptionManager
import android.text.TextUtils
import androidx.core.app.NotificationCompat
import androidx.core.app.RemoteInput
import com.klinker.android.send_message.Settings
import com.klinker.android.send_message.Transaction.getAddressSeparator
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
@ -974,16 +973,6 @@ fun Context.getFileSizeFromUri(uri: Uri): Long {
}
}
fun Context.getSendMessageSettings(): Settings {
val settings = Settings()
settings.useSystemSending = true
settings.deliveryReports = config.enableDeliveryReports
settings.sendLongAsMms = config.sendLongMessageMMS
settings.sendLongAsMmsAfter = 1
settings.group = config.sendGroupMessageMMS
return settings
}
// fix a glitch at enabling Release version minifying from 5.12.3
// reset messages in 5.14.3 again, as PhoneNumber is no longer minified
fun Context.clearAllMessagesIfNeeded() {

View file

@ -1,8 +1,19 @@
package com.simplemobiletools.smsmessenger.extensions
import android.content.Context
import android.text.format.DateFormat
import android.text.format.DateUtils
import org.joda.time.DateTime
import java.util.*
fun Date.format(pattern: String): String {
return DateFormat.format(pattern, this).toString()
}
fun DateTime.humanize(context: Context, now: DateTime = DateTime.now(), pattern: String = "EEE, MMM dd, YYYY, hh:mm a"): String {
return if (yearOfCentury().get() > now.yearOfCentury().get()) {
toString(pattern)
} else {
DateUtils.getRelativeDateTimeString(context, millis, DateUtils.MINUTE_IN_MILLIS, DateUtils.DAY_IN_MILLIS, 0).toString()
}
}

View file

@ -0,0 +1,8 @@
package com.simplemobiletools.smsmessenger.extensions
import kotlin.math.roundToInt
/**
* Returns the closest next number divisible by [multipleOf].
*/
fun Int.round(multipleOf: Int = 1) = (toDouble() / multipleOf).roundToInt() * multipleOf

View file

@ -34,6 +34,8 @@ private const val PATH = "com.simplemobiletools.smsmessenger.action."
const val MARK_AS_READ = PATH + "mark_as_read"
const val REPLY = PATH + "reply"
const val DATE_FORMAT_PATTERN = "dd MMM, YYYY"
// view types for the thread list view
const val THREAD_DATE_TIME = 1
const val THREAD_RECEIVED_MESSAGE = 2

View file

@ -0,0 +1,59 @@
package com.simplemobiletools.smsmessenger.helpers
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.klinker.android.send_message.Settings
import com.klinker.android.send_message.Transaction
import com.klinker.android.send_message.Utils
import com.simplemobiletools.commons.extensions.showErrorToast
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.extensions.config
import com.simplemobiletools.smsmessenger.receivers.SmsStatusDeliveredReceiver
import com.simplemobiletools.smsmessenger.receivers.SmsStatusSentReceiver
fun Context.getSendMessageSettings(): Settings {
val settings = Settings()
settings.useSystemSending = true
settings.deliveryReports = config.enableDeliveryReports
settings.sendLongAsMms = config.sendLongMessageMMS
settings.sendLongAsMmsAfter = 1
settings.group = config.sendGroupMessageMMS
return settings
}
fun Context.sendTransactionMessage(text: String, addresses: List<String>, subscriptionId: Int?, attachments: List<Uri>) {
val settings = getSendMessageSettings()
if (subscriptionId != null) {
settings.subscriptionId = subscriptionId
}
val transaction = Transaction(this, settings)
val message = com.klinker.android.send_message.Message(text, addresses.toTypedArray())
if (attachments.isNotEmpty()) {
for (uri in attachments) {
try {
val byteArray = contentResolver.openInputStream(uri)?.readBytes() ?: continue
val mimeType = contentResolver.getType(uri) ?: continue
message.addMedia(byteArray, mimeType)
} catch (e: Exception) {
showErrorToast(e)
} catch (e: Error) {
showErrorToast(e.localizedMessage ?: getString(R.string.unknown_error_occurred))
}
}
}
val smsSentIntent = Intent(this, SmsStatusSentReceiver::class.java)
val deliveredIntent = Intent(this, SmsStatusDeliveredReceiver::class.java)
transaction.setExplicitBroadcastForSentSms(smsSentIntent)
transaction.setExplicitBroadcastForDeliveredSms(deliveredIntent)
transaction.sendNewMessage(message)
}
fun Context.isLongMmsMessage(text: String): Boolean {
val settings = getSendMessageSettings()
return Utils.getNumPages(settings, text) > settings.sendLongAsMmsAfter
}

View file

@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.telephony.SubscriptionManager
import androidx.core.app.RemoteInput
import com.klinker.android.send_message.Transaction
import com.simplemobiletools.commons.extensions.notificationManager
@ -14,6 +13,7 @@ import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.REPLY
import com.simplemobiletools.smsmessenger.helpers.THREAD_ID
import com.simplemobiletools.smsmessenger.helpers.THREAD_NUMBER
import com.simplemobiletools.smsmessenger.helpers.getSendMessageSettings
class DirectReplyReceiver : BroadcastReceiver() {
@SuppressLint("MissingPermission")

View file

@ -0,0 +1,11 @@
package com.simplemobiletools.smsmessenger.receivers
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
class ScheduledMessageReceiver: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
TODO("Not yet implemented")
}
}

View file

@ -4,8 +4,7 @@ import android.app.Service
import android.content.Intent
import android.net.Uri
import com.klinker.android.send_message.Transaction
import com.simplemobiletools.smsmessenger.extensions.getSendMessageSettings
import com.simplemobiletools.smsmessenger.extensions.getThreadId
import com.simplemobiletools.smsmessenger.helpers.getSendMessageSettings
import com.simplemobiletools.smsmessenger.receivers.SmsStatusDeliveredReceiver
import com.simplemobiletools.smsmessenger.receivers.SmsStatusSentReceiver