Compare commits

...

36 commits
v1.8.1 ... main

Author SHA1 Message Date
Víctor Assunção
bd61fe6748 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (51 of 51 strings)

Co-authored-by: Víctor Assunção <joaovictor.jvas@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/pt_BR/
Translation: PrivateDNSAndroid/Private DNS Quick Toggle
2025-02-25 13:29:40 +03:00
Michal L
87fe66af20 Translated using Weblate (Polish)
Currently translated at 100.0% (51 of 51 strings)

Co-authored-by: Michal L <michalrmsmi@wp.pl>
Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/pl/
Translation: PrivateDNSAndroid/Private DNS Quick Toggle
2025-02-25 13:29:40 +03:00
தமிழ்நேரம்
c375776575 Translated using Weblate (Tamil)
Currently translated at 100.0% (51 of 51 strings)

Added translation using Weblate (Tamil)

Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/ta/
Translation: PrivateDNSAndroid/Private DNS Quick Toggle
2025-02-25 13:29:40 +03:00
Mustafa A
3b24d24ba4 Translated using Weblate (Turkish)
Currently translated at 98.0% (50 of 51 strings)

Added translation using Weblate (Turkish)

Co-authored-by: Mustafa A <musty_99@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/tr/
Translation: PrivateDNSAndroid/Private DNS Quick Toggle
2025-02-25 13:29:40 +03:00
ajan
e8885409b5 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (51 of 51 strings)

Co-authored-by: ajan <ajan.ib.rown969@googlemail.com>
Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/pt_BR/
Translation: PrivateDNSAndroid/Private DNS Quick Toggle
2025-02-25 13:29:40 +03:00
Maksim Karasev
2aa895cd5e Add 1.10.0 changelog 2025-02-25 13:29:11 +03:00
Maksim Karasev
bdd98109ed Update layout for Android 15 2025-02-25 13:21:09 +03:00
Maksim Karasev
3b09f605f9 Bump dependencies 2025-02-25 11:27:46 +03:00
papaindiatango
9d62e91b60 Translated using Weblate (French)
Currently translated at 90.1% (46 of 51 strings)

Translation: PrivateDNSAndroid/Private DNS Quick Toggle
Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/fr/
2025-01-18 18:55:27 +03:00
tuấn nguyễn
e155d17dd7 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (49 of 49 strings)

Translation: PrivateDNSAndroid/Private DNS Quick Toggle
Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/vi/
2025-01-17 16:00:25 +03:00
tuấn nguyễn
e2104952bc Added translation using Weblate (Vietnamese) 2025-01-17 16:00:25 +03:00
ajan
24800f7f2d Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (49 of 49 strings)

Translation: PrivateDNSAndroid/Private DNS Quick Toggle
Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/pt_BR/
2025-01-17 16:00:25 +03:00
ajan
6a5f2af6f6 Added translation using Weblate (Portuguese (Brazil)) 2025-01-17 16:00:25 +03:00
Michal L
0e0e0bf9b4 Translated using Weblate (Polish)
Currently translated at 100.0% (49 of 49 strings)

Translation: PrivateDNSAndroid/Private DNS Quick Toggle
Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/pl/
2025-01-17 16:00:25 +03:00
Michal L
4c6240bd34 Translated using Weblate (Polish)
Currently translated at 71.4% (35 of 49 strings)

Translation: PrivateDNSAndroid/Private DNS Quick Toggle
Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/pl/
2025-01-17 16:00:25 +03:00
Michal L
e4b9e84f8c Translated using Weblate (Polish)
Currently translated at 67.3% (33 of 49 strings)

Translation: PrivateDNSAndroid/Private DNS Quick Toggle
Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/pl/
2025-01-17 16:00:25 +03:00
Eryk Michalak
a379c81cb9 Translated using Weblate (Polish)
Currently translated at 59.1% (29 of 49 strings)

Translation: PrivateDNSAndroid/Private DNS Quick Toggle
Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/pl/
2025-01-17 16:00:25 +03:00
Michal L
6164a35f04 Translated using Weblate (Polish)
Currently translated at 59.1% (29 of 49 strings)

Translation: PrivateDNSAndroid/Private DNS Quick Toggle
Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/pl/
2025-01-17 16:00:25 +03:00
Michal L
e4498ca64a Added translation using Weblate (Polish) 2025-01-17 16:00:25 +03:00
Purevbaatar Tuvshinjargal
75413fddcd Translated using Weblate (Mongolian)
Currently translated at 93.8% (46 of 49 strings)

Translation: PrivateDNSAndroid/Private DNS Quick Toggle
Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/mn/
2025-01-17 16:00:25 +03:00
Purevbaatar Tuvshinjargal
6cc99b7820 Added translation using Weblate (Mongolian) 2025-01-17 16:00:25 +03:00
Maksim Karasev
efd48b8984 Add support for Shizuku on Android 14 QPR3+ (#45) 2025-01-17 15:56:15 +03:00
Maksim Karasev
b39d7e3624 Update .gitignore 2025-01-17 15:52:24 +03:00
Maksim Karasev
4f6dc13c12 Fix miscellaneous bugs and simplify code 2025-01-17 15:50:14 +03:00
Maksim Karasev
5ba03acbcc Enable Strict Mode for debug builds 2025-01-17 15:44:32 +03:00
Maksim Karasev
627771d4b1 Migrate build config to Kotlin 2025-01-17 15:38:40 +03:00
Pacuka
8c7ff2ca8f
Add files via upload (#43)
Hungarian translation by Pacuka
2024-12-17 13:13:46 +03:00
Maksim Karasev
402b084954 Include Contributing section in the readme 2024-11-17 12:57:43 +03:00
Maksim Karasev
8c927d6b26 Remove region from RU locale 2024-11-06 14:16:03 +03:00
Maksim Karasev
7cdc2bbb84 Bump version 2024-11-05 15:59:41 +03:00
Maksim Karasev
835e9381ea Add Russian translation 2024-11-05 15:54:07 +03:00
Maksim Karasev
14b320ac68 Enable per-app language support 2024-11-05 15:53:22 +03:00
Maksim Karasev
681e6ceef4 Reset provider when disabling through the dialog 2024-10-03 17:10:23 +03:00
Maksim Karasev
6a5f405211 Replace gson with kotlinx-serialization 2024-10-03 16:38:23 +03:00
Weiguangtwk
fa9d259a21
Add Chinese Simplified Translation (#32)
* Add Chinese Simplified Translation

* Add new translation

---------

Co-authored-by: WeiguangTWK <weiguangtwk@outlook.com>
2024-09-24 13:43:47 +03:00
Maksim Karasev
36be36d69b Major refactor
* Replaced server storage backend with Room, allowing for easier further expansion
* Add option to disable saved servers
* Improved backup handling
* Fixed desync bug while dragging servers
* Reorganized source file structure
* Updated Kotlin version
* Updated Java version
2024-09-13 12:45:11 +03:00
52 changed files with 1985 additions and 869 deletions

5
.gitignore vendored
View file

@ -67,3 +67,8 @@ fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# kotlin
.kotlin/
*~

2
.idea/compiler.xml generated
View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
<bytecodeTargetLevel target="21" />
</component>
</project>

2
.idea/kotlinc.xml generated
View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.8.22" />
<option name="version" value="2.0.20" />
</component>
</project>

View file

@ -1,6 +1,7 @@
[![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/karasevm/PrivateDNSAndroid/total)](https://github.com/karasevm/PrivateDNSAndroid/releases/latest)
[![GitHub Release](https://img.shields.io/github/v/release/karasevm/PrivateDNSAndroid)](https://github.com/karasevm/PrivateDNSAndroid/releases/latest)
[![IzzyOnDroid](https://img.shields.io/endpoint?url=https://apt.izzysoft.de/fdroid/api/v1/shield/ru.karasevm.privatednstoggle&label=IzzyOnDroid)](https://apt.izzysoft.de/fdroid/index/apk/ru.karasevm.privatednstoggle)
[![Translation status](https://hosted.weblate.org/widget/privatednsandroid/private-dns-quick-toggle/svg-badge.svg)](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
@ -12,11 +13,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).
@ -31,3 +32,18 @@ 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
<a href="https://hosted.weblate.org/engage/privatednsandroid/">
<img src="https://hosted.weblate.org/widget/privatednsandroid/private-dns-quick-toggle/multi-auto.svg" alt="Translation status" />
</a>
### 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)

View file

@ -1,66 +0,0 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
}
android {
compileSdk 34
defaultConfig {
applicationId "ru.karasevm.privatednstoggle"
versionCode 15
versionName "1.8.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
targetSdkVersion 34
minSdkVersion 28
}
buildFeatures {
viewBinding true
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
applicationIdSuffix ".dev"
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
namespace 'ru.karasevm.privatednstoggle'
}
dependencies {
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.google.guava:guava:33.1.0-android'
implementation 'com.google.code.gson:gson:2.11.0'
def shizuku_version = '13.1.5'
implementation "dev.rikka.shizuku:api:$shizuku_version"
implementation "dev.rikka.shizuku:provider:$shizuku_version"
implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3'
compileOnly 'dev.rikka.hidden:stub:4.3.2'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
}

88
app/build.gradle.kts Normal file
View file

@ -0,0 +1,88 @@
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")
}

View file

@ -19,7 +19,7 @@
android:exported="true"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
<activity
android:name=".MainActivity"
android:name=".ui.MainActivity"
android:theme="@style/Theme.MyApplication"
android:taskAffinity="${applicationId}.main"
android:launchMode="singleInstance"
@ -31,7 +31,7 @@
</activity>
<activity
android:name=".SettingsDialogActivity"
android:name=".ui.SettingsDialogActivity"
android:theme="@style/Theme.Transparent"
android:excludeFromRecents="true"
android:exported="true">
@ -41,7 +41,7 @@
</activity>
<service
android:name=".DnsTileService"
android:name=".service.DnsTileService"
android:exported="true"
android:icon="@drawable/ic_unknown_black_24dp"
android:label="@string/tile_name"

View file

@ -1,117 +0,0 @@
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<String>()
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"
}
}

View file

@ -1,299 +0,0 @@
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]
}
}

View file

@ -1,11 +1,33 @@
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()
)
}
}
}

