Files
Simple-SMS-Messenger/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt

477 lines
18 KiB
Kotlin

package com.simplemobiletools.smsmessenger.activities
import android.app.Activity
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.drawable.Drawable
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Bundle
import android.provider.Telephony
import android.text.TextUtils
import android.view.Gravity
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.LinearLayout
import android.widget.LinearLayout.LayoutParams
import android.widget.RelativeLayout
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.Target
import com.klinker.android.send_message.Settings
import com.klinker.android.send_message.Transaction
import com.simplemobiletools.commons.dialogs.ConfirmationDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.adapters.AutoCompleteTextViewAdapter
import com.simplemobiletools.smsmessenger.adapters.ThreadAdapter
import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.THREAD_ID
import com.simplemobiletools.smsmessenger.helpers.THREAD_TEXT
import com.simplemobiletools.smsmessenger.helpers.THREAD_TITLE
import com.simplemobiletools.smsmessenger.helpers.refreshMessages
import com.simplemobiletools.smsmessenger.models.*
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
class ThreadActivity : SimpleActivity() {
private val MIN_DATE_TIME_DIFF_SECS = 300
private val PICK_ATTACHMENT_INTENT = 1
private var threadId = 0
private var threadItems = ArrayList<ThreadItem>()
private var bus: EventBus? = null
private var participants = ArrayList<Contact>()
private var messages = ArrayList<Message>()
private var attachmentUris = LinkedHashSet<Uri>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_thread)
val extras = intent.extras
if (extras == null) {
toast(R.string.unknown_error_occurred)
finish()
return
}
threadId = intent.getIntExtra(THREAD_ID, 0)
intent.getStringExtra(THREAD_TITLE)?.let {
supportActionBar?.title = it
}
bus = EventBus.getDefault()
bus!!.register(this)
ensureBackgroundThread {
messages = getMessages(threadId)
participants = if (messages.isEmpty()) {
getThreadParticipants(threadId, null)
} else {
messages.first().participants
}
messages.filter { it.attachment != null }.forEach {
it.attachment!!.attachments.forEach {
try {
if (it.mimetype.startsWith("image/")) {
val fileOptions = BitmapFactory.Options()
fileOptions.inJustDecodeBounds = true
BitmapFactory.decodeStream(contentResolver.openInputStream(it.uri), null, fileOptions)
it.width = fileOptions.outWidth
it.height = fileOptions.outHeight
} else if (it.mimetype.startsWith("video/")) {
val metaRetriever = MediaMetadataRetriever()
metaRetriever.setDataSource(this, it.uri)
it.width = metaRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH).toInt()
it.height = metaRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT).toInt()
}
if (it.width < 0) {
it.width = 0
}
if (it.height < 0) {
it.height = 0
}
} catch (ignored: Exception) {
}
}
}
setupAdapter()
runOnUiThread {
supportActionBar?.title = participants.getThreadTitle()
}
}
setupButtons()
}
override fun onDestroy() {
super.onDestroy()
bus?.unregister(this)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_thread, menu)
menu.apply {
findItem(R.id.delete).isVisible = threadItems.isNotEmpty()
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (participants.isEmpty()) {
return true
}
when (item.itemId) {
R.id.block_number -> blockNumber()
R.id.delete -> askConfirmDelete()
R.id.manage_people -> managePeople()
else -> return super.onOptionsItemSelected(item)
}
return true
}
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
super.onActivityResult(requestCode, resultCode, resultData)
if (requestCode == PICK_ATTACHMENT_INTENT && resultCode == Activity.RESULT_OK && resultData != null && resultData.data != null) {
addAttachment(resultData.data!!)
}
}
private fun setupAdapter() {
threadItems = getThreadItems()
invalidateOptionsMenu()
runOnUiThread {
val adapter = ThreadAdapter(this, threadItems, thread_messages_list, thread_messages_fastscroller) {}
thread_messages_list.adapter = adapter
}
getAvailableContacts {
runOnUiThread {
val adapter = AutoCompleteTextViewAdapter(this, it)
add_contact_or_number.setAdapter(adapter)
add_contact_or_number.imeOptions = EditorInfo.IME_ACTION_NEXT
add_contact_or_number.setOnItemClickListener { _, _, position, _ ->
val currContacts = (add_contact_or_number.adapter as AutoCompleteTextViewAdapter).resultList
val selectedContact = currContacts[position]
addSelectedContact(selectedContact)
}
add_contact_or_number.onTextChangeListener {
confirm_inserted_number.beVisibleIf(it.length > 2)
}
}
}
confirm_inserted_number.setOnClickListener {
val number = add_contact_or_number.value
val contact = Contact(number.hashCode(), number, "", number)
addSelectedContact(contact)
}
}
private fun setupButtons() {
updateTextColors(thread_holder)
thread_send_message.applyColorFilter(config.textColor)
confirm_manage_contacts.applyColorFilter(config.textColor)
thread_add_attachment.applyColorFilter(config.textColor)
thread_send_message.setOnClickListener {
sendMessage()
}
thread_send_message.isClickable = false
thread_type_message.onTextChangeListener {
checkSendMessageAvailability()
}
confirm_manage_contacts.setOnClickListener {
hideKeyboard()
thread_add_contacts.beGone()
val numbers = participants.map { it.phoneNumber }.toSet()
val newThreadId = getThreadId(numbers).toInt()
if (threadId != newThreadId) {
Intent(this, ThreadActivity::class.java).apply {
putExtra(THREAD_ID, newThreadId)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(this)
}
}
}
thread_type_message.setText(intent.getStringExtra(THREAD_TEXT))
thread_add_attachment.setOnClickListener {
launchPickPhotoVideoIntent()
}
}
private fun blockNumber() {
val baseString = R.string.block_confirmation
val numbers = participants.map { it.phoneNumber }.toTypedArray()
val numbersString = TextUtils.join(", ", numbers)
val question = String.format(resources.getString(baseString), numbersString)
ConfirmationDialog(this, question) {
ensureBackgroundThread {
numbers.forEach {
addBlockedNumber(it)
}
refreshMessages()
finish()
}
}
}
private fun askConfirmDelete() {
ConfirmationDialog(this, getString(R.string.delete_whole_conversation_confirmation)) {
deleteConversation(threadId)
refreshMessages()
finish()
}
}
private fun managePeople() {
if (thread_add_contacts.isVisible()) {
hideKeyboard()
thread_add_contacts.beGone()
} else {
showSelectedContacts()
thread_add_contacts.beVisible()
add_contact_or_number.requestFocus()
showKeyboard(add_contact_or_number)
}
}
private fun showSelectedContacts() {
val views = ArrayList<View>()
participants.forEach {
val contact = it
layoutInflater.inflate(R.layout.item_selected_contact, null).apply {
selected_contact_name.text = contact.name
selected_contact_remove.setOnClickListener {
if (contact.id != participants.first().id) {
removeSelectedContact(contact.id)
}
}
views.add(this)
}
}
showSelectedContact(views)
}
private fun addSelectedContact(contact: Contact) {
add_contact_or_number.setText("")
if (participants.map { it.id }.contains(contact.id)) {
return
}
participants.add(contact)
showSelectedContacts()
}
private fun getThreadItems(): ArrayList<ThreadItem> {
messages.sortBy { it.date }
val items = ArrayList<ThreadItem>()
var prevDateTime = 0
var hadUnreadItems = false
messages.forEach {
// do not show the date/time above every message, only if the difference between the 2 messages is at least MIN_DATE_TIME_DIFF_SECS
if (it.date - prevDateTime > MIN_DATE_TIME_DIFF_SECS) {
items.add(ThreadDateTime(it.date))
prevDateTime = it.date
}
items.add(it)
if (it.type == Telephony.Sms.MESSAGE_TYPE_FAILED) {
items.add(ThreadError(it.id))
}
if (!it.read) {
hadUnreadItems = true
markMessageRead(it.id, it.isMMS)
}
}
if (hadUnreadItems) {
bus?.post(Events.RefreshMessages())
}
return items
}
private fun launchPickPhotoVideoIntent() {
val mimeTypes = arrayOf("image/*", "video/*")
Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
startActivityForResult(this, PICK_ATTACHMENT_INTENT)
}
}
private fun addAttachment(uri: Uri) {
if (attachmentUris.contains(uri)) {
return
}
attachmentUris.add(uri)
thread_attachments_holder.beVisible()
val attachmentView = layoutInflater.inflate(R.layout.item_attachment, null).apply {
thread_attachments_wrapper.addView(this)
thread_remove_attachment.setOnClickListener {
thread_attachments_wrapper.removeView(this)
attachmentUris.remove(uri)
if (attachmentUris.isEmpty()) {
thread_attachments_holder.beGone()
}
}
}
val roundedCornersRadius = resources.getDimension(R.dimen.medium_margin).toInt()
val options = RequestOptions()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.transform(CenterCrop(), RoundedCorners(roundedCornersRadius))
Glide.with(this)
.load(uri)
.transition(DrawableTransitionOptions.withCrossFade())
.apply(options)
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
attachmentView.thread_attachment_preview.beGone()
attachmentView.thread_remove_attachment.beGone()
showErrorToast(e?.localizedMessage ?: "")
return false
}
override fun onResourceReady(dr: Drawable?, a: Any?, t: Target<Drawable>?, d: DataSource?, i: Boolean): Boolean {
attachmentView.thread_attachment_preview.beVisible()
attachmentView.thread_remove_attachment.beVisible()
checkSendMessageAvailability()
return false
}
})
.into(attachmentView.thread_attachment_preview)
}
private fun checkSendMessageAvailability() {
if (thread_type_message.text.isNotEmpty() || attachmentUris.isNotEmpty()) {
thread_send_message.isClickable = true
thread_send_message.alpha = 0.9f
} else {
thread_send_message.isClickable = false
thread_send_message.alpha = 0.4f
}
}
private fun sendMessage() {
val msg = thread_type_message.value
if (msg.isEmpty() && attachmentUris.isEmpty()) {
return
}
val numbers = participants.map { it.phoneNumber }.toTypedArray()
val settings = Settings()
settings.useSystemSending = true
val transaction = Transaction(this, settings)
val message = com.klinker.android.send_message.Message(msg, numbers)
if (attachmentUris.isNotEmpty()) {
for (uri in attachmentUris) {
val byteArray = contentResolver.openInputStream(uri)?.readBytes() ?: continue
val mimeType = contentResolver.getType(uri) ?: continue
message.addMedia(byteArray, mimeType)
}
}
transaction.sendNewMessage(message, threadId.toLong())
thread_type_message.setText("")
attachmentUris.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>) {
selected_contacts.removeAllViews()
var newLinearLayout = LinearLayout(this)
newLinearLayout.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
newLinearLayout.orientation = LinearLayout.HORIZONTAL
val sideMargin = (selected_contacts.layoutParams as RelativeLayout.LayoutParams).leftMargin
val mediumMargin = resources.getDimension(R.dimen.medium_margin).toInt()
val parentWidth = realScreenSize.x - sideMargin * 2
val firstRowWidth = parentWidth - resources.getDimension(R.dimen.normal_icon_size).toInt() + sideMargin / 2
var widthSoFar = 0
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)
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)
widthSoFar += views[i].measuredWidth + mediumMargin
val checkWidth = if (isFirstRow) firstRowWidth else parentWidth
if (widthSoFar >= checkWidth) {
isFirstRow = false
selected_contacts.addView(newLinearLayout)
newLinearLayout = LinearLayout(this)
newLinearLayout.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
newLinearLayout.orientation = LinearLayout.HORIZONTAL
params = LayoutParams(LL.measuredWidth, LL.measuredHeight)
params.topMargin = mediumMargin
newLinearLayout.addView(LL, params)
widthSoFar = LL.measuredWidth
} else {
if (!isFirstRow) {
(LL.layoutParams as LayoutParams).topMargin = mediumMargin
}
newLinearLayout.addView(LL)
}
}
selected_contacts.addView(newLinearLayout)
}
private fun removeSelectedContact(id: Int) {
participants = participants.filter { it.id != id }.toMutableList() as ArrayList<Contact>
showSelectedContacts()
}
@Subscribe(threadMode = ThreadMode.ASYNC)
fun refreshMessages(event: Events.RefreshMessages) {
messages = getMessages(threadId)
setupAdapter()
}
}