Merge pull request #496 from peanut250/feat/message-content-filters

Block messages based on content
This commit is contained in:
octoshrimpy
2025-07-18 17:05:14 -06:00
committed by GitHub
23 changed files with 714 additions and 2 deletions
@@ -242,6 +242,17 @@ class QkRealmMigration @Inject constructor(
version++
}
if (version == 11L) {
realm.schema.create("MessageContentFilter")
.addField("id", Long::class.java, FieldAttribute.PRIMARY_KEY, FieldAttribute.REQUIRED)
.addField("value", String::class.java, FieldAttribute.REQUIRED)
.addField("caseSensitive", Boolean::class.java, FieldAttribute.REQUIRED)
.addField("isRegex", Boolean::class.java, FieldAttribute.REQUIRED)
.addField("includeContacts", Boolean::class.java, FieldAttribute.REQUIRED)
version++
}
check(version >= newVersion) { "Migration missing from v$oldVersion to v$newVersion" }
}
@@ -171,4 +171,14 @@ class ContactRepositoryImpl @Inject constructor(
}
}
override fun isContact(address: String): Boolean {
val uri : Uri
if (address.contains('@')) {
uri = Uri.withAppendedPath(Email.CONTENT_FILTER_URI, Uri.encode(address))
} else {
uri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(address))
}
return context.contentResolver.query(uri, arrayOf(BaseColumns._ID), null, null, null)?.count!! > 0
}
}
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
*
* This file is part of QKSMS.
*
* QKSMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QKSMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
*/
package dev.octoshrimpy.quik.repository
import dev.octoshrimpy.quik.model.MessageContentFilter
import dev.octoshrimpy.quik.model.MessageContentFilterData
import io.realm.Realm
import io.realm.RealmResults
import javax.inject.Inject
class MessageContentFilterRepositoryImpl @Inject constructor() : MessageContentFilterRepository {
override fun createFilter(data: MessageContentFilterData) {
Realm.getDefaultInstance().use { realm ->
realm.refresh()
val maxId = realm.where(MessageContentFilter::class.java)
.max("id")?.toLong() ?: -1
realm.executeTransaction {
realm.insert(MessageContentFilter(maxId + 1, data.value, data.caseSensitive, data.isRegex, data.includeContacts))
}
}
}
override fun getMessageContentFilters(): RealmResults<MessageContentFilter> {
return Realm.getDefaultInstance()
.where(MessageContentFilter::class.java)
.findAllAsync()
}
override fun getMessageContentFilter(id: Long): MessageContentFilter? {
return Realm.getDefaultInstance()
.where(MessageContentFilter::class.java)
.equalTo("id", id)
.findFirst()
}
override fun isBlocked(messageBody: String, address: String, contactsRepo: ContactRepository): Boolean {
val isContact = contactsRepo.isContact(address)
return Realm.getDefaultInstance().use { realm ->
realm.where(MessageContentFilter::class.java)
.findAll()
.any { filter ->
if (isContact && !filter.includeContacts) {
false
} else if (filter.isRegex) {
Regex(filter.value).matches(messageBody)
} else if (filter.caseSensitive) {
val regexp = ".*\\b" + Regex.escape(filter.value) + "\\b.*"
Regex(regexp).matches(messageBody)
} else {
val regexp = ".*\\b" + Regex.escape(filter.value.lowercase()) + "\\b.*"
Regex(regexp).matches(messageBody.lowercase())
}
}
}
}
override fun removeFilter(id: Long) {
Realm.getDefaultInstance().use { realm ->
realm.executeTransaction {
realm.where(MessageContentFilter::class.java)
.equalTo("id", id)
.findAll()
.deleteAllFromRealm()
}
}
}
}
@@ -23,7 +23,9 @@ import dev.octoshrimpy.quik.blocking.BlockingClient
import dev.octoshrimpy.quik.extensions.mapNotNull
import dev.octoshrimpy.quik.manager.ActiveConversationManager
import dev.octoshrimpy.quik.manager.NotificationManager
import dev.octoshrimpy.quik.repository.ContactRepository
import dev.octoshrimpy.quik.repository.ConversationRepository
import dev.octoshrimpy.quik.repository.MessageContentFilterRepository
import dev.octoshrimpy.quik.repository.MessageRepository
import dev.octoshrimpy.quik.repository.SyncRepository
import dev.octoshrimpy.quik.util.Preferences
@@ -39,7 +41,9 @@ class ReceiveMms @Inject constructor(
private val syncManager: SyncRepository,
private val messageRepo: MessageRepository,
private val notificationManager: NotificationManager,
private val updateBadge: UpdateBadge
private val updateBadge: UpdateBadge,
private val filterRepo: MessageContentFilterRepository,
private val contactsRepo: ContactRepository
) : Interactor<Uri>() {
override fun buildObservable(params: Uri): Flowable<*> {
@@ -75,6 +79,12 @@ class ReceiveMms @Inject constructor(
else -> Unit
}
if (filterRepo.isBlocked(message.getText(), message.address, contactsRepo)) {
Timber.v("message dropped based on content filters")
messageRepo.deleteMessages(listOf(message.id))
return@mapNotNull null
}
message
}
.doOnNext { message ->
@@ -22,7 +22,9 @@ import dev.octoshrimpy.quik.blocking.BlockingClient
import dev.octoshrimpy.quik.extensions.mapNotNull
import dev.octoshrimpy.quik.manager.NotificationManager
import dev.octoshrimpy.quik.manager.ShortcutManager
import dev.octoshrimpy.quik.repository.ContactRepository
import dev.octoshrimpy.quik.repository.ConversationRepository
import dev.octoshrimpy.quik.repository.MessageContentFilterRepository
import dev.octoshrimpy.quik.repository.MessageRepository
import dev.octoshrimpy.quik.util.Preferences
import io.reactivex.Flowable
@@ -36,7 +38,9 @@ class ReceiveSms @Inject constructor(
private val messageRepo: MessageRepository,
private val notificationManager: NotificationManager,
private val updateBadge: UpdateBadge,
private val shortcutManager: ShortcutManager
private val shortcutManager: ShortcutManager,
private val filterRepo: MessageContentFilterRepository,
private val contactsRepo: ContactRepository
) : Interactor<Long>() {
override fun buildObservable(params: Long): Flowable<*> {
@@ -69,6 +73,12 @@ class ReceiveSms @Inject constructor(
}
}
if (filterRepo.isBlocked(it.getText(), it.address, contactsRepo)) {
Timber.v("message dropped based on content filters")
messageRepo.deleteMessages(listOf(it.id))
return@mapNotNull null
}
// update and fetch conversation
conversationRepo.updateConversations(it.threadId)
conversationRepo.getOrCreateConversation(it.threadId)
@@ -0,0 +1,20 @@
package dev.octoshrimpy.quik.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class MessageContentFilter(
@PrimaryKey var id: Long = 0,
var value: String = "",
var caseSensitive: Boolean = false,
var isRegex: Boolean = false,
var includeContacts: Boolean = false
) : RealmObject()
data class MessageContentFilterData(
var value: String = "",
var caseSensitive: Boolean = false,
var isRegex: Boolean = false,
var includeContacts: Boolean = false
)
@@ -41,4 +41,6 @@ interface ContactRepository {
fun setDefaultPhoneNumber(lookupKey: String, phoneNumberId: Long)
fun isContact(address: String): Boolean
}
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
*
* This file is part of QKSMS.
*
* QKSMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QKSMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
*/
package dev.octoshrimpy.quik.repository
import dev.octoshrimpy.quik.model.MessageContentFilter
import dev.octoshrimpy.quik.model.MessageContentFilterData
import io.realm.RealmResults
interface MessageContentFilterRepository {
fun createFilter(data: MessageContentFilterData)
fun getMessageContentFilters(): RealmResults<MessageContentFilter>
fun getMessageContentFilter(id: Long): MessageContentFilter?
fun isBlocked(messageBody: String, address: String, contactsRepo: ContactRepository): Boolean
fun removeFilter(id: Long)
}
@@ -29,6 +29,7 @@ import dev.octoshrimpy.quik.common.util.extensions.animateLayoutChanges
import dev.octoshrimpy.quik.feature.blocking.manager.BlockingManagerController
import dev.octoshrimpy.quik.feature.blocking.messages.BlockedMessagesController
import dev.octoshrimpy.quik.feature.blocking.numbers.BlockedNumbersController
import dev.octoshrimpy.quik.feature.blocking.filters.MessageContentFiltersController
import dev.octoshrimpy.quik.injection.appComponent
import kotlinx.android.synthetic.main.blocking_controller.*
import kotlinx.android.synthetic.main.settings_switch_widget.view.*
@@ -38,6 +39,7 @@ class BlockingController : QkController<BlockingView, BlockingState, BlockingPre
override val blockingManagerIntent by lazy { blockingManager.clicks() }
override val blockedNumbersIntent by lazy { blockedNumbers.clicks() }
override val messageContentFiltersIntent by lazy { messageContentFilters.clicks() }
override val blockedMessagesIntent by lazy { blockedMessages.clicks() }
override val dropClickedIntent by lazy { drop.clicks() }
@@ -74,6 +76,12 @@ class BlockingController : QkController<BlockingView, BlockingState, BlockingPre
.popChangeHandler(QkChangeHandler()))
}
override fun openMessageContentFilters() {
router.pushController(RouterTransaction.with(MessageContentFiltersController())
.pushChangeHandler(QkChangeHandler())
.popChangeHandler(QkChangeHandler()))
}
override fun openBlockedMessages() {
router.pushController(RouterTransaction.with(BlockedMessagesController())
.pushChangeHandler(QkChangeHandler())
@@ -69,6 +69,10 @@ class BlockingPresenter @Inject constructor(
}
}
view.messageContentFiltersIntent
.autoDisposable(view.scope())
.subscribe { view.openMessageContentFilters() }
view.blockedMessagesIntent
.autoDisposable(view.scope())
.subscribe { view.openBlockedMessages() }
@@ -25,10 +25,12 @@ interface BlockingView : QkViewContract<BlockingState> {
val blockingManagerIntent: Observable<*>
val blockedNumbersIntent: Observable<*>
val messageContentFiltersIntent: Observable<*>
val blockedMessagesIntent: Observable<*>
val dropClickedIntent: Observable<*>
fun openBlockingManager()
fun openBlockedNumbers()
fun openMessageContentFilters()
fun openBlockedMessages()
}
@@ -0,0 +1,55 @@
/*
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
*
* This file is part of QKSMS.
*
* QKSMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QKSMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
*/
package dev.octoshrimpy.quik.feature.blocking.filters
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import dev.octoshrimpy.quik.R
import dev.octoshrimpy.quik.common.base.QkRealmAdapter
import dev.octoshrimpy.quik.common.base.QkViewHolder
import dev.octoshrimpy.quik.model.MessageContentFilter
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.message_content_filter_list_item.*
import kotlinx.android.synthetic.main.message_content_filter_list_item.view.*
class MessageContentFiltersAdapter : QkRealmAdapter<MessageContentFilter>() {
val removeMessageContentFilter: Subject<Long> = PublishSubject.create()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.message_content_filter_list_item, parent, false)
return QkViewHolder(view).apply {
containerView.removeFilter.setOnClickListener {
val filter = getItem(adapterPosition) ?: return@setOnClickListener
removeMessageContentFilter.onNext(filter.id)
}
}
}
override fun onBindViewHolder(holder: QkViewHolder, position: Int) {
val item = getItem(position)!!
holder.caseIcon.visibility = if (item.caseSensitive) View.VISIBLE else View.GONE
holder.regexIcon.visibility = if (item.isRegex) View.VISIBLE else View.GONE
holder.contactsIcon.visibility = if (item.includeContacts) View.VISIBLE else View.GONE
holder.filter.text = item.value
}
}
@@ -0,0 +1,115 @@
/*
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
*
* This file is part of QKSMS.
*
* QKSMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QKSMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
*/
package dev.octoshrimpy.quik.feature.blocking.filters
import android.view.LayoutInflater
import android.view.View
import androidx.appcompat.app.AlertDialog
import com.jakewharton.rxbinding2.view.clicks
import com.uber.autodispose.android.lifecycle.scope
import com.uber.autodispose.autoDisposable
import dev.octoshrimpy.quik.R
import dev.octoshrimpy.quik.common.base.QkController
import dev.octoshrimpy.quik.common.util.Colors
import dev.octoshrimpy.quik.common.util.extensions.setBackgroundTint
import dev.octoshrimpy.quik.common.util.extensions.setTint
import dev.octoshrimpy.quik.common.widget.PreferenceView
import dev.octoshrimpy.quik.injection.appComponent
import dev.octoshrimpy.quik.model.MessageContentFilterData
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.message_content_filters_add_dialog.view.*
import kotlinx.android.synthetic.main.message_content_filters_controller.*
import kotlinx.android.synthetic.main.settings_switch_widget.view.*
import javax.inject.Inject
class MessageContentFiltersController : QkController<MessageContentFiltersView, MessageContentFiltersState,
MessageContentFiltersPresenter>(), MessageContentFiltersView {
@Inject override lateinit var presenter: MessageContentFiltersPresenter
@Inject lateinit var colors: Colors
private val adapter = MessageContentFiltersAdapter()
private val saveFilterSubject: Subject<MessageContentFilterData> = PublishSubject.create()
init {
appComponent.inject(this)
retainViewMode = RetainViewMode.RETAIN_DETACH
layoutRes = R.layout.message_content_filters_controller
}
override fun onAttach(view: View) {
super.onAttach(view)
presenter.bindIntents(this)
setTitle(R.string.message_content_filters_title)
showBackButton(true)
}
override fun onViewCreated() {
super.onViewCreated()
add.setBackgroundTint(colors.theme().theme)
add.setTint(colors.theme().textPrimary)
adapter.emptyView = empty
filters.adapter = adapter
}
override fun render(state: MessageContentFiltersState) {
adapter.updateData(state.filters)
}
override fun removeFilter(): Observable<Long> = adapter.removeMessageContentFilter
override fun addFilter(): Observable<*> = add.clicks()
override fun saveFilter(): Observable<MessageContentFilterData> = saveFilterSubject
override fun showAddDialog() {
val layout = LayoutInflater.from(activity).inflate(R.layout.message_content_filters_add_dialog, null)
(0 until layout.add_dialog.childCount)
.map { index -> layout.add_dialog.getChildAt(index) }
.mapNotNull { view -> view as? PreferenceView }
.map { preference -> preference.clicks().map { preference } }
.let { Observable.merge(it) }
.autoDisposable(scope())
.subscribe {
it.checkbox.isChecked = !it.checkbox.isChecked
layout.caseSensitivity.isEnabled = !layout.regexp.checkbox.isChecked
}
val dialog = AlertDialog.Builder(activity!!)
.setView(layout)
.setPositiveButton(R.string.message_content_filters_dialog_create) { _, _ ->
var text = layout.input.text.toString();
if (!text.isBlank()) {
if (!layout.regexp.checkbox.isChecked) text = text.trim()
saveFilterSubject.onNext(
MessageContentFilterData(
text,
layout.caseSensitivity.checkbox.isChecked && !layout.regexp.checkbox.isChecked,
layout.regexp.checkbox.isChecked,
layout.contacts.checkbox.isChecked
)
)
}
}
.setNegativeButton(R.string.button_cancel) { _, _ -> }
dialog.show()
}
}
@@ -0,0 +1,55 @@
/*
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
*
* This file is part of QKSMS.
*
* QKSMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QKSMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
*/
package dev.octoshrimpy.quik.feature.blocking.filters
import com.uber.autodispose.android.lifecycle.scope
import com.uber.autodispose.autoDisposable
import dev.octoshrimpy.quik.common.base.QkPresenter
import dev.octoshrimpy.quik.repository.MessageContentFilterRepository
import io.reactivex.schedulers.Schedulers
import javax.inject.Inject
class MessageContentFiltersPresenter @Inject constructor(
private val filterRepo: MessageContentFilterRepository,
) : QkPresenter<MessageContentFiltersView, MessageContentFiltersState>(
MessageContentFiltersState(filters = filterRepo.getMessageContentFilters())
) {
override fun bindIntents(view: MessageContentFiltersView) {
super.bindIntents(view)
view.removeFilter()
.observeOn(Schedulers.io())
.doOnNext(filterRepo::removeFilter)
.subscribeOn(Schedulers.io())
.autoDisposable(view.scope())
.subscribe()
view.addFilter()
.autoDisposable(view.scope())
.subscribe { view.showAddDialog() }
view.saveFilter()
.observeOn(Schedulers.io())
.subscribeOn(Schedulers.io())
.autoDisposable(view.scope())
.subscribe { filterData -> filterRepo.createFilter(filterData) }
}
}
@@ -0,0 +1,26 @@
/*
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
*
* This file is part of QKSMS.
*
* QKSMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QKSMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
*/
package dev.octoshrimpy.quik.feature.blocking.filters
import dev.octoshrimpy.quik.model.MessageContentFilter
import io.realm.RealmResults
data class MessageContentFiltersState(
val filters: RealmResults<MessageContentFilter>? = null
)
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
*
* This file is part of QKSMS.
*
* QKSMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QKSMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
*/
package dev.octoshrimpy.quik.feature.blocking.filters
import dev.octoshrimpy.quik.common.base.QkViewContract
import dev.octoshrimpy.quik.model.MessageContentFilterData
import io.reactivex.Observable
interface MessageContentFiltersView : QkViewContract<MessageContentFiltersState> {
fun removeFilter(): Observable<Long>
fun addFilter(): Observable<*>
fun saveFilter(): Observable<MessageContentFilterData>
fun showAddDialog()
}
@@ -32,6 +32,7 @@ import dev.octoshrimpy.quik.common.widget.QkTextView
import dev.octoshrimpy.quik.common.widget.RadioPreferenceView
import dev.octoshrimpy.quik.feature.backup.BackupController
import dev.octoshrimpy.quik.feature.blocking.BlockingController
import dev.octoshrimpy.quik.feature.blocking.filters.MessageContentFiltersController
import dev.octoshrimpy.quik.feature.blocking.manager.BlockingManagerController
import dev.octoshrimpy.quik.feature.blocking.messages.BlockedMessagesController
import dev.octoshrimpy.quik.feature.blocking.numbers.BlockedNumbersController
@@ -65,6 +66,7 @@ interface AppComponent {
fun inject(controller: BackupController)
fun inject(controller: BlockedMessagesController)
fun inject(controller: BlockedNumbersController)
fun inject(controller: MessageContentFiltersController)
fun inject(controller: BlockingController)
fun inject(controller: BlockingManagerController)
fun inject(controller: SettingsController)
@@ -81,6 +81,8 @@ import dev.octoshrimpy.quik.repository.ContactRepository
import dev.octoshrimpy.quik.repository.ContactRepositoryImpl
import dev.octoshrimpy.quik.repository.ConversationRepository
import dev.octoshrimpy.quik.repository.ConversationRepositoryImpl
import dev.octoshrimpy.quik.repository.MessageContentFilterRepository
import dev.octoshrimpy.quik.repository.MessageContentFilterRepositoryImpl
import dev.octoshrimpy.quik.repository.MessageRepository
import dev.octoshrimpy.quik.repository.MessageRepositoryImpl
import dev.octoshrimpy.quik.repository.ScheduledMessageRepository
@@ -199,6 +201,9 @@ class AppModule(private var application: Application) {
@Provides
fun provideBlockingRepository(repository: BlockingRepositoryImpl): BlockingRepository = repository
@Provides
fun provideMessageContentFilterRepository(repository: MessageContentFilterRepositoryImpl): MessageContentFilterRepository = repository
@Provides
fun provideContactRepository(repository: ContactRepositoryImpl): ContactRepository = repository
@@ -46,6 +46,12 @@
android:layout_height="wrap_content"
app:title="@string/blocked_numbers_title" />
<dev.octoshrimpy.quik.common.widget.PreferenceView
android:id="@+id/messageContentFilters"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/message_content_filters_title" />
<dev.octoshrimpy.quik.common.widget.PreferenceView
android:id="@+id/drop"
android:layout_width="match_parent"
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
~
~ This file is part of QKSMS.
~
~ QKSMS is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ QKSMS is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal">
<dev.octoshrimpy.quik.common.widget.QkTextView
android:id="@+id/filter"
style="@style/TextPrimary"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center_vertical"
android:paddingStart="24dp"
tools:text="value"
android:textDirection="ltr" />
<ImageView
android:id="@+id/caseIcon"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="12dp"
android:src="@drawable/ic_format_size_black_24dp"
android:tint="?android:attr/textColorSecondary" />
<ImageView
android:id="@+id/regexIcon"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="12dp"
android:src="@drawable/ic_settings_black_24dp"
android:tint="?android:attr/textColorSecondary" />
<ImageView
android:id="@+id/contactsIcon"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="12dp"
android:src="@drawable/ic_person_black_24dp"
android:tint="?android:attr/textColorSecondary" />
<ImageView
android:id="@+id/removeFilter"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:background="?android:attr/selectableItemBackground"
android:padding="12dp"
android:src="@drawable/ic_close_black_24dp"
android:tint="?android:attr/textColorSecondary" />
</LinearLayout>
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
~
~ This file is part of QKSMS.
~
~ QKSMS is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ QKSMS is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/add_dialog"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<dev.octoshrimpy.quik.common.widget.QkTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:text="@string/message_content_filters_dialog_title"
android:textColor="?android:attr/textColorPrimary"
app:textSize="dialog" />
<dev.octoshrimpy.quik.common.widget.QkEditText
android:id="@+id/input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
android:hint="@string/message_content_filters_dialog_hint"
android:paddingStart="24dp"
android:paddingTop="16dp"
android:paddingEnd="24dp"
android:paddingBottom="16dp"
android:textColorHint="?android:attr/textColorTertiary"
app:textSize="primary" />
<dev.octoshrimpy.quik.common.widget.PreferenceView
android:id="@+id/caseSensitivity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/message_content_filters_dialog_case_switch"
app:icon="@drawable/ic_format_size_black_24dp"
app:widget="@layout/settings_switch_widget" />
<dev.octoshrimpy.quik.common.widget.PreferenceView
android:id="@+id/regexp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/message_content_filters_dialog_regex_switch"
app:icon="@drawable/ic_settings_black_24dp"
app:widget="@layout/settings_switch_widget" />
<dev.octoshrimpy.quik.common.widget.PreferenceView
android:id="@+id/contacts"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/message_content_filters_dialog_contacts_switch"
app:icon="@drawable/ic_person_black_24dp"
app:widget="@layout/settings_switch_widget" />
</LinearLayout>
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
~
~ This file is part of QKSMS.
~
~ QKSMS is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ QKSMS is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/windowBackground"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/filters"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="8dp"
android:paddingBottom="8dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<dev.octoshrimpy.quik.common.widget.QkTextView
android:id="@+id/empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/message_content_filters_empty"
android:textColor="?android:attr/textColorTertiary" />
<ImageView
android:id="@+id/add"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:background="@drawable/circle_ripple"
android:contentDescription="@string/message_content_filters_add_cd"
android:elevation="8dp"
android:padding="16dp"
android:src="@drawable/ic_add_black_24dp"
android:tint="@color/white"
tools:backgroundTint="@color/tools_theme" />
</FrameLayout>
@@ -362,6 +362,16 @@
<string name="blocked_numbers_dialog_hint">Phone number</string>
<string name="blocked_numbers_dialog_block">Block</string>
<string name="message_content_filters_title">Message content filters</string>
<string name="message_content_filters_empty">Your message content filters will appear here</string>
<string name="message_content_filters_add_cd">Create a new filter</string>
<string name="message_content_filters_dialog_title">Drop messages matching</string>
<string name="message_content_filters_dialog_hint">Word, phrase or regular expression</string>
<string name="message_content_filters_dialog_case_switch">Case sensitive</string>
<string name="message_content_filters_dialog_regex_switch">Regular expression</string>
<string name="message_content_filters_dialog_contacts_switch">Include contacts</string>
<string name="message_content_filters_dialog_create">Create</string>
<string name="blocked_messages_title">Blocked messages</string>
<string name="blocked_messages_empty">Your blocked messages will appear here</string>