View file

@ -1,84 +0,0 @@
package ru.karasevm.privatednstoggle
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import java.util.Collections
class RecyclerAdapter(private val items: MutableList<String>, private val showDragHandle: Boolean, private val onDataChanged: () -> Unit) :
RecyclerView.Adapter<RecyclerAdapter.ViewHolder>() {
var onItemClick: ((Int) -> Unit)? = null
var onItemsChanged: ((MutableList<String>) -> Unit)? = null
var onDragStart: ((RecyclerAdapter.ViewHolder) -> Unit)? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerAdapter.ViewHolder {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.recyclerview_row, parent, false)
val vh = ViewHolder(view)
return vh
}
@SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: RecyclerAdapter.ViewHolder, position: Int) {
val item = items[position]
val parts = item.split(" : ")
if (parts.size == 2) {
holder.labelTextView.visibility = View.VISIBLE
holder.labelTextView.text = parts[0]
holder.textView.text = parts[1]
} else {
holder.labelTextView.visibility = View.GONE
holder.textView.text = parts[0]
}
if (showDragHandle) {
holder.dragHandle.visibility = View.VISIBLE
holder.dragHandle.setOnTouchListener { _, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
onDragStart?.invoke(holder)
}
return@setOnTouchListener true
}
}
}
override fun getItemCount(): Int {
return items.size
}
fun onItemMove(fromPosition: Int, toPosition: Int) {
// Swap items in your data list
Collections.swap(items, fromPosition, toPosition)
notifyItemMoved(fromPosition, toPosition)
onItemsChanged?.invoke(items)
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textView: TextView = itemView.findViewById(R.id.textView)
val labelTextView: TextView = itemView.findViewById(R.id.labelTextView)
val dragHandle: ImageView = itemView.findViewById(R.id.dragHandle)
init {
itemView.setOnClickListener {
onItemClick?.invoke(adapterPosition)
}
}
}
fun setData(newItems: MutableList<String>) {
items.run {
clear()
addAll(newItems)
}
onDataChanged()
}
}

View file

@ -0,0 +1,84 @@
package ru.karasevm.privatednstoggle.data
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Transaction
import kotlinx.coroutines.flow.Flow
import ru.karasevm.privatednstoggle.model.DnsServer
@Dao
interface DnsServerDao {
@Query("SELECT * FROM dns_servers ORDER BY sortOrder ASC")
fun getAll(): Flow<List<DnsServer>>
@Query("SELECT * FROM dns_servers WHERE enabled = 1 ORDER BY sortOrder ASC LIMIT 1")
suspend fun getFirstEnabled(): DnsServer
@Query("SELECT * FROM dns_servers WHERE server = :server LIMIT 1")
suspend fun getFirstByServer(server: String): DnsServer?
@Query("SELECT * FROM dns_servers WHERE id = :id")
suspend fun getById(id: Int): DnsServer?
@Query("SELECT * FROM dns_servers " +
"WHERE sortOrder > (SELECT sortOrder FROM dns_servers WHERE server = :server) AND enabled = 1 " +
"ORDER BY sortOrder ASC " +
"LIMIT 1")
suspend fun getNextEnabledByServer(server: String): DnsServer?
@Query("DELETE FROM dns_servers")
suspend fun deleteAll()
@Query("DELETE FROM dns_servers WHERE id = :id")
suspend fun deleteById(id: Int)
@Query("UPDATE dns_servers SET sortOrder = sortOrder + 1 " +
"WHERE sortOrder >= :startSortOrder AND sortOrder <= :endSortOrder")
suspend fun incrementSortOrder(startSortOrder: Int, endSortOrder: Int = Int.MAX_VALUE)
@Query("UPDATE dns_servers SET sortOrder = sortOrder - 1 " +
"WHERE sortOrder >= :startSortOrder AND sortOrder <= :endSortOrder")
suspend fun decrementSortOrder(startSortOrder: Int, endSortOrder: Int = Int.MAX_VALUE)
@Query("UPDATE dns_servers SET sortOrder = sortOrder - 1 " +
"WHERE sortOrder > (SELECT sortOrder FROM dns_servers WHERE id = :id)")
suspend fun decrementSortOrderById(id: Int)
@Transaction
suspend fun deleteAndDecrement(id: Int) {
decrementSortOrderById(id)
deleteById(id)
}
@Query("UPDATE dns_servers SET label = :label WHERE id = :id")
suspend fun updateLabel(id: Int, label: String)
@Query("UPDATE dns_servers SET server = :server WHERE id = :id")
suspend fun updateServer(id: Int, server: String)
@Query("UPDATE dns_servers " +
"SET server = COALESCE(:server, server), " +
" label = COALESCE(:label, label), " +
" sortOrder = COALESCE(:sortOrder, sortOrder), " +
" enabled = COALESCE(:enabled, enabled) " +
"WHERE id = :id")
suspend fun update(id: Int, server: String?, label: String?, sortOrder: Int?, enabled: Boolean?)
@Transaction
suspend fun moveUp(sortOrder: Int, newSortOrder: Int, id: Int){
incrementSortOrder(newSortOrder, sortOrder)
update(id, null, null, newSortOrder, null)
}
@Transaction
suspend fun moveDown(sortOrder: Int, newSortOrder: Int, id: Int){
decrementSortOrder(sortOrder, newSortOrder)
update(id, null, null, newSortOrder, null)
}
@Query("INSERT INTO dns_servers(server, label, sortOrder, enabled) " +
"VALUES(:server, :label, COALESCE((SELECT MAX(sortOrder) + 1 FROM dns_servers), 0), :enabled)")
suspend fun insert(server: String, label: String, enabled: Boolean)
}

View file

@ -0,0 +1,60 @@
package ru.karasevm.privatednstoggle.data
import androidx.annotation.WorkerThread
import kotlinx.coroutines.flow.Flow
import ru.karasevm.privatednstoggle.model.DnsServer
class DnsServerRepository(private val dnsServerDao: DnsServerDao) {
val allServers: Flow<List<DnsServer>> = dnsServerDao.getAll()
@WorkerThread
fun getAll() = dnsServerDao.getAll()
@WorkerThread
suspend fun getFirstEnabled() = dnsServerDao.getFirstEnabled()
@WorkerThread
suspend fun getById(id: Int) = dnsServerDao.getById(id)
@WorkerThread
suspend fun getFirstByServer(server: String) = dnsServerDao.getFirstByServer(server)
@WorkerThread
suspend fun getNextByServer(server: String) = dnsServerDao.getNextEnabledByServer(server)
@WorkerThread
suspend fun insert(dnsServer: DnsServer) {
dnsServerDao.insert(dnsServer.server, dnsServer.label, dnsServer.enabled)
}
@WorkerThread
suspend fun update(
id: Int,
server: String?,
label: String?,
sortOrder: Int?,
enabled: Boolean?
) {
dnsServerDao.update(id, server, label, sortOrder, enabled)
}
@WorkerThread
suspend fun move(sortOrder: Int, newSortOrder: Int, id: Int) {
if (sortOrder == newSortOrder) {
return
}
if (newSortOrder > sortOrder) {
dnsServerDao.moveDown(sortOrder, newSortOrder, id)
} else {
dnsServerDao.moveUp(sortOrder, newSortOrder, id)
}
}
@WorkerThread
suspend fun delete(id: Int) {
dnsServerDao.deleteAndDecrement(id)
}
}

View file

@ -0,0 +1,48 @@
package ru.karasevm.privatednstoggle.data
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import ru.karasevm.privatednstoggle.model.DnsServer
class DnsServerViewModel(private val dnsServerRepository: DnsServerRepository) : ViewModel() {
val allServers: LiveData<List<DnsServer>> = dnsServerRepository.allServers.asLiveData()
fun getAll() = dnsServerRepository.getAll()
suspend fun getById(id: Int) = dnsServerRepository.getById(id)
fun insert(dnsServer: DnsServer) =
viewModelScope.launch {
dnsServerRepository.insert(dnsServer)
}
fun update(
id: Int,
server: String? = null,
label: String? = null,
sortOrder: Int? = null,
enabled: Boolean? = null
) = viewModelScope.launch { dnsServerRepository.update(id, server, label, sortOrder, enabled) }
fun move(sortOrder: Int, newSortOrder: Int, id: Int) =
viewModelScope.launch { dnsServerRepository.move(sortOrder, newSortOrder, id) }
fun delete(id: Int) = viewModelScope.launch { dnsServerRepository.delete(id) }
}
class DnsServerViewModelFactory(private val dnsServerRepository: DnsServerRepository) :
ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(DnsServerViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return DnsServerViewModel(dnsServerRepository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}

View file

@ -0,0 +1,34 @@
package ru.karasevm.privatednstoggle.data.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import ru.karasevm.privatednstoggle.data.DnsServerDao
import ru.karasevm.privatednstoggle.model.DnsServer
@Database(entities = [DnsServer::class], version = 1, exportSchema = false)
abstract class DnsServerRoomDatabase : RoomDatabase() {
abstract fun dnsServerDao(): DnsServerDao
companion object {
@Volatile
private var INSTANCE: DnsServerRoomDatabase? = null
fun getDatabase(context: Context): DnsServerRoomDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
DnsServerRoomDatabase::class.java,
"dns_server_database"
).build()
INSTANCE = instance
return instance
}
}
}
}

