Add support for blocking keywords for incoming messages

This adds support for filtering incoming messages based on
message body by checking if it contains any of the blocked keywords
(case insensitive). Regex and patterns are not supported at the moment.

NOTE: This does not currently support MMS, only SMS.

This closes #33
This commit is contained in:
Ensar Sarajčić 2023-07-10 14:11:41 +02:00
parent 9942fb788a
commit 674a467694
61 changed files with 752 additions and 21 deletions

View file

@ -0,0 +1,89 @@
package com.simplemobiletools.smsmessenger.activities
import android.os.Bundle
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.beVisibleIf
import com.simplemobiletools.commons.extensions.getProperPrimaryColor
import com.simplemobiletools.commons.extensions.underlineText
import com.simplemobiletools.commons.extensions.updateTextColors
import com.simplemobiletools.commons.helpers.APP_ICON_IDS
import com.simplemobiletools.commons.helpers.APP_LAUNCHER_NAME
import com.simplemobiletools.commons.helpers.NavigationIcon
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.commons.interfaces.RefreshRecyclerViewListener
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.dialogs.AddBlockedKeywordDialog
import com.simplemobiletools.smsmessenger.dialogs.ManageBlockedKeywordsAdapter
import com.simplemobiletools.smsmessenger.extensions.config
import com.simplemobiletools.smsmessenger.extensions.toArrayList
import kotlinx.android.synthetic.main.activity_manage_blocked_keywords.*
class ManageBlockedKeywordsActivity : BaseSimpleActivity(), RefreshRecyclerViewListener {
override fun getAppIconIDs() = intent.getIntegerArrayListExtra(APP_ICON_IDS) ?: ArrayList()
override fun getAppLauncherName() = intent.getStringExtra(APP_LAUNCHER_NAME) ?: ""
override fun onCreate(savedInstanceState: Bundle?) {
isMaterialActivity = true
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_manage_blocked_keywords)
updateBlockedKeywords()
setupOptionsMenu()
updateMaterialActivityViews(block_keywords_coordinator, manage_blocked_keywords_list, useTransparentNavigation = true, useTopSearchMenu = false)
setupMaterialScrollListener(manage_blocked_keywords_list, block_keywords_toolbar)
updateTextColors(manage_blocked_keywords_wrapper)
manage_blocked_keywords_placeholder_2.apply {
underlineText()
setTextColor(getProperPrimaryColor())
setOnClickListener {
addOrEditBlockedKeyword()
}
}
}
override fun onResume() {
super.onResume()
setupToolbar(block_keywords_toolbar, NavigationIcon.Arrow)
}
private fun setupOptionsMenu() {
block_keywords_toolbar.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.add_blocked_keyword -> {
addOrEditBlockedKeyword()
true
}
else -> false
}
}
}
override fun refreshItems() {
updateBlockedKeywords()
}
private fun updateBlockedKeywords() {
ensureBackgroundThread {
val blockedKeywords = config.blockedKeywords
runOnUiThread {
ManageBlockedKeywordsAdapter(this, blockedKeywords.toArrayList(), this, manage_blocked_keywords_list) {
addOrEditBlockedKeyword(it as String)
}.apply {
manage_blocked_keywords_list.adapter = this
}
manage_blocked_keywords_placeholder.beVisibleIf(blockedKeywords.isEmpty())
manage_blocked_keywords_placeholder_2.beVisibleIf(blockedKeywords.isEmpty())
}
}
}
private fun addOrEditBlockedKeyword(keyword: String? = null) {
AddBlockedKeywordDialog(this, keyword) {
updateBlockedKeywords()
}
}
}

View file

