Merge pull request #438 from Naveen3Singh/feature_schedule_send

Add schedule send feature
This commit is contained in:
Tibor Kaputa 2022-10-14 23:29:49 +02:00 committed by GitHub
commit d41c57093e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1028 additions and 163 deletions

View File

@ -11,6 +11,7 @@
<uses-permission android:name="android.permission.RECEIVE_MMS" /> <uses-permission android:name="android.permission.RECEIVE_MMS" />
<uses-permission android:name="android.provider.Telephony.SMS_RECEIVED" /> <uses-permission android:name="android.provider.Telephony.SMS_RECEIVED" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <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.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
@ -212,6 +213,10 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver
android:name=".receivers.ScheduledMessageReceiver"
android:exported="false" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"

View File

@ -36,7 +36,6 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.OutputStream import java.io.OutputStream
import java.util.*
class MainActivity : SimpleActivity() { class MainActivity : SimpleActivity() {
private val MAKE_DEFAULT_APP_REQUEST = 1 private val MAKE_DEFAULT_APP_REQUEST = 1
@ -208,42 +207,62 @@ class MainActivity : SimpleActivity() {
setupConversations(conversations) setupConversations(conversations)
getNewConversations(conversations) getNewConversations(conversations)
} }
conversations.forEach {
clearExpiredScheduledMessages(it.threadId)
}
} }
} }
private fun getNewConversations(cachedConversations: ArrayList<Conversation>) { private fun getNewConversations(cachedConversations: ArrayList<Conversation>) {
val privateCursor = getMyContactsCursor(false, true) val privateCursor = getMyContactsCursor(favoritesOnly = false, withPhoneNumbersOnly = true)
ensureBackgroundThread { ensureBackgroundThread {
val privateContacts = MyContactsContentProvider.getSimpleContacts(this, privateCursor) val privateContacts = MyContactsContentProvider.getSimpleContacts(this, privateCursor)
val conversations = getConversations(privateContacts = privateContacts) val conversations = getConversations(privateContacts = privateContacts)
runOnUiThread {
setupConversations(conversations)
}
conversations.forEach { clonedConversation -> 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) conversationsDB.insertOrUpdate(clonedConversation)
cachedConversations.add(clonedConversation) cachedConversations.add(clonedConversation)
} }
} }
cachedConversations.forEach { cachedConversation -> cachedConversations.forEach { cachedConversation ->
if (!conversations.map { it.threadId }.contains(cachedConversation.threadId)) { val threadId = cachedConversation.threadId
conversationsDB.deleteThreadId(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 -> cachedConversations.forEach { cachedConv ->
val conv = conversations.firstOrNull { it.threadId == cachedConversation.threadId && it.toString() != cachedConversation.toString() } val conv = conversations.find { it.threadId == cachedConv.threadId && !cachedConv.areContentsTheSame(it) }
if (conv != null) { 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) { if (config.appRunCount == 1) {
conversations.map { it.threadId }.forEach { threadId -> conversations.map { it.threadId }.forEach { threadId ->
val messages = getMessages(threadId, false) val messages = getMessages(threadId, getImageResolutions = false, includeScheduledMessages = false)
messages.chunked(30).forEach { currentMessages -> messages.chunked(30).forEach { currentMessages ->
messagesDB.insertMessages(*currentMessages.toTypedArray()) messagesDB.insertMessages(*currentMessages.toTypedArray())
} }
@ -273,8 +292,9 @@ class MainActivity : SimpleActivity() {
hideKeyboard() hideKeyboard()
ConversationsAdapter(this, sortedConversations, conversations_list) { ConversationsAdapter(this, sortedConversations, conversations_list) {
Intent(this, ThreadActivity::class.java).apply { Intent(this, ThreadActivity::class.java).apply {
putExtra(THREAD_ID, (it as Conversation).threadId) val conversation = it as Conversation
putExtra(THREAD_TITLE, it.title) putExtra(THREAD_ID, conversation.threadId)
putExtra(THREAD_TITLE, conversation.title)
startActivity(this) startActivity(this)
} }
}.apply { }.apply {
@ -313,7 +333,7 @@ class MainActivity : SimpleActivity() {
val manager = getSystemService(ShortcutManager::class.java) val manager = getSystemService(ShortcutManager::class.java)
try { try {
manager.dynamicShortcuts = Arrays.asList(newConversation) manager.dynamicShortcuts = listOf(newConversation)
config.lastHandledShortcutColor = appIconColor config.lastHandledShortcutColor = appIconColor
} catch (ignored: Exception) { } catch (ignored: Exception) {
} }

View File

@ -13,10 +13,16 @@ import android.os.Bundle
import android.provider.ContactsContract import android.provider.ContactsContract
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.Telephony 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.SmsManager
import android.telephony.SmsMessage import android.telephony.SmsMessage
import android.telephony.SubscriptionInfo import android.telephony.SubscriptionInfo
import android.text.TextUtils 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.util.TypedValue
import android.view.Gravity import android.view.Gravity
import android.view.View import android.view.View
@ -25,6 +31,7 @@ import android.view.inputmethod.EditorInfo
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.LinearLayout.LayoutParams import android.widget.LinearLayout.LayoutParams
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.core.content.res.ResourcesCompat
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.DiskCacheStrategy 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.bumptech.glide.request.target.Target
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken 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.ConfirmationDialog
import com.simplemobiletools.commons.dialogs.RadioGroupDialog import com.simplemobiletools.commons.dialogs.RadioGroupDialog
import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.extensions.*
@ -50,17 +55,17 @@ import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.smsmessenger.R import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.adapters.AutoCompleteTextViewAdapter import com.simplemobiletools.smsmessenger.adapters.AutoCompleteTextViewAdapter
import com.simplemobiletools.smsmessenger.adapters.ThreadAdapter import com.simplemobiletools.smsmessenger.adapters.ThreadAdapter
import com.simplemobiletools.smsmessenger.dialogs.ScheduleSendDialog
import com.simplemobiletools.smsmessenger.extensions.* import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.* import com.simplemobiletools.smsmessenger.helpers.*
import com.simplemobiletools.smsmessenger.models.* 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.activity_thread.*
import kotlinx.android.synthetic.main.item_attachment.view.* import kotlinx.android.synthetic.main.item_attachment.view.*
import kotlinx.android.synthetic.main.item_selected_contact.view.* import kotlinx.android.synthetic.main.item_selected_contact.view.*
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import org.joda.time.DateTime
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
@ -74,6 +79,10 @@ class ThreadActivity : SimpleActivity() {
private val TYPE_TAKE_PHOTO = 12 private val TYPE_TAKE_PHOTO = 12
private val TYPE_CHOOSE_PHOTO = 13 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 threadId = 0L
private var currentSIMCardIndex = 0 private var currentSIMCardIndex = 0
private var isActivityVisible = false private var isActivityVisible = false
@ -92,6 +101,10 @@ class ThreadActivity : SimpleActivity() {
private var allMessagesFetched = false private var allMessagesFetched = false
private var oldestMessageDate = -1 private var oldestMessageDate = -1
private var isScheduledMessage: Boolean = false
private var scheduledMessage: Message? = null
private lateinit var scheduledDateTime: DateTime
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_thread) setContentView(R.layout.activity_thread)
@ -227,6 +240,8 @@ class ThreadActivity : SimpleActivity() {
} catch (e: Exception) { } catch (e: Exception) {
ArrayList() ArrayList()
} }
clearExpiredScheduledMessages(threadId, messages)
messages.removeAll { it.isScheduled && it.millis() < System.currentTimeMillis() }
messages.sortBy { it.date } messages.sortBy { it.date }
if (messages.size > MESSAGES_LIMIT) { if (messages.size > MESSAGES_LIMIT) {
@ -258,8 +273,8 @@ class ThreadActivity : SimpleActivity() {
val cachedMessagesCode = messages.clone().hashCode() val cachedMessagesCode = messages.clone().hashCode()
messages = getMessages(threadId, true) messages = getMessages(threadId, true)
val hasParticipantWithoutName = participants.any { val hasParticipantWithoutName = participants.any { contact ->
it.phoneNumbers.map { it.normalizedNumber }.contains(it.name) contact.phoneNumbers.map { it.normalizedNumber }.contains(contact.name)
} }
try { try {
@ -325,11 +340,13 @@ class ThreadActivity : SimpleActivity() {
val currAdapter = thread_messages_list.adapter val currAdapter = thread_messages_list.adapter
if (currAdapter == null) { if (currAdapter == null) {
ThreadAdapter(this, threadItems, thread_messages_list) { ThreadAdapter(
(it as? ThreadError)?.apply { activity = this,
thread_type_message.setText(it.messageText) messages = threadItems,
} recyclerView = thread_messages_list,
}.apply { itemClick = { handleItemClick(it) },
onThreadIdUpdate = { threadId = it }
).apply {
thread_messages_list.adapter = this 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() { private fun fetchNextMessages() {
if (messages.isEmpty() || allMessagesFetched || loadingOlderMessages) { if (messages.isEmpty() || allMessagesFetched || loadingOlderMessages) {
return return
@ -425,6 +449,12 @@ class ThreadActivity : SimpleActivity() {
thread_send_message.setOnClickListener { thread_send_message.setOnClickListener {
sendMessage() sendMessage()
} }
thread_send_message.setOnLongClickListener {
if (!isScheduledMessage) {
launchScheduleSendDialog()
}
true
}
thread_send_message.isClickable = false thread_send_message.isClickable = false
thread_type_message.onTextChangeListener { thread_type_message.onTextChangeListener {
@ -468,6 +498,8 @@ class ThreadActivity : SimpleActivity() {
addAttachment(it) addAttachment(it)
} }
} }
setupScheduleSendUi()
} }
private fun setupAttachmentSizes() { private fun setupAttachmentSizes() {
@ -580,8 +612,7 @@ class ThreadActivity : SimpleActivity() {
val defaultSmsSubscriptionId = SmsManager.getDefaultSmsSubscriptionId() val defaultSmsSubscriptionId = SmsManager.getDefaultSmsSubscriptionId()
val systemPreferredSimIdx = if (defaultSmsSubscriptionId >= 0) { val systemPreferredSimIdx = if (defaultSmsSubscriptionId >= 0) {
val defaultSmsSIM = subscriptionManagerCompat().getActiveSubscriptionInfo(defaultSmsSubscriptionId) availableSIMs.indexOfFirstOrNull { it.subscriptionId == defaultSmsSubscriptionId }
availableSIMs.indexOfFirstOrNull { it.subscriptionId == defaultSmsSIM.subscriptionId }
} else { } else {
null null
} }
@ -590,13 +621,7 @@ class ThreadActivity : SimpleActivity() {
} }
private fun blockNumber() { private fun blockNumber() {
val numbers = ArrayList<String>() val numbers = participants.getAddresses()
participants.forEach {
it.phoneNumbers.forEach {
numbers.add(it.normalizedNumber)
}
}
val numbersString = TextUtils.join(", ", numbers) val numbersString = TextUtils.join(", ", numbers)
val question = String.format(resources.getString(R.string.block_confirmation), numbersString) val question = String.format(resources.getString(R.string.block_confirmation), numbersString)
@ -909,9 +934,11 @@ class ThreadActivity : SimpleActivity() {
private fun checkSendMessageAvailability() { private fun checkSendMessageAvailability() {
if (thread_type_message.text!!.isNotEmpty() || (attachmentSelections.isNotEmpty() && !attachmentSelections.values.any { it.isPending })) { 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.isClickable = true
thread_send_message.alpha = 0.9f thread_send_message.alpha = 0.9f
} else { } else {
thread_send_message.isEnabled = false
thread_send_message.isClickable = false thread_send_message.isClickable = false
thread_send_message.alpha = 0.4f thread_send_message.alpha = 0.4f
} }
@ -919,57 +946,69 @@ class ThreadActivity : SimpleActivity() {
} }
private fun sendMessage() { private fun sendMessage() {
var msg = thread_type_message.value var text = thread_type_message.value
if (msg.isEmpty() && attachmentSelections.isEmpty()) { if (text.isEmpty() && attachmentSelections.isEmpty()) {
showErrorToast(getString(R.string.unknown_error_occurred)) showErrorToast(getString(R.string.unknown_error_occurred))
return return
} }
msg = removeDiacriticsIfNeeded(msg) text = removeDiacriticsIfNeeded(text)
val numbers = ArrayList<String>() val subscriptionId = availableSIMCards.getOrNull(currentSIMCardIndex)?.subscriptionId ?: SmsManager.getDefaultSmsSubscriptionId()
participants.forEach { contact ->
contact.phoneNumbers.forEach { if (isScheduledMessage) {
numbers.add(it.normalizedNumber) sendScheduledMessage(text, subscriptionId)
} else {
sendNormalMessage(text, subscriptionId)
} }
} }
val settings = getSendMessageSettings() private fun sendScheduledMessage(text: String, subscriptionId: Int) {
val currentSubscriptionId = availableSIMCards.getOrNull(currentSIMCardIndex)?.subscriptionId if (scheduledDateTime.millis < System.currentTimeMillis() + 1000L) {
if (currentSubscriptionId != null) { toast(R.string.must_pick_time_in_the_future)
settings.subscriptionId = currentSubscriptionId launchScheduleSendDialog(scheduledDateTime)
return
} }
val transaction = Transaction(this, settings) refreshedSinceSent = false
val message = com.klinker.android.send_message.Message(msg, numbers.toTypedArray())
if (attachmentSelections.isNotEmpty()) {
for (selection in attachmentSelections.values) {
try { try {
val byteArray = contentResolver.openInputStream(selection.uri)?.readBytes() ?: continue ensureBackgroundThread {
val mimeType = contentResolver.getType(selection.uri) ?: continue val messageId = scheduledMessage?.id ?: generateRandomId()
message.addMedia(byteArray, mimeType) 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) { } catch (e: Exception) {
showErrorToast(e)
} catch (e: Error) {
showErrorToast(e.localizedMessage ?: getString(R.string.unknown_error_occurred)) 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 { try {
val smsSentIntent = Intent(this, SmsStatusSentReceiver::class.java)
val deliveredIntent = Intent(this, SmsStatusDeliveredReceiver::class.java)
transaction.setExplicitBroadcastForSentSms(smsSentIntent)
transaction.setExplicitBroadcastForDeliveredSms(deliveredIntent)
refreshedSinceSent = false refreshedSinceSent = false
transaction.sendNewMessage(message) sendMessage(text, addresses, subscriptionId, attachments)
thread_type_message.setText("") clearCurrentMessage()
attachmentSelections.clear()
thread_attachments_holder.beGone()
thread_attachments_wrapper.removeAllViews()
if (!refreshedSinceSent) { if (!refreshedSinceSent) {
refreshMessages() 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 // show selected contacts, properly split to new lines when appropriate
// based on https://stackoverflow.com/a/13505029/1967672 // based on https://stackoverflow.com/a/13505029/1967672
private fun showSelectedContact(views: ArrayList<View>) { private fun showSelectedContact(views: ArrayList<View>) {
@ -997,16 +1043,16 @@ class ThreadActivity : SimpleActivity() {
var isFirstRow = true var isFirstRow = true
for (i in views.indices) { for (i in views.indices) {
val LL = LinearLayout(this) val layout = LinearLayout(this)
LL.orientation = LinearLayout.HORIZONTAL layout.orientation = LinearLayout.HORIZONTAL
LL.gravity = Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM layout.gravity = Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM
LL.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) layout.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
views[i].measure(0, 0) views[i].measure(0, 0)
var params = LayoutParams(views[i].measuredWidth, LayoutParams.WRAP_CONTENT) var params = LayoutParams(views[i].measuredWidth, LayoutParams.WRAP_CONTENT)
params.setMargins(0, 0, mediumMargin, 0) params.setMargins(0, 0, mediumMargin, 0)
LL.addView(views[i], params) layout.addView(views[i], params)
LL.measure(0, 0) layout.measure(0, 0)
widthSoFar += views[i].measuredWidth + mediumMargin widthSoFar += views[i].measuredWidth + mediumMargin
val checkWidth = if (isFirstRow) firstRowWidth else parentWidth val checkWidth = if (isFirstRow) firstRowWidth else parentWidth
@ -1016,15 +1062,15 @@ class ThreadActivity : SimpleActivity() {
newLinearLayout = LinearLayout(this) newLinearLayout = LinearLayout(this)
newLinearLayout.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) newLinearLayout.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
newLinearLayout.orientation = LinearLayout.HORIZONTAL newLinearLayout.orientation = LinearLayout.HORIZONTAL
params = LayoutParams(LL.measuredWidth, LL.measuredHeight) params = LayoutParams(layout.measuredWidth, layout.measuredHeight)
params.topMargin = mediumMargin params.topMargin = mediumMargin
newLinearLayout.addView(LL, params) newLinearLayout.addView(layout, params)
widthSoFar = LL.measuredWidth widthSoFar = layout.measuredWidth
} else { } else {
if (!isFirstRow) { if (!isFirstRow) {
(LL.layoutParams as LayoutParams).topMargin = mediumMargin (layout.layoutParams as LayoutParams).topMargin = mediumMargin
} }
newLinearLayout.addView(LL) newLinearLayout.addView(layout)
} }
} }
selected_contacts.addView(newLinearLayout) selected_contacts.addView(newLinearLayout)
@ -1125,16 +1171,26 @@ class ThreadActivity : SimpleActivity() {
notificationManager.cancel(threadId.hashCode()) notificationManager.cancel(threadId.hashCode())
} }
val lastMaxId = messages.maxByOrNull { it.id }?.id ?: 0L val newThreadId = getThreadId(participants.getAddresses().toSet())
messages = getMessages(threadId, true) 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 -> 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 // subscriptionIds seem to be not filled out at sending with multiple SIM cards, so fill it manually
if ((subscriptionManagerCompat().activeSubscriptionInfoList?.size ?: 0) > 1) { if ((subscriptionManagerCompat().activeSubscriptionInfoList?.size ?: 0) > 1) {
val SIMId = availableSIMCards.getOrNull(currentSIMCardIndex)?.subscriptionId val subscriptionId = availableSIMCards.getOrNull(currentSIMCardIndex)?.subscriptionId
if (SIMId != null) { if (subscriptionId != null) {
updateMessageSubscriptionId(latestMessage.id, SIMId) updateMessageSubscriptionId(latestMessage.id, subscriptionId)
latestMessage.subscriptionId = SIMId latestMessage.subscriptionId = subscriptionId
} }
} }
@ -1145,12 +1201,15 @@ class ThreadActivity : SimpleActivity() {
setupSIMSelector() setupSIMSelector()
} }
private fun updateMessageType() { private fun isMmsMessage(text: String): Boolean {
val settings = getSendMessageSettings()
val text = thread_type_message.text.toString()
val isGroupMms = participants.size > 1 && config.sendGroupMessageMMS 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) { return attachmentSelections.isNotEmpty() || isGroupMms || isLongMmsMessage
}
private fun updateMessageType() {
val text = thread_type_message.text.toString()
val stringId = if (isMmsMessage(text)) {
R.string.mms R.string.mms
} else { } else {
R.string.sms R.string.sms
@ -1166,4 +1225,143 @@ class ThreadActivity : SimpleActivity() {
} }
return File.createTempFile("IMG_", ".jpg", outputDirectory) 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)
}
} }

View File

@ -173,7 +173,7 @@ class ConversationsAdapter(
} }
try { try {
conversations.removeAll(conversationsToRemove) conversations.removeAll(conversationsToRemove.toSet())
} catch (ignored: Exception) { } catch (ignored: Exception) {
} }
@ -319,15 +319,16 @@ class ConversationsAdapter(
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 0.8f) setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 0.8f)
} }
if (conversation.read) { val style = if (conversation.read) {
conversation_address.setTypeface(null, Typeface.NORMAL)
conversation_body_short.setTypeface(null, Typeface.NORMAL)
conversation_body_short.alpha = 0.7f conversation_body_short.alpha = 0.7f
if (conversation.isScheduled) Typeface.ITALIC else Typeface.NORMAL
} else { } else {
conversation_address.setTypeface(null, Typeface.BOLD)
conversation_body_short.setTypeface(null, Typeface.BOLD)
conversation_body_short.alpha = 1f 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 { arrayListOf<TextView>(conversation_address, conversation_body_short, conversation_date).forEach {
it.setTextColor(textColor) it.setTextColor(textColor)

View File

@ -4,10 +4,10 @@ import android.annotation.SuppressLint
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.telephony.SubscriptionManager
import android.util.TypedValue import android.util.TypedValue
import android.view.Menu import android.view.Menu
import android.view.View 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_image.view.*
import kotlinx.android.synthetic.main.item_attachment_vcard.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.*
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_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_sent_unknown_attachment.view.*
import kotlinx.android.synthetic.main.item_thread_date_time.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_error.view.*
import kotlinx.android.synthetic.main.item_thread_sending.view.* import kotlinx.android.synthetic.main.item_thread_sending.view.*
import kotlinx.android.synthetic.main.item_thread_success.view.* import kotlinx.android.synthetic.main.item_thread_success.view.*
import java.util.*
class ThreadAdapter( 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) { ) : MyRecyclerViewAdapter(activity, recyclerView, itemClick) {
private var fontSize = activity.getTextSize() private var fontSize = activity.getTextSize()
@ -199,11 +205,21 @@ class ThreadAdapter(
messagesToRemove.forEach { messagesToRemove.forEach {
activity.deleteMessage((it as Message).id, it.isMMS) activity.deleteMessage((it as Message).id, it.isMMS)
} }
messages.removeAll(messagesToRemove) messages.removeAll(messagesToRemove.toSet())
activity.updateLastConversationMessage(threadId) 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 { activity.runOnUiThread {
if (messages.filter { it is Message }.isEmpty()) { if (messages.isEmpty()) {
activity.finish() activity.finish()
} else { } else {
removeSelectedItems(positions) removeSelectedItems(positions)
@ -252,29 +268,9 @@ class ThreadAdapter(
thread_message_body.beVisibleIf(message.body.isNotEmpty()) thread_message_body.beVisibleIf(message.body.isNotEmpty())
if (message.isReceivedMessage()) { if (message.isReceivedMessage()) {
thread_message_sender_photo.beVisible() setupReceivedMessageView(view, message)
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)
}
} else { } else {
thread_message_sender_photo?.beGone() setupSentMessageView(view, message)
val background = context.getProperPrimaryColor()
thread_message_body.background.applyColorFilter(background)
val contrastColor = background.getContrastColor()
thread_message_body.setTextColor(contrastColor)
thread_message_body.setLinkTextColor(contrastColor)
} }
thread_message_body.setOnLongClickListener { 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) { private fun setupImageView(holder: ViewHolder, parent: View, message: Message, attachment: Attachment) {
val mimetype = attachment.mimetype val mimetype = attachment.mimetype
val uri = attachment.getUri() val uri = attachment.getUri()
@ -461,7 +505,7 @@ class ThreadAdapter(
private fun launchViewIntent(uri: Uri, mimetype: String, filename: String) { private fun launchViewIntent(uri: Uri, mimetype: String, filename: String) {
Intent().apply { Intent().apply {
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
setDataAndType(uri, mimetype.toLowerCase()) setDataAndType(uri, mimetype.lowercase(Locale.getDefault()))
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
try { try {

View File

@ -17,7 +17,7 @@ import com.simplemobiletools.smsmessenger.models.Conversation
import com.simplemobiletools.smsmessenger.models.Message import com.simplemobiletools.smsmessenger.models.Message
import com.simplemobiletools.smsmessenger.models.MessageAttachment 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) @TypeConverters(Converters::class)
abstract class MessagesDatabase : RoomDatabase() { abstract class MessagesDatabase : RoomDatabase() {
@ -41,6 +41,7 @@ abstract class MessagesDatabase : RoomDatabase() {
.addMigrations(MIGRATION_1_2) .addMigrations(MIGRATION_1_2)
.addMigrations(MIGRATION_2_3) .addMigrations(MIGRATION_2_3)
.addMigrations(MIGRATION_3_4) .addMigrations(MIGRATION_3_4)
.addMigrations(MIGRATION_4_5)
.build() .build()
} }
} }
@ -66,8 +67,10 @@ abstract class MessagesDatabase : RoomDatabase() {
database.apply { 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("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) " + execSQL(
"SELECT thread_id, snippet, date, read, title, photo_uri, is_group_conversation, phone_number FROM conversations") "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") 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")
}
}
}
} }
} }

View File

@ -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)
}

View File

@ -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())

View File

@ -30,3 +30,5 @@ fun Map<String, Any>.toContentValues(): ContentValues {
return contentValues return contentValues
} }
fun <T> Collection<T>.toArrayList() = ArrayList(this)

View File

@ -26,7 +26,6 @@ import android.telephony.SubscriptionManager
import android.text.TextUtils import android.text.TextUtils
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.RemoteInput import androidx.core.app.RemoteInput
import com.klinker.android.send_message.Settings
import com.klinker.android.send_message.Transaction.getAddressSeparator import com.klinker.android.send_message.Transaction.getAddressSeparator
import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.* import com.simplemobiletools.commons.helpers.*
@ -58,7 +57,7 @@ val Context.messageAttachmentsDB: MessageAttachmentsDao get() = getMessagesDB().
val Context.messagesDB: MessagesDao get() = getMessagesDB().MessagesDao() 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 uri = Sms.CONTENT_URI
val projection = arrayOf( val projection = arrayOf(
Sms._ID, Sms._ID,
@ -117,8 +116,21 @@ fun Context.getMessages(threadId: Long, getImageResolutions: Boolean, dateFrom:
} }
messages.addAll(getMMS(threadId, getImageResolutions, sortOrder)) 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 return messages
} }
@ -570,7 +582,11 @@ fun Context.deleteConversation(threadId: Long) {
} }
uri = Mms.CONTENT_URI uri = Mms.CONTENT_URI
try {
contentResolver.delete(uri, selection, selectionArgs) contentResolver.delete(uri, selection, selectionArgs)
} catch (e: Exception) {
e.printStackTrace()
}
conversationsDB.deleteThreadId(threadId) conversationsDB.deleteThreadId(threadId)
messagesDB.deleteThreadMessages(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) { fun Context.markMessageRead(id: Long, isMMS: Boolean) {
val uri = if (isMMS) Mms.CONTENT_URI else Sms.CONTENT_URI val uri = if (isMMS) Mms.CONTENT_URI else Sms.CONTENT_URI
val contentValues = ContentValues().apply { 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 // 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 // reset messages in 5.14.3 again, as PhoneNumber is no longer minified
fun Context.clearAllMessagesIfNeeded() { fun Context.clearAllMessagesIfNeeded() {
@ -1001,3 +1015,52 @@ fun Context.subscriptionManagerCompat(): SubscriptionManager {
SubscriptionManager.from(this) 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
}
}

View File

@ -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

View File

@ -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 }

View File

@ -4,7 +4,6 @@ import com.google.gson.*
import java.math.BigDecimal import java.math.BigDecimal
import java.math.BigInteger import java.math.BigInteger
val JsonElement.optString: String? val JsonElement.optString: String?
get() = safeConversion { asString } get() = safeConversion { asString }

View File

@ -29,11 +29,14 @@ const val IMPORT_SMS = "import_sms"
const val IMPORT_MMS = "import_mms" const val IMPORT_MMS = "import_mms"
const val WAS_DB_CLEARED = "was_db_cleared_2" const val WAS_DB_CLEARED = "was_db_cleared_2"
const val EXTRA_VCARD_URI = "vcard" const val EXTRA_VCARD_URI = "vcard"
const val SCHEDULED_MESSAGE_ID = "scheduled_message_id"
private const val PATH = "com.simplemobiletools.smsmessenger.action." private const val PATH = "com.simplemobiletools.smsmessenger.action."
const val MARK_AS_READ = PATH + "mark_as_read" const val MARK_AS_READ = PATH + "mark_as_read"
const val REPLY = PATH + "reply" const val REPLY = PATH + "reply"
const val DATE_FORMAT_PATTERN = "dd MMM, YYYY"
// view types for the thread list view // view types for the thread list view
const val THREAD_DATE_TIME = 1 const val THREAD_DATE_TIME = 1
const val THREAD_RECEIVED_MESSAGE = 2 const val THREAD_RECEIVED_MESSAGE = 2

View File

@ -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()
}

View File

@ -14,6 +14,9 @@ interface ConversationsDao {
@Query("SELECT * FROM conversations") @Query("SELECT * FROM conversations")
fun getAll(): List<Conversation> fun getAll(): List<Conversation>
@Query("SELECT * FROM conversations WHERE thread_id = :threadId")
fun getConversationWithThreadId(threadId: Long): Conversation?
@Query("SELECT * FROM conversations WHERE read = 0") @Query("SELECT * FROM conversations WHERE read = 0")
fun getUnreadConversations(): List<Conversation> fun getUnreadConversations(): List<Conversation>

View File

@ -23,6 +23,12 @@ interface MessagesDao {
@Query("SELECT * FROM messages WHERE thread_id = :threadId") @Query("SELECT * FROM messages WHERE thread_id = :threadId")
fun getThreadMessages(threadId: Long): List<Message> 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") @Query("SELECT * FROM messages WHERE body LIKE :text")
fun getMessagesWithText(text: String): List<Message> fun getMessagesWithText(text: String): List<Message>

View File

@ -14,5 +14,17 @@ data class Conversation(
@ColumnInfo(name = "title") var title: String, @ColumnInfo(name = "title") var title: String,
@ColumnInfo(name = "photo_uri") var photoUri: String, @ColumnInfo(name = "photo_uri") var photoUri: String,
@ColumnInfo(name = "is_group_conversation") var isGroupConversation: Boolean, @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
}
}

View File

@ -20,7 +20,11 @@ data class Message(
@ColumnInfo(name = "attachment") val attachment: MessageAttachment?, @ColumnInfo(name = "attachment") val attachment: MessageAttachment?,
@ColumnInfo(name = "sender_name") var senderName: String, @ColumnInfo(name = "sender_name") var senderName: String,
@ColumnInfo(name = "sender_photo_uri") val senderPhotoUri: 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 isReceivedMessage() = type == Telephony.Sms.MESSAGE_TYPE_INBOX
fun millis() = date * 1000L
} }

View File

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

View File

@ -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))
}
}
}

View File

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

View 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>

View 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>

View File

@ -117,9 +117,12 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false" android:clipToPadding="false"
android:overScrollMode="ifContentScrolls" android:overScrollMode="ifContentScrolls"
android:paddingBottom="@dimen/small_margin"
android:scrollbars="none" android:scrollbars="none"
app:layoutManager="com.simplemobiletools.commons.views.MyLinearLayoutManager" 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> </com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
@ -127,7 +130,7 @@
android:id="@+id/message_divider" android:id="@+id/message_divider"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1px" android:layout_height="1px"
android:layout_above="@+id/thread_attachments_holder" android:layout_above="@+id/scheduled_message_holder"
android:background="@color/divider_grey" android:background="@color/divider_grey"
android:importantForAccessibility="no" /> android:importantForAccessibility="no" />
@ -145,13 +148,57 @@
android:padding="@dimen/normal_margin" android:padding="@dimen/normal_margin"
android:src="@drawable/ic_plus_vector" /> 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 <HorizontalScrollView
android:id="@+id/thread_attachments_holder" android:id="@+id/thread_attachments_holder"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_above="@+id/thread_type_message" android:layout_above="@+id/thread_type_message"
android:layout_alignStart="@+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:layout_marginBottom="@dimen/small_margin"
android:overScrollMode="never" android:overScrollMode="never"
android:scrollbars="none" android:scrollbars="none"

View File

@ -48,4 +48,15 @@
android:textSize="@dimen/normal_text_size" android:textSize="@dimen/normal_text_size"
tools:text="Sent message" /> tools:text="Sent message" />
</RelativeLayout> </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> </androidx.constraintlayout.widget.ConstraintLayout>

View 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>

View File

@ -7,4 +7,7 @@
<dimen name="remove_attachment_size">24dp</dimen> <dimen name="remove_attachment_size">24dp</dimen>
<dimen name="pin_icon_size">15dp</dimen> <dimen name="pin_icon_size">15dp</dimen>
<dimen name="vcard_property_start_margin">64dp</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> </resources>