diff --git a/.github/ISSUE_TEMPLATE/01-bug-report.yml b/.github/ISSUE_TEMPLATE/01-bug-report.yml
deleted file mode 100644
index c906e0f..0000000
--- a/.github/ISSUE_TEMPLATE/01-bug-report.yml
+++ /dev/null
@@ -1,73 +0,0 @@
-name: Bug report
-description: File a bug report.
-labels: ['bug']
-assignees: ['karasevm']
-body:
- - type: markdown
- attributes:
- value: |
- Thanks for taking the time to fill out this bug report!
- - type: input
- id: app_version
- attributes:
- label: Application Version
- description: What version of the app are you running?
- placeholder: ex. 1.0
- validations:
- required: true
- - type: input
- id: android_version
- attributes:
- label: Application Version
- description: What version of Android you running?
- placeholder: ex. 13
- validations:
- required: true
- - type: input
- id: device
- attributes:
- label: Device
- description: What device are you using?
- placeholder: ex. Pixel 5
- validations:
- required: true
- - type: dropdown
- id: install_method
- attributes:
- label: How do you provide the permission?
- options:
- - Shizuku
- - ADB
- - Other
- validations:
- required: true
- - type: textarea
- id: what-happened
- attributes:
- label: What happened?
- description: Also tell us, what did you expect to happen?
- placeholder: A bug happened!
- validations:
- required: true
- - type: textarea
- id: steps-to-reproduce
- attributes:
- label: Steps to reproduce
- description: |
- Please describe what you did to reproduce the bug.
- - type: textarea
- id: logs
- attributes:
- label: Relevant log output
- description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
- render: shell
- - type: textarea
- id: screens
- attributes:
- label: Screenshots
- description: If applicable, add screenshots to help explain your problem.
- - type: textarea
- id: additional-context
- attributes:
- label: Additional context
- description: Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/02-feature-request.yml b/.github/ISSUE_TEMPLATE/02-feature-request.yml
deleted file mode 100644
index 7acccf3..0000000
--- a/.github/ISSUE_TEMPLATE/02-feature-request.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-name: Feature request
-description: Suggest an idea for this project.
-labels: ["enhancement"]
-assignees: ["karasevm"]
-body:
- - type: textarea
- id: problem
- attributes:
- label: Is your feature request related to a problem? Please describe.
- description: A clear and concise description of what the problem is.
- placeholder: I'm always frustrated when [...]
- validations:
- required: true
- - type: textarea
- id: solution
- attributes:
- label: Describe the solution you'd like.
- description: A clear and concise description of what you want to happen.
- validations:
- required: true
- - type: textarea
- id: alternative
- attributes:
- label: Describe alternatives you've considered.
- description: A clear and concise description of any alternative solutions or features you've considered.
- - type: textarea
- id: additional-context
- attributes:
- label: Additional context
- description: Add any other context about the problem here.
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/03-other.yml b/.github/ISSUE_TEMPLATE/03-other.yml
deleted file mode 100644
index 6ec8a5e..0000000
--- a/.github/ISSUE_TEMPLATE/03-other.yml
+++ /dev/null
@@ -1,9 +0,0 @@
-name: Other
-description: If other options don't fit your question.
-body:
- - type: textarea
- id: other
- attributes:
- label: Ask a question
- validations:
- required: true
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/.idea/vcs.xml b/.idea/vcs.xml
index 35eb1dd..94a25f7 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.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..1b43aa7
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,65 @@
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+}
+
+android {
+ compileSdk 34
+
+ defaultConfig {
+ applicationId "ru.karasevm.privatednstoggle"
+ versionCode 12
+ versionName "1.6"
+
+ 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'
+
+ 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.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.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
- )
- }
- .setNegativeButton(
- R.string.cancel
- ) { _, _ ->
- dialog?.cancel()
- }
- .setNeutralButton(
- R.string.delete
- ) { _, _ ->
- listener.onDeleteItemClicked(dnsServer.id)
- }
- } else {
- builder.setTitle(R.string.add_server)
- .setView(view)
- // Add action buttons
- .setPositiveButton(
- R.string.menu_add
- ) { _, _ ->
- listener.onAddDialogPositiveClick(
- binding.editTextServerHint.text.toString().trim(),
- binding.editTextServerAddr.text.toString().trim()
- )
- }
- .setNegativeButton(
- R.string.cancel
- ) { _, _ ->
- dialog?.cancel()
- }
- }
+ builder.setTitle(R.string.add_server)
+ .setView(view)
+ // Add action buttons
+ .setPositiveButton(
+ R.string.menu_add
+ ) { _, _ ->
+ listener.onDialogPositiveClick(
+ binding.editTextServerHint.text.toString().trim(),
+ binding.editTextServerAddr.text.toString().trim()
+ )
+ }
+ .setNegativeButton(
+ R.string.cancel
+ ) { _, _ ->
+ dialog?.cancel()
+ }
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
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..4666321
--- /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/MainActivity.kt b/app/src/main/java/ru/karasevm/privatednstoggle/MainActivity.kt
new file mode 100644
index 0000000..f19a122
--- /dev/null
+++ b/app/src/main/java/ru/karasevm/privatednstoggle/MainActivity.kt
@@ -0,0 +1,280 @@
+package ru.karasevm.privatednstoggle
+
+import android.Manifest
+import android.annotation.SuppressLint
+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.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+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 org.lsposed.hiddenapibypass.HiddenApiBypass
+import rikka.shizuku.Shizuku
+import rikka.shizuku.ShizukuBinderWrapper
+import rikka.shizuku.ShizukuProvider
+import rikka.shizuku.SystemServiceHelper
+import ru.karasevm.privatednstoggle.databinding.ActivityMainBinding
+import ru.karasevm.privatednstoggle.utils.PreferenceHelper
+import ru.karasevm.privatednstoggle.utils.PreferenceHelper.dns_servers
+
+
+class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogListener,
+ DeleteServerDialogFragment.NoticeDialogListener, Shizuku.OnRequestPermissionResultListener {
+
+ private lateinit var linearLayoutManager: LinearLayoutManager
+ private lateinit var binding: ActivityMainBinding
+ private var items = mutableListOf()
+ private lateinit var sharedPrefs: SharedPreferences
+ private lateinit var adapter: RecyclerAdapter
+
+ private val itemTouchHelper by lazy {
+ val simpleItemTouchCallback =
+ object : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) {
+
+ 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)
+ }
+ }
+ }
+
+ 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)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ Shizuku.addRequestPermissionResultListener(this::onRequestPermissionResult)
+
+ binding = ActivityMainBinding.inflate(layoutInflater)
+ val view = binding.root
+ setContentView(view)
+
+ linearLayoutManager = LinearLayoutManager(this)
+ binding.recyclerView.layoutManager = linearLayoutManager
+
+ sharedPrefs = PreferenceHelper.defaultPreference(this)
+
+ items = sharedPrefs.dns_servers
+ if (items[0] == "") {
+ items.removeAt(0)
+ }
+ adapter = RecyclerAdapter(items, true)
+ adapter.onItemClick = { position ->
+ val newFragment = DeleteServerDialogFragment(position)
+ newFragment.show(supportFragmentManager, "delete_server")
+ }
+ adapter.onItemsChanged = { swapedItems ->
+ items = swapedItems
+ sharedPrefs.dns_servers = swapedItems
+ }
+ adapter.onDragStart = { viewHolder ->
+ itemTouchHelper.startDrag(viewHolder)
+ }
+ binding.floatingActionButton.setOnClickListener {
+ val newFragment = AddServerDialogFragment()
+ newFragment.show(supportFragmentManager, "add_server")
+ }
+ binding.recyclerView.adapter = adapter
+ itemTouchHelper.attachToRecyclerView(binding.recyclerView)
+
+ binding.topAppBar.setOnMenuItemClickListener { item ->
+ when (item.itemId) {
+ R.id.privacy_policy -> {
+ val browserIntent = Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/privacy_policy")
+ )
+ startActivity(browserIntent)
+ true
+ }
+
+ R.id.options -> {
+ val newFragment = OptionsDialogFragment()
+ newFragment.show(supportFragmentManager, "options")
+ true
+ }
+
+ else -> true
+ }
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ menuInflater.inflate(R.menu.menu_main, menu)
+ return true
+ }
+
+ override fun onResume() {
+ super.onResume()
+ // Check if WRITE_SECURE_SETTINGS is granted
+ if (checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) {
+ // Check if Shizuku is available
+ if (Shizuku.pingBinder()) {
+ // check if permission is granted already
+ val isGranted = if (Shizuku.isPreV11() || Shizuku.getVersion() < 11) {
+ checkSelfPermission(ShizukuProvider.PERMISSION) == PackageManager.PERMISSION_GRANTED
+ } else {
+ Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
+ }
+ // request permission if not granted
+ if (!isGranted && !Shizuku.shouldShowRequestPermissionRationale()) {
+ if (Shizuku.isPreV11() || Shizuku.getVersion() < 11) {
+ requestPermissions(arrayOf(ShizukuProvider.PERMISSION), 1)
+ } else {
+ Shizuku.requestPermission(1)
+ }
+ } else {
+ grantPermissionWithShizuku()
+ }
+ } else {
+ if (checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) {
+ val browserIntent = Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/")
+ )
+ startActivity(browserIntent)
+ finish()
+ }
+ }
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ Shizuku.removeRequestPermissionResultListener(this::onRequestPermissionResult)
+ }
+
+ override fun onDialogPositiveClick(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)
+ } else {
+ items.add("$label : $server")
+ }
+ 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
+ }
+
+ /**
+ * Attempts to grant WRITE_SECURE_SETTINGS permission with Shizuku
+ */
+ private fun grantPermissionWithShizuku() {
+ val packageName = applicationContext.packageName
+ runCatching {
+ if (Build.VERSION.SDK_INT >= 31) {
+ HiddenApiBypass.addHiddenApiExemptions("Landroid/permission")
+ val binder =
+ ShizukuBinderWrapper(SystemServiceHelper.getSystemService("permissionmgr"))
+ val pm = IPermissionManager.Stub.asInterface(binder)
+ runCatching {
+ pm.grantRuntimePermission(
+ packageName,
+ Manifest.permission.WRITE_SECURE_SETTINGS,
+ 0
+ )
+ }.onFailure { _ ->
+ if (Build.VERSION.SDK_INT >= 34) {
+ pm.grantRuntimePermission(
+ packageName,
+ Manifest.permission.WRITE_SECURE_SETTINGS,
+ applicationContext.deviceId,
+ 0
+ )
+ }
+ }
+ } else {
+ val binder = ShizukuBinderWrapper(SystemServiceHelper.getSystemService("package"))
+ val pm = IPackageManager.Stub.asInterface(binder)
+ pm.grantRuntimePermission(
+ packageName,
+ Manifest.permission.WRITE_SECURE_SETTINGS,
+ 0
+ )
+ }
+ }.onFailure { e ->
+ Log.e("SHIZUKU", "onRequestPermissionResult: ", e)
+ }.also {
+ if (checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) {
+ val browserIntent = Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/")
+ )
+ startActivity(browserIntent)
+ finish()
+ }
+ }
+
+ }
+
+ @SuppressLint("PrivateApi")
+ override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) {
+ val isGranted = grantResult == PackageManager.PERMISSION_GRANTED
+
+ if (isGranted) {
+ grantPermissionWithShizuku()
+ } else if (checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) {
+ val browserIntent = Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/")
+ )
+ startActivity(browserIntent)
+ finish()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/ui/OptionsDialogFragment.kt b/app/src/main/java/ru/karasevm/privatednstoggle/OptionsDialogFragment.kt
similarity index 66%
rename from app/src/main/java/ru/karasevm/privatednstoggle/ui/OptionsDialogFragment.kt
rename to app/src/main/java/ru/karasevm/privatednstoggle/OptionsDialogFragment.kt
index 2ca7b14..e4b9cb4 100644
--- a/app/src/main/java/ru/karasevm/privatednstoggle/ui/OptionsDialogFragment.kt
+++ b/app/src/main/java/ru/karasevm/privatednstoggle/OptionsDialogFragment.kt
@@ -1,20 +1,18 @@
-package ru.karasevm.privatednstoggle.ui
+package ru.karasevm.privatednstoggle
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.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.utils.PreferenceHelper
+import ru.karasevm.privatednstoggle.utils.PreferenceHelper.autoMode
+import ru.karasevm.privatednstoggle.utils.PreferenceHelper.requireUnlock
+import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils
class OptionsDialogFragment : DialogFragment() {
private var _binding: DialogOptionsBinding? = null
private val binding get() = _binding!!
- private val sharedPreferences by lazy { PreferenceHelper.defaultPreference(requireContext()) }
override fun onCreateDialog(
savedInstanceState: Bundle?
@@ -29,13 +27,13 @@ class OptionsDialogFragment : DialogFragment() {
.setView(view)
.setPositiveButton(R.string.ok, null)
builder.create()
-
} ?: throw IllegalStateException("Activity cannot be null")
}
override fun onStart() {
super.onStart()
- val autoModeOption = sharedPreferences.autoMode
+ val sharedPrefs = PreferenceHelper.defaultPreference(requireContext())
+ val autoModeOption = sharedPrefs.autoMode
when (autoModeOption) {
PrivateDNSUtils.AUTO_MODE_OPTION_OFF -> binding.autoOptionRadioGroup.check(R.id.autoOptionOff)
PrivateDNSUtils.AUTO_MODE_OPTION_AUTO -> binding.autoOptionRadioGroup.check(R.id.autoOptionAuto)
@@ -44,20 +42,20 @@ class OptionsDialogFragment : DialogFragment() {
}
binding.autoOptionRadioGroup.setOnCheckedChangeListener { _, checkedId ->
when (checkedId) {
- R.id.autoOptionOff -> sharedPreferences.autoMode = PrivateDNSUtils.AUTO_MODE_OPTION_OFF
- R.id.autoOptionAuto -> sharedPreferences.autoMode = PrivateDNSUtils.AUTO_MODE_OPTION_AUTO
- R.id.autoOptionOffAuto -> sharedPreferences.autoMode =
+ R.id.autoOptionOff -> sharedPrefs.autoMode = PrivateDNSUtils.AUTO_MODE_OPTION_OFF
+ R.id.autoOptionAuto -> sharedPrefs.autoMode = PrivateDNSUtils.AUTO_MODE_OPTION_AUTO
+ R.id.autoOptionOffAuto -> sharedPrefs.autoMode =
PrivateDNSUtils.AUTO_MODE_OPTION_OFF_AUTO
- R.id.autoOptionPrivate -> sharedPreferences.autoMode =
+ R.id.autoOptionPrivate -> sharedPrefs.autoMode =
PrivateDNSUtils.AUTO_MODE_OPTION_PRIVATE
}
}
- val requireUnlock = sharedPreferences.requireUnlock
+ val requireUnlock = sharedPrefs.requireUnlock
binding.requireUnlockSwitch.isChecked = requireUnlock
binding.requireUnlockSwitch.setOnCheckedChangeListener { _, isChecked ->
- sharedPreferences.requireUnlock = isChecked
+ sharedPrefs.requireUnlock = isChecked
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/PrivateDNSApp.kt b/app/src/main/java/ru/karasevm/privatednstoggle/PrivateDNSApp.kt
index c404503..fd28566 100644
--- a/app/src/main/java/ru/karasevm/privatednstoggle/PrivateDNSApp.kt
+++ b/app/src/main/java/ru/karasevm/privatednstoggle/PrivateDNSApp.kt
@@ -1,33 +1,11 @@
package ru.karasevm.privatednstoggle
import android.app.Application
-import android.os.StrictMode
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)
-
- if (BuildConfig.DEBUG){
- StrictMode.setThreadPolicy(
- StrictMode.ThreadPolicy.Builder()
- .detectAll()
- .penaltyLog()
- .build()
- )
- StrictMode.setVmPolicy(
- StrictMode.VmPolicy.Builder()
- .detectAll()
- .penaltyLog()
- .build()
- )
- }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/RecyclerAdapter.kt b/app/src/main/java/ru/karasevm/privatednstoggle/RecyclerAdapter.kt
new file mode 100644
index 0000000..6c9e648
--- /dev/null
+++ b/app/src/main/java/ru/karasevm/privatednstoggle/RecyclerAdapter.kt
@@ -0,0 +1,82 @@
+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, private val showDragHandle: Boolean) :
+ RecyclerView.Adapter() {
+
+ var onItemClick: ((Int) -> Unit)? = null
+ var onItemsChanged: ((MutableList) -> 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.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) {
+ items.run {
+ clear()
+ addAll(newItems)
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/ui/SettingsDialogActivity.kt b/app/src/main/java/ru/karasevm/privatednstoggle/SettingsDialogActivity.kt
similarity index 89%
rename from app/src/main/java/ru/karasevm/privatednstoggle/ui/SettingsDialogActivity.kt
rename to app/src/main/java/ru/karasevm/privatednstoggle/SettingsDialogActivity.kt
index 4b15e05..c8a86b6 100644
--- a/app/src/main/java/ru/karasevm/privatednstoggle/ui/SettingsDialogActivity.kt
+++ b/app/src/main/java/ru/karasevm/privatednstoggle/SettingsDialogActivity.kt
@@ -1,4 +1,4 @@
-package ru.karasevm.privatednstoggle.ui
+package ru.karasevm.privatednstoggle
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/data/DnsServerDao.kt b/app/src/main/java/ru/karasevm/privatednstoggle/data/DnsServerDao.kt
deleted file mode 100644
index 026de12..0000000
--- a/app/src/main/java/ru/karasevm/privatednstoggle/data/DnsServerDao.kt
+++ /dev/null
@@ -1,84 +0,0 @@
-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>
-
- @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)
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/data/DnsServerRepository.kt b/app/src/main/java/ru/karasevm/privatednstoggle/data/DnsServerRepository.kt
deleted file mode 100644
index 4c7ddd4..0000000
--- a/app/src/main/java/ru/karasevm/privatednstoggle/data/DnsServerRepository.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-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> = 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)
- }
-
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/data/DnsServerViewModel.kt b/app/src/main/java/ru/karasevm/privatednstoggle/data/DnsServerViewModel.kt
deleted file mode 100644
index bd1987a..0000000
--- a/app/src/main/java/ru/karasevm/privatednstoggle/data/DnsServerViewModel.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-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> = 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 create(modelClass: Class): T {
- if (modelClass.isAssignableFrom(DnsServerViewModel::class.java)) {
- @Suppress("UNCHECKED_CAST")
- return DnsServerViewModel(dnsServerRepository) as T
- }
- throw IllegalArgumentException("Unknown ViewModel class")
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/data/database/DnsServerRoomDatabase.kt b/app/src/main/java/ru/karasevm/privatednstoggle/data/database/DnsServerRoomDatabase.kt
deleted file mode 100644
index 72b6f25..0000000
--- a/app/src/main/java/ru/karasevm/privatednstoggle/data/database/DnsServerRoomDatabase.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-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
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/model/DnsServer.kt b/app/src/main/java/ru/karasevm/privatednstoggle/model/DnsServer.kt
deleted file mode 100644
index 38c1be4..0000000
--- a/app/src/main/java/ru/karasevm/privatednstoggle/model/DnsServer.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package ru.karasevm.privatednstoggle.model
-
-import androidx.room.ColumnInfo
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-
-// All fields must have default values for proper deserialization
-@Serializable
-@Entity(tableName = "dns_servers")
-data class DnsServer(
- @SerialName("id")
- @PrimaryKey(autoGenerate = true)
- val id: Int = 0,
- @SerialName("server")
- val server: String = "",
- @SerialName("label")
- val label: String = "",
- @SerialName("enabled")
- @ColumnInfo(defaultValue = "1")
- val enabled: Boolean = true,
- val sortOrder: Int? = null
-)
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/service/DnsTileService.kt b/app/src/main/java/ru/karasevm/privatednstoggle/service/DnsTileService.kt
deleted file mode 100644
index 1bb1944..0000000
--- a/app/src/main/java/ru/karasevm/privatednstoggle/service/DnsTileService.kt
+++ /dev/null
@@ -1,302 +0,0 @@
-package ru.karasevm.privatednstoggle.service
-
-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 android.util.Log
-import androidx.core.content.ContextCompat
-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)
- private val sharedPreferences by lazy { PreferenceHelper.defaultPreference(this) }
- private var isBroadcastReceiverRegistered = false
-
- 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 and system settings 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")
-
- if (dnsMode.equals(DNS_MODE_OFF, ignoreCase = true)) {
- if (sharedPreferences.autoMode == AUTO_MODE_OPTION_AUTO || sharedPreferences.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)) {
- scope.launch {
- if (getNextAddress(dnsProvider) == null) {
- if (sharedPreferences.autoMode == AUTO_MODE_OPTION_PRIVATE) {
- changeDNSServer(DNS_MODE_PRIVATE, null)
- } else {
- if (sharedPreferences.autoMode == AUTO_MODE_OPTION_AUTO) {
- changeDNSServer(DNS_MODE_AUTO, dnsProvider)
- } else {
- changeDNSServer(DNS_MODE_OFF, dnsProvider)
- }
- }
- } else {
- changeDNSServer(DNS_MODE_PRIVATE, dnsProvider)
- }
- }
- }
- }
-
- /**
- * 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,
- 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 -> {
- 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
- )
- }
- }
- }
- }
- }
-
- override fun onClick() {
- super.onClick()
- if (!checkForPermission(this)) {
- return
- }
-
- // Require unlock to change mode according to user preference
- val requireUnlock = sharedPreferences.requireUnlock
- if (isLocked && requireUnlock) {
- unlockAndRun(this::cycleState)
- } else {
- cycleState()
- }
-
-
- }
-
- /**
- * Refreshes the state of the tile
- */
- private fun refreshTile() {
- val isPermissionGranted = checkForPermission(this)
- val dnsMode = Settings.Global.getString(contentResolver, "private_dns_mode")
- when (dnsMode?.lowercase()) {
- DNS_MODE_OFF -> {
- setTile(
- qsTile,
- if (!isPermissionGranted) Tile.STATE_UNAVAILABLE else Tile.STATE_INACTIVE,
- getString(R.string.dns_off),
- R.drawable.ic_off_black_24dp
- )
- }
-
- DNS_MODE_AUTO -> {
- setTile(
- qsTile,
- if (!isPermissionGranted) Tile.STATE_UNAVAILABLE else 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,
- if (!isPermissionGranted) Tile.STATE_UNAVAILABLE else Tile.STATE_ACTIVE,
- // 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,
- if (!isPermissionGranted) Tile.STATE_UNAVAILABLE else Tile.STATE_INACTIVE,
- getString(R.string.dns_unknown),
- R.drawable.ic_unknown_black_24dp
- )
- }
- }
- }
-
- private val broadcastReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- refreshTile()
- }
- }
-
- override fun onStartListening() {
- super.onStartListening()
-
- // Prevent some crashes
- if (qsTile == null) {
- Log.w(TAG, "onStartListening: qsTile is null")
- return
- }
-
-
- // Receive broadcasts to update the tile when server is changed from the dialog
- ContextCompat.registerReceiver(
- this,
- broadcastReceiver,
- IntentFilter("refresh_tile"),
- ContextCompat.RECEIVER_NOT_EXPORTED
- )
- isBroadcastReceiverRegistered = true
- refreshTile()
- }
-
- override fun onStopListening() {
- super.onStopListening()
- if (isBroadcastReceiverRegistered) {
- unregisterReceiver(broadcastReceiver)
- isBroadcastReceiverRegistered = false
- }
-
- }
-
- override fun onDestroy() {
- super.onDestroy()
- job.cancelChildren()
- }
-
- /**
- * 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 the database,
- * if current address is last or unknown returns null
- *
- * @param currentAddress currently set address
- * @return next address
- */
- private suspend fun getNextAddress(currentAddress: String?): DnsServer? {
- return if (currentAddress.isNullOrEmpty()) {
- repository.getFirstEnabled()
- } else {
- repository.getNextByServer(currentAddress)
- }
- }
-
- companion object {
- private const val TAG = "DnsTileService"
- }
-}
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/ui/DNSServerDialogFragment.kt b/app/src/main/java/ru/karasevm/privatednstoggle/ui/DNSServerDialogFragment.kt
deleted file mode 100644
index b2fa284..0000000
--- a/app/src/main/java/ru/karasevm/privatednstoggle/ui/DNSServerDialogFragment.kt
+++ /dev/null
@@ -1,140 +0,0 @@
-package ru.karasevm.privatednstoggle.ui
-
-import android.app.Dialog
-import android.content.Intent
-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.model.DnsServer
-import ru.karasevm.privatednstoggle.util.PrivateDNSUtils
-import ru.karasevm.privatednstoggle.util.PrivateDNSUtils.checkForPermission
-
-class DNSServerDialogFragment : DialogFragment() {
-
- private var _binding: SheetDnsSelectorBinding? = null
- private val binding get() = _binding!!
-
- private lateinit var linearLayoutManager: LinearLayoutManager
- private lateinit var adapter: ServerListRecyclerAdapter
- private var servers: MutableList = mutableListOf()
- private val dnsServerViewModel: DnsServerViewModel by viewModels { DnsServerViewModelFactory((requireActivity().application as PrivateDNSApp).repository) }
- private val contentResolver by lazy { requireActivity().contentResolver }
-
- 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
-
- adapter = ServerListRecyclerAdapter(false)
- binding.recyclerView.adapter = adapter
- 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(
- 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()
- if (!checkForPermission(requireContext())) {
- Toast.makeText(
- context, R.string.permission_missing, Toast.LENGTH_SHORT
- ).show()
- dialog!!.dismiss()
- }
- adapter.onItemClick = { id ->
- when (id) {
- OFF_ID -> {
- PrivateDNSUtils.setPrivateMode(
- contentResolver,
- PrivateDNSUtils.DNS_MODE_OFF
- )
- PrivateDNSUtils.setPrivateProvider(
- contentResolver,
- null)
- Toast.makeText(context, R.string.set_to_off_toast, Toast.LENGTH_SHORT).show()
- }
-
- AUTO_ID -> {
- PrivateDNSUtils.setPrivateMode(
- contentResolver,
- PrivateDNSUtils.DNS_MODE_AUTO
- )
- PrivateDNSUtils.setPrivateProvider(
- contentResolver,
- null)
- Toast.makeText(context, R.string.set_to_auto_toast, Toast.LENGTH_SHORT).show()
- }
-
- else -> {
- lifecycleScope.launch {
- val server = servers.find { server -> server.id == id }
- PrivateDNSUtils.setPrivateMode(
- contentResolver,
- PrivateDNSUtils.DNS_MODE_PRIVATE
- )
- PrivateDNSUtils.setPrivateProvider(
- contentResolver,
- server?.server
- )
- Toast.makeText(
- context,
- getString(
- R.string.set_to_provider_toast,
- server?.label?.ifEmpty { server.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"
- private const val AUTO_ID = -1
- private const val OFF_ID = -2
- }
-}
\ 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/ui/MainActivity.kt
deleted file mode 100644
index 26695a3..0000000
--- a/app/src/main/java/ru/karasevm/privatednstoggle/ui/MainActivity.kt
+++ /dev/null
@@ -1,480 +0,0 @@
-package ru.karasevm.privatednstoggle.ui
-
-import android.Manifest
-import android.content.ClipData
-import android.content.ClipDescription.MIMETYPE_TEXT_PLAIN
-import android.content.ClipboardManager
-import android.content.Intent
-import android.content.SharedPreferences
-import android.content.pm.PackageManager
-import android.graphics.Color
-import android.net.Uri
-import android.os.Build
-import android.os.Bundle
-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.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 rikka.shizuku.Shizuku
-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 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
-
-
-class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogListener,
- DeleteServerDialogFragment.NoticeDialogListener, Shizuku.OnRequestPermissionResultListener {
-
- private lateinit var linearLayoutManager: LinearLayoutManager
- private lateinit var binding: ActivityMainBinding
- private lateinit var sharedPrefs: SharedPreferences
- private lateinit var adapter: ServerListRecyclerAdapter
- private lateinit var clipboard: ClipboardManager
- 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) {
- var dragFrom = -1
- var dragTo = -1
-
- 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 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
- }
- }
- ItemTouchHelper(simpleItemTouchCallback)
- }
-
- private fun importSettings(json: String) {
- runCatching {
- val data: BackupUtils.Backup = Json.decodeFromString(json)
- BackupUtils.import(data, dnsServerViewModel, sharedPrefs)
- }.onSuccess {
- Toast.makeText(
- this, getString(R.string.import_success), Toast.LENGTH_SHORT
- ).show()
- }.onFailure { exception ->
- runCatching {
- Log.e("IMPORT", "Malformed json, falling back to legacy", exception)
- val data = Json.decodeFromString(json)
- BackupUtils.importLegacy(data, dnsServerViewModel, sharedPrefs)
- }.onSuccess {
- Toast.makeText(
- this, getString(R.string.import_success), Toast.LENGTH_SHORT
- ).show()
- }.onFailure { exception ->
- Log.e("IMPORT", "Import failed", exception)
- Toast.makeText(
- this, getString(R.string.import_failure), Toast.LENGTH_SHORT
- ).show()
- }
- }
- }
-
- /**
- * Migrate the SharedPreferences server list to Room
- */
- private fun migrateServerList() {
- dnsServerViewModel.viewModelScope.launch {
- 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().toMutableList()
- }
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- Shizuku.addRequestPermissionResultListener(this::onRequestPermissionResult)
-
- binding = ActivityMainBinding.inflate(layoutInflater)
- val view = binding.root
- setContentView(view)
-
- linearLayoutManager = LinearLayoutManager(this)
- binding.recyclerView.layoutManager = linearLayoutManager
-
- sharedPrefs = PreferenceHelper.defaultPreference(this)
- clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
-
- migrateServerList()
-
- 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
- }
- }
- 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)
- newFragment.show(supportFragmentManager, "add_server")
- }
- binding.recyclerView.adapter = adapter
- itemTouchHelper.attachToRecyclerView(binding.recyclerView)
-
- binding.topAppBar.setOnMenuItemClickListener { item ->
- when (item.itemId) {
- R.id.privacy_policy -> {
- val browserIntent = Intent(
- Intent.ACTION_VIEW,
- Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/privacy_policy")
- )
- startActivity(browserIntent)
- true
- }
-
- R.id.export_settings_clipboard -> {
- dnsServerViewModel.viewModelScope.launch {
- val data = BackupUtils.export(dnsServerViewModel, sharedPrefs)
- val jsonData = Json.encodeToString(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 activityContext = this
- dnsServerViewModel.viewModelScope.launch {
- val data = BackupUtils.export(dnsServerViewModel, sharedPrefs)
- val jsonData = Json.encodeToString(data)
- ShareCompat.IntentBuilder(activityContext).setText(jsonData)
- .setType("text/plain")
- .startChooser()
- }
- true
- }
-
- R.id.export_settings_file -> {
-
- 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)
- }
- true
- }
-
- R.id.import_settings_file -> {
- val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
- addCategory(Intent.CATEGORY_OPENABLE)
- type = "text/plain"
- }
- importResultLauncher.launch(intent)
- true
- }
-
- R.id.import_settings_clipboard -> {
- val clipData = clipboard.primaryClip?.getItemAt(0)
- val textData = clipData?.text
-
- if (textData != null) {
- importSettings(textData.toString())
- }
- true
- }
-
- R.id.options -> {
- val newFragment = OptionsDialogFragment()
- newFragment.show(supportFragmentManager, "options")
- true
- }
-
- else -> true
- }
- }
- }
-
- private var saveResultLauncher =
- registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
- if (result.resultCode == RESULT_OK) {
- val data: Intent? = result.data
- data?.data?.also { uri ->
- val jsonData = Json.encodeToString(BackupUtils.export(dnsServerViewModel, sharedPrefs))
- val contentResolver = applicationContext.contentResolver
- runCatching {
- contentResolver.openOutputStream(uri)?.use { outputStream ->
- outputStream.write(jsonData.toByteArray())
- }
- }.onFailure { exception ->
- Log.e("EXPORT", "Export failed", exception)
- Toast.makeText(
- this, getString(R.string.export_failure), Toast.LENGTH_SHORT
- ).show()
- }.onSuccess {
- Toast.makeText(
- this, getString(R.string.export_success), Toast.LENGTH_SHORT
- ).show()
- }
- }
- }
- }
-
- private var importResultLauncher =
- registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
- if (result.resultCode == RESULT_OK) {
- val data: Intent? = result.data
- data?.data?.also { uri ->
- val contentResolver = applicationContext.contentResolver
- contentResolver.openInputStream(uri)?.use { inputStream ->
- val jsonData = inputStream.bufferedReader().use { it.readText() }
- importSettings(jsonData)
- }
- }
- }
- }
-
- override fun onCreateOptionsMenu(menu: Menu?): Boolean {
- menuInflater.inflate(R.menu.menu_main, menu)
- return true
- }
-
- override fun onResume() {
- super.onResume()
- // Check if WRITE_SECURE_SETTINGS is granted
- if (checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) {
- // Check if Shizuku is available
- if (Shizuku.pingBinder()) {
- // check if permission is granted already
- val isGranted = if (Shizuku.isPreV11() || Shizuku.getVersion() < 11) {
- checkSelfPermission(ShizukuProvider.PERMISSION) == PackageManager.PERMISSION_GRANTED
- } else {
- Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
- }
- // request permission if not granted
- if (!isGranted && !Shizuku.shouldShowRequestPermissionRationale()) {
- if (Shizuku.isPreV11() || Shizuku.getVersion() < 11) {
- requestPermissions(arrayOf(ShizukuProvider.PERMISSION), 1)
- } else {
- Shizuku.requestPermission(1)
- }
- } else {
- grantPermission()
- }
- } else {
- if (checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) {
- val browserIntent = Intent(
- Intent.ACTION_VIEW,
- Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/")
- )
- Toast.makeText(
- this, R.string.shizuku_failure_toast, Toast.LENGTH_SHORT
- ).show()
- startActivity(browserIntent)
- finish()
- }
- }
- }
- }
-
- override fun onWindowFocusChanged(hasFocus: Boolean) {
- super.onWindowFocusChanged(hasFocus)
- if (!hasFocus) {
- // Gets the ID of the "paste" menu item.
- val pasteItem = binding.topAppBar.menu.findItem(R.id.import_settings_clipboard)
-
- // If the clipboard doesn't contain data, disable the paste menu item.
- // If it does contain data, decide whether you can handle the data.
- pasteItem.isEnabled = when {
- !clipboard.hasPrimaryClip() -> false
- !(clipboard.primaryClipDescription?.hasMimeType(MIMETYPE_TEXT_PLAIN))!! -> false
- else -> true
-
- }
- }
- }
-
- override fun onDestroy() {
- super.onDestroy()
- Shizuku.removeRequestPermissionResultListener(this::onRequestPermissionResult)
- }
-
- /**
- * 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")
- }
-
- /**
- * 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()) {
- dnsServerViewModel.insert(DnsServer(0, server))
- } else {
- dnsServerViewModel.insert(DnsServer(0, server, label))
- }
- }
-
- /**
- * Callback for deleting the server
- * @param id The server id
- */
- override fun onDeleteDialogPositiveClick(id: Int) {
- dnsServerViewModel.delete(id)
- }
-
- /**
- * 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
- }
- dnsServerViewModel.update(id, server, label, null, enabled)
- }
-
- private fun grantPermission() {
- if (grantPermissionWithShizuku(this)) {
- Toast.makeText(
- this, R.string.shizuku_success_toast, Toast.LENGTH_SHORT
- ).show()
- } else {
- Toast.makeText(
- this, R.string.shizuku_failure_toast, Toast.LENGTH_SHORT
- ).show()
- val browserIntent = Intent(
- Intent.ACTION_VIEW, Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/")
- )
- startActivity(browserIntent)
- finish()
- }
- }
-
- override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) {
- val isGranted = grantResult == PackageManager.PERMISSION_GRANTED
-
- if (!isGranted && checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) {
- val browserIntent = Intent(
- Intent.ACTION_VIEW, Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/")
- )
- startActivity(browserIntent)
- finish()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/ui/ServerListRecyclerAdapter.kt b/app/src/main/java/ru/karasevm/privatednstoggle/ui/ServerListRecyclerAdapter.kt
deleted file mode 100644
index fec1eb0..0000000
--- a/app/src/main/java/ru/karasevm/privatednstoggle/ui/ServerListRecyclerAdapter.kt
+++ /dev/null
@@ -1,124 +0,0 @@
-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() {
-
-
- 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 = 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, private var newList: List
- ) : 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) {
- 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)
- }
- }
- }
-
-}
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/util/BackupUtils.kt b/app/src/main/java/ru/karasevm/privatednstoggle/util/BackupUtils.kt
deleted file mode 100644
index 304f1db..0000000
--- a/app/src/main/java/ru/karasevm/privatednstoggle/util/BackupUtils.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-package ru.karasevm.privatednstoggle.util
-
-import android.content.SharedPreferences
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-import ru.karasevm.privatednstoggle.data.DnsServerViewModel
-import ru.karasevm.privatednstoggle.model.DnsServer
-import ru.karasevm.privatednstoggle.util.PreferenceHelper.autoMode
-import ru.karasevm.privatednstoggle.util.PreferenceHelper.requireUnlock
-
-object BackupUtils {
- @Serializable
- data class Backup(
- @SerialName("dns_servers") val dnsServers: List,
- @SerialName("auto_mode") val autoMode: Int?,
- @SerialName("require_unlock") val requireUnlock: Boolean?,
- )
-
- @Serializable
- data class LegacyBackup(
- @SerialName("dns_servers") val dnsServers: String,
- @SerialName("auto_mode") val autoMode: Int?,
- @SerialName("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 legacyBackup Deserialized backup
- * @param viewModel View model
- * @param sharedPreferences Shared preferences
- */
- fun importLegacy(
- legacyBackup: LegacyBackup,
- viewModel: DnsServerViewModel,
- sharedPreferences: SharedPreferences
- ) {
- legacyBackup.dnsServers.let { servers ->
- 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 = legacyBackup.autoMode?: 0
- sharedPreferences.requireUnlock = legacyBackup.requireUnlock == true
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/util/ShizukuUtil.kt b/app/src/main/java/ru/karasevm/privatednstoggle/util/ShizukuUtil.kt
deleted file mode 100644
index fe9b5cb..0000000
--- a/app/src/main/java/ru/karasevm/privatednstoggle/util/ShizukuUtil.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-package ru.karasevm.privatednstoggle.util
-
-import android.Manifest
-import android.content.Context
-import android.content.pm.IPackageManager
-import android.os.Build
-import android.os.Process
-import android.os.UserHandle
-import android.permission.IPermissionManager
-import android.util.Log
-import org.lsposed.hiddenapibypass.HiddenApiBypass
-import rikka.shizuku.ShizukuBinderWrapper
-import rikka.shizuku.SystemServiceHelper
-import ru.karasevm.privatednstoggle.util.PrivateDNSUtils.checkForPermission
-
-object ShizukuUtil {
-
- private const val TAG = "ShizukuUtil"
-
- /**
- * Attempts to grant the WRITE_SECURE_SETTINGS permission using Shizuku.
- *
- * @param context The context from which the method is called.
- * @return True if the permission was granted successfully, false otherwise.
- */
- fun grantPermissionWithShizuku(context: Context): Boolean {
- val packageName = context.packageName
- var userId = 0
- runCatching {
- val userHandle = Process.myUserHandle()
- userId = UserHandle::class.java.getMethod("getIdentifier").invoke(userHandle) as? Int ?: 0
- }
- if (Build.VERSION.SDK_INT >= 31) {
- HiddenApiBypass.addHiddenApiExemptions("Landroid/permission")
- val binder =
- ShizukuBinderWrapper(SystemServiceHelper.getSystemService("permissionmgr"))
- val pm = IPermissionManager.Stub.asInterface(binder)
- runCatching {
- pm.grantRuntimePermission(
- packageName,
- Manifest.permission.WRITE_SECURE_SETTINGS,
- userId
- )
- }.onFailure { e ->
- Log.w(TAG, "Android 12 method failed: ", e)
- runCatching {
- pm.grantRuntimePermission(
- packageName,
- Manifest.permission.WRITE_SECURE_SETTINGS,
- 0,
- userId
- )
- }.onFailure { e ->
- Log.w(TAG, "Android 14 QPR2 method failed: ", e)
- runCatching {
- pm.grantRuntimePermission(
- packageName,
- Manifest.permission.WRITE_SECURE_SETTINGS,
- "default:0",
- userId
- )
- }.onFailure { e ->
- Log.w(TAG, "Android 14 QPR3 method failed: ", e)
- }
- }
- }
- } else {
- val binder = ShizukuBinderWrapper(SystemServiceHelper.getSystemService("package"))
- val pm = IPackageManager.Stub.asInterface(binder)
- runCatching {
- pm.grantRuntimePermission(
- packageName,
- Manifest.permission.WRITE_SECURE_SETTINGS,
- userId
- )
- }.onFailure { e ->
- Log.w(TAG, "Android <12 method failed: ", e)
- }
- }
- return checkForPermission(context)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/utils/DnsServer.kt b/app/src/main/java/ru/karasevm/privatednstoggle/utils/DnsServer.kt
new file mode 100644
index 0000000..8a51706
--- /dev/null
+++ b/app/src/main/java/ru/karasevm/privatednstoggle/utils/DnsServer.kt
@@ -0,0 +1,7 @@
+package ru.karasevm.privatednstoggle.utils
+
+data class DnsServer(val label: String, val server: String) {
+ override fun toString(): String {
+ return "$label : $server"
+ }
+}
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/util/PrivateDNSUtils.kt b/app/src/main/java/ru/karasevm/privatednstoggle/utils/PrivateDNSUtils.kt
similarity index 76%
rename from app/src/main/java/ru/karasevm/privatednstoggle/util/PrivateDNSUtils.kt
rename to app/src/main/java/ru/karasevm/privatednstoggle/utils/PrivateDNSUtils.kt
index 9441421..79ae536 100644
--- a/app/src/main/java/ru/karasevm/privatednstoggle/util/PrivateDNSUtils.kt
+++ b/app/src/main/java/ru/karasevm/privatednstoggle/utils/PrivateDNSUtils.kt
@@ -1,11 +1,13 @@
-package ru.karasevm.privatednstoggle.util
+package ru.karasevm.privatednstoggle.utils
import android.Manifest
import android.content.ContentResolver
import android.content.Context
import android.content.pm.PackageManager
import android.provider.Settings
+import android.widget.Toast
import androidx.core.content.ContextCompat.checkSelfPermission
+import ru.karasevm.privatednstoggle.R
@Suppress("unused")
object PrivateDNSUtils {
@@ -21,32 +23,32 @@ 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)
}
fun checkForPermission(context: Context): Boolean {
- return checkSelfPermission(
- context,
- Manifest.permission.WRITE_SECURE_SETTINGS
- ) == PackageManager.PERMISSION_GRANTED
+ if (checkSelfPermission(
+ context,
+ Manifest.permission.WRITE_SECURE_SETTINGS
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+ return true
+ }
+ Toast.makeText(context, R.string.permission_missing, Toast.LENGTH_SHORT).show()
+ return false
}
}
\ No newline at end of file
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/util/SharedPrefUtils.kt b/app/src/main/java/ru/karasevm/privatednstoggle/utils/SharedPrefUtils.kt
similarity index 87%
rename from app/src/main/java/ru/karasevm/privatednstoggle/util/SharedPrefUtils.kt
rename to app/src/main/java/ru/karasevm/privatednstoggle/utils/SharedPrefUtils.kt
index 129a56b..87cfa96 100644
--- a/app/src/main/java/ru/karasevm/privatednstoggle/util/SharedPrefUtils.kt
+++ b/app/src/main/java/ru/karasevm/privatednstoggle/utils/SharedPrefUtils.kt
@@ -1,13 +1,13 @@
-package ru.karasevm.privatednstoggle.util
+package ru.karasevm.privatednstoggle.utils
import android.content.Context
import android.content.SharedPreferences
object PreferenceHelper {
- const val DNS_SERVERS = "dns_servers"
- const val AUTO_MODE = "auto_mode"
- const val REQUIRE_UNLOCK = "require_unlock"
+ private const val DNS_SERVERS = "dns_servers"
+ private const val AUTO_MODE = "auto_mode"
+ private const val REQUIRE_UNLOCK = "require_unlock"
fun defaultPreference(context: Context): SharedPreferences =
context.getSharedPreferences("app_prefs", 0)
@@ -26,7 +26,7 @@ object PreferenceHelper {
is Boolean -> putBoolean(key, value)
is Long -> putLong(key, value)
is Float -> putFloat(key, value)
- else -> error("Only primitive types can be stored in SharedPreferences, got ${value.javaClass}")
+ else -> error("Only primitive types can be stored in SharedPreferences")
}
}
@@ -38,7 +38,6 @@ object PreferenceHelper {
}
}
-
var SharedPreferences.autoMode
get() = getInt(AUTO_MODE, PrivateDNSUtils.AUTO_MODE_OPTION_OFF)
set(value) {
@@ -54,4 +53,5 @@ object PreferenceHelper {
it.put(REQUIRE_UNLOCK to value)
}
}
+
}
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 88e1536..3b78639 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -4,8 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".ui.MainActivity"
- android:fitsSystemWindows="true">
+ tools:context=".MainActivity">
-
-
-
-
-
-
+ app:layout_constraintTop_toTopOf="parent">
diff --git a/app/src/main/res/layout/recyclerview_row.xml b/app/src/main/res/layout/recyclerview_row.xml
index 4da379a..67c06bf 100644
--- a/app/src/main/res/layout/recyclerview_row.xml
+++ b/app/src/main/res/layout/recyclerview_row.xml
@@ -2,8 +2,7 @@
diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml
index fcfec62..bce5f6b 100644
--- a/app/src/main/res/menu/menu_main.xml
+++ b/app/src/main/res/menu/menu_main.xml
@@ -7,33 +7,6 @@
app:showAsAction="ifRoom"
android:icon="@drawable/ic_baseline_settings_24"
/>
- -
-
-
- -
-
-
diff --git a/app/src/main/res/resources.properties b/app/src/main/res/resources.properties
deleted file mode 100644
index d5a3ddc..0000000
--- a/app/src/main/res/resources.properties
+++ /dev/null
@@ -1 +0,0 @@
-unqualifiedResLocale=en-US
\ No newline at end of file
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
deleted file mode 100644
index 048dcdf..0000000
--- a/app/src/main/res/values-fr/strings.xml
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
- Terminé
- Private DNS Quick Toggle
- Inconnu
- Éteint
- Ajouter un serveur
- Ajouter
- Politique de confidentialité
- Êtes-vous sûr de vouloir supprimer le serveur ?
- Supprimer
- L\'adresse du serveur ne peut pas être vide
- Étiquette du serveur DNS (Facultatif)
- Adresse du serveur DNS
- Options
- OK
- Seulement éteint
- Seulement automatique
- Éteint et automatique
- Automatique
- Commutateur de DNS privé
- Enregistrer
- Autorisation non accordée, vérifiez les instructions dans l\'application
- Sélectionner le serveur
- Définissez les options à inclure
- DNS privé seulement
- Ouvrir l\'application
- DNS privé éteint
- DNS privé réglé sur automatique
- DNS privé réglé sur %1$s
- Requiert le déverrouillage de l\'appareil pour changer de serveur
- Activé
- À partir du fichier
- Aucun serveur ajouté
- Copié
- Éditer le serveur
- Échec de la sauvegarde
- Appuyez sur le bouton ci-dessous pour en ajouter un
- Impossible d\'obtenir l\'autorisation, veuillez l\'accorder manuellement
- Autorisation accordée, vous pouvez désormais révoquer l\'autorisation Shizuku
- À partir du presse-papier
- Vers le presse-papier
- Partager
- Vers le fichier
- Sauvegarde réussie
- Échec de l\'importation
- Échec de l\'importation, le fichier JSON est incorrect
- Importé
- Exporter
- Importer
- Poignée
- Supprimer
- Annuler
-
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
deleted file mode 100644
index 3e9260d..0000000
--- a/app/src/main/res/values-hu/strings.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-
- Privát DNS Gyorskapcsoló
- Privát DNS Kapcsoló
- Nincs engedély megadva, nézd meg az alkalmazásban, hogyan adhatod meg
- Ki
- Automatikus
- Ismeretlen
- Szerver hozzáadása
- Hozzáadás
- Mentés
- Adatvédelmi irányelvek
- Szerver kiválasztása
- Kész
- Mégse
- Törlés
- Biztosan törölni szeretnéd a szervert?
- Törlés
- A szervercím nem lehet üres
- DNS szerver neve (opcionális)
- DNS szerver címe
- Beállítások
- OK
- Válaszd ki, mely opciók jelenjenek meg a csempén
- Csak ki
- Csak automatikus
- Ki és automatikus
- Csak Privát DNS
- Alkalmazás megnyitása
- Privát DNS kikapcsolva
- Privát DNS automatikus módra állítva
- Privát DNS beállítva: %1$s
- Eszköz feloldása szükséges a szerver módosításához
- Húzási fogantyú
- Importálás
- Exportálás
- Importálva
- Importálás sikertelen
- Importálás sikertelen, hibás JSON
- Másolva
- Fájlból
- Vágólapról
- Vágólapra
- Megosztás
- Fájlba
- Mentés sikertelen
- Sikeresen mentve
- Szerver szerkesztése
- Nincsenek szerverek hozzáadva
- Koppints az alábbi gombra, hogy hozzáadj egyet
- Engedélyezve
-
diff --git a/app/src/main/res/values-mn/strings.xml b/app/src/main/res/values-mn/strings.xml
deleted file mode 100644
index 10de470..0000000
--- a/app/src/main/res/values-mn/strings.xml
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
- Хувийн DNS солих
- Унтраах
- Тодорхойгүй
- Сервер нэмэх
- Хадгалах
- Болсон
- Болих
- Устгах
- Та серверийг устгахдаа итгэлтэй байна уу?
- Устгах
- Серверийн хаяг хоосон байж болохгүй
- DNS серверийн шошго (заавал биш)
- DNS серверийн хаяг
- Сонголтууд
- ОК
- Хавтан дээр ямар сонголтыг оруулахаа сонгоно уу
- Зөвхөн унтарсан
- Зөвхөн авто
- Унтарсан болон авто
- Хувийн DNS-г %1$s болгож тохируулсан
- Серверийг өөрчлөхийн тулд төхөөрөмжийн түгжээг тайлах шаардлагатай
- Бариулыг чирэх
- Импорт
- Импортолсон
- Импорт хийж чадсангүй
- Файлаас
- Хадгалж чадсангүй
- Амжилттай хадгалсан
- Сервер засах
- Хувийн DNS хурдан сэлгэх
- Нууцлалын бодлого
- Зөвшөөрөл олгоогүй. Үүнийг хэрхэн хийхийг харна уу
- Авто
- Нэмэх
- Сервер сонгох
- Зөвхөн хувийн DNS
- Апп нээх
- Түр санах ой руу
- Хувийн DNS унтарсан
- Хувийн DNS-г автоматаар тохируулсан
- Экспорт
- Хуулагдсан
- Хуваалцах
- Импорт хийж чадсангүй, алдаатай JSON
- Түр санах ойноос
- Файлруу
-
\ No newline at end of file
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
deleted file mode 100644
index 6c76614..0000000
--- a/app/src/main/res/values-pl/strings.xml
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
- Usuń
- Zapisz
- Polityka prywatności
- Wybierz serwer
- Anuluj
- Adres serwera DNS
- OK
- Otwórz aplikację
- Importuj
- Eksportuj
- Skopiowano
- Z pliku
- Ze schowka
- Do schowka
- Udostępnij
- Do pliku
- Edytuj serwer
- Brak dodanych serwerów
- Włączone
- Dodaj serwer
- Dodaj
- Usuń
- Nieznane
- Gotowe
- Opcje
- Importowanie nie powiodło się
- Automatycznie
- Wyłącz
- Zaimportowano
- Adres serwera nie może być pusty
- Import nie powiódł się, zniekształcony plik JSON
- Zapisano pomyślnie
- Czy na pewno chcesz usunąć serwer?
- Private DNS Quick Toggle
- Przełącznik prywatnego DNS
- Nieprzydzielono uprawnienia, sprawdź w aplikacji, w jaki sposób można to zrobić
- Opis serwera DNS (opcjonalnie)
- Wybierz opcje, które będą dostępne w kafelku
- Tylko wyłączenie
- Tylko automatycznie
- Wyłączenie i automatycznie
- Tylko prywatny DNS
- Prywatny DNS zmieniony na automatyczny
- Prywatny DNS zmieniony na %1$s
- Wymagaj odblokowania urządzenia do zmiany serwera
- Wyłączono Prywatny DNS
- Przeciągnij
- Zapisywanie nie powiodło się
- Kliknij na poniższy przycisk, aby dodać nowy
- Udzielono zezwolenia, możesz teraz cofnąć zezwolenie w Shizuku
- Uzyskanie uprawnień nie powiodło się, udziel ich ręcznie
-
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
deleted file mode 100644
index c4691bd..0000000
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
- Importado
- Permissão não concedida, verifique o app para saber como prosseguir
- Alteração de DNS privado
- Cancelar
- Adicionar servidor
- Endereço do servidor DNS
- Escolha opções disponível em atalho
- Alteração de DNS privado
- Desativado
- Automático
- Indeterminado
- Adicionar
- Salvar
- Política de privacidade
- Concluído
- Apagar
- Tem certeza de que quer apagar o servidor?
- Apagar
- O endereço do servidor não pode estar em branco
- Identificação do servidor DNS (opcional)
- Opções
- Ok
- Somente desativado
- Desativado e automático
- Somente DNS privado
- Abrir app
- DNS privado desativado
- DNS privado definido para automático
- DNS privado definido para %1$s
- Arrastre
- Importar
- Exportar
- Falha na importação, JSON malformado
- Copiado
- Da memória
- Compartilhar
- Para arquivo
- Salvo com sucesso
- Editar servidor
- Nenhum servidor adicionado
- Toque no botão abaixo para adicionar
- Ativado
- Escolha servidor
- Para memória
- De arquivo
- Falha ao importar
- Falha ao salvar
- Necessário desbloquear o dispositivo para alterar servidor
- Somente automático
- Falha ao obter a permissão. Tente conceder manualmente
- Permissão concedida, você pode revogar a permissão do Shizuku agora
-
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
deleted file mode 100644
index cef59e3..0000000
--- a/app/src/main/res/values-ru/strings.xml
+++ /dev/null
@@ -1,53 +0,0 @@
-
- Private DNS Quick Toggle
- Переключить частный DNS
- Разрешение не предоставлено, проверьте приложение для получения информации
- Выкл
- Авто
- Неизвестно
- Добавить сервер
- Добавить
- Сохранить
- Политика конфиденциальности
- Выбрать сервер
- Готово
- Отмена
- Удалить
- Вы уверены, что хотите удалить сервер?
- Удалить
- Адрес сервера не может быть пустым
- Название DNS сервера (необязательно)
- Адрес DNS сервера
- Опции
- OK
- Выберите, какие опции включить в плитке
- Только \"Выкл\"
- Только \"Авто\"
- \"Выкл\" и \"Авто\"
- Только частный DNS
- Открыть приложение
- Частный DNS выключен
- Частный DNS установлен на "Авто"
- Частный DNS установлен на %1$s
- Смена сервера требует разблокировки устройства
- Ручка перетаскивания
- Импорт
- Экспорт
- Успешно импортировано
- Импорт не удался
- Импорт не удался, некорректный JSON
- Скопировано
- Из файла
- Из буфера обмена
- В буфер обмена
- Поделиться
- В файл
- Сохранение не удалось
- Успешно сохранено
- Редактировать сервер
- Нет доступных серверов
- Нажмите на кнопку ниже, чтобы добавить сервер
- Включён
- Разрешение получено, можно отозвать авторизацию Shizuku
- Не удалось получить разрешение, предоставьте его вручную
-
\ No newline at end of file
diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml
deleted file mode 100644
index 1cb02d2..0000000
--- a/app/src/main/res/values-ta/strings.xml
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
- சரி
- தனியார் டி.என்.எச் விரைவாக மாற்று
- தனியார் டி.என் கள் மாறுகின்றன
- இசைவு வழங்கப்படவில்லை, அதை எப்படி செய்வது என்று பார்க்க பயன்பாட்டை சரிபார்க்கவும்
- அணை
- தானி
- தெரியவில்லை
- சேவையகத்தைச் சேர்க்கவும்
- கூட்டு
- சேமி
- தனியுரிமைக் கொள்கை
- சேவையகத்தைத் தேர்ந்தெடுக்கவும்
- முடிந்தது
- ரத்துசெய்
- நீக்கு
- சேவையகத்தை நீக்க விரும்புகிறீர்களா?
- நீக்கு
- டிஎன்எச் சேவையக முகவரி
- விருப்பங்கள்
- ஓடுகளில் எந்த விருப்பங்களைச் சேர்க்க வேண்டும் என்பதைத் தேர்வுசெய்க
- மட்டுமே
- ஆட்டோ மட்டுமே
- ஆஃப் மற்றும் ஆட்டோ
- தனியார் டி.என்.எச் மட்டுமே
- திறந்த பயன்பாடு
- தனியார் டி.என்.எச் அணைக்கப்பட்டது
- தனியார் டி.என்.எச் ஆட்டோவாக அமைக்கப்பட்டுள்ளது
- தனியார் டி.என்.எச் %1$s என அமைக்கப்பட்டுள்ளது
- சேவையகத்தை மாற்ற சாதனத்தைத் திறக்க வேண்டும்
- இழுவை கைப்பிடி
- இறக்குமதி
- ஏற்றுமதி
- இறக்குமதி செய்யப்பட்டது
- இறக்குமதி தோல்வியடைந்தது
- இறக்குமதி தோல்வியுற்றது, தவறாக சாதொபொகு
- நகலெடுக்கப்பட்டது
- கோப்பிலிருந்து
- கிளிப்போர்டிலிருந்து
- இடைநிலைப்பலகைக்கு
- பங்கு
- தாக்கல் செய்ய
- சேமிப்பு தோல்வியடைந்தது
- வெற்றிகரமாக சேமிக்கப்பட்டது
- சேவையகத்தைத் திருத்து
- சேவையகங்கள் எதுவும் சேர்க்கப்படவில்லை
- ஒன்றைச் சேர்க்க கீழே உள்ள பொத்தானைத் தட்டவும்
- இயக்கப்பட்டது
- இசைவு வழங்கப்பட்டது, நீங்கள் இப்போது சிசுகு அனுமதியை ரத்து செய்யலாம்
- இசைவு பெறுவதில் தோல்வி, தயவுசெய்து அதை கைமுறையாக வழங்கவும்
- சேவையக முகவரி காலியாக இருக்க முடியாது
- டிஎன்எச் சேவையக சிட்டை (விரும்பினால்)
-
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
deleted file mode 100644
index 57f2c8d..0000000
--- a/app/src/main/res/values-tr/strings.xml
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
- Sunucuyu silmek istediğinizden emin misiniz?
- Sadece kapalı
- Sunucuyu değiştirmek için cihazın kilidini açmanız gerekiyor
- Oto
- Bilinmeyen
- Tamam
- İptal
- DNS sunucusu Etiketi (İsteğe bağlı)
- Seçenekler
- OK
- Sadece oto
- Kapalı ve oto
- Özel DNS kapandı
- Uygulamayı açınız
- İçe aktar
- Dışa aktar
- içe aktarıldı
- Dosyaya
- Kaydetme başarısız oldu
- Hiç Sunucu Eklenmedi
- Eklemek için aşağıdaki düğmeye dokunun
- İzin alınamadı, lütfen manuel olarak verin
- Karoya hangi seçeneklerin dahil edileceğini seçiniz
- Kapalı
- Gizlilik Politikası
- Sil
- Sunucu adresi boş olamaz
- Sil
- DNS sunucu adresi
- tutacağı sürükle
- Ekle
- Sunucu Ekleyiniz
- İzin verilmedi, nasıl yapıldığını görmek için uygulamayı kontrol ediniz
- İçe aktarma başarısız oldu, hatalı biçimlendirilmiş JSON
- Özel DNS Hızlı Geçiş
- İzin verildi, şimdi Shizuku iznini iptal edebilirsiniz
- Aktarma başarısız
- Panodan
- Başarıyla kaydedildi
- Kaydet
- Özel DNS otomatik olarak ayarlandı
- Özel DNS %1$s olarak ayarlandı
- Sadece Özel DNS
- Sunucuyu Seçin
- Kopyalandı
- Sunucuyu düzenle
- Etkin
- Özel DNS Geçişi
- Dosyadan
- Panoya
- Paylaş
-
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
deleted file mode 100644
index 1f423ae..0000000
--- a/app/src/main/res/values-vi/strings.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
- Thêm máy chủ
- Chỉ tự động
- Chưa được cấp quyền, hãy kiểm tra ứng dụng để biết cách thực hiện
- Tắt
- Tự động
- Thêm
- Chính sách bảo mật
- Chọn máy chủ
- Hoàn thành
- Hủy
- Bạn có chắc chắn muốn xóa máy chủ không?
- Xoá
- Địa chỉ máy chủ DNS
- Tùy chọn
- OK
- Chọn các tùy chọn để đưa vào ô
- Chỉ tắt
- Tắt và tự động
- Mở ứng dụng
- DNS cá nhân được thiết lập tự động
- Yêu cầu mở khóa thiết bị để thay đổi máy chủ
- Tay cầm kéo
- Xuất
- Đã nhập
- Nhập thất bại
- Nhập thất bại, JSON bị lỗi
- Đã sao chép
- Từ tập tin
- Chia sẻ
- Thành tập tin
- Lưu không thành công
- Chỉnh sửa máy chủ
- Chưa có máy chủ nào
- Nhấn vào nút bên dưới để thêm
- Đã bật
- Chuyển đổi DNS cá nhân
- Xoá
- Chuyển đổi nhanh DNS cá nhân
- Không rõ
- Nhãn máy chủ DNS (Không bắt buộc)
- Lưu
- Địa chỉ máy chủ không được để trống
- Chỉ DNS cá nhân
- Đã tắt DNS cá nhân
- DNS cá nhân được đặt thành %1$s
- Nhập
- Từ bảng nhớ tạm
- Vào bảng nhớ tạm
- Đã lưu thành công
-
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
deleted file mode 100644
index a217315..0000000
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
- 私有DNS触发
- 必要权限未授予,请看相关说明
- 关闭
- 自动
- 未知
- 添加DNS服务器
- 添加
- 存储
- 隐私策略
- 选择服务器
- 完成
- 取消
- 删除条目
- 你确认要删除这个服务器条目吗?
- 删除
- 服务器地址不可为空
- DNS服务器标识
- DNS服务器地址
- 选项
- 确认
- 选择要在磁贴中启用的选项
- 仅“关闭”
- 仅“自动”
- “关闭“与”自动“
- 仅设置的私有DNS
- 打开软件
- 不使用私有DNS
- 自动使用私有DNS
- 设置为使用私有DNS\"%1$s\"
- 更改服务器设置要求设备解锁
- 拖动把手
- 导入
- 导出
- 已导入
- 导入失败
- 导入失败,json格式异常
- 已复制
- 从文件导入
- 从剪贴板导入
- 导出至剪贴板
- 分享
- 导出至文件
- 保存失败
- 保存成功
- 编辑服务器条目
- 无可用服务器
- 点击下方\"+\"添加一个吧
- 已启用
-
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8124a26..3f3912e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -7,7 +7,6 @@
Unknown
Add Server
Add
- Save
Privacy Policy
Select Server
Done
@@ -31,23 +30,4 @@
Private DNS set to %1$s
Require unlocking the device to change server
Drag handle
- Import
- Export
- Imported
- Import failed
- Import failed, malformed JSON
- Copied
- From file
- From clipboard
- To clipboard
- Share
- To file
- Saving failed
- Saved successfully
- Edit server
- No Servers Added
- Tap on the button below to add one
- Enabled
- Permission granted, you can revoke the Shizuku permission now
- Failed to acquire permission, please grant it manually
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..01ac801
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,18 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:8.5.0'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22"
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+tasks.register('clean', Delete) {
+ delete rootProject.layout.buildDirectory
+}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
deleted file mode 100644
index 45827dc..0000000
--- a/build.gradle.kts
+++ /dev/null
@@ -1,19 +0,0 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
-buildscript {
- repositories {
- google()
- mavenCentral()
- }
- dependencies {
- classpath("com.android.tools.build:gradle:8.8.1")
- classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.20")
-
- // NOTE: Do not place your application dependencies here; they belong
- // in the individual module build.gradle files
- }
-}
-plugins {
- id("org.jetbrains.kotlin.android") version "2.0.20" apply false
- id("org.jetbrains.kotlin.plugin.serialization") version "2.0.20"
- id("com.google.devtools.ksp") version "2.0.20-1.0.25" apply false
-}
diff --git a/fastlane/metadata/android/en-US/changelogs/12.txt b/fastlane/metadata/android/en-US/changelogs/12.txt
deleted file mode 100644
index 46fe2d8..0000000
--- a/fastlane/metadata/android/en-US/changelogs/12.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-- Support for selection of only Private DNS in Option Dialog by @InfiniteCoder06
-- Support Labels by @InfiniteCoder06
-- Feature: Reordring by @InfiniteCoder06
-- Possible tile update fix
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/13.txt b/fastlane/metadata/android/en-US/changelogs/13.txt
deleted file mode 100644
index c546656..0000000
--- a/fastlane/metadata/android/en-US/changelogs/13.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-- Settings export/import
-- Fix label not appearing in some cases
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/14.txt b/fastlane/metadata/android/en-US/changelogs/14.txt
deleted file mode 100644
index d8ece24..0000000
--- a/fastlane/metadata/android/en-US/changelogs/14.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-- Add an option to edit servers
-- Add placeholder for empty server list
-- Fix layout for longer server addresses
diff --git a/fastlane/metadata/android/en-US/changelogs/15.txt b/fastlane/metadata/android/en-US/changelogs/15.txt
deleted file mode 100644
index 8541dd6..0000000
--- a/fastlane/metadata/android/en-US/changelogs/15.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-- Fix crashes on Android 11 and earlier
-- Fix list entry layout
diff --git a/fastlane/metadata/android/en-US/changelogs/16.txt b/fastlane/metadata/android/en-US/changelogs/16.txt
deleted file mode 100644
index 1005cb7..0000000
--- a/fastlane/metadata/android/en-US/changelogs/16.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-- 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
diff --git a/fastlane/metadata/android/en-US/changelogs/17.txt b/fastlane/metadata/android/en-US/changelogs/17.txt
deleted file mode 100644
index 3709a99..0000000
--- a/fastlane/metadata/android/en-US/changelogs/17.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-- 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
-- Replaced gson with kotlinx.serialization
-- Add Chinese Simplified translation (thanks @WeiguangTWK)
-- Add Russian translation
-- Fixed issue with provider not resetting when disabled through the dialog
diff --git a/fastlane/metadata/android/en-US/changelogs/18.txt b/fastlane/metadata/android/en-US/changelogs/18.txt
deleted file mode 100644
index 067cc8f..0000000
--- a/fastlane/metadata/android/en-US/changelogs/18.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-- Add Shizuku support for newer Android versions
-- Fix some crashes
-- Improve Shizuku process feedback
-- Fix Shizuku when not running as the primary user
-- Hungarian translation by @Pacuka in https://github.com/karasevm/PrivateDNSAndroid/pull/43
-- Add Polish translation (Michal L (@chuckmichael), Eryk Michalak (gnu-ewm))
-- Add Mongolian translation (Purevbaatar Tuvshinjargal (@puujee0238))
-- Add Portuguese (Brazil) translation (ajan, Víctor Assunção (@JoaoVictorAS))
-- Add Vietnamese translation (tuấn nguyễn (@Tuan1-2-3))
-- Add French translation (papaindiatango)
-- Add Tamil translation (தமிழ்நேரம் (@TamilNeram))
-- Add Turkish translation (Mustafa A. (mistiik99))
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/01.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/01.png
index 8d0d479..6e7345f 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/01.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/01.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/05.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/05.png
index abd46d3..6ff6f68 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/05.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/05.png differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index f537c92..0725b7c 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Mon Aug 16 15:36:35 MSK 2021
distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
diff --git a/settings.gradle.kts b/settings.gradle
similarity index 93%
rename from settings.gradle.kts
rename to settings.gradle
index 72cb6bf..4ccd1ba 100644
--- a/settings.gradle.kts
+++ b/settings.gradle
@@ -6,4 +6,4 @@ dependencyResolutionManagement {
}
}
rootProject.name = "Private DNS Quick Toggle"
-include("app")
+include ':app'