View file

@ -0,0 +1,24 @@
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
)

View file

@ -0,0 +1,302 @@
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"
}
}

View file

@ -1,4 +1,4 @@
package ru.karasevm.privatednstoggle
package ru.karasevm.privatednstoggle.ui
import android.app.Dialog
import android.content.Context
@ -11,10 +11,14 @@ import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.common.net.InternetDomainName
import ru.karasevm.privatednstoggle.R
import ru.karasevm.privatednstoggle.databinding.DialogAddBinding
import ru.karasevm.privatednstoggle.model.DnsServer
class AddServerDialogFragment(private val position: Int?, private val label: String?, private val server: String?) : DialogFragment() {
class AddServerDialogFragment(
private val dnsServer: DnsServer?
) : DialogFragment() {
// Use this instance of the interface to deliver action events
private lateinit var listener: NoticeDialogListener
@ -28,9 +32,9 @@ class AddServerDialogFragment(private val position: Int?, private val label: Str
* implement this interface in order to receive event callbacks.
* Each method passes the DialogFragment in case the host needs to query it. */
interface NoticeDialogListener {
fun onDialogPositiveClick(label: String? ,server: String)
fun onDialogPositiveClick(label: String?, server: String, position: Int)
fun onDeleteItemClicked(position: Int)
fun onAddDialogPositiveClick(label: String?, server: String)
fun onUpdateDialogPositiveClick(id: Int, server: String, label: String?, enabled: Boolean)
fun onDeleteItemClicked(id: Int)
}
// Override the Fragment.onAttach() method to instantiate the NoticeDialogListener
@ -61,17 +65,21 @@ class AddServerDialogFragment(private val position: Int?, private val label: Str
val view = binding.root
// Inflate and set the layout for the dialog
// Pass null as the parent view because its going in the dialog layout
if (position != null) {
binding.editTextServerHint.setText(label)
binding.editTextServerAddr.setText(server)
if (dnsServer != null) {
binding.editTextServerHint.setText(dnsServer.label)
binding.editTextServerAddr.setText(dnsServer.server)
binding.serverEnabledSwitch.visibility = android.view.View.VISIBLE
binding.serverEnabledSwitch.isChecked = dnsServer.enabled
builder.setTitle(R.string.edit_server).setView(view)
.setPositiveButton(
R.string.menu_save
) { _, _ ->
listener.onDialogPositiveClick(
binding.editTextServerHint.text.toString().trim(),
binding.editTextServerAddr.text.toString().trim(),
position)
) { _, _ ->
listener.onUpdateDialogPositiveClick(
dnsServer.id,
binding.editTextServerAddr.text.toString().trim(),
binding.editTextServerHint.text.toString().trim(),
binding.serverEnabledSwitch.isChecked
)
}
.setNegativeButton(
R.string.cancel
@ -81,17 +89,16 @@ class AddServerDialogFragment(private val position: Int?, private val label: Str
.setNeutralButton(
R.string.delete
) { _, _ ->
listener.onDeleteItemClicked(position)
}
}
else {
listener.onDeleteItemClicked(dnsServer.id)
}
} else {
builder.setTitle(R.string.add_server)
.setView(view)
// Add action buttons
.setPositiveButton(
R.string.menu_add
) { _, _ ->
listener.onDialogPositiveClick(
listener.onAddDialogPositiveClick(
binding.editTextServerHint.text.toString().trim(),
binding.editTextServerAddr.text.toString().trim()
)

View file

@ -0,0 +1,140 @@
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<DnsServer> = 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
}
}

View file

@ -1,13 +1,14 @@
package ru.karasevm.privatednstoggle
package ru.karasevm.privatednstoggle.ui
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import ru.karasevm.privatednstoggle.R
class DeleteServerDialogFragment(private val position: Int) : DialogFragment() {
class DeleteServerDialogFragment(private val id: Int) : DialogFragment() {
// Use this instance of the interface to deliver action events
private lateinit var listener: NoticeDialogListener
@ -15,7 +16,7 @@ class DeleteServerDialogFragment(private val position: Int) : DialogFragment() {
* implement this interface in order to receive event callbacks.
* Each method passes the DialogFragment in case the host needs to query it. */
interface NoticeDialogListener {
fun onDialogPositiveClick(position: Int)
fun onDeleteDialogPositiveClick(id: Int)
}
// Override the Fragment.onAttach() method to instantiate the NoticeDialogListener
@ -43,7 +44,7 @@ class DeleteServerDialogFragment(private val position: Int) : DialogFragment() {
.setPositiveButton(
R.string.delete
) { _, _ ->
listener.onDialogPositiveClick(position)
listener.onDeleteDialogPositiveClick(id)
}
.setNegativeButton(
R.string.cancel

View file

@ -1,4 +1,4 @@
package ru.karasevm.privatednstoggle
package ru.karasevm.privatednstoggle.ui
import android.Manifest
import android.content.ClipData
@ -6,41 +6,40 @@ import android.content.ClipDescription.MIMETYPE_TEXT_PLAIN
import android.content.ClipboardManager
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.IPackageManager
import android.content.pm.PackageManager
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.permission.IPermissionManager
import android.util.Log
import android.view.Menu
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.app.ShareCompat
import androidx.lifecycle.viewModelScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.DOWN
import androidx.recyclerview.widget.ItemTouchHelper.UP
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonSyntaxException
import com.google.gson.ToNumberPolicy
import com.google.gson.reflect.TypeToken
import org.lsposed.hiddenapibypass.HiddenApiBypass
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import rikka.shizuku.Shizuku
import rikka.shizuku.ShizukuBinderWrapper
import rikka.shizuku.ShizukuProvider
import rikka.shizuku.SystemServiceHelper
import ru.karasevm.privatednstoggle.PrivateDNSApp
import ru.karasevm.privatednstoggle.R
import ru.karasevm.privatednstoggle.data.DnsServerViewModel
import ru.karasevm.privatednstoggle.data.DnsServerViewModelFactory
import ru.karasevm.privatednstoggle.databinding.ActivityMainBinding
import ru.karasevm.privatednstoggle.utils.PreferenceHelper
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.dns_servers
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.export
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.import
import ru.karasevm.privatednstoggle.model.DnsServer
import ru.karasevm.privatednstoggle.util.BackupUtils
import ru.karasevm.privatednstoggle.util.PreferenceHelper
import ru.karasevm.privatednstoggle.util.PreferenceHelper.dns_servers
import ru.karasevm.privatednstoggle.util.ShizukuUtil.grantPermissionWithShizuku
class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogListener,
@ -48,79 +47,117 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
private lateinit var linearLayoutManager: LinearLayoutManager
private lateinit var binding: ActivityMainBinding
private var items = mutableListOf<String>()
private lateinit var sharedPrefs: SharedPreferences
private lateinit var adapter: RecyclerAdapter
private lateinit var adapter: ServerListRecyclerAdapter
private lateinit var clipboard: ClipboardManager
private lateinit var gson: Gson
private val dnsServerViewModel: DnsServerViewModel by viewModels { DnsServerViewModelFactory((application as PrivateDNSApp).repository) }
private val itemTouchHelper by lazy {
val simpleItemTouchCallback = object : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) {
val simpleItemTouchCallback =
object : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) {
var dragFrom = -1
var dragTo = -1
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
adapter.onItemMove(viewHolder.adapterPosition, target.adapterPosition)
return true
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
if (dragFrom == viewHolder.bindingAdapterPosition && dragTo == target.bindingAdapterPosition) {
return true
}
// store the drag position
if (dragFrom == -1) dragFrom = viewHolder.bindingAdapterPosition
dragTo = target.bindingAdapterPosition
adapter.onItemMove(
viewHolder.bindingAdapterPosition,
target.bindingAdapterPosition
)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
override fun onSelectedChanged(
viewHolder: RecyclerView.ViewHolder?, actionState: Int
) {
super.onSelectedChanged(viewHolder, actionState)
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
viewHolder?.itemView?.apply {
// Example: Elevate the view
elevation = 8f
alpha = 0.5f
setBackgroundColor(Color.GRAY)
override fun onSelectedChanged(
viewHolder: RecyclerView.ViewHolder?, actionState: Int
) {
super.onSelectedChanged(viewHolder, actionState)
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
viewHolder?.itemView?.apply {
// Example: Elevate the view
elevation = 8f
alpha = 0.5f
setBackgroundColor(Color.GRAY)
}
}
}
}
override fun clearView(
recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder
) {
super.clearView(recyclerView, viewHolder)
viewHolder.itemView.apply {
// Reset the appearance
elevation = 0f
alpha = 1.0f
setBackgroundColor(Color.TRANSPARENT)
override fun clearView(
recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder
) {
super.clearView(recyclerView, viewHolder)
viewHolder.itemView.apply {
// Reset the appearance
elevation = 0f
alpha = 1.0f
setBackgroundColor(Color.TRANSPARENT)
}
// commit the change to the db
dnsServerViewModel.move(
dragFrom,
dragTo,
(viewHolder as ServerListRecyclerAdapter.DnsServerViewHolder).id
)
dragTo = -1
dragFrom = -1
}
}
}
ItemTouchHelper(simpleItemTouchCallback)
}
private fun importSettings(json: String) {
runCatching {
val objectType = object : TypeToken<Map<String, Any>>() {}.type
val data: Map<String, Any> = gson.fromJson(json, objectType)
sharedPrefs.import(data)
val data: BackupUtils.Backup = Json.decodeFromString<BackupUtils.Backup>(json)
BackupUtils.import(data, dnsServerViewModel, sharedPrefs)
}.onSuccess {
Toast.makeText(
this, getString(R.string.import_success), Toast.LENGTH_SHORT
).show()
ActivityCompat.recreate(this)
}.onFailure { exception ->
Log.e("IMPORT", "Import failed", exception)
when (exception) {
is JsonSyntaxException -> {
Toast.makeText(
this, getString(R.string.import_failure_json), Toast.LENGTH_SHORT
).show()
}
runCatching {
Log.e("IMPORT", "Malformed json, falling back to legacy", exception)
val data = Json.decodeFromString<BackupUtils.LegacyBackup>(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()
}
}
}
else -> {
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<String>().toMutableList()
}
}
}
@ -139,40 +176,37 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
sharedPrefs = PreferenceHelper.defaultPreference(this)
clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
gson = GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create()
items = sharedPrefs.dns_servers
if (items[0] == "") {
items.removeAt(0)
}
migrateServerList()
updateEmptyView()
adapter = RecyclerAdapter(items, true) { updateEmptyView() }
adapter.onItemClick = { position ->
val data = items[position].split(" : ")
val label: String?
val server: String
if (data.size == 2) {
label = data[0]
server = data[1]
adapter = ServerListRecyclerAdapter(true)
binding.recyclerView.adapter = adapter
dnsServerViewModel.allServers.observe(this) { servers ->
adapter.submitList(servers)
if (servers.isEmpty()) {
binding.emptyView.visibility = View.VISIBLE
binding.emptyViewHint.visibility = View.VISIBLE
} else {
binding.emptyView.visibility = View.GONE
binding.emptyViewHint.visibility = View.GONE
}
else {
label = null
server = data[0]
}
val newFragment = AddServerDialogFragment(position, label, server)
newFragment.show(supportFragmentManager, "edit_server")
}
adapter.onItemsChanged = { swappedItems ->
items = swappedItems
sharedPrefs.dns_servers = swappedItems
updateEmptyView()
adapter.onItemClick = { id ->
dnsServerViewModel.viewModelScope.launch {
val server = dnsServerViewModel.getById(id)
if (server != null) {
val newFragment =
AddServerDialogFragment(server)
newFragment.show(supportFragmentManager, "edit_server")
}
}
}
adapter.onDragStart = { viewHolder ->
itemTouchHelper.startDrag(viewHolder)
}
binding.floatingActionButton.setOnClickListener {
val newFragment = AddServerDialogFragment(null, null, null)
val newFragment = AddServerDialogFragment(null)
newFragment.show(supportFragmentManager, "add_server")
}
binding.recyclerView.adapter = adapter
@ -190,31 +224,40 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
}
R.id.export_settings_clipboard -> {
val data = sharedPrefs.export()
val jsonData = gson.toJson(data)
clipboard.setPrimaryClip(ClipData.newPlainText("", jsonData))
// Only show a toast for Android 12 and lower.
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) Toast.makeText(
this, getString(R.string.copy_success), Toast.LENGTH_SHORT
).show()
dnsServerViewModel.viewModelScope.launch {
val data = BackupUtils.export(dnsServerViewModel, sharedPrefs)
val jsonData = 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 data = sharedPrefs.export()
val jsonData = gson.toJson(data)
ShareCompat.IntentBuilder(this).setText(jsonData).setType("text/plain")
.startChooser()
val activityContext = this
dnsServerViewModel.viewModelScope.launch {
val data = BackupUtils.export(dnsServerViewModel, sharedPrefs)
val jsonData = Json.encodeToString(data)
ShareCompat.IntentBuilder(activityContext).setText(jsonData)
.setType("text/plain")
.startChooser()
}
true
}
R.id.export_settings_file -> {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "text/plain"
putExtra(Intent.EXTRA_TITLE, "private-dns-export")
dnsServerViewModel.viewModelScope.launch {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "text/plain"
putExtra(Intent.EXTRA_TITLE, "private-dns-export")
}
saveResultLauncher.launch(intent)
}
saveResultLauncher.launch(intent)
true
}
@ -248,22 +291,12 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
}
}
private fun updateEmptyView() {
if (items.isEmpty()) {
binding.emptyView.visibility = View.VISIBLE
binding.emptyViewHint.visibility = View.VISIBLE
} else {
binding.emptyView.visibility = View.GONE
binding.emptyViewHint.visibility = View.GONE
}
}
private var saveResultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
val data: Intent? = result.data
data?.data?.also { uri ->
val jsonData = gson.toJson(sharedPrefs.export())
val jsonData = Json.encodeToString(BackupUtils.export(dnsServerViewModel, sharedPrefs))
val contentResolver = applicationContext.contentResolver
runCatching {
contentResolver.openOutputStream(uri)?.use { outputStream ->
@ -322,7 +355,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
Shizuku.requestPermission(1)
}
} else {
grantPermissionWithShizuku()
grantPermission()
}
} else {
if (checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) {
@ -330,6 +363,9 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
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()
}
@ -359,100 +395,81 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
Shizuku.removeRequestPermissionResultListener(this::onRequestPermissionResult)
}
override fun onDeleteItemClicked(position: Int) {
val newFragment = DeleteServerDialogFragment(position)
/**
* Show the dialog for deleting the server
* @param id The server id
*/
override fun onDeleteItemClicked(id: Int) {
val newFragment = DeleteServerDialogFragment(id)
newFragment.show(supportFragmentManager, "delete_server")
}
override fun onDialogPositiveClick(label: String?, server: String) {
/**
* Callback for adding the server
* @param label The label
* @param server The server
*/
override fun onAddDialogPositiveClick(label: String?, server: String) {
if (server.isEmpty()) {
Toast.makeText(this, R.string.server_length_error, Toast.LENGTH_SHORT).show()
return
}
if (label.isNullOrEmpty()) {
items.add(server)
} 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
}
override fun onDialogPositiveClick(label: String?, server: String, position: Int) {
if (server.isEmpty()) {
Toast.makeText(this, R.string.server_length_error, Toast.LENGTH_SHORT).show()
return
}
if (label.isNullOrEmpty()) {
items[position] = server
dnsServerViewModel.insert(DnsServer(0, server))
} else {
items[position] = "$label : $server"
dnsServerViewModel.insert(DnsServer(0, server, label))
}
adapter.notifyItemChanged(position)
sharedPrefs.dns_servers = items
binding.recyclerView.adapter?.notifyItemChanged(position)
}
/**
* Attempts to grant WRITE_SECURE_SETTINGS permission with Shizuku
* Callback for deleting the server
* @param id The server id
*/
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()
}
}
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) {
grantPermissionWithShizuku()
} else if (checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != 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/")
)

View file

@ -1,18 +1,20 @@
package ru.karasevm.privatednstoggle
package ru.karasevm.privatednstoggle.ui
import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import ru.karasevm.privatednstoggle.R
import ru.karasevm.privatednstoggle.databinding.DialogOptionsBinding
import ru.karasevm.privatednstoggle.utils.PreferenceHelper
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.autoMode
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.requireUnlock
import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils
import ru.karasevm.privatednstoggle.util.PreferenceHelper
import ru.karasevm.privatednstoggle.util.PreferenceHelper.autoMode
import ru.karasevm.privatednstoggle.util.PreferenceHelper.requireUnlock
import ru.karasevm.privatednstoggle.util.PrivateDNSUtils
class OptionsDialogFragment : DialogFragment() {
private var _binding: DialogOptionsBinding? = null
private val binding get() = _binding!!
private val sharedPreferences by lazy { PreferenceHelper.defaultPreference(requireContext()) }
override fun onCreateDialog(
savedInstanceState: Bundle?
@ -27,13 +29,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 sharedPrefs = PreferenceHelper.defaultPreference(requireContext())
val autoModeOption = sharedPrefs.autoMode
val autoModeOption = sharedPreferences.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)
@ -42,20 +44,20 @@ class OptionsDialogFragment : DialogFragment() {
}
binding.autoOptionRadioGroup.setOnCheckedChangeListener { _, checkedId ->
when (checkedId) {
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 =
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 =
PrivateDNSUtils.AUTO_MODE_OPTION_OFF_AUTO
R.id.autoOptionPrivate -> sharedPrefs.autoMode =
R.id.autoOptionPrivate -> sharedPreferences.autoMode =
PrivateDNSUtils.AUTO_MODE_OPTION_PRIVATE
}
}
val requireUnlock = sharedPrefs.requireUnlock
val requireUnlock = sharedPreferences.requireUnlock
binding.requireUnlockSwitch.isChecked = requireUnlock
binding.requireUnlockSwitch.setOnCheckedChangeListener { _, isChecked ->
sharedPrefs.requireUnlock = isChecked
sharedPreferences.requireUnlock = isChecked
}
}
}

View file

@ -0,0 +1,124 @@
package ru.karasevm.privatednstoggle.ui
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import ru.karasevm.privatednstoggle.R
import ru.karasevm.privatednstoggle.model.DnsServer
class ServerListRecyclerAdapter(private val showDragHandle: Boolean) :
RecyclerView.Adapter<ServerListRecyclerAdapter.DnsServerViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DnsServerViewHolder {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.recyclerview_row, parent, false)
val vh = DnsServerViewHolder(view)
return vh
}
override fun getItemCount(): Int {
return items.size
}
var onItemClick: ((Int) -> Unit)? = null
var onDragStart: ((DnsServerViewHolder) -> Unit)? = null
private var items: MutableList<DnsServer> = mutableListOf()
@SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: DnsServerViewHolder, position: Int) {
val item = items[position]
if (item.label.isNotEmpty()) {
holder.labelTextView.text = item.label
holder.labelTextView.visibility = View.VISIBLE
} else {
holder.labelTextView.visibility = View.GONE
}
holder.serverTextView.text = item.server
holder.id = item.id
if (item.enabled) {
holder.labelTextView.alpha = 1f
holder.serverTextView.alpha = 1f
} else {
holder.labelTextView.alpha = 0.5f
holder.serverTextView.alpha = 0.5f
}
if (showDragHandle) {
holder.dragHandle.visibility = View.VISIBLE
holder.dragHandle.setOnTouchListener { _, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
onDragStart?.invoke(holder)
}
return@setOnTouchListener true
}
}
}
/**
* Update server position in memory
* @param fromPosition old position
* @param toPosition new position
*/
fun onItemMove(fromPosition: Int, toPosition: Int) {
items.add(toPosition, items.removeAt(fromPosition))
notifyItemMoved(fromPosition, toPosition)
}
class DiffCallback(
private val oldList: List<DnsServer>, private var newList: List<DnsServer>
) : DiffUtil.Callback() {
override fun getOldListSize(): Int {
return oldList.size
}
override fun getNewListSize(): Int {
return newList.size
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldList[oldItemPosition]
val newItem = newList[newItemPosition]
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldList[oldItemPosition]
val newItem = newList[newItemPosition]
return oldItem.server == newItem.server && oldItem.label == newItem.label && oldItem.enabled == newItem.enabled
}
}
/**
* Submit list to adapter
* @param list list to submit
*/
fun submitList(list: List<DnsServer>) {
val diffCallback = DiffCallback(items, list)
val diffResult = DiffUtil.calculateDiff(diffCallback)
items.clear()
items.addAll(list)
diffResult.dispatchUpdatesTo(this)
}
inner class DnsServerViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val labelTextView: TextView = view.findViewById(R.id.labelTextView)
val serverTextView: TextView = view.findViewById(R.id.textView)
val dragHandle: ImageView = itemView.findViewById(R.id.dragHandle)
var id = 0
init {
view.setOnClickListener {
onItemClick?.invoke(id)
}
}
}
}

View file

@ -1,4 +1,4 @@
package ru.karasevm.privatednstoggle
package ru.karasevm.privatednstoggle.ui
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

View file

@ -0,0 +1,79 @@
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<DnsServer>,
@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
}
}

View file

@ -1,13 +1,11 @@
package ru.karasevm.privatednstoggle.utils
package ru.karasevm.privatednstoggle.util
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 {
@ -23,32 +21,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 {
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
return checkSelfPermission(
context,
Manifest.permission.WRITE_SECURE_SETTINGS
) == PackageManager.PERMISSION_GRANTED
}
}

View file

@ -1,13 +1,13 @@
package ru.karasevm.privatednstoggle.utils
package ru.karasevm.privatednstoggle.util
import android.content.Context
import android.content.SharedPreferences
object PreferenceHelper {
private const val DNS_SERVERS = "dns_servers"
private const val AUTO_MODE = "auto_mode"
private const val REQUIRE_UNLOCK = "require_unlock"
const val DNS_SERVERS = "dns_servers"
const val AUTO_MODE = "auto_mode"
const val REQUIRE_UNLOCK = "require_unlock"
fun defaultPreference(context: Context): SharedPreferences =
context.getSharedPreferences("app_prefs", 0)
@ -38,6 +38,7 @@ object PreferenceHelper {
}
}
var SharedPreferences.autoMode
get() = getInt(AUTO_MODE, PrivateDNSUtils.AUTO_MODE_OPTION_OFF)
set(value) {
@ -53,24 +54,4 @@ object PreferenceHelper {
it.put(REQUIRE_UNLOCK to value)
}
}
// export all the preferences
fun SharedPreferences.export() = mapOf(
DNS_SERVERS to getString(DNS_SERVERS, ""),
AUTO_MODE to autoMode,
REQUIRE_UNLOCK to requireUnlock
)
// import all the preferences
fun SharedPreferences.import(map: Map<String, Any>) {
editMe {
map.forEach { (key, value) ->
if (value is Number) {
it.put(key to value.toInt())
return@forEach
}
it.put(key to value)
}
}
}
}

View file

@ -0,0 +1,82 @@
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)
}
}

