Commit 3fe69f42 authored by Daniel Wolf's avatar Daniel Wolf
Browse files

Host sources are now saved in the database and displayed in the activity

parent 4e8bd6b3
......@@ -90,7 +90,7 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1'
implementation 'com.frostnerd.utilskt:preferences:1.5.9'
implementation 'com.frostnerd.utilskt:preferences:1.5.11'
implementation 'com.frostnerd.utilskt:navigationdraweractivity:1.3.17'
implementation 'com.frostnerd.utilskt:encrypteddnstunnelproxy:1.5.106'
implementation 'com.frostnerd.utilskt:general:1.0.15'
......
......@@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 6,
"identityHash": "2e8ac76ac9e86bb1cdf9d82108b12c8e",
"identityHash": "207633d815093431015a9df18f6521b0",
"entities": [
{
"tableName": "CachedResponse",
......@@ -142,12 +142,50 @@
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "HostSource",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `source` TEXT NOT NULL, `enabled` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "source",
"columnName": "source",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "enabled",
"columnName": "enabled",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2e8ac76ac9e86bb1cdf9d82108b12c8e')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '207633d815093431015a9df18f6521b0')"
]
}
}
\ No newline at end of file
......@@ -2,15 +2,18 @@ package com.frostnerd.smokescreen.activity
import android.os.Bundle
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.frostnerd.cacheadapter.ListDataSource
import com.frostnerd.cacheadapter.ModelAdapterBuilder
import com.frostnerd.lifecyclemanagement.BaseActivity
import com.frostnerd.lifecyclemanagement.BaseViewHolder
import com.frostnerd.smokescreen.R
import com.frostnerd.smokescreen.database.entities.HostSource
import com.frostnerd.smokescreen.database.getDatabase
import com.frostnerd.smokescreen.getPreferences
import com.frostnerd.smokescreen.util.rules.HostSource
import kotlinx.android.synthetic.main.activity_dns_rules.*
import kotlinx.android.synthetic.main.item_datasource.view.*
/*
* Copyright (C) 2019 Daniel Wolf (Ch4t4r)
......@@ -31,28 +34,84 @@ import kotlinx.android.synthetic.main.activity_dns_rules.*
* You can contact the developer at daniel.wolf@frostnerd.com.
*/
class DnsRuleActivity : BaseActivity() {
private lateinit var sourceAdapter:RecyclerView.Adapter<*>
private lateinit var sourceAdapterList:List<HostSource>
private lateinit var adapterDataSource:ListDataSource<HostSource>
private lateinit var sourceAdapter: RecyclerView.Adapter<*>
private lateinit var sourceAdapterList: MutableList<HostSource>
private lateinit var adapterDataSource: ListDataSource<HostSource>
private var cnt = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_dns_rules)
println(getPreferences().sharedPreferences.all)
addSource.setOnClickListener {
}
sourceAdapterList = getPreferences().hostSources.sortedBy {
it.name
val newSource = HostSource("HerpDerp" + cnt++, "https://test.frostnerd.com/" + cnt)
if (!sourceAdapterList.contains(newSource)) {
val insertPos = sourceAdapterList.indexOfFirst {
it.name > newSource.name
}.let {
println("POS: $it")
if (it - 1 < 0) 0
else it - 1
}
sourceAdapterList.add(insertPos, newSource)
sourceAdapter.notifyItemInserted(insertPos)
getDatabase().hostSourceDao().insert(newSource)
}
}
sourceAdapterList = getDatabase().hostSourceDao().getAll().toMutableList()
adapterDataSource = ListDataSource(sourceAdapterList)
sourceAdapter = ModelAdapterBuilder.withModelAndViewHolder ({ view ->
SourceViewHolder(
view
)
sourceAdapter = ModelAdapterBuilder.withModelAndViewHolder({ view, type ->
if (type == 0) {
SourceViewHolder(view, deleteSource = {
val pos = sourceAdapterList.indexOf(it)
sourceAdapterList.removeAt(pos)
sourceAdapter.notifyItemRemoved(pos)
getDatabase().hostSourceDao().delete(it)
}, changeSourceStatus = { hostSource, enabled ->
hostSource.enabled = enabled
getDatabase().hostSourceDao().update(hostSource)
})
} else {
CustomRulesViewHolder(view, changeSourceStatus = {
getPreferences().customHostsEnabled = it
}, clearRules = {
getDatabase().dnsRuleRepository().deleteAllAsync()
})
}
}, adapterDataSource) {
viewBuilder = { parent, type ->
layoutInflater.inflate(
if (type == 0) R.layout.item_datasource else R.layout.item_datasource_rules,
parent,
false
)
}
getItemCount = {
sourceAdapterList.size + 1
}
bindModelView = { viewHolder, _, data ->
(viewHolder as SourceViewHolder).display(data)
}
bindNonModelView = { viewHolder, position ->
(viewHolder as CustomRulesViewHolder).apply {
this.enabled.isChecked = getPreferences().customHostsEnabled
this.clear.setOnClickListener {
}
}
}
getViewType = { position ->
if (position == getItemCount() - 1) 1
else 0
}
runOnUiThread = {
this@DnsRuleActivity.runOnUiThread(it)
}
}.build()
list.layoutManager = LinearLayoutManager(this)
list.recycledViewPool.setMaxRecycledViews(1, 1)
list.adapter = sourceAdapter
}
override fun getConfiguration(): Configuration {
......@@ -60,7 +119,56 @@ class DnsRuleActivity : BaseActivity() {
}
private class SourceViewHolder(view: View):BaseViewHolder(view) {
private class SourceViewHolder(
view: View,
deleteSource: (HostSource) -> Unit,
changeSourceStatus: (HostSource, enabled: Boolean) -> Unit
) : BaseViewHolder(view) {
val text = view.text
val subText = view.subText
val enabled = view.enable
val delete = view.delete
lateinit var source: HostSource
init {
delete.setOnClickListener {
deleteSource(source)
}
enabled.setOnCheckedChangeListener { _, isChecked ->
changeSourceStatus(source, isChecked)
}
view.cardContent.setOnClickListener {
enabled.isChecked = !enabled.isChecked
}
}
fun display(source: HostSource) {
this.source = source
text.text = source.name
enabled.isChecked = source.enabled
subText.text = source.source
}
override fun destroy() {}
}
private class CustomRulesViewHolder(view: View, changeSourceStatus: (Boolean) -> Unit, clearRules: () -> Unit) :
BaseViewHolder(view) {
val clear = view.delete
val enabled = view.enable
init {
enabled.setOnCheckedChangeListener { _, isChecked ->
changeSourceStatus(isChecked)
}
clear.setOnClickListener {
clearRules()
}
view.cardContent.setOnClickListener {
enabled.isChecked = !enabled.isChecked
}
}
override fun destroy() {}
}
}
\ No newline at end of file
......@@ -5,9 +5,11 @@ import androidx.room.RoomDatabase
import com.frostnerd.smokescreen.database.dao.CachedResponseDao
import com.frostnerd.smokescreen.database.dao.DnsQueryDao
import com.frostnerd.smokescreen.database.dao.DnsRuleDao
import com.frostnerd.smokescreen.database.dao.HostSourceDao
import com.frostnerd.smokescreen.database.entities.CachedResponse
import com.frostnerd.smokescreen.database.entities.DnsQuery
import com.frostnerd.smokescreen.database.entities.DnsRule
import com.frostnerd.smokescreen.database.entities.HostSource
import com.frostnerd.smokescreen.database.repository.CachedResponseRepository
import com.frostnerd.smokescreen.database.repository.DnsQueryRepository
import com.frostnerd.smokescreen.database.repository.DnsRuleRepository
......@@ -31,7 +33,7 @@ import com.frostnerd.smokescreen.database.repository.DnsRuleRepository
* You can contact the developer at daniel.wolf@frostnerd.com.
*/
@Database(entities = [CachedResponse::class, DnsQuery::class, DnsRule::class], version = AppDatabase.currentVersion)
@Database(entities = [CachedResponse::class, DnsQuery::class, DnsRule::class, HostSource::class], version = AppDatabase.currentVersion)
abstract class AppDatabase : RoomDatabase() {
companion object {
const val currentVersion:Int = 6
......@@ -40,6 +42,7 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun cachedResponseDao(): CachedResponseDao
abstract fun dnsQueryDao():DnsQueryDao
abstract fun dnsRuleDao():DnsRuleDao
abstract fun hostSourceDao():HostSourceDao
fun cachedResponseRepository() = CachedResponseRepository(cachedResponseDao())
fun dnsQueryRepository() = DnsQueryRepository(dnsQueryDao())
......
......@@ -52,6 +52,7 @@ private val MIGRATION_4_5 = migration(4, 5) {
private val MIGRATION_5_6 = migration(5, 6) {
Logger.logIfOpen("DB_MIGRATION", "Migrating from 5 to 6")
it.execSQL("CREATE TABLE IF NOT EXISTS `DnsRule` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` INTEGER NOT NULL, `host` TEXT NOT NULL, `ttl` INTEGER NOT NULL, `record` TEXT NOT NULL)")
it.execSQL("CREATE TABLE IF NOT EXISTS `HostSource` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `source` TEXT NOT NULL, `enabled` INTEGER NOT NULL)")
Logger.logIfOpen("DB_MIGRATION", "Migration from 5 to 6 completed")
}
......
package com.frostnerd.smokescreen.database.dao
import androidx.room.Dao
import androidx.room.Query
/*
* Copyright (C) 2019 Daniel Wolf (Ch4t4r)
......@@ -23,4 +24,6 @@ import androidx.room.Dao
@Dao
interface DnsRuleDao {
@Query("DELETE FROM DnsRule")
fun deleteAll()
}
\ No newline at end of file
package com.frostnerd.smokescreen.database.dao
import androidx.room.*
import com.frostnerd.smokescreen.database.entities.CachedResponse
import com.frostnerd.smokescreen.database.entities.HostSource
/*
* Copyright (C) 2019 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.
*/
@Dao
interface HostSourceDao {
@Insert
fun insert(hostSource: HostSource)
@Update
fun update(hostSource: HostSource)
@Delete
fun delete(hostSource: HostSource)
@Query("SELECT * FROM HostSource ORDER BY name ASC")
fun getAll(): List<HostSource>
}
\ No newline at end of file
package com.frostnerd.smokescreen.database.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
/*
* Copyright (C) 2019 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.
*/
@Entity(tableName = "HostSource")
data class HostSource(
var name: String,
var source: String
) {
val isFileSource: Boolean
get() {
return !source.startsWith("http", false)
}
@PrimaryKey(autoGenerate = true) var id: Long = 0
var enabled: Boolean = true
}
\ No newline at end of file
......@@ -2,6 +2,9 @@ package com.frostnerd.smokescreen.database.repository
import com.frostnerd.smokescreen.database.dao.DnsQueryDao
import com.frostnerd.smokescreen.database.dao.DnsRuleDao
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
/*
* Copyright (C) 2019 Daniel Wolf (Ch4t4r)
......@@ -21,4 +24,11 @@ import com.frostnerd.smokescreen.database.dao.DnsRuleDao
*
* You can contact the developer at daniel.wolf@frostnerd.com.
*/
class DnsRuleRepository(val dnsRuleDao: DnsRuleDao)
\ No newline at end of file
class DnsRuleRepository(val dnsRuleDao: DnsRuleDao) {
fun deleteAllAsync(coroutineScope: CoroutineScope = GlobalScope) {
coroutineScope.launch {
dnsRuleDao.deleteAll()
}
}
}
\ No newline at end of file
......@@ -10,8 +10,6 @@ import com.frostnerd.preferenceskt.typedpreferences.cache.ExpirationCacheControl
import com.frostnerd.preferenceskt.typedpreferences.cache.buildCacheStrategy
import com.frostnerd.preferenceskt.typedpreferences.types.*
import com.frostnerd.smokescreen.BuildConfig
import com.frostnerd.smokescreen.util.rules.HostSource
import com.frostnerd.smokescreen.util.rules.HostSourcePreference
import java.util.*
/*
......@@ -241,7 +239,7 @@ class AppSettingsSharedPreferences(context: Context) : AppSettings, SimpleTypedP
}
}, cacheControl)
var hostSources:Set<HostSource> by HostSourcePreference("hostsource").toSetPreference(emptySet())
var customHostsEnabled:Boolean by booleanPref("custom_hosts", false)
}
fun AppSettings.Companion.fromSharedPreferences(context: Context): AppSettingsSharedPreferences {
......
package com.frostnerd.smokescreen.util.rules
import android.content.SharedPreferences
import com.frostnerd.preferenceskt.typedpreferences.TypedPreferences
import com.frostnerd.preferenceskt.typedpreferences.types.PreferenceType
import java.io.File
import kotlin.reflect.KProperty
/*
* Copyright (C) 2019 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.
*/
open class HostSource(var name:String) {
var enabled = true
}
class FileHostSource(name:String, val file: File): HostSource(name)
class HttpHostSource(name:String, val url:String): HostSource(name)
class HostSourcePreference(key:String):PreferenceType<SharedPreferences, HostSource>(key) {
override fun getValue(thisRef: TypedPreferences<SharedPreferences>, property: KProperty<*>): HostSource? {
return if(thisRef.sharedPreferences.contains(key)) {
val type = thisRef.sharedPreferences.getString("${key}_type", null)!!
val name = thisRef.sharedPreferences.getString("${key}_name", null)!!
val source = when(type.toLowerCase()) {
"file" -> FileHostSource(name, File(thisRef.sharedPreferences.getString(key, null)!!))
else -> HttpHostSource(name, thisRef.sharedPreferences.getString(key, null)!!)
}
source.enabled = thisRef.sharedPreferences.getBoolean("${key}_enabled", true)
source
} else {
null
}
}
override fun setValue(thisRef: TypedPreferences<SharedPreferences>, property: KProperty<*>, value: HostSource?) {
thisRef.edit { listener ->
listener(key, value)
if(value == null) {
remove(key)
remove("${key}_type")
remove("${key}_enabled")
remove("${key}_name")
} else {
if(value is FileHostSource) {
putString("${key}_type", "file")
putString(key, value.file.absolutePath)
} else if(value is HttpHostSource) {
putString("${key}_type", "http")
putString(key, value.url)
}
putString("${key}_name", value.name)
putBoolean("${key}_enabled", value.enabled)
}
}
}
}
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M15,16h4v2h-4zM15,8h7v2h-7zM15,12h6v2h-6zM3,18c0,1.1 0.9,2 2,2h6c1.1,0 2,-0.9 2,-2L13,8L3,8v10zM14,5h-3l-1,-1L6,4L5,5L2,5v2h12z"/>
</vector>
......@@ -12,6 +12,7 @@
android:layout_width="wrap_content"
android:id="@+id/addSource"
android:layout_gravity="bottom|end"
android:src="@drawable/ic_plus"
android:layout_height="wrap_content"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
app:cardElevation="0dp"
app:cardCornerRadius="6dp"
app:cardBackgroundColor="?inputElementColor"
android:layout_height="wrap_content">
<RelativeLayout
android:layout_width="match_parent"
android:background="?selectableItemBackground"
android:id="@+id/cardContent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:id="@+id/text"
android:layout_toStartOf="@id/enable"
android:layout_alignParentStart="true"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:id="@+id/subText"
android:layout_below="@id/text"
android:layout_toStartOf="@id/enable"
android:layout_alignParentStart="true"
android:layout_height="wrap_content"/>
<Switch
android:layout_width="wrap_content"
android:id="@+id/enable"
android:layout_toStartOf="@id/delete"
android:layout_centerVertical="true"
android:layout_height="wrap_content"/>
<ImageButton
android:layout_width="48dp"
style="@style/Widget.AppCompat.Button.Borderless"
android:src="@drawable/ic_delete"
android:id="@+id/delete"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:tint="?android:attr/textColor"
android:layout_height="48dp"/>
</RelativeLayout>
</androidx.cardview.widget.CardView>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
app:cardElevation="0dp"
app:cardCornerRadius="6dp"
app:cardBackgroundColor="?inputElementColor"
android:layout_height="wrap_content">