Compare commits
95 commits
Author | SHA1 | Date | |
---|---|---|---|
|
bd61fe6748 | ||
|
87fe66af20 | ||
|
c375776575 | ||
|
3b24d24ba4 | ||
|
e8885409b5 | ||
|
2aa895cd5e | ||
|
bdd98109ed | ||
|
3b09f605f9 | ||
|
9d62e91b60 | ||
|
e155d17dd7 | ||
|
e2104952bc | ||
|
24800f7f2d | ||
|
6a5f2af6f6 | ||
|
0e0e0bf9b4 | ||
|
4c6240bd34 | ||
|
e4b9e84f8c | ||
|
a379c81cb9 | ||
|
6164a35f04 | ||
|
e4498ca64a | ||
|
75413fddcd | ||
|
6cc99b7820 | ||
|
efd48b8984 | ||
|
b39d7e3624 | ||
|
4f6dc13c12 | ||
|
5ba03acbcc | ||
|
627771d4b1 | ||
|
8c7ff2ca8f | ||
|
402b084954 | ||
|
8c927d6b26 | ||
|
7cdc2bbb84 | ||
|
835e9381ea | ||
|
14b320ac68 | ||
|
681e6ceef4 | ||
|
6a5f405211 | ||
|
fa9d259a21 | ||
|
36be36d69b | ||
|
eead2a912d | ||
|
95f778a787 | ||
|
53ab655eae | ||
|
af61a85f9e | ||
|
0028d72095 | ||
|
470f8445f9 | ||
|
5fe2354e7d | ||
|
213d3e4dee | ||
|
a6ed85e2c8 | ||
|
66a83b3dac | ||
|
3a697e32e3 | ||
|
6caa1432aa | ||
|
7dd0fbf802 | ||
|
5c38ec2db8 | ||
|
29957ef908 | ||
|
6b7324a2a8 | ||
|
08a601533e | ||
|
17f62ac892 | ||
|
baaffd334d | ||
|
c85a96fdcf | ||
|
c24f8e0b38 | ||
|
01650977c3 | ||
|
d9ad7d2030 | ||
|
52fc30e96d | ||
|
f0cc3171ba | ||
|
610444bde3 | ||
|
391842244c | ||
|
2d6dd3a4e5 | ||
|
e04f1246f6 | ||
|
0ee473d320 | ||
|
3275548201 | ||
|
347781b61e | ||
|
65b75d53dd | ||
|
0c208ad745 | ||
|
6efc52c870 | ||
|
5bd8d2c122 | ||
|
1a9573bb41 | ||
|
fdd711eef4 | ||
|
c5f2aed736 | ||
|
48bb2698bd | ||
|
8965ba6a3d | ||
|
85d6a7410c | ||
|
9aabfa6261 | ||
|
96e345606e | ||
|
e50b99e673 | ||
|
42a6220de3 | ||
|
aefa31b8f4 | ||
|
f6ddc85b43 | ||
|
afa2398188 | ||
|
c5a5be4ed4 | ||
|
2ab8354116 | ||
|
989f340fc2 | ||
|
51774a1675 | ||
|
936fdc5121 | ||
|
c1e563dcee | ||
|
30bd02d8ff | ||
|
04cc8ccf02 | ||
|
1b321af43e | ||
|
5d072ed0af |
73
.github/ISSUE_TEMPLATE/01-bug-report.yml
vendored
Normal 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.
|
30
.github/ISSUE_TEMPLATE/02-feature-request.yml
vendored
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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>
|
29
README.md
|
@ -1,17 +1,23 @@
|
|||
[](https://github.com/karasevm/PrivateDNSAndroid/releases/latest)
|
||||
[](https://github.com/karasevm/PrivateDNSAndroid/releases/latest)
|
||||
[](https://apt.izzysoft.de/fdroid/index/apk/ru.karasevm.privatednstoggle)
|
||||
[](https://hosted.weblate.org/engage/privatednsandroid/)
|
||||
|
||||
# Private DNS Quick Toggle
|
||||
A quick settings tile to switch your private dns provider. Supports any number of providers. Makes it easy to turn adblocking dns servers on or off with just
|
||||
a single tap.
|
||||
|
||||

|
||||

|
||||
|
||||
## 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)
|
||||
|
|
|
@ -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
|
@ -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")
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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>
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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]
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package ru.karasevm.privatednstoggle.data
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import ru.karasevm.privatednstoggle.model.DnsServer
|
||||
|
||||
@Dao
|
||||
interface DnsServerDao {
|
||||
|
||||
@Query("SELECT * FROM dns_servers ORDER BY sortOrder ASC")
|
||||
fun getAll(): Flow<List<DnsServer>>
|
||||
|
||||
@Query("SELECT * FROM dns_servers WHERE enabled = 1 ORDER BY sortOrder ASC LIMIT 1")
|
||||
suspend fun getFirstEnabled(): DnsServer
|
||||
|
||||
@Query("SELECT * FROM dns_servers WHERE server = :server LIMIT 1")
|
||||
suspend fun getFirstByServer(server: String): DnsServer?
|
||||
|
||||
@Query("SELECT * FROM dns_servers WHERE id = :id")
|
||||
suspend fun getById(id: Int): DnsServer?
|
||||
|
||||
@Query("SELECT * FROM dns_servers " +
|
||||
"WHERE sortOrder > (SELECT sortOrder FROM dns_servers WHERE server = :server) AND enabled = 1 " +
|
||||
"ORDER BY sortOrder ASC " +
|
||||
"LIMIT 1")
|
||||
suspend fun getNextEnabledByServer(server: String): DnsServer?
|
||||
|
||||
@Query("DELETE FROM dns_servers")
|
||||
suspend fun deleteAll()
|
||||
|
||||
@Query("DELETE FROM dns_servers WHERE id = :id")
|
||||
suspend fun deleteById(id: Int)
|
||||
|
||||
@Query("UPDATE dns_servers SET sortOrder = sortOrder + 1 " +
|
||||
"WHERE sortOrder >= :startSortOrder AND sortOrder <= :endSortOrder")
|
||||
suspend fun incrementSortOrder(startSortOrder: Int, endSortOrder: Int = Int.MAX_VALUE)
|
||||
|
||||
@Query("UPDATE dns_servers SET sortOrder = sortOrder - 1 " +
|
||||
"WHERE sortOrder >= :startSortOrder AND sortOrder <= :endSortOrder")
|
||||
suspend fun decrementSortOrder(startSortOrder: Int, endSortOrder: Int = Int.MAX_VALUE)
|
||||
|
||||
@Query("UPDATE dns_servers SET sortOrder = sortOrder - 1 " +
|
||||
"WHERE sortOrder > (SELECT sortOrder FROM dns_servers WHERE id = :id)")
|
||||
suspend fun decrementSortOrderById(id: Int)
|
||||
|
||||
@Transaction
|
||||
suspend fun deleteAndDecrement(id: Int) {
|
||||
decrementSortOrderById(id)
|
||||
deleteById(id)
|
||||
}
|
||||
|
||||
@Query("UPDATE dns_servers SET label = :label WHERE id = :id")
|
||||
suspend fun updateLabel(id: Int, label: String)
|
||||
|
||||
@Query("UPDATE dns_servers SET server = :server WHERE id = :id")
|
||||
suspend fun updateServer(id: Int, server: String)
|
||||
|
||||
@Query("UPDATE dns_servers " +
|
||||
"SET server = COALESCE(:server, server), " +
|
||||
" label = COALESCE(:label, label), " +
|
||||
" sortOrder = COALESCE(:sortOrder, sortOrder), " +
|
||||
" enabled = COALESCE(:enabled, enabled) " +
|
||||
"WHERE id = :id")
|
||||
suspend fun update(id: Int, server: String?, label: String?, sortOrder: Int?, enabled: Boolean?)
|
||||
|
||||
@Transaction
|
||||
suspend fun moveUp(sortOrder: Int, newSortOrder: Int, id: Int){
|
||||
incrementSortOrder(newSortOrder, sortOrder)
|
||||
update(id, null, null, newSortOrder, null)
|
||||
}
|
||||
|
||||
@Transaction
|
||||
suspend fun moveDown(sortOrder: Int, newSortOrder: Int, id: Int){
|
||||
decrementSortOrder(sortOrder, newSortOrder)
|
||||
update(id, null, null, newSortOrder, null)
|
||||
}
|
||||
|
||||
@Query("INSERT INTO dns_servers(server, label, sortOrder, enabled) " +
|
||||
"VALUES(:server, :label, COALESCE((SELECT MAX(sortOrder) + 1 FROM dns_servers), 0), :enabled)")
|
||||
suspend fun insert(server: String, label: String, enabled: Boolean)
|
||||
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package ru.karasevm.privatednstoggle.data
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import ru.karasevm.privatednstoggle.model.DnsServer
|
||||
|
||||
class DnsServerRepository(private val dnsServerDao: DnsServerDao) {
|
||||
|
||||
val allServers: Flow<List<DnsServer>> = dnsServerDao.getAll()
|
||||
|
||||
@WorkerThread
|
||||
fun getAll() = dnsServerDao.getAll()
|
||||
|
||||
@WorkerThread
|
||||
suspend fun getFirstEnabled() = dnsServerDao.getFirstEnabled()
|
||||
|
||||
@WorkerThread
|
||||
suspend fun getById(id: Int) = dnsServerDao.getById(id)
|
||||
|
||||
@WorkerThread
|
||||
suspend fun getFirstByServer(server: String) = dnsServerDao.getFirstByServer(server)
|
||||
|
||||
@WorkerThread
|
||||
suspend fun getNextByServer(server: String) = dnsServerDao.getNextEnabledByServer(server)
|
||||
|
||||
@WorkerThread
|
||||
suspend fun insert(dnsServer: DnsServer) {
|
||||
dnsServerDao.insert(dnsServer.server, dnsServer.label, dnsServer.enabled)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
suspend fun update(
|
||||
id: Int,
|
||||
server: String?,
|
||||
label: String?,
|
||||
sortOrder: Int?,
|
||||
enabled: Boolean?
|
||||
) {
|
||||
dnsServerDao.update(id, server, label, sortOrder, enabled)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
suspend fun move(sortOrder: Int, newSortOrder: Int, id: Int) {
|
||||
if (sortOrder == newSortOrder) {
|
||||
return
|
||||
}
|
||||
if (newSortOrder > sortOrder) {
|
||||
dnsServerDao.moveDown(sortOrder, newSortOrder, id)
|
||||
} else {
|
||||
dnsServerDao.moveUp(sortOrder, newSortOrder, id)
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
suspend fun delete(id: Int) {
|
||||
dnsServerDao.deleteAndDecrement(id)
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package ru.karasevm.privatednstoggle.data
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.karasevm.privatednstoggle.model.DnsServer
|
||||
|
||||
class DnsServerViewModel(private val dnsServerRepository: DnsServerRepository) : ViewModel() {
|
||||
|
||||
val allServers: LiveData<List<DnsServer>> = dnsServerRepository.allServers.asLiveData()
|
||||
|
||||
fun getAll() = dnsServerRepository.getAll()
|
||||
|
||||
suspend fun getById(id: Int) = dnsServerRepository.getById(id)
|
||||
|
||||
fun insert(dnsServer: DnsServer) =
|
||||
viewModelScope.launch {
|
||||
dnsServerRepository.insert(dnsServer)
|
||||
}
|
||||
|
||||
fun update(
|
||||
id: Int,
|
||||
server: String? = null,
|
||||
label: String? = null,
|
||||
sortOrder: Int? = null,
|
||||
enabled: Boolean? = null
|
||||
) = viewModelScope.launch { dnsServerRepository.update(id, server, label, sortOrder, enabled) }
|
||||
|
||||
fun move(sortOrder: Int, newSortOrder: Int, id: Int) =
|
||||
viewModelScope.launch { dnsServerRepository.move(sortOrder, newSortOrder, id) }
|
||||
|
||||
fun delete(id: Int) = viewModelScope.launch { dnsServerRepository.delete(id) }
|
||||
|
||||
}
|
||||
|
||||
class DnsServerViewModelFactory(private val dnsServerRepository: DnsServerRepository) :
|
||||
ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
if (modelClass.isAssignableFrom(DnsServerViewModel::class.java)) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return DnsServerViewModel(dnsServerRepository) as T
|
||||
}
|
||||
throw IllegalArgumentException("Unknown ViewModel class")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package ru.karasevm.privatednstoggle.data.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import ru.karasevm.privatednstoggle.data.DnsServerDao
|
||||
import ru.karasevm.privatednstoggle.model.DnsServer
|
||||
|
||||
@Database(entities = [DnsServer::class], version = 1, exportSchema = false)
|
||||
abstract class DnsServerRoomDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun dnsServerDao(): DnsServerDao
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var INSTANCE: DnsServerRoomDatabase? = null
|
||||
fun getDatabase(context: Context): DnsServerRoomDatabase {
|
||||
val tempInstance = INSTANCE
|
||||
if (tempInstance != null) {
|
||||
return tempInstance
|
||||
}
|
||||
synchronized(this) {
|
||||
val instance = Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
DnsServerRoomDatabase::class.java,
|
||||
"dns_server_database"
|
||||
).build()
|
||||
INSTANCE = instance
|
||||
return instance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,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
|
||||
)
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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>
|
Before Width: | Height: | Size: 383 B |
Before Width: | Height: | Size: 296 B |
Before Width: | Height: | Size: 507 B |
Before Width: | Height: | Size: 685 B |
|
@ -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>
|
||||
|
|
|
@ -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"/>
|
||||
|
|
10
app/src/main/res/drawable/ic_baseline_settings_24.xml
Normal 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>
|
10
app/src/main/res/drawable/ic_drag_handle_24.xml
Normal 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>
|
13
app/src/main/res/drawable/ic_unknown_black_24dp.xml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
65
app/src/main/res/layout/dialog_options.xml
Normal 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>
|
|
@ -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>
|
22
app/src/main/res/layout/sheet_dns_selector.xml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3 KiB |
Before Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 13 KiB |
|
@ -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>
|
1
app/src/main/res/resources.properties
Normal file
|
@ -0,0 +1 @@
|
|||
unqualifiedResLocale=en-US
|
54
app/src/main/res/values-fr/strings.xml
Normal 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>
|
51
app/src/main/res/values-hu/strings.xml
Normal 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>
|
49
app/src/main/res/values-mn/strings.xml
Normal 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>
|
|
@ -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>
|
54
app/src/main/res/values-pl/strings.xml
Normal 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>
|
54
app/src/main/res/values-pt-rBR/strings.xml
Normal 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>
|
53
app/src/main/res/values-ru/strings.xml
Normal 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>
|
54
app/src/main/res/values-ta/strings.xml
Normal 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>
|
54
app/src/main/res/values-tr/strings.xml
Normal 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ı açı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>
|
52
app/src/main/res/values-vi/strings.xml
Normal 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>
|
51
app/src/main/res/values-zh-rCN/strings.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#498EE8</color>
|
||||
</resources>
|
|
@ -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>
|
|
@ -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>
|
18
build.gradle
|
@ -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
|
@ -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
|
||||
}
|
2
fastlane/metadata/android/en-US/changelogs/11.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
- Add option to require unlocking the device to use the tile
|
||||
- Fix invisible nav buttons on some devices
|
4
fastlane/metadata/android/en-US/changelogs/12.txt
Normal 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
|
2
fastlane/metadata/android/en-US/changelogs/13.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
- Settings export/import
|
||||
- Fix label not appearing in some cases
|
3
fastlane/metadata/android/en-US/changelogs/14.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
- Add an option to edit servers
|
||||
- Add placeholder for empty server list
|
||||
- Fix layout for longer server addresses
|
2
fastlane/metadata/android/en-US/changelogs/15.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
- Fix crashes on Android 11 and earlier
|
||||
- Fix list entry layout
|
7
fastlane/metadata/android/en-US/changelogs/16.txt
Normal file
|
@ -0,0 +1,7 @@
|
|||
- Replaced server storage backend with Room, allowing for easier further expansion
|
||||
- Add option to disable saved servers
|
||||
- Improved backup handling
|
||||
- Fixed desync bug while dragging servers
|
||||
- Reorganized source file structure
|
||||
- Updated Kotlin version
|
||||
- Updated Java version
|
11
fastlane/metadata/android/en-US/changelogs/17.txt
Normal 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
|
12
fastlane/metadata/android/en-US/changelogs/18.txt
Normal 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))
|
2
fastlane/metadata/android/en-US/changelogs/default.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Changelog for latest release is available on GitHub:
|
||||
https://github.com/karasevm/PrivateDNSAndroid/releases/latest
|
15
fastlane/metadata/android/en-US/full_description.txt
Normal 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>
|
BIN
fastlane/metadata/android/en-US/images/icon.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/01.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/02.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/03.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/04.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/05.png
Normal file
After Width: | Height: | Size: 12 KiB |
1
fastlane/metadata/android/en-US/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Quick settings tile to switch active private DNS server
|
1
fastlane/metadata/android/en-US/title.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Private DNS Quick Toggle
|
|
@ -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
|