View file

@ -1,7 +0,0 @@
package ru.karasevm.privatednstoggle.utils
data class DnsServer(val label: String, val server: String) {
override fun toString(): String {
return "$label : $server"
}
}

View file

@ -4,7 +4,8 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
tools:context=".ui.MainActivity"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/topAppBarLayout"
@ -48,7 +49,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/empty_hint"
android:textColor="@color/material_dynamic_neutral50"
android:textColor="@color/material_on_surface_emphasis_medium"
android:visibility="visible"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"

View file

@ -4,6 +4,20 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/serverEnabledSwitch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:maxWidth="320dp"
android:text="@string/add_server_enabled"
android:textSize="16sp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/editTextServerHintLayout"
android:layout_width="match_parent"
@ -12,7 +26,7 @@
android:hint="@string/add_edittext_label_hint"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toBottomOf="@id/serverEnabledSwitch">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextServerHint"

View file

@ -54,10 +54,11 @@
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/requireUnlockSwitch"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:maxWidth="320dp"
android:text="@string/require_unlock_setting"
android:textSize="16sp" />

View file

@ -0,0 +1 @@
unqualifiedResLocale=en-US

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="done">Terminé</string>
<string name="app_name">Private DNS Quick Toggle</string>
<string name="dns_unknown">Inconnu</string>
<string name="dns_off">Éteint</string>
<string name="add_server">Ajouter un serveur</string>
<string name="menu_add">Ajouter</string>
<string name="menu_privacy_policy">Politique de confidentialité</string>
<string name="delete_message">Êtes-vous sûr de vouloir supprimer le serveur?</string>
<string name="delete">Supprimer</string>
<string name="server_length_error">L\'adresse du serveur ne peut pas être vide</string>
<string name="add_edittext_label_hint">Étiquette du serveur DNS (Facultatif)</string>
<string name="add_edittext_hint">Adresse du serveur DNS</string>
<string name="options">Options</string>
<string name="ok">OK</string>
<string name="auto_option_only_off">Seulement éteint</string>
<string name="auto_option_only_auto">Seulement automatique</string>
<string name="auto_option_off_and_auto">Éteint et automatique</string>
<string name="dns_auto">Automatique</string>
<string name="tile_name">Commutateur de DNS privé</string>
<string name="menu_save">Enregistrer</string>
<string name="permission_missing">Autorisation non accordée, vérifiez les instructions dans l\'application</string>
<string name="select_server">Sélectionner le serveur</string>
<string name="auto_option_description">Définissez les options à inclure</string>
<string name="auto_option_only_private">DNS privé seulement</string>
<string name="open_app">Ouvrir l\'application</string>
<string name="set_to_off_toast">DNS privé éteint</string>
<string name="set_to_auto_toast">DNS privé réglé sur automatique</string>
<string name="set_to_provider_toast">DNS privé réglé sur %1$s</string>
<string name="require_unlock_setting">Requiert le déverrouillage de l\'appareil pour changer de serveur</string>
<string name="add_server_enabled">Activé</string>
<string name="menu_import_from_file">À partir du fichier</string>
<string name="no_servers_added">Aucun serveur ajouté</string>
<string name="copy_success">Copié</string>
<string name="edit_server">Éditer le serveur</string>
<string name="export_failure">Échec de la sauvegarde</string>
<string name="empty_hint">Appuyez sur le bouton ci-dessous pour en ajouter un</string>
<string name="shizuku_failure_toast">Impossible d\'obtenir l\'autorisation, veuillez l\'accorder manuellement</string>
<string name="shizuku_success_toast">Autorisation accordée, vous pouvez désormais révoquer l\'autorisation Shizuku</string>
<string name="menu_import_from_clipboard">À partir du presse-papier</string>
<string name="menu_export_to_clipboard">Vers le presse-papier</string>
<string name="menu_export_share">Partager</string>
<string name="menu_export_to_file">Vers le fichier</string>
<string name="export_success">Sauvegarde réussie</string>
<string name="import_failure">Échec de l\'importation</string>
<string name="import_failure_json">Échec de l\'importation, le fichier JSON est incorrect</string>
<string name="import_success">Importé</string>
<string name="menu_export">Exporter</string>
<string name="menu_import">Importer</string>
<string name="a11y_drag_handle">Poignée</string>
<string name="delete_question">Supprimer</string>
<string name="cancel">Annuler</string>
</resources>

