mirror of
https://github.com/karasevm/PrivateDNSAndroid.git
synced 2025-06-28 12:19:57 +00:00
Major refactor
* Replaced server storage backend with Room, allowing for easier further expansion * Add option to disable saved servers * Improved backup handling * Fixed desync bug while dragging servers * Reorganized source file structure * Updated Kotlin version * Updated Java version
This commit is contained in:
parent
eead2a912d
commit
36be36d69b
29 changed files with 909 additions and 435 deletions
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="1.8.22" />
|
<option name="version" value="1.9.10" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
|
@ -1,6 +1,7 @@
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'kotlin-android'
|
id 'kotlin-android'
|
||||||
|
id 'com.google.devtools.ksp'
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
@ -8,8 +9,8 @@ android {
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "ru.karasevm.privatednstoggle"
|
applicationId "ru.karasevm.privatednstoggle"
|
||||||
versionCode 15
|
versionCode 16
|
||||||
versionName "1.8.1"
|
versionName "1.9.0-beta1"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
targetSdkVersion 34
|
targetSdkVersion 34
|
||||||
|
@ -29,11 +30,11 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = '1.8'
|
jvmTarget = '17'
|
||||||
}
|
}
|
||||||
dependenciesInfo {
|
dependenciesInfo {
|
||||||
// Disables dependency metadata when building APKs.
|
// Disables dependency metadata when building APKs.
|
||||||
|
@ -48,6 +49,9 @@ dependencies {
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.13.1'
|
implementation 'androidx.core:core-ktx:1.13.1'
|
||||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||||
|
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||||
|
implementation 'androidx.activity:activity-ktx:1.9.2'
|
||||||
|
implementation 'androidx.fragment:fragment-ktx:1.8.3'
|
||||||
implementation 'com.google.android.material:material:1.12.0'
|
implementation 'com.google.android.material:material:1.12.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'com.google.guava:guava:33.1.0-android'
|
implementation 'com.google.guava:guava:33.1.0-android'
|
||||||
|
@ -60,6 +64,18 @@ dependencies {
|
||||||
implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3'
|
implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3'
|
||||||
compileOnly 'dev.rikka.hidden:stub:4.3.2'
|
compileOnly 'dev.rikka.hidden:stub:4.3.2'
|
||||||
|
|
||||||
|
// Room components
|
||||||
|
def roomVersion = '2.6.1'
|
||||||
|
implementation "androidx.room:room-ktx:$roomVersion"
|
||||||
|
ksp "androidx.room:room-compiler:$roomVersion"
|
||||||
|
androidTestImplementation "androidx.room:room-testing:$roomVersion"
|
||||||
|
|
||||||
|
// Lifecycle components
|
||||||
|
def lifecycleVersion = '2.8.5'
|
||||||
|
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
|
||||||
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
|
||||||
|
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
android:theme="@style/Theme.MyApplication"
|
android:theme="@style/Theme.MyApplication"
|
||||||
android:taskAffinity="${applicationId}.main"
|
android:taskAffinity="${applicationId}.main"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".SettingsDialogActivity"
|
android:name=".ui.SettingsDialogActivity"
|
||||||
android:theme="@style/Theme.Transparent"
|
android:theme="@style/Theme.Transparent"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
|
@ -41,7 +41,7 @@
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".DnsTileService"
|
android:name=".service.DnsTileService"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:icon="@drawable/ic_unknown_black_24dp"
|
android:icon="@drawable/ic_unknown_black_24dp"
|
||||||
android:label="@string/tile_name"
|
android:label="@string/tile_name"
|
||||||
|
|
|
@ -2,8 +2,14 @@ package ru.karasevm.privatednstoggle
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import com.google.android.material.color.DynamicColors
|
import com.google.android.material.color.DynamicColors
|
||||||
|
import ru.karasevm.privatednstoggle.data.DnsServerRepository
|
||||||
|
import ru.karasevm.privatednstoggle.data.database.DnsServerRoomDatabase
|
||||||
|
|
||||||
class PrivateDNSApp : Application() {
|
class PrivateDNSApp : Application() {
|
||||||
|
|
||||||
|
private val database by lazy { DnsServerRoomDatabase.getDatabase(this) }
|
||||||
|
val repository by lazy { DnsServerRepository(database.dnsServerDao()) }
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||||
|
|
|
@ -1,84 +0,0 @@
|
||||||
package ru.karasevm.privatednstoggle
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import java.util.Collections
|
|
||||||
|
|
||||||
class RecyclerAdapter(private val items: MutableList<String>, private val showDragHandle: Boolean, private val onDataChanged: () -> Unit) :
|
|
||||||
RecyclerView.Adapter<RecyclerAdapter.ViewHolder>() {
|
|
||||||
|
|
||||||
var onItemClick: ((Int) -> Unit)? = null
|
|
||||||
var onItemsChanged: ((MutableList<String>) -> Unit)? = null
|
|
||||||
var onDragStart: ((RecyclerAdapter.ViewHolder) -> Unit)? = null
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerAdapter.ViewHolder {
|
|
||||||
val view =
|
|
||||||
LayoutInflater.from(parent.context).inflate(R.layout.recyclerview_row, parent, false)
|
|
||||||
val vh = ViewHolder(view)
|
|
||||||
return vh
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
override fun onBindViewHolder(holder: RecyclerAdapter.ViewHolder, position: Int) {
|
|
||||||
val item = items[position]
|
|
||||||
val parts = item.split(" : ")
|
|
||||||
if (parts.size == 2) {
|
|
||||||
holder.labelTextView.visibility = View.VISIBLE
|
|
||||||
holder.labelTextView.text = parts[0]
|
|
||||||
holder.textView.text = parts[1]
|
|
||||||
} else {
|
|
||||||
holder.labelTextView.visibility = View.GONE
|
|
||||||
holder.textView.text = parts[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showDragHandle) {
|
|
||||||
holder.dragHandle.visibility = View.VISIBLE
|
|
||||||
holder.dragHandle.setOnTouchListener { _, event ->
|
|
||||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
|
||||||
onDragStart?.invoke(holder)
|
|
||||||
}
|
|
||||||
return@setOnTouchListener true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return items.size
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onItemMove(fromPosition: Int, toPosition: Int) {
|
|
||||||
// Swap items in your data list
|
|
||||||
Collections.swap(items, fromPosition, toPosition)
|
|
||||||
notifyItemMoved(fromPosition, toPosition)
|
|
||||||
onItemsChanged?.invoke(items)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
|
||||||
val textView: TextView = itemView.findViewById(R.id.textView)
|
|
||||||
val labelTextView: TextView = itemView.findViewById(R.id.labelTextView)
|
|
||||||
val dragHandle: ImageView = itemView.findViewById(R.id.dragHandle)
|
|
||||||
|
|
||||||
init {
|
|
||||||
itemView.setOnClickListener {
|
|
||||||
onItemClick?.invoke(adapterPosition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setData(newItems: MutableList<String>) {
|
|
||||||
items.run {
|
|
||||||
clear()
|
|
||||||
addAll(newItems)
|
|
||||||
}
|
|
||||||
onDataChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
package ru.karasevm.privatednstoggle.data
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import ru.karasevm.privatednstoggle.model.DnsServer
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface DnsServerDao {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM dns_servers ORDER BY sortOrder ASC")
|
||||||
|
fun getAll(): Flow<List<DnsServer>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM dns_servers WHERE enabled = 1 ORDER BY sortOrder ASC LIMIT 1")
|
||||||
|
suspend fun getFirstEnabled(): DnsServer
|
||||||
|
|
||||||
|
@Query("SELECT * FROM dns_servers WHERE server = :server LIMIT 1")
|
||||||
|
suspend fun getFirstByServer(server: String): DnsServer?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM dns_servers WHERE id = :id")
|
||||||
|
suspend fun getById(id: Int): DnsServer?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM dns_servers " +
|
||||||
|
"WHERE sortOrder > (SELECT sortOrder FROM dns_servers WHERE server = :server) AND enabled = 1 " +
|
||||||
|
"ORDER BY sortOrder ASC " +
|
||||||
|
"LIMIT 1")
|
||||||
|
suspend fun getNextEnabledByServer(server: String): DnsServer?
|
||||||
|
|
||||||
|
@Query("DELETE FROM dns_servers")
|
||||||
|
suspend fun deleteAll()
|
||||||
|
|
||||||
|
@Query("DELETE FROM dns_servers WHERE id = :id")
|
||||||
|
suspend fun deleteById(id: Int)
|
||||||
|
|
||||||
|
@Query("UPDATE dns_servers SET sortOrder = sortOrder + 1 " +
|
||||||
|
"WHERE sortOrder >= :startSortOrder AND sortOrder <= :endSortOrder")
|
||||||
|
suspend fun incrementSortOrder(startSortOrder: Int, endSortOrder: Int = Int.MAX_VALUE)
|
||||||
|
|
||||||
|
@Query("UPDATE dns_servers SET sortOrder = sortOrder - 1 " +
|
||||||
|
"WHERE sortOrder >= :startSortOrder AND sortOrder <= :endSortOrder")
|
||||||
|
suspend fun decrementSortOrder(startSortOrder: Int, endSortOrder: Int = Int.MAX_VALUE)
|
||||||
|
|
||||||
|
@Query("UPDATE dns_servers SET sortOrder = sortOrder - 1 " +
|
||||||
|
"WHERE sortOrder > (SELECT sortOrder FROM dns_servers WHERE id = :id)")
|
||||||
|
suspend fun decrementSortOrderById(id: Int)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
suspend fun deleteAndDecrement(id: Int) {
|
||||||
|
decrementSortOrderById(id)
|
||||||
|
deleteById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("UPDATE dns_servers SET label = :label WHERE id = :id")
|
||||||
|
suspend fun updateLabel(id: Int, label: String)
|
||||||
|
|
||||||
|
@Query("UPDATE dns_servers SET server = :server WHERE id = :id")
|
||||||
|
suspend fun updateServer(id: Int, server: String)
|
||||||
|
|
||||||
|
@Query("UPDATE dns_servers " +
|
||||||
|
"SET server = COALESCE(:server, server), " +
|
||||||
|
" label = COALESCE(:label, label), " +
|
||||||
|
" sortOrder = COALESCE(:sortOrder, sortOrder), " +
|
||||||
|
" enabled = COALESCE(:enabled, enabled) " +
|
||||||
|
"WHERE id = :id")
|
||||||
|
suspend fun update(id: Int, server: String?, label: String?, sortOrder: Int?, enabled: Boolean?)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
suspend fun moveUp(sortOrder: Int, newSortOrder: Int, id: Int){
|
||||||
|
incrementSortOrder(newSortOrder, sortOrder)
|
||||||
|
update(id, null, null, newSortOrder, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
suspend fun moveDown(sortOrder: Int, newSortOrder: Int, id: Int){
|
||||||
|
decrementSortOrder(sortOrder, newSortOrder)
|
||||||
|
update(id, null, null, newSortOrder, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("INSERT INTO dns_servers(server, label, sortOrder, enabled) " +
|
||||||
|
"VALUES(:server, :label, COALESCE((SELECT MAX(sortOrder) + 1 FROM dns_servers), 0), :enabled)")
|
||||||
|
suspend fun insert(server: String, label: String, enabled: Boolean)
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
package ru.karasevm.privatednstoggle.data
|
||||||
|
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import ru.karasevm.privatednstoggle.model.DnsServer
|
||||||
|
|
||||||
|
class DnsServerRepository(private val dnsServerDao: DnsServerDao) {
|
||||||
|
|
||||||
|
val allServers: Flow<List<DnsServer>> = dnsServerDao.getAll()
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
fun getAll() = dnsServerDao.getAll()
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
suspend fun getFirstEnabled() = dnsServerDao.getFirstEnabled()
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
suspend fun getById(id: Int) = dnsServerDao.getById(id)
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
suspend fun getFirstByServer(server: String) = dnsServerDao.getFirstByServer(server)
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
suspend fun getNextByServer(server: String) = dnsServerDao.getNextEnabledByServer(server)
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
suspend fun insert(dnsServer: DnsServer) {
|
||||||
|
dnsServerDao.insert(dnsServer.server, dnsServer.label, dnsServer.enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
suspend fun update(
|
||||||
|
id: Int,
|
||||||
|
server: String?,
|
||||||
|
label: String?,
|
||||||
|
sortOrder: Int?,
|
||||||
|
enabled: Boolean?
|
||||||
|
) {
|
||||||
|
dnsServerDao.update(id, server, label, sortOrder, enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
suspend fun move(sortOrder: Int, newSortOrder: Int, id: Int) {
|
||||||
|
if (sortOrder == newSortOrder) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (newSortOrder > sortOrder) {
|
||||||
|
dnsServerDao.moveDown(sortOrder, newSortOrder, id)
|
||||||
|
} else {
|
||||||
|
dnsServerDao.moveUp(sortOrder, newSortOrder, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
suspend fun delete(id: Int) {
|
||||||
|
dnsServerDao.deleteAndDecrement(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package ru.karasevm.privatednstoggle.data
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.asLiveData
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import ru.karasevm.privatednstoggle.model.DnsServer
|
||||||
|
|
||||||
|
class DnsServerViewModel(private val dnsServerRepository: DnsServerRepository) : ViewModel() {
|
||||||
|
|
||||||
|
val allServers: LiveData<List<DnsServer>> = dnsServerRepository.allServers.asLiveData()
|
||||||
|
|
||||||
|
fun getAll() = dnsServerRepository.getAll()
|
||||||
|
|
||||||
|
suspend fun getById(id: Int) = dnsServerRepository.getById(id)
|
||||||
|
|
||||||
|
fun insert(dnsServer: DnsServer) =
|
||||||
|
viewModelScope.launch {
|
||||||
|
dnsServerRepository.insert(dnsServer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(
|
||||||
|
id: Int,
|
||||||
|
server: String? = null,
|
||||||
|
label: String? = null,
|
||||||
|
sortOrder: Int? = null,
|
||||||
|
enabled: Boolean? = null
|
||||||
|
) = viewModelScope.launch { dnsServerRepository.update(id, server, label, sortOrder, enabled) }
|
||||||
|
|
||||||
|
fun move(sortOrder: Int, newSortOrder: Int, id: Int) =
|
||||||
|
viewModelScope.launch { dnsServerRepository.move(sortOrder, newSortOrder, id) }
|
||||||
|
|
||||||
|
fun delete(id: Int) = viewModelScope.launch { dnsServerRepository.delete(id) }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class DnsServerViewModelFactory(private val dnsServerRepository: DnsServerRepository) :
|
||||||
|
ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
if (modelClass.isAssignableFrom(DnsServerViewModel::class.java)) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return DnsServerViewModel(dnsServerRepository) as T
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException("Unknown ViewModel class")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package ru.karasevm.privatednstoggle.data.database
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import ru.karasevm.privatednstoggle.data.DnsServerDao
|
||||||
|
import ru.karasevm.privatednstoggle.model.DnsServer
|
||||||
|
|
||||||
|
@Database(entities = [DnsServer::class], version = 1, exportSchema = false)
|
||||||
|
abstract class DnsServerRoomDatabase : RoomDatabase() {
|
||||||
|
|
||||||
|
abstract fun dnsServerDao(): DnsServerDao
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Volatile
|
||||||
|
private var INSTANCE: DnsServerRoomDatabase? = null
|
||||||
|
fun getDatabase(context: Context): DnsServerRoomDatabase {
|
||||||
|
val tempInstance = INSTANCE
|
||||||
|
if (tempInstance != null) {
|
||||||
|
return tempInstance
|
||||||
|
}
|
||||||
|
synchronized(this) {
|
||||||
|
val instance = Room.databaseBuilder(
|
||||||
|
context.applicationContext,
|
||||||
|
DnsServerRoomDatabase::class.java,
|
||||||
|
"dns_server_database"
|
||||||
|
).build()
|
||||||
|
INSTANCE = instance
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package ru.karasevm.privatednstoggle.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
// All fields must have default values for proper deserialization
|
||||||
|
@Entity(tableName = "dns_servers")
|
||||||
|
data class DnsServer(
|
||||||
|
@SerializedName("id")
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
val id: Int = 0,
|
||||||
|
@SerializedName("server")
|
||||||
|
val server: String = "",
|
||||||
|
@SerializedName("label")
|
||||||
|
val label: String = "",
|
||||||
|
@SerializedName("enabled")
|
||||||
|
@ColumnInfo(defaultValue = "1")
|
||||||
|
val enabled: Boolean = true,
|
||||||
|
val sortOrder: Int? = null
|
||||||
|
)
|
|
@ -1,4 +1,4 @@
|
||||||
package ru.karasevm.privatednstoggle
|
package ru.karasevm.privatednstoggle.service
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -9,22 +9,33 @@ import android.provider.Settings
|
||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import ru.karasevm.privatednstoggle.utils.DnsServer
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper
|
import kotlinx.coroutines.Dispatchers
|
||||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.autoMode
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.dns_servers
|
import kotlinx.coroutines.cancelChildren
|
||||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.requireUnlock
|
import kotlinx.coroutines.launch
|
||||||
import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils
|
import ru.karasevm.privatednstoggle.PrivateDNSApp
|
||||||
import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils.AUTO_MODE_OPTION_AUTO
|
import ru.karasevm.privatednstoggle.R
|
||||||
import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils.AUTO_MODE_OPTION_OFF_AUTO
|
import ru.karasevm.privatednstoggle.data.DnsServerRepository
|
||||||
import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils.AUTO_MODE_OPTION_PRIVATE
|
import ru.karasevm.privatednstoggle.model.DnsServer
|
||||||
import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils.DNS_MODE_AUTO
|
import ru.karasevm.privatednstoggle.util.PreferenceHelper
|
||||||
import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils.DNS_MODE_OFF
|
import ru.karasevm.privatednstoggle.util.PreferenceHelper.autoMode
|
||||||
import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils.DNS_MODE_PRIVATE
|
import ru.karasevm.privatednstoggle.util.PreferenceHelper.requireUnlock
|
||||||
import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils.checkForPermission
|
import ru.karasevm.privatednstoggle.util.PrivateDNSUtils
|
||||||
|
import ru.karasevm.privatednstoggle.util.PrivateDNSUtils.AUTO_MODE_OPTION_AUTO
|
||||||
|
import ru.karasevm.privatednstoggle.util.PrivateDNSUtils.AUTO_MODE_OPTION_OFF_AUTO
|
||||||
|
import ru.karasevm.privatednstoggle.util.PrivateDNSUtils.AUTO_MODE_OPTION_PRIVATE
|
||||||
|
import ru.karasevm.privatednstoggle.util.PrivateDNSUtils.DNS_MODE_AUTO
|
||||||
|
import ru.karasevm.privatednstoggle.util.PrivateDNSUtils.DNS_MODE_OFF
|
||||||
|
import ru.karasevm.privatednstoggle.util.PrivateDNSUtils.DNS_MODE_PRIVATE
|
||||||
|
import ru.karasevm.privatednstoggle.util.PrivateDNSUtils.checkForPermission
|
||||||
|
|
||||||
class DnsTileService : TileService() {
|
class DnsTileService : TileService() {
|
||||||
|
|
||||||
|
private val repository: DnsServerRepository by lazy { (application as PrivateDNSApp).repository }
|
||||||
|
private val job = SupervisorJob()
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO + job)
|
||||||
|
|
||||||
override fun onTileAdded() {
|
override fun onTileAdded() {
|
||||||
super.onTileAdded()
|
super.onTileAdded()
|
||||||
checkForPermission(this)
|
checkForPermission(this)
|
||||||
|
@ -36,7 +47,7 @@ class DnsTileService : TileService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set's the state of the tile to the next state
|
* Set's the state of the tile and system settings to the next state
|
||||||
*/
|
*/
|
||||||
private fun cycleState() {
|
private fun cycleState() {
|
||||||
val dnsMode = Settings.Global.getString(contentResolver, "private_dns_mode")
|
val dnsMode = Settings.Global.getString(contentResolver, "private_dns_mode")
|
||||||
|
@ -53,24 +64,31 @@ class DnsTileService : TileService() {
|
||||||
} else if (dnsMode == null || dnsMode.equals(DNS_MODE_AUTO, ignoreCase = true)) {
|
} else if (dnsMode == null || dnsMode.equals(DNS_MODE_AUTO, ignoreCase = true)) {
|
||||||
changeDNSServer(DNS_MODE_PRIVATE, null)
|
changeDNSServer(DNS_MODE_PRIVATE, null)
|
||||||
} else if (dnsMode.equals(DNS_MODE_PRIVATE, ignoreCase = true)) {
|
} else if (dnsMode.equals(DNS_MODE_PRIVATE, ignoreCase = true)) {
|
||||||
if (getNextAddress(dnsProvider) == null) {
|
scope.launch {
|
||||||
if (sharedPrefs.autoMode == AUTO_MODE_OPTION_PRIVATE) {
|
if (getNextAddress(dnsProvider) == null) {
|
||||||
changeDNSServer(DNS_MODE_PRIVATE, null)
|
if (sharedPrefs.autoMode == AUTO_MODE_OPTION_PRIVATE) {
|
||||||
} else {
|
changeDNSServer(DNS_MODE_PRIVATE, null)
|
||||||
if (sharedPrefs.autoMode == AUTO_MODE_OPTION_AUTO) {
|
|
||||||
changeDNSServer(DNS_MODE_AUTO, dnsProvider)
|
|
||||||
} else {
|
} else {
|
||||||
changeDNSServer(DNS_MODE_OFF, dnsProvider)
|
if (sharedPrefs.autoMode == AUTO_MODE_OPTION_AUTO) {
|
||||||
|
changeDNSServer(DNS_MODE_AUTO, dnsProvider)
|
||||||
|
} else {
|
||||||
|
changeDNSServer(DNS_MODE_OFF, dnsProvider)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
changeDNSServer(DNS_MODE_PRIVATE, dnsProvider)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
changeDNSServer(DNS_MODE_PRIVATE, dnsProvider)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun changeDNSServer(server: String, dnsProvider: String?) {
|
/**
|
||||||
when (server) {
|
* Sets the state of the tile to the provided values
|
||||||
|
* @param mode dns mode
|
||||||
|
* @param dnsProvider dns provider
|
||||||
|
*/
|
||||||
|
private fun changeDNSServer(mode: String, dnsProvider: String?) {
|
||||||
|
when (mode) {
|
||||||
DNS_MODE_OFF -> {
|
DNS_MODE_OFF -> {
|
||||||
changeTileState(
|
changeTileState(
|
||||||
qsTile,
|
qsTile,
|
||||||
|
@ -94,14 +112,19 @@ class DnsTileService : TileService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
DNS_MODE_PRIVATE -> {
|
DNS_MODE_PRIVATE -> {
|
||||||
changeTileState(
|
scope.launch {
|
||||||
qsTile,
|
val nextDnsServer = getNextAddress(dnsProvider)
|
||||||
Tile.STATE_ACTIVE,
|
if (nextDnsServer != null) {
|
||||||
getNextAddress(dnsProvider)?.label,
|
changeTileState(
|
||||||
R.drawable.ic_private_black_24dp,
|
qsTile,
|
||||||
DNS_MODE_PRIVATE,
|
Tile.STATE_ACTIVE,
|
||||||
getNextAddress(dnsProvider)?.server
|
nextDnsServer.label.ifEmpty { nextDnsServer.server },
|
||||||
)
|
R.drawable.ic_private_black_24dp,
|
||||||
|
DNS_MODE_PRIVATE,
|
||||||
|
getNextAddress(dnsProvider)?.server
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,6 +135,8 @@ class DnsTileService : TileService() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val sharedPrefs = PreferenceHelper.defaultPreference(this)
|
val sharedPrefs = PreferenceHelper.defaultPreference(this)
|
||||||
|
|
||||||
|
// Require unlock to change mode according to user preference
|
||||||
val requireUnlock = sharedPrefs.requireUnlock
|
val requireUnlock = sharedPrefs.requireUnlock
|
||||||
if (isLocked && requireUnlock) {
|
if (isLocked && requireUnlock) {
|
||||||
unlockAndRun(this::cycleState)
|
unlockAndRun(this::cycleState)
|
||||||
|
@ -127,63 +152,48 @@ class DnsTileService : TileService() {
|
||||||
*/
|
*/
|
||||||
private fun refreshTile() {
|
private fun refreshTile() {
|
||||||
val dnsMode = Settings.Global.getString(contentResolver, "private_dns_mode")
|
val dnsMode = Settings.Global.getString(contentResolver, "private_dns_mode")
|
||||||
if (dnsMode.equals(DNS_MODE_OFF, ignoreCase = true)) {
|
when (dnsMode?.lowercase()) {
|
||||||
setTile(
|
DNS_MODE_OFF -> {
|
||||||
qsTile,
|
|
||||||
Tile.STATE_INACTIVE,
|
|
||||||
getString(R.string.dns_off),
|
|
||||||
R.drawable.ic_off_black_24dp
|
|
||||||
)
|
|
||||||
} else if (dnsMode == null) {
|
|
||||||
setTile(
|
|
||||||
qsTile,
|
|
||||||
Tile.STATE_INACTIVE,
|
|
||||||
getString(R.string.dns_unknown),
|
|
||||||
R.drawable.ic_unknown_black_24dp
|
|
||||||
)
|
|
||||||
} else if (dnsMode.equals(DNS_MODE_AUTO, ignoreCase = true)) {
|
|
||||||
setTile(
|
|
||||||
qsTile,
|
|
||||||
Tile.STATE_INACTIVE,
|
|
||||||
getString(R.string.dns_auto),
|
|
||||||
R.drawable.ic_auto_black_24dp
|
|
||||||
)
|
|
||||||
} else if (dnsMode.equals(DNS_MODE_PRIVATE, ignoreCase = true)) {
|
|
||||||
val dnsProvider = Settings.Global.getString(contentResolver, "private_dns_specifier")
|
|
||||||
val sharedPrefs = PreferenceHelper.defaultPreference(this)
|
|
||||||
val items = sharedPrefs.dns_servers.map {
|
|
||||||
val parts = it.split(" : ")
|
|
||||||
if (parts.size == 2)
|
|
||||||
DnsServer(parts[0], parts[1])
|
|
||||||
else
|
|
||||||
DnsServer(parts[0], parts[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (items.isEmpty() || items[0].server == "") {
|
|
||||||
setTile(
|
setTile(
|
||||||
qsTile,
|
qsTile,
|
||||||
Tile.STATE_ACTIVE,
|
Tile.STATE_INACTIVE,
|
||||||
"Google",
|
getString(R.string.dns_off),
|
||||||
R.drawable.ic_private_black_24dp
|
R.drawable.ic_off_black_24dp
|
||||||
)
|
)
|
||||||
} else {
|
}
|
||||||
val index = items.indexOfFirst { it.server == dnsProvider }
|
|
||||||
if (index == -1) {
|
DNS_MODE_AUTO -> {
|
||||||
|
setTile(
|
||||||
|
qsTile,
|
||||||
|
Tile.STATE_INACTIVE,
|
||||||
|
getString(R.string.dns_auto),
|
||||||
|
R.drawable.ic_auto_black_24dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DNS_MODE_PRIVATE -> {
|
||||||
|
scope.launch {
|
||||||
|
val activeAddress =
|
||||||
|
Settings.Global.getString(contentResolver, "private_dns_specifier")
|
||||||
|
val dnsServer = repository.getFirstByServer(activeAddress)
|
||||||
setTile(
|
setTile(
|
||||||
qsTile,
|
qsTile,
|
||||||
Tile.STATE_ACTIVE,
|
Tile.STATE_ACTIVE,
|
||||||
dnsProvider,
|
// display server address if either there is no label or the server is not known
|
||||||
R.drawable.ic_private_black_24dp
|
dnsServer?.label?.ifBlank { activeAddress } ?: activeAddress,
|
||||||
)
|
|
||||||
} else {
|
|
||||||
setTile(
|
|
||||||
qsTile,
|
|
||||||
Tile.STATE_ACTIVE,
|
|
||||||
items[index].label,
|
|
||||||
R.drawable.ic_private_black_24dp
|
R.drawable.ic_private_black_24dp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
setTile(
|
||||||
|
qsTile,
|
||||||
|
Tile.STATE_INACTIVE,
|
||||||
|
getString(R.string.dns_unknown),
|
||||||
|
R.drawable.ic_unknown_black_24dp
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,6 +213,9 @@ class DnsTileService : TileService() {
|
||||||
if (qsTile == null) {
|
if (qsTile == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Receive broadcasts to update the tile when server is changed from the dialog
|
||||||
ContextCompat.registerReceiver(
|
ContextCompat.registerReceiver(
|
||||||
this,
|
this,
|
||||||
broadcastReceiver,
|
broadcastReceiver,
|
||||||
|
@ -219,6 +232,11 @@ class DnsTileService : TileService() {
|
||||||
unregisterReceiver(broadcastReceiver)
|
unregisterReceiver(broadcastReceiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
job.cancelChildren()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates tile to specified parameters
|
* Updates tile to specified parameters
|
||||||
*
|
*
|
||||||
|
@ -261,39 +279,17 @@ class DnsTileService : TileService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets next dns address from preferences,
|
* Gets next dns address from the database,
|
||||||
* if current address is last returns null
|
* if current address is last or unknown returns null
|
||||||
*
|
*
|
||||||
* @param currentAddress currently set address
|
* @param currentAddress currently set address
|
||||||
* @return next address
|
* @return next address
|
||||||
*/
|
*/
|
||||||
private fun getNextAddress(currentAddress: String?): DnsServer? {
|
private suspend fun getNextAddress(currentAddress: String?): DnsServer? {
|
||||||
val sharedPrefs = PreferenceHelper.defaultPreference(this)
|
return if (currentAddress.isNullOrEmpty()) {
|
||||||
val items = sharedPrefs.dns_servers.map {
|
repository.getFirstEnabled()
|
||||||
val parts = it.split(" : ")
|
} else {
|
||||||
// Assuming string is in the format "$label : $server"
|
repository.getNextByServer(currentAddress)
|
||||||
if (parts.size == 2)
|
|
||||||
DnsServer(parts[0], parts[1])
|
|
||||||
else
|
|
||||||
DnsServer(parts[0], parts[0])
|
|
||||||
}.toMutableList()
|
|
||||||
|
|
||||||
// Fallback if list is empty
|
|
||||||
if (items.isEmpty() || items[0].server == "") {
|
|
||||||
items.apply {
|
|
||||||
removeAt(0)
|
|
||||||
add(DnsServer("Google", "dns.google"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val index = items.indexOfFirst { it.server == currentAddress }
|
|
||||||
|
|
||||||
if (index == -1 || currentAddress == null) {
|
|
||||||
return items[0]
|
|
||||||
}
|
|
||||||
if (index == items.size - 1) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return items[index + 1]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package ru.karasevm.privatednstoggle
|
package ru.karasevm.privatednstoggle.ui
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -11,10 +11,14 @@ import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.common.net.InternetDomainName
|
import com.google.common.net.InternetDomainName
|
||||||
|
import ru.karasevm.privatednstoggle.R
|
||||||
import ru.karasevm.privatednstoggle.databinding.DialogAddBinding
|
import ru.karasevm.privatednstoggle.databinding.DialogAddBinding
|
||||||
|
import ru.karasevm.privatednstoggle.model.DnsServer
|
||||||
|
|
||||||
|
|
||||||
class AddServerDialogFragment(private val position: Int?, private val label: String?, private val server: String?) : DialogFragment() {
|
class AddServerDialogFragment(
|
||||||
|
private val dnsServer: DnsServer?
|
||||||
|
) : DialogFragment() {
|
||||||
// Use this instance of the interface to deliver action events
|
// Use this instance of the interface to deliver action events
|
||||||
private lateinit var listener: NoticeDialogListener
|
private lateinit var listener: NoticeDialogListener
|
||||||
|
|
||||||
|
@ -28,9 +32,9 @@ class AddServerDialogFragment(private val position: Int?, private val label: Str
|
||||||
* implement this interface in order to receive event callbacks.
|
* implement this interface in order to receive event callbacks.
|
||||||
* Each method passes the DialogFragment in case the host needs to query it. */
|
* Each method passes the DialogFragment in case the host needs to query it. */
|
||||||
interface NoticeDialogListener {
|
interface NoticeDialogListener {
|
||||||
fun onDialogPositiveClick(label: String? ,server: String)
|
fun onAddDialogPositiveClick(label: String?, server: String)
|
||||||
fun onDialogPositiveClick(label: String?, server: String, position: Int)
|
fun onUpdateDialogPositiveClick(id: Int, server: String, label: String?, enabled: Boolean)
|
||||||
fun onDeleteItemClicked(position: Int)
|
fun onDeleteItemClicked(id: Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override the Fragment.onAttach() method to instantiate the NoticeDialogListener
|
// Override the Fragment.onAttach() method to instantiate the NoticeDialogListener
|
||||||
|
@ -61,17 +65,21 @@ class AddServerDialogFragment(private val position: Int?, private val label: Str
|
||||||
val view = binding.root
|
val view = binding.root
|
||||||
// Inflate and set the layout for the dialog
|
// Inflate and set the layout for the dialog
|
||||||
// Pass null as the parent view because its going in the dialog layout
|
// Pass null as the parent view because its going in the dialog layout
|
||||||
if (position != null) {
|
if (dnsServer != null) {
|
||||||
binding.editTextServerHint.setText(label)
|
binding.editTextServerHint.setText(dnsServer.label)
|
||||||
binding.editTextServerAddr.setText(server)
|
binding.editTextServerAddr.setText(dnsServer.server)
|
||||||
|
binding.serverEnabledSwitch.visibility = android.view.View.VISIBLE
|
||||||
|
binding.serverEnabledSwitch.isChecked = dnsServer.enabled
|
||||||
builder.setTitle(R.string.edit_server).setView(view)
|
builder.setTitle(R.string.edit_server).setView(view)
|
||||||
.setPositiveButton(
|
.setPositiveButton(
|
||||||
R.string.menu_save
|
R.string.menu_save
|
||||||
) { _, _ ->
|
) { _, _ ->
|
||||||
listener.onDialogPositiveClick(
|
listener.onUpdateDialogPositiveClick(
|
||||||
binding.editTextServerHint.text.toString().trim(),
|
dnsServer.id,
|
||||||
binding.editTextServerAddr.text.toString().trim(),
|
binding.editTextServerAddr.text.toString().trim(),
|
||||||
position)
|
binding.editTextServerHint.text.toString().trim(),
|
||||||
|
binding.serverEnabledSwitch.isChecked
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.setNegativeButton(
|
.setNegativeButton(
|
||||||
R.string.cancel
|
R.string.cancel
|
||||||
|
@ -81,17 +89,16 @@ class AddServerDialogFragment(private val position: Int?, private val label: Str
|
||||||
.setNeutralButton(
|
.setNeutralButton(
|
||||||
R.string.delete
|
R.string.delete
|
||||||
) { _, _ ->
|
) { _, _ ->
|
||||||
listener.onDeleteItemClicked(position)
|
listener.onDeleteItemClicked(dnsServer.id)
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
builder.setTitle(R.string.add_server)
|
builder.setTitle(R.string.add_server)
|
||||||
.setView(view)
|
.setView(view)
|
||||||
// Add action buttons
|
// Add action buttons
|
||||||
.setPositiveButton(
|
.setPositiveButton(
|
||||||
R.string.menu_add
|
R.string.menu_add
|
||||||
) { _, _ ->
|
) { _, _ ->
|
||||||
listener.onDialogPositiveClick(
|
listener.onAddDialogPositiveClick(
|
||||||
binding.editTextServerHint.text.toString().trim(),
|
binding.editTextServerHint.text.toString().trim(),
|
||||||
binding.editTextServerAddr.text.toString().trim()
|
binding.editTextServerAddr.text.toString().trim()
|
||||||
)
|
)
|
|
@ -1,17 +1,22 @@
|
||||||
package ru.karasevm.privatednstoggle
|
package ru.karasevm.privatednstoggle.ui
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import ru.karasevm.privatednstoggle.PrivateDNSApp
|
||||||
|
import ru.karasevm.privatednstoggle.R
|
||||||
|
import ru.karasevm.privatednstoggle.data.DnsServerViewModel
|
||||||
|
import ru.karasevm.privatednstoggle.data.DnsServerViewModelFactory
|
||||||
import ru.karasevm.privatednstoggle.databinding.SheetDnsSelectorBinding
|
import ru.karasevm.privatednstoggle.databinding.SheetDnsSelectorBinding
|
||||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.defaultPreference
|
import ru.karasevm.privatednstoggle.model.DnsServer
|
||||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.dns_servers
|
import ru.karasevm.privatednstoggle.util.PrivateDNSUtils
|
||||||
import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils
|
|
||||||
|
|
||||||
class DNSServerDialogFragment : DialogFragment() {
|
class DNSServerDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
|
@ -19,37 +24,36 @@ class DNSServerDialogFragment : DialogFragment() {
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private lateinit var linearLayoutManager: LinearLayoutManager
|
private lateinit var linearLayoutManager: LinearLayoutManager
|
||||||
private lateinit var adapter: RecyclerAdapter
|
private lateinit var adapter: ServerListRecyclerAdapter
|
||||||
private var items = mutableListOf<String>()
|
private var servers: MutableList<DnsServer> = mutableListOf()
|
||||||
private lateinit var sharedPrefs: SharedPreferences
|
private val dnsServerViewModel: DnsServerViewModel by viewModels { DnsServerViewModelFactory((requireActivity().application as PrivateDNSApp).repository) }
|
||||||
|
|
||||||
|
|
||||||
override fun onCreateDialog(
|
override fun onCreateDialog(
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): Dialog {
|
): Dialog {
|
||||||
return activity?.let {
|
return activity?.let {
|
||||||
|
val startIntent = Intent(context, MainActivity::class.java)
|
||||||
|
|
||||||
val builder = MaterialAlertDialogBuilder(it)
|
val builder = MaterialAlertDialogBuilder(it)
|
||||||
val inflater = requireActivity().layoutInflater
|
val inflater = requireActivity().layoutInflater
|
||||||
_binding = SheetDnsSelectorBinding.inflate(inflater)
|
_binding = SheetDnsSelectorBinding.inflate(inflater)
|
||||||
|
|
||||||
linearLayoutManager = LinearLayoutManager(context)
|
linearLayoutManager = LinearLayoutManager(context)
|
||||||
binding.recyclerView.layoutManager = linearLayoutManager
|
binding.recyclerView.layoutManager = linearLayoutManager
|
||||||
|
|
||||||
sharedPrefs = defaultPreference(requireContext())
|
adapter = ServerListRecyclerAdapter(false)
|
||||||
items = sharedPrefs.dns_servers
|
|
||||||
if (items[0] == "") {
|
|
||||||
items.removeAt(0)
|
|
||||||
items.add("dns.google")
|
|
||||||
}
|
|
||||||
|
|
||||||
items.add(0, resources.getString(R.string.dns_auto))
|
|
||||||
items.add(0, resources.getString(R.string.dns_off))
|
|
||||||
|
|
||||||
adapter = RecyclerAdapter(items, false) {}
|
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
|
lifecycleScope.launch {
|
||||||
|
dnsServerViewModel.getAll().collect { s ->
|
||||||
val startIntent = Intent(context, MainActivity::class.java)
|
servers = s.toMutableList()
|
||||||
|
if (servers.isEmpty()) {
|
||||||
|
servers.add(DnsServer(0, "dns.google"))
|
||||||
|
}
|
||||||
|
servers.add(0, DnsServer(-1, resources.getString(R.string.dns_auto)))
|
||||||
|
servers.add(0, DnsServer(-2, resources.getString(R.string.dns_off)))
|
||||||
|
adapter.submitList(servers)
|
||||||
|
}
|
||||||
|
}
|
||||||
builder.setTitle(R.string.select_server)
|
builder.setTitle(R.string.select_server)
|
||||||
.setView(binding.root)
|
.setView(binding.root)
|
||||||
.setPositiveButton(
|
.setPositiveButton(
|
||||||
|
@ -65,9 +69,9 @@ class DNSServerDialogFragment : DialogFragment() {
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
|
|
||||||
adapter.onItemClick = { position ->
|
adapter.onItemClick = { id ->
|
||||||
when (position) {
|
when (id) {
|
||||||
0 -> {
|
OFF_ID -> {
|
||||||
PrivateDNSUtils.setPrivateMode(
|
PrivateDNSUtils.setPrivateMode(
|
||||||
requireActivity().contentResolver,
|
requireActivity().contentResolver,
|
||||||
PrivateDNSUtils.DNS_MODE_OFF
|
PrivateDNSUtils.DNS_MODE_OFF
|
||||||
|
@ -75,7 +79,7 @@ class DNSServerDialogFragment : DialogFragment() {
|
||||||
Toast.makeText(context, R.string.set_to_off_toast, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, R.string.set_to_off_toast, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
1 -> {
|
AUTO_ID -> {
|
||||||
PrivateDNSUtils.setPrivateMode(
|
PrivateDNSUtils.setPrivateMode(
|
||||||
requireActivity().contentResolver,
|
requireActivity().contentResolver,
|
||||||
PrivateDNSUtils.DNS_MODE_AUTO
|
PrivateDNSUtils.DNS_MODE_AUTO
|
||||||
|
@ -84,20 +88,25 @@ class DNSServerDialogFragment : DialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val server = items[position].split(" : ").last()
|
lifecycleScope.launch {
|
||||||
PrivateDNSUtils.setPrivateMode(
|
val server = servers.find { server -> server.id == id }
|
||||||
requireActivity().contentResolver,
|
PrivateDNSUtils.setPrivateMode(
|
||||||
PrivateDNSUtils.DNS_MODE_PRIVATE
|
requireActivity().contentResolver,
|
||||||
)
|
PrivateDNSUtils.DNS_MODE_PRIVATE
|
||||||
PrivateDNSUtils.setPrivateProvider(
|
)
|
||||||
requireActivity().contentResolver,
|
PrivateDNSUtils.setPrivateProvider(
|
||||||
server
|
requireActivity().contentResolver,
|
||||||
)
|
server?.server
|
||||||
Toast.makeText(
|
)
|
||||||
context,
|
Toast.makeText(
|
||||||
getString(R.string.set_to_provider_toast, server),
|
context,
|
||||||
Toast.LENGTH_SHORT
|
getString(
|
||||||
).show()
|
R.string.set_to_provider_toast,
|
||||||
|
server?.label?.ifEmpty { server.server }
|
||||||
|
),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dialog?.dismiss()
|
dialog?.dismiss()
|
||||||
|
@ -113,5 +122,7 @@ class DNSServerDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "DNSServerDialogFragment"
|
const val TAG = "DNSServerDialogFragment"
|
||||||
|
private const val AUTO_ID = -1
|
||||||
|
private const val OFF_ID = -2
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,13 +1,14 @@
|
||||||
package ru.karasevm.privatednstoggle
|
package ru.karasevm.privatednstoggle.ui
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import ru.karasevm.privatednstoggle.R
|
||||||
|
|
||||||
|
|
||||||
class DeleteServerDialogFragment(private val position: Int) : DialogFragment() {
|
class DeleteServerDialogFragment(private val id: Int) : DialogFragment() {
|
||||||
// Use this instance of the interface to deliver action events
|
// Use this instance of the interface to deliver action events
|
||||||
private lateinit var listener: NoticeDialogListener
|
private lateinit var listener: NoticeDialogListener
|
||||||
|
|
||||||
|
@ -15,7 +16,7 @@ class DeleteServerDialogFragment(private val position: Int) : DialogFragment() {
|
||||||
* implement this interface in order to receive event callbacks.
|
* implement this interface in order to receive event callbacks.
|
||||||
* Each method passes the DialogFragment in case the host needs to query it. */
|
* Each method passes the DialogFragment in case the host needs to query it. */
|
||||||
interface NoticeDialogListener {
|
interface NoticeDialogListener {
|
||||||
fun onDialogPositiveClick(position: Int)
|
fun onDeleteDialogPositiveClick(id: Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override the Fragment.onAttach() method to instantiate the NoticeDialogListener
|
// Override the Fragment.onAttach() method to instantiate the NoticeDialogListener
|
||||||
|
@ -43,7 +44,7 @@ class DeleteServerDialogFragment(private val position: Int) : DialogFragment() {
|
||||||
.setPositiveButton(
|
.setPositiveButton(
|
||||||
R.string.delete
|
R.string.delete
|
||||||
) { _, _ ->
|
) { _, _ ->
|
||||||
listener.onDialogPositiveClick(position)
|
listener.onDeleteDialogPositiveClick(id)
|
||||||
}
|
}
|
||||||
.setNegativeButton(
|
.setNegativeButton(
|
||||||
R.string.cancel
|
R.string.cancel
|
|
@ -1,4 +1,4 @@
|
||||||
package ru.karasevm.privatednstoggle
|
package ru.karasevm.privatednstoggle.ui
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
|
@ -18,9 +18,10 @@ import android.view.Menu
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.app.ShareCompat
|
import androidx.core.app.ShareCompat
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper.DOWN
|
import androidx.recyclerview.widget.ItemTouchHelper.DOWN
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper.UP
|
import androidx.recyclerview.widget.ItemTouchHelper.UP
|
||||||
|
@ -31,16 +32,21 @@ import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.JsonSyntaxException
|
import com.google.gson.JsonSyntaxException
|
||||||
import com.google.gson.ToNumberPolicy
|
import com.google.gson.ToNumberPolicy
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||||
import rikka.shizuku.Shizuku
|
import rikka.shizuku.Shizuku
|
||||||
import rikka.shizuku.ShizukuBinderWrapper
|
import rikka.shizuku.ShizukuBinderWrapper
|
||||||
import rikka.shizuku.ShizukuProvider
|
import rikka.shizuku.ShizukuProvider
|
||||||
import rikka.shizuku.SystemServiceHelper
|
import rikka.shizuku.SystemServiceHelper
|
||||||
|
import ru.karasevm.privatednstoggle.PrivateDNSApp
|
||||||
|
import ru.karasevm.privatednstoggle.R
|
||||||
|
import ru.karasevm.privatednstoggle.data.DnsServerViewModel
|
||||||
|
import ru.karasevm.privatednstoggle.data.DnsServerViewModelFactory
|
||||||
import ru.karasevm.privatednstoggle.databinding.ActivityMainBinding
|
import ru.karasevm.privatednstoggle.databinding.ActivityMainBinding
|
||||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper
|
import ru.karasevm.privatednstoggle.model.DnsServer
|
||||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.dns_servers
|
import ru.karasevm.privatednstoggle.util.BackupUtils
|
||||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.export
|
import ru.karasevm.privatednstoggle.util.PreferenceHelper
|
||||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.import
|
import ru.karasevm.privatednstoggle.util.PreferenceHelper.dns_servers
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogListener,
|
class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogListener,
|
||||||
|
@ -50,81 +56,129 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
private var items = mutableListOf<String>()
|
private var items = mutableListOf<String>()
|
||||||
private lateinit var sharedPrefs: SharedPreferences
|
private lateinit var sharedPrefs: SharedPreferences
|
||||||
private lateinit var adapter: RecyclerAdapter
|
private lateinit var adapter: ServerListRecyclerAdapter
|
||||||
private lateinit var clipboard: ClipboardManager
|
private lateinit var clipboard: ClipboardManager
|
||||||
private lateinit var gson: Gson
|
private lateinit var gson: Gson
|
||||||
|
private val dnsServerViewModel: DnsServerViewModel by viewModels { DnsServerViewModelFactory((application as PrivateDNSApp).repository) }
|
||||||
|
|
||||||
private val itemTouchHelper by lazy {
|
private val itemTouchHelper by lazy {
|
||||||
val simpleItemTouchCallback = object : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) {
|
val simpleItemTouchCallback =
|
||||||
|
object : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) {
|
||||||
|
var dragFrom = -1
|
||||||
|
var dragTo = -1
|
||||||
|
|
||||||
override fun onMove(
|
override fun onMove(
|
||||||
recyclerView: RecyclerView,
|
recyclerView: RecyclerView,
|
||||||
viewHolder: RecyclerView.ViewHolder,
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
target: RecyclerView.ViewHolder
|
target: RecyclerView.ViewHolder
|
||||||
): Boolean {
|
): Boolean {
|
||||||
adapter.onItemMove(viewHolder.adapterPosition, target.adapterPosition)
|
if (dragFrom == viewHolder.bindingAdapterPosition && dragTo == target.bindingAdapterPosition) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
// store the drag position
|
||||||
|
if (dragFrom == -1) dragFrom = viewHolder.bindingAdapterPosition
|
||||||
|
dragTo = target.bindingAdapterPosition
|
||||||
|
adapter.onItemMove(
|
||||||
|
viewHolder.bindingAdapterPosition,
|
||||||
|
target.bindingAdapterPosition
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
|
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
|
||||||
|
|
||||||
override fun onSelectedChanged(
|
override fun onSelectedChanged(
|
||||||
viewHolder: RecyclerView.ViewHolder?, actionState: Int
|
viewHolder: RecyclerView.ViewHolder?, actionState: Int
|
||||||
) {
|
) {
|
||||||
super.onSelectedChanged(viewHolder, actionState)
|
super.onSelectedChanged(viewHolder, actionState)
|
||||||
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
|
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
|
||||||
viewHolder?.itemView?.apply {
|
viewHolder?.itemView?.apply {
|
||||||
// Example: Elevate the view
|
// Example: Elevate the view
|
||||||
elevation = 8f
|
elevation = 8f
|
||||||
alpha = 0.5f
|
alpha = 0.5f
|
||||||
setBackgroundColor(Color.GRAY)
|
setBackgroundColor(Color.GRAY)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun clearView(
|
override fun clearView(
|
||||||
recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder
|
recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder
|
||||||
) {
|
) {
|
||||||
super.clearView(recyclerView, viewHolder)
|
super.clearView(recyclerView, viewHolder)
|
||||||
viewHolder.itemView.apply {
|
viewHolder.itemView.apply {
|
||||||
// Reset the appearance
|
// Reset the appearance
|
||||||
elevation = 0f
|
elevation = 0f
|
||||||
alpha = 1.0f
|
alpha = 1.0f
|
||||||
setBackgroundColor(Color.TRANSPARENT)
|
setBackgroundColor(Color.TRANSPARENT)
|
||||||
|
}
|
||||||
|
// commit the change to the db
|
||||||
|
dnsServerViewModel.move(
|
||||||
|
dragFrom,
|
||||||
|
dragTo,
|
||||||
|
(viewHolder as ServerListRecyclerAdapter.DnsServerViewHolder).id
|
||||||
|
)
|
||||||
|
dragTo = -1
|
||||||
|
dragFrom = -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
ItemTouchHelper(simpleItemTouchCallback)
|
ItemTouchHelper(simpleItemTouchCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun importSettings(json: String) {
|
private fun importSettings(json: String) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val objectType = object : TypeToken<Map<String, Any>>() {}.type
|
val data: BackupUtils.Backup = gson.fromJson(json, BackupUtils.Backup::class.java)
|
||||||
val data: Map<String, Any> = gson.fromJson(json, objectType)
|
BackupUtils.import(data, dnsServerViewModel, sharedPrefs)
|
||||||
sharedPrefs.import(data)
|
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
this, getString(R.string.import_success), Toast.LENGTH_SHORT
|
this, getString(R.string.import_success), Toast.LENGTH_SHORT
|
||||||
).show()
|
).show()
|
||||||
ActivityCompat.recreate(this)
|
|
||||||
}.onFailure { exception ->
|
}.onFailure { exception ->
|
||||||
Log.e("IMPORT", "Import failed", exception)
|
runCatching {
|
||||||
when (exception) {
|
val objectType = object : TypeToken<Map<String, Any>>() {}.type
|
||||||
is JsonSyntaxException -> {
|
val data: Map<String, Any> = gson.fromJson(json, objectType)
|
||||||
Toast.makeText(
|
BackupUtils.importLegacy(data, dnsServerViewModel, sharedPrefs)
|
||||||
this, getString(R.string.import_failure_json), Toast.LENGTH_SHORT
|
}.onSuccess {
|
||||||
).show()
|
Toast.makeText(
|
||||||
}
|
this, getString(R.string.import_success), Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}.onFailure {
|
||||||
|
Log.e("IMPORT", "Import failed", exception)
|
||||||
|
when (exception) {
|
||||||
|
is JsonSyntaxException -> {
|
||||||
|
Toast.makeText(
|
||||||
|
this, getString(R.string.import_failure_json), Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
this, getString(R.string.import_failure), Toast.LENGTH_SHORT
|
this, getString(R.string.import_failure), Toast.LENGTH_SHORT
|
||||||
).show()
|
).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate the SharedPreferences server list to Room
|
||||||
|
*/
|
||||||
|
private fun migrateServerList() {
|
||||||
|
if (sharedPrefs.dns_servers.isNotEmpty() && sharedPrefs.dns_servers[0] != "") {
|
||||||
|
Log.i(
|
||||||
|
"migrate",
|
||||||
|
"existing sharedPrefs list: ${sharedPrefs.dns_servers} ${sharedPrefs.dns_servers.size}"
|
||||||
|
)
|
||||||
|
sharedPrefs.dns_servers.forEach { server ->
|
||||||
|
val parts = server.split(" : ").toMutableList()
|
||||||
|
if (parts.size != 2) parts.add(0, "")
|
||||||
|
Log.i("migrate", "migrating: $server -> $parts")
|
||||||
|
dnsServerViewModel.insert(DnsServer(0, parts[1], parts[0]))
|
||||||
|
}
|
||||||
|
sharedPrefs.dns_servers = emptyList<String>().toMutableList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
@ -141,38 +195,41 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
||||||
clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
gson = GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create()
|
gson = GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create()
|
||||||
|
|
||||||
|
migrateServerList()
|
||||||
|
|
||||||
items = sharedPrefs.dns_servers
|
items = sharedPrefs.dns_servers
|
||||||
if (items[0] == "") {
|
if (items[0] == "") {
|
||||||
items.removeAt(0)
|
items.removeAt(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateEmptyView()
|
adapter = ServerListRecyclerAdapter(true)
|
||||||
adapter = RecyclerAdapter(items, true) { updateEmptyView() }
|
binding.recyclerView.adapter = adapter
|
||||||
adapter.onItemClick = { position ->
|
|
||||||
val data = items[position].split(" : ")
|
dnsServerViewModel.allServers.observe(this) { servers ->
|
||||||
val label: String?
|
adapter.submitList(servers)
|
||||||
val server: String
|
if (servers.isEmpty()) {
|
||||||
if (data.size == 2) {
|
binding.emptyView.visibility = View.VISIBLE
|
||||||
label = data[0]
|
binding.emptyViewHint.visibility = View.VISIBLE
|
||||||
server = data[1]
|
} else {
|
||||||
|
binding.emptyView.visibility = View.GONE
|
||||||
|
binding.emptyViewHint.visibility = View.GONE
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
label = null
|
|
||||||
server = data[0]
|
|
||||||
}
|
|
||||||
val newFragment = AddServerDialogFragment(position, label, server)
|
|
||||||
newFragment.show(supportFragmentManager, "edit_server")
|
|
||||||
}
|
}
|
||||||
adapter.onItemsChanged = { swappedItems ->
|
adapter.onItemClick = { id ->
|
||||||
items = swappedItems
|
dnsServerViewModel.viewModelScope.launch {
|
||||||
sharedPrefs.dns_servers = swappedItems
|
val server = dnsServerViewModel.getById(id)
|
||||||
updateEmptyView()
|
if (server != null) {
|
||||||
|
val newFragment =
|
||||||
|
AddServerDialogFragment(server)
|
||||||
|
newFragment.show(supportFragmentManager, "edit_server")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
adapter.onDragStart = { viewHolder ->
|
adapter.onDragStart = { viewHolder ->
|
||||||
itemTouchHelper.startDrag(viewHolder)
|
itemTouchHelper.startDrag(viewHolder)
|
||||||
}
|
}
|
||||||
binding.floatingActionButton.setOnClickListener {
|
binding.floatingActionButton.setOnClickListener {
|
||||||
val newFragment = AddServerDialogFragment(null, null, null)
|
val newFragment = AddServerDialogFragment(null)
|
||||||
newFragment.show(supportFragmentManager, "add_server")
|
newFragment.show(supportFragmentManager, "add_server")
|
||||||
}
|
}
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
|
@ -190,31 +247,40 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.export_settings_clipboard -> {
|
R.id.export_settings_clipboard -> {
|
||||||
val data = sharedPrefs.export()
|
dnsServerViewModel.viewModelScope.launch {
|
||||||
val jsonData = gson.toJson(data)
|
val data = BackupUtils.export(dnsServerViewModel, sharedPrefs)
|
||||||
clipboard.setPrimaryClip(ClipData.newPlainText("", jsonData))
|
val jsonData = gson.toJson(data)
|
||||||
// Only show a toast for Android 12 and lower.
|
clipboard.setPrimaryClip(ClipData.newPlainText("", jsonData))
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) Toast.makeText(
|
// Only show a toast for Android 12 and lower.
|
||||||
this, getString(R.string.copy_success), Toast.LENGTH_SHORT
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) Toast.makeText(
|
||||||
).show()
|
applicationContext, getString(R.string.copy_success), Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.export_settings_share -> {
|
R.id.export_settings_share -> {
|
||||||
val data = sharedPrefs.export()
|
val activityContext = this
|
||||||
val jsonData = gson.toJson(data)
|
dnsServerViewModel.viewModelScope.launch {
|
||||||
ShareCompat.IntentBuilder(this).setText(jsonData).setType("text/plain")
|
val data = BackupUtils.export(dnsServerViewModel, sharedPrefs)
|
||||||
.startChooser()
|
val jsonData = gson.toJson(data)
|
||||||
|
ShareCompat.IntentBuilder(activityContext).setText(jsonData)
|
||||||
|
.setType("text/plain")
|
||||||
|
.startChooser()
|
||||||
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.export_settings_file -> {
|
R.id.export_settings_file -> {
|
||||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
dnsServerViewModel.viewModelScope.launch {
|
||||||
type = "text/plain"
|
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||||
putExtra(Intent.EXTRA_TITLE, "private-dns-export")
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = "text/plain"
|
||||||
|
putExtra(Intent.EXTRA_TITLE, "private-dns-export")
|
||||||
|
}
|
||||||
|
saveResultLauncher.launch(intent)
|
||||||
}
|
}
|
||||||
saveResultLauncher.launch(intent)
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,22 +314,12 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateEmptyView() {
|
|
||||||
if (items.isEmpty()) {
|
|
||||||
binding.emptyView.visibility = View.VISIBLE
|
|
||||||
binding.emptyViewHint.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
binding.emptyView.visibility = View.GONE
|
|
||||||
binding.emptyViewHint.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var saveResultLauncher =
|
private var saveResultLauncher =
|
||||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
if (result.resultCode == RESULT_OK) {
|
if (result.resultCode == RESULT_OK) {
|
||||||
val data: Intent? = result.data
|
val data: Intent? = result.data
|
||||||
data?.data?.also { uri ->
|
data?.data?.also { uri ->
|
||||||
val jsonData = gson.toJson(sharedPrefs.export())
|
val jsonData = gson.toJson(BackupUtils.export(dnsServerViewModel, sharedPrefs))
|
||||||
val contentResolver = applicationContext.contentResolver
|
val contentResolver = applicationContext.contentResolver
|
||||||
runCatching {
|
runCatching {
|
||||||
contentResolver.openOutputStream(uri)?.use { outputStream ->
|
contentResolver.openOutputStream(uri)?.use { outputStream ->
|
||||||
|
@ -359,46 +415,58 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
||||||
Shizuku.removeRequestPermissionResultListener(this::onRequestPermissionResult)
|
Shizuku.removeRequestPermissionResultListener(this::onRequestPermissionResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDeleteItemClicked(position: Int) {
|
/**
|
||||||
val newFragment = DeleteServerDialogFragment(position)
|
* Show the dialog for deleting the server
|
||||||
|
* @param id The server id
|
||||||
|
*/
|
||||||
|
override fun onDeleteItemClicked(id: Int) {
|
||||||
|
val newFragment = DeleteServerDialogFragment(id)
|
||||||
newFragment.show(supportFragmentManager, "delete_server")
|
newFragment.show(supportFragmentManager, "delete_server")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogPositiveClick(label: String?, server: String) {
|
/**
|
||||||
|
* Callback for adding the server
|
||||||
|
* @param label The label
|
||||||
|
* @param server The server
|
||||||
|
*/
|
||||||
|
override fun onAddDialogPositiveClick(label: String?, server: String) {
|
||||||
if (server.isEmpty()) {
|
if (server.isEmpty()) {
|
||||||
Toast.makeText(this, R.string.server_length_error, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.server_length_error, Toast.LENGTH_SHORT).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (label.isNullOrEmpty()) {
|
if (label.isNullOrEmpty()) {
|
||||||
items.add(server)
|
dnsServerViewModel.insert(DnsServer(0, server))
|
||||||
} else {
|
} else {
|
||||||
items.add("$label : $server")
|
dnsServerViewModel.insert(DnsServer(0, server, label))
|
||||||
}
|
}
|
||||||
adapter.setData(items.toMutableList())
|
|
||||||
binding.recyclerView.adapter?.notifyItemInserted(items.size - 1)
|
|
||||||
sharedPrefs.dns_servers = items
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogPositiveClick(position: Int) {
|
/**
|
||||||
items.removeAt(position)
|
* Callback for deleting the server
|
||||||
adapter.setData(items.toMutableList())
|
* @param id The server id
|
||||||
adapter.notifyItemRemoved(position)
|
*/
|
||||||
sharedPrefs.dns_servers = items
|
override fun onDeleteDialogPositiveClick(id: Int) {
|
||||||
|
dnsServerViewModel.delete(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogPositiveClick(label: String?, server: String, position: Int) {
|
/**
|
||||||
|
* Callback for updating the server
|
||||||
|
* @param label New label
|
||||||
|
* @param server New server address
|
||||||
|
* @param id The server id
|
||||||
|
*/
|
||||||
|
override fun onUpdateDialogPositiveClick(
|
||||||
|
id: Int,
|
||||||
|
server: String,
|
||||||
|
label: String?,
|
||||||
|
enabled: Boolean
|
||||||
|
) {
|
||||||
if (server.isEmpty()) {
|
if (server.isEmpty()) {
|
||||||
Toast.makeText(this, R.string.server_length_error, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.server_length_error, Toast.LENGTH_SHORT).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (label.isNullOrEmpty()) {
|
dnsServerViewModel.update(id, server, label, null, enabled)
|
||||||
items[position] = server
|
|
||||||
} else {
|
|
||||||
items[position] = "$label : $server"
|
|
||||||
}
|
|
||||||
adapter.notifyItemChanged(position)
|
|
||||||
sharedPrefs.dns_servers = items
|
|
||||||
binding.recyclerView.adapter?.notifyItemChanged(position)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -1,14 +1,15 @@
|
||||||
package ru.karasevm.privatednstoggle
|
package ru.karasevm.privatednstoggle.ui
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import ru.karasevm.privatednstoggle.R
|
||||||
import ru.karasevm.privatednstoggle.databinding.DialogOptionsBinding
|
import ru.karasevm.privatednstoggle.databinding.DialogOptionsBinding
|
||||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper
|
import ru.karasevm.privatednstoggle.util.PreferenceHelper
|
||||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.autoMode
|
import ru.karasevm.privatednstoggle.util.PreferenceHelper.autoMode
|
||||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.requireUnlock
|
import ru.karasevm.privatednstoggle.util.PreferenceHelper.requireUnlock
|
||||||
import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils
|
import ru.karasevm.privatednstoggle.util.PrivateDNSUtils
|
||||||
|
|
||||||
class OptionsDialogFragment : DialogFragment() {
|
class OptionsDialogFragment : DialogFragment() {
|
||||||
private var _binding: DialogOptionsBinding? = null
|
private var _binding: DialogOptionsBinding? = null
|
||||||
|
@ -27,6 +28,7 @@ class OptionsDialogFragment : DialogFragment() {
|
||||||
.setView(view)
|
.setView(view)
|
||||||
.setPositiveButton(R.string.ok, null)
|
.setPositiveButton(R.string.ok, null)
|
||||||
builder.create()
|
builder.create()
|
||||||
|
|
||||||
} ?: throw IllegalStateException("Activity cannot be null")
|
} ?: throw IllegalStateException("Activity cannot be null")
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
package ru.karasevm.privatednstoggle.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import ru.karasevm.privatednstoggle.R
|
||||||
|
import ru.karasevm.privatednstoggle.model.DnsServer
|
||||||
|
|
||||||
|
|
||||||
|
class ServerListRecyclerAdapter(private val showDragHandle: Boolean) :
|
||||||
|
RecyclerView.Adapter<ServerListRecyclerAdapter.DnsServerViewHolder>() {
|
||||||
|
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DnsServerViewHolder {
|
||||||
|
val view =
|
||||||
|
LayoutInflater.from(parent.context).inflate(R.layout.recyclerview_row, parent, false)
|
||||||
|
val vh = DnsServerViewHolder(view)
|
||||||
|
return vh
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return items.size
|
||||||
|
}
|
||||||
|
|
||||||
|
var onItemClick: ((Int) -> Unit)? = null
|
||||||
|
var onDragStart: ((DnsServerViewHolder) -> Unit)? = null
|
||||||
|
private var items: MutableList<DnsServer> = mutableListOf()
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
override fun onBindViewHolder(holder: DnsServerViewHolder, position: Int) {
|
||||||
|
val item = items[position]
|
||||||
|
if (item.label.isNotEmpty()) {
|
||||||
|
holder.labelTextView.text = item.label
|
||||||
|
holder.labelTextView.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
holder.labelTextView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
holder.serverTextView.text = item.server
|
||||||
|
holder.id = item.id
|
||||||
|
if (item.enabled) {
|
||||||
|
holder.labelTextView.alpha = 1f
|
||||||
|
holder.serverTextView.alpha = 1f
|
||||||
|
} else {
|
||||||
|
holder.labelTextView.alpha = 0.5f
|
||||||
|
holder.serverTextView.alpha = 0.5f
|
||||||
|
}
|
||||||
|
if (showDragHandle) {
|
||||||
|
holder.dragHandle.visibility = View.VISIBLE
|
||||||
|
holder.dragHandle.setOnTouchListener { _, event ->
|
||||||
|
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||||
|
onDragStart?.invoke(holder)
|
||||||
|
}
|
||||||
|
return@setOnTouchListener true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update server position in memory
|
||||||
|
* @param fromPosition old position
|
||||||
|
* @param toPosition new position
|
||||||
|
*/
|
||||||
|
fun onItemMove(fromPosition: Int, toPosition: Int) {
|
||||||
|
items.add(toPosition, items.removeAt(fromPosition))
|
||||||
|
notifyItemMoved(fromPosition, toPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiffCallback(
|
||||||
|
private val oldList: List<DnsServer>, private var newList: List<DnsServer>
|
||||||
|
) : DiffUtil.Callback() {
|
||||||
|
|
||||||
|
override fun getOldListSize(): Int {
|
||||||
|
return oldList.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getNewListSize(): Int {
|
||||||
|
return newList.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
|
val oldItem = oldList[oldItemPosition]
|
||||||
|
val newItem = newList[newItemPosition]
|
||||||
|
return oldItem.id == newItem.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
|
val oldItem = oldList[oldItemPosition]
|
||||||
|
val newItem = newList[newItemPosition]
|
||||||
|
return oldItem.server == newItem.server && oldItem.label == newItem.label && oldItem.enabled == newItem.enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit list to adapter
|
||||||
|
* @param list list to submit
|
||||||
|
*/
|
||||||
|
fun submitList(list: List<DnsServer>) {
|
||||||
|
val diffCallback = DiffCallback(items, list)
|
||||||
|
val diffResult = DiffUtil.calculateDiff(diffCallback)
|
||||||
|
items.clear()
|
||||||
|
items.addAll(list)
|
||||||
|
diffResult.dispatchUpdatesTo(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class DnsServerViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||||
|
val labelTextView: TextView = view.findViewById(R.id.labelTextView)
|
||||||
|
val serverTextView: TextView = view.findViewById(R.id.textView)
|
||||||
|
val dragHandle: ImageView = itemView.findViewById(R.id.dragHandle)
|
||||||
|
var id = 0
|
||||||
|
|
||||||
|
init {
|
||||||
|
view.setOnClickListener {
|
||||||
|
onItemClick?.invoke(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package ru.karasevm.privatednstoggle
|
package ru.karasevm.privatednstoggle.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
|
@ -0,0 +1,75 @@
|
||||||
|
package ru.karasevm.privatednstoggle.util
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import ru.karasevm.privatednstoggle.data.DnsServerViewModel
|
||||||
|
import ru.karasevm.privatednstoggle.model.DnsServer
|
||||||
|
import ru.karasevm.privatednstoggle.util.PreferenceHelper.AUTO_MODE
|
||||||
|
import ru.karasevm.privatednstoggle.util.PreferenceHelper.DNS_SERVERS
|
||||||
|
import ru.karasevm.privatednstoggle.util.PreferenceHelper.REQUIRE_UNLOCK
|
||||||
|
import ru.karasevm.privatednstoggle.util.PreferenceHelper.autoMode
|
||||||
|
import ru.karasevm.privatednstoggle.util.PreferenceHelper.requireUnlock
|
||||||
|
|
||||||
|
object BackupUtils {
|
||||||
|
data class Backup(
|
||||||
|
@SerializedName("dns_servers") val dnsServers: List<DnsServer>,
|
||||||
|
@SerializedName("auto_mode") val autoMode: Int?,
|
||||||
|
@SerializedName("require_unlock") val requireUnlock: Boolean?,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports all the preferences
|
||||||
|
* @param viewModel View model
|
||||||
|
* @param sharedPreferences Shared preferences
|
||||||
|
*/
|
||||||
|
fun export(viewModel: DnsServerViewModel, sharedPreferences: SharedPreferences): Backup {
|
||||||
|
return Backup(
|
||||||
|
viewModel.allServers.value ?: listOf(),
|
||||||
|
sharedPreferences.autoMode,
|
||||||
|
sharedPreferences.requireUnlock
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports all the preferences
|
||||||
|
* @param backup Deserialized backup
|
||||||
|
* @param viewModel View model
|
||||||
|
*/
|
||||||
|
fun import(
|
||||||
|
backup: Backup,
|
||||||
|
viewModel: DnsServerViewModel,
|
||||||
|
sharedPreferences: SharedPreferences
|
||||||
|
) {
|
||||||
|
backup.dnsServers.forEach { viewModel.insert(it) }
|
||||||
|
sharedPreferences.autoMode = backup.autoMode ?: sharedPreferences.autoMode
|
||||||
|
sharedPreferences.requireUnlock = backup.requireUnlock ?: sharedPreferences.requireUnlock
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports old server list
|
||||||
|
* @param map Deserialized backup
|
||||||
|
* @param viewModel View model
|
||||||
|
* @param sharedPreferences Shared preferences
|
||||||
|
*/
|
||||||
|
fun importLegacy(
|
||||||
|
map: Map<String, Any>,
|
||||||
|
viewModel: DnsServerViewModel,
|
||||||
|
sharedPreferences: SharedPreferences
|
||||||
|
) {
|
||||||
|
map[DNS_SERVERS]?.let { servers ->
|
||||||
|
if (servers is String) {
|
||||||
|
val serverList = servers.split(",")
|
||||||
|
serverList.forEach { server ->
|
||||||
|
val parts = server.split(" : ")
|
||||||
|
if (parts.size == 2) {
|
||||||
|
viewModel.insert(DnsServer(0, parts[1], parts[0]))
|
||||||
|
} else {
|
||||||
|
viewModel.insert(DnsServer(0, server, ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sharedPreferences.autoMode = map[AUTO_MODE] as? Int ?: 0
|
||||||
|
sharedPreferences.requireUnlock = map[REQUIRE_UNLOCK] as? Boolean ?: false
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package ru.karasevm.privatednstoggle.utils
|
package ru.karasevm.privatednstoggle.util
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
|
@ -23,18 +23,23 @@ object PrivateDNSUtils {
|
||||||
private const val PRIVATE_DNS_MODE = "private_dns_mode"
|
private const val PRIVATE_DNS_MODE = "private_dns_mode"
|
||||||
private const val PRIVATE_DNS_PROVIDER = "private_dns_specifier"
|
private const val PRIVATE_DNS_PROVIDER = "private_dns_specifier"
|
||||||
|
|
||||||
|
|
||||||
|
// Gets the system dns mode
|
||||||
fun getPrivateMode(contentResolver: ContentResolver): String {
|
fun getPrivateMode(contentResolver: ContentResolver): String {
|
||||||
return Settings.Global.getString(contentResolver, PRIVATE_DNS_MODE)
|
return Settings.Global.getString(contentResolver, PRIVATE_DNS_MODE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gets the system dns provider
|
||||||
fun getPrivateProvider(contentResolver: ContentResolver): String {
|
fun getPrivateProvider(contentResolver: ContentResolver): String {
|
||||||
return Settings.Global.getString(contentResolver, PRIVATE_DNS_PROVIDER)
|
return Settings.Global.getString(contentResolver, PRIVATE_DNS_PROVIDER)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sets the system dns mode
|
||||||
fun setPrivateMode(contentResolver: ContentResolver, value: String) {
|
fun setPrivateMode(contentResolver: ContentResolver, value: String) {
|
||||||
Settings.Global.putString(contentResolver, PRIVATE_DNS_MODE, value)
|
Settings.Global.putString(contentResolver, PRIVATE_DNS_MODE, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sets the system dns provider
|
||||||
fun setPrivateProvider(contentResolver: ContentResolver, value: String?) {
|
fun setPrivateProvider(contentResolver: ContentResolver, value: String?) {
|
||||||
Settings.Global.putString(contentResolver, PRIVATE_DNS_PROVIDER, value)
|
Settings.Global.putString(contentResolver, PRIVATE_DNS_PROVIDER, value)
|
||||||
}
|
}
|
|
@ -1,13 +1,13 @@
|
||||||
package ru.karasevm.privatednstoggle.utils
|
package ru.karasevm.privatednstoggle.util
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
|
||||||
object PreferenceHelper {
|
object PreferenceHelper {
|
||||||
|
|
||||||
private const val DNS_SERVERS = "dns_servers"
|
const val DNS_SERVERS = "dns_servers"
|
||||||
private const val AUTO_MODE = "auto_mode"
|
const val AUTO_MODE = "auto_mode"
|
||||||
private const val REQUIRE_UNLOCK = "require_unlock"
|
const val REQUIRE_UNLOCK = "require_unlock"
|
||||||
|
|
||||||
fun defaultPreference(context: Context): SharedPreferences =
|
fun defaultPreference(context: Context): SharedPreferences =
|
||||||
context.getSharedPreferences("app_prefs", 0)
|
context.getSharedPreferences("app_prefs", 0)
|
||||||
|
@ -38,6 +38,7 @@ object PreferenceHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var SharedPreferences.autoMode
|
var SharedPreferences.autoMode
|
||||||
get() = getInt(AUTO_MODE, PrivateDNSUtils.AUTO_MODE_OPTION_OFF)
|
get() = getInt(AUTO_MODE, PrivateDNSUtils.AUTO_MODE_OPTION_OFF)
|
||||||
set(value) {
|
set(value) {
|
||||||
|
@ -53,24 +54,4 @@ object PreferenceHelper {
|
||||||
it.put(REQUIRE_UNLOCK to value)
|
it.put(REQUIRE_UNLOCK to value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// export all the preferences
|
|
||||||
fun SharedPreferences.export() = mapOf(
|
|
||||||
DNS_SERVERS to getString(DNS_SERVERS, ""),
|
|
||||||
AUTO_MODE to autoMode,
|
|
||||||
REQUIRE_UNLOCK to requireUnlock
|
|
||||||
)
|
|
||||||
|
|
||||||
// import all the preferences
|
|
||||||
fun SharedPreferences.import(map: Map<String, Any>) {
|
|
||||||
editMe {
|
|
||||||
map.forEach { (key, value) ->
|
|
||||||
if (value is Number) {
|
|
||||||
it.put(key to value.toInt())
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
it.put(key to value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,7 +0,0 @@
|
||||||
package ru.karasevm.privatednstoggle.utils
|
|
||||||
|
|
||||||
data class DnsServer(val label: String, val server: String) {
|
|
||||||
override fun toString(): String {
|
|
||||||
return "$label : $server"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,7 +4,7 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".MainActivity">
|
tools:context=".ui.MainActivity">
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:id="@+id/topAppBarLayout"
|
android:id="@+id/topAppBarLayout"
|
||||||
|
@ -48,7 +48,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:text="@string/empty_hint"
|
android:text="@string/empty_hint"
|
||||||
android:textColor="@color/material_dynamic_neutral50"
|
android:textColor="@color/material_on_surface_emphasis_medium"
|
||||||
android:visibility="visible"
|
android:visibility="visible"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
|
|
@ -4,6 +4,20 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
|
android:id="@+id/serverEnabledSwitch"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:maxWidth="320dp"
|
||||||
|
android:text="@string/add_server_enabled"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:id="@+id/editTextServerHintLayout"
|
android:id="@+id/editTextServerHintLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -12,7 +26,7 @@
|
||||||
android:hint="@string/add_edittext_label_hint"
|
android:hint="@string/add_edittext_label_hint"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
app:layout_constraintTop_toBottomOf="@id/serverEnabledSwitch">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/editTextServerHint"
|
android:id="@+id/editTextServerHint"
|
||||||
|
|
|
@ -54,10 +54,11 @@
|
||||||
|
|
||||||
<com.google.android.material.materialswitch.MaterialSwitch
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
android:id="@+id/requireUnlockSwitch"
|
android:id="@+id/requireUnlockSwitch"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="24dp"
|
android:layout_marginStart="24dp"
|
||||||
android:layout_marginEnd="24dp"
|
android:layout_marginEnd="24dp"
|
||||||
|
android:maxWidth="320dp"
|
||||||
android:text="@string/require_unlock_setting"
|
android:text="@string/require_unlock_setting"
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,4 @@
|
||||||
<color name="md_theme_dark_secondary">#CCC2DC</color>
|
<color name="md_theme_dark_secondary">#CCC2DC</color>
|
||||||
<color name="md_theme_dark_tertiary">#EFB8C8</color>
|
<color name="md_theme_dark_tertiary">#EFB8C8</color>
|
||||||
<color name="md_theme_dark_background">#1C1B1F</color>
|
<color name="md_theme_dark_background">#1C1B1F</color>
|
||||||
|
|
||||||
<color name="material_dynamic_neutral50">#747879</color>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -47,4 +47,5 @@
|
||||||
<string name="edit_server">Edit server</string>
|
<string name="edit_server">Edit server</string>
|
||||||
<string name="no_servers_added">No Servers Added</string>
|
<string name="no_servers_added">No Servers Added</string>
|
||||||
<string name="empty_hint">Tap on the button below to add one</string>
|
<string name="empty_hint">Tap on the button below to add one</string>
|
||||||
|
<string name="add_server_enabled">Enabled</string>
|
||||||
</resources>
|
</resources>
|
|
@ -5,13 +5,17 @@ buildscript {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:8.5.1'
|
classpath 'com.android.tools.build:gradle:8.5.2'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10"
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
plugins {
|
||||||
|
id 'org.jetbrains.kotlin.android' version '1.9.10' apply false
|
||||||
|
id "com.google.devtools.ksp" version "1.9.10-1.0.13" apply false
|
||||||
|
}
|
||||||
|
|
||||||
tasks.register('clean', Delete) {
|
tasks.register('clean', Delete) {
|
||||||
delete rootProject.layout.buildDirectory
|
delete rootProject.layout.buildDirectory
|
||||||
|
|
7
fastlane/metadata/android/en-US/changelogs/16.txt
Normal file
7
fastlane/metadata/android/en-US/changelogs/16.txt
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
- Replaced server storage backend with Room, allowing for easier further expansion
|
||||||
|
- Add option to disable saved servers
|
||||||
|
- Improved backup handling
|
||||||
|
- Fixed desync bug while dragging servers
|
||||||
|
- Reorganized source file structure
|
||||||
|
- Updated Kotlin version
|
||||||
|
- Updated Java version
|
Loading…
Add table
Add a link
Reference in a new issue