mirror of
https://github.com/SimpleMobileTools/Simple-SMS-Messenger.git
synced 2025-02-17 04:00:35 +01:00
Merge pull request #438 from Naveen3Singh/feature_schedule_send
Add schedule send feature
This commit is contained in:
commit
d41c57093e
@ -11,6 +11,7 @@
|
||||
<uses-permission android:name="android.permission.RECEIVE_MMS" />
|
||||
<uses-permission android:name="android.provider.Telephony.SMS_RECEIVED" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
@ -212,6 +213,10 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".receivers.ScheduledMessageReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
|
@ -36,7 +36,6 @@ import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import java.io.FileOutputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.*
|
||||
|
||||
class MainActivity : SimpleActivity() {
|
||||
private val MAKE_DEFAULT_APP_REQUEST = 1
|
||||
@ -208,42 +207,62 @@ class MainActivity : SimpleActivity() {
|
||||
setupConversations(conversations)
|
||||
getNewConversations(conversations)
|
||||
}
|
||||
conversations.forEach {
|
||||
clearExpiredScheduledMessages(it.threadId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNewConversations(cachedConversations: ArrayList<Conversation>) {
|
||||
val privateCursor = getMyContactsCursor(false, true)
|
||||
val privateCursor = getMyContactsCursor(favoritesOnly = false, withPhoneNumbersOnly = true)
|
||||
ensureBackgroundThread {
|
||||
val privateContacts = MyContactsContentProvider.getSimpleContacts(this, privateCursor)
|
||||
val conversations = getConversations(privateContacts = privateContacts)
|
||||
|
||||
runOnUiThread {
|
||||
setupConversations(conversations)
|
||||
}
|
||||
|
||||
conversations.forEach { clonedConversation ->
|
||||
if (!cachedConversations.map { it.threadId }.contains(clonedConversation.threadId)) {
|
||||
val threadIds = cachedConversations.map { it.threadId }
|
||||
if (!threadIds.contains(clonedConversation.threadId)) {
|
||||
conversationsDB.insertOrUpdate(clonedConversation)
|
||||
cachedConversations.add(clonedConversation)
|
||||
}
|
||||
}
|
||||
|
||||
cachedConversations.forEach { cachedConversation ->
|
||||
if (!conversations.map { it.threadId }.contains(cachedConversation.threadId)) {
|
||||
conversationsDB.deleteThreadId(cachedConversation.threadId)
|
||||
val threadId = cachedConversation.threadId
|
||||
|
||||
val isTemporaryThread = cachedConversation.isScheduled
|
||||
val isConversationDeleted = !conversations.map { it.threadId }.contains(threadId)
|
||||
if (isConversationDeleted && !isTemporaryThread) {
|
||||
conversationsDB.deleteThreadId(threadId)
|
||||
}
|
||||
|
||||
val newConversation = conversations.find { it.phoneNumber == cachedConversation.phoneNumber }
|
||||
if (isTemporaryThread && newConversation != null) {
|
||||
// delete the original temporary thread and move any scheduled messages to the new thread
|
||||
conversationsDB.deleteThreadId(threadId)
|
||||
messagesDB.getScheduledThreadMessages(threadId)
|
||||
.forEach { message ->
|
||||
messagesDB.insertOrUpdate(message.copy(threadId = newConversation.threadId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cachedConversations.forEach { cachedConversation ->
|
||||
val conv = conversations.firstOrNull { it.threadId == cachedConversation.threadId && it.toString() != cachedConversation.toString() }
|
||||
cachedConversations.forEach { cachedConv ->
|
||||
val conv = conversations.find { it.threadId == cachedConv.threadId && !cachedConv.areContentsTheSame(it) }
|
||||
if (conv != null) {
|
||||
conversationsDB.insertOrUpdate(conv)
|
||||
val conversation = conv.copy(date = maxOf(cachedConv.date, conv.date))
|
||||
conversationsDB.insertOrUpdate(conversation)
|
||||
}
|
||||
}
|
||||
|
||||
val allConversations = conversationsDB.getAll() as ArrayList<Conversation>
|
||||
runOnUiThread {
|
||||
setupConversations(allConversations)
|
||||
}
|
||||
|
||||
if (config.appRunCount == 1) {
|
||||
conversations.map { it.threadId }.forEach { threadId ->
|
||||
val messages = getMessages(threadId, false)
|
||||
val messages = getMessages(threadId, getImageResolutions = false, includeScheduledMessages = false)
|
||||
messages.chunked(30).forEach { currentMessages ->
|
||||
messagesDB.insertMessages(*currentMessages.toTypedArray())
|
||||
}
|
||||
@ -273,8 +292,9 @@ class MainActivity : SimpleActivity() {
|
||||
hideKeyboard()
|
||||
ConversationsAdapter(this, sortedConversations, conversations_list) {
|
||||
Intent(this, ThreadActivity::class.java).apply {
|
||||
putExtra(THREAD_ID, (it as Conversation).threadId)
|
||||
putExtra(THREAD_TITLE, it.title)
|
||||
val conversation = it as Conversation
|
||||
putExtra(THREAD_ID, conversation.threadId)
|
||||
putExtra(THREAD_TITLE, conversation.title)
|
||||
startActivity(this)
|
||||
}
|
||||
}.apply {
|
||||
@ -313,7 +333,7 @@ class MainActivity : SimpleActivity() {
|
||||
|
||||
val manager = getSystemService(ShortcutManager::class.java)
|
||||
try {
|
||||
manager.dynamicShortcuts = Arrays.asList(newConversation)
|
||||
manager.dynamicShortcuts = listOf(newConversation)
|
||||
config.lastHandledShortcutColor = appIconColor
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
|
@ -13,10 +13,16 @@ import android.os.Bundle
|
||||
import android.provider.ContactsContract
|
||||
import android.provider.MediaStore
|
||||
import android.provider.Telephony
|
||||
import android.provider.Telephony.Sms.MESSAGE_TYPE_QUEUED
|
||||
import android.provider.Telephony.Sms.STATUS_NONE
|
||||
import android.telephony.SmsManager
|
||||
import android.telephony.SmsMessage
|
||||
import android.telephony.SubscriptionInfo
|
||||
import android.text.TextUtils
|
||||
import android.text.format.DateUtils
|
||||
import android.text.format.DateUtils.FORMAT_NO_YEAR
|
||||
import android.text.format.DateUtils.FORMAT_SHOW_DATE
|
||||
import android.text.format.DateUtils.FORMAT_SHOW_TIME
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
@ -25,6 +31,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 +44,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 +55,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
|
||||
@ -74,6 +79,10 @@ class ThreadActivity : SimpleActivity() {
|
||||
private val TYPE_TAKE_PHOTO = 12
|
||||
private val TYPE_CHOOSE_PHOTO = 13
|
||||
|
||||
private val TYPE_EDIT = 14
|
||||
private val TYPE_SEND = 15
|
||||
private val TYPE_DELETE = 16
|
||||
|
||||
private var threadId = 0L
|
||||
private var currentSIMCardIndex = 0
|
||||
private var isActivityVisible = false
|
||||
@ -92,6 +101,10 @@ class ThreadActivity : SimpleActivity() {
|
||||
private var allMessagesFetched = false
|
||||
private var oldestMessageDate = -1
|
||||
|
||||
private var isScheduledMessage: Boolean = false
|
||||
private var scheduledMessage: Message? = null
|
||||
private lateinit var scheduledDateTime: DateTime
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_thread)
|
||||
@ -227,6 +240,8 @@ class ThreadActivity : SimpleActivity() {
|
||||
} catch (e: Exception) {
|
||||
ArrayList()
|
||||
}
|
||||
clearExpiredScheduledMessages(threadId, messages)
|
||||
messages.removeAll { it.isScheduled && it.millis() < System.currentTimeMillis() }
|
||||
|
||||
messages.sortBy { it.date }
|
||||
if (messages.size > MESSAGES_LIMIT) {
|
||||
@ -258,8 +273,8 @@ class ThreadActivity : SimpleActivity() {
|
||||
val cachedMessagesCode = messages.clone().hashCode()
|
||||
messages = getMessages(threadId, true)
|
||||
|
||||
val hasParticipantWithoutName = participants.any {
|
||||
it.phoneNumbers.map { it.normalizedNumber }.contains(it.name)
|
||||
val hasParticipantWithoutName = participants.any { contact ->
|
||||
contact.phoneNumbers.map { it.normalizedNumber }.contains(contact.name)
|
||||
}
|
||||
|
||||
try {
|
||||
@ -325,11 +340,13 @@ class ThreadActivity : SimpleActivity() {
|
||||
|
||||
val currAdapter = thread_messages_list.adapter
|
||||
if (currAdapter == null) {
|
||||
ThreadAdapter(this, threadItems, thread_messages_list) {
|
||||
(it as? ThreadError)?.apply {
|
||||
thread_type_message.setText(it.messageText)
|
||||
}
|
||||
}.apply {
|
||||
ThreadAdapter(
|
||||
activity = this,
|
||||
messages = threadItems,
|
||||
recyclerView = thread_messages_list,
|
||||
itemClick = { handleItemClick(it) },
|
||||
onThreadIdUpdate = { threadId = it }
|
||||
).apply {
|
||||
thread_messages_list.adapter = this
|
||||
}
|
||||
|
||||
@ -371,6 +388,13 @@ class ThreadActivity : SimpleActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleItemClick(any: Any) {
|
||||
when {
|
||||
any is Message && any.isScheduled -> showScheduledMessageInfo(any)
|
||||
any is ThreadError -> thread_type_message.setText(any.messageText)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchNextMessages() {
|
||||
if (messages.isEmpty() || allMessagesFetched || loadingOlderMessages) {
|
||||
return
|
||||
@ -425,6 +449,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 +498,8 @@ class ThreadActivity : SimpleActivity() {
|
||||
addAttachment(it)
|
||||
}
|
||||
}
|
||||
|
||||
setupScheduleSendUi()
|
||||
}
|
||||
|
||||
private fun setupAttachmentSizes() {
|
||||
@ -580,8 +612,7 @@ class ThreadActivity : SimpleActivity() {
|
||||
|
||||
val defaultSmsSubscriptionId = SmsManager.getDefaultSmsSubscriptionId()
|
||||
val systemPreferredSimIdx = if (defaultSmsSubscriptionId >= 0) {
|
||||
val defaultSmsSIM = subscriptionManagerCompat().getActiveSubscriptionInfo(defaultSmsSubscriptionId)
|
||||
availableSIMs.indexOfFirstOrNull { it.subscriptionId == defaultSmsSIM.subscriptionId }
|
||||
availableSIMs.indexOfFirstOrNull { it.subscriptionId == defaultSmsSubscriptionId }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@ -590,13 +621,7 @@ class ThreadActivity : SimpleActivity() {
|
||||
}
|
||||
|
||||
private fun blockNumber() {
|
||||
val numbers = ArrayList<String>()
|
||||
participants.forEach {
|
||||
it.phoneNumbers.forEach {
|
||||
numbers.add(it.normalizedNumber)
|
||||
}
|
||||
}
|
||||
|
||||
val numbers = participants.getAddresses()
|
||||
val numbersString = TextUtils.join(", ", numbers)
|
||||
val question = String.format(resources.getString(R.string.block_confirmation), numbersString)
|
||||
|
||||
@ -909,9 +934,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,57 +946,69 @@ 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 subscriptionId = availableSIMCards.getOrNull(currentSIMCardIndex)?.subscriptionId ?: SmsManager.getDefaultSmsSubscriptionId()
|
||||
|
||||
if (isScheduledMessage) {
|
||||
sendScheduledMessage(text, subscriptionId)
|
||||
} else {
|
||||
sendNormalMessage(text, subscriptionId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendScheduledMessage(text: String, subscriptionId: Int) {
|
||||
if (scheduledDateTime.millis < System.currentTimeMillis() + 1000L) {
|
||||
toast(R.string.must_pick_time_in_the_future)
|
||||
launchScheduleSendDialog(scheduledDateTime)
|
||||
return
|
||||
}
|
||||
|
||||
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))
|
||||
refreshedSinceSent = false
|
||||
try {
|
||||
ensureBackgroundThread {
|
||||
val messageId = scheduledMessage?.id ?: generateRandomId()
|
||||
val message = buildScheduledMessage(text, subscriptionId, messageId)
|
||||
if (messages.isEmpty()) {
|
||||
// create a temporary thread until a real message is sent
|
||||
threadId = message.threadId
|
||||
createTemporaryThread(message, message.threadId)
|
||||
}
|
||||
messagesDB.insertOrUpdate(message)
|
||||
val conversation = conversationsDB.getConversationWithThreadId(threadId)
|
||||
if (conversation != null) {
|
||||
val nowSeconds = (System.currentTimeMillis() / 1000).toInt()
|
||||
conversationsDB.insertOrUpdate(conversation.copy(date = nowSeconds))
|
||||
}
|
||||
scheduleMessage(message)
|
||||
}
|
||||
clearCurrentMessage()
|
||||
hideScheduleSendUi()
|
||||
scheduledMessage = null
|
||||
|
||||
if (!refreshedSinceSent) {
|
||||
refreshMessages()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
showErrorToast(e.localizedMessage ?: getString(R.string.unknown_error_occurred))
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendNormalMessage(text: String, subscriptionId: Int) {
|
||||
val addresses = participants.getAddresses()
|
||||
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)
|
||||
thread_type_message.setText("")
|
||||
attachmentSelections.clear()
|
||||
thread_attachments_holder.beGone()
|
||||
thread_attachments_wrapper.removeAllViews()
|
||||
sendMessage(text, addresses, subscriptionId, attachments)
|
||||
clearCurrentMessage()
|
||||
|
||||
if (!refreshedSinceSent) {
|
||||
refreshMessages()
|
||||
@ -981,6 +1020,13 @@ class ThreadActivity : SimpleActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearCurrentMessage() {
|
||||
thread_type_message.setText("")
|
||||
attachmentSelections.clear()
|
||||
thread_attachments_holder.beGone()
|
||||
thread_attachments_wrapper.removeAllViews()
|
||||
}
|
||||
|
||||
// show selected contacts, properly split to new lines when appropriate
|
||||
// based on https://stackoverflow.com/a/13505029/1967672
|
||||
private fun showSelectedContact(views: ArrayList<View>) {
|
||||
@ -997,16 +1043,16 @@ class ThreadActivity : SimpleActivity() {
|
||||
var isFirstRow = true
|
||||
|
||||
for (i in views.indices) {
|
||||
val LL = LinearLayout(this)
|
||||
LL.orientation = LinearLayout.HORIZONTAL
|
||||
LL.gravity = Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM
|
||||
LL.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
|
||||
val layout = LinearLayout(this)
|
||||
layout.orientation = LinearLayout.HORIZONTAL
|
||||
layout.gravity = Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM
|
||||
layout.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
|
||||
views[i].measure(0, 0)
|
||||
|
||||
var params = LayoutParams(views[i].measuredWidth, LayoutParams.WRAP_CONTENT)
|
||||
params.setMargins(0, 0, mediumMargin, 0)
|
||||
LL.addView(views[i], params)
|
||||
LL.measure(0, 0)
|
||||
layout.addView(views[i], params)
|
||||
layout.measure(0, 0)
|
||||
widthSoFar += views[i].measuredWidth + mediumMargin
|
||||
|
||||
val checkWidth = if (isFirstRow) firstRowWidth else parentWidth
|
||||
@ -1016,15 +1062,15 @@ class ThreadActivity : SimpleActivity() {
|
||||
newLinearLayout = LinearLayout(this)
|
||||
newLinearLayout.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
||||
newLinearLayout.orientation = LinearLayout.HORIZONTAL
|
||||
params = LayoutParams(LL.measuredWidth, LL.measuredHeight)
|
||||
params = LayoutParams(layout.measuredWidth, layout.measuredHeight)
|
||||
params.topMargin = mediumMargin
|
||||
newLinearLayout.addView(LL, params)
|
||||
widthSoFar = LL.measuredWidth
|
||||
newLinearLayout.addView(layout, params)
|
||||
widthSoFar = layout.measuredWidth
|
||||
} else {
|
||||
if (!isFirstRow) {
|
||||
(LL.layoutParams as LayoutParams).topMargin = mediumMargin
|
||||
(layout.layoutParams as LayoutParams).topMargin = mediumMargin
|
||||
}
|
||||
newLinearLayout.addView(LL)
|
||||
newLinearLayout.addView(layout)
|
||||
}
|
||||
}
|
||||
selected_contacts.addView(newLinearLayout)
|
||||
@ -1125,16 +1171,26 @@ class ThreadActivity : SimpleActivity() {
|
||||
notificationManager.cancel(threadId.hashCode())
|
||||
}
|
||||
|
||||
val lastMaxId = messages.maxByOrNull { it.id }?.id ?: 0L
|
||||
messages = getMessages(threadId, true)
|
||||
val newThreadId = getThreadId(participants.getAddresses().toSet())
|
||||
val newMessages = getMessages(newThreadId, false)
|
||||
messages = if (messages.all { it.isScheduled } && newMessages.isNotEmpty()) {
|
||||
threadId = newThreadId
|
||||
// update scheduled messages with real thread id
|
||||
updateScheduledMessagesThreadId(messages, newThreadId)
|
||||
getMessages(newThreadId, true)
|
||||
} else {
|
||||
getMessages(threadId, true)
|
||||
}
|
||||
|
||||
val lastMaxId = messages.filterNot { it.isScheduled }.maxByOrNull { it.id }?.id ?: 0L
|
||||
|
||||
messages.filter { !it.isReceivedMessage() && it.id > lastMaxId }.forEach { latestMessage ->
|
||||
// subscriptionIds seem to be not filled out at sending with multiple SIM cards, so fill it manually
|
||||
if ((subscriptionManagerCompat().activeSubscriptionInfoList?.size ?: 0) > 1) {
|
||||
val SIMId = availableSIMCards.getOrNull(currentSIMCardIndex)?.subscriptionId
|
||||
if (SIMId != null) {
|
||||
updateMessageSubscriptionId(latestMessage.id, SIMId)
|
||||
latestMessage.subscriptionId = SIMId
|
||||
val subscriptionId = availableSIMCards.getOrNull(currentSIMCardIndex)?.subscriptionId
|
||||
if (subscriptionId != null) {
|
||||
updateMessageSubscriptionId(latestMessage.id, subscriptionId)
|
||||
latestMessage.subscriptionId = subscriptionId
|
||||
}
|
||||
}
|
||||
|
||||
@ -1145,12 +1201,15 @@ class ThreadActivity : SimpleActivity() {
|
||||
setupSIMSelector()
|
||||
}
|
||||
|
||||
private fun updateMessageType() {
|
||||
val settings = getSendMessageSettings()
|
||||
val text = thread_type_message.text.toString()
|
||||
private fun isMmsMessage(text: String): Boolean {
|
||||
val isGroupMms = participants.size > 1 && config.sendGroupMessageMMS
|
||||
val isLongMmsMessage = getNumPages(settings, text) > settings.sendLongAsMmsAfter && config.sendLongMessageMMS
|
||||
val stringId = if (attachmentSelections.isNotEmpty() || isGroupMms || isLongMmsMessage) {
|
||||
val isLongMmsMessage = isLongMmsMessage(text) && config.sendLongMessageMMS
|
||||
return attachmentSelections.isNotEmpty() || isGroupMms || isLongMmsMessage
|
||||
}
|
||||
|
||||
private fun updateMessageType() {
|
||||
val text = thread_type_message.text.toString()
|
||||
val stringId = if (isMmsMessage(text)) {
|
||||
R.string.mms
|
||||
} else {
|
||||
R.string.sms
|
||||
@ -1166,4 +1225,143 @@ class ThreadActivity : SimpleActivity() {
|
||||
}
|
||||
return File.createTempFile("IMG_", ".jpg", outputDirectory)
|
||||
}
|
||||
|
||||
private fun showScheduledMessageInfo(message: Message) {
|
||||
val items = arrayListOf(
|
||||
RadioItem(TYPE_EDIT, getString(R.string.update_message)),
|
||||
RadioItem(TYPE_SEND, getString(R.string.send_now)),
|
||||
RadioItem(TYPE_DELETE, getString(R.string.delete))
|
||||
)
|
||||
RadioGroupDialog(activity = this, items = items, titleId = R.string.scheduled_message) {
|
||||
when (it as Int) {
|
||||
TYPE_DELETE -> cancelScheduledMessageAndRefresh(message.id)
|
||||
TYPE_EDIT -> editScheduledMessage(message)
|
||||
TYPE_SEND -> {
|
||||
extractAttachments(message)
|
||||
sendNormalMessage(message.body, message.subscriptionId)
|
||||
cancelScheduledMessageAndRefresh(message.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractAttachments(message: Message) {
|
||||
val messageAttachment = message.attachment
|
||||
if (messageAttachment != null) {
|
||||
for (attachment in messageAttachment.attachments) {
|
||||
addAttachment(attachment.getUri())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun editScheduledMessage(message: Message) {
|
||||
scheduledMessage = message
|
||||
clearCurrentMessage()
|
||||
thread_type_message.setText(message.body)
|
||||
extractAttachments(message)
|
||||
scheduledDateTime = DateTime(message.millis())
|
||||
showScheduleSendUi()
|
||||
}
|
||||
|
||||
private fun cancelScheduledMessageAndRefresh(messageId: Long) {
|
||||
ensureBackgroundThread {
|
||||
deleteScheduledMessage(messageId)
|
||||
cancelScheduleSendPendingIntent(messageId)
|
||||
refreshMessages()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
if (scheduledMessage != null) {
|
||||
cancelScheduledMessageAndRefresh(scheduledMessage!!.id)
|
||||
scheduledMessage = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showScheduleSendUi() {
|
||||
isScheduledMessage = true
|
||||
updateSendButtonDrawable()
|
||||
scheduled_message_holder.beVisible()
|
||||
|
||||
val dt = scheduledDateTime
|
||||
val millis = dt.millis
|
||||
scheduled_message_button.text = if (dt.yearOfCentury().get() > DateTime.now().yearOfCentury().get()) {
|
||||
millis.formatDate(this)
|
||||
} else {
|
||||
val flags = FORMAT_SHOW_TIME or FORMAT_SHOW_DATE or FORMAT_NO_YEAR
|
||||
DateUtils.formatDateTime(this, millis, flags)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideScheduleSendUi() {
|
||||
isScheduledMessage = false
|
||||
scheduled_message_holder.beGone()
|
||||
updateSendButtonDrawable()
|
||||
}
|
||||
|
||||
private fun updateSendButtonDrawable() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildScheduledMessage(text: String, subscriptionId: Int, messageId: Long): Message {
|
||||
val threadId = if (messages.isEmpty()) messageId else threadId
|
||||
return Message(
|
||||
id = messageId,
|
||||
body = text,
|
||||
type = MESSAGE_TYPE_QUEUED,
|
||||
status = STATUS_NONE,
|
||||
participants = participants,
|
||||
date = (scheduledDateTime.millis / 1000).toInt(),
|
||||
read = false,
|
||||
threadId = threadId,
|
||||
isMMS = isMmsMessage(text),
|
||||
attachment = buildMessageAttachment(text, messageId),
|
||||
senderName = "",
|
||||
senderPhotoUri = "",
|
||||
subscriptionId = subscriptionId,
|
||||
isScheduled = true
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildMessageAttachment(text: String, messageId: Long): MessageAttachment {
|
||||
val attachments = attachmentSelections.values
|
||||
.map { Attachment(null, messageId, it.uri.toString(), contentResolver.getType(it.uri) ?: "*/*", 0, 0, "") }
|
||||
.toArrayList()
|
||||
|
||||
return MessageAttachment(messageId, text, attachments)
|
||||
}
|
||||
}
|
||||
|
@ -173,7 +173,7 @@ class ConversationsAdapter(
|
||||
}
|
||||
|
||||
try {
|
||||
conversations.removeAll(conversationsToRemove)
|
||||
conversations.removeAll(conversationsToRemove.toSet())
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
|
||||
@ -319,15 +319,16 @@ class ConversationsAdapter(
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 0.8f)
|
||||
}
|
||||
|
||||
if (conversation.read) {
|
||||
conversation_address.setTypeface(null, Typeface.NORMAL)
|
||||
conversation_body_short.setTypeface(null, Typeface.NORMAL)
|
||||
val style = if (conversation.read) {
|
||||
conversation_body_short.alpha = 0.7f
|
||||
if (conversation.isScheduled) Typeface.ITALIC else Typeface.NORMAL
|
||||
} else {
|
||||
conversation_address.setTypeface(null, Typeface.BOLD)
|
||||
conversation_body_short.setTypeface(null, Typeface.BOLD)
|
||||
conversation_body_short.alpha = 1f
|
||||
if (conversation.isScheduled) Typeface.BOLD_ITALIC else Typeface.BOLD
|
||||
|
||||
}
|
||||
conversation_address.setTypeface(null, style)
|
||||
conversation_body_short.setTypeface(null, style)
|
||||
|
||||
arrayListOf<TextView>(conversation_address, conversation_body_short, conversation_date).forEach {
|
||||
it.setTextColor(textColor)
|
||||
|
@ -4,10 +4,10 @@ import android.annotation.SuppressLint
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.telephony.SubscriptionManager
|
||||
import android.util.TypedValue
|
||||
import android.view.Menu
|
||||
import android.view.View
|
||||
@ -40,15 +40,21 @@ import com.simplemobiletools.smsmessenger.models.*
|
||||
import kotlinx.android.synthetic.main.item_attachment_image.view.*
|
||||
import kotlinx.android.synthetic.main.item_attachment_vcard.view.*
|
||||
import kotlinx.android.synthetic.main.item_received_message.view.*
|
||||
import kotlinx.android.synthetic.main.item_received_message.view.thread_mesage_attachments_holder
|
||||
import kotlinx.android.synthetic.main.item_received_message.view.thread_message_body
|
||||
import kotlinx.android.synthetic.main.item_received_message.view.thread_message_holder
|
||||
import kotlinx.android.synthetic.main.item_received_message.view.thread_message_play_outline
|
||||
import kotlinx.android.synthetic.main.item_received_unknown_attachment.view.*
|
||||
import kotlinx.android.synthetic.main.item_sent_message.view.*
|
||||
import kotlinx.android.synthetic.main.item_sent_unknown_attachment.view.*
|
||||
import kotlinx.android.synthetic.main.item_thread_date_time.view.*
|
||||
import kotlinx.android.synthetic.main.item_thread_error.view.*
|
||||
import kotlinx.android.synthetic.main.item_thread_sending.view.*
|
||||
import kotlinx.android.synthetic.main.item_thread_success.view.*
|
||||
import java.util.*
|
||||
|
||||
class ThreadAdapter(
|
||||
activity: SimpleActivity, var messages: ArrayList<ThreadItem>, recyclerView: MyRecyclerView, itemClick: (Any) -> Unit
|
||||
activity: SimpleActivity, var messages: ArrayList<ThreadItem>, recyclerView: MyRecyclerView, itemClick: (Any) -> Unit, val onThreadIdUpdate: (Long) -> Unit
|
||||
) : MyRecyclerViewAdapter(activity, recyclerView, itemClick) {
|
||||
private var fontSize = activity.getTextSize()
|
||||
|
||||
@ -199,11 +205,21 @@ class ThreadAdapter(
|
||||
messagesToRemove.forEach {
|
||||
activity.deleteMessage((it as Message).id, it.isMMS)
|
||||
}
|
||||
messages.removeAll(messagesToRemove)
|
||||
messages.removeAll(messagesToRemove.toSet())
|
||||
activity.updateLastConversationMessage(threadId)
|
||||
|
||||
val messages = messages.filterIsInstance<Message>()
|
||||
if (messages.isNotEmpty() && messages.all { it.isScheduled }) {
|
||||
// move all scheduled messages to a temporary thread as there are no real messages left
|
||||
val message = messages.last()
|
||||
val newThreadId = generateRandomId()
|
||||
activity.createTemporaryThread(message, newThreadId)
|
||||
activity.updateScheduledMessagesThreadId(messages, newThreadId)
|
||||
onThreadIdUpdate(newThreadId)
|
||||
}
|
||||
|
||||
activity.runOnUiThread {
|
||||
if (messages.filter { it is Message }.isEmpty()) {
|
||||
if (messages.isEmpty()) {
|
||||
activity.finish()
|
||||
} else {
|
||||
removeSelectedItems(positions)
|
||||
@ -252,29 +268,9 @@ class ThreadAdapter(
|
||||
thread_message_body.beVisibleIf(message.body.isNotEmpty())
|
||||
|
||||
if (message.isReceivedMessage()) {
|
||||
thread_message_sender_photo.beVisible()
|
||||
thread_message_sender_photo.setOnClickListener {
|
||||
val contact = message.participants.first()
|
||||
context.getContactFromAddress(contact.phoneNumbers.first().normalizedNumber) {
|
||||
if (it != null) {
|
||||
(activity as ThreadActivity).startContactDetailsIntent(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
thread_message_body.setTextColor(textColor)
|
||||
thread_message_body.setLinkTextColor(context.getProperPrimaryColor())
|
||||
|
||||
if (!activity.isFinishing && !activity.isDestroyed) {
|
||||
SimpleContactsHelper(context).loadContactImage(message.senderPhotoUri, thread_message_sender_photo, message.senderName)
|
||||
}
|
||||
setupReceivedMessageView(view, message)
|
||||
} else {
|
||||
thread_message_sender_photo?.beGone()
|
||||
val background = context.getProperPrimaryColor()
|
||||
thread_message_body.background.applyColorFilter(background)
|
||||
|
||||
val contrastColor = background.getContrastColor()
|
||||
thread_message_body.setTextColor(contrastColor)
|
||||
thread_message_body.setLinkTextColor(contrastColor)
|
||||
setupSentMessageView(view, message)
|
||||
}
|
||||
|
||||
thread_message_body.setOnLongClickListener {
|
||||
@ -304,6 +300,54 @@ class ThreadAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupReceivedMessageView(view: View, message: Message) {
|
||||
view.apply {
|
||||
thread_message_sender_photo.beVisible()
|
||||
thread_message_sender_photo.setOnClickListener {
|
||||
val contact = message.participants.first()
|
||||
context.getContactFromAddress(contact.phoneNumbers.first().normalizedNumber) {
|
||||
if (it != null) {
|
||||
(activity as ThreadActivity).startContactDetailsIntent(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
thread_message_body.setTextColor(textColor)
|
||||
thread_message_body.setLinkTextColor(context.getProperPrimaryColor())
|
||||
|
||||
if (!activity.isFinishing && !activity.isDestroyed) {
|
||||
SimpleContactsHelper(context).loadContactImage(message.senderPhotoUri, thread_message_sender_photo, message.senderName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSentMessageView(view: View, message: Message) {
|
||||
view.apply {
|
||||
thread_message_sender_photo?.beGone()
|
||||
val background = context.getProperPrimaryColor()
|
||||
thread_message_body.background.applyColorFilter(background)
|
||||
|
||||
val contrastColor = background.getContrastColor()
|
||||
thread_message_body.setTextColor(contrastColor)
|
||||
thread_message_body.setLinkTextColor(contrastColor)
|
||||
|
||||
val padding = thread_message_body.paddingStart
|
||||
if (message.isScheduled) {
|
||||
thread_message_scheduled_icon.beVisible()
|
||||
thread_message_scheduled_icon.applyColorFilter(contrastColor)
|
||||
|
||||
val iconWidth = resources.getDimensionPixelSize(R.dimen.small_icon_size)
|
||||
val rightPadding = padding + iconWidth
|
||||
thread_message_body.setPadding(padding, padding, rightPadding, padding)
|
||||
thread_message_body.typeface = Typeface.create(Typeface.DEFAULT, Typeface.ITALIC)
|
||||
} else {
|
||||
thread_message_scheduled_icon.beGone()
|
||||
|
||||
thread_message_body.setPadding(padding, padding, padding, padding)
|
||||
thread_message_body.typeface = Typeface.DEFAULT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupImageView(holder: ViewHolder, parent: View, message: Message, attachment: Attachment) {
|
||||
val mimetype = attachment.mimetype
|
||||
val uri = attachment.getUri()
|
||||
@ -461,7 +505,7 @@ class ThreadAdapter(
|
||||
private fun launchViewIntent(uri: Uri, mimetype: String, filename: String) {
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
setDataAndType(uri, mimetype.toLowerCase())
|
||||
setDataAndType(uri, mimetype.lowercase(Locale.getDefault()))
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
||||
try {
|
||||
|
@ -17,7 +17,7 @@ import com.simplemobiletools.smsmessenger.models.Conversation
|
||||
import com.simplemobiletools.smsmessenger.models.Message
|
||||
import com.simplemobiletools.smsmessenger.models.MessageAttachment
|
||||
|
||||
@Database(entities = [Conversation::class, Attachment::class, MessageAttachment::class, Message::class], version = 4)
|
||||
@Database(entities = [Conversation::class, Attachment::class, MessageAttachment::class, Message::class], version = 5)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class MessagesDatabase : RoomDatabase() {
|
||||
|
||||
@ -41,6 +41,7 @@ abstract class MessagesDatabase : RoomDatabase() {
|
||||
.addMigrations(MIGRATION_1_2)
|
||||
.addMigrations(MIGRATION_2_3)
|
||||
.addMigrations(MIGRATION_3_4)
|
||||
.addMigrations(MIGRATION_4_5)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@ -66,8 +67,10 @@ abstract class MessagesDatabase : RoomDatabase() {
|
||||
database.apply {
|
||||
execSQL("CREATE TABLE conversations_new (`thread_id` INTEGER NOT NULL PRIMARY KEY, `snippet` TEXT NOT NULL, `date` INTEGER NOT NULL, `read` INTEGER NOT NULL, `title` TEXT NOT NULL, `photo_uri` TEXT NOT NULL, `is_group_conversation` INTEGER NOT NULL, `phone_number` TEXT NOT NULL)")
|
||||
|
||||
execSQL("INSERT OR IGNORE INTO conversations_new (thread_id, snippet, date, read, title, photo_uri, is_group_conversation, phone_number) " +
|
||||
"SELECT thread_id, snippet, date, read, title, photo_uri, is_group_conversation, phone_number FROM conversations")
|
||||
execSQL(
|
||||
"INSERT OR IGNORE INTO conversations_new (thread_id, snippet, date, read, title, photo_uri, is_group_conversation, phone_number) " +
|
||||
"SELECT thread_id, snippet, date, read, title, photo_uri, is_group_conversation, phone_number FROM conversations"
|
||||
)
|
||||
|
||||
execSQL("DROP TABLE conversations")
|
||||
|
||||
@ -85,5 +88,14 @@ abstract class MessagesDatabase : RoomDatabase() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_4_5 = object : Migration(4, 5) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.apply {
|
||||
execSQL("ALTER TABLE messages ADD COLUMN is_scheduled INTEGER NOT NULL DEFAULT 0")
|
||||
execSQL("ALTER TABLE conversations ADD COLUMN is_scheduled INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,155 @@
|
||||
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 androidx.appcompat.app.AlertDialog
|
||||
import com.simplemobiletools.commons.activities.BaseSimpleActivity
|
||||
import com.simplemobiletools.commons.extensions.*
|
||||
import com.simplemobiletools.smsmessenger.R
|
||||
import com.simplemobiletools.smsmessenger.extensions.config
|
||||
import com.simplemobiletools.smsmessenger.extensions.roundToClosestMultipleOf
|
||||
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 dateFormat = activity.config.dateFormat
|
||||
val timeFormat = activity.getTimeFormat()
|
||||
view.edit_date.text = dt.toString(dateFormat)
|
||||
view.edit_time.text = dt.toString(timeFormat)
|
||||
}
|
||||
|
||||
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).coerceIn(0, 23)
|
||||
|
||||
private fun getNextMinute() = (calendar.get(Calendar.MINUTE) + 5).roundToClosestMultipleOf(5).coerceIn(0, 59)
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
package com.simplemobiletools.smsmessenger.extensions
|
||||
|
||||
import android.text.TextUtils
|
||||
import com.simplemobiletools.commons.models.SimpleContact
|
||||
|
||||
fun ArrayList<SimpleContact>.getThreadTitle() = TextUtils.join(", ", map { it.name }.toTypedArray())
|
@ -30,3 +30,5 @@ fun Map<String, Any>.toContentValues(): ContentValues {
|
||||
|
||||
return contentValues
|
||||
}
|
||||
|
||||
fun <T> Collection<T>.toArrayList() = ArrayList(this)
|
||||
|
@ -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.*
|
||||
@ -58,7 +57,7 @@ val Context.messageAttachmentsDB: MessageAttachmentsDao get() = getMessagesDB().
|
||||
|
||||
val Context.messagesDB: MessagesDao get() = getMessagesDB().MessagesDao()
|
||||
|
||||
fun Context.getMessages(threadId: Long, getImageResolutions: Boolean, dateFrom: Int = -1): ArrayList<Message> {
|
||||
fun Context.getMessages(threadId: Long, getImageResolutions: Boolean, dateFrom: Int = -1, includeScheduledMessages: Boolean = true): ArrayList<Message> {
|
||||
val uri = Sms.CONTENT_URI
|
||||
val projection = arrayOf(
|
||||
Sms._ID,
|
||||
@ -117,8 +116,21 @@ fun Context.getMessages(threadId: Long, getImageResolutions: Boolean, dateFrom:
|
||||
}
|
||||
|
||||
messages.addAll(getMMS(threadId, getImageResolutions, sortOrder))
|
||||
messages = messages.filter { it.participants.isNotEmpty() }
|
||||
.sortedWith(compareBy<Message> { it.date }.thenBy { it.id }).toMutableList() as ArrayList<Message>
|
||||
|
||||
if (includeScheduledMessages) {
|
||||
try {
|
||||
val scheduledMessages = messagesDB.getScheduledThreadMessages(threadId)
|
||||
messages.addAll(scheduledMessages)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
messages = messages
|
||||
.filter { it.participants.isNotEmpty() }
|
||||
.filterNot { it.isScheduled && it.millis() < System.currentTimeMillis() }
|
||||
.sortedWith(compareBy<Message> { it.date }.thenBy { it.id })
|
||||
.toMutableList() as ArrayList<Message>
|
||||
|
||||
return messages
|
||||
}
|
||||
@ -570,7 +582,11 @@ fun Context.deleteConversation(threadId: Long) {
|
||||
}
|
||||
|
||||
uri = Mms.CONTENT_URI
|
||||
contentResolver.delete(uri, selection, selectionArgs)
|
||||
try {
|
||||
contentResolver.delete(uri, selection, selectionArgs)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
conversationsDB.deleteThreadId(threadId)
|
||||
messagesDB.deleteThreadMessages(threadId)
|
||||
@ -588,6 +604,14 @@ fun Context.deleteMessage(id: Long, isMMS: Boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.deleteScheduledMessage(messageId: Long) {
|
||||
try {
|
||||
messagesDB.delete(messageId)
|
||||
} catch (e: Exception) {
|
||||
showErrorToast(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.markMessageRead(id: Long, isMMS: Boolean) {
|
||||
val uri = if (isMMS) Mms.CONTENT_URI else Sms.CONTENT_URI
|
||||
val contentValues = ContentValues().apply {
|
||||
@ -972,16 +996,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() {
|
||||
@ -1001,3 +1015,52 @@ fun Context.subscriptionManagerCompat(): SubscriptionManager {
|
||||
SubscriptionManager.from(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.createTemporaryThread(message: Message, threadId: Long = generateRandomId()) {
|
||||
val simpleContactHelper = SimpleContactsHelper(this)
|
||||
val addresses = message.participants.getAddresses()
|
||||
val photoUri = if (addresses.size == 1) simpleContactHelper.getPhotoUriFromPhoneNumber(addresses.first()) else ""
|
||||
|
||||
val conversation = Conversation(
|
||||
threadId = threadId,
|
||||
snippet = message.body,
|
||||
date = message.date,
|
||||
read = true,
|
||||
title = message.participants.getThreadTitle(),
|
||||
photoUri = photoUri,
|
||||
isGroupConversation = addresses.size > 1,
|
||||
phoneNumber = addresses.first(),
|
||||
isScheduled = true
|
||||
)
|
||||
try {
|
||||
conversationsDB.insertOrUpdate(conversation)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.updateScheduledMessagesThreadId(messages: List<Message>, newThreadId: Long) {
|
||||
val scheduledMessages = messages.map { it.copy(threadId = newThreadId) }.toTypedArray()
|
||||
messagesDB.insertMessages(*scheduledMessages)
|
||||
}
|
||||
|
||||
fun Context.clearExpiredScheduledMessages(threadId: Long, messagesToDelete: List<Message>? = null) {
|
||||
val messages = messagesToDelete ?: messagesDB.getScheduledThreadMessages(threadId)
|
||||
val now = System.currentTimeMillis() + 500L
|
||||
|
||||
try {
|
||||
messages.filter { it.isScheduled && it.millis() < now }.forEach { msg ->
|
||||
messagesDB.delete(msg.id)
|
||||
}
|
||||
if (messages.filterNot { it.isScheduled && it.millis() < now }.isEmpty()) {
|
||||
// delete empty temporary thread
|
||||
val conversation = conversationsDB.getConversationWithThreadId(threadId)
|
||||
if (conversation != null && conversation.isScheduled) {
|
||||
conversationsDB.deleteThreadId(threadId)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
package com.simplemobiletools.smsmessenger.extensions
|
||||
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Returns the closest number divisible by [multipleOf].
|
||||
*/
|
||||
fun Int.roundToClosestMultipleOf(multipleOf: Int = 1) = (toDouble() / multipleOf).roundToInt() * multipleOf
|
@ -0,0 +1,8 @@
|
||||
package com.simplemobiletools.smsmessenger.extensions
|
||||
|
||||
import android.text.TextUtils
|
||||
import com.simplemobiletools.commons.models.SimpleContact
|
||||
|
||||
fun ArrayList<SimpleContact>.getThreadTitle(): String = TextUtils.join(", ", map { it.name }.toTypedArray()).orEmpty()
|
||||
|
||||
fun ArrayList<SimpleContact>.getAddresses() = flatMap { it.phoneNumbers }.map { it.normalizedNumber }
|
@ -4,7 +4,6 @@ import com.google.gson.*
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
|
||||
val JsonElement.optString: String?
|
||||
get() = safeConversion { asString }
|
||||
|
||||
|
@ -29,11 +29,14 @@ const val IMPORT_SMS = "import_sms"
|
||||
const val IMPORT_MMS = "import_mms"
|
||||
const val WAS_DB_CLEARED = "was_db_cleared_2"
|
||||
const val EXTRA_VCARD_URI = "vcard"
|
||||
const val SCHEDULED_MESSAGE_ID = "scheduled_message_id"
|
||||
|
||||
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
|
||||
|
@ -0,0 +1,111 @@
|
||||
package com.simplemobiletools.smsmessenger.helpers
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.core.app.AlarmManagerCompat
|
||||
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.commons.helpers.isMarshmallowPlus
|
||||
import com.simplemobiletools.smsmessenger.R
|
||||
import com.simplemobiletools.smsmessenger.extensions.config
|
||||
import com.simplemobiletools.smsmessenger.models.Message
|
||||
import com.simplemobiletools.smsmessenger.receivers.ScheduledMessageReceiver
|
||||
import com.simplemobiletools.smsmessenger.receivers.SmsStatusDeliveredReceiver
|
||||
import com.simplemobiletools.smsmessenger.receivers.SmsStatusSentReceiver
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
import kotlin.math.abs
|
||||
import kotlin.random.Random
|
||||
|
||||
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.sendMessage(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)
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
transaction.sendNewMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.getScheduleSendPendingIntent(message: Message): PendingIntent {
|
||||
val intent = Intent(this, ScheduledMessageReceiver::class.java)
|
||||
intent.putExtra(THREAD_ID, message.threadId)
|
||||
intent.putExtra(SCHEDULED_MESSAGE_ID, message.id)
|
||||
|
||||
var flags = PendingIntent.FLAG_UPDATE_CURRENT
|
||||
if (isMarshmallowPlus()) {
|
||||
flags = flags or PendingIntent.FLAG_IMMUTABLE
|
||||
}
|
||||
|
||||
return PendingIntent.getBroadcast(this, message.id.toInt(), intent, flags)
|
||||
}
|
||||
|
||||
fun Context.scheduleMessage(message: Message) {
|
||||
val pendingIntent = getScheduleSendPendingIntent(message)
|
||||
val triggerAtMillis = message.millis()
|
||||
|
||||
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
AlarmManagerCompat.setExactAndAllowWhileIdle(alarmManager, AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent)
|
||||
}
|
||||
|
||||
fun Context.cancelScheduleSendPendingIntent(messageId: Long) {
|
||||
val intent = Intent(this, ScheduledMessageReceiver::class.java)
|
||||
var flags = PendingIntent.FLAG_UPDATE_CURRENT
|
||||
if (isMarshmallowPlus()) {
|
||||
flags = flags or PendingIntent.FLAG_IMMUTABLE
|
||||
}
|
||||
|
||||
PendingIntent.getBroadcast(this, messageId.toInt(), intent, flags).cancel()
|
||||
}
|
||||
|
||||
fun Context.isLongMmsMessage(text: String): Boolean {
|
||||
val settings = getSendMessageSettings()
|
||||
return Utils.getNumPages(settings, text) > settings.sendLongAsMmsAfter
|
||||
}
|
||||
|
||||
/** Not to be used with real messages persisted in the telephony db. This is for internal use only (e.g. scheduled messages). */
|
||||
fun generateRandomId(length: Int = 9): Long {
|
||||
val millis = DateTime.now(DateTimeZone.UTC).millis
|
||||
val random = abs(Random(millis).nextLong())
|
||||
return random.toString().takeLast(length).toLong()
|
||||
}
|
@ -14,6 +14,9 @@ interface ConversationsDao {
|
||||
@Query("SELECT * FROM conversations")
|
||||
fun getAll(): List<Conversation>
|
||||
|
||||
@Query("SELECT * FROM conversations WHERE thread_id = :threadId")
|
||||
fun getConversationWithThreadId(threadId: Long): Conversation?
|
||||
|
||||
@Query("SELECT * FROM conversations WHERE read = 0")
|
||||
fun getUnreadConversations(): List<Conversation>
|
||||
|
||||
|
@ -23,6 +23,12 @@ interface MessagesDao {
|
||||
@Query("SELECT * FROM messages WHERE thread_id = :threadId")
|
||||
fun getThreadMessages(threadId: Long): List<Message>
|
||||
|
||||
@Query("SELECT * FROM messages WHERE thread_id = :threadId AND is_scheduled = 1")
|
||||
fun getScheduledThreadMessages(threadId: Long): List<Message>
|
||||
|
||||
@Query("SELECT * FROM messages WHERE thread_id = :threadId AND id = :messageId AND is_scheduled = 1")
|
||||
fun getScheduledMessageWithId(threadId: Long, messageId: Long): Message
|
||||
|
||||
@Query("SELECT * FROM messages WHERE body LIKE :text")
|
||||
fun getMessagesWithText(text: String): List<Message>
|
||||
|
||||
|
@ -14,5 +14,17 @@ data class Conversation(
|
||||
@ColumnInfo(name = "title") var title: String,
|
||||
@ColumnInfo(name = "photo_uri") var photoUri: String,
|
||||
@ColumnInfo(name = "is_group_conversation") var isGroupConversation: Boolean,
|
||||
@ColumnInfo(name = "phone_number") var phoneNumber: String
|
||||
)
|
||||
@ColumnInfo(name = "phone_number") var phoneNumber: String,
|
||||
@ColumnInfo(name = "is_scheduled") var isScheduled: Boolean = false
|
||||
) {
|
||||
|
||||
fun areContentsTheSame(other: Conversation): Boolean {
|
||||
return snippet == other.snippet &&
|
||||
date == other.date &&
|
||||
read == other.read &&
|
||||
title == other.title &&
|
||||
photoUri == other.photoUri &&
|
||||
isGroupConversation == other.isGroupConversation &&
|
||||
phoneNumber == other.phoneNumber
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,11 @@ data class Message(
|
||||
@ColumnInfo(name = "attachment") val attachment: MessageAttachment?,
|
||||
@ColumnInfo(name = "sender_name") var senderName: String,
|
||||
@ColumnInfo(name = "sender_photo_uri") val senderPhotoUri: String,
|
||||
@ColumnInfo(name = "subscription_id") var subscriptionId: Int) : ThreadItem() {
|
||||
@ColumnInfo(name = "subscription_id") var subscriptionId: Int,
|
||||
@ColumnInfo(name = "is_scheduled") var isScheduled: Boolean = false
|
||||
) : ThreadItem() {
|
||||
|
||||
fun isReceivedMessage() = type == Telephony.Sms.MESSAGE_TYPE_INBOX
|
||||
|
||||
fun millis() = date * 1000L
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -0,0 +1,58 @@
|
||||
package com.simplemobiletools.smsmessenger.receivers
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.PowerManager
|
||||
import com.simplemobiletools.commons.extensions.showErrorToast
|
||||
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
|
||||
import com.simplemobiletools.smsmessenger.R
|
||||
import com.simplemobiletools.smsmessenger.extensions.conversationsDB
|
||||
import com.simplemobiletools.smsmessenger.extensions.deleteScheduledMessage
|
||||
import com.simplemobiletools.smsmessenger.extensions.getAddresses
|
||||
import com.simplemobiletools.smsmessenger.extensions.messagesDB
|
||||
import com.simplemobiletools.smsmessenger.helpers.SCHEDULED_MESSAGE_ID
|
||||
import com.simplemobiletools.smsmessenger.helpers.THREAD_ID
|
||||
import com.simplemobiletools.smsmessenger.helpers.refreshMessages
|
||||
import com.simplemobiletools.smsmessenger.helpers.sendMessage
|
||||
|
||||
class ScheduledMessageReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
val wakelock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "simple.messenger:scheduled.message.receiver")
|
||||
wakelock.acquire(3000)
|
||||
|
||||
|
||||
ensureBackgroundThread {
|
||||
handleIntent(context, intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleIntent(context: Context, intent: Intent) {
|
||||
val threadId = intent.getLongExtra(THREAD_ID, 0L)
|
||||
val messageId = intent.getLongExtra(SCHEDULED_MESSAGE_ID, 0L)
|
||||
val message = try {
|
||||
context.messagesDB.getScheduledMessageWithId(threadId, messageId)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return
|
||||
}
|
||||
|
||||
val addresses = message.participants.getAddresses()
|
||||
val attachments = message.attachment?.attachments?.mapNotNull { it.getUri() } ?: emptyList()
|
||||
|
||||
try {
|
||||
context.sendMessage(message.body, addresses, message.subscriptionId, attachments)
|
||||
|
||||
// delete temporary conversation and message as it's already persisted to the telephony db now
|
||||
context.deleteScheduledMessage(messageId)
|
||||
context.conversationsDB.deleteThreadId(messageId)
|
||||
refreshMessages()
|
||||
} catch (e: Exception) {
|
||||
context.showErrorToast(e)
|
||||
} catch (e: Error) {
|
||||
context.showErrorToast(e.localizedMessage ?: context.getString(R.string.unknown_error_occurred))
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
|
10
app/src/main/res/drawable/ic_calendar_month_vector.xml
Normal file
10
app/src/main/res/drawable/ic_calendar_month_vector.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,4h-1V2h-2v2H8V2H6v2H5C3.89,4 3.01,4.9 3.01,6L3,20c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V6C21,4.9 20.1,4 19,4zM19,20H5V10h14V20zM9,14H7v-2h2V14zM13,14h-2v-2h2V14zM17,14h-2v-2h2V14zM9,18H7v-2h2V18zM13,18h-2v-2h2V18zM17,18h-2v-2h2V18z" />
|
||||
</vector>
|
11
app/src/main/res/drawable/ic_schedule_send_vector.xml
Normal file
11
app/src/main/res/drawable/ic_schedule_send_vector.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M16.5,12.5L15,12.5v4l3,2 0.75,-1.23 -2.25,-1.52L16.5,12.5zM16,9L2,3v7l9,2 -9,2v7l7.27,-3.11C10.09,20.83 12.79,23 16,23c3.86,0 7,-3.14 7,-7s-3.14,-7 -7,-7zM16,21c-2.75,0 -4.98,-2.22 -5,-4.97v-0.07c0.02,-2.74 2.25,-4.97 5,-4.97 2.76,0 5,2.24 5,5S18.76,21 16,21z" />
|
||||
</vector>
|
@ -117,9 +117,12 @@
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:overScrollMode="ifContentScrolls"
|
||||
android:paddingBottom="@dimen/small_margin"
|
||||
android:scrollbars="none"
|
||||
app:layoutManager="com.simplemobiletools.commons.views.MyLinearLayoutManager"
|
||||
app:stackFromEnd="true" />
|
||||
app:stackFromEnd="true"
|
||||
tools:itemCount="3"
|
||||
tools:listitem="@layout/item_sent_message" />
|
||||
|
||||
</com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
|
||||
|
||||
@ -127,7 +130,7 @@
|
||||
android:id="@+id/message_divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:layout_above="@+id/thread_attachments_holder"
|
||||
android:layout_above="@+id/scheduled_message_holder"
|
||||
android:background="@color/divider_grey"
|
||||
android:importantForAccessibility="no" />
|
||||
|
||||
@ -145,13 +148,57 @@
|
||||
android:padding="@dimen/normal_margin"
|
||||
android:src="@drawable/ic_plus_vector" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/scheduled_message_holder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_above="@+id/thread_attachments_holder"
|
||||
android:layout_alignStart="@id/thread_type_message"
|
||||
android:layout_marginTop="@dimen/medium_margin"
|
||||
android:layout_marginEnd="@dimen/medium_margin"
|
||||
android:layout_marginBottom="@dimen/small_margin"
|
||||
android:background="@drawable/section_holder_stroke"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.simplemobiletools.commons.views.MyTextView
|
||||
android:id="@+id/scheduled_message_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:drawableStart="@drawable/ic_clock_vector"
|
||||
android:drawablePadding="@dimen/normal_margin"
|
||||
android:focusable="true"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="@dimen/normal_icon_size"
|
||||
android:paddingStart="@dimen/normal_margin"
|
||||
android:paddingEnd="48dp"
|
||||
android:text="Tomorrow at 6PM GMT +05:30"
|
||||
android:textSize="@dimen/middle_text_size"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/discard_scheduled_message"
|
||||
android:layout_width="@dimen/normal_icon_size"
|
||||
android:layout_height="@dimen/normal_icon_size"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/cancel_schedule_send"
|
||||
android:padding="@dimen/normal_margin"
|
||||
android:src="@drawable/ic_cross_vector" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/thread_attachments_holder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_above="@+id/thread_type_message"
|
||||
android:layout_alignStart="@+id/thread_type_message"
|
||||
android:layout_marginTop="@dimen/normal_margin"
|
||||
android:layout_marginTop="@dimen/medium_margin"
|
||||
android:layout_marginBottom="@dimen/small_margin"
|
||||
android:overScrollMode="never"
|
||||
android:scrollbars="none"
|
||||
|
@ -48,4 +48,15 @@
|
||||
android:textSize="@dimen/normal_text_size"
|
||||
tools:text="Sent message" />
|
||||
</RelativeLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/thread_message_scheduled_icon"
|
||||
android:layout_width="@dimen/small_icon_size"
|
||||
android:layout_height="@dimen/small_icon_size"
|
||||
android:layout_margin="@dimen/tiny_margin"
|
||||
android:src="@drawable/ic_clock_vector"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:visibility="visible" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
78
app/src/main/res/layout/schedule_message_dialog.xml
Normal file
78
app/src/main/res/layout/schedule_message_dialog.xml
Normal file
@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/scheduled_message_dialog_holder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="@dimen/activity_margin">
|
||||
|
||||
<com.simplemobiletools.commons.views.MyTextView
|
||||
android:id="@+id/subtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/activity_margin"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:includeFontPadding="false"
|
||||
android:padding="@dimen/tiny_margin"
|
||||
android:text="@string/schedule_send_warning"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/dateIcon"
|
||||
android:layout_width="@dimen/date_time_icon_size"
|
||||
android:layout_height="@dimen/date_time_icon_size"
|
||||
android:layout_marginStart="@dimen/big_margin"
|
||||
android:layout_marginTop="@dimen/big_margin"
|
||||
android:src="@drawable/ic_calendar_month_vector"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/subtitle" />
|
||||
|
||||
<com.simplemobiletools.commons.views.MyTextView
|
||||
android:id="@+id/edit_date"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/medium_margin"
|
||||
android:layout_marginEnd="@dimen/activity_margin"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:padding="@dimen/small_margin"
|
||||
android:textSize="@dimen/date_time_text_size"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/dateIcon"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/dateIcon"
|
||||
app:layout_constraintTop_toTopOf="@+id/dateIcon"
|
||||
tools:text="25 sep, 2022" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/timeIcon"
|
||||
android:layout_width="@dimen/date_time_icon_size"
|
||||
android:layout_height="@dimen/date_time_icon_size"
|
||||
android:layout_marginStart="@dimen/big_margin"
|
||||
android:layout_marginTop="@dimen/big_margin"
|
||||
android:src="@drawable/ic_clock_vector"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/dateIcon" />
|
||||
|
||||
<com.simplemobiletools.commons.views.MyTextView
|
||||
android:id="@+id/edit_time"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/medium_margin"
|
||||
android:layout_marginEnd="@dimen/activity_margin"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:padding="@dimen/small_margin"
|
||||
android:textSize="@dimen/date_time_text_size"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/timeIcon"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/timeIcon"
|
||||
app:layout_constraintTop_toTopOf="@+id/timeIcon"
|
||||
tools:text="07:00 AM" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
@ -7,4 +7,7 @@
|
||||
<dimen name="remove_attachment_size">24dp</dimen>
|
||||
<dimen name="pin_icon_size">15dp</dimen>
|
||||
<dimen name="vcard_property_start_margin">64dp</dimen>
|
||||
<dimen name="date_time_icon_size">20dp</dimen>
|
||||
<dimen name="date_time_text_size">22sp</dimen>
|
||||
<dimen name="small_icon_size">20dp</dimen>
|
||||
</resources>
|
||||
|
Loading…
x
Reference in New Issue
Block a user