View file

@ -0,0 +1,51 @@
<resources>
<string name="app_name">Privát DNS Gyorskapcsoló</string>
<string name="tile_name">Privát DNS Kapcsoló</string>
<string name="permission_missing">Nincs engedély megadva, nézd meg az alkalmazásban, hogyan adhatod meg</string>
<string name="dns_off">Ki</string>
<string name="dns_auto">Automatikus</string>
<string name="dns_unknown">Ismeretlen</string>
<string name="add_server">Szerver hozzáadása</string>
<string name="menu_add">Hozzáadás</string>
<string name="menu_save">Mentés</string>
<string name="menu_privacy_policy">Adatvédelmi irányelvek</string>
<string name="select_server">Szerver kiválasztása</string>
<string name="done">Kész</string>
<string name="cancel">Mégse</string>
<string name="delete_question">Törlés</string>
<string name="delete_message">Biztosan törölni szeretnéd a szervert?</string>
<string name="delete">Törlés</string>
<string name="server_length_error">A szervercím nem lehet üres</string>
<string name="add_edittext_label_hint">DNS szerver neve (opcionális)</string>
<string name="add_edittext_hint">DNS szerver címe</string>
<string name="options">Beállítások</string>
<string name="ok">OK</string>
<string name="auto_option_description">Válaszd ki, mely opciók jelenjenek meg a csempén</string>
<string name="auto_option_only_off">Csak ki</string>
<string name="auto_option_only_auto">Csak automatikus</string>
<string name="auto_option_off_and_auto">Ki és automatikus</string>
<string name="auto_option_only_private">Csak Privát DNS</string>
<string name="open_app">Alkalmazás megnyitása</string>
<string name="set_to_off_toast">Privát DNS kikapcsolva</string>
<string name="set_to_auto_toast">Privát DNS automatikus módra állítva</string>
<string name="set_to_provider_toast">Privát DNS beállítva: %1$s</string>
<string name="require_unlock_setting">Eszköz feloldása szükséges a szerver módosításához</string>
<string name="a11y_drag_handle">Húzási fogantyú</string>
<string name="menu_import">Importálás</string>
<string name="menu_export">Exportálás</string>
<string name="import_success">Importálva</string>
<string name="import_failure">Importálás sikertelen</string>
<string name="import_failure_json">Importálás sikertelen, hibás JSON</string>
<string name="copy_success">Másolva</string>
<string name="menu_import_from_file">Fájlból</string>
<string name="menu_import_from_clipboard">Vágólapról</string>
<string name="menu_export_to_clipboard">Vágólapra</string>
<string name="menu_export_share">Megosztás</string>
<string name="menu_export_to_file">Fájlba</string>
<string name="export_failure">Mentés sikertelen</string>
<string name="export_success">Sikeresen mentve</string>
<string name="edit_server">Szerver szerkesztése</string>
<string name="no_servers_added">Nincsenek szerverek hozzáadva</string>
<string name="empty_hint">Koppints az alábbi gombra, hogy hozzáadj egyet</string>
<string name="add_server_enabled">Engedélyezve</string>
</resources>

