diff --git a/.gitignore b/.gitignore
index 8d9ff13..8c470b3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -67,8 +67,3 @@ fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
-
-# kotlin
-.kotlin/
-
-*~
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index b86273d..b589d56 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index d4b7acc..9a55c2d 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 57ac435..a8c97eb 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,6 @@
[](https://github.com/karasevm/PrivateDNSAndroid/releases/latest)
[](https://github.com/karasevm/PrivateDNSAndroid/releases/latest)
[](https://apt.izzysoft.de/fdroid/index/apk/ru.karasevm.privatednstoggle)
-[](https://hosted.weblate.org/engage/privatednsandroid/)
# Private DNS Quick Toggle
A quick settings tile to switch your private dns provider. Supports any number of providers. Makes it easy to turn adblocking dns servers on or off with just
@@ -13,11 +12,11 @@ a single tap.
Get the latest apk on the [releases page](https://github.com/karasevm/PrivateDNSAndroid/releases/latest)
or from [IzzyOnDroid repo](https://apt.izzysoft.de/fdroid/index/apk/ru.karasevm.privatednstoggle).
-### Automatic (Shizuku)
+## Automatic (Shizuku)
1. Install and start [Shizuku](https://shizuku.rikka.app/).
2. Start the app and allow Shizuku access when prompted.
-### Manual
+## Manual
For the app to work properly you'll need to provide it permissions via ADB:
1. Get to your PC and download platform tools from google [here](https://developer.android.com/studio/releases/platform-tools).
@@ -32,18 +31,3 @@ For the app to work properly you'll need to provide it permissions via ADB:
6. That's it, you should have the app installed.
-## Contributing
-
-### Translation
-The easiest way to contribute would be to submit a translation to your language. Thanks to Weblate gratis hosting for open-source projects you can do it without any programming knowledge on [their website](https://hosted.weblate.org/engage/privatednsandroid/).
-#### Translation status
-
-
-
-
-### Code
-If you want to contribute code please try to adhere to the following guidelines:
-- Include javadoc comments for all the public methods you add
-- Keep the code neatly formatted, you can you the built-in Android Studio formatter
-- Please describe what your code does and how does it do that when sending a PR
-- Before sending a PR please test your change on the oldest and latest supported Android versions (9 and 14 at the time of writing)
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..78848c4
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,66 @@
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+}
+
+android {
+ compileSdk 34
+
+ defaultConfig {
+ applicationId "ru.karasevm.privatednstoggle"
+ versionCode 15
+ versionName "1.8.1"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ targetSdkVersion 34
+ minSdkVersion 28
+ }
+ buildFeatures {
+ viewBinding true
+ }
+ buildTypes {
+ release {
+ minifyEnabled true
+ shrinkResources true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ debug {
+ applicationIdSuffix ".dev"
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ dependenciesInfo {
+ // Disables dependency metadata when building APKs.
+ includeInApk = false
+ // Disables dependency metadata when building Android App Bundles.
+ includeInBundle = false
+ }
+ namespace 'ru.karasevm.privatednstoggle'
+}
+
+dependencies {
+
+ implementation 'androidx.core:core-ktx:1.13.1'
+ implementation 'androidx.appcompat:appcompat:1.7.0'
+ 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'
+ implementation 'com.google.code.gson:gson:2.11.0'
+
+ def shizuku_version = '13.1.5'
+ implementation "dev.rikka.shizuku:api:$shizuku_version"
+ implementation "dev.rikka.shizuku:provider:$shizuku_version"
+
+ implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3'
+ compileOnly 'dev.rikka.hidden:stub:4.3.2'
+
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.2.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
+}
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
deleted file mode 100644
index c06d657..0000000
--- a/app/build.gradle.kts
+++ /dev/null
@@ -1,88 +0,0 @@
-plugins {
- id("com.android.application")
- id("kotlin-android")
- id("com.google.devtools.ksp")
- id("org.jetbrains.kotlin.plugin.serialization")
-}
-
-android {
- compileSdk = 35
- androidResources {
- generateLocaleConfig = true
- }
- defaultConfig {
- applicationId = "ru.karasevm.privatednstoggle"
- versionCode = 18
- versionName = "1.10.0"
-
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- targetSdk = 35
- minSdk = 28
- }
- buildFeatures {
- viewBinding = true
- buildConfig = true
- }
- buildTypes {
- release {
- isMinifyEnabled = true
- isShrinkResources = true
- proguardFiles(
- getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
- )
- }
- debug {
- applicationIdSuffix = ".dev"
- }
- }
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
- kotlinOptions {
- jvmTarget = "17"
- }
- dependenciesInfo {
- // Disables dependency metadata when building APKs.
- includeInApk = false
- // Disables dependency metadata when building Android App Bundles.
- includeInBundle = false
- }
- namespace = "ru.karasevm.privatednstoggle"
-}
-
-dependencies {
- implementation("androidx.core:core-ktx:1.15.0")
- implementation("androidx.appcompat:appcompat:1.7.0")
- implementation("androidx.recyclerview:recyclerview:1.4.0")
- implementation("androidx.activity:activity-ktx:1.10.0")
- implementation("androidx.fragment:fragment-ktx:1.8.6")
- implementation("com.google.android.material:material:1.12.0")
- implementation("androidx.constraintlayout:constraintlayout:2.2.0")
- implementation("com.google.guava:guava:33.1.0-android")
- implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
-
- val shizukuVersion = "13.1.5"
- implementation("dev.rikka.shizuku:api:$shizukuVersion")
- implementation("dev.rikka.shizuku:provider:$shizukuVersion")
- compileOnly("dev.rikka.hidden:stub:4.3.3")
-
- implementation("org.lsposed.hiddenapibypass:hiddenapibypass:4.3")
-
- // Room components
- val roomVersion = "2.6.1"
- implementation("androidx.room:room-ktx:$roomVersion")
- ksp("androidx.room:room-compiler:$roomVersion")
- androidTestImplementation("androidx.room:room-testing:$roomVersion")
-
- // Lifecycle components
- val lifecycleVersion = "2.8.7"
- 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")
-}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 21da365..a695d06 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -19,7 +19,7 @@
android:exported="true"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
@@ -41,7 +41,7 @@
- listener.onUpdateDialogPositiveClick(
- dnsServer.id,
- binding.editTextServerAddr.text.toString().trim(),
- binding.editTextServerHint.text.toString().trim(),
- binding.serverEnabledSwitch.isChecked
- )
+ ) { _, _ ->
+ listener.onDialogPositiveClick(
+ binding.editTextServerHint.text.toString().trim(),
+ binding.editTextServerAddr.text.toString().trim(),
+ position)
}
.setNegativeButton(
R.string.cancel
@@ -89,16 +81,17 @@ class AddServerDialogFragment(
.setNeutralButton(
R.string.delete
) { _, _ ->
- listener.onDeleteItemClicked(dnsServer.id)
- }
- } else {
+ listener.onDeleteItemClicked(position)
+ }
+ }
+ else {
builder.setTitle(R.string.add_server)
.setView(view)
// Add action buttons
.setPositiveButton(
R.string.menu_add
) { _, _ ->
- listener.onAddDialogPositiveClick(
+ listener.onDialogPositiveClick(
binding.editTextServerHint.text.toString().trim(),
binding.editTextServerAddr.text.toString().trim()
)
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/DNSServerDialogFragment.kt b/app/src/main/java/ru/karasevm/privatednstoggle/DNSServerDialogFragment.kt
new file mode 100644
index 0000000..3ea8912
--- /dev/null
+++ b/app/src/main/java/ru/karasevm/privatednstoggle/DNSServerDialogFragment.kt
@@ -0,0 +1,117 @@
+package ru.karasevm.privatednstoggle
+
+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.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+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
+
+class DNSServerDialogFragment : DialogFragment() {
+
+ private var _binding: SheetDnsSelectorBinding? = null
+ private val binding get() = _binding!!
+
+ private lateinit var linearLayoutManager: LinearLayoutManager
+ private lateinit var adapter: RecyclerAdapter
+ private var items = mutableListOf()
+ private lateinit var sharedPrefs: SharedPreferences
+
+ override fun onCreateDialog(
+ savedInstanceState: Bundle?
+ ): Dialog {
+ return activity?.let {
+ 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) {}
+ binding.recyclerView.adapter = adapter
+
+
+ val startIntent = Intent(context, MainActivity::class.java)
+
+ builder.setTitle(R.string.select_server)
+ .setView(binding.root)
+ .setPositiveButton(
+ R.string.done
+ ) { _, _ ->
+ dialog?.dismiss()
+ }
+ .setNeutralButton(R.string.open_app) { _, _ -> context?.startActivity(startIntent) }
+ builder.create()
+ } ?: throw IllegalStateException("Activity cannot be null")
+ }
+
+ override fun onStart() {
+ super.onStart()
+
+ adapter.onItemClick = { position ->
+ when (position) {
+ 0 -> {
+ PrivateDNSUtils.setPrivateMode(
+ requireActivity().contentResolver,
+ PrivateDNSUtils.DNS_MODE_OFF
+ )
+ Toast.makeText(context, R.string.set_to_off_toast, Toast.LENGTH_SHORT).show()
+ }
+
+ 1 -> {
+ PrivateDNSUtils.setPrivateMode(
+ requireActivity().contentResolver,
+ PrivateDNSUtils.DNS_MODE_AUTO
+ )
+ Toast.makeText(context, R.string.set_to_auto_toast, Toast.LENGTH_SHORT).show()
+ }
+
+ 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()
+ }
+ }
+ dialog?.dismiss()
+ requireContext().sendBroadcast(Intent("refresh_tile").setPackage(requireContext().packageName))
+ }
+
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ activity?.finish()
+ }
+
+ companion object {
+ const val TAG = "DNSServerDialogFragment"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/ui/DeleteServerDialogFragment.kt b/app/src/main/java/ru/karasevm/privatednstoggle/DeleteServerDialogFragment.kt
similarity index 88%
rename from app/src/main/java/ru/karasevm/privatednstoggle/ui/DeleteServerDialogFragment.kt
rename to app/src/main/java/ru/karasevm/privatednstoggle/DeleteServerDialogFragment.kt
index a6a2d98..b69ce1d 100644
--- a/app/src/main/java/ru/karasevm/privatednstoggle/ui/DeleteServerDialogFragment.kt
+++ b/app/src/main/java/ru/karasevm/privatednstoggle/DeleteServerDialogFragment.kt
@@ -1,14 +1,13 @@
-package ru.karasevm.privatednstoggle.ui
+package ru.karasevm.privatednstoggle
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 id: Int) : DialogFragment() {
+class DeleteServerDialogFragment(private val position: Int) : DialogFragment() {
// Use this instance of the interface to deliver action events
private lateinit var listener: NoticeDialogListener
@@ -16,7 +15,7 @@ class DeleteServerDialogFragment(private val id: 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 onDeleteDialogPositiveClick(id: Int)
+ fun onDialogPositiveClick(position: Int)
}
// Override the Fragment.onAttach() method to instantiate the NoticeDialogListener
@@ -44,7 +43,7 @@ class DeleteServerDialogFragment(private val id: Int) : DialogFragment() {
.setPositiveButton(
R.string.delete
) { _, _ ->
- listener.onDeleteDialogPositiveClick(id)
+ listener.onDialogPositiveClick(position)
}
.setNegativeButton(
R.string.cancel
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/DnsTileService.kt b/app/src/main/java/ru/karasevm/privatednstoggle/DnsTileService.kt
new file mode 100644
index 0000000..02635c5
--- /dev/null
+++ b/app/src/main/java/ru/karasevm/privatednstoggle/DnsTileService.kt
@@ -0,0 +1,299 @@
+package ru.karasevm.privatednstoggle
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.graphics.drawable.Icon
+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
+
+class DnsTileService : TileService() {
+
+ override fun onTileAdded() {
+ super.onTileAdded()
+ checkForPermission(this)
+ // Update state
+ qsTile.state = Tile.STATE_INACTIVE
+
+ // Update looks
+ qsTile.updateTile()
+ }
+
+ /**
+ * Set's the state of the tile to the next state
+ */
+ private fun cycleState() {
+ val dnsMode = Settings.Global.getString(contentResolver, "private_dns_mode")
+ val dnsProvider = Settings.Global.getString(contentResolver, "private_dns_specifier")
+
+ val sharedPrefs = PreferenceHelper.defaultPreference(this)
+ if (dnsMode.equals(DNS_MODE_OFF, ignoreCase = true)) {
+ if (sharedPrefs.autoMode == AUTO_MODE_OPTION_AUTO || sharedPrefs.autoMode == AUTO_MODE_OPTION_OFF_AUTO) {
+ changeDNSServer(DNS_MODE_AUTO, dnsProvider)
+ } else {
+ changeDNSServer(DNS_MODE_PRIVATE, dnsProvider)
+ }
+
+ } 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)
+ } else {
+ changeDNSServer(DNS_MODE_OFF, dnsProvider)
+ }
+ }
+ } else {
+ changeDNSServer(DNS_MODE_PRIVATE, dnsProvider)
+ }
+ }
+ }
+
+ private fun changeDNSServer(server: String, dnsProvider: String?) {
+ when (server) {
+ DNS_MODE_OFF -> {
+ changeTileState(
+ qsTile,
+ Tile.STATE_INACTIVE,
+ getString(R.string.dns_off),
+ R.drawable.ic_off_black_24dp,
+ DNS_MODE_OFF,
+ null
+ )
+ }
+
+ DNS_MODE_AUTO -> {
+ changeTileState(
+ qsTile,
+ Tile.STATE_INACTIVE,
+ getString(R.string.dns_auto),
+ R.drawable.ic_auto_black_24dp,
+ DNS_MODE_AUTO,
+ dnsProvider
+ )
+ }
+
+ DNS_MODE_PRIVATE -> {
+ changeTileState(
+ qsTile,
+ Tile.STATE_ACTIVE,
+ getNextAddress(dnsProvider)?.label,
+ R.drawable.ic_private_black_24dp,
+ DNS_MODE_PRIVATE,
+ getNextAddress(dnsProvider)?.server
+ )
+ }
+ }
+ }
+
+ override fun onClick() {
+ super.onClick()
+ if (!checkForPermission(this)) {
+ return
+ }
+ val sharedPrefs = PreferenceHelper.defaultPreference(this)
+ val requireUnlock = sharedPrefs.requireUnlock
+ if (isLocked && requireUnlock) {
+ unlockAndRun(this::cycleState)
+ } else {
+ cycleState()
+ }
+
+
+ }
+
+ /**
+ * Refreshes the state of the tile
+ */
+ 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 == "") {
+ setTile(
+ qsTile,
+ Tile.STATE_ACTIVE,
+ "Google",
+ R.drawable.ic_private_black_24dp
+ )
+ } else {
+ val index = items.indexOfFirst { it.server == dnsProvider }
+ if (index == -1) {
+ setTile(
+ qsTile,
+ Tile.STATE_ACTIVE,
+ dnsProvider,
+ R.drawable.ic_private_black_24dp
+ )
+ } else {
+ setTile(
+ qsTile,
+ Tile.STATE_ACTIVE,
+ items[index].label,
+ R.drawable.ic_private_black_24dp
+ )
+ }
+ }
+ }
+ }
+
+ private val broadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ refreshTile()
+ }
+ }
+
+ override fun onStartListening() {
+ super.onStartListening()
+ if (!checkForPermission(this)) {
+ return
+ }
+
+ // Prevent some crashes
+ if (qsTile == null) {
+ return
+ }
+ ContextCompat.registerReceiver(
+ this,
+ broadcastReceiver,
+ IntentFilter("refresh_tile"),
+ ContextCompat.RECEIVER_NOT_EXPORTED
+ )
+
+ refreshTile()
+
+ }
+
+ override fun onStopListening() {
+ super.onStopListening()
+ unregisterReceiver(broadcastReceiver)
+ }
+
+ /**
+ * Updates tile to specified parameters
+ *
+ * @param tile tile to update
+ * @param state tile state
+ * @param label tile label
+ * @param icon tile icon
+ */
+ private fun setTile(tile: Tile, state: Int, label: String?, icon: Int) {
+ tile.state = state
+ tile.label = label
+ tile.icon = Icon.createWithResource(this, icon)
+ tile.updateTile()
+ }
+
+ /**
+ * Updates tile and system settings to specified parameters
+ *
+ * @param tile tile to update
+ * @param state tile state
+ * @param label tile label
+ * @param icon tile icon
+ * @param dnsMode system dns mode
+ * @param dnsProvider system dns provider
+ */
+ private fun changeTileState(
+ tile: Tile,
+ state: Int,
+ label: String?,
+ icon: Int,
+ dnsMode: String,
+ dnsProvider: String?
+ ) {
+ tile.label = label
+ tile.state = state
+ tile.icon = Icon.createWithResource(this, icon)
+ PrivateDNSUtils.setPrivateMode(contentResolver, dnsMode)
+ PrivateDNSUtils.setPrivateProvider(contentResolver, dnsProvider)
+ tile.updateTile()
+ }
+
+ /**
+ * Gets next dns address from preferences,
+ * if current address is last 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"))
+ }
+ }
+
+ 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]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/ui/MainActivity.kt b/app/src/main/java/ru/karasevm/privatednstoggle/MainActivity.kt
similarity index 51%
rename from app/src/main/java/ru/karasevm/privatednstoggle/ui/MainActivity.kt
rename to app/src/main/java/ru/karasevm/privatednstoggle/MainActivity.kt
index 26695a3..230eadb 100644
--- a/app/src/main/java/ru/karasevm/privatednstoggle/ui/MainActivity.kt
+++ b/app/src/main/java/ru/karasevm/privatednstoggle/MainActivity.kt
@@ -1,4 +1,4 @@
-package ru.karasevm.privatednstoggle.ui
+package ru.karasevm.privatednstoggle
import android.Manifest
import android.content.ClipData
@@ -6,40 +6,41 @@ import android.content.ClipDescription.MIMETYPE_TEXT_PLAIN
import android.content.ClipboardManager
import android.content.Intent
import android.content.SharedPreferences
+import android.content.pm.IPackageManager
import android.content.pm.PackageManager
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Bundle
+import android.permission.IPermissionManager
import android.util.Log
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
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
-import kotlinx.coroutines.launch
-import kotlinx.serialization.encodeToString
-import kotlinx.serialization.json.Json
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import com.google.gson.JsonSyntaxException
+import com.google.gson.ToNumberPolicy
+import com.google.gson.reflect.TypeToken
+import org.lsposed.hiddenapibypass.HiddenApiBypass
import rikka.shizuku.Shizuku
+import rikka.shizuku.ShizukuBinderWrapper
import rikka.shizuku.ShizukuProvider
-import ru.karasevm.privatednstoggle.PrivateDNSApp
-import ru.karasevm.privatednstoggle.R
-import ru.karasevm.privatednstoggle.data.DnsServerViewModel
-import ru.karasevm.privatednstoggle.data.DnsServerViewModelFactory
+import rikka.shizuku.SystemServiceHelper
import ru.karasevm.privatednstoggle.databinding.ActivityMainBinding
-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
-import ru.karasevm.privatednstoggle.util.ShizukuUtil.grantPermissionWithShizuku
+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
class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogListener,
@@ -47,117 +48,79 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
private lateinit var linearLayoutManager: LinearLayoutManager
private lateinit var binding: ActivityMainBinding
+ private var items = mutableListOf()
private lateinit var sharedPrefs: SharedPreferences
- private lateinit var adapter: ServerListRecyclerAdapter
+ private lateinit var adapter: RecyclerAdapter
private lateinit var clipboard: ClipboardManager
- private val dnsServerViewModel: DnsServerViewModel by viewModels { DnsServerViewModelFactory((application as PrivateDNSApp).repository) }
+ private lateinit var gson: Gson
private val itemTouchHelper by lazy {
- val simpleItemTouchCallback =
- object : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) {
- var dragFrom = -1
- var dragTo = -1
+ val simpleItemTouchCallback = object : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) {
- override fun onMove(
- recyclerView: RecyclerView,
- viewHolder: RecyclerView.ViewHolder,
- target: RecyclerView.ViewHolder
- ): Boolean {
- if (dragFrom == viewHolder.bindingAdapterPosition && dragTo == target.bindingAdapterPosition) {
- return true
+ override fun onMove(
+ recyclerView: RecyclerView,
+ viewHolder: RecyclerView.ViewHolder,
+ target: RecyclerView.ViewHolder
+ ): Boolean {
+ adapter.onItemMove(viewHolder.adapterPosition, target.adapterPosition)
+ return true
+ }
+
+ 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)
}
- // 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 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)
- }
- // commit the change to the db
- dnsServerViewModel.move(
- dragFrom,
- dragTo,
- (viewHolder as ServerListRecyclerAdapter.DnsServerViewHolder).id
- )
- dragTo = -1
- dragFrom = -1
}
}
+
+ 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)
+ }
+ }
+ }
ItemTouchHelper(simpleItemTouchCallback)
}
private fun importSettings(json: String) {
runCatching {
- val data: BackupUtils.Backup = Json.decodeFromString(json)
- BackupUtils.import(data, dnsServerViewModel, sharedPrefs)
+ val objectType = object : TypeToken