mirror of
https://github.com/karasevm/PrivateDNSAndroid.git
synced 2025-06-28 20:29:56 +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"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.8.22" />
|
||||
<option name="version" value="1.9.10" />
|
||||
</component>
|
||||
</project>
|
|
@ -1,6 +1,7 @@
|
|||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
id 'com.google.devtools.ksp'
|
||||
}
|
||||
|
||||
android {
|
||||
|
@ -8,8 +9,8 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
applicationId "ru.karasevm.privatednstoggle"
|
||||
versionCode 15
|
||||
versionName "1.8.1"
|
||||
versionCode 16
|
||||
versionName "1.9.0-beta1"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
targetSdkVersion 34
|
||||
|
@ -29,11 +30,11 @@ android {
|
|||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
jvmTarget = '17'
|
||||
}
|
||||
dependenciesInfo {
|
||||
// Disables dependency metadata when building APKs.
|
||||
|
@ -48,6 +49,9 @@ dependencies {
|
|||
|
||||
implementation 'androidx.core:core-ktx:1.13.1'
|
||||
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 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'com.google.guava:guava:33.1.0-android'
|
||||
|
@ -60,6 +64,18 @@ dependencies {
|
|||
implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3'
|
||||
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'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
android:exported="true"
|
||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:name=".ui.MainActivity"
|
||||
android:theme="@style/Theme.MyApplication"
|
||||
android:taskAffinity="${applicationId}.main"
|
||||
android:launchMode="singleInstance"
|
||||
|
@ -31,7 +31,7 @@
|
|||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".SettingsDialogActivity"
|
||||
android:name=".ui.SettingsDialogActivity"
|
||||
android:theme="@style/Theme.Transparent"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true">
|
||||
|
@ -41,7 +41,7 @@
|
|||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".DnsTileService"
|
||||
android:name=".service.DnsTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_unknown_black_24dp"
|
||||
android:label="@string/tile_name"
|
||||
|
|
|
@ -2,8 +2,14 @@ package ru.karasevm.privatednstoggle
|
|||
|
||||
import android.app.Application
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import ru.karasevm.privatednstoggle.data.DnsServerRepository
|
||||
import ru.karasevm.privatednstoggle.data.database.DnsServerRoomDatabase
|
||||
|
||||
class PrivateDNSApp : Application() {
|
||||
|
||||
private val database by lazy { DnsServerRoomDatabase.getDatabase(this) }
|
||||
val repository by lazy { DnsServerRepository(database.dnsServerDao()) }
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
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.Context
|
||||
|
@ -9,22 +9,33 @@ import android.provider.Settings
|
|||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.core.content.ContextCompat
|
||||
import ru.karasevm.privatednstoggle.utils.DnsServer
|
||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper
|
||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.autoMode
|
||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.dns_servers
|
||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.requireUnlock
|
||||
import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils
|
||||
import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils.AUTO_MODE_OPTION_AUTO
|
||||
import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils.AUTO_MODE_OPTION_OFF_AUTO
|
||||
import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils.AUTO_MODE_OPTION_PRIVATE
|
||||
import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils.DNS_MODE_AUTO
|
||||
import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils.DNS_MODE_OFF
|
||||
import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils.DNS_MODE_PRIVATE
|
||||
import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils.checkForPermission
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.karasevm.privatednstoggle.PrivateDNSApp
|
||||
import ru.karasevm.privatednstoggle.R
|
||||
import ru.karasevm.privatednstoggle.data.DnsServerRepository
|
||||
import ru.karasevm.privatednstoggle.model.DnsServer
|
||||
import ru.karasevm.privatednstoggle.util.PreferenceHelper
|
||||
import ru.karasevm.privatednstoggle.util.PreferenceHelper.autoMode
|
||||
import ru.karasevm.privatednstoggle.util.PreferenceHelper.requireUnlock
|
||||
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() {
|
||||
|
||||
private val repository: DnsServerRepository by lazy { (application as PrivateDNSApp).repository }
|
||||
private val job = SupervisorJob()
|
||||
private val scope = CoroutineScope(Dispatchers.IO + job)
|
||||
|
||||
override fun onTileAdded() {
|
||||
super.onTileAdded()
|
||||
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() {
|
||||
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)) {
|
||||
changeDNSServer(DNS_MODE_PRIVATE, null)
|
||||
} else if (dnsMode.equals(DNS_MODE_PRIVATE, ignoreCase = true)) {
|
||||
if (getNextAddress(dnsProvider) == null) {
|
||||
if (sharedPrefs.autoMode == AUTO_MODE_OPTION_PRIVATE) {
|
||||
changeDNSServer(DNS_MODE_PRIVATE, null)
|
||||
} else {
|
||||
if (sharedPrefs.autoMode == AUTO_MODE_OPTION_AUTO) {
|
||||
changeDNSServer(DNS_MODE_AUTO, dnsProvider)
|
||||
scope.launch {
|
||||
if (getNextAddress(dnsProvider) == null) {
|
||||
if (sharedPrefs.autoMode == AUTO_MODE_OPTION_PRIVATE) {
|
||||
changeDNSServer(DNS_MODE_PRIVATE, null)
|
||||
} 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 -> {
|
||||
changeTileState(
|
||||
qsTile,
|
||||
|
@ -94,14 +112,19 @@ class DnsTileService : TileService() {
|
|||
}
|
||||
|
||||
DNS_MODE_PRIVATE -> {
|
||||
changeTileState(
|
||||
qsTile,
|
||||
Tile.STATE_ACTIVE,
|
||||
getNextAddress(dnsProvider)?.label,
|
||||
R.drawable.ic_private_black_24dp,
|
||||
DNS_MODE_PRIVATE,
|
||||
getNextAddress(dnsProvider)?.server
|
||||
)
|
||||
scope.launch {
|
||||
val nextDnsServer = getNextAddress(dnsProvider)
|
||||
if (nextDnsServer != null) {
|
||||
changeTileState(
|
||||
qsTile,
|
||||
Tile.STATE_ACTIVE,
|
||||
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
|
||||
}
|
||||
val sharedPrefs = PreferenceHelper.defaultPreference(this)
|
||||
|
||||
// Require unlock to change mode according to user preference
|
||||
val requireUnlock = sharedPrefs.requireUnlock
|
||||
if (isLocked && requireUnlock) {
|
||||
unlockAndRun(this::cycleState)
|
||||
|
@ -127,63 +152,48 @@ class DnsTileService : TileService() {
|
|||
*/
|
||||
private fun refreshTile() {
|
||||
val dnsMode = Settings.Global.getString(contentResolver, "private_dns_mode")
|
||||
if (dnsMode.equals(DNS_MODE_OFF, ignoreCase = true)) {
|
||||
setTile(
|
||||
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 == "") {
|
||||
when (dnsMode?.lowercase()) {
|
||||
DNS_MODE_OFF -> {
|
||||
setTile(
|
||||
qsTile,
|
||||
Tile.STATE_ACTIVE,
|
||||
"Google",
|
||||
R.drawable.ic_private_black_24dp
|
||||
Tile.STATE_INACTIVE,
|
||||
getString(R.string.dns_off),
|
||||
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(
|
||||
qsTile,
|
||||
Tile.STATE_ACTIVE,
|
||||
dnsProvider,
|
||||
R.drawable.ic_private_black_24dp
|
||||
)
|
||||
} else {
|
||||
setTile(
|
||||
qsTile,
|
||||
Tile.STATE_ACTIVE,
|
||||
items[index].label,
|
||||
// display server address if either there is no label or the server is not known
|
||||
dnsServer?.label?.ifBlank { activeAddress } ?: activeAddress,
|
||||
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) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Receive broadcasts to update the tile when server is changed from the dialog
|
||||
ContextCompat.registerReceiver(
|
||||
this,
|
||||
broadcastReceiver,
|
||||
|
@ -219,6 +232,11 @@ class DnsTileService : TileService() {
|
|||
unregisterReceiver(broadcastReceiver)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
job.cancelChildren()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates tile to specified parameters
|
||||
*
|
||||
|
@ -261,39 +279,17 @@ class DnsTileService : TileService() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets next dns address from preferences,
|
||||
* if current address is last returns null
|
||||
* Gets next dns address from the database,
|
||||
* if current address is last or unknown returns null
|
||||
*
|
||||
* @param currentAddress currently set address
|
||||
* @return next address
|
||||
*/
|
||||
private fun getNextAddress(currentAddress: String?): DnsServer? {
|
||||
val sharedPrefs = PreferenceHelper.defaultPreference(this)
|
||||
val items = sharedPrefs.dns_servers.map {
|
||||
val parts = it.split(" : ")
|
||||
// Assuming string is in the format "$label : $server"
|
||||
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"))
|
||||
}
|
||||
private suspend fun getNextAddress(currentAddress: String?): DnsServer? {
|
||||
return if (currentAddress.isNullOrEmpty()) {
|
||||
repository.getFirstEnabled()
|
||||
} else {
|
||||
repository.getNextByServer(currentAddress)
|
||||
}
|
||||
|
||||
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.content.Context
|
||||
|
@ -11,10 +11,14 @@ import androidx.appcompat.app.AlertDialog
|
|||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.common.net.InternetDomainName
|
||||
import ru.karasevm.privatednstoggle.R
|
||||
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
|
||||
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.
|
||||
* Each method passes the DialogFragment in case the host needs to query it. */
|
||||
interface NoticeDialogListener {
|
||||
fun onDialogPositiveClick(label: String? ,server: String)
|
||||
fun onDialogPositiveClick(label: String?, server: String, position: Int)
|
||||
fun onDeleteItemClicked(position: Int)
|
||||
fun onAddDialogPositiveClick(label: String?, server: String)
|
||||
fun onUpdateDialogPositiveClick(id: Int, server: String, label: String?, enabled: Boolean)
|
||||
fun onDeleteItemClicked(id: Int)
|
||||
}
|
||||
|
||||
// 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
|
||||
// Inflate and set the layout for the dialog
|
||||
// Pass null as the parent view because its going in the dialog layout
|
||||
if (position != null) {
|
||||
binding.editTextServerHint.setText(label)
|
||||
binding.editTextServerAddr.setText(server)
|
||||
if (dnsServer != null) {
|
||||
binding.editTextServerHint.setText(dnsServer.label)
|
||||
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)
|
||||
.setPositiveButton(
|
||||
R.string.menu_save
|
||||
) { _, _ ->
|
||||
listener.onDialogPositiveClick(
|
||||
binding.editTextServerHint.text.toString().trim(),
|
||||
binding.editTextServerAddr.text.toString().trim(),
|
||||
position)
|
||||
) { _, _ ->
|
||||
listener.onUpdateDialogPositiveClick(
|
||||
dnsServer.id,
|
||||
binding.editTextServerAddr.text.toString().trim(),
|
||||
binding.editTextServerHint.text.toString().trim(),
|
||||
binding.serverEnabledSwitch.isChecked
|
||||
)
|
||||
}
|
||||
.setNegativeButton(
|
||||
R.string.cancel
|
||||
|
@ -81,17 +89,16 @@ class AddServerDialogFragment(private val position: Int?, private val label: Str
|
|||
.setNeutralButton(
|
||||
R.string.delete
|
||||
) { _, _ ->
|
||||
listener.onDeleteItemClicked(position)
|
||||
}
|
||||
}
|
||||
else {
|
||||
listener.onDeleteItemClicked(dnsServer.id)
|
||||
}
|
||||
} else {
|
||||
builder.setTitle(R.string.add_server)
|
||||
.setView(view)
|
||||
// Add action buttons
|
||||
.setPositiveButton(
|
||||
R.string.menu_add
|
||||
) { _, _ ->
|
||||
listener.onDialogPositiveClick(
|
||||
listener.onAddDialogPositiveClick(
|
||||
binding.editTextServerHint.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.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.utils.PreferenceHelper.defaultPreference
|
||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.dns_servers
|
||||
import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils
|
||||
import ru.karasevm.privatednstoggle.model.DnsServer
|
||||
import ru.karasevm.privatednstoggle.util.PrivateDNSUtils
|
||||
|
||||
class DNSServerDialogFragment : DialogFragment() {
|
||||
|
||||
|
@ -19,37 +24,36 @@ class DNSServerDialogFragment : DialogFragment() {
|
|||
private val binding get() = _binding!!
|
||||
|
||||
private lateinit var linearLayoutManager: LinearLayoutManager
|
||||
private lateinit var adapter: RecyclerAdapter
|
||||
private var items = mutableListOf<String>()
|
||||
private lateinit var sharedPrefs: SharedPreferences
|
||||
private lateinit var adapter: ServerListRecyclerAdapter
|
||||
private var servers: MutableList<DnsServer> = mutableListOf()
|
||||
private val dnsServerViewModel: DnsServerViewModel by viewModels { DnsServerViewModelFactory((requireActivity().application as PrivateDNSApp).repository) }
|
||||
|
||||
|
||||
override fun onCreateDialog(
|
||||
savedInstanceState: Bundle?
|
||||
): Dialog {
|
||||
return activity?.let {
|
||||
val startIntent = Intent(context, MainActivity::class.java)
|
||||
|
||||
val builder = MaterialAlertDialogBuilder(it)
|
||||
val inflater = requireActivity().layoutInflater
|
||||
_binding = SheetDnsSelectorBinding.inflate(inflater)
|
||||
|
||||
linearLayoutManager = LinearLayoutManager(context)
|
||||
binding.recyclerView.layoutManager = linearLayoutManager
|
||||
|
||||
sharedPrefs = defaultPreference(requireContext())
|
||||
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) {}
|
||||
adapter = ServerListRecyclerAdapter(false)
|
||||
binding.recyclerView.adapter = adapter
|
||||
|
||||
|
||||
val startIntent = Intent(context, MainActivity::class.java)
|
||||
|
||||
lifecycleScope.launch {
|
||||
dnsServerViewModel.getAll().collect { s ->
|
||||
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)
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(
|
||||
|
@ -65,9 +69,9 @@ class DNSServerDialogFragment : DialogFragment() {
|
|||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
adapter.onItemClick = { position ->
|
||||
when (position) {
|
||||
0 -> {
|
||||
adapter.onItemClick = { id ->
|
||||
when (id) {
|
||||
OFF_ID -> {
|
||||
PrivateDNSUtils.setPrivateMode(
|
||||
requireActivity().contentResolver,
|
||||
PrivateDNSUtils.DNS_MODE_OFF
|
||||
|
@ -75,7 +79,7 @@ class DNSServerDialogFragment : DialogFragment() {
|
|||
Toast.makeText(context, R.string.set_to_off_toast, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
1 -> {
|
||||
AUTO_ID -> {
|
||||
PrivateDNSUtils.setPrivateMode(
|
||||
requireActivity().contentResolver,
|
||||
PrivateDNSUtils.DNS_MODE_AUTO
|
||||
|
@ -84,20 +88,25 @@ class DNSServerDialogFragment : DialogFragment() {
|
|||
}
|
||||
|
||||
else -> {
|
||||
val server = items[position].split(" : ").last()
|
||||
PrivateDNSUtils.setPrivateMode(
|
||||
requireActivity().contentResolver,
|
||||
PrivateDNSUtils.DNS_MODE_PRIVATE
|
||||
)
|
||||
PrivateDNSUtils.setPrivateProvider(
|
||||
requireActivity().contentResolver,
|
||||
server
|
||||
)
|
||||
Toast.makeText(
|
||||
context,
|
||||
getString(R.string.set_to_provider_toast, server),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
lifecycleScope.launch {
|
||||
val server = servers.find { server -> server.id == id }
|
||||
PrivateDNSUtils.setPrivateMode(
|
||||
requireActivity().contentResolver,
|
||||
PrivateDNSUtils.DNS_MODE_PRIVATE
|
||||
)
|
||||
PrivateDNSUtils.setPrivateProvider(
|
||||
requireActivity().contentResolver,
|
||||
server?.server
|
||||
)
|
||||
Toast.makeText(
|
||||
context,
|
||||
getString(
|
||||
R.string.set_to_provider_toast,
|
||||
server?.label?.ifEmpty { server.server }
|
||||
),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
dialog?.dismiss()
|
||||
|
@ -113,5 +122,7 @@ class DNSServerDialogFragment : DialogFragment() {
|
|||
|
||||
companion object {
|
||||
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.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
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
|
||||
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.
|
||||
* Each method passes the DialogFragment in case the host needs to query it. */
|
||||
interface NoticeDialogListener {
|
||||
fun onDialogPositiveClick(position: Int)
|
||||
fun onDeleteDialogPositiveClick(id: Int)
|
||||
}
|
||||
|
||||
// Override the Fragment.onAttach() method to instantiate the NoticeDialogListener
|
||||
|
@ -43,7 +44,7 @@ class DeleteServerDialogFragment(private val position: Int) : DialogFragment() {
|
|||
.setPositiveButton(
|
||||
R.string.delete
|
||||
) { _, _ ->
|
||||
listener.onDialogPositiveClick(position)
|
||||
listener.onDeleteDialogPositiveClick(id)
|
||||
}
|
||||
.setNegativeButton(
|
||||
R.string.cancel
|
|
@ -1,4 +1,4 @@
|
|||
package ru.karasevm.privatednstoggle
|
||||
package ru.karasevm.privatednstoggle.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ClipData
|
||||
|
@ -18,9 +18,10 @@ import android.view.Menu
|
|||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.ShareCompat
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.ItemTouchHelper.DOWN
|
||||
import androidx.recyclerview.widget.ItemTouchHelper.UP
|
||||
|
@ -31,16 +32,21 @@ import com.google.gson.GsonBuilder
|
|||
import com.google.gson.JsonSyntaxException
|
||||
import com.google.gson.ToNumberPolicy
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kotlinx.coroutines.launch
|
||||
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||
import rikka.shizuku.Shizuku
|
||||
import rikka.shizuku.ShizukuBinderWrapper
|
||||
import rikka.shizuku.ShizukuProvider
|
||||
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.utils.PreferenceHelper
|
||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.dns_servers
|
||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.export
|
||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.import
|
||||
import ru.karasevm.privatednstoggle.model.DnsServer
|
||||
import ru.karasevm.privatednstoggle.util.BackupUtils
|
||||
import ru.karasevm.privatednstoggle.util.PreferenceHelper
|
||||
import ru.karasevm.privatednstoggle.util.PreferenceHelper.dns_servers
|
||||
|
||||
|
||||
class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogListener,
|
||||
|
@ -50,81 +56,129 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
|||
private lateinit var binding: ActivityMainBinding
|
||||
private var items = mutableListOf<String>()
|
||||
private lateinit var sharedPrefs: SharedPreferences
|
||||
private lateinit var adapter: RecyclerAdapter
|
||||
private lateinit var adapter: ServerListRecyclerAdapter
|
||||
private lateinit var clipboard: ClipboardManager
|
||||
private lateinit var gson: Gson
|
||||
private val dnsServerViewModel: DnsServerViewModel by viewModels { DnsServerViewModelFactory((application as PrivateDNSApp).repository) }
|
||||
|
||||
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(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
adapter.onItemMove(viewHolder.adapterPosition, target.adapterPosition)
|
||||
return true
|
||||
}
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
if (dragFrom == viewHolder.bindingAdapterPosition && dragTo == target.bindingAdapterPosition) {
|
||||
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(
|
||||
viewHolder: RecyclerView.ViewHolder?, actionState: Int
|
||||
) {
|
||||
super.onSelectedChanged(viewHolder, actionState)
|
||||
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
|
||||
viewHolder?.itemView?.apply {
|
||||
// Example: Elevate the view
|
||||
elevation = 8f
|
||||
alpha = 0.5f
|
||||
setBackgroundColor(Color.GRAY)
|
||||
override fun onSelectedChanged(
|
||||
viewHolder: RecyclerView.ViewHolder?, actionState: Int
|
||||
) {
|
||||
super.onSelectedChanged(viewHolder, actionState)
|
||||
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
|
||||
viewHolder?.itemView?.apply {
|
||||
// Example: Elevate the view
|
||||
elevation = 8f
|
||||
alpha = 0.5f
|
||||
setBackgroundColor(Color.GRAY)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearView(
|
||||
recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder
|
||||
) {
|
||||
super.clearView(recyclerView, viewHolder)
|
||||
viewHolder.itemView.apply {
|
||||
// Reset the appearance
|
||||
elevation = 0f
|
||||
alpha = 1.0f
|
||||
setBackgroundColor(Color.TRANSPARENT)
|
||||
override fun clearView(
|
||||
recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder
|
||||
) {
|
||||
super.clearView(recyclerView, viewHolder)
|
||||
viewHolder.itemView.apply {
|
||||
// Reset the appearance
|
||||
elevation = 0f
|
||||
alpha = 1.0f
|
||||
setBackgroundColor(Color.TRANSPARENT)
|
||||
}
|
||||
// commit the change to the db
|
||||
dnsServerViewModel.move(
|
||||
dragFrom,
|
||||
dragTo,
|
||||
(viewHolder as ServerListRecyclerAdapter.DnsServerViewHolder).id
|
||||
)
|
||||
dragTo = -1
|
||||
dragFrom = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
ItemTouchHelper(simpleItemTouchCallback)
|
||||
}
|
||||
|
||||
private fun importSettings(json: String) {
|
||||
runCatching {
|
||||
val objectType = object : TypeToken<Map<String, Any>>() {}.type
|
||||
val data: Map<String, Any> = gson.fromJson(json, objectType)
|
||||
sharedPrefs.import(data)
|
||||
val data: BackupUtils.Backup = gson.fromJson(json, BackupUtils.Backup::class.java)
|
||||
BackupUtils.import(data, dnsServerViewModel, sharedPrefs)
|
||||
}.onSuccess {
|
||||
Toast.makeText(
|
||||
this, getString(R.string.import_success), Toast.LENGTH_SHORT
|
||||
).show()
|
||||
ActivityCompat.recreate(this)
|
||||
}.onFailure { exception ->
|
||||
Log.e("IMPORT", "Import failed", exception)
|
||||
when (exception) {
|
||||
is JsonSyntaxException -> {
|
||||
Toast.makeText(
|
||||
this, getString(R.string.import_failure_json), Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
runCatching {
|
||||
val objectType = object : TypeToken<Map<String, Any>>() {}.type
|
||||
val data: Map<String, Any> = gson.fromJson(json, objectType)
|
||||
BackupUtils.importLegacy(data, dnsServerViewModel, sharedPrefs)
|
||||
}.onSuccess {
|
||||
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 -> {
|
||||
Toast.makeText(
|
||||
this, getString(R.string.import_failure), Toast.LENGTH_SHORT
|
||||
).show()
|
||||
else -> {
|
||||
Toast.makeText(
|
||||
this, getString(R.string.import_failure), Toast.LENGTH_SHORT
|
||||
).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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
@ -141,38 +195,41 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
|||
clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||
gson = GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create()
|
||||
|
||||
migrateServerList()
|
||||
|
||||
items = sharedPrefs.dns_servers
|
||||
if (items[0] == "") {
|
||||
items.removeAt(0)
|
||||
}
|
||||
|
||||
updateEmptyView()
|
||||
adapter = RecyclerAdapter(items, true) { updateEmptyView() }
|
||||
adapter.onItemClick = { position ->
|
||||
val data = items[position].split(" : ")
|
||||
val label: String?
|
||||
val server: String
|
||||
if (data.size == 2) {
|
||||
label = data[0]
|
||||
server = data[1]
|
||||
adapter = ServerListRecyclerAdapter(true)
|
||||
binding.recyclerView.adapter = adapter
|
||||
|
||||
dnsServerViewModel.allServers.observe(this) { servers ->
|
||||
adapter.submitList(servers)
|
||||
if (servers.isEmpty()) {
|
||||
binding.emptyView.visibility = View.VISIBLE
|
||||
binding.emptyViewHint.visibility = View.VISIBLE
|
||||
} 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 ->
|
||||
items = swappedItems
|
||||
sharedPrefs.dns_servers = swappedItems
|
||||
updateEmptyView()
|
||||
adapter.onItemClick = { id ->
|
||||
dnsServerViewModel.viewModelScope.launch {
|
||||
val server = dnsServerViewModel.getById(id)
|
||||
if (server != null) {
|
||||
val newFragment =
|
||||
AddServerDialogFragment(server)
|
||||
newFragment.show(supportFragmentManager, "edit_server")
|
||||
}
|
||||
}
|
||||
}
|
||||
adapter.onDragStart = { viewHolder ->
|
||||
itemTouchHelper.startDrag(viewHolder)
|
||||
}
|
||||
binding.floatingActionButton.setOnClickListener {
|
||||
val newFragment = AddServerDialogFragment(null, null, null)
|
||||
val newFragment = AddServerDialogFragment(null)
|
||||
newFragment.show(supportFragmentManager, "add_server")
|
||||
}
|
||||
binding.recyclerView.adapter = adapter
|
||||
|
@ -190,31 +247,40 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
|||
}
|
||||
|
||||
R.id.export_settings_clipboard -> {
|
||||
val data = sharedPrefs.export()
|
||||
val jsonData = gson.toJson(data)
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("", jsonData))
|
||||
// Only show a toast for Android 12 and lower.
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) Toast.makeText(
|
||||
this, getString(R.string.copy_success), Toast.LENGTH_SHORT
|
||||
).show()
|
||||
dnsServerViewModel.viewModelScope.launch {
|
||||
val data = BackupUtils.export(dnsServerViewModel, sharedPrefs)
|
||||
val jsonData = gson.toJson(data)
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("", jsonData))
|
||||
// Only show a toast for Android 12 and lower.
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) Toast.makeText(
|
||||
applicationContext, getString(R.string.copy_success), Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.export_settings_share -> {
|
||||
val data = sharedPrefs.export()
|
||||
val jsonData = gson.toJson(data)
|
||||
ShareCompat.IntentBuilder(this).setText(jsonData).setType("text/plain")
|
||||
.startChooser()
|
||||
val activityContext = this
|
||||
dnsServerViewModel.viewModelScope.launch {
|
||||
val data = BackupUtils.export(dnsServerViewModel, sharedPrefs)
|
||||
val jsonData = gson.toJson(data)
|
||||
ShareCompat.IntentBuilder(activityContext).setText(jsonData)
|
||||
.setType("text/plain")
|
||||
.startChooser()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.export_settings_file -> {
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TITLE, "private-dns-export")
|
||||
|
||||
dnsServerViewModel.viewModelScope.launch {
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TITLE, "private-dns-export")
|
||||
}
|
||||
saveResultLauncher.launch(intent)
|
||||
}
|
||||
saveResultLauncher.launch(intent)
|
||||
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 =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
val data: Intent? = result.data
|
||||
data?.data?.also { uri ->
|
||||
val jsonData = gson.toJson(sharedPrefs.export())
|
||||
val jsonData = gson.toJson(BackupUtils.export(dnsServerViewModel, sharedPrefs))
|
||||
val contentResolver = applicationContext.contentResolver
|
||||
runCatching {
|
||||
contentResolver.openOutputStream(uri)?.use { outputStream ->
|
||||
|
@ -359,46 +415,58 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
|||
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")
|
||||
}
|
||||
|
||||
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()) {
|
||||
Toast.makeText(this, R.string.server_length_error, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
if (label.isNullOrEmpty()) {
|
||||
items.add(server)
|
||||
dnsServerViewModel.insert(DnsServer(0, server))
|
||||
} 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)
|
||||
adapter.setData(items.toMutableList())
|
||||
adapter.notifyItemRemoved(position)
|
||||
sharedPrefs.dns_servers = items
|
||||
/**
|
||||
* Callback for deleting the server
|
||||
* @param id The server id
|
||||
*/
|
||||
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()) {
|
||||
Toast.makeText(this, R.string.server_length_error, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
if (label.isNullOrEmpty()) {
|
||||
items[position] = server
|
||||
} else {
|
||||
items[position] = "$label : $server"
|
||||
}
|
||||
adapter.notifyItemChanged(position)
|
||||
sharedPrefs.dns_servers = items
|
||||
binding.recyclerView.adapter?.notifyItemChanged(position)
|
||||
dnsServerViewModel.update(id, server, label, null, enabled)
|
||||
}
|
||||
|
||||
/**
|
|
@ -1,14 +1,15 @@
|
|||
package ru.karasevm.privatednstoggle
|
||||
package ru.karasevm.privatednstoggle.ui
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import ru.karasevm.privatednstoggle.R
|
||||
import ru.karasevm.privatednstoggle.databinding.DialogOptionsBinding
|
||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper
|
||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.autoMode
|
||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.requireUnlock
|
||||
import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils
|
||||
import ru.karasevm.privatednstoggle.util.PreferenceHelper
|
||||
import ru.karasevm.privatednstoggle.util.PreferenceHelper.autoMode
|
||||
import ru.karasevm.privatednstoggle.util.PreferenceHelper.requireUnlock
|
||||
import ru.karasevm.privatednstoggle.util.PrivateDNSUtils
|
||||
|
||||
class OptionsDialogFragment : DialogFragment() {
|
||||
private var _binding: DialogOptionsBinding? = null
|
||||
|
@ -27,6 +28,7 @@ class OptionsDialogFragment : DialogFragment() {
|
|||
.setView(view)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
builder.create()
|
||||
|
||||
} ?: 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 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.content.ContentResolver
|
||||
|
@ -23,18 +23,23 @@ object PrivateDNSUtils {
|
|||
private const val PRIVATE_DNS_MODE = "private_dns_mode"
|
||||
private const val PRIVATE_DNS_PROVIDER = "private_dns_specifier"
|
||||
|
||||
|
||||
// Gets the system dns mode
|
||||
fun getPrivateMode(contentResolver: ContentResolver): String {
|
||||
return Settings.Global.getString(contentResolver, PRIVATE_DNS_MODE)
|
||||
}
|
||||
|
||||
// Gets the system dns provider
|
||||
fun getPrivateProvider(contentResolver: ContentResolver): String {
|
||||
return Settings.Global.getString(contentResolver, PRIVATE_DNS_PROVIDER)
|
||||
}
|
||||
|
||||
// Sets the system dns mode
|
||||
fun setPrivateMode(contentResolver: ContentResolver, value: String) {
|
||||
Settings.Global.putString(contentResolver, PRIVATE_DNS_MODE, value)
|
||||
}
|
||||
|
||||
// Sets the system dns provider
|
||||
fun setPrivateProvider(contentResolver: ContentResolver, value: String?) {
|
||||
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.SharedPreferences
|
||||
|
||||
object PreferenceHelper {
|
||||
|
||||
private const val DNS_SERVERS = "dns_servers"
|
||||
private const val AUTO_MODE = "auto_mode"
|
||||
private const val REQUIRE_UNLOCK = "require_unlock"
|
||||
const val DNS_SERVERS = "dns_servers"
|
||||
const val AUTO_MODE = "auto_mode"
|
||||
const val REQUIRE_UNLOCK = "require_unlock"
|
||||
|
||||
fun defaultPreference(context: Context): SharedPreferences =
|
||||
context.getSharedPreferences("app_prefs", 0)
|
||||
|
@ -38,6 +38,7 @@ object PreferenceHelper {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
var SharedPreferences.autoMode
|
||||
get() = getInt(AUTO_MODE, PrivateDNSUtils.AUTO_MODE_OPTION_OFF)
|
||||
set(value) {
|
||||
|
@ -53,24 +54,4 @@ object PreferenceHelper {
|
|||
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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
tools:context=".ui.MainActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/topAppBarLayout"
|
||||
|
@ -48,7 +48,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/empty_hint"
|
||||
android:textColor="@color/material_dynamic_neutral50"
|
||||
android:textColor="@color/material_on_surface_emphasis_medium"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
|
|
|
@ -4,6 +4,20 @@
|
|||
android:layout_width="match_parent"
|
||||
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
|
||||
android:id="@+id/editTextServerHintLayout"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -12,7 +26,7 @@
|
|||
android:hint="@string/add_edittext_label_hint"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
app:layout_constraintTop_toBottomOf="@id/serverEnabledSwitch">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/editTextServerHint"
|
||||
|
|
|
@ -54,10 +54,11 @@
|
|||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/requireUnlockSwitch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:maxWidth="320dp"
|
||||
android:text="@string/require_unlock_setting"
|
||||
android:textSize="16sp" />
|
||||
|
||||
|
|
|
@ -12,6 +12,4 @@
|
|||
<color name="md_theme_dark_secondary">#CCC2DC</color>
|
||||
<color name="md_theme_dark_tertiary">#EFB8C8</color>
|
||||
<color name="md_theme_dark_background">#1C1B1F</color>
|
||||
|
||||
<color name="material_dynamic_neutral50">#747879</color>
|
||||
</resources>
|
||||
|
|
|
@ -47,4 +47,5 @@
|
|||
<string name="edit_server">Edit server</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="add_server_enabled">Enabled</string>
|
||||
</resources>
|
|
@ -5,13 +5,17 @@ buildscript {
|
|||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.5.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22"
|
||||
classpath 'com.android.tools.build:gradle:8.5.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// 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) {
|
||||
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