View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="tile_name">Хувийн DNS солих</string>
<string name="dns_off">Унтраах</string>
<string name="dns_unknown">Тодорхойгүй</string>
<string name="add_server">Сервер нэмэх</string>
<string name="menu_save">Хадгалах</string>
<string name="done">Болсон</string>
<string name="cancel">Болих</string>
<string name="delete_question">Устгах</string>
<string name="delete_message">Та серверийг устгахдаа итгэлтэй байна уу?</string>
<string name="delete">Устгах</string>
<string name="server_length_error">Серверийн хаяг хоосон байж болохгүй</string>
<string name="add_edittext_label_hint">DNS серверийн шошго (заавал биш)</string>
<string name="add_edittext_hint">DNS серверийн хаяг</string>
<string name="options">Сонголтууд</string>
<string name="ok">ОК</string>
<string name="auto_option_description">Хавтан дээр ямар сонголтыг оруулахаа сонгоно уу</string>
<string name="auto_option_only_off">Зөвхөн унтарсан</string>
<string name="auto_option_only_auto">Зөвхөн авто</string>
<string name="auto_option_off_and_auto">Унтарсан болон авто</string>
<string name="set_to_provider_toast">Хувийн DNS-г %1$s болгож тохируулсан</string>
<string name="require_unlock_setting">Серверийг өөрчлөхийн тулд төхөөрөмжийн түгжээг тайлах шаардлагатай</string>
<string name="a11y_drag_handle">Бариулыг чирэх</string>
<string name="menu_import">Импорт</string>
<string name="import_success">Импортолсон</string>
<string name="import_failure">Импорт хийж чадсангүй</string>
<string name="menu_import_from_file">Файлаас</string>
<string name="export_failure">Хадгалж чадсангүй</string>
<string name="export_success">Амжилттай хадгалсан</string>
<string name="edit_server">Сервер засах</string>
<string name="app_name">Хувийн DNS хурдан сэлгэх</string>
<string name="menu_privacy_policy">Нууцлалын бодлого</string>
<string name="permission_missing">Зөвшөөрөл олгоогүй. Үүнийг хэрхэн хийхийг харна уу</string>
<string name="dns_auto">Авто</string>
<string name="menu_add">Нэмэх</string>
<string name="select_server">Сервер сонгох</string>
<string name="auto_option_only_private">Зөвхөн хувийн DNS</string>
<string name="open_app">Апп нээх</string>
<string name="menu_export_to_clipboard">Түр санах ой руу</string>
<string name="set_to_off_toast">Хувийн DNS унтарсан</string>
<string name="set_to_auto_toast">Хувийн DNS-г автоматаар тохируулсан</string>
<string name="menu_export">Экспорт</string>
<string name="copy_success">Хуулагдсан</string>
<string name="menu_export_share">Хуваалцах</string>
<string name="import_failure_json">Импорт хийж чадсангүй, алдаатай JSON</string>
<string name="menu_import_from_clipboard">Түр санах ойноос</string>
<string name="menu_export_to_file">Файлруу</string>
</resources>

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="delete">Usuń</string>
<string name="menu_save">Zapisz</string>
<string name="menu_privacy_policy">Polityka prywatności</string>
<string name="select_server">Wybierz serwer</string>
<string name="cancel">Anuluj</string>
<string name="add_edittext_hint">Adres serwera DNS</string>
<string name="ok">OK</string>
<string name="open_app">Otwórz aplikację</string>
<string name="menu_import">Importuj</string>
<string name="menu_export">Eksportuj</string>
<string name="copy_success">Skopiowano</string>
<string name="menu_import_from_file">Z pliku</string>
<string name="menu_import_from_clipboard">Ze schowka</string>
<string name="menu_export_to_clipboard">Do schowka</string>
<string name="menu_export_share">Udostępnij</string>
<string name="menu_export_to_file">Do pliku</string>
<string name="edit_server">Edytuj serwer</string>
<string name="no_servers_added">Brak dodanych serwerów</string>
<string name="add_server_enabled">Włączone</string>
<string name="add_server">Dodaj serwer</string>
<string name="menu_add">Dodaj</string>
<string name="delete_question">Usuń</string>
<string name="dns_unknown">Nieznane</string>
<string name="done">Gotowe</string>
<string name="options">Opcje</string>
<string name="import_failure">Importowanie nie powiodło się</string>
<string name="dns_auto">Automatycznie</string>
<string name="dns_off">Wyłącz</string>
<string name="import_success">Zaimportowano</string>
<string name="server_length_error">Adres serwera nie może być pusty</string>
<string name="import_failure_json">Import nie powiódł się, zniekształcony plik JSON</string>
<string name="export_success">Zapisano pomyślnie</string>
<string name="delete_message">Czy na pewno chcesz usunąć serwer?</string>
<string name="app_name">Private DNS Quick Toggle</string>
<string name="tile_name">Przełącznik prywatnego DNS</string>
<string name="permission_missing">Nieprzydzielono uprawnienia, sprawdź w aplikacji, w jaki sposób można to zrobić</string>
<string name="add_edittext_label_hint">Opis serwera DNS (opcjonalnie)</string>
<string name="auto_option_description">Wybierz opcje, które będą dostępne w kafelku</string>
<string name="auto_option_only_off">Tylko wyłączenie</string>
<string name="auto_option_only_auto">Tylko automatycznie</string>
<string name="auto_option_off_and_auto">Wyłączenie i automatycznie</string>
<string name="auto_option_only_private">Tylko prywatny DNS</string>
<string name="set_to_auto_toast">Prywatny DNS zmieniony na automatyczny</string>
<string name="set_to_provider_toast">Prywatny DNS zmieniony na %1$s</string>
<string name="require_unlock_setting">Wymagaj odblokowania urządzenia do zmiany serwera</string>
<string name="set_to_off_toast">Wyłączono Prywatny DNS</string>
<string name="a11y_drag_handle">Przeciągnij</string>
<string name="export_failure">Zapisywanie nie powiodło się</string>
<string name="empty_hint">Kliknij na poniższy przycisk, aby dodać nowy</string>
<string name="shizuku_success_toast">Udzielono zezwolenia, możesz teraz cofnąć zezwolenie w Shizuku</string>
<string name="shizuku_failure_toast">Uzyskanie uprawnień nie powiodło się, udziel ich ręcznie</string>
</resources>

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="import_success">Importado</string>
<string name="permission_missing">Permissão não concedida, verifique o app para saber como prosseguir</string>
<string name="app_name">Alteração de DNS privado</string>
<string name="cancel">Cancelar</string>
<string name="add_server">Adicionar servidor</string>
<string name="add_edittext_hint">Endereço do servidor DNS</string>
<string name="auto_option_description">Escolha opções disponível em atalho</string>
<string name="tile_name">Alteração de DNS privado</string>
<string name="dns_off">Desativado</string>
<string name="dns_auto">Automático</string>
<string name="dns_unknown">Indeterminado</string>
<string name="menu_add">Adicionar</string>
<string name="menu_save">Salvar</string>
<string name="menu_privacy_policy">Política de privacidade</string>
<string name="done">Concluído</string>
<string name="delete_question">Apagar</string>
<string name="delete_message">Tem certeza de que quer apagar o servidor?</string>
<string name="delete">Apagar</string>
<string name="server_length_error">O endereço do servidor não pode estar em branco</string>
<string name="add_edittext_label_hint">Identificação do servidor DNS (opcional)</string>
<string name="options">Opções</string>
<string name="ok">Ok</string>
<string name="auto_option_only_off">Somente desativado</string>
<string name="auto_option_off_and_auto">Desativado e automático</string>
<string name="auto_option_only_private">Somente DNS privado</string>
<string name="open_app">Abrir app</string>
<string name="set_to_off_toast">DNS privado desativado</string>
<string name="set_to_auto_toast">DNS privado definido para automático</string>
<string name="set_to_provider_toast">DNS privado definido para %1$s</string>
<string name="a11y_drag_handle">Arrastre</string>
<string name="menu_import">Importar</string>
<string name="menu_export">Exportar</string>
<string name="import_failure_json">Falha na importação, JSON malformado</string>
<string name="copy_success">Copiado</string>
<string name="menu_import_from_clipboard">Da memória</string>
<string name="menu_export_share">Compartilhar</string>
<string name="menu_export_to_file">Para arquivo</string>
<string name="export_success">Salvo com sucesso</string>
<string name="edit_server">Editar servidor</string>
<string name="no_servers_added">Nenhum servidor adicionado</string>
<string name="empty_hint">Toque no botão abaixo para adicionar</string>
<string name="add_server_enabled">Ativado</string>
<string name="select_server">Escolha servidor</string>
<string name="menu_export_to_clipboard">Para memória</string>
<string name="menu_import_from_file">De arquivo</string>
<string name="import_failure">Falha ao importar</string>
<string name="export_failure">Falha ao salvar</string>
<string name="require_unlock_setting">Necessário desbloquear o dispositivo para alterar servidor</string>
<string name="auto_option_only_auto">Somente automático</string>
<string name="shizuku_failure_toast">Falha ao obter a permissão. Tente conceder manualmente</string>
<string name="shizuku_success_toast">Permissão concedida, você pode revogar a permissão do Shizuku agora</string>
</resources>

