Compare commits

...

95 commits
beta-3 ... 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
Maksim Karasev
eead2a912d Bump version 2024-08-13 22:44:55 +03:00
Maksim Karasev
95f778a787 Fix list entry layout 2024-08-13 22:32:43 +03:00
Maksim Karasev
53ab655eae Fix crashes on Android 11 and earlier 2024-08-13 22:22:50 +03:00
Maksim Karasev
af61a85f9e Bump version 2024-08-12 15:30:38 +03:00
Maksim Karasev
0028d72095 Fix layout when using long addresses (#30) 2024-08-12 15:26:58 +03:00
Stephen Vaz
470f8445f9
Option to edit the server (#29)
* Option to edit the server

* Empty List of Severs notified
2024-08-12 15:18:47 +03:00
Maksim Karasev
5fe2354e7d Bump version 2024-07-03 18:12:50 +03:00
Maksim Karasev
213d3e4dee Add more ways to export/import settings 2024-07-02 20:39:26 +03:00
Maksim Karasev
a6ed85e2c8 Fix label not appearing sometimes 2024-07-02 16:56:42 +03:00
Maksim Karasev
66a83b3dac Update screenshots 2024-07-02 16:18:43 +03:00
Maksim Karasev
3a697e32e3 Implement settings export/import (#26) 2024-07-02 16:07:04 +03:00
Maksim Karasev
6caa1432aa Add GitHub issue forms 2024-06-27 22:25:17 +03:00
Maksim Karasev
7dd0fbf802 Fastlane changelog 2024-06-27 21:18:07 +03:00
Maksim Karasev
5c38ec2db8 Bump version 2024-06-27 21:04:13 +03:00
Maksim Karasev
29957ef908 Refresh tile on server pick (#24) 2024-06-27 21:03:37 +03:00
Maksim Karasev
6b7324a2a8 Update README.md 2024-06-27 15:30:10 +03:00
Maksim Karasev
08a601533e Formatting 2024-06-27 15:13:24 +03:00
Maksim Karasev
17f62ac892 Improve label UI 2024-06-27 15:09:25 +03:00
Maksim Karasev
baaffd334d Fix build 2024-06-27 15:04:49 +03:00
Maksim Karasev
c85a96fdcf Change debug package name 2024-06-27 15:04:34 +03:00
Praveen Kumar
c24f8e0b38
Feature: Reordring (#23)
* Reordering of Servers

* Fix drag handle being drawn outside of the screen

---------

Co-authored-by: Maksim Karasev <karasevm98@gmail.com>
2024-06-27 14:45:05 +03:00
Praveen Kumar
01650977c3
Support Labels (#22)
* Support Labels

* Fix Bug - If the current item is deleted

---------

Co-authored-by: Maksim Karasev <karasevm98@gmail.com>
2024-06-27 14:33:12 +03:00
Praveen Kumar
d9ad7d2030
Support for selection of only Private DNS in Option Dialog (#21) 2024-06-27 14:21:21 +03:00
Maksim Karasev
52fc30e96d Disable encrypted dependency metadata 2024-06-24 14:45:35 +03:00
Maksim Karasev
f0cc3171ba Update fastlane description 2024-06-21 23:31:49 +03:00
Maksim Karasev
610444bde3 Improve screenshots 2024-06-21 22:55:26 +03:00
Maksim Karasev
391842244c Bump version 2024-06-21 14:19:52 +03:00
Maksim Karasev
2d6dd3a4e5 Add fastlane metadata 2024-06-21 14:19:42 +03:00
Maksim Karasev
e04f1246f6 Update gradle 2024-06-21 13:08:42 +03:00
Maksim Karasev
0ee473d320 Fix invisible nav buttons on some devices 2024-06-21 13:03:52 +03:00
Maksim Karasev
3275548201 Add option to require unlocking the device (#18) 2024-06-21 12:58:45 +03:00
Maksim Karasev
347781b61e Bump version 2024-06-14 17:50:57 +03:00
Maksim Karasev
65b75d53dd Set recommended build options 2024-06-14 17:46:36 +03:00
Maksim Karasev
0c208ad745 More cleanup 2024-06-14 17:44:37 +03:00
Maksim Karasev
6efc52c870 Update dependencies 2024-06-14 17:09:04 +03:00
Maksim Karasev
5bd8d2c122 Replace deprecated buildDir 2024-06-14 17:03:24 +03:00
Maksim Karasev
1a9573bb41 Misc. cleanup and formatting 2024-06-14 16:57:47 +03:00
Maksim Karasev
fdd711eef4 Improve shizuku error handling 2024-06-14 16:31:10 +03:00
Maksim Karasev
c5f2aed736 Adjust tile selection dialog 2024-06-14 16:30:34 +03:00
Maksim Karasev
48bb2698bd Implement option menu 2024-06-14 16:29:25 +03:00
Maksim Karasev
8965ba6a3d Update gitignore 2024-06-14 14:45:20 +03:00
Praveen Kumar
85d6a7410c
Add Text field Validation (#9)
* Add Text field validation

* Implement Better Validation
2024-06-13 13:42:36 +03:00
Praveen Kumar
9aabfa6261
Quick tile Selection Dialog (#12)
* Feature Quick tile Server Selection dialog

* Fix Bug

* Add Toast Messages
2024-06-13 13:38:56 +03:00
Maksim Karasev
96e345606e
Merge pull request #13 from InfiniteCoder06/docs-readme
Update Screenshots in Readme
2024-04-13 19:43:51 +03:00
Maksim Karasev
e50b99e673
Merge pull request #10 from InfiniteCoder06/refractor-sharedPreferences
Refactor Shared Preferences to a Class
2024-04-13 19:43:07 +03:00
Praveen Kumar
42a6220de3
Update Screenshots in Readme 2024-04-07 14:12:10 +05:30
Praveen Kumar
aefa31b8f4
Refactor Shared Preferences to a Class 2024-04-06 19:39:48 +05:30
Maksim Karasev
f6ddc85b43 Bump version 2024-04-03 21:22:23 +03:00
Maksim Karasev
afa2398188
Merge pull request #4 from InfiniteCoder06/feature-m3
Material 3 Redesign and Material You Theming
2024-04-03 21:07:49 +03:00
Praveen Kumar
c5a5be4ed4
Material 3 Redesign and Material You Theming 2024-04-03 18:50:17 +05:30
Maksim Karasev
2ab8354116 Provide monochrome icon for theming 2024-03-09 14:10:52 +03:00
Maksim Karasev
989f340fc2 Remove unnecessary code 2024-03-09 14:04:12 +03:00
Maksim Karasev
51774a1675 Rewrite Shizuku related code for newer Android versions 2024-03-09 14:00:01 +03:00
Maksim Karasev
936fdc5121 Fix quick tile init crash 2024-03-09 13:59:00 +03:00
Maksim Karasev
c1e563dcee Bump version 2024-03-07 15:24:46 +03:00
Maksim Karasev
30bd02d8ff Migrate BuildConfig to Gradle Build Files 2024-03-07 15:20:37 +03:00
Maksim Karasev
04cc8ccf02 Implement automatic dns option 2024-03-07 15:20:37 +03:00
Maksim Karasev
1b321af43e Update dependencies 2024-03-07 14:32:53 +03:00
Maksim Karasev
5d072ed0af Clarify resource names 2024-03-07 14:31:51 +03:00
104 changed files with 3115 additions and 760 deletions

View file

@ -0,0 +1,73 @@
name: Bug report
description: File a bug report.
labels: ['bug']
assignees: ['karasevm']
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: input
id: app_version
attributes:
label: Application Version
description: What version of the app are you running?
placeholder: ex. 1.0
validations:
required: true
- type: input
id: android_version
attributes:
label: Application Version
description: What version of Android you running?
placeholder: ex. 13
validations:
required: true
- type: input
id: device
attributes:
label: Device
description: What device are you using?
placeholder: ex. Pixel 5
validations:
required: true
- type: dropdown
id: install_method
attributes:
label: How do you provide the permission?
options:
- Shizuku
- ADB
- Other
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: A bug happened!
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to reproduce
description: |
Please describe what you did to reproduce the bug.
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
- type: textarea
id: screens
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem.
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add any other context about the problem here.

View file

@ -0,0 +1,30 @@
name: Feature request
description: Suggest an idea for this project.
labels: ["enhancement"]
assignees: ["karasevm"]
body:
- type: textarea
id: problem
attributes:
label: Is your feature request related to a problem? Please describe.
description: A clear and concise description of what the problem is.
placeholder: I'm always frustrated when [...]
validations:
required: true
- type: textarea
id: solution
attributes:
label: Describe the solution you'd like.
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: alternative
attributes:
label: Describe alternatives you've considered.
description: A clear and concise description of any alternative solutions or features you've considered.
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add any other context about the problem here.

9
.github/ISSUE_TEMPLATE/03-other.yml vendored Normal file
View file

@ -0,0 +1,9 @@
name: Other
description: If other options don't fit your question.
body:
- type: textarea
id: other
attributes:
label: Ask a question
validations:
required: true

33
.gitignore vendored
View file

@ -3,6 +3,15 @@
*.ap_
*.aab
# Android Studio generated files and folders
captures/
.externalNativeBuild/
.cxx/
output.json
# Release dir
app/release/*
# Files for the ART/Dalvik VM
*.dex
@ -30,23 +39,16 @@ proguard/
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
.idea/caches
.idea/
misc.xml
deploymentTargetDropDown.xml
render.experimental.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
*.jks
*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
@ -65,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>

24
.idea/misc.xml generated
View file

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DesignSurface">
<option name="filePathToZoomLevelMap">
<map>
<entry key="..\:/dev/PrivateDNS/app/src/main/res/drawable/ic_auto_black_24dp.xml" value="0.2526041666666667" />
<entry key="..\:/dev/PrivateDNS/app/src/main/res/drawable/ic_tile_default.xml" value="0.4282051282051282" />
<entry key="..\:/dev/PrivateDNS/app/src/main/res/layout/activity_main.xml" value="0.3365036231884058" />
<entry key="..\:/dev/PrivateDNS/app/src/main/res/layout/dialog_add.xml" value="0.20364741641337386" />
<entry key="..\:/dev/PrivateDNS/app/src/main/res/layout/menu_main.xml" value="0.20364741641337386" />
<entry key="..\:/dev/PrivateDNS/app/src/main/res/layout/recyclerview_row.xml" value="0.3365036231884058" />
<entry key="..\:/dev/PrivateDNS/app/src/main/res/menu/menu_main.xml" value="0.40925925925925927" />
<entry key="..\:/dev/PrivateDNS/app/src/main/res/menu/temp.xml" value="0.3055555555555556" />
</map>
</option>
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

2
.idea/vcs.xml generated
View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="" vcs="Git" />
</component>
</project>

View file

@ -1,17 +1,23 @@
[![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
a single tap.
![Private DNS app screenshot](readme.png)
![Private DNS app screenshot](readme.jpg)
## Installation
Get the latest apk on the [releases page](https://github.com/karasevm/PrivateDNSAndroid/releases/latest).
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).
@ -26,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,54 +0,0 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
}
android {
compileSdk 34
defaultConfig {
applicationId "ru.karasevm.privatednstoggle"
minSdk 21
targetSdk 34
versionCode 5
versionName "1.0"
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'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
namespace 'ru.karasevm.privatednstoggle'
}
dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
def shizuku_version = '13.1.4'
implementation "dev.rikka.shizuku:api:$shizuku_version"
implementation "dev.rikka.shizuku:provider:$shizuku_version"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.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

@ -1,20 +0,0 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "ru.karasevm.privatednstoggle",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "1.0",
"outputFile": "app-release.apk"
}
],
"elementType": "File"
}

View file

@ -3,41 +3,53 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"
tools:ignore="ProtectedPermissions" />
<application
android:name=".PrivateDNSApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MyApplication">
android:theme="@style/Theme.Transparent">
<provider
android:name="rikka.shizuku.ShizukuProvider"
android:authorities="${applicationId}.shizuku"
android:multiprocess="false"
android:enabled="true"
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"
android:exported="true">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".DnsTileService"
android:icon="@drawable/ic_auto_black_24dp"
android:label="@string/tile_name"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
<activity
android:name=".ui.SettingsDialogActivity"
android:theme="@style/Theme.Transparent"
android:excludeFromRecents="true"
android:exported="true">
<intent-filter>
<action
android:name="android.service.quicksettings.action.QS_TILE"/>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter>
</activity>
<service
android:name=".service.DnsTileService"
android:exported="true"
android:icon="@drawable/ic_unknown_black_24dp"
android:label="@string/tile_name"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
</application>
</manifest>

View file

@ -1,75 +0,0 @@
package ru.karasevm.privatednstoggle
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import ru.karasevm.privatednstoggle.databinding.DialogAddBinding
class AddServerDialogFragment : DialogFragment() {
// Use this instance of the interface to deliver action events
private lateinit var listener: NoticeDialogListener
private var _binding: DialogAddBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
/* The activity that creates an instance of this dialog fragment must
* 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(dialog: DialogFragment, server: String)
}
// Override the Fragment.onAttach() method to instantiate the NoticeDialogListener
override fun onAttach(context: Context) {
super.onAttach(context)
// Verify that the host activity implements the callback interface
try {
// Instantiate the NoticeDialogListener so we can send events to the host
listener = context as NoticeDialogListener
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException(
(context.toString() +
" must implement NoticeDialogListener")
)
}
}
override fun onCreateDialog(
savedInstanceState: Bundle?
): Dialog {
return activity?.let {
val builder = AlertDialog.Builder(it)
// Get the layout inflater
val inflater = requireActivity().layoutInflater
_binding = DialogAddBinding.inflate(inflater)
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
builder.setTitle(R.string.add_server)
.setView(view)
// Add action buttons
.setPositiveButton(R.string.add
) { _, _ ->
listener.onDialogPositiveClick(
this,
binding.editTextServerAddr.text.toString()
)
}
.setNegativeButton(R.string.cancel
) { _, _ ->
dialog?.cancel()
}
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
}

View file

@ -1,195 +0,0 @@
package ru.karasevm.privatednstoggle
import android.Manifest
import android.content.pm.PackageManager
import android.graphics.drawable.Icon
import android.provider.Settings
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import android.util.Log
import android.widget.Toast
const val DNS_MODE_OFF = "off"
const val DNS_MODE_AUTO = "opportunistic"
const val DNS_MODE_PRIVATE = "hostname"
class DnsTileService : TileService() {
private fun checkForPermission(): Boolean {
if (checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) == PackageManager.PERMISSION_GRANTED) {
return true
}
Toast.makeText(this, R.string.permission_missing, Toast.LENGTH_SHORT).show()
return false
}
override fun onTileAdded() {
super.onTileAdded()
checkForPermission()
// Update state
qsTile.state = Tile.STATE_INACTIVE
// Update looks
qsTile.updateTile()
}
override fun onClick() {
super.onClick()
if (!checkForPermission()) {
return
}
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)) {
// refreshTile(qsTile, Tile.STATE_INACTIVE, getString(R.string.dns_off), R.drawable.ic_off_black_24dp)
changeTileState(
qsTile,
Tile.STATE_ACTIVE,
getNextAddress(dnsProvider),
R.drawable.ic_private_black_24dp,
DNS_MODE_PRIVATE,
getNextAddress(dnsProvider)
)
} else if (dnsMode == null || dnsMode.equals(DNS_MODE_AUTO, ignoreCase = true)) {
changeTileState(
qsTile,
Tile.STATE_ACTIVE,
getNextAddress(dnsProvider),
R.drawable.ic_private_black_24dp,
DNS_MODE_PRIVATE,
getNextAddress(dnsProvider)
)
} else if (dnsMode.equals(DNS_MODE_PRIVATE, ignoreCase = true)) {
if (getNextAddress(dnsProvider) == null) {
changeTileState(
qsTile,
Tile.STATE_INACTIVE,
getString(R.string.dns_off),
R.drawable.ic_off_black_24dp,
DNS_MODE_OFF,
getNextAddress(dnsProvider)
)
} else {
changeTileState(
qsTile,
Tile.STATE_ACTIVE,
getNextAddress(dnsProvider),
R.drawable.ic_private_black_24dp,
DNS_MODE_PRIVATE,
getNextAddress(dnsProvider)
)
}
}
}
override fun onStartListening() {
super.onStartListening()
if (!checkForPermission()) {
return
}
val dnsMode = Settings.Global.getString(contentResolver, "private_dns_mode")
Log.d("TEMP", "onStartListening: called $dnsMode")
if (dnsMode.equals(DNS_MODE_OFF, ignoreCase = true)) {
refreshTile(
qsTile,
Tile.STATE_INACTIVE,
getString(R.string.dns_off),
R.drawable.ic_off_black_24dp
)
} else if (dnsMode == null || dnsMode.equals(DNS_MODE_AUTO, ignoreCase = true)) {
refreshTile(
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")
if (dnsProvider != null) {
refreshTile(
qsTile,
Tile.STATE_ACTIVE,
dnsProvider,
R.drawable.ic_private_black_24dp
)
} else {
Toast.makeText(this, R.string.permission_missing, Toast.LENGTH_SHORT).show()
}
}
}
/**
* Updates tile to specified parameters
*
* @param tile tile to update
* @param state tile state
* @param label tile label
* @param icon tile icon
*/
private fun refreshTile(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)
Settings.Global.putString(contentResolver, "private_dns_mode", dnsMode)
Settings.Global.putString(contentResolver, "private_dns_specifier", 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?): String? {
val sharedPrefs = this.getSharedPreferences("app_prefs", 0)
val items = sharedPrefs.getString("dns_servers", "dns.google")!!.split(",").toMutableList()
// Fallback if list is empty
if (items[0] == "") {
items.removeAt(0)
items.add("dns.google")
}
val index = items.indexOf(currentAddress)
if (index == -1 || currentAddress == null) {
return items[0]
}
if (index == items.size - 1) {
return null
}
return items[index + 1]
}
}

View file

@ -1,172 +0,0 @@
package ru.karasevm.privatednstoggle
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.os.IBinder
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.DialogFragment
import androidx.recyclerview.widget.LinearLayoutManager
import rikka.shizuku.Shizuku
import rikka.shizuku.ShizukuBinderWrapper
import rikka.shizuku.ShizukuProvider
import rikka.shizuku.SystemServiceHelper
import ru.karasevm.privatednstoggle.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogListener, DeleteServerDialogFragment.NoticeDialogListener, Shizuku.OnRequestPermissionResultListener {
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
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Shizuku.addRequestPermissionResultListener(this::onRequestPermissionResult)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
linearLayoutManager = LinearLayoutManager(this)
binding.recyclerView.layoutManager = linearLayoutManager
sharedPrefs = this.getSharedPreferences("app_prefs", 0)
items = sharedPrefs.getString("dns_servers", "")!!.split(",").toMutableList()
if (items[0] == "") {
items.removeAt(0)
}
adapter = RecyclerAdapter(items)
adapter.onItemClick = { position ->
val newFragment = DeleteServerDialogFragment(position)
newFragment.show(supportFragmentManager, "delete_server")
}
binding.recyclerView.adapter = adapter
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onResume() {
super.onResume()
// Check if Shizuku is available
if (Shizuku.pingBinder()) {
// check if permission is granted already
val isGranted = if (Shizuku.isPreV11() || Shizuku.getVersion() < 11) {
checkSelfPermission(ShizukuProvider.PERMISSION) == PackageManager.PERMISSION_GRANTED
} else {
Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
}
// request permission if not granted
if (!isGranted && !Shizuku.shouldShowRequestPermissionRationale()) {
if (Shizuku.isPreV11() || Shizuku.getVersion() < 11) {
requestPermissions(arrayOf(ShizukuProvider.PERMISSION), 1)
} else {
Shizuku.requestPermission(1)
}
}
} else {
if (checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/"))
startActivity(browserIntent)
finish()
}
}
}
override fun onDestroy() {
super.onDestroy()
Shizuku.removeRequestPermissionResultListener(this::onRequestPermissionResult)
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.add_server -> {
val newFragment = AddServerDialogFragment()
newFragment.show(supportFragmentManager, "add_server")
true
}
R.id.privacy_policy -> {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/privacy_policy"))
startActivity(browserIntent)
true
}
else -> {
// If we got here, the user's action was not recognized.
// Invoke the superclass to handle it.
super.onOptionsItemSelected(item)
}
}
override fun onDialogPositiveClick(dialog: DialogFragment, server: String) {
if (server.isEmpty()) {
Toast.makeText(this, R.string.server_length_error, Toast.LENGTH_SHORT).show()
return
}
items.add(server)
adapter.setData(items.toMutableList())
binding.recyclerView.adapter?.notifyItemInserted(items.size - 1)
sharedPrefs.edit()
.putString("dns_servers", items.joinToString(separator = ",") { it }).apply()
}
override fun onDialogPositiveClick(dialog: DialogFragment,position: Int) {
items.removeAt(position)
adapter.setData(items.toMutableList())
adapter.notifyItemRemoved(position)
sharedPrefs.edit()
.putString("dns_servers", items.joinToString(separator = ",") { it }).apply()
}
@SuppressLint("PrivateApi")
override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) {
val isGranted = grantResult == PackageManager.PERMISSION_GRANTED
if (isGranted) {
val packageName = "ru.karasevm.privatednstoggle"
val iPmClass = Class.forName("android.content.pm.IPackageManager")
val iPmStub = Class.forName("android.content.pm.IPackageManager\$Stub")
val asInterfaceMethod = iPmStub.getMethod("asInterface", IBinder::class.java)
val grantRuntimePermissionMethod = iPmClass.getMethod(
"grantRuntimePermission",
String::class.java /* package name */,
String::class.java /* permission name */,
Int::class.java /* user ID */
)
val iPmInstance = asInterfaceMethod.invoke(
null, ShizukuBinderWrapper(
SystemServiceHelper.getSystemService("package")
)
)
grantRuntimePermissionMethod.invoke(
iPmInstance,
packageName,
Manifest.permission.WRITE_SECURE_SETTINGS,
0
)
} else if (checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/"))
startActivity(browserIntent)
finish()
}
}
}

View file

@ -0,0 +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,50 +0,0 @@
package ru.karasevm.privatednstoggle
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import java.util.*
class RecyclerAdapter(val items: MutableList<String>): RecyclerView.Adapter<RecyclerAdapter.ViewHolder>() {
var onItemClick: ((Int) -> 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
}
override fun onBindViewHolder(holder: RecyclerAdapter.ViewHolder, position: Int) {
val item = items[position]
// sets the text to the textview from our itemHolder class
holder.textView.text = item
}
override fun getItemCount(): Int {
return items.size
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textView: TextView = itemView.findViewById(R.id.textView)
init {
itemView.setOnClickListener {
onItemClick?.invoke(adapterPosition)
}
}
}
fun setData(newItems: MutableList<String>) {
items.run {
clear()
addAll(newItems)
}
}
}

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

@ -0,0 +1,140 @@
package ru.karasevm.privatednstoggle.ui
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import android.text.Editable
import android.text.TextUtils
import android.text.TextWatcher
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 dnsServer: DnsServer?
) : DialogFragment() {
// Use this instance of the interface to deliver action events
private lateinit var listener: NoticeDialogListener
private var _binding: DialogAddBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
/* The activity that creates an instance of this dialog fragment must
* 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 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
override fun onAttach(context: Context) {
super.onAttach(context)
// Verify that the host activity implements the callback interface
try {
// Instantiate the NoticeDialogListener so we can send events to the host
listener = context as NoticeDialogListener
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException(
(context.toString() +
" must implement NoticeDialogListener")
)
}
}
override fun onCreateDialog(
savedInstanceState: Bundle?
): Dialog {
return activity?.let {
val builder = MaterialAlertDialogBuilder(it)
// Get the layout inflater
val inflater = requireActivity().layoutInflater
_binding = DialogAddBinding.inflate(inflater)
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 (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.onUpdateDialogPositiveClick(
dnsServer.id,
binding.editTextServerAddr.text.toString().trim(),
binding.editTextServerHint.text.toString().trim(),
binding.serverEnabledSwitch.isChecked
)
}
.setNegativeButton(
R.string.cancel
) { _, _ ->
dialog?.cancel()
}
.setNeutralButton(
R.string.delete
) { _, _ ->
listener.onDeleteItemClicked(dnsServer.id)
}
} else {
builder.setTitle(R.string.add_server)
.setView(view)
// Add action buttons
.setPositiveButton(
R.string.menu_add
) { _, _ ->
listener.onAddDialogPositiveClick(
binding.editTextServerHint.text.toString().trim(),
binding.editTextServerAddr.text.toString().trim()
)
}
.setNegativeButton(
R.string.cancel
) { _, _ ->
dialog?.cancel()
}
}
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
override fun onStart() {
super.onStart()
val button = ((dialog) as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
binding.editTextServerAddr.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
val server = binding.editTextServerAddr.text.toString().trim()
if (TextUtils.isEmpty(server) || !isValidServer(server)) {
button.isEnabled = false
} else {
binding.editTextServerAddr.error = null
button.isEnabled = true
}
}
})
}
private fun isValidServer(str: String): Boolean {
return InternetDomainName.isValid(str)
}
}

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.appcompat.app.AlertDialog
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(dialog: DialogFragment, position: Int)
fun onDeleteDialogPositiveClick(id: Int)
}
// Override the Fragment.onAttach() method to instantiate the NoticeDialogListener
@ -27,20 +28,26 @@ class DeleteServerDialogFragment(private val position: Int): DialogFragment() {
listener = context as NoticeDialogListener
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException((context.toString() +
" must implement NoticeDialogListener"))
throw ClassCastException(
(context.toString() +
" must implement NoticeDialogListener")
)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
val builder = AlertDialog.Builder(it)
val builder = MaterialAlertDialogBuilder(it)
builder.setMessage(R.string.delete_question)
.setPositiveButton(R.string.delete
builder.setTitle(R.string.delete_question)
.setMessage(R.string.delete_message)
.setPositiveButton(
R.string.delete
) { _, _ ->
listener.onDialogPositiveClick(this, position)
listener.onDeleteDialogPositiveClick(id)
}
.setNegativeButton(R.string.cancel
.setNegativeButton(
R.string.cancel
) { _, _ ->
dialog?.cancel()
}

View file

@ -0,0 +1,480 @@
package ru.karasevm.privatednstoggle.ui
import android.Manifest
import android.content.ClipData
import android.content.ClipDescription.MIMETYPE_TEXT_PLAIN
import android.content.ClipboardManager
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ShareCompat
import androidx.lifecycle.viewModelScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.DOWN
import androidx.recyclerview.widget.ItemTouchHelper.UP
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import rikka.shizuku.Shizuku
import rikka.shizuku.ShizukuProvider
import ru.karasevm.privatednstoggle.PrivateDNSApp
import ru.karasevm.privatednstoggle.R
import ru.karasevm.privatednstoggle.data.DnsServerViewModel
import ru.karasevm.privatednstoggle.data.DnsServerViewModelFactory
import ru.karasevm.privatednstoggle.databinding.ActivityMainBinding
import ru.karasevm.privatednstoggle.model.DnsServer
import ru.karasevm.privatednstoggle.util.BackupUtils
import ru.karasevm.privatednstoggle.util.PreferenceHelper
import ru.karasevm.privatednstoggle.util.PreferenceHelper.dns_servers
import ru.karasevm.privatednstoggle.util.ShizukuUtil.grantPermissionWithShizuku
class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogListener,
DeleteServerDialogFragment.NoticeDialogListener, Shizuku.OnRequestPermissionResultListener {
private lateinit var linearLayoutManager: LinearLayoutManager
private lateinit var binding: ActivityMainBinding
private lateinit var sharedPrefs: SharedPreferences
private lateinit var adapter: ServerListRecyclerAdapter
private lateinit var clipboard: ClipboardManager
private val dnsServerViewModel: DnsServerViewModel by viewModels { DnsServerViewModelFactory((application as PrivateDNSApp).repository) }
private val itemTouchHelper by lazy {
val simpleItemTouchCallback =
object : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) {
var dragFrom = -1
var dragTo = -1
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
if (dragFrom == viewHolder.bindingAdapterPosition && dragTo == target.bindingAdapterPosition) {
return true
}
// store the drag position
if (dragFrom == -1) dragFrom = viewHolder.bindingAdapterPosition
dragTo = target.bindingAdapterPosition
adapter.onItemMove(
viewHolder.bindingAdapterPosition,
target.bindingAdapterPosition
)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
override fun onSelectedChanged(
viewHolder: RecyclerView.ViewHolder?, actionState: Int
) {
super.onSelectedChanged(viewHolder, actionState)
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
viewHolder?.itemView?.apply {
// Example: Elevate the view
elevation = 8f
alpha = 0.5f
setBackgroundColor(Color.GRAY)
}
}
}
override fun clearView(
recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder
) {
super.clearView(recyclerView, viewHolder)
viewHolder.itemView.apply {
// Reset the appearance
elevation = 0f
alpha = 1.0f
setBackgroundColor(Color.TRANSPARENT)
}
// commit the change to the db
dnsServerViewModel.move(
dragFrom,
dragTo,
(viewHolder as ServerListRecyclerAdapter.DnsServerViewHolder).id
)
dragTo = -1
dragFrom = -1
}
}
ItemTouchHelper(simpleItemTouchCallback)
}
private fun importSettings(json: String) {
runCatching {
val data: BackupUtils.Backup = Json.decodeFromString<BackupUtils.Backup>(json)
BackupUtils.import(data, dnsServerViewModel, sharedPrefs)
}.onSuccess {
Toast.makeText(
this, getString(R.string.import_success), Toast.LENGTH_SHORT
).show()
}.onFailure { exception ->
runCatching {
Log.e("IMPORT", "Malformed json, falling back to legacy", exception)
val data = Json.decodeFromString<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()
}
}
}
/**
* 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()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Shizuku.addRequestPermissionResultListener(this::onRequestPermissionResult)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
linearLayoutManager = LinearLayoutManager(this)
binding.recyclerView.layoutManager = linearLayoutManager
sharedPrefs = PreferenceHelper.defaultPreference(this)
clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
migrateServerList()
adapter = ServerListRecyclerAdapter(true)
binding.recyclerView.adapter = adapter
dnsServerViewModel.allServers.observe(this) { servers ->
adapter.submitList(servers)
if (servers.isEmpty()) {
binding.emptyView.visibility = View.VISIBLE
binding.emptyViewHint.visibility = View.VISIBLE
} else {
binding.emptyView.visibility = View.GONE
binding.emptyViewHint.visibility = View.GONE
}
}
adapter.onItemClick = { id ->
dnsServerViewModel.viewModelScope.launch {
val server = dnsServerViewModel.getById(id)
if (server != null) {
val newFragment =
AddServerDialogFragment(server)
newFragment.show(supportFragmentManager, "edit_server")
}
}
}
adapter.onDragStart = { viewHolder ->
itemTouchHelper.startDrag(viewHolder)
}
binding.floatingActionButton.setOnClickListener {
val newFragment = AddServerDialogFragment(null)
newFragment.show(supportFragmentManager, "add_server")
}
binding.recyclerView.adapter = adapter
itemTouchHelper.attachToRecyclerView(binding.recyclerView)
binding.topAppBar.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.privacy_policy -> {
val browserIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/privacy_policy")
)
startActivity(browserIntent)
true
}
R.id.export_settings_clipboard -> {
dnsServerViewModel.viewModelScope.launch {
val data = BackupUtils.export(dnsServerViewModel, sharedPrefs)
val jsonData = Json.encodeToString(data)
clipboard.setPrimaryClip(ClipData.newPlainText("", jsonData))
// Only show a toast for Android 12 and lower.
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) Toast.makeText(
applicationContext, getString(R.string.copy_success), Toast.LENGTH_SHORT
).show()
}
true
}
R.id.export_settings_share -> {
val activityContext = this
dnsServerViewModel.viewModelScope.launch {
val data = BackupUtils.export(dnsServerViewModel, sharedPrefs)
val jsonData = Json.encodeToString(data)
ShareCompat.IntentBuilder(activityContext).setText(jsonData)
.setType("text/plain")
.startChooser()
}
true
}
R.id.export_settings_file -> {
dnsServerViewModel.viewModelScope.launch {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "text/plain"
putExtra(Intent.EXTRA_TITLE, "private-dns-export")
}
saveResultLauncher.launch(intent)
}
true
}
R.id.import_settings_file -> {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "text/plain"
}
importResultLauncher.launch(intent)
true
}
R.id.import_settings_clipboard -> {
val clipData = clipboard.primaryClip?.getItemAt(0)
val textData = clipData?.text
if (textData != null) {
importSettings(textData.toString())
}
true
}
R.id.options -> {
val newFragment = OptionsDialogFragment()
newFragment.show(supportFragmentManager, "options")
true
}
else -> true
}
}
}
private var saveResultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
val data: Intent? = result.data
data?.data?.also { uri ->
val jsonData = Json.encodeToString(BackupUtils.export(dnsServerViewModel, sharedPrefs))
val contentResolver = applicationContext.contentResolver
runCatching {
contentResolver.openOutputStream(uri)?.use { outputStream ->
outputStream.write(jsonData.toByteArray())
}
}.onFailure { exception ->
Log.e("EXPORT", "Export failed", exception)
Toast.makeText(
this, getString(R.string.export_failure), Toast.LENGTH_SHORT
).show()
}.onSuccess {
Toast.makeText(
this, getString(R.string.export_success), Toast.LENGTH_SHORT
).show()
}
}
}
}
private var importResultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
val data: Intent? = result.data
data?.data?.also { uri ->
val contentResolver = applicationContext.contentResolver
contentResolver.openInputStream(uri)?.use { inputStream ->
val jsonData = inputStream.bufferedReader().use { it.readText() }
importSettings(jsonData)
}
}
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onResume() {
super.onResume()
// Check if WRITE_SECURE_SETTINGS is granted
if (checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) {
// Check if Shizuku is available
if (Shizuku.pingBinder()) {
// check if permission is granted already
val isGranted = if (Shizuku.isPreV11() || Shizuku.getVersion() < 11) {
checkSelfPermission(ShizukuProvider.PERMISSION) == PackageManager.PERMISSION_GRANTED
} else {
Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
}
// request permission if not granted
if (!isGranted && !Shizuku.shouldShowRequestPermissionRationale()) {
if (Shizuku.isPreV11() || Shizuku.getVersion() < 11) {
requestPermissions(arrayOf(ShizukuProvider.PERMISSION), 1)
} else {
Shizuku.requestPermission(1)
}
} else {
grantPermission()
}
} else {
if (checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) {
val browserIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/")
)
Toast.makeText(
this, R.string.shizuku_failure_toast, Toast.LENGTH_SHORT
).show()
startActivity(browserIntent)
finish()
}
}
}
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (!hasFocus) {
// Gets the ID of the "paste" menu item.
val pasteItem = binding.topAppBar.menu.findItem(R.id.import_settings_clipboard)
// If the clipboard doesn't contain data, disable the paste menu item.
// If it does contain data, decide whether you can handle the data.
pasteItem.isEnabled = when {
!clipboard.hasPrimaryClip() -> false
!(clipboard.primaryClipDescription?.hasMimeType(MIMETYPE_TEXT_PLAIN))!! -> false
else -> true
}
}
}
override fun onDestroy() {
super.onDestroy()
Shizuku.removeRequestPermissionResultListener(this::onRequestPermissionResult)
}
/**
* Show the dialog for deleting the server
* @param id The server id
*/
override fun onDeleteItemClicked(id: Int) {
val newFragment = DeleteServerDialogFragment(id)
newFragment.show(supportFragmentManager, "delete_server")
}
/**
* Callback for adding the server
* @param label The label
* @param server The server
*/
override fun onAddDialogPositiveClick(label: String?, server: String) {
if (server.isEmpty()) {
Toast.makeText(this, R.string.server_length_error, Toast.LENGTH_SHORT).show()
return
}
if (label.isNullOrEmpty()) {
dnsServerViewModel.insert(DnsServer(0, server))
} else {
dnsServerViewModel.insert(DnsServer(0, server, label))
}
}
/**
* Callback for deleting the server
* @param id The server id
*/
override fun onDeleteDialogPositiveClick(id: Int) {
dnsServerViewModel.delete(id)
}
/**
* Callback for updating the server
* @param label New label
* @param server New server address
* @param id The server id
*/
override fun onUpdateDialogPositiveClick(
id: Int,
server: String,
label: String?,
enabled: Boolean
) {
if (server.isEmpty()) {
Toast.makeText(this, R.string.server_length_error, Toast.LENGTH_SHORT).show()
return
}
dnsServerViewModel.update(id, server, label, null, enabled)
}
private fun grantPermission() {
if (grantPermissionWithShizuku(this)) {
Toast.makeText(
this, R.string.shizuku_success_toast, Toast.LENGTH_SHORT
).show()
} else {
Toast.makeText(
this, R.string.shizuku_failure_toast, Toast.LENGTH_SHORT
).show()
val browserIntent = Intent(
Intent.ACTION_VIEW, Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/")
)
startActivity(browserIntent)
finish()
}
}
override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) {
val isGranted = grantResult == PackageManager.PERMISSION_GRANTED
if (!isGranted && checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) {
val browserIntent = Intent(
Intent.ACTION_VIEW, Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/")
)
startActivity(browserIntent)
finish()
}
}
}

View file

@ -0,0 +1,63 @@
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.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?
): Dialog {
return activity?.let {
val builder = MaterialAlertDialogBuilder(it)
val inflater = requireActivity().layoutInflater
_binding = DialogOptionsBinding.inflate(inflater)
val view = binding.root
builder.setTitle(R.string.options)
.setView(view)
.setPositiveButton(R.string.ok, null)
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
override fun onStart() {
super.onStart()
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)
PrivateDNSUtils.AUTO_MODE_OPTION_OFF_AUTO -> binding.autoOptionRadioGroup.check(R.id.autoOptionOffAuto)
PrivateDNSUtils.AUTO_MODE_OPTION_PRIVATE -> binding.autoOptionRadioGroup.check(R.id.autoOptionPrivate)
}
binding.autoOptionRadioGroup.setOnCheckedChangeListener { _, checkedId ->
when (checkedId) {
R.id.autoOptionOff -> sharedPreferences.autoMode = PrivateDNSUtils.AUTO_MODE_OPTION_OFF
R.id.autoOptionAuto -> sharedPreferences.autoMode = PrivateDNSUtils.AUTO_MODE_OPTION_AUTO
R.id.autoOptionOffAuto -> sharedPreferences.autoMode =
PrivateDNSUtils.AUTO_MODE_OPTION_OFF_AUTO
R.id.autoOptionPrivate -> sharedPreferences.autoMode =
PrivateDNSUtils.AUTO_MODE_OPTION_PRIVATE
}
}
val requireUnlock = sharedPreferences.requireUnlock
binding.requireUnlockSwitch.isChecked = requireUnlock
binding.requireUnlockSwitch.setOnCheckedChangeListener { _, 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

@ -0,0 +1,12 @@
package ru.karasevm.privatednstoggle.ui
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
class SettingsDialogActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val newFragment = DNSServerDialogFragment()
newFragment.show(supportFragmentManager, DNSServerDialogFragment.TAG)
}
}

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

@ -0,0 +1,52 @@
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 androidx.core.content.ContextCompat.checkSelfPermission
@Suppress("unused")
object PrivateDNSUtils {
const val DNS_MODE_OFF = "off"
const val DNS_MODE_AUTO = "opportunistic"
const val DNS_MODE_PRIVATE = "hostname"
const val AUTO_MODE_OPTION_OFF = 0
const val AUTO_MODE_OPTION_AUTO = 1
const val AUTO_MODE_OPTION_OFF_AUTO = 2
const val AUTO_MODE_OPTION_PRIVATE = 3
private const val PRIVATE_DNS_MODE = "private_dns_mode"
private const val PRIVATE_DNS_PROVIDER = "private_dns_specifier"
// Gets the system dns mode
fun getPrivateMode(contentResolver: ContentResolver): String {
return Settings.Global.getString(contentResolver, PRIVATE_DNS_MODE)
}
// Gets the system dns provider
fun getPrivateProvider(contentResolver: ContentResolver): String {
return Settings.Global.getString(contentResolver, PRIVATE_DNS_PROVIDER)
}
// Sets the system dns mode
fun setPrivateMode(contentResolver: ContentResolver, value: String) {
Settings.Global.putString(contentResolver, PRIVATE_DNS_MODE, value)
}
// Sets the system dns provider
fun setPrivateProvider(contentResolver: ContentResolver, value: String?) {
Settings.Global.putString(contentResolver, PRIVATE_DNS_PROVIDER, value)
}
fun checkForPermission(context: Context): Boolean {
return checkSelfPermission(
context,
Manifest.permission.WRITE_SECURE_SETTINGS
) == PackageManager.PERMISSION_GRANTED
}
}

View file

@ -0,0 +1,57 @@
package ru.karasevm.privatednstoggle.util
import android.content.Context
import android.content.SharedPreferences
object PreferenceHelper {
const val DNS_SERVERS = "dns_servers"
const val AUTO_MODE = "auto_mode"
const val REQUIRE_UNLOCK = "require_unlock"
fun defaultPreference(context: Context): SharedPreferences =
context.getSharedPreferences("app_prefs", 0)
private inline fun SharedPreferences.editMe(operation: (SharedPreferences.Editor) -> Unit) {
val editMe = edit()
operation(editMe)
editMe.apply()
}
private fun SharedPreferences.Editor.put(pair: Pair<String, Any>) {
val key = pair.first
when (val value = pair.second) {
is String -> putString(key, value)
is Int -> putInt(key, value)
is Boolean -> putBoolean(key, value)
is Long -> putLong(key, value)
is Float -> putFloat(key, value)
else -> error("Only primitive types can be stored in SharedPreferences, got ${value.javaClass}")
}
}
var SharedPreferences.dns_servers
get() = getString(DNS_SERVERS, "")!!.split(",").toMutableList()
set(items) {
editMe {
it.put(DNS_SERVERS to items.joinToString(separator = ","))
}
}
var SharedPreferences.autoMode
get() = getInt(AUTO_MODE, PrivateDNSUtils.AUTO_MODE_OPTION_OFF)
set(value) {
editMe {
it.put(AUTO_MODE to value)
}
}
var SharedPreferences.requireUnlock
get() = getBoolean(REQUIRE_UNLOCK, false)
set(value) {
editMe {
it.put(REQUIRE_UNLOCK 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,15 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<group android:scaleX="0.92"
android:scaleY="0.92"
android:translateX="0.96"
android:translateY="0.96">
<path
android:fillColor="@android:color/white"
android:pathData="M11.8,10.9c-2.27,-0.59 -3,-1.2 -3,-2.15 0,-1.09 1.01,-1.85 2.7,-1.85 1.78,0 2.44,0.85 2.5,2.1h2.21c-0.07,-1.72 -1.12,-3.3 -3.21,-3.81V3h-3v2.16c-1.94,0.42 -3.5,1.68 -3.5,3.61 0,2.31 1.91,3.46 4.7,4.13 2.5,0.6 3,1.48 3,2.41 0,0.69 -0.49,1.79 -2.7,1.79 -2.06,0 -2.87,-0.92 -2.98,-2.1h-2.2c0.12,2.19 1.76,3.42 3.68,3.83V21h3v-2.15c1.95,-0.37 3.5,-1.5 3.5,-3.55 0,-2.84 -2.43,-3.81 -4.7,-4.4z"/>
</group>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 685 B

View file

@ -4,10 +4,10 @@
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,6c2.62,0 4.88,1.86 5.39,4.43l0.3,1.5 1.53,0.11c1.56,0.1 2.78,1.41 2.78,2.96 0,1.65 -1.35,3 -3,3H6c-2.21,0 -4,-1.79 -4,-4 0,-2.05 1.53,-3.76 3.56,-3.97l1.07,-0.11 0.5,-0.95C8.08,7.14 9.94,6 12,6m0,-2C9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96C18.67,6.59 15.64,4 12,4z"
android:pathData="M13.925,14.369L10.121,14.369L9.267,16.738L8.032,16.738L11.499,7.659h1.048l3.473,9.079h-1.228zM10.483,13.383h3.087L12.023,9.137Z"
android:strokeWidth="1.06419"
android:fillColor="#000000"/>
<path
android:pathData="m11.1286,17.2286h1.7429L12.8714,15.4857L11.1286,15.4857ZM12,6.7714c-1.9259,0 -3.4857,1.5599 -3.4857,3.4857h1.7429c0,-0.9586 0.7843,-1.7429 1.7429,-1.7429 0.9586,0 1.7429,0.7843 1.7429,1.7429 0,1.7429 -2.6143,1.525 -2.6143,4.3571h1.7429c0,-1.9607 2.6143,-2.1786 2.6143,-4.3571 0,-1.9259 -1.5599,-3.4857 -3.4857,-3.4857z"
android:strokeWidth="0.871429"
android:pathData="m12,6c2.62,0 4.88,1.86 5.39,4.43l0.3,1.5 1.53,0.11c1.56,0.1 2.78,1.41 2.78,2.96 0,1.65 -1.35,3 -3,3H6C3.79,18 2,16.21 2,14 2,11.95 3.53,10.24 5.56,10.03L6.63,9.92 7.13,8.97C8.08,7.14 9.94,6 12,6M12,4C9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96C18.67,6.59 15.64,4 12,4Z"
android:fillColor="#000000"/>
</vector>

View file

@ -1,4 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
<vector android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="@android:color/white"
android:pathData="M17.1 10.9c0-0.3 0.1-0.6 0.1-0.9s0-0.6-0.1-0.9l2-1.6c0.2-0.1 0.2-0.4 0.1-0.6l-1.9-3.3c-0.1-0.2-0.4-0.3-0.6-0.2l-2.4 1c-0.5-0.4-1-0.7-1.6-0.9l-0.4-2.5c0-0.2-0.2-0.4-0.5-0.4h-3.8c-0.2 0-0.4 0.2-0.5 0.4l-0.4 2.5c-0.6 0.2-1.1 0.6-1.6 0.9l-2.4-1c-0.2-0.1-0.5 0-0.6 0.2l-1.9 3.3c-0.1 0.2-0.1 0.5 0.1 0.6l2 1.6c-0.1 0.3-0.1 0.6-0.1 0.9s0 0.6 0.1 0.9l-2 1.6c-0.2 0.1-0.2 0.4-0.1 0.6l1.9 3.3c0.1 0.2 0.4 0.3 0.6 0.2l2.4-1c0.5 0.4 1 0.7 1.6 0.9l0.4 2.5c0.1 0.2 0.2 0.4 0.5 0.4h3.8c0.2 0 0.4-0.2 0.5-0.4l0.4-2.5c0.6-0.2 1.1-0.6 1.6-0.9l2.4 1c0.2 0.1 0.5 0 0.6-0.2l1.9-3.3c0.1-0.2 0.1-0.5-0.1-0.6l-2-1.6zm-7.1 2.7c-2 0-3.6-1.6-3.6-3.6s1.6-3.6 3.6-3.6s3.6 1.6 3.6 3.6s-1.6 3.6-3.6 3.6z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M20,9H4v2h16V9zM4,15h16v-2H4v2z"/>
</vector>

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,6c2.62,0 4.88,1.86 5.39,4.43l0.3,1.5 1.53,0.11c1.56,0.1 2.78,1.41 2.78,2.96 0,1.65 -1.35,3 -3,3H6c-2.21,0 -4,-1.79 -4,-4 0,-2.05 1.53,-3.76 3.56,-3.97l1.07,-0.11 0.5,-0.95C8.08,7.14 9.94,6 12,6m0,-2C9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96C18.67,6.59 15.64,4 12,4z"
android:fillColor="#000000"/>
<path
android:pathData="m11.1286,17.2286h1.7429L12.8714,15.4857L11.1286,15.4857ZM12,6.7714c-1.9259,0 -3.4857,1.5599 -3.4857,3.4857h1.7429c0,-0.9586 0.7843,-1.7429 1.7429,-1.7429 0.9586,0 1.7429,0.7843 1.7429,1.7429 0,1.7429 -2.6143,1.525 -2.6143,4.3571h1.7429c0,-1.9607 2.6143,-2.1786 2.6143,-4.3571 0,-1.9259 -1.5599,-3.4857 -3.4857,-3.4857z"
android:strokeWidth="0.871429"
android:fillColor="#000000"/>
</vector>

View file

@ -4,12 +4,65 @@
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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintLeft_toRightOf="parent"
app:layout_constraintRight_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/topAppBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
app:menu="@menu/menu_main"
app:title="@string/app_name" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout_editor_absoluteX="1dp"
tools:layout_editor_absoluteY="1dp" />
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/topAppBarLayout" />
<TextView
android:id="@+id/empty_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/no_servers_added"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.45" />
<TextView
android:id="@+id/empty_view_hint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/empty_hint"
android:textColor="@color/material_on_surface_emphasis_medium"
android:visibility="visible"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/empty_view" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/floating_action_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/add_server"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:srcCompat="@drawable/ic_baseline_add_24" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -4,18 +4,60 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/editTextServerAddr"
android:layout_width="0dp"
android:layout_height="45dp"
android:layout_margin="24dp"
android:ems="10"
android:maxLines="1"
android:inputType="textNoSuggestions"
android:importantForAutofill="no"
<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"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:hint="@string/add_edittext_label_hint"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/serverEnabledSwitch">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextServerHint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:importantForAutofill="no"
android:inputType="textNoSuggestions"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/editTextServerAddrLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:hint="@string/add_edittext_hint"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toBottomOf="@+id/editTextServerHintLayout">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextServerAddr"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:importantForAutofill="no"
android:inputType="textNoSuggestions"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RadioGroup
android:id="@+id/autoOptionRadioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="17dp"
android:paddingTop="10dp"
android:paddingEnd="17dp">
<TextView
android:id="@+id/autoOptionHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="7dp"
android:text="@string/auto_option_description"
android:textSize="16sp" />
<RadioButton
android:id="@+id/autoOptionOff"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/auto_option_only_off" />
<RadioButton
android:id="@+id/autoOptionAuto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/auto_option_only_auto" />
<RadioButton
android:id="@+id/autoOptionOffAuto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/auto_option_off_and_auto" />
<RadioButton
android:id="@+id/autoOptionPrivate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/auto_option_only_private" />
</RadioGroup>
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginVertical="5dp" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/requireUnlockSwitch"
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" />
</LinearLayout>

View file

@ -1,22 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackground"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:clickable="true"
android:focusable="true">
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground">
<TextView
android:id="@+id/textView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:text="TextView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:textSize="18sp"
app:layout_constraintBottom_toTopOf="@id/labelTextView"
app:layout_constraintEnd_toStartOf="@+id/dragHandle"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/labelTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/dragHandle"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textView" />
<ImageView
android:id="@+id/dragHandle"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginEnd="32dp"
android:contentDescription="@string/a11y_drag_handle"
android:src="@drawable/ic_drag_handle_24"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/bottom_sheet"
style="@style/Widget.Material3.BottomSheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxWidth="300dp" />
</LinearLayout>
</LinearLayout>

View file

@ -1,11 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:id="@+id/add_server"
android:icon="@drawable/ic_baseline_add_24"
android:title="@string/add"
app:showAsAction="ifRoom" />
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/options"
android:title="@string/options"
app:showAsAction="ifRoom"
android:icon="@drawable/ic_baseline_settings_24"
/>
<item android:id="@+id/import_settings_submenu"
android:title="@string/menu_import"
app:showAsAction="never" >
<menu>
<item android:id="@+id/import_settings_file"
android:title="@string/menu_import_from_file"
app:showAsAction="never" />
<item android:id="@+id/import_settings_clipboard"
android:title="@string/menu_import_from_clipboard"
app:showAsAction="never" />
</menu>
</item>
<item android:id="@+id/export_settings"
android:title="@string/menu_export"
app:showAsAction="never" >
<menu>
<item android:id="@+id/export_settings_clipboard"
android:title="@string/menu_export_to_clipboard"
app:showAsAction="never" />
<item android:id="@+id/export_settings_share"
android:title="@string/menu_export_share"
app:showAsAction="never" />
<item android:id="@+id/export_settings_file"
android:title="@string/menu_export_to_file"
app:showAsAction="never" />
</menu>
</item>
<item android:id="@+id/privacy_policy"
android:title="@string/privacy_policy"
android:title="@string/menu_privacy_policy"
app:showAsAction="never" />
</menu>

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

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

@ -1,16 +1,12 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.MyApplication" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
<resources>
<style name="Theme.MyApplication" parent="Theme.Material3.Dark.NoActionBar">
<item name="colorPrimary">@color/md_theme_dark_primary</item>
<item name="colorSecondary">@color/md_theme_dark_secondary</item>
<item name="colorTertiary">@color/md_theme_dark_tertiary</item>
<item name="android:colorBackground">@color/md_theme_dark_background</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">false</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
</style>
</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

@ -1,10 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<color name="ic_launcher_background">#498EE8</color>
<color name="seed" tools:keep="@color/seed">#6750A4</color>
<color name="md_theme_light_primary">#6750A4</color>
<color name="md_theme_light_secondary">#625B71</color>
<color name="md_theme_light_tertiary">#7D5260</color>
<color name="md_theme_light_background">#FFFBFE</color>
<color name="md_theme_dark_primary">#D0BCFF</color>
<color name="md_theme_dark_secondary">#CCC2DC</color>
<color name="md_theme_dark_tertiary">#EFB8C8</color>
<color name="md_theme_dark_background">#1C1B1F</color>
</resources>

View file

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#498EE8</color>
</resources>

View file

@ -4,13 +4,50 @@
<string name="permission_missing">Permission not granted, check app to see how to do it</string>
<string name="dns_off">Off</string>
<string name="dns_auto">Auto</string>
<string name="dns_unknown">Unknown</string>
<string name="add_server">Add Server</string>
<string name="add">Add</string>
<string name="menu_add">Add</string>
<string name="menu_save">Save</string>
<string name="menu_privacy_policy">Privacy Policy</string>
<string name="select_server">Select Server</string>
<string name="done">Done</string>
<string name="cancel">Cancel</string>
<string name="delete_question">Delete server?</string>
<string name="delete_question">Delete</string>
<string name="delete_message">Are you sure you want to delete server?</string>
<string name="delete">Delete</string>
<string name="server_length_error">Server address cannot be empty</string>
<string name="privacy_policy">Privacy Policy</string>
<string name="add_edittext_label_hint">DNS server Label (Optional)</string>
<string name="add_edittext_hint">DNS server address</string>
<string name="options">Options</string>
<string name="ok">OK</string>
<string name="auto_option_description">Choose which options to include in the tile</string>
<string name="auto_option_only_off">Only off</string>
<string name="auto_option_only_auto">Only auto</string>
<string name="auto_option_off_and_auto">Off and auto</string>
<string name="auto_option_only_private">Only Private DNS</string>
<string name="open_app">Open app</string>
<string name="set_to_off_toast">Private DNS turned off</string>
<string name="set_to_auto_toast">Private DNS set to auto</string>
<string name="set_to_provider_toast">Private DNS set to %1$s</string>
<string name="require_unlock_setting">Require unlocking the device to change server</string>
<string name="a11y_drag_handle">Drag handle</string>
<string name="menu_import">Import</string>
<string name="menu_export">Export</string>
<string name="import_success">Imported</string>
<string name="import_failure">Import failed</string>
<string name="import_failure_json">Import failed, malformed JSON</string>
<string name="copy_success">Copied</string>
<string name="menu_import_from_file">From file</string>
<string name="menu_import_from_clipboard">From clipboard</string>
<string name="menu_export_to_clipboard">To clipboard</string>
<string name="menu_export_share">Share</string>
<string name="menu_export_to_file">To file</string>
<string name="export_failure">Saving failed</string>
<string name="export_success">Saved successfully</string>
<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,16 +1,27 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.MyApplication" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
<resources>
<style name="Theme.MyApplication" parent="Theme.Material3.Light.NoActionBar">
<item name="colorPrimary">@color/md_theme_light_primary</item>
<item name="colorSecondary">@color/md_theme_light_secondary</item>
<item name="colorTertiary">@color/md_theme_light_tertiary</item>
<item name="android:colorBackground">@color/md_theme_light_background</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightNavigationBar">true</item>
</style>
<style name="Theme.Transparent" parent="Theme.Material3.DayNight.NoActionBar">
<item name="android:background">@android:color/transparent</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowAnimationStyle">@null</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
</style>
</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.1.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.buildDir
}

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,2 @@
- Add option to require unlocking the device to use the tile
- Fix invisible nav buttons on some devices

View file

@ -0,0 +1,4 @@
- Support for selection of only Private DNS in Option Dialog by @InfiniteCoder06
- Support Labels by @InfiniteCoder06
- Feature: Reordring by @InfiniteCoder06
- Possible tile update fix

View file

@ -0,0 +1,2 @@
- Settings export/import
- Fix label not appearing in some cases

View file

@ -0,0 +1,3 @@
- Add an option to edit servers
- Add placeholder for empty server list
- Fix layout for longer server addresses

View file

@ -0,0 +1,2 @@
- Fix crashes on Android 11 and earlier
- Fix list entry layout

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

@ -0,0 +1,2 @@
Changelog for latest release is available on GitHub:
https://github.com/karasevm/PrivateDNSAndroid/releases/latest

View file

@ -0,0 +1,15 @@
<p>
Private DNS Quick Toggle is a quick settings tile to switch your private
dns provider.
Supports any number of providers. Makes it easy to turn ad-blocking
dns servers on or off with just a single tap.
</p>
<b>Permissions</b>
<p>
Requires WRITE_SECURE_SETTINGS permission to change the private dns settings.
The permission must be provided either with Shizuku or
<a href="https://karasevm.github.io/PrivateDNSAndroid/">
manually through adb
</a>.
</p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1 @@
Quick settings tile to switch active private DNS server

View file

@ -0,0 +1 @@
Private DNS Quick Toggle

View file

@ -16,9 +16,9 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
android.enableJetifier=false
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
android.nonTransitiveRClass=true
android.nonFinalResIds=true
org.gradle.configuration-cache=true

Some files were not shown because too many files have changed in this diff Show more