Merge pull request #6 from SimpleMobileTools/master

upd
This commit is contained in:
solokot 2021-01-01 16:16:29 +03:00 committed by GitHub
commit cdb135dfbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 889 additions and 216 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,3 @@
github: [tibbi]
patreon: tiborkaputa
custom: ["https://www.paypal.com/paypalme/simplemobiletools", "https://www.simplemobiletools.com/donate"]

View File

@ -1,6 +1,37 @@
Changelog
==========
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)*
----------------------------

View File

@ -16,8 +16,8 @@ android {
applicationId "com.simplemobiletools.smsmessenger"
minSdkVersion 22
targetSdkVersion 30
versionCode 18
versionName "5.5.0"
versionCode 23
versionName "5.7.0"
setProperty("archivesBaseName", "sms-messenger")
}
@ -56,13 +56,14 @@ android {
}
dependencies {
implementation 'com.simplemobiletools:commons:5.31.23'
implementation 'com.simplemobiletools:commons:5.32.19'
implementation 'org.greenrobot:eventbus:3.2.0'
implementation 'com.klinkerapps:android-smsmms:5.2.6'
implementation 'com.github.tibbi:IndicatorFastScroll:08f512858a'
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"
}

View File

@ -18,10 +18,7 @@ import com.simplemobiletools.commons.models.FAQItem
import com.simplemobiletools.smsmessenger.BuildConfig
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.adapters.ConversationsAdapter
import com.simplemobiletools.smsmessenger.extensions.config
import com.simplemobiletools.smsmessenger.extensions.conversationsDB
import com.simplemobiletools.smsmessenger.extensions.getConversations
import com.simplemobiletools.smsmessenger.extensions.updateUnreadCountBadge
import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.THREAD_ID
import com.simplemobiletools.smsmessenger.helpers.THREAD_TITLE
import com.simplemobiletools.smsmessenger.models.Conversation
@ -86,6 +83,8 @@ class MainActivity : SimpleActivity() {
updateTextColors(main_coordinator)
no_conversations_placeholder_2.setTextColor(getAdjustedPrimaryColor())
no_conversations_placeholder_2.underlineText()
conversations_fastscroller.updatePrimaryColor()
conversations_fastscroller.updateBubbleColors()
checkShortcut()
}
@ -183,46 +182,43 @@ class MainActivity : SimpleActivity() {
}
private fun getNewConversations(cachedConversations: ArrayList<Conversation>) {
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())
}
}
}
}
}
@ -232,17 +228,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)

View File

@ -8,10 +8,7 @@ import android.view.WindowManager
import com.google.gson.Gson
import com.reddit.indicatorfastscroll.FastScrollItemIndicator
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.SimpleContact
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.adapters.ContactsAdapter
@ -144,7 +141,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)
@ -194,7 +191,7 @@ class NewConversationActivity : SimpleActivity() {
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(numbers).toInt())
putExtra(THREAD_ID, getThreadId(numbers))
putExtra(THREAD_TITLE, name)
putExtra(THREAD_TEXT, text)
putExtra(THREAD_NUMBER, number)

View File

@ -53,15 +53,16 @@ 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()
}
}

View File