View file

@ -0,0 +1,53 @@
<resources>
<string name="app_name">Private DNS Quick Toggle</string>
<string name="tile_name">Переключить частный DNS</string>
<string name="permission_missing">Разрешение не предоставлено, проверьте приложение для получения информации</string>
<string name="dns_off">Выкл</string>
<string name="dns_auto">Авто</string>
<string name="dns_unknown">Неизвестно</string>
<string name="add_server">Добавить сервер</string>
<string name="menu_add">Добавить</string>
<string name="menu_save">Сохранить</string>
<string name="menu_privacy_policy">Политика конфиденциальности</string>
<string name="select_server">Выбрать сервер</string>
<string name="done">Готово</string>
<string name="cancel">Отмена</string>
<string name="delete_question">Удалить</string>
<string name="delete_message">Вы уверены, что хотите удалить сервер?</string>
<string name="delete">Удалить</string>
<string name="server_length_error">Адрес сервера не может быть пустым</string>
<string name="add_edittext_label_hint">Название DNS сервера (необязательно)</string>
<string name="add_edittext_hint">Адрес DNS сервера</string>
<string name="options">Опции</string>
<string name="ok">OK</string>
<string name="auto_option_description">Выберите, какие опции включить в плитке</string>
<string name="auto_option_only_off">Только \"Выкл\"</string>
<string name="auto_option_only_auto">Только \"Авто\"</string>
<string name="auto_option_off_and_auto">\"Выкл\" и \"Авто\"</string>
<string name="auto_option_only_private">Только частный DNS</string>
<string name="open_app">Открыть приложение</string>
<string name="set_to_off_toast">Частный DNS выключен</string>
<string name="set_to_auto_toast">Частный DNS установлен на "Авто"</string>
<string name="set_to_provider_toast">Частный DNS установлен на %1$s</string>
<string name="require_unlock_setting">Смена сервера требует разблокировки устройства</string>
<string name="a11y_drag_handle">Ручка перетаскивания</string>
<string name="menu_import">Импорт</string>
<string name="menu_export">Экспорт</string>
<string name="import_success">Успешно импортировано</string>
<string name="import_failure">Импорт не удался</string>
<string name="import_failure_json">Импорт не удался, некорректный JSON</string>
<string name="copy_success">Скопировано</string>
<string name="menu_import_from_file">Из файла</string>
<string name="menu_import_from_clipboard">Из буфера обмена</string>
<string name="menu_export_to_clipboard">В буфер обмена</string>
<string name="menu_export_share">Поделиться</string>
<string name="menu_export_to_file">В файл</string>
<string name="export_failure">Сохранение не удалось</string>
<string name="export_success">Успешно сохранено</string>
<string name="edit_server">Редактировать сервер</string>
<string name="no_servers_added">Нет доступных серверов</string>
<string name="empty_hint">Нажмите на кнопку ниже, чтобы добавить сервер</string>
<string name="add_server_enabled">Включён</string>
<string name="shizuku_success_toast">Разрешение получено, можно отозвать авторизацию Shizuku</string>
<string name="shizuku_failure_toast">Не удалось получить разрешение, предоставьте его вручную</string>
</resources>

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="ok">சரி</string>
<string name="app_name">தனியார் டி.என்.எச் விரைவாக மாற்று</string>
<string name="tile_name">தனியார் டி.என் கள் மாறுகின்றன</string>
<string name="permission_missing">இசைவு வழங்கப்படவில்லை, அதை எப்படி செய்வது என்று பார்க்க பயன்பாட்டை சரிபார்க்கவும்</string>
<string name="dns_off">அணை</string>
<string name="dns_auto">தானி</string>
<string name="dns_unknown">தெரியவில்லை</string>
<string name="add_server">சேவையகத்தைச் சேர்க்கவும்</string>
<string name="menu_add">கூட்டு</string>
<string name="menu_save">சேமி</string>
<string name="menu_privacy_policy">தனியுரிமைக் கொள்கை</string>
<string name="select_server">சேவையகத்தைத் தேர்ந்தெடுக்கவும்</string>
<string name="done">முடிந்தது</string>
<string name="cancel">ரத்துசெய்</string>
<string name="delete_question">நீக்கு</string>
<string name="delete_message">சேவையகத்தை நீக்க விரும்புகிறீர்களா?</string>
<string name="delete">நீக்கு</string>
<string name="add_edittext_hint">டிஎன்எச் சேவையக முகவரி</string>
<string name="options">விருப்பங்கள்</string>
<string name="auto_option_description">ஓடுகளில் எந்த விருப்பங்களைச் சேர்க்க வேண்டும் என்பதைத் தேர்வுசெய்க</string>
<string name="auto_option_only_off">மட்டுமே</string>
<string name="auto_option_only_auto">ஆட்டோ மட்டுமே</string>
<string name="auto_option_off_and_auto">ஆஃப் மற்றும் ஆட்டோ</string>
<string name="auto_option_only_private">தனியார் டி.என்.எச் மட்டுமே</string>
<string name="open_app">திறந்த பயன்பாடு</string>
<string name="set_to_off_toast">தனியார் டி.என்.எச் அணைக்கப்பட்டது</string>
<string name="set_to_auto_toast">தனியார் டி.என்.எச் ஆட்டோவாக அமைக்கப்பட்டுள்ளது</string>
<string name="set_to_provider_toast">தனியார் டி.என்.எச் %1$s என அமைக்கப்பட்டுள்ளது</string>
<string name="require_unlock_setting">சேவையகத்தை மாற்ற சாதனத்தைத் திறக்க வேண்டும்</string>
<string name="a11y_drag_handle">இழுவை கைப்பிடி</string>
<string name="menu_import">இறக்குமதி</string>
<string name="menu_export">ஏற்றுமதி</string>
<string name="import_success">இறக்குமதி செய்யப்பட்டது</string>
<string name="import_failure">இறக்குமதி தோல்வியடைந்தது</string>
<string name="import_failure_json">இறக்குமதி தோல்வியுற்றது, தவறாக சாதொபொகு</string>
<string name="copy_success">நகலெடுக்கப்பட்டது</string>
<string name="menu_import_from_file">கோப்பிலிருந்து</string>
<string name="menu_import_from_clipboard">கிளிப்போர்டிலிருந்து</string>
<string name="menu_export_to_clipboard">இடைநிலைப்பலகைக்கு</string>
<string name="menu_export_share">பங்கு</string>
<string name="menu_export_to_file">தாக்கல் செய்ய</string>
<string name="export_failure">சேமிப்பு தோல்வியடைந்தது</string>
<string name="export_success">வெற்றிகரமாக சேமிக்கப்பட்டது</string>
<string name="edit_server">சேவையகத்தைத் திருத்து</string>
<string name="no_servers_added">சேவையகங்கள் எதுவும் சேர்க்கப்படவில்லை</string>
<string name="empty_hint">ஒன்றைச் சேர்க்க கீழே உள்ள பொத்தானைத் தட்டவும்</string>
<string name="add_server_enabled">இயக்கப்பட்டது</string>
<string name="shizuku_success_toast">இசைவு வழங்கப்பட்டது, நீங்கள் இப்போது சிசுகு அனுமதியை ரத்து செய்யலாம்</string>
<string name="shizuku_failure_toast">இசைவு பெறுவதில் தோல்வி, தயவுசெய்து அதை கைமுறையாக வழங்கவும்</string>
<string name="server_length_error">சேவையக முகவரி காலியாக இருக்க முடியாது</string>
<string name="add_edittext_label_hint">டிஎன்எச் சேவையக சிட்டை (விரும்பினால்)</string>
</resources>

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="delete_message">Sunucuyu silmek istediğinizden emin misiniz?</string>
<string name="auto_option_only_off">Sadece kapalı</string>
<string name="require_unlock_setting">Sunucuyu değiştirmek için cihazın kilidini açmanız gerekiyor</string>
<string name="dns_auto">Oto</string>
<string name="dns_unknown">Bilinmeyen</string>
<string name="done">Tamam</string>
<string name="cancel">İptal</string>
<string name="add_edittext_label_hint">DNS sunucusu Etiketi (İsteğe bağlı)</string>
<string name="options">Seçenekler</string>
<string name="ok">OK</string>
<string name="auto_option_only_auto">Sadece oto</string>
<string name="auto_option_off_and_auto">Kapalı ve oto</string>
<string name="set_to_off_toast">Özel DNS kapandı</string>
<string name="open_app">Uygulamayıınız</string>
<string name="menu_import">İçe aktar</string>
<string name="menu_export">Dışa aktar</string>
<string name="import_success">içe aktarıldı</string>
<string name="menu_export_to_file">Dosyaya</string>
<string name="export_failure">Kaydetme başarısız oldu</string>
<string name="no_servers_added">Hiç Sunucu Eklenmedi</string>
<string name="empty_hint">Eklemek için aşağıdaki düğmeye dokunun</string>
<string name="shizuku_failure_toast">İzin alınamadı, lütfen manuel olarak verin</string>
<string name="auto_option_description">Karoya hangi seçeneklerin dahil edileceğini seçiniz</string>
<string name="dns_off">Kapalı</string>
<string name="menu_privacy_policy">Gizlilik Politikası</string>
<string name="delete_question">Sil</string>
<string name="server_length_error">Sunucu adresi boş olamaz</string>
<string name="delete">Sil</string>
<string name="add_edittext_hint">DNS sunucu adresi</string>
<string name="a11y_drag_handle">tutacağı sürükle</string>
<string name="menu_add">Ekle</string>
<string name="add_server">Sunucu Ekleyiniz</string>
<string name="permission_missing">İzin verilmedi, nasıl yapıldığını görmek için uygulamayı kontrol ediniz</string>
<string name="import_failure_json">İçe aktarma başarısız oldu, hatalı biçimlendirilmiş JSON</string>
<string name="app_name">Özel DNS Hızlı Geçiş</string>
<string name="shizuku_success_toast">İzin verildi, şimdi Shizuku iznini iptal edebilirsiniz</string>
<string name="import_failure">Aktarma başarısız</string>
<string name="menu_import_from_clipboard">Panodan</string>
<string name="export_success">Başarıyla kaydedildi</string>
<string name="menu_save">Kaydet</string>
<string name="set_to_auto_toast">Özel DNS otomatik olarak ayarlandı</string>
<string name="set_to_provider_toast">Özel DNS %1$s olarak ayarlandı</string>
<string name="auto_option_only_private">Sadece Özel DNS</string>
<string name="select_server">Sunucuyu Seçin</string>
<string name="copy_success">Kopyalandı</string>
<string name="edit_server">Sunucuyu düzenle</string>
<string name="add_server_enabled">Etkin</string>
<string name="tile_name">Özel DNS Geçişi</string>
<string name="menu_import_from_file">Dosyadan</string>
<string name="menu_export_to_clipboard">Panoya</string>
<string name="menu_export_share">Paylaş</string>
</resources>

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="add_server">Thêm máy chủ</string>
<string name="auto_option_only_auto">Chỉ tự động</string>
<string name="permission_missing">Chưa được cấp quyền, hãy kiểm tra ứng dụng để biết cách thực hiện</string>
<string name="dns_off">Tắt</string>
<string name="dns_auto">Tự động</string>
<string name="menu_add">Thêm</string>
<string name="menu_privacy_policy">Chính sách bảo mật</string>
<string name="select_server">Chọn máy chủ</string>
<string name="done">Hoàn thành</string>
<string name="cancel">Hủy</string>
<string name="delete_message">Bạn có chắc chắn muốn xóa máy chủ không?</string>
<string name="delete">Xoá</string>
<string name="add_edittext_hint">Địa chỉ máy chủ DNS</string>
<string name="options">Tùy chọn</string>
<string name="ok">OK</string>
<string name="auto_option_description">Chọn các tùy chọn để đưa vào ô</string>
<string name="auto_option_only_off">Chỉ tắt</string>
<string name="auto_option_off_and_auto">Tắt và tự động</string>
<string name="open_app">Mở ứng dụng</string>
<string name="set_to_auto_toast">DNS cá nhân được thiết lập tự động</string>
<string name="require_unlock_setting">Yêu cầu mở khóa thiết bị để thay đổi máy chủ</string>
<string name="a11y_drag_handle">Tay cầm kéo</string>
<string name="menu_export">Xuất</string>
<string name="import_success">Đã nhập</string>
<string name="import_failure">Nhập thất bại</string>
<string name="import_failure_json">Nhập thất bại, JSON bị lỗi</string>
<string name="copy_success">Đã sao chép</string>
<string name="menu_import_from_file">Từ tập tin</string>
<string name="menu_export_share">Chia sẻ</string>
<string name="menu_export_to_file">Thành tập tin</string>
<string name="export_failure">Lưu không thành công</string>
<string name="edit_server">Chỉnh sửa máy chủ</string>
<string name="no_servers_added">Chưa có máy chủ nào</string>
<string name="empty_hint">Nhấn vào nút bên dưới để thêm</string>
<string name="add_server_enabled">Đã bật</string>
<string name="tile_name">Chuyển đổi DNS cá nhân</string>
<string name="delete_question">Xoá</string>
<string name="app_name">Chuyển đổi nhanh DNS cá nhân</string>
<string name="dns_unknown">Không rõ</string>
<string name="add_edittext_label_hint">Nhãn máy chủ DNS (Không bắt buộc)</string>
<string name="menu_save">Lưu</string>
<string name="server_length_error">Địa chỉ máy chủ không được để trống</string>
<string name="auto_option_only_private">Chỉ DNS cá nhân</string>
<string name="set_to_off_toast">Đã tắt DNS cá nhân</string>
<string name="set_to_provider_toast">DNS cá nhân được đặt thành %1$s</string>
<string name="menu_import">Nhập</string>
<string name="menu_import_from_clipboard">Từ bảng nhớ tạm</string>
<string name="menu_export_to_clipboard">Vào bảng nhớ tạm</string>
<string name="export_success">Đã lưu thành công</string>
</resources>

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="tile_name">私有DNS触发</string>
<string name="permission_missing">必要权限未授予,请看相关说明</string>
<string name="dns_off">关闭</string>
<string name="dns_auto">自动</string>
<string name="dns_unknown">未知</string>
<string name="add_server">添加DNS服务器</string>
<string name="menu_add">添加</string>
<string name="menu_save">存储</string>
<string name="menu_privacy_policy">隐私策略</string>
<string name="select_server">选择服务器</string>
<string name="done">完成</string>
<string name="cancel">取消</string>
<string name="delete_question">删除条目</string>
<string name="delete_message">你确认要删除这个服务器条目吗?</string>
<string name="delete">删除</string>
<string name="server_length_error">服务器地址不可为空</string>
<string name="add_edittext_label_hint">DNS服务器标识</string>
<string name="add_edittext_hint">DNS服务器地址</string>
<string name="options">选项</string>
<string name="ok">确认</string>
<string name="auto_option_description">选择要在磁贴中启用的选项</string>
<string name="auto_option_only_off">仅“关闭”</string>
<string name="auto_option_only_auto">仅“自动”</string>
<string name="auto_option_off_and_auto">“关闭“与”自动“</string>
<string name="auto_option_only_private">仅设置的私有DNS</string>
<string name="open_app">打开软件</string>
<string name="set_to_off_toast">不使用私有DNS</string>
<string name="set_to_auto_toast">自动使用私有DNS</string>
<string name="set_to_provider_toast">设置为使用私有DNS\"%1$s\"</string>
<string name="require_unlock_setting">更改服务器设置要求设备解锁</string>
<string name="a11y_drag_handle">拖动把手</string>
<string name="menu_import">导入</string>
<string name="menu_export">导出</string>
<string name="import_success">已导入</string>
<string name="import_failure">导入失败</string>
<string name="import_failure_json">导入失败json格式异常</string>
<string name="copy_success">已复制</string>
<string name="menu_import_from_file">从文件导入</string>
<string name="menu_import_from_clipboard">从剪贴板导入</string>
<string name="menu_export_to_clipboard">导出至剪贴板</string>
<string name="menu_export_share">分享</string>
<string name="menu_export_to_file">导出至文件</string>
<string name="export_failure">保存失败</string>
<string name="export_success">保存成功</string>
<string name="edit_server">编辑服务器条目</string>
<string name="no_servers_added">无可用服务器</string>
<string name="empty_hint">点击下方\"+\"添加一个吧</string>
<string name="add_server_enabled">已启用</string>
</resources>

