diff --git a/.editorconfig b/.editorconfig
index 1606d61f..c79c990f 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -17,7 +17,7 @@ insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 4
-continuation_indent_size = 8
+continuation_indent_size = 4
[*.xml]
continuation_indent_size = 4
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 00000000..9bad22a9
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+github: [tibbi]
+patreon: tiborkaputa
+custom: ["https://www.paypal.com/paypalme/simplemobiletools", "https://www.simplemobiletools.com/donate"]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e332694f..dd463d04 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,84 @@
Changelog
==========
+Version 5.9.0 *(2021-02-16)*
+----------------------------
+
+ * Added Search
+ * Added a White theme with special handling
+ * Some stability and translation improvements
+
+Version 5.8.3 *(2021-01-27)*
+----------------------------
+
+ * Adding some stability and translation improvements
+
+Version 5.8.2 *(2021-01-18)*
+----------------------------
+
+ * Fixed a glitch with inability to send messages in empty conversations
+ * Adding a settings item for quickly getting into notification settings
+ * Some stability and translation improvements
+
+Version 5.8.1 *(2021-01-11)*
+----------------------------
+
+ * Fixed a glitch with "Sending..." stuck at messages
+ * Allow selecting a phone numbers at contacts with multiple ones
+ * Some translation and stability improvements
+
+Version 5.8.0 *(2021-01-02)*
+----------------------------
+
+ * Improved delivery reports, show a notification at failed messages
+ * Allow resending failed messages with tapping them
+ * Added multiple dual-SIM related improvements
+ * Many other UX, stability, performance and translation improvements
+
+Version 5.7.0 *(2020-12-30)*
+----------------------------
+
+ * Prefetch all messages at first launch for better performance
+ * Require Simple Thank You for color customization
+ * Some stability, translation and UX improvements
+
+Version 5.6.2 *(2020-12-25)*
+----------------------------
+
+ * Fixed messages not being sent in some cases
+
+Version 5.6.1 *(2020-12-24)*
+----------------------------
+
+ * Fixing a crash at devices with multiple SIM cards
+
+Version 5.6.0 *(2020-12-23)*
+----------------------------
+
+ * Cache messages for better performance
+ * Add a scrollbar on the main screen
+ * Adding some stability and translation improvements
+
+Version 5.5.1 *(2020-12-06)*
+----------------------------
+
+ * Properly show private contact names at group conversations
+ * Fixed private contacts not being visible on Android 11+
+ * Some stability and translation improvements
+
+Version 5.5.0 *(2020-11-04)*
+----------------------------
+
+ * Allow dialing or copying selected conversation phone numbers
+ * Allow copying specific parts of messages into clipboard
+ * Adding an option to show character counter at outgoing messages
+ * Couple other UI, translation and stability improvements
+
+Version 5.4.5 *(2020-10-27)*
+----------------------------
+
+ * Fixed some smaller glitches + translation improvements
+
Version 5.4.4 *(2020-09-23)*
----------------------------
diff --git a/README.md b/README.md
index 6ab5c2c8..34019b8a 100644
--- a/README.md
+++ b/README.md
@@ -11,20 +11,21 @@ It comes with material design and dark theme by default, provides great user exp
Contains no ads or unnecessary permissions. It is fully opensource, provides customizable colors.
-Check out the full suite of Simple Tools here:
+Check out the full suite of Simple Tools here:
https://www.simplemobiletools.com
-Facebook:
+Facebook:
https://www.facebook.com/simplemobiletools
-Reddit:
+Reddit:
https://www.reddit.com/r/SimpleMobileTools
diff --git a/app/build.gradle b/app/build.gradle
index 7e4c0ba9..e52f8787 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -16,8 +16,8 @@ android {
applicationId "com.simplemobiletools.smsmessenger"
minSdkVersion 22
targetSdkVersion 30
- versionCode 16
- versionName "5.4.4"
+ versionCode 29
+ versionName "5.9.0"
setProperty("archivesBaseName", "sms-messenger")
}
@@ -56,13 +56,14 @@ android {
}
dependencies {
- implementation 'com.simplemobiletools:commons:5.30.12'
+ implementation 'com.simplemobiletools:commons:5.33.32'
implementation 'org.greenrobot:eventbus:3.2.0'
implementation 'com.klinkerapps:android-smsmms:5.2.6'
- implementation 'com.github.tibbi:IndicatorFastScroll:08f512858a'
+ implementation 'com.github.tibbi:IndicatorFastScroll:c3de1d040a'
implementation "me.leolin:ShortcutBadger:1.1.22"
+ implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
- kapt "androidx.room:room-compiler:2.2.5"
- implementation "androidx.room:room-runtime:2.2.5"
- annotationProcessor "androidx.room:room-compiler:2.2.5"
+ kapt "androidx.room:room-compiler:2.2.6"
+ implementation "androidx.room:room-runtime:2.2.6"
+ annotationProcessor "androidx.room:room-compiler:2.2.6"
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 814fde4a..0dabfa90 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -38,6 +38,21 @@
android:name=".activities.ThreadActivity"
android:parentActivityName=".activities.MainActivity" />
+
+
+
+
+
+
+
+
+
@@ -121,6 +136,10 @@
+
+
+
+
@@ -149,11 +168,6 @@
android:exported="true"
android:taskAffinity="com.klinker.android.messaging.MMS_RECEIVED" />
-
-
launchSearch()
R.id.settings -> launchSettings()
R.id.about -> launchAbout()
else -> return super.onOptionsItemSelected(item)
@@ -138,7 +139,10 @@ class MainActivity : SimpleActivity() {
handlePermission(PERMISSION_READ_CONTACTS) {
initMessenger()
bus = EventBus.getDefault()
- bus!!.register(this)
+ try {
+ bus!!.register(this)
+ } catch (e: Exception) {
+ }
}
} else {
finish()
@@ -165,7 +169,12 @@ class MainActivity : SimpleActivity() {
private fun getCachedConversations() {
ensureBackgroundThread {
- val conversations = conversationsDB.getAll().sortedByDescending { it.date }.toMutableList() as ArrayList
+ val conversations = try {
+ conversationsDB.getAll().sortedByDescending { it.date }.toMutableList() as ArrayList
+ } catch (e: Exception) {
+ ArrayList()
+ }
+
updateUnreadCountBadge(conversations)
runOnUiThread {
setupConversations(conversations)
@@ -175,46 +184,43 @@ class MainActivity : SimpleActivity() {
}
private fun getNewConversations(cachedConversations: ArrayList) {
- val privateCursor = getMyContactsCursor().loadInBackground()
+ val privateCursor = getMyContactsCursor()?.loadInBackground()
ensureBackgroundThread {
- val conversations = getConversations()
-
- // check if no message came from a privately stored contact in Simple Contacts
val privateContacts = MyContactsContentProvider.getSimpleContacts(this, privateCursor)
- if (privateContacts.isNotEmpty()) {
- conversations.filter { it.title == it.phoneNumber }.forEach { conversation ->
- privateContacts.forEach { contact ->
- if (contact.doesContainPhoneNumber(conversation.phoneNumber)) {
- conversation.title = contact.name
- conversation.photoUri = contact.photoUri
- }
- }
- }
- }
+ val conversations = getConversations(privateContacts = privateContacts)
runOnUiThread {
setupConversations(conversations)
}
conversations.forEach { clonedConversation ->
- if (!cachedConversations.map { it.thread_id }.contains(clonedConversation.thread_id)) {
+ if (!cachedConversations.map { it.threadId }.contains(clonedConversation.threadId)) {
conversationsDB.insertOrUpdate(clonedConversation)
cachedConversations.add(clonedConversation)
}
}
cachedConversations.forEach { cachedConversation ->
- if (!conversations.map { it.thread_id }.contains(cachedConversation.thread_id)) {
- conversationsDB.delete(cachedConversation.id!!)
+ if (!conversations.map { it.threadId }.contains(cachedConversation.threadId)) {
+ conversationsDB.deleteThreadId(cachedConversation.threadId)
}
}
cachedConversations.forEach { cachedConversation ->
- val conv = conversations.firstOrNull { it.thread_id == cachedConversation.thread_id && it.getStringToCompare() != cachedConversation.getStringToCompare() }
+ val conv = conversations.firstOrNull { it.threadId == cachedConversation.threadId && it.toString() != cachedConversation.toString() }
if (conv != null) {
conversationsDB.insertOrUpdate(conv)
}
}
+
+ if (config.appRunCount == 1) {
+ conversations.map { it.threadId }.forEach { threadId ->
+ val messages = getMessages(threadId)
+ messages.chunked(30).forEach { currentMessages ->
+ messagesDB.insertMessages(*currentMessages.toTypedArray())
+ }
+ }
+ }
}
}
@@ -224,17 +230,27 @@ class MainActivity : SimpleActivity() {
no_conversations_placeholder.beVisibleIf(!hasConversations)
no_conversations_placeholder_2.beVisibleIf(!hasConversations)
+ if (!hasConversations && config.appRunCount == 1) {
+ no_conversations_placeholder.text = getString(R.string.loading_messages)
+ no_conversations_placeholder_2.beGone()
+ }
+
val currAdapter = conversations_list.adapter
if (currAdapter == null) {
ConversationsAdapter(this, conversations, conversations_list, conversations_fastscroller) {
Intent(this, ThreadActivity::class.java).apply {
- putExtra(THREAD_ID, (it as Conversation).thread_id)
+ putExtra(THREAD_ID, (it as Conversation).threadId)
putExtra(THREAD_TITLE, it.title)
startActivity(this)
}
}.apply {
conversations_list.adapter = this
}
+
+ conversations_fastscroller.setViews(conversations_list) {
+ val listItem = (conversations_list.adapter as? ConversationsAdapter)?.conversations?.getOrNull(it)
+ conversations_fastscroller.updateBubbleText(listItem?.title ?: "")
+ }
} else {
try {
(currAdapter as ConversationsAdapter).updateConversations(conversations)
@@ -281,6 +297,10 @@ class MainActivity : SimpleActivity() {
.build()
}
+ private fun launchSearch() {
+ startActivity(Intent(applicationContext, SearchActivity::class.java))
+ }
+
private fun launchSettings() {
startActivity(Intent(applicationContext, SettingsActivity::class.java))
}
@@ -290,7 +310,8 @@ class MainActivity : SimpleActivity() {
val faqItems = arrayListOf(
FAQItem(R.string.faq_2_title_commons, R.string.faq_2_text_commons),
- FAQItem(R.string.faq_6_title_commons, R.string.faq_6_text_commons)
+ FAQItem(R.string.faq_6_title_commons, R.string.faq_6_text_commons),
+ FAQItem(R.string.faq_9_title_commons, R.string.faq_9_text_commons)
)
startAboutActivity(R.string.app_name, licenses, BuildConfig.VERSION_NAME, faqItems, true)
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/NewConversationActivity.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/NewConversationActivity.kt
index cc276e4d..88549ae2 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/NewConversationActivity.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/NewConversationActivity.kt
@@ -5,12 +5,12 @@ import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.WindowManager
+import com.google.gson.Gson
import com.reddit.indicatorfastscroll.FastScrollItemIndicator
+import com.simplemobiletools.commons.dialogs.RadioGroupDialog
import com.simplemobiletools.commons.extensions.*
-import com.simplemobiletools.commons.helpers.MyContactsContentProvider
-import com.simplemobiletools.commons.helpers.PERMISSION_READ_CONTACTS
-import com.simplemobiletools.commons.helpers.SimpleContactsHelper
-import com.simplemobiletools.commons.helpers.ensureBackgroundThread
+import com.simplemobiletools.commons.helpers.*
+import com.simplemobiletools.commons.models.RadioItem
import com.simplemobiletools.commons.models.SimpleContact
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.adapters.ContactsAdapter
@@ -89,9 +89,12 @@ class NewConversationActivity : SimpleActivity() {
}
}
+ val adjustedPrimaryColor = getAdjustedPrimaryColor()
contacts_letter_fastscroller.textColor = config.textColor.getColorStateList()
+ contacts_letter_fastscroller.pressedTextColor = adjustedPrimaryColor
contacts_letter_fastscroller_thumb.setupWithFastScroller(contacts_letter_fastscroller)
- contacts_letter_fastscroller_thumb.textColor = config.primaryColor.getContrastColor()
+ contacts_letter_fastscroller_thumb?.textColor = adjustedPrimaryColor.getContrastColor()
+ contacts_letter_fastscroller_thumb?.thumbColor = adjustedPrimaryColor.getColorStateList()
}
private fun isThirdPartyIntent(): Boolean {
@@ -134,7 +137,20 @@ class NewConversationActivity : SimpleActivity() {
ContactsAdapter(this, contacts, contacts_list, null) {
hideKeyboard()
- launchThreadActivity((it as SimpleContact).phoneNumbers.first(), it.name)
+ val contact = it as SimpleContact
+ val phoneNumbers = contact.phoneNumbers
+ if (phoneNumbers.size > 1) {
+ val items = ArrayList()
+ phoneNumbers.forEachIndexed { index, phoneNumber ->
+ items.add(RadioItem(index, phoneNumber, phoneNumber))
+ }
+
+ RadioGroupDialog(this, items) {
+ launchThreadActivity(it as String, contact.name)
+ }
+ } else {
+ launchThreadActivity(phoneNumbers.first(), contact.name)
+ }
}.apply {
contacts_list.adapter = this
}
@@ -143,7 +159,7 @@ class NewConversationActivity : SimpleActivity() {
}
private fun fillSuggestedContacts(callback: () -> Unit) {
- val privateCursor = getMyContactsCursor().loadInBackground()
+ val privateCursor = getMyContactsCursor()?.loadInBackground()
ensureBackgroundThread {
privateContacts = MyContactsContentProvider.getSimpleContacts(this, privateCursor)
val suggestions = getSuggestedContacts(privateContacts)
@@ -160,10 +176,13 @@ class NewConversationActivity : SimpleActivity() {
layoutInflater.inflate(R.layout.item_suggested_contact, null).apply {
suggested_contact_name.text = contact.name
suggested_contact_name.setTextColor(baseConfig.textColor)
- SimpleContactsHelper(this@NewConversationActivity).loadContactImage(contact.photoUri, suggested_contact_image, contact.name)
- suggestions_holder.addView(this)
- setOnClickListener {
- launchThreadActivity(contact.phoneNumbers.first(), contact.name)
+
+ if (!isDestroyed) {
+ SimpleContactsHelper(this@NewConversationActivity).loadContactImage(contact.photoUri, suggested_contact_image, contact.name)
+ suggestions_holder.addView(this)
+ setOnClickListener {
+ launchThreadActivity(contact.phoneNumbers.first(), contact.name)
+ }
}
}
}
@@ -187,11 +206,13 @@ class NewConversationActivity : SimpleActivity() {
private fun launchThreadActivity(phoneNumber: String, name: String) {
val text = intent.getStringExtra(Intent.EXTRA_TEXT) ?: ""
+ val numbers = phoneNumber.split(";").toSet()
+ val number = if (numbers.size == 1) phoneNumber else Gson().toJson(numbers)
Intent(this, ThreadActivity::class.java).apply {
- putExtra(THREAD_ID, getThreadId(phoneNumber).toInt())
+ putExtra(THREAD_ID, getThreadId(numbers))
putExtra(THREAD_TITLE, name)
putExtra(THREAD_TEXT, text)
- putExtra(THREAD_NUMBER, phoneNumber)
+ putExtra(THREAD_NUMBER, number)
if (intent.action == Intent.ACTION_SEND && intent.extras?.containsKey(Intent.EXTRA_STREAM) == true) {
val uri = intent.getParcelableExtra(Intent.EXTRA_STREAM)
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/SearchActivity.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/SearchActivity.kt
new file mode 100644
index 00000000..a3ea9ad5
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/SearchActivity.kt
@@ -0,0 +1,146 @@
+package com.simplemobiletools.smsmessenger.activities
+
+import android.annotation.SuppressLint
+import android.app.SearchManager
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.text.TextUtils
+import android.util.TypedValue
+import android.view.Menu
+import android.view.MenuItem
+import androidx.appcompat.widget.SearchView
+import androidx.core.view.MenuItemCompat
+import com.simplemobiletools.commons.extensions.*
+import com.simplemobiletools.commons.helpers.ensureBackgroundThread
+import com.simplemobiletools.smsmessenger.R
+import com.simplemobiletools.smsmessenger.adapters.SearchResultsAdapter
+import com.simplemobiletools.smsmessenger.extensions.conversationsDB
+import com.simplemobiletools.smsmessenger.extensions.messagesDB
+import com.simplemobiletools.smsmessenger.helpers.SEARCHED_MESSAGE_ID
+import com.simplemobiletools.smsmessenger.helpers.THREAD_ID
+import com.simplemobiletools.smsmessenger.helpers.THREAD_TITLE
+import com.simplemobiletools.smsmessenger.models.Conversation
+import com.simplemobiletools.smsmessenger.models.Message
+import com.simplemobiletools.smsmessenger.models.SearchResult
+import kotlinx.android.synthetic.main.activity_search.*
+
+class SearchActivity : SimpleActivity() {
+ private var mIsSearchOpen = false
+ private var mLastSearchedText = ""
+ private var mSearchMenuItem: MenuItem? = null
+
+ @SuppressLint("InlinedApi")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_search)
+ updateTextColors(search_holder)
+ search_placeholder.setTextSize(TypedValue.COMPLEX_UNIT_PX, getTextSize())
+ search_placeholder_2.setTextSize(TypedValue.COMPLEX_UNIT_PX, getTextSize())
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.menu_search, menu)
+ setupSearch(menu)
+ return true
+ }
+
+ private fun setupSearch(menu: Menu) {
+ val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
+ mSearchMenuItem = menu.findItem(R.id.search)
+
+ MenuItemCompat.setOnActionExpandListener(mSearchMenuItem, object : MenuItemCompat.OnActionExpandListener {
+ override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
+ mIsSearchOpen = true
+ return true
+ }
+
+ // this triggers on device rotation too, avoid doing anything
+ override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
+ if (mIsSearchOpen) {
+ mIsSearchOpen = false
+ mLastSearchedText = ""
+ finish()
+ }
+ return true
+ }
+ })
+
+ mSearchMenuItem?.expandActionView()
+ (mSearchMenuItem?.actionView as? SearchView)?.apply {
+ setSearchableInfo(searchManager.getSearchableInfo(componentName))
+ isSubmitButtonEnabled = false
+ setOnQueryTextListener(object : SearchView.OnQueryTextListener {
+ override fun onQueryTextSubmit(query: String) = false
+
+ override fun onQueryTextChange(newText: String): Boolean {
+ if (mIsSearchOpen) {
+ mLastSearchedText = newText
+ textChanged(newText)
+ }
+ return true
+ }
+ })
+ }
+ }
+
+ private fun textChanged(text: String) {
+ search_placeholder_2.beGoneIf(text.length >= 2)
+ if (text.length >= 2) {
+ ensureBackgroundThread {
+ val searchQuery = "%$text%"
+ val messages = messagesDB.getMessagesWithText(searchQuery)
+ val conversations = conversationsDB.getConversationsWithText(searchQuery)
+ if (text == mLastSearchedText) {
+ showSearchResults(messages, conversations, text)
+
+ }
+ }
+ } else {
+ search_placeholder.beVisible()
+ search_results_list.beGone()
+ }
+ }
+
+ private fun showSearchResults(messages: List, conversations: List, searchedText: String) {
+ val searchResults = ArrayList()
+ conversations.forEach { conversation ->
+ val date = conversation.date.formatDateOrTime(this, true, true)
+ val searchResult = SearchResult(-1, conversation.title, conversation.phoneNumber, date, conversation.threadId, conversation.photoUri)
+ searchResults.add(searchResult)
+ }
+
+ messages.forEach { message ->
+ var recipient = message.senderName
+ if (recipient.isEmpty() && message.participants.isNotEmpty()) {
+ val participantNames = message.participants.map { it.name }
+ recipient = TextUtils.join(", ", participantNames)
+ }
+
+ val date = message.date.formatDateOrTime(this, true, true)
+ val searchResult = SearchResult(message.id, recipient, message.body, date, message.threadId, message.senderPhotoUri)
+ searchResults.add(searchResult)
+ }
+
+ runOnUiThread {
+ search_results_list.beVisibleIf(searchResults.isNotEmpty())
+ search_placeholder.beVisibleIf(searchResults.isEmpty())
+
+ val currAdapter = search_results_list.adapter
+ if (currAdapter == null) {
+ SearchResultsAdapter(this, searchResults, search_results_list, searchedText) {
+ Intent(this, ThreadActivity::class.java).apply {
+ putExtra(THREAD_ID, (it as SearchResult).threadId)
+ putExtra(THREAD_TITLE, it.title)
+ putExtra(SEARCHED_MESSAGE_ID, it.messageId)
+ startActivity(this)
+ }
+ }.apply {
+ search_results_list.adapter = this
+ }
+ } else {
+ (currAdapter as SearchResultsAdapter).updateItems(searchResults, searchedText)
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/SettingsActivity.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/SettingsActivity.kt
index c9a3d38d..5b57f772 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/SettingsActivity.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/SettingsActivity.kt
@@ -30,10 +30,12 @@ class SettingsActivity : SimpleActivity() {
setupPurchaseThankYou()
setupCustomizeColors()
+ setupCustomizeNotifications()
setupUseEnglish()
setupManageBlockedNumbers()
setupChangeDateTimeFormat()
setupFontSize()
+ setupShowCharacterCounter()
updateTextColors(settings_scrollview)
if (blockedNumbersAtPause != -1 && blockedNumbersAtPause != getBlockedNumbers().hashCode()) {
@@ -52,15 +54,23 @@ class SettingsActivity : SimpleActivity() {
}
private fun setupPurchaseThankYou() {
- settings_purchase_thank_you_holder.beVisibleIf(!isThankYouInstalled())
+ settings_purchase_thank_you_holder.beGoneIf(isOrWasThankYouInstalled())
settings_purchase_thank_you_holder.setOnClickListener {
launchPurchaseThankYouIntent()
}
}
private fun setupCustomizeColors() {
+ settings_customize_colors_label.text = getCustomizeColorsString()
settings_customize_colors_holder.setOnClickListener {
- startCustomizationActivity()
+ handleCustomizeColorsClick()
+ }
+ }
+
+ private fun setupCustomizeNotifications() {
+ settings_customize_notifications_holder.beVisibleIf(isOreoPlus())
+ settings_customize_notifications_holder.setOnClickListener {
+ launchCustomizeNotificationsIntent()
}
}
@@ -106,4 +116,12 @@ class SettingsActivity : SimpleActivity() {
}
}
}
+
+ private fun setupShowCharacterCounter() {
+ settings_show_character_counter.isChecked = config.showCharacterCounter
+ settings_show_character_counter_holder.setOnClickListener {
+ settings_show_character_counter.toggle()
+ config.showCharacterCounter = settings_show_character_counter.isChecked
+ }
+ }
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt
index 0831549f..2c46ac2f 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt
@@ -5,12 +5,15 @@ import android.app.Activity
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.drawable.Drawable
+import android.graphics.drawable.LayerDrawable
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Bundle
+import android.os.Handler
import android.provider.Telephony
import android.telephony.SubscriptionManager
import android.text.TextUtils
+import android.util.TypedValue
import android.view.*
import android.view.inputmethod.EditorInfo
import android.widget.LinearLayout
@@ -38,6 +41,9 @@ 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.SmsStatusDeliveredReceiver
+import com.simplemobiletools.smsmessenger.receivers.SmsStatusSentReceiver
+import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.activity_thread.*
import kotlinx.android.synthetic.main.item_attachment.view.*
import kotlinx.android.synthetic.main.item_selected_contact.view.*
@@ -49,9 +55,10 @@ class ThreadActivity : SimpleActivity() {
private val MIN_DATE_TIME_DIFF_SECS = 300
private val PICK_ATTACHMENT_INTENT = 1
- private var threadId = 0
+ private var threadId = 0L
private var currentSIMCardIndex = 0
private var isActivityVisible = false
+ private var refreshedSinceSent = false
private var threadItems = ArrayList()
private var bus: EventBus? = null
private var participants = ArrayList()
@@ -71,7 +78,7 @@ class ThreadActivity : SimpleActivity() {
return
}
- threadId = intent.getIntExtra(THREAD_ID, 0)
+ threadId = intent.getLongExtra(THREAD_ID, 0L)
intent.getStringExtra(THREAD_TITLE)?.let {
supportActionBar?.title = it
}
@@ -80,7 +87,19 @@ class ThreadActivity : SimpleActivity() {
bus!!.register(this)
handlePermission(PERMISSION_READ_PHONE_STATE) {
if (it) {
- setupThread()
+ setupButtons()
+ setupCachedMessages {
+ val searchedMessageId = intent.getLongExtra(SEARCHED_MESSAGE_ID, -1L)
+ intent.removeExtra(SEARCHED_MESSAGE_ID)
+ if (searchedMessageId != -1L) {
+ val index = threadItems.indexOfFirst { (it as? Message)?.id == searchedMessageId }
+ if (index != -1) {
+ thread_messages_list.smoothScrollToPosition(index)
+ }
+ }
+
+ setupThread()
+ }
} else {
finish()
}
@@ -97,94 +116,6 @@ class ThreadActivity : SimpleActivity() {
isActivityVisible = false
}
- private fun setupThread() {
- val privateCursor = getMyContactsCursor().loadInBackground()
- ensureBackgroundThread {
- messages = getMessages(threadId)
- participants = if (messages.isEmpty()) {
- getThreadParticipants(threadId, null)
- } else {
- messages.first().participants
- }
-
- // check if no participant came from a privately stored contact in Simple Contacts
- privateContacts = MyContactsContentProvider.getSimpleContacts(this, privateCursor)
- if (privateContacts.isNotEmpty()) {
- val senderNumbersToReplace = HashMap()
- participants.filter { it.doesContainPhoneNumber(it.name) }.forEach { participant ->
- privateContacts.firstOrNull { it.doesContainPhoneNumber(participant.phoneNumbers.first()) }?.apply {
- senderNumbersToReplace[participant.phoneNumbers.first()] = name
- participant.name = name
- participant.photoUri = photoUri
- }
- }
-
- messages.forEach { message ->
- if (senderNumbersToReplace.keys.contains(message.senderName)) {
- message.senderName = senderNumbersToReplace[message.senderName]!!
- }
- }
- }
-
- if (participants.isEmpty()) {
- val name = intent.getStringExtra(THREAD_TITLE) ?: ""
- val number = intent.getStringExtra(THREAD_NUMBER)
- if (number == null) {
- toast(R.string.unknown_error_occurred)
- finish()
- return@ensureBackgroundThread
- }
-
- val contact = SimpleContact(0, 0, name, "", arrayListOf(number))
- participants.add(contact)
- }
-
- 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 {
- val threadTitle = participants.getThreadTitle()
- if (threadTitle.isNotEmpty()) {
- supportActionBar?.title = participants.getThreadTitle()
- }
-
- if (messages.isEmpty()) {
- window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
- thread_type_message.requestFocus()
- }
-
- setupSIMSelector()
- }
- }
- setupButtons()
- }
-
override fun onDestroy() {
super.onDestroy()
bus?.unregister(this)
@@ -223,13 +154,98 @@ class ThreadActivity : SimpleActivity() {
}
}
+ private fun setupCachedMessages(callback: () -> Unit) {
+ ensureBackgroundThread {
+ messages = messagesDB.getThreadMessages(threadId).toMutableList() as ArrayList
+ setupParticipants()
+ setupAdapter()
+
+ runOnUiThread {
+ if (messages.isEmpty()) {
+ window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
+ thread_type_message.requestFocus()
+ }
+
+ setupThreadTitle()
+ setupSIMSelector()
+ callback()
+ }
+ }
+ }
+
+ private fun setupThread() {
+ val privateCursor = getMyContactsCursor()?.loadInBackground()
+ ensureBackgroundThread {
+ val cachedMessagesCode = messages.hashCode()
+ messages = getMessages(threadId)
+ if (messages.hashCode() == cachedMessagesCode && participants.isNotEmpty()) {
+ return@ensureBackgroundThread
+ }
+
+ setupParticipants()
+
+ // check if no participant came from a privately stored contact in Simple Contacts
+ privateContacts = MyContactsContentProvider.getSimpleContacts(this, privateCursor)
+ if (privateContacts.isNotEmpty()) {
+ val senderNumbersToReplace = HashMap()
+ participants.filter { it.doesContainPhoneNumber(it.name) }.forEach { participant ->
+ privateContacts.firstOrNull { it.doesContainPhoneNumber(participant.phoneNumbers.first()) }?.apply {
+ senderNumbersToReplace[participant.phoneNumbers.first()] = name
+ participant.name = name
+ participant.photoUri = photoUri
+ }
+ }
+
+ messages.forEach { message ->
+ if (senderNumbersToReplace.keys.contains(message.senderName)) {
+ message.senderName = senderNumbersToReplace[message.senderName]!!
+ }
+ }
+ }
+
+ if (participants.isEmpty()) {
+ val name = intent.getStringExtra(THREAD_TITLE) ?: ""
+ val number = intent.getStringExtra(THREAD_NUMBER)
+ if (number == null) {
+ toast(R.string.unknown_error_occurred)
+ finish()
+ return@ensureBackgroundThread
+ }
+
+ val contact = SimpleContact(0, 0, name, "", arrayListOf(number), ArrayList(), ArrayList())
+ participants.add(contact)
+ }
+
+ messages.chunked(30).forEach { currentMessages ->
+ messagesDB.insertMessages(*currentMessages.toTypedArray())
+ }
+
+ setupAttachmentSizes()
+ setupAdapter()
+ runOnUiThread {
+ setupThreadTitle()
+ setupSIMSelector()
+ }
+ }
+ }
+
private fun setupAdapter() {
threadItems = getThreadItems()
invalidateOptionsMenu()
runOnUiThread {
- val adapter = ThreadAdapter(this, threadItems, thread_messages_list, thread_messages_fastscroller) {}
- thread_messages_list.adapter = adapter
+ val currAdapter = thread_messages_list.adapter
+ if (currAdapter == null) {
+ ThreadAdapter(this, threadItems, thread_messages_list, thread_messages_fastscroller) {
+ (it as? ThreadError)?.apply {
+ thread_type_message.setText(it.messageText)
+ }
+ }.apply {
+ thread_messages_list.adapter = this
+ }
+ } else {
+ (currAdapter as ThreadAdapter).updateMessages(threadItems)
+ }
}
SimpleContactsHelper(this).getAvailableContacts(false) { contacts ->
@@ -252,7 +268,7 @@ class ThreadActivity : SimpleActivity() {
confirm_inserted_number?.setOnClickListener {
val number = add_contact_or_number.value
- val contact = SimpleContact(number.hashCode(), number.hashCode(), number, "", arrayListOf(number))
+ val contact = SimpleContact(number.hashCode(), number.hashCode(), number, "", arrayListOf(number), ArrayList(), ArrayList())
addSelectedContact(contact)
}
}
@@ -264,6 +280,10 @@ class ThreadActivity : SimpleActivity() {
confirm_manage_contacts.applyColorFilter(textColor)
thread_add_attachment.applyColorFilter(textColor)
+ thread_character_counter.beVisibleIf(config.showCharacterCounter)
+ thread_character_counter.setTextSize(TypedValue.COMPLEX_UNIT_PX, getTextSize())
+
+ thread_type_message.setTextSize(TypedValue.COMPLEX_UNIT_PX, getTextSize())
thread_send_message.setOnClickListener {
sendMessage()
}
@@ -271,6 +291,7 @@ class ThreadActivity : SimpleActivity() {
thread_send_message.isClickable = false
thread_type_message.onTextChangeListener {
checkSendMessageAvailability()
+ thread_character_counter.text = it.length.toString()
}
confirm_manage_contacts.setOnClickListener {
@@ -284,7 +305,7 @@ class ThreadActivity : SimpleActivity() {
}
}
- val newThreadId = getThreadId(numbers).toInt()
+ val newThreadId = getThreadId(numbers)
if (threadId != newThreadId) {
Intent(this, ThreadActivity::class.java).apply {
putExtra(THREAD_ID, newThreadId)
@@ -309,12 +330,59 @@ class ThreadActivity : SimpleActivity() {
}
}
+ private fun setupAttachmentSizes() {
+ 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.getUri()), null, fileOptions)
+ it.width = fileOptions.outWidth
+ it.height = fileOptions.outHeight
+ } else if (it.mimetype.startsWith("video/")) {
+ val metaRetriever = MediaMetadataRetriever()
+ metaRetriever.setDataSource(this, it.getUri())
+ 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) {
+ }
+ }
+ }
+ }
+
+ private fun setupParticipants() {
+ if (participants.isEmpty()) {
+ participants = if (messages.isEmpty()) {
+ getThreadParticipants(threadId, null)
+ } else {
+ messages.first().participants
+ }
+ }
+ }
+
+ private fun setupThreadTitle() {
+ val threadTitle = participants.getThreadTitle()
+ if (threadTitle.isNotEmpty()) {
+ supportActionBar?.title = participants.getThreadTitle()
+ }
+ }
+
@SuppressLint("MissingPermission")
private fun setupSIMSelector() {
val availableSIMs = SubscriptionManager.from(this).activeSubscriptionInfoList ?: return
if (availableSIMs.size > 1) {
availableSIMs.forEachIndexed { index, subscriptionInfo ->
- var label = subscriptionInfo.displayName.toString()
+ var label = subscriptionInfo.displayName?.toString() ?: ""
if (subscriptionInfo.number?.isNotEmpty() == true) {
label += " (${subscriptionInfo.number})"
}
@@ -329,6 +397,10 @@ class ThreadActivity : SimpleActivity() {
}
}
+ if (numbers.isEmpty()) {
+ return
+ }
+
currentSIMCardIndex = availableSIMs.indexOfFirstOrNull { it.subscriptionId == config.getUseSIMIdAtNumber(numbers.first()) } ?: 0
thread_select_sim_icon.applyColorFilter(config.textColor)
@@ -396,11 +468,20 @@ class ThreadActivity : SimpleActivity() {
}
private fun showSelectedContacts() {
+ val adjustedColor = getAdjustedPrimaryColor()
+
val views = ArrayList()
participants.forEach {
val contact = it
layoutInflater.inflate(R.layout.item_selected_contact, null).apply {
+ val selectedContactBg = resources.getDrawable(R.drawable.item_selected_contact_background)
+ (selectedContactBg as LayerDrawable).findDrawableByLayerId(R.id.selected_contact_bg).applyColorFilter(adjustedColor)
+ selected_contact_holder.background = selectedContactBg
+
selected_contact_name.text = contact.name
+ selected_contact_name.setTextColor(adjustedColor.getContrastColor())
+ selected_contact_remove.applyColorFilter(adjustedColor.getContrastColor())
+
selected_contact_remove.setOnClickListener {
if (contact.rawId != participants.first().rawId) {
removeSelectedContact(contact.rawId)
@@ -424,7 +505,7 @@ class ThreadActivity : SimpleActivity() {
private fun markAsUnread() {
ensureBackgroundThread {
- conversationsDB.markUnread(threadId.toLong())
+ conversationsDB.markUnread(threadId)
markThreadMessagesUnread(threadId)
runOnUiThread {
finish()
@@ -462,13 +543,17 @@ class ThreadActivity : SimpleActivity() {
items.add(message)
if (message.type == Telephony.Sms.MESSAGE_TYPE_FAILED) {
- items.add(ThreadError(message.id))
+ items.add(ThreadError(message.id, message.body))
+ }
+
+ if (message.type == Telephony.Sms.MESSAGE_TYPE_OUTBOX) {
+ items.add(ThreadSending(message.id))
}
if (!message.read) {
hadUnreadItems = true
markMessageRead(message.id, message.isMMS)
- conversationsDB.markRead(threadId.toLong())
+ conversationsDB.markRead(threadId)
}
if (i == cnt - 1 && message.type == Telephony.Sms.MESSAGE_TYPE_SENT) {
@@ -562,6 +647,7 @@ class ThreadActivity : SimpleActivity() {
val settings = Settings()
settings.useSystemSending = true
+ settings.deliveryReports = true
val SIMId = availableSIMCards.getOrNull(currentSIMCardIndex)?.subscriptionId
if (SIMId != null) {
@@ -582,19 +668,35 @@ class ThreadActivity : SimpleActivity() {
message.addMedia(byteArray, mimeType)
} catch (e: Exception) {
showErrorToast(e)
+ } catch (e: Error) {
+ toast(e.localizedMessage ?: getString(R.string.unknown_error_occurred))
}
}
}
try {
- transaction.sendNewMessage(message, threadId.toLong())
+ val smsSentIntent = Intent(this, SmsStatusSentReceiver::class.java)
+ val deliveredIntent = Intent(this, SmsStatusDeliveredReceiver::class.java)
+ transaction.setExplicitBroadcastForSentSms(smsSentIntent)
+ transaction.setExplicitBroadcastForDeliveredSms(deliveredIntent)
+
+ refreshedSinceSent = false
+ transaction.sendNewMessage(message, threadId)
thread_type_message.setText("")
attachmentUris.clear()
thread_attachments_holder.beGone()
thread_attachments_wrapper.removeAllViews()
+
+ Handler().postDelayed({
+ if (!refreshedSinceSent) {
+ refreshMessages()
+ }
+ }, 2000)
} catch (e: Exception) {
showErrorToast(e)
+ } catch (e: Error) {
+ toast(e.localizedMessage ?: getString(R.string.unknown_error_occurred))
}
}
@@ -652,13 +754,30 @@ class ThreadActivity : SimpleActivity() {
showSelectedContacts()
}
+ @SuppressLint("MissingPermission")
@Subscribe(threadMode = ThreadMode.ASYNC)
fun refreshMessages(event: Events.RefreshMessages) {
+ refreshedSinceSent = true
if (isActivityVisible) {
- notificationManager.cancel(threadId)
+ notificationManager.cancel(threadId.hashCode())
}
+ val lastMaxId = messages.maxByOrNull { it.id }?.id ?: 0L
messages = getMessages(threadId)
+
+ messages.filter { !it.isReceivedMessage() && it.id > lastMaxId }.forEach { latestMessage ->
+ // subscriptionIds seem to be not filled out at sending with multiple SIM cards, so fill it manually
+ if (SubscriptionManager.from(this).activeSubscriptionInfoList?.size ?: 0 > 1) {
+ val SIMId = availableSIMCards.getOrNull(currentSIMCardIndex)?.subscriptionId
+ if (SIMId != null) {
+ updateMessageSubscriptionId(latestMessage.id, SIMId)
+ latestMessage.subscriptionId = SIMId
+ }
+ }
+
+ messagesDB.insertOrIgnore(latestMessage)
+ }
+
setupAdapter()
}
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/AutoCompleteTextViewAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/AutoCompleteTextViewAdapter.kt
index 445c8294..1f9c20de 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/AutoCompleteTextViewAdapter.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/AutoCompleteTextViewAdapter.kt
@@ -5,12 +5,16 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.Filter
+import android.widget.RelativeLayout
import android.widget.TextView
+import com.simplemobiletools.commons.extensions.darkenColor
+import com.simplemobiletools.commons.extensions.getContrastColor
import com.simplemobiletools.commons.extensions.normalizeString
import com.simplemobiletools.commons.helpers.SimpleContactsHelper
import com.simplemobiletools.commons.models.SimpleContact
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.activities.SimpleActivity
+import com.simplemobiletools.smsmessenger.extensions.config
class AutoCompleteTextViewAdapter(val activity: SimpleActivity, val contacts: ArrayList) : ArrayAdapter(activity, 0, contacts) {
var resultList = ArrayList()
@@ -30,8 +34,13 @@ class AutoCompleteTextViewAdapter(val activity: SimpleActivity, val contacts: Ar
isFocusable = false
}
+ val backgroundColor = activity.config.backgroundColor
findViewById(R.id.item_contact_name).text = contact.name
findViewById(R.id.item_contact_number).text = contact.phoneNumbers.first()
+ findViewById(R.id.item_contact_holder).setBackgroundColor(backgroundColor.darkenColor())
+
+ findViewById(R.id.item_contact_name).setTextColor(backgroundColor.getContrastColor())
+ findViewById(R.id.item_contact_number).setTextColor(backgroundColor.getContrastColor())
SimpleContactsHelper(context).loadContactImage(contact.photoUri, findViewById(R.id.item_contact_image), contact.name)
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ConversationsAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ConversationsAdapter.kt
index a1d0a118..ed8f66ae 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ConversationsAdapter.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ConversationsAdapter.kt
@@ -2,6 +2,7 @@ package com.simplemobiletools.smsmessenger.adapters
import android.content.Intent
import android.graphics.Typeface
+import android.net.Uri
import android.text.TextUtils
import android.util.TypedValue
import android.view.Menu
@@ -37,8 +38,10 @@ class ConversationsAdapter(activity: SimpleActivity, var conversations: ArrayLis
override fun prepareActionMode(menu: Menu) {
menu.apply {
- findItem(R.id.cab_add_number_to_contact).isVisible = isOneItemSelected() && getSelectedItems().firstOrNull()?.isGroupConversation == false
findItem(R.id.cab_block_number).isVisible = isNougatPlus()
+ findItem(R.id.cab_add_number_to_contact).isVisible = isOneItemSelected() && getSelectedItems().firstOrNull()?.isGroupConversation == false
+ findItem(R.id.cab_dial_number).isVisible = isOneItemSelected() && getSelectedItems().firstOrNull()?.isGroupConversation == false
+ findItem(R.id.cab_copy_number).isVisible = isOneItemSelected() && getSelectedItems().firstOrNull()?.isGroupConversation == false
}
}
@@ -50,8 +53,10 @@ class ConversationsAdapter(activity: SimpleActivity, var conversations: ArrayLis
when (id) {
R.id.cab_add_number_to_contact -> addNumberToContact()
R.id.cab_block_number -> askConfirmBlock()
- R.id.cab_select_all -> selectAll()
+ R.id.cab_dial_number -> dialNumber()
+ R.id.cab_copy_number -> copyNumberToClipboard()
R.id.cab_delete -> askConfirmDelete()
+ R.id.cab_select_all -> selectAll()
}
}
@@ -59,9 +64,9 @@ class ConversationsAdapter(activity: SimpleActivity, var conversations: ArrayLis
override fun getIsItemSelectable(position: Int) = true
- override fun getItemSelectionKey(position: Int) = conversations.getOrNull(position)?.thread_id
+ override fun getItemSelectionKey(position: Int) = conversations.getOrNull(position)?.hashCode()
- override fun getItemKeyPosition(key: Int) = conversations.indexOfFirst { it.thread_id == key }
+ override fun getItemKeyPosition(key: Int) = conversations.indexOfFirst { it.hashCode() == key }
override fun onActionModeCreated() {}
@@ -110,6 +115,26 @@ class ConversationsAdapter(activity: SimpleActivity, var conversations: ArrayLis
}
}
+ private fun dialNumber() {
+ val conversation = getSelectedItems().firstOrNull() ?: return
+ Intent(Intent.ACTION_DIAL).apply {
+ data = Uri.fromParts("tel", conversation.phoneNumber, null)
+
+ if (resolveActivity(activity.packageManager) != null) {
+ activity.startActivity(this)
+ finishActMode()
+ } else {
+ activity.toast(R.string.no_app_found)
+ }
+ }
+ }
+
+ private fun copyNumberToClipboard() {
+ val conversation = getSelectedItems().firstOrNull() ?: return
+ activity.copyToClipboard(conversation.phoneNumber)
+ finishActMode()
+ }
+
private fun askConfirmDelete() {
val itemsCnt = selectedKeys.size
val items = resources.getQuantityString(R.plurals.delete_conversations, itemsCnt, itemsCnt)
@@ -129,13 +154,17 @@ class ConversationsAdapter(activity: SimpleActivity, var conversations: ArrayLis
return
}
- val conversationsToRemove = conversations.filter { selectedKeys.contains(it.thread_id) } as ArrayList
+ val conversationsToRemove = conversations.filter { selectedKeys.contains(it.hashCode()) } as ArrayList
val positions = getSelectedItemPositions()
conversationsToRemove.forEach {
- activity.deleteConversation(it.thread_id)
- activity.notificationManager.cancel(it.thread_id)
+ activity.deleteConversation(it.threadId)
+ activity.notificationManager.cancel(it.hashCode())
+ }
+
+ try {
+ conversations.removeAll(conversationsToRemove)
+ } catch (ignored: Exception) {
}
- conversations.removeAll(conversationsToRemove)
activity.runOnUiThread {
if (conversationsToRemove.isEmpty()) {
@@ -165,7 +194,7 @@ class ConversationsAdapter(activity: SimpleActivity, var conversations: ArrayLis
}
}
- private fun getSelectedItems() = conversations.filter { selectedKeys.contains(it.thread_id) } as ArrayList
+ private fun getSelectedItems() = conversations.filter { selectedKeys.contains(it.hashCode()) } as ArrayList
override fun onViewRecycled(holder: ViewHolder) {
super.onViewRecycled(holder)
@@ -190,7 +219,7 @@ class ConversationsAdapter(activity: SimpleActivity, var conversations: ArrayLis
private fun setupView(view: View, conversation: Conversation) {
view.apply {
- conversation_frame.isSelected = selectedKeys.contains(conversation.thread_id)
+ conversation_frame.isSelected = selectedKeys.contains(conversation.hashCode())
conversation_address.apply {
text = conversation.title
@@ -203,7 +232,7 @@ class ConversationsAdapter(activity: SimpleActivity, var conversations: ArrayLis
}
conversation_date.apply {
- text = conversation.date.formatDateOrTime(context, true)
+ text = conversation.date.formatDateOrTime(context, true, false)
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 0.8f)
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/SearchResultsAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/SearchResultsAdapter.kt
new file mode 100644
index 00000000..01df81e2
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/SearchResultsAdapter.kt
@@ -0,0 +1,96 @@
+package com.simplemobiletools.smsmessenger.adapters
+
+import android.util.TypedValue
+import android.view.Menu
+import android.view.View
+import android.view.ViewGroup
+import com.bumptech.glide.Glide
+import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter
+import com.simplemobiletools.commons.extensions.getTextSize
+import com.simplemobiletools.commons.extensions.highlightTextPart
+import com.simplemobiletools.commons.helpers.SimpleContactsHelper
+import com.simplemobiletools.commons.views.MyRecyclerView
+import com.simplemobiletools.smsmessenger.R
+import com.simplemobiletools.smsmessenger.activities.SimpleActivity
+import com.simplemobiletools.smsmessenger.models.SearchResult
+import kotlinx.android.synthetic.main.item_search_result.view.*
+import java.util.*
+
+class SearchResultsAdapter(
+ activity: SimpleActivity, var searchResults: ArrayList, recyclerView: MyRecyclerView, highlightText: String, itemClick: (Any) -> Unit
+) : MyRecyclerViewAdapter(activity, recyclerView, null, itemClick) {
+
+ private var fontSize = activity.getTextSize()
+ private var textToHighlight = highlightText
+
+ override fun getActionMenuId() = 0
+
+ override fun prepareActionMode(menu: Menu) {}
+
+ override fun actionItemPressed(id: Int) {}
+
+ override fun getSelectableItemCount() = searchResults.size
+
+ override fun getIsItemSelectable(position: Int) = false
+
+ override fun getItemSelectionKey(position: Int) = searchResults.getOrNull(position)?.hashCode()
+
+ override fun getItemKeyPosition(key: Int) = searchResults.indexOfFirst { it.hashCode() == key }
+
+ override fun onActionModeCreated() {}
+
+ override fun onActionModeDestroyed() {}
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(R.layout.item_search_result, parent)
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val searchResult = searchResults[position]
+ holder.bindView(searchResult, true, false) { itemView, layoutPosition ->
+ setupView(itemView, searchResult)
+ }
+ bindViewHolder(holder)
+ }
+
+ override fun getItemCount() = searchResults.size
+
+ fun updateItems(newItems: ArrayList, highlightText: String = "") {
+ if (newItems.hashCode() != searchResults.hashCode()) {
+ searchResults = newItems.clone() as ArrayList
+ textToHighlight = highlightText
+ notifyDataSetChanged()
+ } else if (textToHighlight != highlightText) {
+ textToHighlight = highlightText
+ notifyDataSetChanged()
+ }
+ }
+
+ private fun setupView(view: View, searchResult: SearchResult) {
+ view.apply {
+ search_result_title.apply {
+ text = searchResult.title.highlightTextPart(textToHighlight, adjustedPrimaryColor)
+ setTextColor(textColor)
+ setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 1.2f)
+ }
+
+ search_result_snippet.apply {
+ text = searchResult.snippet.highlightTextPart(textToHighlight, adjustedPrimaryColor)
+ setTextColor(textColor)
+ setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 0.9f)
+ }
+
+ search_result_date.apply {
+ text = searchResult.date
+ setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 0.8f)
+ }
+
+ SimpleContactsHelper(context).loadContactImage(searchResult.photoUri, search_result_image, searchResult.title)
+ }
+ }
+
+ override fun onViewRecycled(holder: ViewHolder) {
+ super.onViewRecycled(holder)
+ if (!activity.isDestroyed && !activity.isFinishing && holder.itemView.search_result_image != null) {
+ Glide.with(activity).clear(holder.itemView.search_result_image)
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ThreadAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ThreadAdapter.kt
index 73086af1..5590a1d0 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ThreadAdapter.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ThreadAdapter.kt
@@ -29,6 +29,7 @@ import com.simplemobiletools.commons.views.FastScroller
import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.activities.SimpleActivity
+import com.simplemobiletools.smsmessenger.dialogs.SelectTextDialog
import com.simplemobiletools.smsmessenger.extensions.deleteMessage
import com.simplemobiletools.smsmessenger.helpers.*
import com.simplemobiletools.smsmessenger.models.*
@@ -37,10 +38,14 @@ import kotlinx.android.synthetic.main.item_received_message.view.*
import kotlinx.android.synthetic.main.item_received_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_error.view.*
+import kotlinx.android.synthetic.main.item_thread_sending.view.*
import kotlinx.android.synthetic.main.item_thread_success.view.*
-class ThreadAdapter(activity: SimpleActivity, var messages: ArrayList, recyclerView: MyRecyclerView, fastScroller: FastScroller,
- itemClick: (Any) -> Unit) : MyRecyclerViewAdapter(activity, recyclerView, fastScroller, itemClick) {
+class ThreadAdapter(
+ activity: SimpleActivity, var messages: ArrayList, recyclerView: MyRecyclerView, fastScroller: FastScroller,
+ itemClick: (Any) -> Unit
+) : MyRecyclerViewAdapter(activity, recyclerView, fastScroller, itemClick) {
private val roundedCornersRadius = resources.getDimension(R.dimen.normal_margin).toInt()
private var fontSize = activity.getTextSize()
@@ -58,6 +63,7 @@ class ThreadAdapter(activity: SimpleActivity, var messages: ArrayList copyToClipboard()
R.id.cab_share -> shareText()
- R.id.cab_select_all -> selectAll()
+ R.id.cab_select_text -> selectText()
R.id.cab_delete -> askConfirmDelete()
+ R.id.cab_select_all -> selectAll()
}
}
@@ -78,9 +85,9 @@ class ThreadAdapter(activity: SimpleActivity, var messages: ArrayList R.layout.item_received_message
THREAD_SENT_MESSAGE_ERROR -> R.layout.item_thread_error
THREAD_SENT_MESSAGE_SUCCESS -> R.layout.item_thread_success
+ THREAD_SENT_MESSAGE_SENDING -> R.layout.item_thread_sending
else -> R.layout.item_sent_message
}
return createViewHolder(layout, parent)
@@ -99,11 +107,15 @@ class ThreadAdapter(activity: SimpleActivity, var messages: ArrayList
+ val isClickable = item is ThreadError || item is Message
+ val isLongClickable = item is Message
+ holder.bindView(item, isClickable, isLongClickable) { itemView, layoutPosition ->
when (item) {
is ThreadDateTime -> setupDateTime(itemView, item)
is ThreadSuccess -> setupThreadSuccess(itemView)
- !is ThreadError -> setupView(itemView, item as Message)
+ is ThreadError -> setupThreadError(itemView)
+ is ThreadSending -> setupThreadSending(itemView)
+ else -> setupView(itemView, item as Message)
}
}
bindViewHolder(holder)
@@ -118,23 +130,38 @@ class ThreadAdapter(activity: SimpleActivity, var messages: ArrayList THREAD_RECEIVED_MESSAGE
item is ThreadError -> THREAD_SENT_MESSAGE_ERROR
item is ThreadSuccess -> THREAD_SENT_MESSAGE_SUCCESS
+ item is ThreadSending -> THREAD_SENT_MESSAGE_SENDING
else -> THREAD_SENT_MESSAGE
}
}
private fun copyToClipboard() {
- val firstItem = getSelectedItems().first() as? Message ?: return
+ val firstItem = getSelectedItems().firstOrNull() as? Message ?: return
activity.copyToClipboard(firstItem.body)
}
private fun shareText() {
- val firstItem = getSelectedItems().first() as? Message ?: return
+ val firstItem = getSelectedItems().firstOrNull() as? Message ?: return
activity.shareTextIntent(firstItem.body)
}
+ private fun selectText() {
+ val firstItem = getSelectedItems().firstOrNull() as? Message ?: return
+ if (firstItem.body.trim().isNotEmpty()) {
+ SelectTextDialog(activity, firstItem.body)
+ }
+ }
+
private fun askConfirmDelete() {
val itemsCnt = selectedKeys.size
- val items = resources.getQuantityString(R.plurals.delete_messages, itemsCnt, itemsCnt)
+
+ // not sure how we can get UnknownFormatConversionException here, so show the error and hope that someone reports it
+ val items = try {
+ resources.getQuantityString(R.plurals.delete_messages, itemsCnt, itemsCnt)
+ } catch (e: Exception) {
+ activity.showErrorToast(e)
+ return
+ }
val baseString = R.string.deletion_confirmation
val question = String.format(resources.getString(baseString), items)
@@ -151,7 +178,7 @@ class ThreadAdapter(activity: SimpleActivity, var messages: ArrayList
+ val messagesToRemove = getSelectedItems()
val positions = getSelectedItemPositions()
messagesToRemove.forEach {
activity.deleteMessage((it as Message).id, it.isMMS)
@@ -168,20 +195,23 @@ class ThreadAdapter(activity: SimpleActivity, var messages: ArrayList
+ private fun getSelectedItems() = messages.filter { selectedKeys.contains((it as? Message)?.hashCode() ?: 0) } as ArrayList
private fun isThreadDateTime(position: Int) = messages.getOrNull(position) is ThreadDateTime
- override fun onViewRecycled(holder: ViewHolder) {
- super.onViewRecycled(holder)
- if (!activity.isDestroyed && !activity.isFinishing && holder.itemView.thread_message_sender_photo != null) {
- Glide.with(activity).clear(holder.itemView.thread_message_sender_photo)
+ fun updateMessages(newMessages: ArrayList) {
+ val oldHashCode = messages.hashCode()
+ val newHashCode = newMessages.hashCode()
+ if (newHashCode != oldHashCode) {
+ messages = newMessages
+ notifyDataSetChanged()
+ recyclerView.scrollToPosition(messages.size - 1)
}
}
private fun setupView(view: View, message: Message) {
view.apply {
- thread_message_holder.isSelected = selectedKeys.contains(message.id)
+ thread_message_holder.isSelected = selectedKeys.contains(message.hashCode())
thread_message_body.apply {
text = message.body
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize)
@@ -196,7 +226,7 @@ class ThreadAdapter(activity: SimpleActivity, var messages: ArrayList { } }
+ .create().apply {
+ activity.setupDialogStuff(view, this)
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/ArrayList.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/ArrayList.kt
index 0ff90913..dfaf3202 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/ArrayList.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/ArrayList.kt
@@ -2,8 +2,5 @@ package com.simplemobiletools.smsmessenger.extensions
import android.text.TextUtils
import com.simplemobiletools.commons.models.SimpleContact
-import com.simplemobiletools.smsmessenger.models.Conversation
fun ArrayList.getThreadTitle() = TextUtils.join(", ", map { it.name }.toTypedArray())
-
-fun ArrayList.getHashToCompare() = map { it.getStringToCompare() }.hashCode()
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt
index 7c8a734b..e7dfd13c 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt
@@ -8,6 +8,7 @@ import android.app.PendingIntent
import android.content.ContentValues
import android.content.Context
import android.content.Intent
+import android.database.Cursor
import android.graphics.Bitmap
import android.media.AudioAttributes
import android.media.AudioManager
@@ -27,7 +28,10 @@ import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.activities.ThreadActivity
import com.simplemobiletools.smsmessenger.databases.MessagesDatabase
import com.simplemobiletools.smsmessenger.helpers.*
+import com.simplemobiletools.smsmessenger.interfaces.AttachmentsDao
import com.simplemobiletools.smsmessenger.interfaces.ConversationsDao
+import com.simplemobiletools.smsmessenger.interfaces.MessageAttachmentsDao
+import com.simplemobiletools.smsmessenger.interfaces.MessagesDao
import com.simplemobiletools.smsmessenger.models.*
import com.simplemobiletools.smsmessenger.receivers.DirectReplyReceiver
import com.simplemobiletools.smsmessenger.receivers.MarkAsReadReceiver
@@ -41,7 +45,13 @@ fun Context.getMessagessDB() = MessagesDatabase.getInstance(this)
val Context.conversationsDB: ConversationsDao get() = getMessagessDB().ConversationsDao()
-fun Context.getMessages(threadId: Int): ArrayList {
+val Context.attachmentsDB: AttachmentsDao get() = getMessagessDB().AttachmentsDao()
+
+val Context.messageAttachmentsDB: MessageAttachmentsDao get() = getMessagessDB().MessageAttachmentsDao()
+
+val Context.messagesDB: MessagesDao get() = getMessagessDB().MessagesDao()
+
+fun Context.getMessages(threadId: Long): ArrayList {
val uri = Sms.CONTENT_URI
val projection = arrayOf(
Sms._ID,
@@ -76,17 +86,17 @@ fun Context.getMessages(threadId: Int): ArrayList {
return@queryCursor
}
- val id = cursor.getIntValue(Sms._ID)
+ val id = cursor.getLongValue(Sms._ID)
val body = cursor.getStringValue(Sms.BODY)
val type = cursor.getIntValue(Sms.TYPE)
val namePhoto = getNameAndPhotoFromPhoneNumber(senderNumber)
- val senderName = namePhoto?.name ?: ""
- val photoUri = namePhoto?.photoUri ?: ""
+ val senderName = namePhoto.name
+ val photoUri = namePhoto.photoUri ?: ""
val date = (cursor.getLongValue(Sms.DATE) / 1000).toInt()
val read = cursor.getIntValue(Sms.READ) == 1
- val thread = cursor.getIntValue(Sms.THREAD_ID)
+ val thread = cursor.getLongValue(Sms.THREAD_ID)
val subscriptionId = cursor.getIntValue(Sms.SUBSCRIPTION_ID)
- val participant = SimpleContact(0, 0, senderName, photoUri, arrayListOf(senderNumber))
+ val participant = SimpleContact(0, 0, senderName, photoUri, arrayListOf(senderNumber), ArrayList(), ArrayList())
val isMMS = false
val message = Message(id, body, type, arrayListOf(participant), date, read, thread, isMMS, null, senderName, photoUri, subscriptionId)
messages.add(message)
@@ -100,7 +110,7 @@ fun Context.getMessages(threadId: Int): ArrayList {
}
// as soon as a message contains multiple recipients it counts as an MMS instead of SMS
-fun Context.getMMS(threadId: Int? = null, sortOrder: String? = null): ArrayList {
+fun Context.getMMS(threadId: Long? = null, sortOrder: String? = null): ArrayList {
val uri = Mms.CONTENT_URI
val projection = arrayOf(
Mms._ID,
@@ -125,13 +135,13 @@ fun Context.getMMS(threadId: Int? = null, sortOrder: String? = null): ArrayList<
val messages = ArrayList()
val contactsMap = HashMap()
- val threadParticipants = HashMap>()
+ val threadParticipants = HashMap>()
queryCursor(uri, projection, selection, selectionArgs, sortOrder, showErrors = true) { cursor ->
- val mmsId = cursor.getIntValue(Mms._ID)
+ val mmsId = cursor.getLongValue(Mms._ID)
val type = cursor.getIntValue(Mms.MESSAGE_BOX)
val date = cursor.getLongValue(Mms.DATE).toInt()
val read = cursor.getIntValue(Mms.READ) == 1
- val threadId = cursor.getIntValue(Mms.THREAD_ID)
+ val threadId = cursor.getLongValue(Mms.THREAD_ID)
val subscriptionId = cursor.getIntValue(Mms.SUBSCRIPTION_ID)
val participants = if (threadParticipants.containsKey(threadId)) {
threadParticipants[threadId]!!
@@ -143,17 +153,15 @@ fun Context.getMMS(threadId: Int? = null, sortOrder: String? = null): ArrayList<
val isMMS = true
val attachment = getMmsAttachment(mmsId)
- val body = attachment?.text ?: ""
+ val body = attachment.text
var senderName = ""
var senderPhotoUri = ""
if (type != Mms.MESSAGE_BOX_SENT && type != Mms.MESSAGE_BOX_FAILED) {
val number = getMMSSender(mmsId)
val namePhoto = getNameAndPhotoFromPhoneNumber(number)
- if (namePhoto != null) {
- senderName = namePhoto.name
- senderPhotoUri = namePhoto.photoUri ?: ""
- }
+ senderName = namePhoto.name
+ senderPhotoUri = namePhoto.photoUri ?: ""
}
val message = Message(mmsId, body, type, participants, date, read, threadId, isMMS, attachment, senderName, senderPhotoUri, subscriptionId)
@@ -167,7 +175,7 @@ fun Context.getMMS(threadId: Int? = null, sortOrder: String? = null): ArrayList<
return messages
}
-fun Context.getMMSSender(msgId: Int): String {
+fun Context.getMMSSender(msgId: Long): String {
val uri = Uri.parse("${Mms.CONTENT_URI}/$msgId/addr")
val projection = arrayOf(
Mms.Addr.ADDRESS
@@ -185,7 +193,7 @@ fun Context.getMMSSender(msgId: Int): String {
return ""
}
-fun Context.getConversations(threadId: Long? = null): ArrayList {
+fun Context.getConversations(threadId: Long? = null, privateContacts: ArrayList = ArrayList()): ArrayList {
val uri = Uri.parse("${Threads.CONTENT_URI}?simple=true")
val projection = arrayOf(
Threads._ID,
@@ -208,7 +216,7 @@ fun Context.getConversations(threadId: Long? = null): ArrayList {
val simpleContactHelper = SimpleContactsHelper(this)
val blockedNumbers = getBlockedNumbers()
queryCursor(uri, projection, selection, selectionArgs, sortOrder, true) { cursor ->
- val id = cursor.getIntValue(Threads._ID)
+ val id = cursor.getLongValue(Threads._ID)
var snippet = cursor.getStringValue(Threads.SNIPPET) ?: ""
if (snippet.isEmpty()) {
snippet = getThreadSnippet(id)
@@ -226,12 +234,12 @@ fun Context.getConversations(threadId: Long? = null): ArrayList {
return@queryCursor
}
- val names = getThreadContactNames(phoneNumbers)
+ val names = getThreadContactNames(phoneNumbers, privateContacts)
val title = TextUtils.join(", ", names.toTypedArray())
val photoUri = if (phoneNumbers.size == 1) simpleContactHelper.getPhotoUriFromPhoneNumber(phoneNumbers.first()) else ""
val isGroupConversation = phoneNumbers.size > 1
val read = cursor.getIntValue(Threads.READ) == 1
- val conversation = Conversation(null, id, snippet, date.toInt(), read, title, photoUri, isGroupConversation, phoneNumbers.first())
+ val conversation = Conversation(id, snippet, date.toInt(), read, title, photoUri, isGroupConversation, phoneNumbers.first())
conversations.add(conversation)
}
@@ -241,7 +249,7 @@ fun Context.getConversations(threadId: Long? = null): ArrayList {
// based on https://stackoverflow.com/a/6446831/1967672
@SuppressLint("NewApi")
-fun Context.getMmsAttachment(id: Int): MessageAttachment? {
+fun Context.getMmsAttachment(id: Long): MessageAttachment {
val uri = if (isQPlus()) {
Mms.Part.CONTENT_URI
} else {
@@ -259,15 +267,15 @@ fun Context.getMmsAttachment(id: Int): MessageAttachment? {
var attachmentName = ""
queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor ->
- val partId = cursor.getStringValue(Mms._ID)
+ val partId = cursor.getLongValue(Mms._ID)
val mimetype = cursor.getStringValue(Mms.Part.CONTENT_TYPE)
if (mimetype == "text/plain") {
messageAttachment.text = cursor.getStringValue(Mms.Part.TEXT) ?: ""
} else if (mimetype.startsWith("image/") || mimetype.startsWith("video/")) {
- val attachment = Attachment(Uri.withAppendedPath(uri, partId), mimetype, 0, 0, "")
+ val attachment = Attachment(partId, id, Uri.withAppendedPath(uri, partId.toString()).toString(), mimetype, 0, 0, "")
messageAttachment.attachments.add(attachment)
} else if (mimetype != "application/smil") {
- val attachment = Attachment(Uri.withAppendedPath(uri, partId), mimetype, 0, 0, attachmentName)
+ val attachment = Attachment(partId, id, Uri.withAppendedPath(uri, partId.toString()).toString(), mimetype, 0, 0, attachmentName)
messageAttachment.attachments.add(attachment)
} else {
val text = cursor.getStringValue(Mms.Part.TEXT)
@@ -286,7 +294,7 @@ fun Context.getLatestMMS(): Message? {
return getMMS(sortOrder = sortOrder).firstOrNull()
}
-fun Context.getThreadSnippet(threadId: Int): String {
+fun Context.getThreadSnippet(threadId: Long): String {
val sortOrder = "${Mms.DATE} DESC LIMIT 1"
val latestMms = getMMS(threadId, sortOrder).firstOrNull()
var snippet = latestMms?.body ?: ""
@@ -313,7 +321,29 @@ fun Context.getThreadSnippet(threadId: Int): String {
return snippet
}
-fun Context.getThreadParticipants(threadId: Int, contactsMap: HashMap?): ArrayList {
+fun Context.getMessageRecipientAddress(messageId: Long): String {
+ val uri = Sms.CONTENT_URI
+ val projection = arrayOf(
+ Sms.ADDRESS
+ )
+
+ val selection = "${Sms._ID} = ?"
+ val selectionArgs = arrayOf(messageId.toString())
+
+ try {
+ val cursor = contentResolver.query(uri, projection, selection, selectionArgs, null)
+ cursor?.use {
+ if (cursor.moveToFirst()) {
+ return cursor.getStringValue(Sms.ADDRESS)
+ }
+ }
+ } catch (e: Exception) {
+ }
+
+ return ""
+}
+
+fun Context.getThreadParticipants(threadId: Long, contactsMap: HashMap?): ArrayList {
val uri = Uri.parse("${MmsSms.CONTENT_CONVERSATIONS_URI}?simple=true")
val projection = arrayOf(
ThreadsColumns.RECIPIENT_IDS
@@ -335,9 +365,9 @@ fun Context.getThreadParticipants(threadId: Int, contactsMap: HashMap): ArrayList {
return numbers
}
-fun Context.getThreadContactNames(phoneNumbers: List): ArrayList {
+fun Context.getThreadContactNames(phoneNumbers: List, privateContacts: ArrayList): ArrayList {
val names = ArrayList()
- phoneNumbers.forEach {
- names.add(SimpleContactsHelper(this).getNameFromPhoneNumber(it))
+ phoneNumbers.forEach { number ->
+ val name = SimpleContactsHelper(this).getNameFromPhoneNumber(number)
+ if (name != number) {
+ names.add(name)
+ } else {
+ val privateContact = privateContacts.firstOrNull { it.doesContainPhoneNumber(number) }
+ if (privateContact == null) {
+ names.add(name)
+ } else {
+ names.add(privateContact.name)
+ }
+ }
}
return names
}
@@ -400,9 +440,9 @@ fun Context.getSuggestedContacts(privateContacts: ArrayList): Arr
queryCursor(uri, projection, selection, selectionArgs, sortOrder, showErrors = true) { cursor ->
val senderNumber = cursor.getStringValue(Sms.ADDRESS) ?: return@queryCursor
val namePhoto = getNameAndPhotoFromPhoneNumber(senderNumber)
- var senderName = namePhoto?.name ?: ""
- var photoUri = namePhoto?.photoUri ?: ""
- if (namePhoto == null || isNumberBlocked(senderNumber, blockedNumbers)) {
+ var senderName = namePhoto.name
+ var photoUri = namePhoto.photoUri ?: ""
+ if (isNumberBlocked(senderNumber, blockedNumbers)) {
return@queryCursor
} else if (namePhoto.name == senderNumber) {
if (privateContacts.isNotEmpty()) {
@@ -418,7 +458,7 @@ fun Context.getSuggestedContacts(privateContacts: ArrayList): Arr
}
}
- val contact = SimpleContact(0, 0, senderName, photoUri, arrayListOf(senderNumber))
+ val contact = SimpleContact(0, 0, senderName, photoUri, arrayListOf(senderNumber), ArrayList(), ArrayList())
if (!contacts.map { it.phoneNumbers.first().trimToComparableNumber() }.contains(senderNumber.trimToComparableNumber())) {
contacts.add(contact)
}
@@ -427,7 +467,7 @@ fun Context.getSuggestedContacts(privateContacts: ArrayList): Arr
return contacts
}
-fun Context.getNameAndPhotoFromPhoneNumber(number: String): NamePhoto? {
+fun Context.getNameAndPhotoFromPhoneNumber(number: String): NamePhoto {
if (!hasPermission(PERMISSION_READ_CONTACTS)) {
return NamePhoto(number, null)
}
@@ -454,7 +494,7 @@ fun Context.getNameAndPhotoFromPhoneNumber(number: String): NamePhoto? {
return NamePhoto(number, null)
}
-fun Context.insertNewSMS(address: String, subject: String, body: String, date: Long, read: Int, threadId: Long, type: Int, subscriptionId: Int): Int {
+fun Context.insertNewSMS(address: String, subject: String, body: String, date: Long, read: Int, threadId: Long, type: Int, subscriptionId: Int): Long {
val uri = Sms.CONTENT_URI
val contentValues = ContentValues().apply {
put(Sms.ADDRESS, address)
@@ -468,29 +508,39 @@ fun Context.insertNewSMS(address: String, subject: String, body: String, date: L
}
val newUri = contentResolver.insert(uri, contentValues)
- return newUri?.lastPathSegment?.toInt() ?: 0
+ return newUri?.lastPathSegment?.toLong() ?: 0L
}
-fun Context.deleteConversation(threadId: Int) {
+fun Context.deleteConversation(threadId: Long) {
var uri = Sms.CONTENT_URI
val selection = "${Sms.THREAD_ID} = ?"
val selectionArgs = arrayOf(threadId.toString())
- contentResolver.delete(uri, selection, selectionArgs)
+ try {
+ contentResolver.delete(uri, selection, selectionArgs)
+ } catch (e: Exception) {
+ showErrorToast(e)
+ }
uri = Mms.CONTENT_URI
contentResolver.delete(uri, selection, selectionArgs)
- conversationsDB.deleteThreadId(threadId.toLong())
+ conversationsDB.deleteThreadId(threadId)
+ messagesDB.deleteThreadMessages(threadId)
}
-fun Context.deleteMessage(id: Int, isMMS: Boolean) {
+fun Context.deleteMessage(id: Long, isMMS: Boolean) {
val uri = if (isMMS) Mms.CONTENT_URI else Sms.CONTENT_URI
val selection = "${Sms._ID} = ?"
val selectionArgs = arrayOf(id.toString())
- contentResolver.delete(uri, selection, selectionArgs)
+ try {
+ contentResolver.delete(uri, selection, selectionArgs)
+ messagesDB.delete(id)
+ } catch (e: Exception) {
+ showErrorToast(e)
+ }
}
-fun Context.markMessageRead(id: Int, isMMS: Boolean) {
+fun Context.markMessageRead(id: Long, isMMS: Boolean) {
val uri = if (isMMS) Mms.CONTENT_URI else Sms.CONTENT_URI
val contentValues = ContentValues().apply {
put(Sms.READ, 1)
@@ -499,9 +549,10 @@ fun Context.markMessageRead(id: Int, isMMS: Boolean) {
val selection = "${Sms._ID} = ?"
val selectionArgs = arrayOf(id.toString())
contentResolver.update(uri, contentValues, selection, selectionArgs)
+ messagesDB.markRead(id)
}
-fun Context.markThreadMessagesRead(threadId: Int) {
+fun Context.markThreadMessagesRead(threadId: Long) {
arrayOf(Sms.CONTENT_URI, Mms.CONTENT_URI).forEach { uri ->
val contentValues = ContentValues().apply {
put(Sms.READ, 1)
@@ -511,9 +562,10 @@ fun Context.markThreadMessagesRead(threadId: Int) {
val selectionArgs = arrayOf(threadId.toString())
contentResolver.update(uri, contentValues, selection, selectionArgs)
}
+ messagesDB.markThreadRead(threadId)
}
-fun Context.markThreadMessagesUnread(threadId: Int) {
+fun Context.markThreadMessagesUnread(threadId: Long) {
arrayOf(Sms.CONTENT_URI, Mms.CONTENT_URI).forEach { uri ->
val contentValues = ContentValues().apply {
put(Sms.READ, 0)
@@ -525,6 +577,26 @@ fun Context.markThreadMessagesUnread(threadId: Int) {
}
}
+fun Context.updateMessageType(id: Long, status: Int) {
+ val uri = Sms.CONTENT_URI
+ val contentValues = ContentValues().apply {
+ put(Sms.TYPE, status)
+ }
+ val selection = "${Sms._ID} = ?"
+ val selectionArgs = arrayOf(id.toString())
+ contentResolver.update(uri, contentValues, selection, selectionArgs)
+}
+
+fun Context.updateMessageSubscriptionId(messageId: Long, subscriptionId: Int) {
+ val uri = Sms.CONTENT_URI
+ val contentValues = ContentValues().apply {
+ put(Sms.SUBSCRIPTION_ID, subscriptionId)
+ }
+ val selection = "${Sms._ID} = ?"
+ val selectionArgs = arrayOf(messageId.toString())
+ contentResolver.update(uri, contentValues, selection, selectionArgs)
+}
+
fun Context.updateUnreadCountBadge(conversations: List) {
val unreadCount = conversations.count { !it.read }
if (unreadCount == 0) {
@@ -560,23 +632,28 @@ fun Context.getThreadId(addresses: Set): Long {
}
}
-fun Context.showReceivedMessageNotification(address: String, body: String, threadID: Int, bitmap: Bitmap?) {
- val privateCursor = getMyContactsCursor().loadInBackground()
+fun Context.showReceivedMessageNotification(address: String, body: String, threadId: Long, bitmap: Bitmap?) {
+ val privateCursor = getMyContactsCursor()?.loadInBackground()
ensureBackgroundThread {
- var sender = getNameAndPhotoFromPhoneNumber(address)?.name ?: ""
- if (address == sender) {
- val privateContacts = MyContactsContentProvider.getSimpleContacts(this, privateCursor)
- sender = privateContacts.firstOrNull { it.doesContainPhoneNumber(address) }?.name ?: address
- }
+ val senderName = getNameFromAddress(address, privateCursor)
Handler(Looper.getMainLooper()).post {
- showMessageNotification(address, body, threadID, bitmap, sender)
+ showMessageNotification(address, body, threadId, bitmap, senderName)
}
}
}
+fun Context.getNameFromAddress(address: String, privateCursor: Cursor?): String {
+ var sender = getNameAndPhotoFromPhoneNumber(address).name
+ if (address == sender) {
+ val privateContacts = MyContactsContentProvider.getSimpleContacts(this, privateCursor)
+ sender = privateContacts.firstOrNull { it.doesContainPhoneNumber(address) }?.name ?: address
+ }
+ return sender
+}
+
@SuppressLint("NewApi")
-fun Context.showMessageNotification(address: String, body: String, threadID: Int, bitmap: Bitmap?, sender: String) {
+fun Context.showMessageNotification(address: String, body: String, threadId: Long, bitmap: Bitmap?, sender: String) {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
if (isOreoPlus()) {
@@ -598,14 +675,14 @@ fun Context.showMessageNotification(address: String, body: String, threadID: Int
}
val intent = Intent(this, ThreadActivity::class.java).apply {
- putExtra(THREAD_ID, threadID)
+ putExtra(THREAD_ID, threadId)
}
- val pendingIntent = PendingIntent.getActivity(this, threadID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ val pendingIntent = PendingIntent.getActivity(this, threadId.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT)
val summaryText = getString(R.string.new_message)
val markAsReadIntent = Intent(this, MarkAsReadReceiver::class.java).apply {
action = MARK_AS_READ
- putExtra(THREAD_ID, threadID)
+ putExtra(THREAD_ID, threadId)
}
val markAsReadPendingIntent = PendingIntent.getBroadcast(this, 0, markAsReadIntent, PendingIntent.FLAG_CANCEL_CURRENT)
@@ -618,11 +695,11 @@ fun Context.showMessageNotification(address: String, body: String, threadID: Int
.build()
val replyIntent = Intent(this, DirectReplyReceiver::class.java).apply {
- putExtra(THREAD_ID, threadID)
+ putExtra(THREAD_ID, threadId)
putExtra(THREAD_NUMBER, address)
}
- val replyPendingIntent = PendingIntent.getBroadcast(applicationContext, threadID, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT)
+ val replyPendingIntent = PendingIntent.getBroadcast(applicationContext, threadId.hashCode(), replyIntent, PendingIntent.FLAG_UPDATE_CURRENT)
replyAction = NotificationCompat.Action.Builder(R.drawable.ic_send_vector, replyLabel, replyPendingIntent)
.addRemoteInput(remoteInput)
.build()
@@ -632,7 +709,7 @@ fun Context.showMessageNotification(address: String, body: String, threadID: Int
val builder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL)
.setContentTitle(sender)
.setContentText(body)
- .setColor(config.primaryColor)
+ .setColor(getAdjustedPrimaryColor())
.setSmallIcon(R.drawable.ic_messenger)
.setLargeIcon(largeIcon)
.setStyle(NotificationCompat.BigTextStyle().setSummaryText(summaryText).bigText(body))
@@ -648,5 +725,5 @@ fun Context.showMessageNotification(address: String, body: String, threadID: Int
builder.addAction(R.drawable.ic_check_vector, getString(R.string.mark_as_read), markAsReadPendingIntent)
.setChannelId(NOTIFICATION_CHANNEL)
- notificationManager.notify(threadID, builder.build())
+ notificationManager.notify(threadId.hashCode(), builder.build())
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Config.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Config.kt
index c5e95e68..451fe60f 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Config.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Config.kt
@@ -13,4 +13,8 @@ class Config(context: Context) : BaseConfig(context) {
}
fun getUseSIMIdAtNumber(number: String) = prefs.getInt(USE_SIM_ID_PREFIX + number, 0)
+
+ var showCharacterCounter: Boolean
+ get() = prefs.getBoolean(SHOW_CHARACTER_COUNTER, false)
+ set(showCharacterCounter) = prefs.edit().putBoolean(SHOW_CHARACTER_COUNTER, showCharacterCounter).apply()
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt
index d98055db..477c8f9b 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt
@@ -9,8 +9,10 @@ const val THREAD_TEXT = "thread_text"
const val THREAD_NUMBER = "thread_number"
const val THREAD_ATTACHMENT_URI = "thread_attachment_uri"
const val THREAD_ATTACHMENT_URIS = "thread_attachment_uris"
+const val SEARCHED_MESSAGE_ID = "searched_message_id"
const val USE_SIM_ID_PREFIX = "use_sim_id_"
const val NOTIFICATION_CHANNEL = "simple_sms_messenger"
+const val SHOW_CHARACTER_COUNTER = "show_character_counter"
private const val PATH = "com.simplemobiletools.smsmessenger.action."
const val MARK_AS_READ = PATH + "mark_as_read"
@@ -22,6 +24,7 @@ const val THREAD_RECEIVED_MESSAGE = 2
const val THREAD_SENT_MESSAGE = 3
const val THREAD_SENT_MESSAGE_ERROR = 4
const val THREAD_SENT_MESSAGE_SUCCESS = 5
+const val THREAD_SENT_MESSAGE_SENDING = 6
fun refreshMessages() {
EventBus.getDefault().post(Events.RefreshMessages())
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Converters.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Converters.kt
new file mode 100644
index 00000000..b11cbeb3
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Converters.kt
@@ -0,0 +1,33 @@
+package com.simplemobiletools.smsmessenger.helpers
+
+import androidx.room.TypeConverter
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import com.simplemobiletools.commons.models.SimpleContact
+import com.simplemobiletools.smsmessenger.models.Attachment
+import com.simplemobiletools.smsmessenger.models.MessageAttachment
+
+class Converters {
+ private val gson = Gson()
+ private val attachmentType = object : TypeToken>() {}.type
+ private val simpleContactType = object : TypeToken>() {}.type
+ private val messageAttachmentType = object : TypeToken() {}.type
+
+ @TypeConverter
+ fun jsonToAttachmentList(value: String) = gson.fromJson>(value, attachmentType)
+
+ @TypeConverter
+ fun attachmentListToJson(list: ArrayList) = gson.toJson(list)
+
+ @TypeConverter
+ fun jsonToSimpleContactList(value: String) = gson.fromJson>(value, simpleContactType)
+
+ @TypeConverter
+ fun simpleContactListToJson(list: ArrayList) = gson.toJson(list)
+
+ @TypeConverter
+ fun jsonToMessageAttachment(value: String) = gson.fromJson(value, messageAttachmentType)
+
+ @TypeConverter
+ fun messageAttachmentToJson(messageAttachment: MessageAttachment?) = gson.toJson(messageAttachment)
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/AttachmentsDao.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/AttachmentsDao.kt
new file mode 100644
index 00000000..240fa747
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/AttachmentsDao.kt
@@ -0,0 +1,11 @@
+package com.simplemobiletools.smsmessenger.interfaces
+
+import androidx.room.Dao
+import androidx.room.Query
+import com.simplemobiletools.smsmessenger.models.Attachment
+
+@Dao
+interface AttachmentsDao {
+ @Query("SELECT * FROM attachments")
+ fun getAll(): List
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/ConversationsDao.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/ConversationsDao.kt
index d50507f7..534cdc1f 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/ConversationsDao.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/ConversationsDao.kt
@@ -17,15 +17,15 @@ interface ConversationsDao {
@Query("SELECT * FROM conversations WHERE read = 0")
fun getUnreadConversations(): List
+ @Query("SELECT * FROM conversations WHERE title LIKE :text")
+ fun getConversationsWithText(text: String): List
+
@Query("UPDATE conversations SET read = 1 WHERE thread_id = :threadId")
fun markRead(threadId: Long)
@Query("UPDATE conversations SET read = 0 WHERE thread_id = :threadId")
fun markUnread(threadId: Long)
- @Query("DELETE FROM conversations WHERE id = :id")
- fun delete(id: Long)
-
@Query("DELETE FROM conversations WHERE thread_id = :threadId")
fun deleteThreadId(threadId: Long)
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/MessageAttachmentsDao.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/MessageAttachmentsDao.kt
new file mode 100644
index 00000000..8517f80d
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/MessageAttachmentsDao.kt
@@ -0,0 +1,11 @@
+package com.simplemobiletools.smsmessenger.interfaces
+
+import androidx.room.Dao
+import androidx.room.Query
+import com.simplemobiletools.smsmessenger.models.MessageAttachment
+
+@Dao
+interface MessageAttachmentsDao {
+ @Query("SELECT * FROM message_attachments")
+ fun getAll(): List
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/MessagesDao.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/MessagesDao.kt
new file mode 100644
index 00000000..c4689b4d
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/MessagesDao.kt
@@ -0,0 +1,43 @@
+package com.simplemobiletools.smsmessenger.interfaces
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import com.simplemobiletools.smsmessenger.models.Message
+
+@Dao
+interface MessagesDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insertOrUpdate(message: Message)
+
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ fun insertOrIgnore(message: Message): Long
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insertMessages(vararg message: Message)
+
+ @Query("SELECT * FROM messages")
+ fun getAll(): List
+
+ @Query("SELECT * FROM messages WHERE thread_id = :threadId")
+ fun getThreadMessages(threadId: Long): List
+
+ @Query("SELECT * FROM messages WHERE body LIKE :text")
+ fun getMessagesWithText(text: String): List
+
+ @Query("UPDATE messages SET read = 1 WHERE id = :id")
+ fun markRead(id: Long)
+
+ @Query("UPDATE messages SET read = 1 WHERE thread_id = :threadId")
+ fun markThreadRead(threadId: Long)
+
+ @Query("UPDATE messages SET type = :type WHERE id = :id")
+ fun updateType(id: Long, type: Int): Int
+
+ @Query("DELETE FROM messages WHERE id = :id")
+ fun delete(id: Long)
+
+ @Query("DELETE FROM messages WHERE thread_id = :threadId")
+ fun deleteThreadMessages(threadId: Long)
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/Attachment.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/Attachment.kt
index f7f4ab7a..4aabd072 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/Attachment.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/Attachment.kt
@@ -1,5 +1,20 @@
package com.simplemobiletools.smsmessenger.models
import android.net.Uri
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.Index
+import androidx.room.PrimaryKey
-data class Attachment(var uri: Uri, var mimetype: String, var width: Int, var height: Int, var filename: String)
+@Entity(tableName = "attachments", indices = [(Index(value = ["message_id"], unique = true))])
+data class Attachment(
+ @PrimaryKey(autoGenerate = true) var id: Long?,
+ @ColumnInfo(name = "message_id") var messageId: Long,
+ @ColumnInfo(name = "uri_string") var uriString: String,
+ @ColumnInfo(name = "mimetype") var mimetype: String,
+ @ColumnInfo(name = "width") var width: Int,
+ @ColumnInfo(name = "height") var height: Int,
+ @ColumnInfo(name = "filename") var filename: String) {
+
+ fun getUri() = Uri.parse(uriString)
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/Conversation.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/Conversation.kt
index 73e557c6..fc5698f4 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/Conversation.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/Conversation.kt
@@ -7,8 +7,7 @@ import androidx.room.PrimaryKey
@Entity(tableName = "conversations", indices = [(Index(value = ["thread_id"], unique = true))])
data class Conversation(
- @PrimaryKey(autoGenerate = true) var id: Long?,
- @ColumnInfo(name = "thread_id") var thread_id: Int,
+ @PrimaryKey @ColumnInfo(name = "thread_id") var threadId: Long,
@ColumnInfo(name = "snippet") var snippet: String,
@ColumnInfo(name = "date") var date: Int,
@ColumnInfo(name = "read") var read: Boolean,
@@ -16,8 +15,4 @@ data class Conversation(
@ColumnInfo(name = "photo_uri") var photoUri: String,
@ColumnInfo(name = "is_group_conversation") var isGroupConversation: Boolean,
@ColumnInfo(name = "phone_number") var phoneNumber: String
-) {
- fun getStringToCompare(): String {
- return copy(id = 0).toString()
- }
-}
+)
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/Message.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/Message.kt
index e18df07c..54d96d04 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/Message.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/Message.kt
@@ -1,10 +1,25 @@
package com.simplemobiletools.smsmessenger.models
import android.provider.Telephony
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
import com.simplemobiletools.commons.models.SimpleContact
+@Entity(tableName = "messages")
data class Message(
- val id: Int, val body: String, val type: Int, val participants: ArrayList, val date: Int, val read: Boolean, val thread: Int,
- val isMMS: Boolean, val attachment: MessageAttachment?, var senderName: String, val senderPhotoUri: String, val subscriptionId: Int) : ThreadItem() {
+ @PrimaryKey val id: Long,
+ @ColumnInfo(name = "body") val body: String,
+ @ColumnInfo(name = "type") val type: Int,
+ @ColumnInfo(name = "participants") val participants: ArrayList,
+ @ColumnInfo(name = "date") val date: Int,
+ @ColumnInfo(name = "read") val read: Boolean,
+ @ColumnInfo(name = "thread_id") val threadId: Long,
+ @ColumnInfo(name = "is_mms") val isMMS: Boolean,
+ @ColumnInfo(name = "attachment") val attachment: MessageAttachment?,
+ @ColumnInfo(name = "sender_name") var senderName: String,
+ @ColumnInfo(name = "sender_photo_uri") val senderPhotoUri: String,
+ @ColumnInfo(name = "subscription_id") var subscriptionId: Int) : ThreadItem() {
+
fun isReceivedMessage() = type == Telephony.Sms.MESSAGE_TYPE_INBOX
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/MessageAttachment.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/MessageAttachment.kt
index 53f4b94f..a95ea775 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/MessageAttachment.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/MessageAttachment.kt
@@ -1,3 +1,11 @@
package com.simplemobiletools.smsmessenger.models
-data class MessageAttachment(val id: Int, var text: String, var attachments: ArrayList)
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "message_attachments")
+data class MessageAttachment(
+ @PrimaryKey val id: Long,
+ @ColumnInfo(name = "text") var text: String,
+ @ColumnInfo(name = "attachments") var attachments: ArrayList)
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/SearchResult.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/SearchResult.kt
new file mode 100644
index 00000000..e6fd47ed
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/SearchResult.kt
@@ -0,0 +1,3 @@
+package com.simplemobiletools.smsmessenger.models
+
+data class SearchResult(val messageId: Long, val title: String, val snippet: String, val date: String, val threadId: Long, var photoUri: String)
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadError.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadError.kt
index 69df786f..9e32530d 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadError.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadError.kt
@@ -1,3 +1,3 @@
package com.simplemobiletools.smsmessenger.models
-data class ThreadError(val messageID: Int) : ThreadItem()
+data class ThreadError(val messageId: Long, val messageText: String) : ThreadItem()
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadSending.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadSending.kt
new file mode 100644
index 00000000..131f7386
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadSending.kt
@@ -0,0 +1,3 @@
+package com.simplemobiletools.smsmessenger.models
+
+data class ThreadSending(val messageId: Long) : ThreadItem()
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadSuccess.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadSuccess.kt
index ec7a4d20..fa75eb53 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadSuccess.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadSuccess.kt
@@ -1,4 +1,4 @@
package com.simplemobiletools.smsmessenger.models
// show a check after the latest message, if it is a sent one and succeeded
-data class ThreadSuccess(val messageID: Int) : ThreadItem()
+data class ThreadSuccess(val messageID: Long) : ThreadItem()
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/DirectReplyReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/DirectReplyReceiver.kt
index be837509..d4148c86 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/DirectReplyReceiver.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/DirectReplyReceiver.kt
@@ -21,7 +21,7 @@ import com.simplemobiletools.smsmessenger.helpers.THREAD_NUMBER
class DirectReplyReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val address = intent.getStringExtra(THREAD_NUMBER)
- val threadId = intent.getIntExtra(THREAD_ID, 0)
+ val threadId = intent.getLongExtra(THREAD_ID, 0L)
val msg = RemoteInput.getResultsFromIntent(intent).getCharSequence(REPLY).toString()
val settings = Settings()
@@ -31,7 +31,7 @@ class DirectReplyReceiver : BroadcastReceiver() {
val message = com.klinker.android.send_message.Message(msg, address)
try {
- transaction.sendNewMessage(message, threadId.toLong())
+ transaction.sendNewMessage(message, threadId)
} catch (e: Exception) {
context.showErrorToast(e)
}
@@ -41,11 +41,11 @@ class DirectReplyReceiver : BroadcastReceiver() {
.setContentText(msg)
.build()
- context.notificationManager.notify(threadId, repliedNotification)
+ context.notificationManager.notify(threadId.hashCode(), repliedNotification)
ensureBackgroundThread {
context.markThreadMessagesRead(threadId)
- context.conversationsDB.markRead(threadId.toLong())
+ context.conversationsDB.markRead(threadId)
}
}
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/MarkAsReadReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/MarkAsReadReceiver.kt
index 3ef30ed4..4525f30d 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/MarkAsReadReceiver.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/MarkAsReadReceiver.kt
@@ -15,11 +15,11 @@ class MarkAsReadReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
MARK_AS_READ -> {
- val threadId = intent.getIntExtra(THREAD_ID, 0)
- context.notificationManager.cancel(threadId)
+ val threadId = intent.getLongExtra(THREAD_ID, 0L)
+ context.notificationManager.cancel(threadId.hashCode())
ensureBackgroundThread {
context.markThreadMessagesRead(threadId)
- context.conversationsDB.markRead(threadId.toLong())
+ context.conversationsDB.markRead(threadId)
context.updateUnreadCountBadge(context.conversationsDB.getUnreadConversations())
}
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/MmsReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/MmsReceiver.kt
index e8ae7d76..f19a826d 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/MmsReceiver.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/MmsReceiver.kt
@@ -24,7 +24,7 @@ class MmsReceiver : com.klinker.android.send_message.MmsReceivedReceiver() {
val glideBitmap = try {
Glide.with(context)
.asBitmap()
- .load(mms.attachment!!.attachments.first().uri)
+ .load(mms.attachment!!.attachments.first().getUri())
.centerCrop()
.into(size, size)
.get()
@@ -33,8 +33,8 @@ class MmsReceiver : com.klinker.android.send_message.MmsReceivedReceiver() {
}
Handler(Looper.getMainLooper()).post {
- context.showReceivedMessageNotification(address, mms.body, mms.thread, glideBitmap)
- val conversation = context.getConversations(mms.thread.toLong()).firstOrNull() ?: return@post
+ context.showReceivedMessageNotification(address, mms.body, mms.threadId, glideBitmap)
+ val conversation = context.getConversations(mms.threadId).firstOrNull() ?: return@post
ensureBackgroundThread {
context.conversationsDB.insertOrUpdate(conversation)
context.updateUnreadCountBadge(context.conversationsDB.getUnreadConversations())
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsReceiver.kt
index 37380eca..cc2efb88 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsReceiver.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsReceiver.kt
@@ -3,11 +3,15 @@ package com.simplemobiletools.smsmessenger.receivers
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
+import android.os.Handler
+import android.os.Looper
import android.provider.Telephony
import com.simplemobiletools.commons.extensions.isNumberBlocked
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
+import com.simplemobiletools.commons.models.SimpleContact
import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.refreshMessages
+import com.simplemobiletools.smsmessenger.models.Message
class SmsReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
@@ -21,23 +25,33 @@ class SmsReceiver : BroadcastReceiver() {
val read = 0
val subscriptionId = intent.getIntExtra("subscription", -1)
- messages.forEach {
- address = it.originatingAddress ?: ""
- subject = it.pseudoSubject
- body += it.messageBody
- date = Math.min(it.timestampMillis, System.currentTimeMillis())
- threadId = context.getThreadId(address)
- }
+ ensureBackgroundThread {
+ messages.forEach {
+ address = it.originatingAddress ?: ""
+ subject = it.pseudoSubject
+ body += it.messageBody
+ date = Math.min(it.timestampMillis, System.currentTimeMillis())
+ threadId = context.getThreadId(address)
+ }
- if (!context.isNumberBlocked(address)) {
- context.insertNewSMS(address, subject, body, date, read, threadId, type, subscriptionId)
- context.showReceivedMessageNotification(address, body, threadId.toInt(), null)
- refreshMessages()
+ Handler(Looper.getMainLooper()).post {
+ if (!context.isNumberBlocked(address)) {
+ ensureBackgroundThread {
+ val newMessageId = context.insertNewSMS(address, subject, body, date, read, threadId, type, subscriptionId)
- ensureBackgroundThread {
- val conversation = context.getConversations(threadId).firstOrNull() ?: return@ensureBackgroundThread
- context.conversationsDB.insertOrUpdate(conversation)
- context.updateUnreadCountBadge(context.conversationsDB.getUnreadConversations())
+ val conversation = context.getConversations(threadId).firstOrNull() ?: return@ensureBackgroundThread
+ context.conversationsDB.insertOrUpdate(conversation)
+ context.updateUnreadCountBadge(context.conversationsDB.getUnreadConversations())
+
+ val participant = SimpleContact(0, 0, address, "", arrayListOf(address), ArrayList(), ArrayList())
+ val message = Message(newMessageId, body, type, arrayListOf(participant), (date / 1000).toInt(), false, threadId,
+ false, null, address, "", subscriptionId)
+ context.messagesDB.insertOrUpdate(message)
+ }
+
+ context.showReceivedMessageNotification(address, body, threadId, null)
+ refreshMessages()
+ }
}
}
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsSentReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsSentReceiver.kt
deleted file mode 100644
index 2321c414..00000000
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsSentReceiver.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.simplemobiletools.smsmessenger.receivers
-
-import android.content.Context
-import android.content.Intent
-import com.klinker.android.send_message.SentReceiver
-import com.simplemobiletools.smsmessenger.helpers.refreshMessages
-
-class SmsSentReceiver : SentReceiver() {
- override fun onMessageStatusUpdated(context: Context, intent: Intent, receiverResultCode: Int) {
- refreshMessages()
- }
-}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsStatusDeliveredReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsStatusDeliveredReceiver.kt
new file mode 100644
index 00000000..b4c805e5
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsStatusDeliveredReceiver.kt
@@ -0,0 +1,37 @@
+package com.simplemobiletools.smsmessenger.receivers
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Handler
+import android.os.Looper
+import android.provider.Telephony
+import com.klinker.android.send_message.DeliveredReceiver
+import com.simplemobiletools.commons.helpers.ensureBackgroundThread
+import com.simplemobiletools.smsmessenger.extensions.messagesDB
+import com.simplemobiletools.smsmessenger.extensions.updateMessageType
+import com.simplemobiletools.smsmessenger.helpers.refreshMessages
+
+class SmsStatusDeliveredReceiver : DeliveredReceiver() {
+
+ override fun onMessageStatusUpdated(context: Context, intent: Intent, receiverResultCode: Int) {
+ if (intent.extras?.containsKey("message_uri") == true) {
+ val uri = Uri.parse(intent.getStringExtra("message_uri"))
+ val messageId = uri?.lastPathSegment?.toLong() ?: 0L
+ ensureBackgroundThread {
+ val type = Telephony.Sms.MESSAGE_TYPE_SENT
+ context.updateMessageType(messageId, type)
+ val updated = context.messagesDB.updateType(messageId, type)
+ if (updated == 0) {
+ Handler(Looper.getMainLooper()).postDelayed({
+ ensureBackgroundThread {
+ context.messagesDB.updateType(messageId, type)
+ }
+ }, 2000)
+ }
+
+ refreshMessages()
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsStatusSentReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsStatusSentReceiver.kt
new file mode 100644
index 00000000..0418c743
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsStatusSentReceiver.kt
@@ -0,0 +1,110 @@
+package com.simplemobiletools.smsmessenger.receivers
+
+import android.annotation.SuppressLint
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.media.AudioAttributes
+import android.media.AudioManager
+import android.media.RingtoneManager
+import android.net.Uri
+import android.os.Handler
+import android.os.Looper
+import android.provider.Telephony
+import androidx.core.app.NotificationCompat
+import com.klinker.android.send_message.SentReceiver
+import com.simplemobiletools.commons.extensions.getAdjustedPrimaryColor
+import com.simplemobiletools.commons.extensions.getMyContactsCursor
+import com.simplemobiletools.commons.helpers.SimpleContactsHelper
+import com.simplemobiletools.commons.helpers.ensureBackgroundThread
+import com.simplemobiletools.commons.helpers.isOreoPlus
+import com.simplemobiletools.smsmessenger.R
+import com.simplemobiletools.smsmessenger.activities.ThreadActivity
+import com.simplemobiletools.smsmessenger.extensions.*
+import com.simplemobiletools.smsmessenger.helpers.NOTIFICATION_CHANNEL
+import com.simplemobiletools.smsmessenger.helpers.THREAD_ID
+import com.simplemobiletools.smsmessenger.helpers.refreshMessages
+
+class SmsStatusSentReceiver : SentReceiver() {
+
+ override fun onMessageStatusUpdated(context: Context, intent: Intent, receiverResultCode: Int) {
+ if (intent.extras?.containsKey("message_uri") == true) {
+ val uri = Uri.parse(intent.getStringExtra("message_uri"))
+ val messageId = uri?.lastPathSegment?.toLong() ?: 0L
+ ensureBackgroundThread {
+ val type = if (intent.extras!!.containsKey("errorCode")) {
+ showSendingFailedNotification(context, messageId)
+ Telephony.Sms.MESSAGE_TYPE_FAILED
+ } else {
+ Telephony.Sms.MESSAGE_TYPE_OUTBOX
+ }
+ context.updateMessageType(messageId, type)
+ context.messagesDB.updateType(messageId, type)
+ refreshMessages()
+ }
+ }
+ }
+
+ private fun showSendingFailedNotification(context: Context, messageId: Long) {
+ Handler(Looper.getMainLooper()).post {
+ val privateCursor = context.getMyContactsCursor()?.loadInBackground()
+ ensureBackgroundThread {
+ val address = context.getMessageRecipientAddress(messageId)
+ val threadId = context.getThreadId(address)
+ val senderName = context.getNameFromAddress(address, privateCursor)
+ showNotification(context, senderName, threadId)
+ }
+ }
+ }
+
+ @SuppressLint("NewApi")
+ private fun showNotification(context: Context, recipientName: String, threadId: Long) {
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ val soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
+ if (isOreoPlus()) {
+ val audioAttributes = AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .setLegacyStreamType(AudioManager.STREAM_NOTIFICATION)
+ .build()
+
+ val name = context.getString(R.string.message_not_sent_short)
+ val importance = NotificationManager.IMPORTANCE_HIGH
+ NotificationChannel(NOTIFICATION_CHANNEL, name, importance).apply {
+ setBypassDnd(false)
+ enableLights(true)
+ setSound(soundUri, audioAttributes)
+ enableVibration(true)
+ notificationManager.createNotificationChannel(this)
+ }
+ }
+
+ val intent = Intent(context, ThreadActivity::class.java).apply {
+ putExtra(THREAD_ID, threadId)
+ }
+
+ val pendingIntent = PendingIntent.getActivity(context, threadId.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ val summaryText = String.format(context.getString(R.string.message_sending_error), recipientName)
+
+ val largeIcon = SimpleContactsHelper(context).getContactLetterIcon(recipientName)
+ val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL)
+ .setContentTitle(context.getString(R.string.message_not_sent_short))
+ .setContentText(summaryText)
+ .setColor(context.getAdjustedPrimaryColor())
+ .setSmallIcon(R.drawable.ic_messenger)
+ .setLargeIcon(largeIcon)
+ .setStyle(NotificationCompat.BigTextStyle().bigText(summaryText))
+ .setContentIntent(pendingIntent)
+ .setPriority(NotificationCompat.PRIORITY_MAX)
+ .setDefaults(Notification.DEFAULT_LIGHTS)
+ .setCategory(Notification.CATEGORY_MESSAGE)
+ .setAutoCancel(true)
+ .setSound(soundUri, AudioManager.STREAM_NOTIFICATION)
+ .setChannelId(NOTIFICATION_CHANNEL)
+
+ notificationManager.notify(threadId.hashCode(), builder.build())
+ }
+}
diff --git a/app/src/main/res/drawable/item_selected_contact_background.xml b/app/src/main/res/drawable/item_selected_contact_background.xml
index f55fbc8f..f0d2b728 100644
--- a/app/src/main/res/drawable/item_selected_contact_background.xml
+++ b/app/src/main/res/drawable/item_selected_contact_background.xml
@@ -1,9 +1,12 @@
-
+
+ -
+
-
+
-
+
-
+
+
+
diff --git a/app/src/main/res/layout/activity_new_conversation.xml b/app/src/main/res/layout/activity_new_conversation.xml
index f76e807f..aaf95c05 100644
--- a/app/src/main/res/layout/activity_new_conversation.xml
+++ b/app/src/main/res/layout/activity_new_conversation.xml
@@ -42,7 +42,7 @@
android:id="@+id/no_contacts_placeholder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_below="@+id/message_divider_two"
+ android:layout_below="@+id/suggestions_scrollview"
android:layout_centerHorizontal="true"
android:layout_marginTop="@dimen/bigger_margin"
android:alpha="0.8"
diff --git a/app/src/main/res/layout/activity_search.xml b/app/src/main/res/layout/activity_search.xml
new file mode 100644
index 00000000..a8e2fe2d
--- /dev/null
+++ b/app/src/main/res/layout/activity_search.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml
index 618b9b70..25fd8250 100644
--- a/app/src/main/res/layout/activity_settings.xml
+++ b/app/src/main/res/layout/activity_settings.xml
@@ -53,6 +53,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_thread.xml b/app/src/main/res/layout/activity_thread.xml
index 33ec03ed..240c0c39 100644
--- a/app/src/main/res/layout/activity_thread.xml
+++ b/app/src/main/res/layout/activity_thread.xml
@@ -66,6 +66,7 @@
android:layout_marginStart="@dimen/medium_margin"
android:layout_marginEnd="@dimen/medium_margin"
android:background="?selectableItemBackgroundBorderless"
+ android:contentDescription="@string/confirm_selection"
android:paddingStart="@dimen/medium_margin"
android:paddingEnd="@dimen/medium_margin"
android:src="@drawable/ic_check_vector"
@@ -125,6 +126,7 @@
android:layout_marginEnd="@dimen/small_margin"
android:alpha="0.9"
android:background="?selectableItemBackgroundBorderless"
+ android:contentDescription="@string/attachment"
android:padding="@dimen/normal_margin"
android:src="@drawable/ic_plus_vector" />
@@ -166,14 +168,17 @@
@@ -190,6 +195,21 @@
android:visibility="gone"
tools:text="1" />
+
+
diff --git a/app/src/main/res/layout/dialog_select_text.xml b/app/src/main/res/layout/dialog_select_text.xml
new file mode 100644
index 00000000..71f9cba7
--- /dev/null
+++ b/app/src/main/res/layout/dialog_select_text.xml
@@ -0,0 +1,12 @@
+
+
diff --git a/app/src/main/res/layout/item_received_message.xml b/app/src/main/res/layout/item_received_message.xml
index 7d476239..c36e4b8c 100644
--- a/app/src/main/res/layout/item_received_message.xml
+++ b/app/src/main/res/layout/item_received_message.xml
@@ -18,7 +18,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintWidth_percent="0.7">
+ app:layout_constraintWidth_percent="0.8">
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_sent_message.xml b/app/src/main/res/layout/item_sent_message.xml
index 0a420c0c..3af7a076 100644
--- a/app/src/main/res/layout/item_sent_message.xml
+++ b/app/src/main/res/layout/item_sent_message.xml
@@ -18,7 +18,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintWidth_percent="0.7">
+ app:layout_constraintWidth_percent="0.8">
+ android:layout_height="wrap_content"
+ android:background="?selectableItemBackground">
diff --git a/app/src/main/res/layout/item_thread_sending.xml b/app/src/main/res/layout/item_thread_sending.xml
new file mode 100644
index 00000000..b89f4f6a
--- /dev/null
+++ b/app/src/main/res/layout/item_thread_sending.xml
@@ -0,0 +1,10 @@
+
+
diff --git a/app/src/main/res/menu/cab_conversations.xml b/app/src/main/res/menu/cab_conversations.xml
index 84e02dc5..19f7a611 100644
--- a/app/src/main/res/menu/cab_conversations.xml
+++ b/app/src/main/res/menu/cab_conversations.xml
@@ -1,6 +1,11 @@
diff --git a/app/src/main/res/menu/cab_thread.xml b/app/src/main/res/menu/cab_thread.xml
index ab250efa..ed5b8a85 100644
--- a/app/src/main/res/menu/cab_thread.xml
+++ b/app/src/main/res/menu/cab_thread.xml
@@ -12,13 +12,16 @@
android:title="@string/share"
app:showAsAction="ifRoom" />
+ android:id="@+id/cab_select_text"
+ android:title="@string/select_text"
+ app:showAsAction="never" />
+
diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml
index a0c32233..adda1b70 100644
--- a/app/src/main/res/menu/menu_main.xml
+++ b/app/src/main/res/menu/menu_main.xml
@@ -1,6 +1,11 @@