package com.simplemobiletools.smsmessenger.activities import android.app.Activity import android.app.PendingIntent 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.MediaStore import android.provider.Telephony import android.telephony.SmsManager 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.* import com.simplemobiletools.smsmessenger.models.* import com.simplemobiletools.smsmessenger.receivers.SmsSentReceiver import kotlinx.android.synthetic.main.activity_thread.* 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() private var bus: EventBus? = null private var participants = ArrayList() private var messages = ArrayList() private var attachmentUris = ArrayList() 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.type.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.type.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) new_message_to.setAdapter(adapter) new_message_to.imeOptions = EditorInfo.IME_ACTION_NEXT new_message_to.setOnItemClickListener { _, _, position, _ -> val currContacts = (new_message_to.adapter as AutoCompleteTextViewAdapter).resultList val selectedContact = currContacts[position] addSelectedContact(selectedContact) } } } } private fun setupButtons() { thread_type_message.setColors(config.textColor, config.primaryColor, config.backgroundColor) 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() } thread_remove_attachment.setOnClickListener { } } 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() new_message_to.requestFocus() showKeyboard(new_message_to) } } private fun showSelectedContacts() { val views = ArrayList() 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) { new_message_to.setText("") if (participants.map { it.id }.contains(contact.id)) { return } participants.add(contact) showSelectedContacts() } private fun getThreadItems(): ArrayList { messages.sortBy { it.date } val items = ArrayList() 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) { attachmentUris.add(uri) 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 { override fun onLoadFailed(e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean): Boolean { thread_attachment_preview.beGone() thread_remove_attachment.beGone() showErrorToast(e?.localizedMessage ?: "") return false } override fun onResourceReady(dr: Drawable?, a: Any?, t: Target?, d: DataSource?, i: Boolean): Boolean { thread_attachment_preview.beVisible() thread_remove_attachment.beVisible() checkSendMessageAvailability() return false } }) .into(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()) { return } participants.forEach { if (attachmentUris.isEmpty()) { val intent = Intent(this, SmsSentReceiver::class.java).apply { putExtra(MESSAGE_BODY, msg) putExtra(MESSAGE_ADDRESS, it.phoneNumber) } val pendingIntent = PendingIntent.getBroadcast(this, threadId, intent, PendingIntent.FLAG_UPDATE_CURRENT) val smsManager = SmsManager.getDefault() smsManager.sendTextMessage(it.phoneNumber, null, msg, pendingIntent, null) } else { val settings = Settings() settings.useSystemSending = true val transaction = Transaction(this, settings) val message = com.klinker.android.send_message.Message(msg, it.phoneNumber) val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, attachmentUris.first()) message.setImage(bitmap) transaction.sendNewMessage(message, threadId.toLong()) } } thread_type_message.setText("") } // show selected contacts, properly split to new lines when appropriate // based on https://stackoverflow.com/a/13505029/1967672 private fun showSelectedContact(views: ArrayList) { 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 showSelectedContacts() } @Subscribe(threadMode = ThreadMode.ASYNC) fun refreshMessages(event: Events.RefreshMessages) { messages = getMessages(threadId) setupAdapter() } }