@ -50,7 +50,7 @@ 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 threadItems = ArrayList<ThreadItem>()
@ -72,7 +72,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
}
@ -81,7 +81,10 @@ class ThreadActivity : SimpleActivity() {
bus!!.register(this)
handlePermission(PERMISSION_READ_PHONE_STATE) {
if (it) {
setupThread()
setupButtons()
setupCachedMessages {
setupThread()
}
} else {
finish()
}
@ -99,15 +102,16 @@ class ThreadActivity : SimpleActivity() {
}
private fun setupThread() {
val privateCursor = getMyContactsCursor().loadInBackground()
val privateCursor = getMyContactsCursor()?.loadInBackground()
ensureBackgroundThread {
val cachedMessagesCode = messages.hashCode()
messages = getMessages(threadId)
participants = if (messages.isEmpty()) {
getThreadParticipants(threadId, null)
} else {
messages.first().participants
if (messages.hashCode() == cachedMessagesCode) {
return@ensureBackgroundThread
}
setupParticipants()
// check if no participant came from a privately stored contact in Simple Contacts
privateContacts = MyContactsContentProvider.getSimpleContacts(this, privateCursor)
if (privateContacts.isNotEmpty()) {
@ -140,50 +144,17 @@ class ThreadActivity : SimpleActivity() {
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) {
}
}
messages.chunked(30).forEach { currentMessages ->
messagesDB.insertMessages(*currentMessages.toTypedArray())
}
setupAttachmentSizes()
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()
}
setupThreadTitle()
setupSIMSelector()
}
}
setupButtons()
}
override fun onDestroy() {
@ -224,6 +195,25 @@ class ThreadActivity : SimpleActivity() {
}
}
private fun setupCachedMessages(callback: () -> Unit) {
ensureBackgroundThread {
messages = messagesDB.getThreadMessages(threadId).toMutableList() as ArrayList<Message>
setupParticipants()
setupAdapter()
runOnUiThread {
if (messages.isEmpty()) {
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
thread_type_message.requestFocus()
}
setupThreadTitle()
setupSIMSelector()
callback()
}
}
}
private fun setupAdapter() {
threadItems = getThreadItems()
invalidateOptionsMenu()
@ -290,7 +280,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)
@ -315,6 +305,53 @@ 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
@ -335,6 +372,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)
@ -430,7 +471,7 @@ class ThreadActivity : SimpleActivity() {
private fun markAsUnread() {
ensureBackgroundThread {
conversationsDB.markUnread(threadId.toLong())
conversationsDB.markUnread(threadId)
markThreadMessagesUnread(threadId)
runOnUiThread {
finish()
@ -474,7 +515,7 @@ class ThreadActivity : SimpleActivity() {
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) {
@ -568,6 +609,7 @@ class ThreadActivity : SimpleActivity() {
val settings = Settings()
settings.useSystemSending = true
settings.deliveryReports = true
val SIMId = availableSIMCards.getOrNull(currentSIMCardIndex)?.subscriptionId
if (SIMId != null) {
@ -588,12 +630,14 @@ 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())
transaction.sendNewMessage(message, threadId)
thread_type_message.setText("")
attachmentUris.clear()
@ -601,6 +645,8 @@ class ThreadActivity : SimpleActivity() {
thread_attachments_wrapper.removeAllViews()
} catch (e: Exception) {
showErrorToast(e)
} catch (e: Error) {
toast(e.localizedMessage ?: getString(R.string.unknown_error_occurred))
}
}
@ -661,10 +707,16 @@ class ThreadActivity : SimpleActivity() {
@Subscribe(threadMode = ThreadMode.ASYNC)
fun refreshMessages(event: Events.RefreshMessages) {
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 {
messagesDB.insertOrIgnore(it)
}
setupAdapter()
}
}

View File

@ -63,9 +63,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() {}
@ -153,11 +153,11 @@ class ConversationsAdapter(activity: SimpleActivity, var conversations: ArrayLis
return
}
val conversationsToRemove = conversations.filter { selectedKeys.contains(it.thread_id) } as ArrayList<Conversation>
val conversationsToRemove = conversations.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
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 {
@ -193,7 +193,7 @@ class ConversationsAdapter(activity: SimpleActivity, var conversations: ArrayLis
}
}
private fun getSelectedItems() = conversations.filter { selectedKeys.contains(it.thread_id) } as ArrayList<Conversation>
private fun getSelectedItems() = conversations.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
override fun onViewRecycled(holder: ViewHolder) {
super.onViewRecycled(holder)
@ -218,7 +218,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

View File

@ -81,9 +81,9 @@ class ThreadAdapter(activity: SimpleActivity, var messages: ArrayList<ThreadItem
override fun getIsItemSelectable(position: Int) = !isThreadDateTime(position)
override fun getItemSelectionKey(position: Int) = (messages.getOrNull(position) as? Message)?.id
override fun getItemSelectionKey(position: Int) = (messages.getOrNull(position) as? Message)?.hashCode()
override fun getItemKeyPosition(key: Int) = messages.indexOfFirst { (it as? Message)?.id == key }
override fun getItemKeyPosition(key: Int) = messages.indexOfFirst { (it as? Message)?.hashCode() == key }
override fun onActionModeCreated() {}
@ -145,7 +145,14 @@ class ThreadAdapter(activity: SimpleActivity, var messages: ArrayList<ThreadItem
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)
@ -162,7 +169,7 @@ class ThreadAdapter(activity: SimpleActivity, var messages: ArrayList<ThreadItem
return
}
val messagesToRemove = messages.filter { selectedKeys.contains((it as? Message)?.id ?: 0) } as ArrayList<ThreadItem>
val messagesToRemove = getSelectedItems()
val positions = getSelectedItemPositions()
messagesToRemove.forEach {
activity.deleteMessage((it as Message).id, it.isMMS)
@ -179,7 +186,7 @@ class ThreadAdapter(activity: SimpleActivity, var messages: ArrayList<ThreadItem
}
}
private fun getSelectedItems() = messages.filter { selectedKeys.contains((it as? Message)?.id ?: 0) } as ArrayList<ThreadItem>
private fun getSelectedItems() = messages.filter { selectedKeys.contains((it as? Message)?.hashCode() ?: 0) } as ArrayList<ThreadItem>
private fun isThreadDateTime(position: Int) = messages.getOrNull(position) is ThreadDateTime
@ -192,7 +199,7 @@ class ThreadAdapter(activity: SimpleActivity, var messages: ArrayList<ThreadItem
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)
@ -218,7 +225,7 @@ class ThreadAdapter(activity: SimpleActivity, var messages: ArrayList<ThreadItem
if (message.attachment?.attachments?.isNotEmpty() == true) {
for (attachment in message.attachment.attachments) {
val mimetype = attachment.mimetype
val uri = attachment.uri
val uri = attachment.getUri()
if (mimetype.startsWith("image/") || mimetype.startsWith("video/")) {
val imageView = layoutInflater.inflate(R.layout.item_attachment_image, null)
thread_mesage_attachments_holder.addView(imageView)

View File

@ -4,14 +4,31 @@ import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.simplemobiletools.smsmessenger.helpers.Converters
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.Attachment
import com.simplemobiletools.smsmessenger.models.Conversation
import com.simplemobiletools.smsmessenger.models.Message
import com.simplemobiletools.smsmessenger.models.MessageAttachment
@Database(entities = [(Conversation::class)], version = 1)
@Database(entities = [Conversation::class, Attachment::class, MessageAttachment::class, Message::class], version = 3)
@TypeConverters(Converters::class)
abstract class MessagesDatabase : RoomDatabase() {
abstract fun ConversationsDao(): ConversationsDao
abstract fun AttachmentsDao(): AttachmentsDao
abstract fun MessageAttachmentsDao(): MessageAttachmentsDao
abstract fun MessagesDao(): MessagesDao
companion object {
private var db: MessagesDatabase? = null
@ -20,11 +37,44 @@ abstract class MessagesDatabase : RoomDatabase() {
synchronized(MessagesDatabase::class) {
if (db == null) {
db = Room.databaseBuilder(context.applicationContext, MessagesDatabase::class.java, "conversations.db")
.fallbackToDestructiveMigration()
.addMigrations(MIGRATION_1_2)
.addMigrations(MIGRATION_2_3)
.build()
}
}
}
return db!!
}
private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.apply {
execSQL("CREATE TABLE IF NOT EXISTS `messages` (`id` INTEGER PRIMARY KEY NOT NULL, `body` TEXT NOT NULL, `type` INTEGER NOT NULL, `participants` TEXT NOT NULL, `date` INTEGER NOT NULL, `read` INTEGER NOT NULL, `thread_id` INTEGER NOT NULL, `is_mms` INTEGER NOT NULL, `attachment` TEXT, `sender_name` TEXT NOT NULL, `sender_photo_uri` TEXT NOT NULL, `subscription_id` INTEGER NOT NULL)")
execSQL("CREATE TABLE IF NOT EXISTS `message_attachments` (`id` INTEGER PRIMARY KEY NOT NULL, `text` TEXT NOT NULL, `attachments` TEXT NOT NULL)")
execSQL("CREATE TABLE IF NOT EXISTS `attachments` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `message_id` INTEGER NOT NULL, `uri_string` TEXT NOT NULL, `mimetype` TEXT NOT NULL, `width` INTEGER NOT NULL, `height` INTEGER NOT NULL, `filename` TEXT NOT NULL)")
execSQL("CREATE UNIQUE INDEX `index_attachments_message_id` ON `attachments` (`message_id`)")
}
}
}
private val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.apply {
execSQL("CREATE TABLE conversations_new (`thread_id` INTEGER NOT NULL PRIMARY KEY, `snippet` TEXT NOT NULL, `date` INTEGER NOT NULL, `read` INTEGER NOT NULL, `title` TEXT NOT NULL, `photo_uri` TEXT NOT NULL, `is_group_conversation` INTEGER NOT NULL, `phone_number` TEXT NOT NULL)")
execSQL("INSERT OR IGNORE INTO conversations_new (thread_id, snippet, date, read, title, photo_uri, is_group_conversation, phone_number) " +
"SELECT thread_id, snippet, date, read, title, photo_uri, is_group_conversation, phone_number FROM conversations")
execSQL("DROP TABLE conversations")
execSQL("ALTER TABLE conversations_new RENAME TO conversations")
execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_conversations_id` ON `conversations` (`thread_id`)")
}
}
}
}
}

View File

@ -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<SimpleContact>.getThreadTitle() = TextUtils.join(", ", map { it.name }.toTypedArray())
fun ArrayList<Conversation>.getHashToCompare() = map { it.getStringToCompare() }.hashCode()

View File

@ -27,7 +27,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 +44,13 @@ fun Context.getMessagessDB() = MessagesDatabase.getInstance(this)
val Context.conversationsDB: ConversationsDao get() = getMessagessDB().ConversationsDao()
fun Context.getMessages(threadId: Int): ArrayList<Message> {
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<Message> {
val uri = Sms.CONTENT_URI
val projection = arrayOf(
Sms._ID,
@ -76,15 +85,15 @@ fun Context.getMessages(threadId: Int): ArrayList<Message> {
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), ArrayList(), ArrayList())
val isMMS = false
@ -100,7 +109,7 @@ fun Context.getMessages(threadId: Int): ArrayList<Message> {
}
// 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<Message> {
fun Context.getMMS(threadId: Long? = null, sortOrder: String? = null): ArrayList<Message> {
val uri = Mms.CONTENT_URI
val projection = arrayOf(
Mms._ID,
@ -125,13 +134,13 @@ fun Context.getMMS(threadId: Int? = null, sortOrder: String? = null): ArrayList<
val messages = ArrayList<Message>()
val contactsMap = HashMap<Int, SimpleContact>()
val threadParticipants = HashMap<Int, ArrayList<SimpleContact>>()
val threadParticipants = HashMap<Long, ArrayList<SimpleContact>>()
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 +152,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 +174,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 +192,7 @@ fun Context.getMMSSender(msgId: Int): String {
return ""
}
fun Context.getConversations(threadId: Long? = null): ArrayList<Conversation> {
fun Context.getConversations(threadId: Long? = null, privateContacts: ArrayList<SimpleContact> = ArrayList()): ArrayList<Conversation> {
val uri = Uri.parse("${Threads.CONTENT_URI}?simple=true")
val projection = arrayOf(
Threads._ID,
@ -208,7 +215,7 @@ fun Context.getConversations(threadId: Long? = null): ArrayList<Conversation> {
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 +233,12 @@ fun Context.getConversations(threadId: Long? = null): ArrayList<Conversation> {
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 +248,7 @@ fun Context.getConversations(threadId: Long? = null): ArrayList<Conversation> {
// 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 +266,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 +293,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 +320,7 @@ fun Context.getThreadSnippet(threadId: Int): String {
return snippet
}
fun Context.getThreadParticipants(threadId: Int, contactsMap: HashMap<Int, SimpleContact>?): ArrayList<SimpleContact> {
fun Context.getThreadParticipants(threadId: Long, contactsMap: HashMap<Int, SimpleContact>?): ArrayList<SimpleContact> {
val uri = Uri.parse("${MmsSms.CONTENT_CONVERSATIONS_URI}?simple=true")
val projection = arrayOf(
ThreadsColumns.RECIPIENT_IDS
@ -335,8 +342,8 @@ fun Context.getThreadParticipants(threadId: Int, contactsMap: HashMap<Int, Simpl
val phoneNumber = getPhoneNumberFromAddressId(addressId)
val namePhoto = getNameAndPhotoFromPhoneNumber(phoneNumber)
val name = namePhoto?.name ?: ""
val photoUri = namePhoto?.photoUri ?: ""
val name = namePhoto.name
val photoUri = namePhoto.photoUri ?: ""
val contact = SimpleContact(addressId, addressId, name, photoUri, arrayListOf(phoneNumber), ArrayList(), ArrayList())
participants.add(contact)
}
@ -356,10 +363,20 @@ fun Context.getThreadPhoneNumbers(recipientIds: List<Int>): ArrayList<String> {
return numbers
}
fun Context.getThreadContactNames(phoneNumbers: List<String>): ArrayList<String> {
fun Context.getThreadContactNames(phoneNumbers: List<String>, privateContacts: ArrayList<SimpleContact>): ArrayList<String> {
val names = ArrayList<String>()
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 +417,9 @@ fun Context.getSuggestedContacts(privateContacts: ArrayList<SimpleContact>): 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()) {
@ -427,7 +444,7 @@ fun Context.getSuggestedContacts(privateContacts: ArrayList<SimpleContact>): 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 +471,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,10 +485,10 @@ 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())
@ -484,17 +501,23 @@ fun Context.deleteConversation(threadId: Int) {
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)
@ -503,9 +526,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)
@ -515,9 +539,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)
@ -564,23 +589,23 @@ fun Context.getThreadId(addresses: Set<String>): 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 ?: ""
var sender = getNameAndPhotoFromPhoneNumber(address).name
if (address == sender) {
val privateContacts = MyContactsContentProvider.getSimpleContacts(this, privateCursor)
sender = privateContacts.firstOrNull { it.doesContainPhoneNumber(address) }?.name ?: address
}
Handler(Looper.getMainLooper()).post {
showMessageNotification(address, body, threadID, bitmap, sender)
showMessageNotification(address, body, threadId, bitmap, 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()) {
@ -602,14 +627,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)
@ -622,11 +647,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()
@ -652,5 +677,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())
}

View File

@ -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<List<Attachment>>() {}.type
private val simpleContactType = object : TypeToken<List<SimpleContact>>() {}.type
private val messageAttachmentType = object : TypeToken<MessageAttachment?>() {}.type
@TypeConverter
fun jsonToAttachmentList(value: String) = gson.fromJson<ArrayList<Attachment>>(value, attachmentType)
@TypeConverter
fun attachmentListToJson(list: ArrayList<Attachment>) = gson.toJson(list)
@TypeConverter
fun jsonToSimpleContactList(value: String) = gson.fromJson<ArrayList<SimpleContact>>(value, simpleContactType)
@TypeConverter
fun simpleContactListToJson(list: ArrayList<SimpleContact>) = gson.toJson(list)
@TypeConverter
fun jsonToMessageAttachment(value: String) = gson.fromJson<MessageAttachment>(value, messageAttachmentType)
@TypeConverter
fun messageAttachmentToJson(messageAttachment: MessageAttachment?) = gson.toJson(messageAttachment)
}

View File

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

View File

@ -23,9 +23,6 @@ interface ConversationsDao {
@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)
}

View File

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

View File

@ -0,0 +1,37 @@
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<Message>
@Query("SELECT * FROM messages WHERE thread_id = :threadId")
fun getThreadMessages(threadId: Long): List<Message>
@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("DELETE FROM messages WHERE id = :id")
fun delete(id: Long)
@Query("DELETE FROM messages WHERE thread_id = :threadId")
fun deleteThreadMessages(threadId: Long)
}

View File

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

View File

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

View File

@ -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<SimpleContact>, 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<SimpleContact>,
@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") val subscriptionId: Int) : ThreadItem() {
fun isReceivedMessage() = type == Telephony.Sms.MESSAGE_TYPE_INBOX
}

View File

@ -1,3 +1,11 @@
package com.simplemobiletools.smsmessenger.models
data class MessageAttachment(val id: Int, var text: String, var attachments: ArrayList<Attachment>)
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<Attachment>)

View File

@ -1,3 +1,3 @@
package com.simplemobiletools.smsmessenger.models
data class ThreadError(val messageID: Int) : ThreadItem()
data class ThreadError(val messageID: Long) : ThreadItem()

View File

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

View File

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

View File

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

View File

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

View File

@ -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,25 +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)
}
if (!context.isNumberBlocked(address)) {
ensureBackgroundThread {
context.insertNewSMS(address, subject, body, date, read, threadId, type, subscriptionId)
val conversation = context.getConversations(threadId).firstOrNull() ?: return@ensureBackgroundThread
context.conversationsDB.insertOrUpdate(conversation)
context.updateUnreadCountBadge(context.conversationsDB.getUnreadConversations())
ensureBackgroundThread {
messages.forEach {
address = it.originatingAddress ?: ""
subject = it.pseudoSubject
body += it.messageBody
date = Math.min(it.timestampMillis, System.currentTimeMillis())
threadId = context.getThreadId(address)
}
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)
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()
}
}
}
}
}

View File

@ -1,6 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/cab_delete"
android:icon="@drawable/ic_delete_vector"
android:title="@string/delete"
app:showAsAction="ifRoom" />
<item
android:id="@+id/cab_add_number_to_contact"
android:icon="@drawable/ic_add_person_vector"
@ -20,9 +25,4 @@
android:id="@+id/cab_copy_number"
android:title="@string/copy_number_to_clipboard"
app:showAsAction="never" />
<item
android:id="@+id/cab_delete"
android:icon="@drawable/ic_delete_vector"
android:title="@string/delete"
app:showAsAction="ifRoom" />
</menu>

View File

@ -1,7 +1,7 @@
<resources>
<string name="app_name">Schlichter SMS Messenger</string>
<string name="app_launcher_name">SMS Messenger</string>
<string name="type_a_message">schreibe eine Nachricht…</string>
<string name="type_a_message">Schreibe eine Nachricht…</string>
<string name="message_not_sent">Nachricht wurde nicht gesendet.</string>
<string name="add_person">Person hinzufügen</string>
<string name="attachment">Anhang</string>
@ -9,6 +9,10 @@
<string name="start_conversation">Neuen Chat beginnen</string>
<string name="reply">Antworten</string>
<string name="show_character_counter">Show a character counter at writing messages</string>
<string name="loading_messages">Loading messages…</string>
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<!-- New conversation -->
<string name="new_conversation">Neuer Chat</string>

View File

@ -8,7 +8,11 @@
<string name="no_conversations_found">Δεν βρέθηκαν αποθηκευμένες συνομιλίες</string>
<string name="start_conversation">Έναρξη συνομιλίας</string>
<string name="reply">Απάντηση</string>
<string name="show_character_counter">Show a character counter at writing messages</string>
<string name="show_character_counter">Εμφάνιση μετρητή χαρακτήρων κατά την πληκτρολόγηση μηνυμάτων</string>
<string name="loading_messages">Φόρτωση μηνυμάτων…</string>
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<!-- New conversation -->
<string name="new_conversation">Νέα συνομιλία</string>

View File

@ -8,7 +8,11 @@
<string name="no_conversations_found">No se han encontrado conversaciones</string>
<string name="start_conversation">Inicia una conversación</string>
<string name="reply">Responder</string>
<string name="show_character_counter">Show a character counter at writing messages</string>
<string name="show_character_counter">Mostrar un contador de caracteres al escribir mensajes</string>
<string name="loading_messages">Loading messages…</string>
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<!-- New conversation -->
<string name="new_conversation">Nueva conversación</string>

View File

@ -1,7 +1,7 @@
<resources>
<string name="app_name">Simple SMS Messenger</string>
<string name="app_launcher_name">SMS Messenger</string>
<string name="type_a_message">Écrivez un message...</string>
<string name="type_a_message">Écrivez un message</string>
<string name="message_not_sent">Le message n\'a pas été envoyé.</string>
<string name="add_person">Ajouter une personne</string>
<string name="attachment">Pièce jointe</string>
@ -9,10 +9,14 @@
<string name="start_conversation">Commencer une conversation</string>
<string name="reply">Répondre</string>
<string name="show_character_counter">Show a character counter at writing messages</string>
<string name="loading_messages">Loading messages…</string>
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<!-- New conversation -->
<string name="new_conversation">Nouvelle conversation</string>
<string name="add_contact_or_number">Ajouter un contact ou un numéro...</string>
<string name="add_contact_or_number">Ajouter un contact ou un numéro</string>
<string name="suggestions">Suggestions</string>
<!-- Notifications -->

View File

@ -0,0 +1,78 @@
<resources>
<string name="app_name">Simple SMS Messenger</string>
<string name="app_launcher_name">SMS Messenger</string>
<string name="type_a_message">Ketik pesan…</string>
<string name="message_not_sent">Pesan belum terkirim.</string>
<string name="add_person">Tambahkan Orang</string>
<string name="attachment">Lampiran</string>
<string name="no_conversations_found">Tidak ada percakapan tersimpan yang ditemukan</string>
<string name="start_conversation">Mulailah percakapan</string>
<string name="reply">Balas</string>
<string name="show_character_counter">Tunjukkan penghitung karakter saat menulis pesan</string>
<string name="loading_messages">Loading messages…</string>
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<!-- New conversation -->
<string name="new_conversation">Percakapan baru</string>
<string name="add_contact_or_number">Tambahkan Kontak atau Nomor…</string>
<string name="suggestions">Saran</string>
<!-- Notifications -->
<string name="channel_received_sms">Menerima SMS</string>
<string name="new_message">Pesan baru</string>
<string name="mark_as_read">Tandai sebagai Dibaca</string>
<string name="mark_as_unread">Tandai sebagai Belum dibaca</string>
<!-- Confirmation dialog -->
<string name="delete_whole_conversation_confirmation">Apakah Anda yakin ingin menghapus semua pesan dari percakapan ini?</string>
<!-- Are you sure you want to delete 5 conversations? -->
<plurals name="delete_conversations">
<item quantity="one">%d conversation</item>
<item quantity="other">%d conversations</item>
</plurals>
<!-- Are you sure you want to delete 5 messages? -->
<plurals name="delete_messages">
<item quantity="one">%d pesan</item>
<item quantity="other">%d pesan</item>
</plurals>
<!-- FAQ -->
<string name="faq_1_title">Mengapa aplikasi membutuhkan akses ke internet?</string>
<string name="faq_1_text">Sayangnya itu diperlukan untuk mengirim lampiran MMS. Tidak dapat mengirim MMS akan menjadi kerugian yang sangat besar dibandingkan dengan aplikasi lain, jadi kami memutuskan untuk menggunakan cara ini.
Namun, seperti biasanya, tidak ada iklan, pelacakan atau analitik apa pun, internet hanya digunakan untuk mengirim MMS.</string>
<!-- Strings displayed only on Google Playstore. Optional, but good to have -->
<!-- App title has to have less than 50 characters. If you cannot squeeze it, just remove a part of it -->
<string name="app_title">Simple SMS Messenger - Manage messages easily</string>
<!-- Short description has to have max 80 characters -->
<string name="app_short_description">An easy and quick way of managing SMS and MMS messages without ads.</string>
<string name="app_long_description">
A great way to stay in touch with your relatives, by sending both SMS and MMS messages. The app properly handles group messaging too, just like blocking numbers from Android 7+.
It offers many date formats to choose from, to make you feel comfortable at using it. You can toggle between 12 and 24 hours time format too.
It has a really tiny app size compared to the competition, making it really fast to download.
It comes with material design and dark theme by default, provides great user experience for easy usage. The lack of internet access gives you more privacy, security and stability than other apps.
Contains no ads or unnecessary permissions. It is fully opensource, provides customizable colors.
<b>Check out the full suite of Simple Tools here:</b>
https://www.simplemobiletools.com
<b>Facebook:</b>
https://www.facebook.com/simplemobiletools
<b>Reddit:</b>
https://www.reddit.com/r/SimpleMobileTools
</string>
<!--
Haven't found some strings? There's more at
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
-->
</resources>

View File

@ -0,0 +1,76 @@
<resources>
<string name="app_name">Simple ショートメール</string>
<string name="app_launcher_name">ショートメール</string>
<string name="type_a_message">メッセージを入力してください…</string>
<string name="message_not_sent">送信されませんでした。</string>
<string name="add_person">宛先を追加</string>
<string name="attachment">添付</string>
<string name="no_conversations_found">保存された会話はありません</string>
<string name="start_conversation">会話を始める</string>
<string name="reply">返信</string>
<string name="show_character_counter">メッセージ入力中に文字カウントを表示する</string>
<string name="loading_messages">Loading messages…</string>
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<!-- New conversation -->
<string name="new_conversation">新しい会話</string>
<string name="add_contact_or_number">連絡先や電話番号を追加する…</string>
<string name="suggestions">おすすめ</string>
<!-- Notifications -->
<string name="channel_received_sms">受信したショートメール</string>
<string name="new_message">新しいメッセージ</string>
<string name="mark_as_read">既読にする</string>
<string name="mark_as_unread">未読にする</string>
<!-- Confirmation dialog -->
<string name="delete_whole_conversation_confirmation">本当にこの会話の全てのメッセージを削除しますか?</string>
<!-- Are you sure you want to delete 5 conversations? -->
<plurals name="delete_conversations">
<item quantity="one">%d件の会話</item>
<item quantity="other">%d件の会話</item>
</plurals>
<!-- Are you sure you want to delete 5 messages? -->
<plurals name="delete_messages">
<item quantity="one">%件のメッセージ</item>
<item quantity="other">%件のメッセージ</item>
</plurals>
<!-- FAQ -->
<string name="faq_1_title">なぜアプリ使用にインターネットへのアクセスが必要なのですか?</string>
<string name="faq_1_text">生憎、MMS(マルチメディアメッセージサービス)にインターネットが必要となります。他のアプリと比較して、MMSを使用出来ないと大きな損になるので、こうすることに決めました。
ただし、通常通り広告・追跡・分析は一切行われず、MMS送信にのみインターネットが使われます。</string>
<!-- Strings displayed only on Google Playstore. Optional, but good to have -->
<!-- App title has to have less than 50 characters. If you cannot squeeze it, just remove a part of it -->
<string name="app_title">シンプルなSMSメッセンジャー-メッセージを簡単に管理</string>
<!-- Short description has to have max 80 characters -->
<string name="app_short_description">SMS、MMSメッセージをすばやく送信。広告なしのきれいで美しいカスタマイズ可能なインターフェイス</string>
<string name="app_long_description">
SMSやMMSメッセージは親戚と連絡を取るのに便利です。グループメッセージも可能で、アンドロイド7以降のように連絡先をブロックすることも出来ます。
日付フォーマットも複数から使いやすいものを選ぶことができます。日時設定は12時間設定と24時間設定に切り変えることも可能です。
容量が他のアプリと比べて小さいため早くダウンロードができます。
広告や不要な権限はありません。 完全にオープンソースで、カラーもカスタマイズ可能。
<b>シンプルツールの完全なリストはこちらからご確認ください:</b>
https://www.simplemobiletools.com
<b> Facebook</b>
https://www.facebook.com/simplemobiletools
<b> Reddit</b>
https://www.reddit.com/r/SimpleMobileTools
</string>
<!--
Haven't found some strings? There's more at
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
-->
</resources>

View File

@ -9,6 +9,10 @@
<string name="start_conversation">Pradėtipokalbį</string>
<string name="reply">Reply</string>
<string name="show_character_counter">Show a character counter at writing messages</string>
<string name="loading_messages">Loading messages…</string>
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<!-- New conversation -->
<string name="new_conversation">Naujas pokalbis</string>

View File

@ -9,6 +9,10 @@
<string name="start_conversation">ഒരു സംഭാഷണം ആരംഭിക്കുക</string>
<string name="reply">ഒരു സംഭാഷണം ആരംഭിക്കുക</string>
<string name="show_character_counter">Show a character counter at writing messages</string>
<string name="loading_messages">Loading messages…</string>
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<!-- New conversation -->
<string name="new_conversation">പുതിയ സംഭാഷണം</string>

View File

@ -8,7 +8,11 @@
<string name="no_conversations_found">Geen opgeslagen berichten gevonden</string>
<string name="start_conversation">Een gesprek starten</string>
<string name="reply">Beantwoorden</string>
<string name="show_character_counter">Show a character counter at writing messages</string>
<string name="show_character_counter">Teller voor het aantal tekens weergeven</string>
<string name="loading_messages">Berichten laden…</string>
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<!-- New conversation -->
<string name="new_conversation">Nieuw gesprek</string>

View File

@ -8,7 +8,11 @@
<string name="no_conversations_found">Não foram encontradas conversas</string>
<string name="start_conversation">Iniciar uma conversa</string>
<string name="reply">Responder</string>
<string name="show_character_counter">Show a character counter at writing messages</string>
<string name="show_character_counter">Mostrar número de caracteres ao escrever a mensagem</string>
<string name="loading_messages">Loading messages…</string>
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<!-- New conversation -->
<string name="new_conversation">Nova conversa</string>

View File

@ -9,6 +9,10 @@
<string name="start_conversation">Начать переписку</string>
<string name="reply">Ответ</string>
<string name="show_character_counter">Показывать счётчик символов при написании сообщений</string>
<string name="loading_messages">Loading messages…</string>
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<!-- New conversation -->
<string name="new_conversation">Новая переписка</string>

View File

@ -9,6 +9,10 @@
<string name="start_conversation">Začať konverzáciu</string>
<string name="reply">Odpovedať</string>
<string name="show_character_counter">Zobraziť počítadlo znakov pri písaní správ</string>
<string name="loading_messages">Načítanie správ…</string>
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Koncept</string>
<string name="sending">Odosiela sa…</string>
<!-- New conversation -->
<string name="new_conversation">Nová konverzácia</string>

View File

@ -0,0 +1,78 @@
<resources>
<string name="app_name">Basit SMS Messenger</string>
<string name="app_launcher_name">SMS Messenger</string>
<string name="type_a_message">Bir mesaj yazın…</string>
<string name="message_not_sent">Mesaj gönderilmedi.</string>
<string name="add_person">Kişi Ekle</string>
<string name="attachment">Ek</string>
<string name="no_conversations_found">Kaydedilmiş görüşme bulunamadı</string>
<string name="start_conversation">Bir görüşme başlat</string>
<string name="reply">Yanıtla</string>
<string name="show_character_counter">Mesaj yazarken bir karakter sayacı göster</string>
<string name="loading_messages">Loading messages…</string>
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<!-- New conversation -->
<string name="new_conversation">Yeni görüşme</string>
<string name="add_contact_or_number">Kişi veya Numara Ekle…</string>
<string name="suggestions">Öneriler</string>
<!-- Notifications -->
<string name="channel_received_sms">SMS alındı</string>
<string name="new_message">Yeni mesaj</string>
<string name="mark_as_read">Okundu olarak işaretle</string>
<string name="mark_as_unread">Okunmadı olarak işaretle</string>
<!-- Confirmation dialog -->
<string name="delete_whole_conversation_confirmation">Bu görüşmenin tüm mesajlarını silmek istediğinizden emin misiniz?</string>
<!-- Are you sure you want to delete 5 conversations? -->
<plurals name="delete_conversations">
<item quantity="one">%d görüşme</item>
<item quantity="other">%d görüşme</item>
</plurals>
<!-- Are you sure you want to delete 5 messages? -->
<plurals name="delete_messages">
<item quantity="one">%d mesaj</item>
<item quantity="other">%d mesaj</item>
</plurals>
<!-- FAQ -->
<string name="faq_1_title">Uygulama neden internete erişim gerektiriyor?</string>
<string name="faq_1_text">Ne yazık ki MMS eklerini göndermek için gerekli. MMS gönderememek, diğer uygulamalara kıyasla gerçekten çok büyük bir dezavantaj olacaktır, bu yüzden bu şekilde gitmeye karar verdik.
Bununla birlikte, genellikle olduğu gibi, hiçbir reklam, izleme veya analiz yoktur, internet yalnızca MMS göndermek için kullanılır.</string>
<!-- Strings displayed only on Google Playstore. Optional, but good to have -->
<!-- App title has to have less than 50 characters. If you cannot squeeze it, just remove a part of it -->
<string name="app_title">Basit SMS Messenger - Mesajları kolayca yönetin</string>
<!-- Short description has to have max 80 characters -->
<string name="app_short_description">SMS ve MMS mesajlarını reklamsız yönetmenin kolay ve hızlı bir yolu.</string>
<string name="app_long_description">
Hem SMS hem de MMS göndererek iletişim halinde kalmanın harika bir yolu. Uygulama, tıpkı Android 7 ve üzerinden gelen numaraları engellemek gibi grup mesajlaşmasını da düzgün bir şekilde yönetiyor.
Kullanırken kendinizi rahat hissetmeniz için pek çok tarih formatı sunar. 12 ve 24 saatlik zaman formatı arasında da geçiş yapabilirsiniz.
Rakiplerine kıyasla gerçekten çok küçük bir uygulama boyutuna sahip ve indirmeyi gerçekten hızlı hale getiriyor.
Varsayılan olarak materyal tasarım ve koyu tema ile birlikte gelir, kolay kullanım için harika bir kullanıcı deneyimi sağlar. İnternet erişiminin olmaması size diğer uygulamalardan daha fazla gizlilik, güvenlik ve istikrar sağlar.
Hiçbir reklam veya gereksiz izinler içermez. Tamamen açık kaynaklıdır, özelleştirilebilir renkler sağlar.
<b>Basit Araçlar paketinin tamamına buradan göz atın:</b>
https://www.simplemobiletools.com
<b>Facebook:</b>
https://www.facebook.com/simplemobiletools
<b>Reddit:</b>
https://www.reddit.com/r/SimpleMobileTools
</string>
<!--
Haven't found some strings? There's more at
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
-->
</resources>

View File

@ -0,0 +1,78 @@
<resources>
<string name="app_name">Simple SMS Messenger</string>
<string name="app_launcher_name">Повідомлення</string>
<string name="type_a_message">Введіть повідомлення…</string>
<string name="message_not_sent">Повідомлення не надіслано. </string>
<string name="add_person">Додати учасник а</string>
<string name="attachment">Вкладення</string>
<string name="no_conversations_found">Немає збережених листувань</string>
<string name="start_conversation">Почати листування</string>
<string name="reply">Відповідь</string>
<string name="show_character_counter">Показувати кількість символів</string>
<string name="loading_messages">Loading messages…</string>
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<!-- New conversation -->
<string name="new_conversation">Нове листування</string>
<string name="add_contact_or_number">Додати контакт аба номер…</string>
<string name="suggestions">Пропозиція</string>
<!-- Notifications -->
<string name="channel_received_sms">Отримано повідомлення</string>
<string name="new_message">Нове повідомлення</string>
<string name="mark_as_read">Прочитано</string>
<string name="mark_as_unread">Не прочитано</string>
<!-- Confirmation dialog -->
<string name="delete_whole_conversation_confirmation">Видалити усі повідомлення у цьому листуванні?</string>
<!-- Are you sure you want to delete 5 conversations? -->
<plurals name="delete_conversations">
<item quantity="one">%d листування</item>
<item quantity="many">%d листувань</item>
</plurals>
<!-- Are you sure you want to delete 5 messages? -->
<plurals name="delete_messages">
<item quantity="one">%d повідомлення</item>
<item quantity="many">%d повідомлень</item>
</plurals>
<!-- FAQ -->
<string name="faq_1_title">Чому додаток потрубує доступу до інтернету?</string>
<string name="faq_1_text">Нажаль, це необхідно для відправки вкладень MMS. Неспроможність надсилати MMS-повідомлення була б великим недоліком нашого додатку порівняно з іншими, тому ми так зробили.
Тим не менше, як і в інших наших додатках, цей не містить реклами, відстеження та аналітики. Інтернет використовується лише для відправки MMS.</string>
<!-- Strings displayed only on Google Playstore. Optional, but good to have -->
<!-- App title has to have less than 50 characters. If you cannot squeeze it, just remove a part of it -->
<string name="app_title">Simple SMS Messenger - просте управління SMS</string>
<!-- Short description has to have max 80 characters -->
<string name="app_short_description">Простий та швидкий спосіб управління повідомленнями SMS та MMS без реклами.</string>
<string name="app_long_description">
SMS/MMS-повідомлення — це чудовий спосіб підтримувати зв\'язок із близькими. Додаток також правильно опрацьовує групові повідомлення та блокування номера на Android 7+.
Підтримується багато форматів часу, щоб вам було зручно користуватися додатком. Також ви можете вибирати між 12- та 24-годинним форматом часу.
Наш додаток має малий розмір в порівнянні з додатками конкурентів, що робить його завантаження таким швидким.
За замовчуванням використовується матеріальний дизайн та темна тема, що забезпечую зручне використання. Відсутність доступу в інтернет забезпечую вам більшу приватність порівняно з іншими додатками.
Цей додаток не буде показувати рекламу, потрібні лише найнеобхідніші дозволи. Додаток має повністю відкритий програмний код, кольори оформлення можна легко налаштувати.
<b>Ознакомьтеся з повним набором інструментів Simple тут:</b>
https://www.simplemobiletools.com
<b>Facebook:</b>
https://www.facebook.com/simplemobiletools
<b>Reddit:</b>
https://www.reddit.com/r/SimpleMobileTools
</string>
<!--
Haven't found some strings? There's more at
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
-->
</resources>

View File

@ -9,6 +9,10 @@
<string name="start_conversation">开始一个对话</string>
<string name="reply">回复</string>
<string name="show_character_counter">Show a character counter at writing messages</string>
<string name="loading_messages">Loading messages…</string>
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<!-- New conversation -->
<string name="new_conversation">新的对话</string>

View File

@ -9,6 +9,10 @@
<string name="start_conversation">Start a conversation</string>
<string name="reply">Reply</string>
<string name="show_character_counter">Show a character counter at writing messages</string>
<string name="loading_messages">Loading messages…</string>
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<!-- New conversation -->
<string name="new_conversation">New conversation</string>

View File

@ -1,14 +1,14 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.4.10'
ext.kotlin_version = '1.4.21'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath 'com.android.tools.build:gradle:4.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong

View File

@ -0,0 +1,3 @@
* Properly show private contact names at group conversations
* Fixed private contacts not being visible on Android 11+
* Some stability and translation improvements

View File

@ -0,0 +1,3 @@
* Cache messages for better performance
* Add a scrollbar on the main screen
* Adding some stability and translation improvements

View File

@ -0,0 +1 @@
* Fixing a crash at devices with multiple SIM cards

View File

@ -0,0 +1 @@
Fixed messages not being sent in some cases

View File

@ -0,0 +1,3 @@
* Prefetch all messages at first launch for better performance
* Require Simple Thank You for color customization
* Some stability, translation and UX improvements