@ -37,6 +37,7 @@ class SettingsActivity : SimpleActivity() {
setupUseEnglish()
setupLanguage()
setupManageBlockedNumbers()
setupManageBlockedKeywords()
setupChangeDateTimeFormat()
setupFontSize()
setupShowCharacterCounter()
@ -126,6 +127,20 @@ class SettingsActivity : SimpleActivity() {
}
}
private fun setupManageBlockedKeywords() {
settings_manage_blocked_keywords.text = addLockedLabelIfNeeded(R.string.manage_blocked_keywords)
settings_manage_blocked_keywords_holder.setOnClickListener {
if (isOrWasThankYouInstalled()) {
Intent(this, ManageBlockedKeywordsActivity::class.java).apply {
startActivity(this)
}
} else {
FeatureLockedDialog(this) { }
}
}
}
private fun setupChangeDateTimeFormat() {
settings_change_date_time_format_holder.setOnClickListener {
ChangeDateTimeFormatDialog(this) {

View file

@ -0,0 +1,43 @@
package com.simplemobiletools.smsmessenger.dialogs
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.getAlertDialogBuilder
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.commons.extensions.showKeyboard
import com.simplemobiletools.commons.extensions.value
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.extensions.config
import kotlinx.android.synthetic.main.dialog_add_blocked_keyword.view.add_blocked_keyword_edittext
class AddBlockedKeywordDialog(val activity: BaseSimpleActivity, private val originalKeyword: String? = null, val callback: () -> Unit) {
init {
val view = activity.layoutInflater.inflate(R.layout.dialog_add_blocked_keyword, null).apply {
if (originalKeyword != null) {
add_blocked_keyword_edittext.setText(originalKeyword)
}
}
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.apply {
activity.setupDialogStuff(view, this) { alertDialog ->
alertDialog.showKeyboard(view.add_blocked_keyword_edittext)
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val newBlockedKeyword = view.add_blocked_keyword_edittext.value
if (originalKeyword != null && newBlockedKeyword != originalKeyword) {
activity.config.removeBlockedKeyword(originalKeyword)
}
if (newBlockedKeyword.isNotEmpty()) {
activity.config.addBlockedKeyword(newBlockedKeyword)
}
callback()
alertDialog.dismiss()
}
}
}
}
}

View file

@ -0,0 +1,148 @@
package com.simplemobiletools.smsmessenger.dialogs
import android.view.*
import android.widget.PopupMenu
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter
import com.simplemobiletools.commons.extensions.copyToClipboard
import com.simplemobiletools.commons.extensions.getPopupMenuTheme
import com.simplemobiletools.commons.extensions.getProperTextColor
import com.simplemobiletools.commons.extensions.setupViewBackground
import com.simplemobiletools.commons.interfaces.RefreshRecyclerViewListener
import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.extensions.config
import kotlinx.android.synthetic.main.item_manage_blocked_keyword.view.manage_blocked_keyword_holder
import kotlinx.android.synthetic.main.item_manage_blocked_keyword.view.manage_blocked_keyword_title
import kotlinx.android.synthetic.main.item_manage_blocked_keyword.view.overflow_menu_anchor
import kotlinx.android.synthetic.main.item_manage_blocked_keyword.view.overflow_menu_icon
class ManageBlockedKeywordsAdapter(
activity: BaseSimpleActivity, var blockedKeywords: ArrayList<String>, val listener: RefreshRecyclerViewListener?,
recyclerView: MyRecyclerView, itemClick: (Any) -> Unit
) : MyRecyclerViewAdapter(activity, recyclerView, itemClick) {
init {
setupDragListener(true)
}
override fun getActionMenuId() = R.menu.cab_blocked_keywords
override fun prepareActionMode(menu: Menu) {
menu.apply {
findItem(R.id.cab_copy_keyword).isVisible = isOneItemSelected()
}
}
override fun actionItemPressed(id: Int) {
if (selectedKeys.isEmpty()) {
return
}
when (id) {
R.id.cab_copy_keyword -> copyKeywordToClipboard()
R.id.cab_delete -> deleteSelection()
}
}
override fun getSelectableItemCount() = blockedKeywords.size
override fun getIsItemSelectable(position: Int) = true
override fun getItemSelectionKey(position: Int) = blockedKeywords.getOrNull(position)?.hashCode()
override fun getItemKeyPosition(key: Int) = blockedKeywords.indexOfFirst { it.hashCode() == key }
override fun onActionModeCreated() {}
override fun onActionModeDestroyed() {}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(R.layout.item_manage_blocked_keyword, parent)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val blockedKeyword = blockedKeywords[position]
holder.bindView(blockedKeyword, true, true) { itemView, _ ->
setupView(itemView, blockedKeyword)
}
bindViewHolder(holder)
}
override fun getItemCount() = blockedKeywords.size
private fun getSelectedItems() = blockedKeywords.filter { selectedKeys.contains(it.hashCode()) }
private fun setupView(view: View, blockedKeyword: String) {
view.apply {
setupViewBackground(activity)
manage_blocked_keyword_holder?.isSelected = selectedKeys.contains(blockedKeyword.hashCode())
manage_blocked_keyword_title.apply {
text = blockedKeyword
setTextColor(textColor)
}
overflow_menu_icon.drawable.apply {
mutate()
setTint(activity.getProperTextColor())
}
overflow_menu_icon.setOnClickListener {
showPopupMenu(overflow_menu_anchor, blockedKeyword)
}
}
}
private fun showPopupMenu(view: View, blockedKeyword: String) {
finishActMode()
val theme = activity.getPopupMenuTheme()
val contextTheme = ContextThemeWrapper(activity, theme)
PopupMenu(contextTheme, view, Gravity.END).apply {
inflate(getActionMenuId())
setOnMenuItemClickListener { item ->
val blockedKeywordId = blockedKeyword.hashCode()
when (item.itemId) {
R.id.cab_copy_keyword -> {
executeItemMenuOperation(blockedKeywordId) {
copyKeywordToClipboard()
}
}
R.id.cab_delete -> {
executeItemMenuOperation(blockedKeywordId) {
deleteSelection()
}
}
}
true
}
show()
}
}
private fun executeItemMenuOperation(blockedKeywordId: Int, callback: () -> Unit) {
selectedKeys.add(blockedKeywordId)
callback()
selectedKeys.remove(blockedKeywordId)
}
private fun copyKeywordToClipboard() {
val selectedKeyword = getSelectedItems().firstOrNull() ?: return
activity.copyToClipboard(selectedKeyword)
finishActMode()
}
private fun deleteSelection() {
val deleteBlockedKeywords = HashSet<String>(selectedKeys.size)
val positions = getSelectedItemPositions()
getSelectedItems().forEach {
deleteBlockedKeywords.add(it)
activity.config.removeBlockedKeyword(it)
}
blockedKeywords.removeAll(deleteBlockedKeywords)
removeSelectedItems(positions)
if (blockedKeywords.isEmpty()) {
listener?.refreshItems()
}
}
}

View file

@ -68,6 +68,18 @@ class Config(context: Context) : BaseConfig(context) {
pinnedConversations = pinnedConversations.minus(conversations.map { it.threadId.toString() })
}
var blockedKeywords: Set<String>
get() = prefs.getStringSet(BLOCKED_KEYWORDS, HashSet<String>())!!
set(blockedKeywords) = prefs.edit().putStringSet(BLOCKED_KEYWORDS, blockedKeywords).apply()
fun addBlockedKeyword(keyword: String) {
blockedKeywords = blockedKeywords.plus(keyword)
}
fun removeBlockedKeyword(keyword: String) {
blockedKeywords = blockedKeywords.minus(keyword)
}
var exportSms: Boolean
get() = prefs.getBoolean(EXPORT_SMS, true)
set(exportSms) = prefs.edit().putBoolean(EXPORT_SMS, exportSms).apply()

View file

@ -25,6 +25,7 @@ const val SEND_LONG_MESSAGE_MMS = "send_long_message_mms"
const val SEND_GROUP_MESSAGE_MMS = "send_group_message_mms"
const val MMS_FILE_SIZE_LIMIT = "mms_file_size_limit"
const val PINNED_CONVERSATIONS = "pinned_conversations"
const val BLOCKED_KEYWORDS = "blocked_keywords"
const val EXPORT_SMS = "export_sms"
const val EXPORT_MMS = "export_mms"
const val EXPORT_MIME_TYPE = "application/json"

View file

@ -57,6 +57,10 @@ class SmsReceiver : BroadcastReceiver() {
private fun handleMessage(
context: Context, address: String, subject: String, body: String, date: Long, read: Int, threadId: Long, type: Int, subscriptionId: Int, status: Int
) {
if (isMessageFilteredOut(context, address, subject, body)) {
return
}
val photoUri = SimpleContactsHelper(context).getPhotoUriFromPhoneNumber(address)
val bitmap = context.getNotificationBitmap(photoUri)
Handler(Looper.getMainLooper()).post {
@ -106,4 +110,14 @@ class SmsReceiver : BroadcastReceiver() {
}
}
}
private fun isMessageFilteredOut(context: Context, address: String, subject: String, body: String): Boolean {
for (blockedKeyword in context.config.blockedKeywords) {
if (body.contains(blockedKeyword, ignoreCase = true)) {
return true
}
}
return false
}
}