View file

@ -12,6 +12,4 @@
<color name="md_theme_dark_secondary">#CCC2DC</color>
<color name="md_theme_dark_tertiary">#EFB8C8</color>
<color name="md_theme_dark_background">#1C1B1F</color>
<color name="material_dynamic_neutral50">#747879</color>
</resources>

View file

@ -47,4 +47,7 @@
<string name="edit_server">Edit server</string>
<string name="no_servers_added">No Servers Added</string>
<string name="empty_hint">Tap on the button below to add one</string>
<string name="add_server_enabled">Enabled</string>
<string name="shizuku_success_toast">Permission granted, you can revoke the Shizuku permission now</string>
<string name="shizuku_failure_toast">Failed to acquire permission, please grant it manually</string>
</resources>

View file

@ -1,18 +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.5.1'
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
}

19
build.gradle.kts Normal file
View file

@ -0,0 +1,19 @@
// 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
}

View file

@ -0,0 +1,7 @@
- Replaced server storage backend with Room, allowing for easier further expansion
- Add option to disable saved servers
- Improved backup handling
- Fixed desync bug while dragging servers
- Reorganized source file structure
- Updated Kotlin version
- Updated Java version

View file

@ -0,0 +1,11 @@
- 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

View file

@ -0,0 +1,12 @@
- 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))

View file

@ -1,6 +1,6 @@
#Mon Aug 16 15:36:35 MSK 2021
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

View file

@ -6,4 +6,4 @@ dependencyResolutionManagement {
}
}
rootProject.name = "Private DNS Quick Toggle"
include ':app'
include("app")