Commit 7b7ad7aa authored by Daniel Wolf's avatar Daniel Wolf
Browse files

Add a dialog to search the DNS rules

Implements #250
parent 3446789b
Pipeline #7377 passed with stage
in 5 minutes and 2 seconds
package com.frostnerd.smokescreen.dialog
import android.content.Context
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import androidx.appcompat.app.AlertDialog
import com.frostnerd.smokescreen.R
import com.frostnerd.smokescreen.database.entities.DnsRule
import com.frostnerd.smokescreen.database.entities.HostSource
import com.frostnerd.smokescreen.database.getDatabase
import com.frostnerd.smokescreen.getPreferences
import kotlinx.android.synthetic.main.dialog_dnsrule_search.view.*
import kotlinx.coroutines.*
import org.minidns.record.Record
import kotlin.coroutines.CoroutineContext
/*
* Copyright (C) 2020 Daniel Wolf (Ch4t4r)
*
* This program 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.
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can contact the developer at daniel.wolf@frostnerd.com.
*/
class DnsRuleSearchDialog(
context: Context
):AlertDialog(context, context.getPreferences().theme.dialogStyle), CoroutineScope {
val supervisor = SupervisorJob()
override val coroutineContext: CoroutineContext = supervisor + Dispatchers.IO
var currentSearchJob:Job? = null
private val watcher = object :TextWatcher{
private var previousSearch = ""
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
val search = s.toString().trim()
if (search == previousSearch)
return
previousSearch = search
currentSearchJob?.cancel()
if(search.isNotBlank()) currentSearchJob = this@DnsRuleSearchDialog.launch {
delay(500) // debounce
if (search != previousSearch)
return@launch
val rule = findRuleForSearch(search)
val hostSource = rule?.importedFrom?.let { getHostSourceForId(it) }
if(isActive) launch(Dispatchers.Main) {
if(rule == null) displayRuleSource(wasFound = false, isUserRule = false, null)
else displayRuleSource(wasFound = true, isUserRule = rule.importedFrom == null, hostSource)
}
}
else clearSearchResultText()
}
override fun afterTextChanged(s: Editable?) = Unit
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
}
val view: View
init {
view = layoutInflater.inflate(R.layout.dialog_dnsrule_search, null, false)
setTitle(R.string.dialog_dnsrules_search_title)
setView(view)
setButton(BUTTON_POSITIVE, context.getText(R.string.all_close)) {_,_ ->
supervisor.cancel()
}
view.searchTerm.addTextChangedListener(watcher)
setOnDismissListener {
supervisor.cancel()
}
}
private fun findRuleForSearch(searchTerm: String): DnsRule? {
var source: DnsRule? = null
if (searchTerm.startsWith("www", ignoreCase = true)) {
source = context.getDatabase().dnsRuleDao().findRuleTargetEntity(
searchTerm.replaceFirst("www.", "", ignoreCase = true),
Record.TYPE.ANY,
true
)
}
source = source ?: context.getDatabase().dnsRuleDao()
.findRuleTargetEntity(searchTerm, Record.TYPE.ANY, true)
return source ?: context.getDatabase().dnsRuleDao().findPossibleWildcardRuleTarget(
searchTerm, type = Record.TYPE.ANY,
useUserRules = true,
includeWhitelistEntries = false,
includeNonWhitelistEntries = true
).firstOrNull {
DnsRuleDialog.databaseHostToMatcher(it.host).reset(searchTerm).matches()
}
}
private fun getHostSourceForId(id:Long):HostSource? {
return context.getDatabase().hostSourceDao().findById(id)
}
private fun displayRuleSource(wasFound:Boolean, isUserRule:Boolean, hostSource: HostSource?) {
val text = if(wasFound) {
if(isUserRule) {
context.getString(R.string.dialog_dnsrules_status_userrule)
} else {
if(hostSource == null) {
context.getString(R.string.dialog_dnsrules_status_sourcenotfound)
} else {
context.getString(R.string.dialog_dnsrules_status_fromsource, hostSource.name)
}
}
} else {
context.getString(R.string.dialog_dnsrules_status_not_found)
}
view.searchResult.text = text
}
private fun clearSearchResultText() {
view.searchResult.text = ""
}
}
\ No newline at end of file
......@@ -533,7 +533,7 @@ class DnsRuleFragment : Fragment() {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_dnsrule, menu)
val switch = menu.getItem(0)?.actionView?.findViewById<Switch>(R.id.actionbarSwitch)
val switch = menu.findItem(R.id.rulesEnabled)?.actionView?.findViewById<Switch>(R.id.actionbarSwitch)
switch?.isChecked = getPreferences().dnsRulesEnabled.also {
overlay.visibility = if(it) View.GONE else View.VISIBLE
}
......@@ -541,6 +541,10 @@ class DnsRuleFragment : Fragment() {
getPreferences().dnsRulesEnabled = isChecked
overlay.visibility = if(isChecked) View.GONE else View.VISIBLE
}
menu.findItem(R.id.search).setOnMenuItemClickListener {
DnsRuleSearchDialog(requireContext()).show()
true
}
}
private class SourceViewHolder(
......
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:padding="16dp"
android:gravity="center_horizontal"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:text="@string/dialog_dnsrules_search_message"
android:layout_height="wrap_content" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:paddingLeft="32dp"
android:paddingRight="32dp"
android:layout_marginTop="32dp"
android:id="@+id/searchTermTIL"
android:visibility="visible"
app:errorEnabled="true"
android:textColorHint="?android:attr/textColorSecondary"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:hint="@string/hint_searchterm"
android:id="@+id/searchTerm"
android:text="example.com"
tools:ignore="HardcodedText"
android:layout_height="wrap_content"/>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="wrap_content"
android:id="@+id/searchResult"
android:layout_height="wrap_content" />
</LinearLayout>
\ No newline at end of file
......@@ -8,4 +8,10 @@
app:showAsAction="always"
app:actionLayout="@layout/actionbar_switch" />
<item
android:id="@+id/search"
android:icon="@drawable/ic_search"
android:title="@string/hint_search"
app:showAsAction="ifRoom" />
</menu>
\ No newline at end of file
......@@ -212,4 +212,11 @@
<string name="dialog_querylog_information_title">Information about query log</string>
<string name="dialog_querylog_information_message">This list shows you all past queries Nebulo logged while it was active. The list updates close to realtime when Nebulo is currently active.\n\nIcons at the start of the row indicate what happened with the query.\n\nFlag: The host was resolved from the DNS rules, or was blocked by the DNS server\nArrow: Nebulo forwarded the query to the DNS server\nDatabase: Nebulo retrieved the DNS response from the cache</string>
<string name="dialog_dnsrules_search_title">Search for a host</string>
<string name="dialog_dnsrules_search_message">Enter a host below to search for it in the DNS rules.</string>
<string name="dialog_dnsrules_status_not_found">No DNS rule found for this host.</string>
<string name="dialog_dnsrules_status_userrule">This host is part of the DNS rules defined by you.</string>
<string name="dialog_dnsrules_status_fromsource">This host is part of the source %1s</string>
<string name="dialog_dnsrules_status_sourcenotfound">This host is in the DNS rules, but the source is unknown.</string>
</resources>
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment