From 6b7324a2a88cf0fdfad41edaafd9f4d9144594a4 Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Thu, 27 Jun 2024 15:30:10 +0300 Subject: [PATCH 01/52] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index aec2448..a8c97eb 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +[![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/karasevm/PrivateDNSAndroid/total)](https://github.com/karasevm/PrivateDNSAndroid/releases/latest) +[![GitHub Release](https://img.shields.io/github/v/release/karasevm/PrivateDNSAndroid)](https://github.com/karasevm/PrivateDNSAndroid/releases/latest) +[![IzzyOnDroid](https://img.shields.io/endpoint?url=https://apt.izzysoft.de/fdroid/api/v1/shield/ru.karasevm.privatednstoggle&label=IzzyOnDroid)](https://apt.izzysoft.de/fdroid/index/apk/ru.karasevm.privatednstoggle) + # 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. @@ -5,7 +9,8 @@ a single tap. ![Private DNS app screenshot](readme.jpg) ## Installation -Get the latest apk on the [releases page](https://github.com/karasevm/PrivateDNSAndroid/releases/latest). +Get the latest apk on the [releases page](https://github.com/karasevm/PrivateDNSAndroid/releases/latest) +or from [IzzyOnDroid repo](https://apt.izzysoft.de/fdroid/index/apk/ru.karasevm.privatednstoggle). ## Automatic (Shizuku) 1. Install and start [Shizuku](https://shizuku.rikka.app/). From 29957ef9087b3f2eee421678c63c65918e1db981 Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Thu, 27 Jun 2024 21:03:37 +0300 Subject: [PATCH 02/52] Refresh tile on server pick (#24) --- .../DNSServerDialogFragment.kt | 2 +- .../privatednstoggle/DnsTileService.kt | 65 ++++++++++++++----- 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/DNSServerDialogFragment.kt b/app/src/main/java/ru/karasevm/privatednstoggle/DNSServerDialogFragment.kt index 8112f6f..4666321 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/DNSServerDialogFragment.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/DNSServerDialogFragment.kt @@ -100,8 +100,8 @@ class DNSServerDialogFragment : DialogFragment() { ).show() } } - dialog?.dismiss() + requireContext().sendBroadcast(Intent("refresh_tile").setPackage(requireContext().packageName)) } } diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/DnsTileService.kt b/app/src/main/java/ru/karasevm/privatednstoggle/DnsTileService.kt index ddf34ab..02635c5 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/DnsTileService.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/DnsTileService.kt @@ -1,9 +1,14 @@ package ru.karasevm.privatednstoggle +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.graphics.drawable.Icon import android.provider.Settings import android.service.quicksettings.Tile import android.service.quicksettings.TileService +import androidx.core.content.ContextCompat import ru.karasevm.privatednstoggle.utils.DnsServer import ru.karasevm.privatednstoggle.utils.PreferenceHelper import ru.karasevm.privatednstoggle.utils.PreferenceHelper.autoMode @@ -117,34 +122,27 @@ class DnsTileService : TileService() { } - override fun onStartListening() { - super.onStartListening() - if (!checkForPermission(this)) { - return - } + /** + * Refreshes the state of the tile + */ + private fun refreshTile() { val dnsMode = Settings.Global.getString(contentResolver, "private_dns_mode") - - // Prevent some crashes - if (qsTile == null) { - return - } - if (dnsMode.equals(DNS_MODE_OFF, ignoreCase = true)) { - refreshTile( + setTile( qsTile, Tile.STATE_INACTIVE, getString(R.string.dns_off), R.drawable.ic_off_black_24dp ) } else if (dnsMode == null) { - refreshTile( + setTile( qsTile, Tile.STATE_INACTIVE, getString(R.string.dns_unknown), R.drawable.ic_unknown_black_24dp ) } else if (dnsMode.equals(DNS_MODE_AUTO, ignoreCase = true)) { - refreshTile( + setTile( qsTile, Tile.STATE_INACTIVE, getString(R.string.dns_auto), @@ -162,7 +160,7 @@ class DnsTileService : TileService() { } if (items.isEmpty() || items[0].server == "") { - refreshTile( + setTile( qsTile, Tile.STATE_ACTIVE, "Google", @@ -171,14 +169,14 @@ class DnsTileService : TileService() { } else { val index = items.indexOfFirst { it.server == dnsProvider } if (index == -1) { - refreshTile( + setTile( qsTile, Tile.STATE_ACTIVE, dnsProvider, R.drawable.ic_private_black_24dp ) } else { - refreshTile( + setTile( qsTile, Tile.STATE_ACTIVE, items[index].label, @@ -187,7 +185,38 @@ class DnsTileService : TileService() { } } } + } + private val broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + refreshTile() + } + } + + override fun onStartListening() { + super.onStartListening() + if (!checkForPermission(this)) { + return + } + + // Prevent some crashes + if (qsTile == null) { + return + } + ContextCompat.registerReceiver( + this, + broadcastReceiver, + IntentFilter("refresh_tile"), + ContextCompat.RECEIVER_NOT_EXPORTED + ) + + refreshTile() + + } + + override fun onStopListening() { + super.onStopListening() + unregisterReceiver(broadcastReceiver) } /** @@ -198,7 +227,7 @@ class DnsTileService : TileService() { * @param label tile label * @param icon tile icon */ - private fun refreshTile(tile: Tile, state: Int, label: String?, icon: Int) { + private fun setTile(tile: Tile, state: Int, label: String?, icon: Int) { tile.state = state tile.label = label tile.icon = Icon.createWithResource(this, icon) From 5c38ec2db8ea5c2ca0ec9a8f7382d925f71630fc Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Thu, 27 Jun 2024 21:04:13 +0300 Subject: [PATCH 03/52] Bump version --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 103ed45..1b43aa7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { defaultConfig { applicationId "ru.karasevm.privatednstoggle" - versionCode 11 - versionName "1.4" + versionCode 12 + versionName "1.6" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" targetSdkVersion 34 From 7dd0fbf8028bec40893e64ff9a6532af6e74eaf9 Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Thu, 27 Jun 2024 21:18:07 +0300 Subject: [PATCH 04/52] Fastlane changelog --- fastlane/metadata/android/en-US/changelogs/12.txt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/12.txt diff --git a/fastlane/metadata/android/en-US/changelogs/12.txt b/fastlane/metadata/android/en-US/changelogs/12.txt new file mode 100644 index 0000000..46fe2d8 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/12.txt @@ -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 \ No newline at end of file From 6caa1432aa8dd4d303fd576f3d23025c2e37f4ba Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Thu, 27 Jun 2024 22:25:17 +0300 Subject: [PATCH 05/52] Add GitHub issue forms --- .github/ISSUE_TEMPLATE/01-bug-report.yml | 73 +++++++++++++++++++ .github/ISSUE_TEMPLATE/02-feature-request.yml | 30 ++++++++ .github/ISSUE_TEMPLATE/03-other.yml | 9 +++ 3 files changed, 112 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/01-bug-report.yml create mode 100644 .github/ISSUE_TEMPLATE/02-feature-request.yml create mode 100644 .github/ISSUE_TEMPLATE/03-other.yml diff --git a/.github/ISSUE_TEMPLATE/01-bug-report.yml b/.github/ISSUE_TEMPLATE/01-bug-report.yml new file mode 100644 index 0000000..c906e0f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01-bug-report.yml @@ -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. diff --git a/.github/ISSUE_TEMPLATE/02-feature-request.yml b/.github/ISSUE_TEMPLATE/02-feature-request.yml new file mode 100644 index 0000000..7acccf3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02-feature-request.yml @@ -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. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/03-other.yml b/.github/ISSUE_TEMPLATE/03-other.yml new file mode 100644 index 0000000..6ec8a5e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/03-other.yml @@ -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 From 3a697e32e338207a1ea089098461ec5861ca9872 Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Tue, 2 Jul 2024 16:07:04 +0300 Subject: [PATCH 06/52] Implement settings export/import (#26) --- app/build.gradle | 5 +- .../karasevm/privatednstoggle/MainActivity.kt | 172 ++++++++++++------ .../privatednstoggle/utils/SharedPrefUtils.kt | 21 ++- app/src/main/res/menu/menu_main.xml | 6 + app/src/main/res/values/strings.xml | 6 + 5 files changed, 156 insertions(+), 54 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 1b43aa7..2a02e63 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -51,6 +51,7 @@ dependencies { implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'com.google.guava:guava:33.1.0-android' + implementation 'com.google.code.gson:gson:2.11.0' def shizuku_version = '13.1.5' implementation "dev.rikka.shizuku:api:$shizuku_version" @@ -60,6 +61,6 @@ dependencies { compileOnly 'dev.rikka.hidden:stub:4.3.2' testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' } \ No newline at end of file diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/MainActivity.kt b/app/src/main/java/ru/karasevm/privatednstoggle/MainActivity.kt index f19a122..1792e30 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/MainActivity.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/MainActivity.kt @@ -1,7 +1,10 @@ package ru.karasevm.privatednstoggle import android.Manifest -import android.annotation.SuppressLint +import android.content.ClipData +import android.content.ClipDescription.MIMETYPE_TEXT_PLAIN +import android.content.ClipboardManager +import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.content.pm.IPackageManager @@ -15,11 +18,17 @@ import android.util.Log import android.view.Menu import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper.DOWN import androidx.recyclerview.widget.ItemTouchHelper.UP import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.JsonSyntaxException +import com.google.gson.ToNumberPolicy +import com.google.gson.reflect.TypeToken import org.lsposed.hiddenapibypass.HiddenApiBypass import rikka.shizuku.Shizuku import rikka.shizuku.ShizukuBinderWrapper @@ -28,6 +37,8 @@ import rikka.shizuku.SystemServiceHelper import ru.karasevm.privatednstoggle.databinding.ActivityMainBinding import ru.karasevm.privatednstoggle.utils.PreferenceHelper import ru.karasevm.privatednstoggle.utils.PreferenceHelper.dns_servers +import ru.karasevm.privatednstoggle.utils.PreferenceHelper.export +import ru.karasevm.privatednstoggle.utils.PreferenceHelper.import class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogListener, @@ -38,50 +49,49 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi private var items = mutableListOf() private lateinit var sharedPrefs: SharedPreferences private lateinit var adapter: RecyclerAdapter + private lateinit var clipboard: ClipboardManager + private lateinit var gson: Gson private val itemTouchHelper by lazy { - val simpleItemTouchCallback = - object : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) { + val simpleItemTouchCallback = object : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) { - override fun onMove( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder - ): Boolean { - adapter.onItemMove(viewHolder.adapterPosition, target.adapterPosition) - return true - } + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + adapter.onItemMove(viewHolder.adapterPosition, target.adapterPosition) + return true + } - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} + override fun 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) + override fun onSelectedChanged( + viewHolder: RecyclerView.ViewHolder?, actionState: Int + ) { + super.onSelectedChanged(viewHolder, actionState) + if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) { + viewHolder?.itemView?.apply { + // Example: Elevate the view + elevation = 8f + alpha = 0.5f + setBackgroundColor(Color.GRAY) } } } + + override fun clearView( + recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder + ) { + super.clearView(recyclerView, viewHolder) + viewHolder.itemView.apply { + // Reset the appearance + elevation = 0f + alpha = 1.0f + setBackgroundColor(Color.TRANSPARENT) + } + } + } ItemTouchHelper(simpleItemTouchCallback) } @@ -98,6 +108,8 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi binding.recyclerView.layoutManager = linearLayoutManager sharedPrefs = PreferenceHelper.defaultPreference(this) + clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + gson = GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create() items = sharedPrefs.dns_servers if (items[0] == "") { @@ -108,9 +120,9 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi val newFragment = DeleteServerDialogFragment(position) newFragment.show(supportFragmentManager, "delete_server") } - adapter.onItemsChanged = { swapedItems -> - items = swapedItems - sharedPrefs.dns_servers = swapedItems + adapter.onItemsChanged = { swappedItems -> + items = swappedItems + sharedPrefs.dns_servers = swappedItems } adapter.onDragStart = { viewHolder -> itemTouchHelper.startDrag(viewHolder) @@ -133,6 +145,54 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi true } + R.id.export_settings -> { + val data = sharedPrefs.export() + val jsonData = gson.toJson(data) + clipboard.setPrimaryClip(ClipData.newPlainText("", jsonData)) + // Only show a toast for Android 12 and lower. + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) Toast.makeText( + this, getString(R.string.copy_success), Toast.LENGTH_SHORT + ).show() + true + } + + R.id.import_settings -> { + val clipData = clipboard.primaryClip?.getItemAt(0) + val textData = clipData?.text + + if (textData != null) { + runCatching { + val jsonData = textData.toString() + val objectType = object : TypeToken>() {}.type + val data: Map = gson.fromJson(jsonData, objectType) + sharedPrefs.import(data) + }.onSuccess { + Toast.makeText( + this, getString(R.string.import_success), Toast.LENGTH_SHORT + ).show() + ActivityCompat.recreate(this) + }.onFailure { exception -> + Log.e("IMPORT", "Import failed", exception) + when (exception) { + is JsonSyntaxException -> { + Toast.makeText( + this, + getString(R.string.import_failure_json), + Toast.LENGTH_SHORT + ).show() + } + + else -> { + Toast.makeText( + this, getString(R.string.import_failure), Toast.LENGTH_SHORT + ).show() + } + } + } + } + true + } + R.id.options -> { val newFragment = OptionsDialogFragment() newFragment.show(supportFragmentManager, "options") @@ -184,6 +244,23 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi } } + 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) + + // 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) @@ -224,9 +301,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi val pm = IPermissionManager.Stub.asInterface(binder) runCatching { pm.grantRuntimePermission( - packageName, - Manifest.permission.WRITE_SECURE_SETTINGS, - 0 + packageName, Manifest.permission.WRITE_SECURE_SETTINGS, 0 ) }.onFailure { _ -> if (Build.VERSION.SDK_INT >= 34) { @@ -242,9 +317,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi val binder = ShizukuBinderWrapper(SystemServiceHelper.getSystemService("package")) val pm = IPackageManager.Stub.asInterface(binder) pm.grantRuntimePermission( - packageName, - Manifest.permission.WRITE_SECURE_SETTINGS, - 0 + packageName, Manifest.permission.WRITE_SECURE_SETTINGS, 0 ) } }.onFailure { e -> @@ -252,8 +325,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi }.also { if (checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) { val browserIntent = Intent( - Intent.ACTION_VIEW, - Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/") + Intent.ACTION_VIEW, Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/") ) startActivity(browserIntent) finish() @@ -262,7 +334,6 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi } - @SuppressLint("PrivateApi") override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) { val isGranted = grantResult == PackageManager.PERMISSION_GRANTED @@ -270,8 +341,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi grantPermissionWithShizuku() } else if (checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) { val browserIntent = Intent( - Intent.ACTION_VIEW, - Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/") + Intent.ACTION_VIEW, Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/") ) startActivity(browserIntent) finish() diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/utils/SharedPrefUtils.kt b/app/src/main/java/ru/karasevm/privatednstoggle/utils/SharedPrefUtils.kt index 87cfa96..8e97931 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/utils/SharedPrefUtils.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/utils/SharedPrefUtils.kt @@ -26,7 +26,7 @@ object PreferenceHelper { is Boolean -> putBoolean(key, value) is Long -> putLong(key, value) is Float -> putFloat(key, value) - else -> error("Only primitive types can be stored in SharedPreferences") + else -> error("Only primitive types can be stored in SharedPreferences, got ${value.javaClass}") } } @@ -54,4 +54,23 @@ object PreferenceHelper { } } + // export all the preferences + fun SharedPreferences.export() = mapOf( + DNS_SERVERS to getString(DNS_SERVERS, ""), + AUTO_MODE to autoMode, + REQUIRE_UNLOCK to requireUnlock + ) + + // import all the preferences + fun SharedPreferences.import(map: Map) { + editMe { + map.forEach { (key, value) -> + if (value is Number) { + it.put(key to value.toInt()) + return@forEach + } + it.put(key to value) + } + } + } } diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml index bce5f6b..02dfde5 100644 --- a/app/src/main/res/menu/menu_main.xml +++ b/app/src/main/res/menu/menu_main.xml @@ -7,6 +7,12 @@ app:showAsAction="ifRoom" android:icon="@drawable/ic_baseline_settings_24" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3f3912e..80fe947 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -30,4 +30,10 @@ Private DNS set to %1$s Require unlocking the device to change server Drag handle + Import from clipboard + Export to clipboard + Imported + Import failed + Import failed, malformed JSON + Copied \ No newline at end of file From 66a83b3dac42cd59384574a31951f4af15b1d133 Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Tue, 2 Jul 2024 16:18:43 +0300 Subject: [PATCH 07/52] Update screenshots --- .../en-US/images/phoneScreenshots/01.png | Bin 2752 -> 3930 bytes .../en-US/images/phoneScreenshots/05.png | Bin 7597 -> 11814 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/01.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/01.png index 6e7345f13e95bef897e6f5fe3fcb97f4a995663e..8d0d4793f3525aec0d054208f26f47ddb32c7432 100644 GIT binary patch delta 3790 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 DRdT6$ delta 7531 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 Date: Tue, 2 Jul 2024 16:56:42 +0300 Subject: [PATCH 08/52] Fix label not appearing sometimes --- .../main/java/ru/karasevm/privatednstoggle/RecyclerAdapter.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/RecyclerAdapter.kt b/app/src/main/java/ru/karasevm/privatednstoggle/RecyclerAdapter.kt index 6c9e648..c4c1730 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/RecyclerAdapter.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/RecyclerAdapter.kt @@ -29,6 +29,7 @@ class RecyclerAdapter(private val items: MutableList, private val showDr val item = items[position] val parts = item.split(" : ") if (parts.size == 2) { + holder.labelTextView.visibility = View.VISIBLE holder.labelTextView.text = parts[0] holder.textView.text = parts[1] } else { From 213d3e4dee0cd82179e88d62b16e044210ec945d Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Tue, 2 Jul 2024 20:39:26 +0300 Subject: [PATCH 09/52] Add more ways to export/import settings --- .../karasevm/privatednstoggle/MainActivity.kt | 132 ++++++++++++++---- app/src/main/res/menu/menu_main.xml | 27 +++- app/src/main/res/values/strings.xml | 11 +- 3 files changed, 134 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/MainActivity.kt b/app/src/main/java/ru/karasevm/privatednstoggle/MainActivity.kt index 1792e30..98e096c 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/MainActivity.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/MainActivity.kt @@ -1,6 +1,7 @@ package ru.karasevm.privatednstoggle import android.Manifest +import android.app.Activity import android.content.ClipData import android.content.ClipDescription.MIMETYPE_TEXT_PLAIN import android.content.ClipboardManager @@ -17,8 +18,10 @@ import android.permission.IPermissionManager import android.util.Log import android.view.Menu import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat +import androidx.core.app.ShareCompat import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper.DOWN import androidx.recyclerview.widget.ItemTouchHelper.UP @@ -95,6 +98,34 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi ItemTouchHelper(simpleItemTouchCallback) } + private fun importSettings(json: String) { + runCatching { + val objectType = object : TypeToken>() {}.type + val data: Map = gson.fromJson(json, objectType) + sharedPrefs.import(data) + }.onSuccess { + Toast.makeText( + this, getString(R.string.import_success), Toast.LENGTH_SHORT + ).show() + ActivityCompat.recreate(this) + }.onFailure { exception -> + Log.e("IMPORT", "Import failed", exception) + when (exception) { + is JsonSyntaxException -> { + Toast.makeText( + this, getString(R.string.import_failure_json), Toast.LENGTH_SHORT + ).show() + } + + else -> { + Toast.makeText( + this, getString(R.string.import_failure), Toast.LENGTH_SHORT + ).show() + } + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -145,7 +176,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi true } - R.id.export_settings -> { + R.id.export_settings_clipboard -> { val data = sharedPrefs.export() val jsonData = gson.toJson(data) clipboard.setPrimaryClip(ClipData.newPlainText("", jsonData)) @@ -156,39 +187,39 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi true } - R.id.import_settings -> { + R.id.export_settings_share -> { + val data = sharedPrefs.export() + val jsonData = gson.toJson(data) + ShareCompat.IntentBuilder(this).setText(jsonData).setType("text/plain") + .startChooser() + true + } + + R.id.export_settings_file -> { + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "text/plain" + putExtra(Intent.EXTRA_TITLE, "private-dns-export") + } + 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) { - runCatching { - val jsonData = textData.toString() - val objectType = object : TypeToken>() {}.type - val data: Map = gson.fromJson(jsonData, objectType) - sharedPrefs.import(data) - }.onSuccess { - Toast.makeText( - this, getString(R.string.import_success), Toast.LENGTH_SHORT - ).show() - ActivityCompat.recreate(this) - }.onFailure { exception -> - Log.e("IMPORT", "Import failed", exception) - when (exception) { - is JsonSyntaxException -> { - Toast.makeText( - this, - getString(R.string.import_failure_json), - Toast.LENGTH_SHORT - ).show() - } - - else -> { - Toast.makeText( - this, getString(R.string.import_failure), Toast.LENGTH_SHORT - ).show() - } - } - } + importSettings(textData.toString()) } true } @@ -204,6 +235,45 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi } } + private var saveResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val data: Intent? = result.data + data?.data?.also { uri -> + val jsonData = gson.toJson(sharedPrefs.export()) + 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 == Activity.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 @@ -248,7 +318,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi super.onWindowFocusChanged(hasFocus) if (!hasFocus) { // Gets the ID of the "paste" menu item. - val pasteItem = binding.topAppBar.menu.findItem(R.id.import_settings) + 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. diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml index 02dfde5..fcfec62 100644 --- a/app/src/main/res/menu/menu_main.xml +++ b/app/src/main/res/menu/menu_main.xml @@ -7,12 +7,33 @@ app:showAsAction="ifRoom" android:icon="@drawable/ic_baseline_settings_24" /> - + app:showAsAction="never" > + + + + + + app:showAsAction="never" > + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 80fe947..49268ae 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -30,10 +30,17 @@ Private DNS set to %1$s Require unlocking the device to change server Drag handle - Import from clipboard - Export to clipboard + Import + Export Imported Import failed Import failed, malformed JSON Copied + From file + From clipboard + To clipboard + Share + To file + Saving failed + Saved successfully \ No newline at end of file From 5fe2354e7d72597d3f220508ca3c550be1b854eb Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Wed, 3 Jul 2024 18:12:50 +0300 Subject: [PATCH 10/52] Bump version --- app/build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/13.txt | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/13.txt diff --git a/app/build.gradle b/app/build.gradle index 2a02e63..38685dc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { defaultConfig { applicationId "ru.karasevm.privatednstoggle" - versionCode 12 - versionName "1.6" + versionCode 13 + versionName "1.7" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" targetSdkVersion 34 diff --git a/fastlane/metadata/android/en-US/changelogs/13.txt b/fastlane/metadata/android/en-US/changelogs/13.txt new file mode 100644 index 0000000..c546656 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/13.txt @@ -0,0 +1,2 @@ +- Settings export/import +- Fix label not appearing in some cases \ No newline at end of file From 470f8445f9daa2a4aefd137fd35440799adfc7cf Mon Sep 17 00:00:00 2001 From: Stephen Vaz <54773712+stephenvaz@users.noreply.github.com> Date: Mon, 12 Aug 2024 17:48:47 +0530 Subject: [PATCH 11/52] Option to edit the server (#29) * Option to edit the server * Empty List of Severs notified --- .idea/vcs.xml | 2 +- .../AddServerDialogFragment.kt | 61 +++++++++++++------ .../DNSServerDialogFragment.kt | 2 +- .../karasevm/privatednstoggle/MainActivity.kt | 61 ++++++++++++++++--- .../privatednstoggle/RecyclerAdapter.kt | 3 +- app/src/main/res/layout/activity_main.xml | 24 ++++++++ app/src/main/res/values/strings.xml | 4 ++ build.gradle | 2 +- 8 files changed, 129 insertions(+), 30 deletions(-) diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 94a25f7..35eb1dd 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/AddServerDialogFragment.kt b/app/src/main/java/ru/karasevm/privatednstoggle/AddServerDialogFragment.kt index 246883c..c05b39e 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/AddServerDialogFragment.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/AddServerDialogFragment.kt @@ -14,7 +14,7 @@ import com.google.common.net.InternetDomainName import ru.karasevm.privatednstoggle.databinding.DialogAddBinding -class AddServerDialogFragment : DialogFragment() { +class AddServerDialogFragment(private val position: Int?, private val label: String?, private val server: String?) : DialogFragment() { // Use this instance of the interface to deliver action events private lateinit var listener: NoticeDialogListener @@ -29,6 +29,8 @@ class AddServerDialogFragment : DialogFragment() { * Each method passes the DialogFragment in case the host needs to query it. */ interface NoticeDialogListener { fun onDialogPositiveClick(label: String? ,server: String) + fun onDialogPositiveClick(label: String?, server: String, position: Int) + fun onDeleteItemClicked(position: Int) } // Override the Fragment.onAttach() method to instantiate the NoticeDialogListener @@ -59,22 +61,47 @@ class AddServerDialogFragment : DialogFragment() { 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.menu_add - ) { _, _ -> - listener.onDialogPositiveClick( - binding.editTextServerHint.text.toString().trim(), - binding.editTextServerAddr.text.toString().trim() - ) - } - .setNegativeButton( - R.string.cancel - ) { _, _ -> - dialog?.cancel() - } + if (position != null) { + binding.editTextServerHint.setText(label) + binding.editTextServerAddr.setText(server) + builder.setTitle(R.string.edit_server).setView(view) + .setPositiveButton( + R.string.menu_save + ) { _, _ -> + listener.onDialogPositiveClick( + binding.editTextServerHint.text.toString().trim(), + binding.editTextServerAddr.text.toString().trim(), + position) + } + .setNegativeButton( + R.string.cancel + ) { _, _ -> + dialog?.cancel() + } + .setNeutralButton( + R.string.delete + ) { _, _ -> + listener.onDeleteItemClicked(position) + } + } + else { + builder.setTitle(R.string.add_server) + .setView(view) + // Add action buttons + .setPositiveButton( + R.string.menu_add + ) { _, _ -> + listener.onDialogPositiveClick( + binding.editTextServerHint.text.toString().trim(), + binding.editTextServerAddr.text.toString().trim() + ) + } + .setNegativeButton( + R.string.cancel + ) { _, _ -> + dialog?.cancel() + } + } builder.create() } ?: throw IllegalStateException("Activity cannot be null") } diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/DNSServerDialogFragment.kt b/app/src/main/java/ru/karasevm/privatednstoggle/DNSServerDialogFragment.kt index 4666321..3ea8912 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/DNSServerDialogFragment.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/DNSServerDialogFragment.kt @@ -44,7 +44,7 @@ class DNSServerDialogFragment : DialogFragment() { items.add(0, resources.getString(R.string.dns_auto)) items.add(0, resources.getString(R.string.dns_off)) - adapter = RecyclerAdapter(items, false) + adapter = RecyclerAdapter(items, false) {} binding.recyclerView.adapter = adapter diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/MainActivity.kt b/app/src/main/java/ru/karasevm/privatednstoggle/MainActivity.kt index 98e096c..230eadb 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/MainActivity.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/MainActivity.kt @@ -1,11 +1,9 @@ package ru.karasevm.privatednstoggle import android.Manifest -import android.app.Activity import android.content.ClipData import android.content.ClipDescription.MIMETYPE_TEXT_PLAIN import android.content.ClipboardManager -import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.content.pm.IPackageManager @@ -17,6 +15,7 @@ import android.os.Bundle import android.permission.IPermissionManager import android.util.Log import android.view.Menu +import android.view.View import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity @@ -139,27 +138,41 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi binding.recyclerView.layoutManager = linearLayoutManager sharedPrefs = PreferenceHelper.defaultPreference(this) - clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager gson = GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create() items = sharedPrefs.dns_servers if (items[0] == "") { items.removeAt(0) } - adapter = RecyclerAdapter(items, true) + + updateEmptyView() + adapter = RecyclerAdapter(items, true) { updateEmptyView() } adapter.onItemClick = { position -> - val newFragment = DeleteServerDialogFragment(position) - newFragment.show(supportFragmentManager, "delete_server") + val data = items[position].split(" : ") + val label: String? + val server: String + if (data.size == 2) { + label = data[0] + server = data[1] + } + else { + label = null + server = data[0] + } + val newFragment = AddServerDialogFragment(position, label, server) + newFragment.show(supportFragmentManager, "edit_server") } adapter.onItemsChanged = { swappedItems -> items = swappedItems sharedPrefs.dns_servers = swappedItems + updateEmptyView() } adapter.onDragStart = { viewHolder -> itemTouchHelper.startDrag(viewHolder) } binding.floatingActionButton.setOnClickListener { - val newFragment = AddServerDialogFragment() + val newFragment = AddServerDialogFragment(null, null, null) newFragment.show(supportFragmentManager, "add_server") } binding.recyclerView.adapter = adapter @@ -235,9 +248,19 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi } } + private fun updateEmptyView() { + if (items.isEmpty()) { + binding.emptyView.visibility = View.VISIBLE + binding.emptyViewHint.visibility = View.VISIBLE + } else { + binding.emptyView.visibility = View.GONE + binding.emptyViewHint.visibility = View.GONE + } + } + private var saveResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode == Activity.RESULT_OK) { + if (result.resultCode == RESULT_OK) { val data: Intent? = result.data data?.data?.also { uri -> val jsonData = gson.toJson(sharedPrefs.export()) @@ -262,7 +285,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi private var importResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode == Activity.RESULT_OK) { + if (result.resultCode == RESULT_OK) { val data: Intent? = result.data data?.data?.also { uri -> val contentResolver = applicationContext.contentResolver @@ -336,6 +359,11 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi Shizuku.removeRequestPermissionResultListener(this::onRequestPermissionResult) } + override fun onDeleteItemClicked(position: Int) { + val newFragment = DeleteServerDialogFragment(position) + newFragment.show(supportFragmentManager, "delete_server") + } + override fun onDialogPositiveClick(label: String?, server: String) { if (server.isEmpty()) { Toast.makeText(this, R.string.server_length_error, Toast.LENGTH_SHORT).show() @@ -358,6 +386,21 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi sharedPrefs.dns_servers = items } + override fun onDialogPositiveClick(label: String?, server: String, position: Int) { + if (server.isEmpty()) { + Toast.makeText(this, R.string.server_length_error, Toast.LENGTH_SHORT).show() + return + } + if (label.isNullOrEmpty()) { + items[position] = server + } else { + items[position] = "$label : $server" + } + adapter.notifyItemChanged(position) + sharedPrefs.dns_servers = items + binding.recyclerView.adapter?.notifyItemChanged(position) + } + /** * Attempts to grant WRITE_SECURE_SETTINGS permission with Shizuku */ diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/RecyclerAdapter.kt b/app/src/main/java/ru/karasevm/privatednstoggle/RecyclerAdapter.kt index c4c1730..0d61cfc 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/RecyclerAdapter.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/RecyclerAdapter.kt @@ -10,7 +10,7 @@ import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import java.util.Collections -class RecyclerAdapter(private val items: MutableList, private val showDragHandle: Boolean) : +class RecyclerAdapter(private val items: MutableList, private val showDragHandle: Boolean, private val onDataChanged: () -> Unit) : RecyclerView.Adapter() { var onItemClick: ((Int) -> Unit)? = null @@ -77,6 +77,7 @@ class RecyclerAdapter(private val items: MutableList, private val showDr clear() addAll(newItems) } + onDataChanged() } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 3b78639..eb0201d 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -30,6 +30,30 @@ android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@+id/topAppBarLayout" /> + + + + Unknown Add Server Add + Save Privacy Policy Select Server Done @@ -43,4 +44,7 @@ To file Saving failed Saved successfully + Edit server + No Servers Added + Tap on the button below to add one \ No newline at end of file diff --git a/build.gradle b/build.gradle index 01ac801..5a61030 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.5.0' + classpath 'com.android.tools.build:gradle:8.5.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22" // NOTE: Do not place your application dependencies here; they belong From 0028d720957c83adf3e46a3084e41be98decd483 Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Mon, 12 Aug 2024 15:26:58 +0300 Subject: [PATCH 12/52] Fix layout when using long addresses (#30) --- app/src/main/res/layout/recyclerview_row.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/recyclerview_row.xml b/app/src/main/res/layout/recyclerview_row.xml index 67c06bf..4b6afbc 100644 --- a/app/src/main/res/layout/recyclerview_row.xml +++ b/app/src/main/res/layout/recyclerview_row.xml @@ -2,7 +2,7 @@ From af61a85f9ec8faa2707f55865c37a08b152ddd84 Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Mon, 12 Aug 2024 15:30:38 +0300 Subject: [PATCH 13/52] Bump version --- app/build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/14.txt | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/14.txt diff --git a/app/build.gradle b/app/build.gradle index 38685dc..0a1e331 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { defaultConfig { applicationId "ru.karasevm.privatednstoggle" - versionCode 13 - versionName "1.7" + versionCode 14 + versionName "1.8" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" targetSdkVersion 34 diff --git a/fastlane/metadata/android/en-US/changelogs/14.txt b/fastlane/metadata/android/en-US/changelogs/14.txt new file mode 100644 index 0000000..d8ece24 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/14.txt @@ -0,0 +1,3 @@ +- Add an option to edit servers +- Add placeholder for empty server list +- Fix layout for longer server addresses From 53ab655eaeb9a4e0edb4b9ac7b8ed102ecd48a1b Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Tue, 13 Aug 2024 22:22:50 +0300 Subject: [PATCH 14/52] Fix crashes on Android 11 and earlier --- app/src/main/res/values/colors.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 3643757..22ecfdd 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -12,4 +12,6 @@ #CCC2DC #EFB8C8 #1C1B1F + + #747879 From 95f778a787f004c027efeebff123ef92ec4be6f1 Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Tue, 13 Aug 2024 22:32:43 +0300 Subject: [PATCH 15/52] Fix list entry layout --- app/src/main/res/layout/recyclerview_row.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/recyclerview_row.xml b/app/src/main/res/layout/recyclerview_row.xml index 4b6afbc..4da379a 100644 --- a/app/src/main/res/layout/recyclerview_row.xml +++ b/app/src/main/res/layout/recyclerview_row.xml @@ -3,6 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" + android:minHeight="48dp" android:clickable="true" android:focusable="true" android:foreground="?android:attr/selectableItemBackground"> From eead2a912d419532481dbc6e20c56cc77c5199a0 Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Tue, 13 Aug 2024 22:44:55 +0300 Subject: [PATCH 16/52] Bump version --- app/build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/15.txt | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/15.txt diff --git a/app/build.gradle b/app/build.gradle index 0a1e331..78848c4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { defaultConfig { applicationId "ru.karasevm.privatednstoggle" - versionCode 14 - versionName "1.8" + versionCode 15 + versionName "1.8.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" targetSdkVersion 34 diff --git a/fastlane/metadata/android/en-US/changelogs/15.txt b/fastlane/metadata/android/en-US/changelogs/15.txt new file mode 100644 index 0000000..8541dd6 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/15.txt @@ -0,0 +1,2 @@ +- Fix crashes on Android 11 and earlier +- Fix list entry layout From 36be36d69bf3d5fec95e552ebee319fc6f23cfbb Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Fri, 13 Sep 2024 12:45:11 +0300 Subject: [PATCH 17/52] Major refactor * Replaced server storage backend with Room, allowing for easier further expansion * Add option to disable saved servers * Improved backup handling * Fixed desync bug while dragging servers * Reorganized source file structure * Updated Kotlin version * Updated Java version --- .idea/kotlinc.xml | 2 +- app/build.gradle | 26 +- app/src/main/AndroidManifest.xml | 6 +- .../privatednstoggle/PrivateDNSApp.kt | 6 + .../privatednstoggle/RecyclerAdapter.kt | 84 ----- .../privatednstoggle/data/DnsServerDao.kt | 84 +++++ .../data/DnsServerRepository.kt | 60 ++++ .../data/DnsServerViewModel.kt | 48 +++ .../data/database/DnsServerRoomDatabase.kt | 34 ++ .../privatednstoggle/model/DnsServer.kt | 22 ++ .../{ => service}/DnsTileService.kt | 216 ++++++------ .../{ => ui}/AddServerDialogFragment.kt | 43 ++- .../{ => ui}/DNSServerDialogFragment.kt | 95 +++--- .../{ => ui}/DeleteServerDialogFragment.kt | 9 +- .../privatednstoggle/{ => ui}/MainActivity.kt | 312 +++++++++++------- .../{ => ui}/OptionsDialogFragment.kt | 12 +- .../ui/ServerListRecyclerAdapter.kt | 124 +++++++ .../{ => ui}/SettingsDialogActivity.kt | 2 +- .../privatednstoggle/util/BackupUtils.kt | 75 +++++ .../{utils => util}/PrivateDNSUtils.kt | 7 +- .../{utils => util}/SharedPrefUtils.kt | 29 +- .../privatednstoggle/utils/DnsServer.kt | 7 - app/src/main/res/layout/activity_main.xml | 4 +- app/src/main/res/layout/dialog_add.xml | 16 +- app/src/main/res/layout/dialog_options.xml | 3 +- app/src/main/res/values/colors.xml | 2 - app/src/main/res/values/strings.xml | 1 + build.gradle | 8 +- .../metadata/android/en-US/changelogs/16.txt | 7 + 29 files changed, 909 insertions(+), 435 deletions(-) delete mode 100644 app/src/main/java/ru/karasevm/privatednstoggle/RecyclerAdapter.kt create mode 100644 app/src/main/java/ru/karasevm/privatednstoggle/data/DnsServerDao.kt create mode 100644 app/src/main/java/ru/karasevm/privatednstoggle/data/DnsServerRepository.kt create mode 100644 app/src/main/java/ru/karasevm/privatednstoggle/data/DnsServerViewModel.kt create mode 100644 app/src/main/java/ru/karasevm/privatednstoggle/data/database/DnsServerRoomDatabase.kt create mode 100644 app/src/main/java/ru/karasevm/privatednstoggle/model/DnsServer.kt rename app/src/main/java/ru/karasevm/privatednstoggle/{ => service}/DnsTileService.kt (53%) rename app/src/main/java/ru/karasevm/privatednstoggle/{ => ui}/AddServerDialogFragment.kt (77%) rename app/src/main/java/ru/karasevm/privatednstoggle/{ => ui}/DNSServerDialogFragment.kt (51%) rename app/src/main/java/ru/karasevm/privatednstoggle/{ => ui}/DeleteServerDialogFragment.kt (88%) rename app/src/main/java/ru/karasevm/privatednstoggle/{ => ui}/MainActivity.kt (59%) rename app/src/main/java/ru/karasevm/privatednstoggle/{ => ui}/OptionsDialogFragment.kt (88%) create mode 100644 app/src/main/java/ru/karasevm/privatednstoggle/ui/ServerListRecyclerAdapter.kt rename app/src/main/java/ru/karasevm/privatednstoggle/{ => ui}/SettingsDialogActivity.kt (89%) create mode 100644 app/src/main/java/ru/karasevm/privatednstoggle/util/BackupUtils.kt rename app/src/main/java/ru/karasevm/privatednstoggle/{utils => util}/PrivateDNSUtils.kt (90%) rename app/src/main/java/ru/karasevm/privatednstoggle/{utils => util}/SharedPrefUtils.kt (67%) delete mode 100644 app/src/main/java/ru/karasevm/privatednstoggle/utils/DnsServer.kt create mode 100644 fastlane/metadata/android/en-US/changelogs/16.txt diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 9a55c2d..f8467b4 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 78848c4..2d17775 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,7 @@ plugins { id 'com.android.application' id 'kotlin-android' + id 'com.google.devtools.ksp' } android { @@ -8,8 +9,8 @@ android { defaultConfig { applicationId "ru.karasevm.privatednstoggle" - versionCode 15 - versionName "1.8.1" + versionCode 16 + versionName "1.9.0-beta1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" targetSdkVersion 34 @@ -29,11 +30,11 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } dependenciesInfo { // Disables dependency metadata when building APKs. @@ -48,6 +49,9 @@ dependencies { implementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'androidx.recyclerview:recyclerview:1.3.2' + implementation 'androidx.activity:activity-ktx:1.9.2' + implementation 'androidx.fragment:fragment-ktx:1.8.3' implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'com.google.guava:guava:33.1.0-android' @@ -60,6 +64,18 @@ dependencies { implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3' compileOnly 'dev.rikka.hidden:stub:4.3.2' + // Room components + def roomVersion = '2.6.1' + implementation "androidx.room:room-ktx:$roomVersion" + ksp "androidx.room:room-compiler:$roomVersion" + androidTestImplementation "androidx.room:room-testing:$roomVersion" + + // Lifecycle components + def lifecycleVersion = '2.8.5' + 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' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a695d06..21da365 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,7 +19,7 @@ android:exported="true" android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" /> @@ -41,7 +41,7 @@ , private val showDragHandle: Boolean, private val onDataChanged: () -> Unit) : - RecyclerView.Adapter() { - - var onItemClick: ((Int) -> Unit)? = null - var onItemsChanged: ((MutableList) -> Unit)? = null - var onDragStart: ((RecyclerAdapter.ViewHolder) -> Unit)? = null - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerAdapter.ViewHolder { - val view = - LayoutInflater.from(parent.context).inflate(R.layout.recyclerview_row, parent, false) - val vh = ViewHolder(view) - return vh - } - - @SuppressLint("ClickableViewAccessibility") - override fun onBindViewHolder(holder: RecyclerAdapter.ViewHolder, position: Int) { - val item = items[position] - val parts = item.split(" : ") - if (parts.size == 2) { - holder.labelTextView.visibility = View.VISIBLE - holder.labelTextView.text = parts[0] - holder.textView.text = parts[1] - } else { - holder.labelTextView.visibility = View.GONE - holder.textView.text = parts[0] - } - - if (showDragHandle) { - holder.dragHandle.visibility = View.VISIBLE - holder.dragHandle.setOnTouchListener { _, event -> - if (event.actionMasked == MotionEvent.ACTION_DOWN) { - onDragStart?.invoke(holder) - } - return@setOnTouchListener true - } - } - } - - override fun getItemCount(): Int { - return items.size - } - - fun onItemMove(fromPosition: Int, toPosition: Int) { - // Swap items in your data list - Collections.swap(items, fromPosition, toPosition) - notifyItemMoved(fromPosition, toPosition) - onItemsChanged?.invoke(items) - } - - - inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - val textView: TextView = itemView.findViewById(R.id.textView) - val labelTextView: TextView = itemView.findViewById(R.id.labelTextView) - val dragHandle: ImageView = itemView.findViewById(R.id.dragHandle) - - init { - itemView.setOnClickListener { - onItemClick?.invoke(adapterPosition) - } - } - } - - fun setData(newItems: MutableList) { - items.run { - clear() - addAll(newItems) - } - onDataChanged() - } - - -} \ No newline at end of file diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/data/DnsServerDao.kt b/app/src/main/java/ru/karasevm/privatednstoggle/data/DnsServerDao.kt new file mode 100644 index 0000000..026de12 --- /dev/null +++ b/app/src/main/java/ru/karasevm/privatednstoggle/data/DnsServerDao.kt @@ -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> + + @Query("SELECT * FROM dns_servers WHERE enabled = 1 ORDER BY sortOrder ASC LIMIT 1") + suspend fun getFirstEnabled(): DnsServer + + @Query("SELECT * FROM dns_servers WHERE server = :server LIMIT 1") + suspend fun getFirstByServer(server: String): DnsServer? + + @Query("SELECT * FROM dns_servers WHERE id = :id") + suspend fun getById(id: Int): DnsServer? + + @Query("SELECT * FROM dns_servers " + + "WHERE sortOrder > (SELECT sortOrder FROM dns_servers WHERE server = :server) AND enabled = 1 " + + "ORDER BY sortOrder ASC " + + "LIMIT 1") + suspend fun getNextEnabledByServer(server: String): DnsServer? + + @Query("DELETE FROM dns_servers") + suspend fun deleteAll() + + @Query("DELETE FROM dns_servers WHERE id = :id") + suspend fun deleteById(id: Int) + + @Query("UPDATE dns_servers SET sortOrder = sortOrder + 1 " + + "WHERE sortOrder >= :startSortOrder AND sortOrder <= :endSortOrder") + suspend fun incrementSortOrder(startSortOrder: Int, endSortOrder: Int = Int.MAX_VALUE) + + @Query("UPDATE dns_servers SET sortOrder = sortOrder - 1 " + + "WHERE sortOrder >= :startSortOrder AND sortOrder <= :endSortOrder") + suspend fun decrementSortOrder(startSortOrder: Int, endSortOrder: Int = Int.MAX_VALUE) + + @Query("UPDATE dns_servers SET sortOrder = sortOrder - 1 " + + "WHERE sortOrder > (SELECT sortOrder FROM dns_servers WHERE id = :id)") + suspend fun decrementSortOrderById(id: Int) + + @Transaction + suspend fun deleteAndDecrement(id: Int) { + decrementSortOrderById(id) + deleteById(id) + } + + @Query("UPDATE dns_servers SET label = :label WHERE id = :id") + suspend fun updateLabel(id: Int, label: String) + + @Query("UPDATE dns_servers SET server = :server WHERE id = :id") + suspend fun updateServer(id: Int, server: String) + + @Query("UPDATE dns_servers " + + "SET server = COALESCE(:server, server), " + + " label = COALESCE(:label, label), " + + " sortOrder = COALESCE(:sortOrder, sortOrder), " + + " enabled = COALESCE(:enabled, enabled) " + + "WHERE id = :id") + suspend fun update(id: Int, server: String?, label: String?, sortOrder: Int?, enabled: Boolean?) + + @Transaction + suspend fun moveUp(sortOrder: Int, newSortOrder: Int, id: Int){ + incrementSortOrder(newSortOrder, sortOrder) + update(id, null, null, newSortOrder, null) + } + + @Transaction + suspend fun moveDown(sortOrder: Int, newSortOrder: Int, id: Int){ + decrementSortOrder(sortOrder, newSortOrder) + update(id, null, null, newSortOrder, null) + } + + @Query("INSERT INTO dns_servers(server, label, sortOrder, enabled) " + + "VALUES(:server, :label, COALESCE((SELECT MAX(sortOrder) + 1 FROM dns_servers), 0), :enabled)") + suspend fun insert(server: String, label: String, enabled: Boolean) + +} \ No newline at end of file diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/data/DnsServerRepository.kt b/app/src/main/java/ru/karasevm/privatednstoggle/data/DnsServerRepository.kt new file mode 100644 index 0000000..4c7ddd4 --- /dev/null +++ b/app/src/main/java/ru/karasevm/privatednstoggle/data/DnsServerRepository.kt @@ -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> = dnsServerDao.getAll() + + @WorkerThread + fun getAll() = dnsServerDao.getAll() + + @WorkerThread + suspend fun getFirstEnabled() = dnsServerDao.getFirstEnabled() + + @WorkerThread + suspend fun getById(id: Int) = dnsServerDao.getById(id) + + @WorkerThread + suspend fun getFirstByServer(server: String) = dnsServerDao.getFirstByServer(server) + + @WorkerThread + suspend fun getNextByServer(server: String) = dnsServerDao.getNextEnabledByServer(server) + + @WorkerThread + suspend fun insert(dnsServer: DnsServer) { + dnsServerDao.insert(dnsServer.server, dnsServer.label, dnsServer.enabled) + } + + @WorkerThread + suspend fun update( + id: Int, + server: String?, + label: String?, + sortOrder: Int?, + enabled: Boolean? + ) { + dnsServerDao.update(id, server, label, sortOrder, enabled) + } + + @WorkerThread + suspend fun move(sortOrder: Int, newSortOrder: Int, id: Int) { + if (sortOrder == newSortOrder) { + return + } + if (newSortOrder > sortOrder) { + dnsServerDao.moveDown(sortOrder, newSortOrder, id) + } else { + dnsServerDao.moveUp(sortOrder, newSortOrder, id) + } + } + + @WorkerThread + suspend fun delete(id: Int) { + dnsServerDao.deleteAndDecrement(id) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/data/DnsServerViewModel.kt b/app/src/main/java/ru/karasevm/privatednstoggle/data/DnsServerViewModel.kt new file mode 100644 index 0000000..bd1987a --- /dev/null +++ b/app/src/main/java/ru/karasevm/privatednstoggle/data/DnsServerViewModel.kt @@ -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> = dnsServerRepository.allServers.asLiveData() + + fun getAll() = dnsServerRepository.getAll() + + suspend fun getById(id: Int) = dnsServerRepository.getById(id) + + fun insert(dnsServer: DnsServer) = + viewModelScope.launch { + dnsServerRepository.insert(dnsServer) + } + + fun update( + id: Int, + server: String? = null, + label: String? = null, + sortOrder: Int? = null, + enabled: Boolean? = null + ) = viewModelScope.launch { dnsServerRepository.update(id, server, label, sortOrder, enabled) } + + fun move(sortOrder: Int, newSortOrder: Int, id: Int) = + viewModelScope.launch { dnsServerRepository.move(sortOrder, newSortOrder, id) } + + fun delete(id: Int) = viewModelScope.launch { dnsServerRepository.delete(id) } + +} + +class DnsServerViewModelFactory(private val dnsServerRepository: DnsServerRepository) : + ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(DnsServerViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return DnsServerViewModel(dnsServerRepository) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/data/database/DnsServerRoomDatabase.kt b/app/src/main/java/ru/karasevm/privatednstoggle/data/database/DnsServerRoomDatabase.kt new file mode 100644 index 0000000..72b6f25 --- /dev/null +++ b/app/src/main/java/ru/karasevm/privatednstoggle/data/database/DnsServerRoomDatabase.kt @@ -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 + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/model/DnsServer.kt b/app/src/main/java/ru/karasevm/privatednstoggle/model/DnsServer.kt new file mode 100644 index 0000000..35375d1 --- /dev/null +++ b/app/src/main/java/ru/karasevm/privatednstoggle/model/DnsServer.kt @@ -0,0 +1,22 @@ +package ru.karasevm.privatednstoggle.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.google.gson.annotations.SerializedName + +// All fields must have default values for proper deserialization +@Entity(tableName = "dns_servers") +data class DnsServer( + @SerializedName("id") + @PrimaryKey(autoGenerate = true) + val id: Int = 0, + @SerializedName("server") + val server: String = "", + @SerializedName("label") + val label: String = "", + @SerializedName("enabled") + @ColumnInfo(defaultValue = "1") + val enabled: Boolean = true, + val sortOrder: Int? = null +) diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/DnsTileService.kt b/app/src/main/java/ru/karasevm/privatednstoggle/service/DnsTileService.kt similarity index 53% rename from app/src/main/java/ru/karasevm/privatednstoggle/DnsTileService.kt rename to app/src/main/java/ru/karasevm/privatednstoggle/service/DnsTileService.kt index 02635c5..f1b25ae 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/DnsTileService.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/service/DnsTileService.kt @@ -1,4 +1,4 @@ -package ru.karasevm.privatednstoggle +package ru.karasevm.privatednstoggle.service import android.content.BroadcastReceiver import android.content.Context @@ -9,22 +9,33 @@ import android.provider.Settings import android.service.quicksettings.Tile import android.service.quicksettings.TileService import androidx.core.content.ContextCompat -import ru.karasevm.privatednstoggle.utils.DnsServer -import ru.karasevm.privatednstoggle.utils.PreferenceHelper -import ru.karasevm.privatednstoggle.utils.PreferenceHelper.autoMode -import ru.karasevm.privatednstoggle.utils.PreferenceHelper.dns_servers -import ru.karasevm.privatednstoggle.utils.PreferenceHelper.requireUnlock -import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils -import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils.AUTO_MODE_OPTION_AUTO -import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils.AUTO_MODE_OPTION_OFF_AUTO -import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils.AUTO_MODE_OPTION_PRIVATE -import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils.DNS_MODE_AUTO -import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils.DNS_MODE_OFF -import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils.DNS_MODE_PRIVATE -import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils.checkForPermission +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) + override fun onTileAdded() { super.onTileAdded() checkForPermission(this) @@ -36,7 +47,7 @@ class DnsTileService : TileService() { } /** - * Set's the state of the tile to the next state + * 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") @@ -53,24 +64,31 @@ class DnsTileService : TileService() { } else if (dnsMode == null || dnsMode.equals(DNS_MODE_AUTO, ignoreCase = true)) { changeDNSServer(DNS_MODE_PRIVATE, null) } else if (dnsMode.equals(DNS_MODE_PRIVATE, ignoreCase = true)) { - if (getNextAddress(dnsProvider) == null) { - if (sharedPrefs.autoMode == AUTO_MODE_OPTION_PRIVATE) { - changeDNSServer(DNS_MODE_PRIVATE, null) - } else { - if (sharedPrefs.autoMode == AUTO_MODE_OPTION_AUTO) { - changeDNSServer(DNS_MODE_AUTO, dnsProvider) + scope.launch { + if (getNextAddress(dnsProvider) == null) { + if (sharedPrefs.autoMode == AUTO_MODE_OPTION_PRIVATE) { + changeDNSServer(DNS_MODE_PRIVATE, null) } else { - changeDNSServer(DNS_MODE_OFF, dnsProvider) + if (sharedPrefs.autoMode == AUTO_MODE_OPTION_AUTO) { + changeDNSServer(DNS_MODE_AUTO, dnsProvider) + } else { + changeDNSServer(DNS_MODE_OFF, dnsProvider) + } } + } else { + changeDNSServer(DNS_MODE_PRIVATE, dnsProvider) } - } else { - changeDNSServer(DNS_MODE_PRIVATE, dnsProvider) } } } - private fun changeDNSServer(server: String, dnsProvider: String?) { - when (server) { + /** + * 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, @@ -94,14 +112,19 @@ class DnsTileService : TileService() { } DNS_MODE_PRIVATE -> { - changeTileState( - qsTile, - Tile.STATE_ACTIVE, - getNextAddress(dnsProvider)?.label, - R.drawable.ic_private_black_24dp, - DNS_MODE_PRIVATE, - getNextAddress(dnsProvider)?.server - ) + 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 + ) + } + } } } } @@ -112,6 +135,8 @@ class DnsTileService : TileService() { return } val sharedPrefs = PreferenceHelper.defaultPreference(this) + + // Require unlock to change mode according to user preference val requireUnlock = sharedPrefs.requireUnlock if (isLocked && requireUnlock) { unlockAndRun(this::cycleState) @@ -127,63 +152,48 @@ class DnsTileService : TileService() { */ private fun refreshTile() { val dnsMode = Settings.Global.getString(contentResolver, "private_dns_mode") - if (dnsMode.equals(DNS_MODE_OFF, ignoreCase = true)) { - setTile( - qsTile, - Tile.STATE_INACTIVE, - getString(R.string.dns_off), - R.drawable.ic_off_black_24dp - ) - } else if (dnsMode == null) { - setTile( - qsTile, - Tile.STATE_INACTIVE, - getString(R.string.dns_unknown), - R.drawable.ic_unknown_black_24dp - ) - } else if (dnsMode.equals(DNS_MODE_AUTO, ignoreCase = true)) { - setTile( - qsTile, - Tile.STATE_INACTIVE, - getString(R.string.dns_auto), - R.drawable.ic_auto_black_24dp - ) - } else if (dnsMode.equals(DNS_MODE_PRIVATE, ignoreCase = true)) { - val dnsProvider = Settings.Global.getString(contentResolver, "private_dns_specifier") - val sharedPrefs = PreferenceHelper.defaultPreference(this) - val items = sharedPrefs.dns_servers.map { - val parts = it.split(" : ") - if (parts.size == 2) - DnsServer(parts[0], parts[1]) - else - DnsServer(parts[0], parts[0]) - } - - if (items.isEmpty() || items[0].server == "") { + when (dnsMode?.lowercase()) { + DNS_MODE_OFF -> { setTile( qsTile, - Tile.STATE_ACTIVE, - "Google", - R.drawable.ic_private_black_24dp + Tile.STATE_INACTIVE, + getString(R.string.dns_off), + R.drawable.ic_off_black_24dp ) - } else { - val index = items.indexOfFirst { it.server == dnsProvider } - if (index == -1) { + } + + DNS_MODE_AUTO -> { + setTile( + qsTile, + 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, Tile.STATE_ACTIVE, - dnsProvider, - R.drawable.ic_private_black_24dp - ) - } else { - setTile( - qsTile, - Tile.STATE_ACTIVE, - items[index].label, + // 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, + Tile.STATE_INACTIVE, + getString(R.string.dns_unknown), + R.drawable.ic_unknown_black_24dp + ) + } } } @@ -203,6 +213,9 @@ class DnsTileService : TileService() { if (qsTile == null) { return } + + + // Receive broadcasts to update the tile when server is changed from the dialog ContextCompat.registerReceiver( this, broadcastReceiver, @@ -219,6 +232,11 @@ class DnsTileService : TileService() { unregisterReceiver(broadcastReceiver) } + override fun onDestroy() { + super.onDestroy() + job.cancelChildren() + } + /** * Updates tile to specified parameters * @@ -261,39 +279,17 @@ class DnsTileService : TileService() { } /** - * Gets next dns address from preferences, - * if current address is last returns null + * 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 fun getNextAddress(currentAddress: String?): DnsServer? { - val sharedPrefs = PreferenceHelper.defaultPreference(this) - val items = sharedPrefs.dns_servers.map { - val parts = it.split(" : ") - // Assuming string is in the format "$label : $server" - if (parts.size == 2) - DnsServer(parts[0], parts[1]) - else - DnsServer(parts[0], parts[0]) - }.toMutableList() - - // Fallback if list is empty - if (items.isEmpty() || items[0].server == "") { - items.apply { - removeAt(0) - add(DnsServer("Google", "dns.google")) - } + private suspend fun getNextAddress(currentAddress: String?): DnsServer? { + return if (currentAddress.isNullOrEmpty()) { + repository.getFirstEnabled() + } else { + repository.getNextByServer(currentAddress) } - - val index = items.indexOfFirst { it.server == currentAddress } - - if (index == -1 || currentAddress == null) { - return items[0] - } - if (index == items.size - 1) { - return null - } - return items[index + 1] } } \ No newline at end of file diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/AddServerDialogFragment.kt b/app/src/main/java/ru/karasevm/privatednstoggle/ui/AddServerDialogFragment.kt similarity index 77% rename from app/src/main/java/ru/karasevm/privatednstoggle/AddServerDialogFragment.kt rename to app/src/main/java/ru/karasevm/privatednstoggle/ui/AddServerDialogFragment.kt index c05b39e..f5440d9 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/AddServerDialogFragment.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/ui/AddServerDialogFragment.kt @@ -1,4 +1,4 @@ -package ru.karasevm.privatednstoggle +package ru.karasevm.privatednstoggle.ui import android.app.Dialog import android.content.Context @@ -11,10 +11,14 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.common.net.InternetDomainName +import ru.karasevm.privatednstoggle.R import ru.karasevm.privatednstoggle.databinding.DialogAddBinding +import ru.karasevm.privatednstoggle.model.DnsServer -class AddServerDialogFragment(private val position: Int?, private val label: String?, private val server: String?) : DialogFragment() { +class AddServerDialogFragment( + private val dnsServer: DnsServer? +) : DialogFragment() { // Use this instance of the interface to deliver action events private lateinit var listener: NoticeDialogListener @@ -28,9 +32,9 @@ class AddServerDialogFragment(private val position: Int?, private val label: Str * implement this interface in order to receive event callbacks. * Each method passes the DialogFragment in case the host needs to query it. */ interface NoticeDialogListener { - fun onDialogPositiveClick(label: String? ,server: String) - fun onDialogPositiveClick(label: String?, server: String, position: Int) - fun onDeleteItemClicked(position: Int) + fun onAddDialogPositiveClick(label: String?, server: String) + fun onUpdateDialogPositiveClick(id: Int, server: String, label: String?, enabled: Boolean) + fun onDeleteItemClicked(id: Int) } // Override the Fragment.onAttach() method to instantiate the NoticeDialogListener @@ -61,17 +65,21 @@ class AddServerDialogFragment(private val position: Int?, private val label: Str val view = binding.root // Inflate and set the layout for the dialog // Pass null as the parent view because its going in the dialog layout - if (position != null) { - binding.editTextServerHint.setText(label) - binding.editTextServerAddr.setText(server) + if (dnsServer != null) { + binding.editTextServerHint.setText(dnsServer.label) + binding.editTextServerAddr.setText(dnsServer.server) + binding.serverEnabledSwitch.visibility = android.view.View.VISIBLE + binding.serverEnabledSwitch.isChecked = dnsServer.enabled builder.setTitle(R.string.edit_server).setView(view) .setPositiveButton( R.string.menu_save - ) { _, _ -> - listener.onDialogPositiveClick( - binding.editTextServerHint.text.toString().trim(), - binding.editTextServerAddr.text.toString().trim(), - position) + ) { _, _ -> + listener.onUpdateDialogPositiveClick( + dnsServer.id, + binding.editTextServerAddr.text.toString().trim(), + binding.editTextServerHint.text.toString().trim(), + binding.serverEnabledSwitch.isChecked + ) } .setNegativeButton( R.string.cancel @@ -81,17 +89,16 @@ class AddServerDialogFragment(private val position: Int?, private val label: Str .setNeutralButton( R.string.delete ) { _, _ -> - listener.onDeleteItemClicked(position) - } - } - else { + listener.onDeleteItemClicked(dnsServer.id) + } + } else { builder.setTitle(R.string.add_server) .setView(view) // Add action buttons .setPositiveButton( R.string.menu_add ) { _, _ -> - listener.onDialogPositiveClick( + listener.onAddDialogPositiveClick( binding.editTextServerHint.text.toString().trim(), binding.editTextServerAddr.text.toString().trim() ) diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/DNSServerDialogFragment.kt b/app/src/main/java/ru/karasevm/privatednstoggle/ui/DNSServerDialogFragment.kt similarity index 51% rename from app/src/main/java/ru/karasevm/privatednstoggle/DNSServerDialogFragment.kt rename to app/src/main/java/ru/karasevm/privatednstoggle/ui/DNSServerDialogFragment.kt index 3ea8912..b0ac54a 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/DNSServerDialogFragment.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/ui/DNSServerDialogFragment.kt @@ -1,17 +1,22 @@ -package ru.karasevm.privatednstoggle +package ru.karasevm.privatednstoggle.ui import android.app.Dialog import android.content.Intent -import android.content.SharedPreferences import android.os.Bundle import android.widget.Toast import androidx.fragment.app.DialogFragment +import androidx.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.utils.PreferenceHelper.defaultPreference -import ru.karasevm.privatednstoggle.utils.PreferenceHelper.dns_servers -import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils +import ru.karasevm.privatednstoggle.model.DnsServer +import ru.karasevm.privatednstoggle.util.PrivateDNSUtils class DNSServerDialogFragment : DialogFragment() { @@ -19,37 +24,36 @@ class DNSServerDialogFragment : DialogFragment() { private val binding get() = _binding!! private lateinit var linearLayoutManager: LinearLayoutManager - private lateinit var adapter: RecyclerAdapter - private var items = mutableListOf() - private lateinit var sharedPrefs: SharedPreferences + private lateinit var adapter: ServerListRecyclerAdapter + private var servers: MutableList = mutableListOf() + private val dnsServerViewModel: DnsServerViewModel by viewModels { DnsServerViewModelFactory((requireActivity().application as PrivateDNSApp).repository) } + 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 - sharedPrefs = defaultPreference(requireContext()) - items = sharedPrefs.dns_servers - if (items[0] == "") { - items.removeAt(0) - items.add("dns.google") - } - - items.add(0, resources.getString(R.string.dns_auto)) - items.add(0, resources.getString(R.string.dns_off)) - - adapter = RecyclerAdapter(items, false) {} + adapter = ServerListRecyclerAdapter(false) binding.recyclerView.adapter = adapter - - - val startIntent = Intent(context, MainActivity::class.java) - + 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( @@ -65,9 +69,9 @@ class DNSServerDialogFragment : DialogFragment() { override fun onStart() { super.onStart() - adapter.onItemClick = { position -> - when (position) { - 0 -> { + adapter.onItemClick = { id -> + when (id) { + OFF_ID -> { PrivateDNSUtils.setPrivateMode( requireActivity().contentResolver, PrivateDNSUtils.DNS_MODE_OFF @@ -75,7 +79,7 @@ class DNSServerDialogFragment : DialogFragment() { Toast.makeText(context, R.string.set_to_off_toast, Toast.LENGTH_SHORT).show() } - 1 -> { + AUTO_ID -> { PrivateDNSUtils.setPrivateMode( requireActivity().contentResolver, PrivateDNSUtils.DNS_MODE_AUTO @@ -84,20 +88,25 @@ class DNSServerDialogFragment : DialogFragment() { } else -> { - val server = items[position].split(" : ").last() - PrivateDNSUtils.setPrivateMode( - requireActivity().contentResolver, - PrivateDNSUtils.DNS_MODE_PRIVATE - ) - PrivateDNSUtils.setPrivateProvider( - requireActivity().contentResolver, - server - ) - Toast.makeText( - context, - getString(R.string.set_to_provider_toast, server), - Toast.LENGTH_SHORT - ).show() + lifecycleScope.launch { + val server = servers.find { server -> server.id == id } + PrivateDNSUtils.setPrivateMode( + requireActivity().contentResolver, + PrivateDNSUtils.DNS_MODE_PRIVATE + ) + PrivateDNSUtils.setPrivateProvider( + requireActivity().contentResolver, + server?.server + ) + Toast.makeText( + context, + getString( + R.string.set_to_provider_toast, + server?.label?.ifEmpty { server.server } + ), + Toast.LENGTH_SHORT + ).show() + } } } dialog?.dismiss() @@ -113,5 +122,7 @@ class DNSServerDialogFragment : DialogFragment() { companion object { const val TAG = "DNSServerDialogFragment" + private const val AUTO_ID = -1 + private const val OFF_ID = -2 } } \ No newline at end of file diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/DeleteServerDialogFragment.kt b/app/src/main/java/ru/karasevm/privatednstoggle/ui/DeleteServerDialogFragment.kt similarity index 88% rename from app/src/main/java/ru/karasevm/privatednstoggle/DeleteServerDialogFragment.kt rename to app/src/main/java/ru/karasevm/privatednstoggle/ui/DeleteServerDialogFragment.kt index b69ce1d..a6a2d98 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/DeleteServerDialogFragment.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/ui/DeleteServerDialogFragment.kt @@ -1,13 +1,14 @@ -package ru.karasevm.privatednstoggle +package ru.karasevm.privatednstoggle.ui import android.app.Dialog import android.content.Context import android.os.Bundle import androidx.fragment.app.DialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder +import ru.karasevm.privatednstoggle.R -class DeleteServerDialogFragment(private val position: Int) : DialogFragment() { +class DeleteServerDialogFragment(private val id: Int) : DialogFragment() { // Use this instance of the interface to deliver action events private lateinit var listener: NoticeDialogListener @@ -15,7 +16,7 @@ class DeleteServerDialogFragment(private val position: Int) : DialogFragment() { * implement this interface in order to receive event callbacks. * Each method passes the DialogFragment in case the host needs to query it. */ interface NoticeDialogListener { - fun onDialogPositiveClick(position: Int) + fun onDeleteDialogPositiveClick(id: Int) } // Override the Fragment.onAttach() method to instantiate the NoticeDialogListener @@ -43,7 +44,7 @@ class DeleteServerDialogFragment(private val position: Int) : DialogFragment() { .setPositiveButton( R.string.delete ) { _, _ -> - listener.onDialogPositiveClick(position) + listener.onDeleteDialogPositiveClick(id) } .setNegativeButton( R.string.cancel diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/MainActivity.kt b/app/src/main/java/ru/karasevm/privatednstoggle/ui/MainActivity.kt similarity index 59% rename from app/src/main/java/ru/karasevm/privatednstoggle/MainActivity.kt rename to app/src/main/java/ru/karasevm/privatednstoggle/ui/MainActivity.kt index 230eadb..1ee354e 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/MainActivity.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/ui/MainActivity.kt @@ -1,4 +1,4 @@ -package ru.karasevm.privatednstoggle +package ru.karasevm.privatednstoggle.ui import android.Manifest import android.content.ClipData @@ -18,9 +18,10 @@ import android.view.Menu import android.view.View import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.core.app.ActivityCompat import androidx.core.app.ShareCompat +import androidx.lifecycle.viewModelScope import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper.DOWN import androidx.recyclerview.widget.ItemTouchHelper.UP @@ -31,16 +32,21 @@ import com.google.gson.GsonBuilder import com.google.gson.JsonSyntaxException import com.google.gson.ToNumberPolicy import com.google.gson.reflect.TypeToken +import kotlinx.coroutines.launch import org.lsposed.hiddenapibypass.HiddenApiBypass import rikka.shizuku.Shizuku import rikka.shizuku.ShizukuBinderWrapper import rikka.shizuku.ShizukuProvider import rikka.shizuku.SystemServiceHelper +import ru.karasevm.privatednstoggle.PrivateDNSApp +import ru.karasevm.privatednstoggle.R +import ru.karasevm.privatednstoggle.data.DnsServerViewModel +import ru.karasevm.privatednstoggle.data.DnsServerViewModelFactory import ru.karasevm.privatednstoggle.databinding.ActivityMainBinding -import ru.karasevm.privatednstoggle.utils.PreferenceHelper -import ru.karasevm.privatednstoggle.utils.PreferenceHelper.dns_servers -import ru.karasevm.privatednstoggle.utils.PreferenceHelper.export -import ru.karasevm.privatednstoggle.utils.PreferenceHelper.import +import ru.karasevm.privatednstoggle.model.DnsServer +import ru.karasevm.privatednstoggle.util.BackupUtils +import ru.karasevm.privatednstoggle.util.PreferenceHelper +import ru.karasevm.privatednstoggle.util.PreferenceHelper.dns_servers class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogListener, @@ -50,81 +56,129 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi private lateinit var binding: ActivityMainBinding private var items = mutableListOf() private lateinit var sharedPrefs: SharedPreferences - private lateinit var adapter: RecyclerAdapter + private lateinit var adapter: ServerListRecyclerAdapter private lateinit var clipboard: ClipboardManager private lateinit var gson: Gson + private val dnsServerViewModel: DnsServerViewModel by viewModels { DnsServerViewModelFactory((application as PrivateDNSApp).repository) } private val itemTouchHelper by lazy { - val simpleItemTouchCallback = object : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) { + val simpleItemTouchCallback = + object : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) { + var dragFrom = -1 + var dragTo = -1 - override fun onMove( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder - ): Boolean { - adapter.onItemMove(viewHolder.adapterPosition, target.adapterPosition) - return true - } + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + if (dragFrom == viewHolder.bindingAdapterPosition && dragTo == target.bindingAdapterPosition) { + return true + } + // store the drag position + if (dragFrom == -1) dragFrom = viewHolder.bindingAdapterPosition + dragTo = target.bindingAdapterPosition + adapter.onItemMove( + viewHolder.bindingAdapterPosition, + target.bindingAdapterPosition + ) + return true + } - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} - override fun onSelectedChanged( - viewHolder: RecyclerView.ViewHolder?, actionState: Int - ) { - super.onSelectedChanged(viewHolder, actionState) - if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) { - viewHolder?.itemView?.apply { - // Example: Elevate the view - elevation = 8f - alpha = 0.5f - setBackgroundColor(Color.GRAY) + override fun onSelectedChanged( + viewHolder: RecyclerView.ViewHolder?, actionState: Int + ) { + super.onSelectedChanged(viewHolder, actionState) + if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) { + viewHolder?.itemView?.apply { + // Example: Elevate the view + elevation = 8f + alpha = 0.5f + setBackgroundColor(Color.GRAY) + } } } - } - override fun clearView( - recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder - ) { - super.clearView(recyclerView, viewHolder) - viewHolder.itemView.apply { - // Reset the appearance - elevation = 0f - alpha = 1.0f - setBackgroundColor(Color.TRANSPARENT) + override fun clearView( + recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder + ) { + super.clearView(recyclerView, viewHolder) + viewHolder.itemView.apply { + // Reset the appearance + elevation = 0f + alpha = 1.0f + setBackgroundColor(Color.TRANSPARENT) + } + // commit the change to the db + dnsServerViewModel.move( + dragFrom, + dragTo, + (viewHolder as ServerListRecyclerAdapter.DnsServerViewHolder).id + ) + dragTo = -1 + dragFrom = -1 } } - } ItemTouchHelper(simpleItemTouchCallback) } private fun importSettings(json: String) { runCatching { - val objectType = object : TypeToken>() {}.type - val data: Map = gson.fromJson(json, objectType) - sharedPrefs.import(data) + val data: BackupUtils.Backup = gson.fromJson(json, BackupUtils.Backup::class.java) + BackupUtils.import(data, dnsServerViewModel, sharedPrefs) }.onSuccess { Toast.makeText( this, getString(R.string.import_success), Toast.LENGTH_SHORT ).show() - ActivityCompat.recreate(this) }.onFailure { exception -> - Log.e("IMPORT", "Import failed", exception) - when (exception) { - is JsonSyntaxException -> { - Toast.makeText( - this, getString(R.string.import_failure_json), Toast.LENGTH_SHORT - ).show() - } + runCatching { + val objectType = object : TypeToken>() {}.type + val data: Map = gson.fromJson(json, objectType) + BackupUtils.importLegacy(data, dnsServerViewModel, sharedPrefs) + }.onSuccess { + Toast.makeText( + this, getString(R.string.import_success), Toast.LENGTH_SHORT + ).show() + }.onFailure { + Log.e("IMPORT", "Import failed", exception) + when (exception) { + is JsonSyntaxException -> { + Toast.makeText( + this, getString(R.string.import_failure_json), Toast.LENGTH_SHORT + ).show() + } - else -> { - Toast.makeText( - this, getString(R.string.import_failure), Toast.LENGTH_SHORT - ).show() + else -> { + Toast.makeText( + this, getString(R.string.import_failure), Toast.LENGTH_SHORT + ).show() + } } } } } + /** + * Migrate the SharedPreferences server list to Room + */ + private fun migrateServerList() { + if (sharedPrefs.dns_servers.isNotEmpty() && sharedPrefs.dns_servers[0] != "") { + Log.i( + "migrate", + "existing sharedPrefs list: ${sharedPrefs.dns_servers} ${sharedPrefs.dns_servers.size}" + ) + sharedPrefs.dns_servers.forEach { server -> + val parts = server.split(" : ").toMutableList() + if (parts.size != 2) parts.add(0, "") + Log.i("migrate", "migrating: $server -> $parts") + dnsServerViewModel.insert(DnsServer(0, parts[1], parts[0])) + } + sharedPrefs.dns_servers = emptyList().toMutableList() + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -141,38 +195,41 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager gson = GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create() + migrateServerList() + items = sharedPrefs.dns_servers if (items[0] == "") { items.removeAt(0) } - updateEmptyView() - adapter = RecyclerAdapter(items, true) { updateEmptyView() } - adapter.onItemClick = { position -> - val data = items[position].split(" : ") - val label: String? - val server: String - if (data.size == 2) { - label = data[0] - server = data[1] + adapter = ServerListRecyclerAdapter(true) + binding.recyclerView.adapter = adapter + + dnsServerViewModel.allServers.observe(this) { servers -> + adapter.submitList(servers) + if (servers.isEmpty()) { + binding.emptyView.visibility = View.VISIBLE + binding.emptyViewHint.visibility = View.VISIBLE + } else { + binding.emptyView.visibility = View.GONE + binding.emptyViewHint.visibility = View.GONE } - else { - label = null - server = data[0] - } - val newFragment = AddServerDialogFragment(position, label, server) - newFragment.show(supportFragmentManager, "edit_server") } - adapter.onItemsChanged = { swappedItems -> - items = swappedItems - sharedPrefs.dns_servers = swappedItems - updateEmptyView() + adapter.onItemClick = { id -> + dnsServerViewModel.viewModelScope.launch { + val server = dnsServerViewModel.getById(id) + if (server != null) { + val newFragment = + AddServerDialogFragment(server) + newFragment.show(supportFragmentManager, "edit_server") + } + } } adapter.onDragStart = { viewHolder -> itemTouchHelper.startDrag(viewHolder) } binding.floatingActionButton.setOnClickListener { - val newFragment = AddServerDialogFragment(null, null, null) + val newFragment = AddServerDialogFragment(null) newFragment.show(supportFragmentManager, "add_server") } binding.recyclerView.adapter = adapter @@ -190,31 +247,40 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi } R.id.export_settings_clipboard -> { - val data = sharedPrefs.export() - val jsonData = gson.toJson(data) - clipboard.setPrimaryClip(ClipData.newPlainText("", jsonData)) - // Only show a toast for Android 12 and lower. - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) Toast.makeText( - this, getString(R.string.copy_success), Toast.LENGTH_SHORT - ).show() + dnsServerViewModel.viewModelScope.launch { + val data = BackupUtils.export(dnsServerViewModel, sharedPrefs) + val jsonData = gson.toJson(data) + clipboard.setPrimaryClip(ClipData.newPlainText("", jsonData)) + // Only show a toast for Android 12 and lower. + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) Toast.makeText( + applicationContext, getString(R.string.copy_success), Toast.LENGTH_SHORT + ).show() + } true } R.id.export_settings_share -> { - val data = sharedPrefs.export() - val jsonData = gson.toJson(data) - ShareCompat.IntentBuilder(this).setText(jsonData).setType("text/plain") - .startChooser() + val activityContext = this + dnsServerViewModel.viewModelScope.launch { + val data = BackupUtils.export(dnsServerViewModel, sharedPrefs) + val jsonData = gson.toJson(data) + ShareCompat.IntentBuilder(activityContext).setText(jsonData) + .setType("text/plain") + .startChooser() + } true } R.id.export_settings_file -> { - val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = "text/plain" - putExtra(Intent.EXTRA_TITLE, "private-dns-export") + + dnsServerViewModel.viewModelScope.launch { + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "text/plain" + putExtra(Intent.EXTRA_TITLE, "private-dns-export") + } + saveResultLauncher.launch(intent) } - saveResultLauncher.launch(intent) true } @@ -248,22 +314,12 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi } } - private fun updateEmptyView() { - if (items.isEmpty()) { - binding.emptyView.visibility = View.VISIBLE - binding.emptyViewHint.visibility = View.VISIBLE - } else { - binding.emptyView.visibility = View.GONE - binding.emptyViewHint.visibility = View.GONE - } - } - private var saveResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == RESULT_OK) { val data: Intent? = result.data data?.data?.also { uri -> - val jsonData = gson.toJson(sharedPrefs.export()) + val jsonData = gson.toJson(BackupUtils.export(dnsServerViewModel, sharedPrefs)) val contentResolver = applicationContext.contentResolver runCatching { contentResolver.openOutputStream(uri)?.use { outputStream -> @@ -359,46 +415,58 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi Shizuku.removeRequestPermissionResultListener(this::onRequestPermissionResult) } - override fun onDeleteItemClicked(position: Int) { - val newFragment = DeleteServerDialogFragment(position) + /** + * Show the dialog for deleting the server + * @param id The server id + */ + override fun onDeleteItemClicked(id: Int) { + val newFragment = DeleteServerDialogFragment(id) newFragment.show(supportFragmentManager, "delete_server") } - override fun onDialogPositiveClick(label: String?, server: String) { + /** + * Callback for adding the server + * @param label The label + * @param server The server + */ + override fun onAddDialogPositiveClick(label: String?, server: String) { if (server.isEmpty()) { Toast.makeText(this, R.string.server_length_error, Toast.LENGTH_SHORT).show() return } + if (label.isNullOrEmpty()) { - items.add(server) + dnsServerViewModel.insert(DnsServer(0, server)) } else { - items.add("$label : $server") + dnsServerViewModel.insert(DnsServer(0, server, label)) } - adapter.setData(items.toMutableList()) - binding.recyclerView.adapter?.notifyItemInserted(items.size - 1) - sharedPrefs.dns_servers = items } - override fun onDialogPositiveClick(position: Int) { - items.removeAt(position) - adapter.setData(items.toMutableList()) - adapter.notifyItemRemoved(position) - sharedPrefs.dns_servers = items + /** + * Callback for deleting the server + * @param id The server id + */ + override fun onDeleteDialogPositiveClick(id: Int) { + dnsServerViewModel.delete(id) } - override fun onDialogPositiveClick(label: String?, server: String, position: Int) { + /** + * 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 } - if (label.isNullOrEmpty()) { - items[position] = server - } else { - items[position] = "$label : $server" - } - adapter.notifyItemChanged(position) - sharedPrefs.dns_servers = items - binding.recyclerView.adapter?.notifyItemChanged(position) + dnsServerViewModel.update(id, server, label, null, enabled) } /** diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/OptionsDialogFragment.kt b/app/src/main/java/ru/karasevm/privatednstoggle/ui/OptionsDialogFragment.kt similarity index 88% rename from app/src/main/java/ru/karasevm/privatednstoggle/OptionsDialogFragment.kt rename to app/src/main/java/ru/karasevm/privatednstoggle/ui/OptionsDialogFragment.kt index e4b9cb4..094d1ba 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/OptionsDialogFragment.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/ui/OptionsDialogFragment.kt @@ -1,14 +1,15 @@ -package ru.karasevm.privatednstoggle +package ru.karasevm.privatednstoggle.ui import android.app.Dialog import android.os.Bundle import androidx.fragment.app.DialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder +import ru.karasevm.privatednstoggle.R import ru.karasevm.privatednstoggle.databinding.DialogOptionsBinding -import ru.karasevm.privatednstoggle.utils.PreferenceHelper -import ru.karasevm.privatednstoggle.utils.PreferenceHelper.autoMode -import ru.karasevm.privatednstoggle.utils.PreferenceHelper.requireUnlock -import ru.karasevm.privatednstoggle.utils.PrivateDNSUtils +import ru.karasevm.privatednstoggle.util.PreferenceHelper +import ru.karasevm.privatednstoggle.util.PreferenceHelper.autoMode +import ru.karasevm.privatednstoggle.util.PreferenceHelper.requireUnlock +import ru.karasevm.privatednstoggle.util.PrivateDNSUtils class OptionsDialogFragment : DialogFragment() { private var _binding: DialogOptionsBinding? = null @@ -27,6 +28,7 @@ class OptionsDialogFragment : DialogFragment() { .setView(view) .setPositiveButton(R.string.ok, null) builder.create() + } ?: throw IllegalStateException("Activity cannot be null") } diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/ui/ServerListRecyclerAdapter.kt b/app/src/main/java/ru/karasevm/privatednstoggle/ui/ServerListRecyclerAdapter.kt new file mode 100644 index 0000000..fec1eb0 --- /dev/null +++ b/app/src/main/java/ru/karasevm/privatednstoggle/ui/ServerListRecyclerAdapter.kt @@ -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() { + + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DnsServerViewHolder { + val view = + LayoutInflater.from(parent.context).inflate(R.layout.recyclerview_row, parent, false) + val vh = DnsServerViewHolder(view) + return vh + } + + override fun getItemCount(): Int { + return items.size + } + + var onItemClick: ((Int) -> Unit)? = null + var onDragStart: ((DnsServerViewHolder) -> Unit)? = null + private var items: MutableList = mutableListOf() + + @SuppressLint("ClickableViewAccessibility") + override fun onBindViewHolder(holder: DnsServerViewHolder, position: Int) { + val item = items[position] + if (item.label.isNotEmpty()) { + holder.labelTextView.text = item.label + holder.labelTextView.visibility = View.VISIBLE + } else { + holder.labelTextView.visibility = View.GONE + } + holder.serverTextView.text = item.server + holder.id = item.id + if (item.enabled) { + holder.labelTextView.alpha = 1f + holder.serverTextView.alpha = 1f + } else { + holder.labelTextView.alpha = 0.5f + holder.serverTextView.alpha = 0.5f + } + if (showDragHandle) { + holder.dragHandle.visibility = View.VISIBLE + holder.dragHandle.setOnTouchListener { _, event -> + if (event.actionMasked == MotionEvent.ACTION_DOWN) { + onDragStart?.invoke(holder) + } + return@setOnTouchListener true + } + } + } + + /** + * Update server position in memory + * @param fromPosition old position + * @param toPosition new position + */ + fun onItemMove(fromPosition: Int, toPosition: Int) { + items.add(toPosition, items.removeAt(fromPosition)) + notifyItemMoved(fromPosition, toPosition) + } + + class DiffCallback( + private val oldList: List, private var newList: List + ) : DiffUtil.Callback() { + + override fun getOldListSize(): Int { + return oldList.size + } + + override fun getNewListSize(): Int { + return newList.size + } + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldList[oldItemPosition] + val newItem = newList[newItemPosition] + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldList[oldItemPosition] + val newItem = newList[newItemPosition] + return oldItem.server == newItem.server && oldItem.label == newItem.label && oldItem.enabled == newItem.enabled + } + } + + /** + * Submit list to adapter + * @param list list to submit + */ + fun submitList(list: List) { + val diffCallback = DiffCallback(items, list) + val diffResult = DiffUtil.calculateDiff(diffCallback) + items.clear() + items.addAll(list) + diffResult.dispatchUpdatesTo(this) + } + + inner class DnsServerViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val labelTextView: TextView = view.findViewById(R.id.labelTextView) + val serverTextView: TextView = view.findViewById(R.id.textView) + val dragHandle: ImageView = itemView.findViewById(R.id.dragHandle) + var id = 0 + + init { + view.setOnClickListener { + onItemClick?.invoke(id) + } + } + } + +} diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/SettingsDialogActivity.kt b/app/src/main/java/ru/karasevm/privatednstoggle/ui/SettingsDialogActivity.kt similarity index 89% rename from app/src/main/java/ru/karasevm/privatednstoggle/SettingsDialogActivity.kt rename to app/src/main/java/ru/karasevm/privatednstoggle/ui/SettingsDialogActivity.kt index c8a86b6..4b15e05 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/SettingsDialogActivity.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/ui/SettingsDialogActivity.kt @@ -1,4 +1,4 @@ -package ru.karasevm.privatednstoggle +package ru.karasevm.privatednstoggle.ui import android.os.Bundle import androidx.appcompat.app.AppCompatActivity diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/util/BackupUtils.kt b/app/src/main/java/ru/karasevm/privatednstoggle/util/BackupUtils.kt new file mode 100644 index 0000000..715c720 --- /dev/null +++ b/app/src/main/java/ru/karasevm/privatednstoggle/util/BackupUtils.kt @@ -0,0 +1,75 @@ +package ru.karasevm.privatednstoggle.util + +import android.content.SharedPreferences +import com.google.gson.annotations.SerializedName +import ru.karasevm.privatednstoggle.data.DnsServerViewModel +import ru.karasevm.privatednstoggle.model.DnsServer +import ru.karasevm.privatednstoggle.util.PreferenceHelper.AUTO_MODE +import ru.karasevm.privatednstoggle.util.PreferenceHelper.DNS_SERVERS +import ru.karasevm.privatednstoggle.util.PreferenceHelper.REQUIRE_UNLOCK +import ru.karasevm.privatednstoggle.util.PreferenceHelper.autoMode +import ru.karasevm.privatednstoggle.util.PreferenceHelper.requireUnlock + +object BackupUtils { + data class Backup( + @SerializedName("dns_servers") val dnsServers: List, + @SerializedName("auto_mode") val autoMode: Int?, + @SerializedName("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 map Deserialized backup + * @param viewModel View model + * @param sharedPreferences Shared preferences + */ + fun importLegacy( + map: Map, + viewModel: DnsServerViewModel, + sharedPreferences: SharedPreferences + ) { + map[DNS_SERVERS]?.let { servers -> + if (servers is String) { + 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 = map[AUTO_MODE] as? Int ?: 0 + sharedPreferences.requireUnlock = map[REQUIRE_UNLOCK] as? Boolean ?: false + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/utils/PrivateDNSUtils.kt b/app/src/main/java/ru/karasevm/privatednstoggle/util/PrivateDNSUtils.kt similarity index 90% rename from app/src/main/java/ru/karasevm/privatednstoggle/utils/PrivateDNSUtils.kt rename to app/src/main/java/ru/karasevm/privatednstoggle/util/PrivateDNSUtils.kt index 79ae536..73a1bc9 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/utils/PrivateDNSUtils.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/util/PrivateDNSUtils.kt @@ -1,4 +1,4 @@ -package ru.karasevm.privatednstoggle.utils +package ru.karasevm.privatednstoggle.util import android.Manifest import android.content.ContentResolver @@ -23,18 +23,23 @@ object PrivateDNSUtils { private const val PRIVATE_DNS_MODE = "private_dns_mode" private const val PRIVATE_DNS_PROVIDER = "private_dns_specifier" + + // Gets the system dns mode fun getPrivateMode(contentResolver: ContentResolver): String { return Settings.Global.getString(contentResolver, PRIVATE_DNS_MODE) } + // Gets the system dns provider fun getPrivateProvider(contentResolver: ContentResolver): String { return Settings.Global.getString(contentResolver, PRIVATE_DNS_PROVIDER) } + // Sets the system dns mode fun setPrivateMode(contentResolver: ContentResolver, value: String) { Settings.Global.putString(contentResolver, PRIVATE_DNS_MODE, value) } + // Sets the system dns provider fun setPrivateProvider(contentResolver: ContentResolver, value: String?) { Settings.Global.putString(contentResolver, PRIVATE_DNS_PROVIDER, value) } diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/utils/SharedPrefUtils.kt b/app/src/main/java/ru/karasevm/privatednstoggle/util/SharedPrefUtils.kt similarity index 67% rename from app/src/main/java/ru/karasevm/privatednstoggle/utils/SharedPrefUtils.kt rename to app/src/main/java/ru/karasevm/privatednstoggle/util/SharedPrefUtils.kt index 8e97931..129a56b 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/utils/SharedPrefUtils.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/util/SharedPrefUtils.kt @@ -1,13 +1,13 @@ -package ru.karasevm.privatednstoggle.utils +package ru.karasevm.privatednstoggle.util import android.content.Context import android.content.SharedPreferences object PreferenceHelper { - private const val DNS_SERVERS = "dns_servers" - private const val AUTO_MODE = "auto_mode" - private const val REQUIRE_UNLOCK = "require_unlock" + const val DNS_SERVERS = "dns_servers" + const val AUTO_MODE = "auto_mode" + const val REQUIRE_UNLOCK = "require_unlock" fun defaultPreference(context: Context): SharedPreferences = context.getSharedPreferences("app_prefs", 0) @@ -38,6 +38,7 @@ object PreferenceHelper { } } + var SharedPreferences.autoMode get() = getInt(AUTO_MODE, PrivateDNSUtils.AUTO_MODE_OPTION_OFF) set(value) { @@ -53,24 +54,4 @@ object PreferenceHelper { it.put(REQUIRE_UNLOCK to value) } } - - // export all the preferences - fun SharedPreferences.export() = mapOf( - DNS_SERVERS to getString(DNS_SERVERS, ""), - AUTO_MODE to autoMode, - REQUIRE_UNLOCK to requireUnlock - ) - - // import all the preferences - fun SharedPreferences.import(map: Map) { - editMe { - map.forEach { (key, value) -> - if (value is Number) { - it.put(key to value.toInt()) - return@forEach - } - it.put(key to value) - } - } - } } diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/utils/DnsServer.kt b/app/src/main/java/ru/karasevm/privatednstoggle/utils/DnsServer.kt deleted file mode 100644 index 8a51706..0000000 --- a/app/src/main/java/ru/karasevm/privatednstoggle/utils/DnsServer.kt +++ /dev/null @@ -1,7 +0,0 @@ -package ru.karasevm.privatednstoggle.utils - -data class DnsServer(val label: String, val server: String) { - override fun toString(): String { - return "$label : $server" - } -} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index eb0201d..f918023 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".MainActivity"> + tools:context=".ui.MainActivity"> + + + app:layout_constraintTop_toBottomOf="@id/serverEnabledSwitch"> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 22ecfdd..3643757 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -12,6 +12,4 @@ #CCC2DC #EFB8C8 #1C1B1F - - #747879 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dbed076..1d41b73 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -47,4 +47,5 @@ Edit server No Servers Added Tap on the button below to add one + Enabled \ No newline at end of file diff --git a/build.gradle b/build.gradle index 5a61030..59c7c50 100644 --- a/build.gradle +++ b/build.gradle @@ -5,13 +5,17 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.5.1' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22" + classpath 'com.android.tools.build:gradle:8.5.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } +plugins { + id 'org.jetbrains.kotlin.android' version '1.9.10' apply false + id "com.google.devtools.ksp" version "1.9.10-1.0.13" apply false +} tasks.register('clean', Delete) { delete rootProject.layout.buildDirectory diff --git a/fastlane/metadata/android/en-US/changelogs/16.txt b/fastlane/metadata/android/en-US/changelogs/16.txt new file mode 100644 index 0000000..1005cb7 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/16.txt @@ -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 From fa9d259a21181640f6dbba4be732ffc1e3f3ab80 Mon Sep 17 00:00:00 2001 From: Weiguangtwk <41853928+WeiguangTWK@users.noreply.github.com> Date: Tue, 24 Sep 2024 18:43:47 +0800 Subject: [PATCH 18/52] Add Chinese Simplified Translation (#32) * Add Chinese Simplified Translation * Add new translation --------- Co-authored-by: WeiguangTWK --- app/src/main/res/values-zh-rCN/strings.xml | 51 ++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 app/src/main/res/values-zh-rCN/strings.xml diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml new file mode 100644 index 0000000..a217315 --- /dev/null +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,51 @@ + + + 私有DNS触发 + 必要权限未授予,请看相关说明 + 关闭 + 自动 + 未知 + 添加DNS服务器 + 添加 + 存储 + 隐私策略 + 选择服务器 + 完成 + 取消 + 删除条目 + 你确认要删除这个服务器条目吗? + 删除 + 服务器地址不可为空 + DNS服务器标识 + DNS服务器地址 + 选项 + 确认 + 选择要在磁贴中启用的选项 + 仅“关闭” + 仅“自动” + “关闭“与”自动“ + 仅设置的私有DNS + 打开软件 + 不使用私有DNS + 自动使用私有DNS + 设置为使用私有DNS\"%1$s\" + 更改服务器设置要求设备解锁 + 拖动把手 + 导入 + 导出 + 已导入 + 导入失败 + 导入失败,json格式异常 + 已复制 + 从文件导入 + 从剪贴板导入 + 导出至剪贴板 + 分享 + 导出至文件 + 保存失败 + 保存成功 + 编辑服务器条目 + 无可用服务器 + 点击下方\"+\"添加一个吧 + 已启用 + \ No newline at end of file From 6a5f40521182213d4bc7d29e46906465d8890521 Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Thu, 3 Oct 2024 16:38:23 +0300 Subject: [PATCH 19/52] Replace gson with kotlinx-serialization --- .gitignore | 3 ++ .idea/kotlinc.xml | 2 +- app/build.gradle | 8 ++-- .../privatednstoggle/model/DnsServer.kt | 12 +++--- .../privatednstoggle/ui/MainActivity.kt | 39 ++++++------------- .../privatednstoggle/util/BackupUtils.kt | 32 ++++++++------- build.gradle | 7 ++-- 7 files changed, 50 insertions(+), 53 deletions(-) diff --git a/.gitignore b/.gitignore index 8c470b3..c216f78 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,6 @@ fastlane/Preview.html fastlane/screenshots fastlane/test_output fastlane/readme.md + +# kotlin +.kotlin/ diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index f8467b4..d4b7acc 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 2d17775..38f3438 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,6 +2,7 @@ plugins { id 'com.android.application' id 'kotlin-android' id 'com.google.devtools.ksp' + id 'org.jetbrains.kotlin.plugin.serialization' } android { @@ -51,11 +52,12 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'androidx.activity:activity-ktx:1.9.2' - implementation 'androidx.fragment:fragment-ktx:1.8.3' + implementation 'androidx.fragment:fragment-ktx:1.8.4' implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'com.google.guava:guava:33.1.0-android' - implementation 'com.google.code.gson:gson:2.11.0' + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3' + def shizuku_version = '13.1.5' implementation "dev.rikka.shizuku:api:$shizuku_version" @@ -71,7 +73,7 @@ dependencies { androidTestImplementation "androidx.room:room-testing:$roomVersion" // Lifecycle components - def lifecycleVersion = '2.8.5' + def lifecycleVersion = '2.8.6' implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion" diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/model/DnsServer.kt b/app/src/main/java/ru/karasevm/privatednstoggle/model/DnsServer.kt index 35375d1..38c1be4 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/model/DnsServer.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/model/DnsServer.kt @@ -3,19 +3,21 @@ package ru.karasevm.privatednstoggle.model import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -import com.google.gson.annotations.SerializedName +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( - @SerializedName("id") + @SerialName("id") @PrimaryKey(autoGenerate = true) val id: Int = 0, - @SerializedName("server") + @SerialName("server") val server: String = "", - @SerializedName("label") + @SerialName("label") val label: String = "", - @SerializedName("enabled") + @SerialName("enabled") @ColumnInfo(defaultValue = "1") val enabled: Boolean = true, val sortOrder: Int? = null diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/ui/MainActivity.kt b/app/src/main/java/ru/karasevm/privatednstoggle/ui/MainActivity.kt index 1ee354e..e029c25 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/ui/MainActivity.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/ui/MainActivity.kt @@ -27,12 +27,9 @@ import androidx.recyclerview.widget.ItemTouchHelper.DOWN import androidx.recyclerview.widget.ItemTouchHelper.UP import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.google.gson.Gson -import com.google.gson.GsonBuilder -import com.google.gson.JsonSyntaxException -import com.google.gson.ToNumberPolicy -import com.google.gson.reflect.TypeToken import kotlinx.coroutines.launch +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import org.lsposed.hiddenapibypass.HiddenApiBypass import rikka.shizuku.Shizuku import rikka.shizuku.ShizukuBinderWrapper @@ -58,7 +55,6 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi private lateinit var sharedPrefs: SharedPreferences private lateinit var adapter: ServerListRecyclerAdapter private lateinit var clipboard: ClipboardManager - private lateinit var gson: Gson private val dnsServerViewModel: DnsServerViewModel by viewModels { DnsServerViewModelFactory((application as PrivateDNSApp).repository) } private val itemTouchHelper by lazy { @@ -126,7 +122,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi private fun importSettings(json: String) { runCatching { - val data: BackupUtils.Backup = gson.fromJson(json, BackupUtils.Backup::class.java) + val data: BackupUtils.Backup = Json.decodeFromString(json) BackupUtils.import(data, dnsServerViewModel, sharedPrefs) }.onSuccess { Toast.makeText( @@ -134,28 +130,18 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi ).show() }.onFailure { exception -> runCatching { - val objectType = object : TypeToken>() {}.type - val data: Map = gson.fromJson(json, objectType) + Log.e("IMPORT", "Malformed json, falling back to legacy", exception) + val data = Json.decodeFromString(json) BackupUtils.importLegacy(data, dnsServerViewModel, sharedPrefs) }.onSuccess { Toast.makeText( this, getString(R.string.import_success), Toast.LENGTH_SHORT ).show() - }.onFailure { + }.onFailure { exception -> Log.e("IMPORT", "Import failed", exception) - when (exception) { - is JsonSyntaxException -> { - Toast.makeText( - this, getString(R.string.import_failure_json), Toast.LENGTH_SHORT - ).show() - } - - else -> { - Toast.makeText( - this, getString(R.string.import_failure), Toast.LENGTH_SHORT - ).show() - } - } + Toast.makeText( + this, getString(R.string.import_failure), Toast.LENGTH_SHORT + ).show() } } } @@ -193,7 +179,6 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi sharedPrefs = PreferenceHelper.defaultPreference(this) clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager - gson = GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create() migrateServerList() @@ -249,7 +234,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi R.id.export_settings_clipboard -> { dnsServerViewModel.viewModelScope.launch { val data = BackupUtils.export(dnsServerViewModel, sharedPrefs) - val jsonData = gson.toJson(data) + 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( @@ -263,7 +248,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi val activityContext = this dnsServerViewModel.viewModelScope.launch { val data = BackupUtils.export(dnsServerViewModel, sharedPrefs) - val jsonData = gson.toJson(data) + val jsonData = Json.encodeToString(data) ShareCompat.IntentBuilder(activityContext).setText(jsonData) .setType("text/plain") .startChooser() @@ -319,7 +304,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi if (result.resultCode == RESULT_OK) { val data: Intent? = result.data data?.data?.also { uri -> - val jsonData = gson.toJson(BackupUtils.export(dnsServerViewModel, sharedPrefs)) + val jsonData = Json.encodeToString(BackupUtils.export(dnsServerViewModel, sharedPrefs)) val contentResolver = applicationContext.contentResolver runCatching { contentResolver.openOutputStream(uri)?.use { outputStream -> diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/util/BackupUtils.kt b/app/src/main/java/ru/karasevm/privatednstoggle/util/BackupUtils.kt index 715c720..549dd9c 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/util/BackupUtils.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/util/BackupUtils.kt @@ -1,20 +1,26 @@ package ru.karasevm.privatednstoggle.util import android.content.SharedPreferences -import com.google.gson.annotations.SerializedName +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.AUTO_MODE -import ru.karasevm.privatednstoggle.util.PreferenceHelper.DNS_SERVERS -import ru.karasevm.privatednstoggle.util.PreferenceHelper.REQUIRE_UNLOCK import ru.karasevm.privatednstoggle.util.PreferenceHelper.autoMode import ru.karasevm.privatednstoggle.util.PreferenceHelper.requireUnlock object BackupUtils { + @Serializable data class Backup( - @SerializedName("dns_servers") val dnsServers: List, - @SerializedName("auto_mode") val autoMode: Int?, - @SerializedName("require_unlock") val requireUnlock: Boolean?, + @SerialName("dns_servers") val dnsServers: List, + @SerialName("auto_mode") val autoMode: Int?, + @SerialName("require_unlock") val requireUnlock: Boolean?, + ) + + @Serializable + data class LegacyBackup( + @SerialName("dns_servers") val dnsServers: String, + @SerialName("auto_mode") val autoMode: Int?, + @SerialName("require_unlock") val requireUnlock: Boolean?, ) /** @@ -47,17 +53,16 @@ object BackupUtils { /** * Imports old server list - * @param map Deserialized backup + * @param legacyBackup Deserialized backup * @param viewModel View model * @param sharedPreferences Shared preferences */ fun importLegacy( - map: Map, + legacyBackup: LegacyBackup, viewModel: DnsServerViewModel, sharedPreferences: SharedPreferences ) { - map[DNS_SERVERS]?.let { servers -> - if (servers is String) { + legacyBackup.dnsServers.let { servers -> val serverList = servers.split(",") serverList.forEach { server -> val parts = server.split(" : ") @@ -67,9 +72,8 @@ object BackupUtils { viewModel.insert(DnsServer(0, server, "")) } } - } } - sharedPreferences.autoMode = map[AUTO_MODE] as? Int ?: 0 - sharedPreferences.requireUnlock = map[REQUIRE_UNLOCK] as? Boolean ?: false + sharedPreferences.autoMode = legacyBackup.autoMode?: 0 + sharedPreferences.requireUnlock = legacyBackup.requireUnlock?: false } } \ No newline at end of file diff --git a/build.gradle b/build.gradle index 59c7c50..5ba942e 100644 --- a/build.gradle +++ b/build.gradle @@ -6,15 +6,16 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:8.5.2' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10" + 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 '1.9.10' apply false - id "com.google.devtools.ksp" version "1.9.10-1.0.13" apply false + 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 } tasks.register('clean', Delete) { From 681e6ceef48b441ad7890bd800cebc1967878556 Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Thu, 3 Oct 2024 17:10:23 +0300 Subject: [PATCH 20/52] Reset provider when disabling through the dialog --- .../ui/DNSServerDialogFragment.kt | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/ui/DNSServerDialogFragment.kt b/app/src/main/java/ru/karasevm/privatednstoggle/ui/DNSServerDialogFragment.kt index b0ac54a..5b8bf0f 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/ui/DNSServerDialogFragment.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/ui/DNSServerDialogFragment.kt @@ -27,7 +27,7 @@ class DNSServerDialogFragment : DialogFragment() { private lateinit var adapter: ServerListRecyclerAdapter private var servers: MutableList = mutableListOf() private val dnsServerViewModel: DnsServerViewModel by viewModels { DnsServerViewModelFactory((requireActivity().application as PrivateDNSApp).repository) } - + private val contentResolver by lazy { requireActivity().contentResolver } override fun onCreateDialog( savedInstanceState: Bundle? @@ -73,17 +73,23 @@ class DNSServerDialogFragment : DialogFragment() { when (id) { OFF_ID -> { PrivateDNSUtils.setPrivateMode( - requireActivity().contentResolver, + 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( - requireActivity().contentResolver, + contentResolver, PrivateDNSUtils.DNS_MODE_AUTO ) + PrivateDNSUtils.setPrivateProvider( + contentResolver, + null) Toast.makeText(context, R.string.set_to_auto_toast, Toast.LENGTH_SHORT).show() } @@ -91,11 +97,11 @@ class DNSServerDialogFragment : DialogFragment() { lifecycleScope.launch { val server = servers.find { server -> server.id == id } PrivateDNSUtils.setPrivateMode( - requireActivity().contentResolver, + contentResolver, PrivateDNSUtils.DNS_MODE_PRIVATE ) PrivateDNSUtils.setPrivateProvider( - requireActivity().contentResolver, + contentResolver, server?.server ) Toast.makeText( From 14b320ac6821bce05941effe20adb12c48e780c1 Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Tue, 5 Nov 2024 15:53:22 +0300 Subject: [PATCH 21/52] Enable per-app language support --- app/build.gradle | 4 +++- app/src/main/res/resources.properties | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/resources.properties diff --git a/app/build.gradle b/app/build.gradle index 38f3438..6a8bb8a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,7 +7,9 @@ plugins { android { compileSdk 34 - + androidResources { + generateLocaleConfig true + } defaultConfig { applicationId "ru.karasevm.privatednstoggle" versionCode 16 diff --git a/app/src/main/res/resources.properties b/app/src/main/res/resources.properties new file mode 100644 index 0000000..d5a3ddc --- /dev/null +++ b/app/src/main/res/resources.properties @@ -0,0 +1 @@ +unqualifiedResLocale=en-US \ No newline at end of file From 835e9381eace2664240c0d7c5b5e4083b7100f14 Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Tue, 5 Nov 2024 15:54:07 +0300 Subject: [PATCH 22/52] Add Russian translation --- app/src/main/res/values-ru-rRU/strings.xml | 51 ++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 app/src/main/res/values-ru-rRU/strings.xml diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml new file mode 100644 index 0000000..c61ca11 --- /dev/null +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -0,0 +1,51 @@ + + Private DNS Quick Toggle + Переключить частный DNS + Разрешение не предоставлено, проверьте приложение для получения информации + Выкл + Авто + Неизвестно + Добавить сервер + Добавить + Сохранить + Политика конфиденциальности + Выбрать сервер + Готово + Отмена + Удалить + Вы уверены, что хотите удалить сервер? + Удалить + Адрес сервера не может быть пустым + Название DNS сервера (необязательно) + Адрес DNS сервера + Опции + OK + Выберите, какие опции включить в плитке + Только \"Выкл\" + Только \"Авто\" + \"Выкл\" и \"Авто\" + Только частный DNS + Открыть приложение + Частный DNS выключен + Частный DNS установлен на "Авто" + Частный DNS установлен на %1$s + Смена сервера требует разблокировки устройства + Ручка перетаскивания + Импорт + Экспорт + Успешно импортировано + Импорт не удался + Импорт не удался, некорректный JSON + Скопировано + Из файла + Из буфера обмена + В буфер обмена + Поделиться + В файл + Сохранение не удалось + Успешно сохранено + Редактировать сервер + Нет доступных серверов + Нажмите на кнопку ниже, чтобы добавить сервер + Включён + \ No newline at end of file From 7cdc2bbb843ba04a00479c1e597e5bd524a81885 Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Tue, 5 Nov 2024 15:59:41 +0300 Subject: [PATCH 23/52] Bump version --- app/build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/17.txt | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/17.txt diff --git a/app/build.gradle b/app/build.gradle index 6a8bb8a..ff42dc4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { } defaultConfig { applicationId "ru.karasevm.privatednstoggle" - versionCode 16 - versionName "1.9.0-beta1" + versionCode 17 + versionName "1.9.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" targetSdkVersion 34 diff --git a/fastlane/metadata/android/en-US/changelogs/17.txt b/fastlane/metadata/android/en-US/changelogs/17.txt new file mode 100644 index 0000000..3709a99 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/17.txt @@ -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 From 8c927d6b264ec974d2ec7b71587497428c55ee26 Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Wed, 6 Nov 2024 14:16:03 +0300 Subject: [PATCH 24/52] Remove region from RU locale --- app/src/main/res/{values-ru-rRU => values-ru}/strings.xml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/res/{values-ru-rRU => values-ru}/strings.xml (100%) diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru/strings.xml similarity index 100% rename from app/src/main/res/values-ru-rRU/strings.xml rename to app/src/main/res/values-ru/strings.xml From 402b0849544d9f5eeabcc94d5f9949d7255dbba7 Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Sun, 17 Nov 2024 12:57:43 +0300 Subject: [PATCH 25/52] Include Contributing section in the readme --- README.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a8c97eb..57ac435 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ [![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/karasevm/PrivateDNSAndroid/total)](https://github.com/karasevm/PrivateDNSAndroid/releases/latest) [![GitHub Release](https://img.shields.io/github/v/release/karasevm/PrivateDNSAndroid)](https://github.com/karasevm/PrivateDNSAndroid/releases/latest) [![IzzyOnDroid](https://img.shields.io/endpoint?url=https://apt.izzysoft.de/fdroid/api/v1/shield/ru.karasevm.privatednstoggle&label=IzzyOnDroid)](https://apt.izzysoft.de/fdroid/index/apk/ru.karasevm.privatednstoggle) +[![Translation status](https://hosted.weblate.org/widget/privatednsandroid/private-dns-quick-toggle/svg-badge.svg)](https://hosted.weblate.org/engage/privatednsandroid/) # Private DNS Quick Toggle A quick settings tile to switch your private dns provider. Supports any number of providers. Makes it easy to turn adblocking dns servers on or off with just @@ -12,11 +13,11 @@ a single tap. Get the latest apk on the [releases page](https://github.com/karasevm/PrivateDNSAndroid/releases/latest) or from [IzzyOnDroid repo](https://apt.izzysoft.de/fdroid/index/apk/ru.karasevm.privatednstoggle). -## Automatic (Shizuku) +### Automatic (Shizuku) 1. Install and start [Shizuku](https://shizuku.rikka.app/). 2. Start the app and allow Shizuku access when prompted. -## Manual +### Manual For the app to work properly you'll need to provide it permissions via ADB: 1. Get to your PC and download platform tools from google [here](https://developer.android.com/studio/releases/platform-tools). @@ -31,3 +32,18 @@ For the app to work properly you'll need to provide it permissions via ADB: 6. That's it, you should have the app installed. +## Contributing + +### Translation +The easiest way to contribute would be to submit a translation to your language. Thanks to Weblate gratis hosting for open-source projects you can do it without any programming knowledge on [their website](https://hosted.weblate.org/engage/privatednsandroid/). +#### Translation status + +Translation status + + +### Code +If you want to contribute code please try to adhere to the following guidelines: +- Include javadoc comments for all the public methods you add +- Keep the code neatly formatted, you can you the built-in Android Studio formatter +- Please describe what your code does and how does it do that when sending a PR +- Before sending a PR please test your change on the oldest and latest supported Android versions (9 and 14 at the time of writing) From 8c7ff2ca8ff6191277ffb6fa0cb595cd5bae2326 Mon Sep 17 00:00:00 2001 From: Pacuka <69674426+Pacuka@users.noreply.github.com> Date: Tue, 17 Dec 2024 11:13:46 +0100 Subject: [PATCH 26/52] Add files via upload (#43) Hungarian translation by Pacuka --- app/src/main/res/values-hu/strings.xml | 51 ++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 app/src/main/res/values-hu/strings.xml diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml new file mode 100644 index 0000000..3e9260d --- /dev/null +++ b/app/src/main/res/values-hu/strings.xml @@ -0,0 +1,51 @@ + + Privát DNS Gyorskapcsoló + Privát DNS Kapcsoló + Nincs engedély megadva, nézd meg az alkalmazásban, hogyan adhatod meg + Ki + Automatikus + Ismeretlen + Szerver hozzáadása + Hozzáadás + Mentés + Adatvédelmi irányelvek + Szerver kiválasztása + Kész + Mégse + Törlés + Biztosan törölni szeretnéd a szervert? + Törlés + A szervercím nem lehet üres + DNS szerver neve (opcionális) + DNS szerver címe + Beállítások + OK + Válaszd ki, mely opciók jelenjenek meg a csempén + Csak ki + Csak automatikus + Ki és automatikus + Csak Privát DNS + Alkalmazás megnyitása + Privát DNS kikapcsolva + Privát DNS automatikus módra állítva + Privát DNS beállítva: %1$s + Eszköz feloldása szükséges a szerver módosításához + Húzási fogantyú + Importálás + Exportálás + Importálva + Importálás sikertelen + Importálás sikertelen, hibás JSON + Másolva + Fájlból + Vágólapról + Vágólapra + Megosztás + Fájlba + Mentés sikertelen + Sikeresen mentve + Szerver szerkesztése + Nincsenek szerverek hozzáadva + Koppints az alábbi gombra, hogy hozzáadj egyet + Engedélyezve + From 627771d4b1aa028faebb8ba191399e8f610139a9 Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Fri, 17 Jan 2025 15:38:40 +0300 Subject: [PATCH 27/52] Migrate build config to Kotlin --- .idea/compiler.xml | 2 +- app/build.gradle | 86 ----------------------- app/build.gradle.kts | 88 ++++++++++++++++++++++++ build.gradle | 23 ------- build.gradle.kts | 19 +++++ gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle => settings.gradle.kts | 2 +- 7 files changed, 110 insertions(+), 112 deletions(-) delete mode 100644 app/build.gradle create mode 100644 app/build.gradle.kts delete mode 100644 build.gradle create mode 100644 build.gradle.kts rename settings.gradle => settings.gradle.kts (93%) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index b589d56..b86273d 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index ff42dc4..0000000 --- a/app/build.gradle +++ /dev/null @@ -1,86 +0,0 @@ -plugins { - id 'com.android.application' - id 'kotlin-android' - id 'com.google.devtools.ksp' - id 'org.jetbrains.kotlin.plugin.serialization' -} - -android { - compileSdk 34 - androidResources { - generateLocaleConfig true - } - defaultConfig { - applicationId "ru.karasevm.privatednstoggle" - versionCode 17 - versionName "1.9.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' - } - 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.13.1' - implementation 'androidx.appcompat:appcompat:1.7.0' - implementation 'androidx.recyclerview:recyclerview:1.3.2' - implementation 'androidx.activity:activity-ktx:1.9.2' - implementation 'androidx.fragment:fragment-ktx:1.8.4' - implementation 'com.google.android.material:material:1.12.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'com.google.guava:guava:33.1.0-android' - implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3' - - - def shizuku_version = '13.1.5' - implementation "dev.rikka.shizuku:api:$shizuku_version" - implementation "dev.rikka.shizuku:provider:$shizuku_version" - - implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3' - compileOnly 'dev.rikka.hidden:stub:4.3.2' - - // Room components - def roomVersion = '2.6.1' - implementation "androidx.room:room-ktx:$roomVersion" - ksp "androidx.room:room-compiler:$roomVersion" - androidTestImplementation "androidx.room:room-testing:$roomVersion" - - // Lifecycle components - def lifecycleVersion = '2.8.6' - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" - implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion" - - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.2.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' -} \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..438062a --- /dev/null +++ b/app/build.gradle.kts @@ -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.5") + implementation("com.google.android.material:material:1.12.0") + implementation("androidx.constraintlayout:constraintlayout:2.2.0") + implementation("com.google.guava:guava:33.1.0-android") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") + + val shizukuVersion = "13.1.5" + implementation("dev.rikka.shizuku:api:$shizukuVersion") + implementation("dev.rikka.shizuku:provider:$shizukuVersion") + compileOnly("dev.rikka.hidden:stub:4.3.3") + + implementation("org.lsposed.hiddenapibypass:hiddenapibypass:4.3") + + // Room components + val roomVersion = "2.6.1" + implementation("androidx.room:room-ktx:$roomVersion") + ksp("androidx.room:room-compiler:$roomVersion") + androidTestImplementation("androidx.room:room-testing:$roomVersion") + + // Lifecycle components + val lifecycleVersion = "2.8.7" + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion") + implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion") + + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.2.1") + androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") +} \ No newline at end of file diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 5ba942e..0000000 --- a/build.gradle +++ /dev/null @@ -1,23 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. -buildscript { - repositories { - google() - mavenCentral() - } - dependencies { - classpath 'com.android.tools.build:gradle:8.5.2' - 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 -} - -tasks.register('clean', Delete) { - delete rootProject.layout.buildDirectory -} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..461e086 --- /dev/null +++ b/build.gradle.kts @@ -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.0") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.20") + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} +plugins { + id("org.jetbrains.kotlin.android") version "2.0.20" apply false + id("org.jetbrains.kotlin.plugin.serialization") version "2.0.20" + id("com.google.devtools.ksp") version "2.0.20-1.0.25" apply false +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0725b7c..f537c92 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Aug 16 15:36:35 MSK 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle b/settings.gradle.kts similarity index 93% rename from settings.gradle rename to settings.gradle.kts index 4ccd1ba..72cb6bf 100644 --- a/settings.gradle +++ b/settings.gradle.kts @@ -6,4 +6,4 @@ dependencyResolutionManagement { } } rootProject.name = "Private DNS Quick Toggle" -include ':app' +include("app") From 5ba03acbcc678e22b7d169e3336dabb22071a833 Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Fri, 17 Jan 2025 15:44:32 +0300 Subject: [PATCH 28/52] Enable Strict Mode for debug builds --- .../karasevm/privatednstoggle/PrivateDNSApp.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/PrivateDNSApp.kt b/app/src/main/java/ru/karasevm/privatednstoggle/PrivateDNSApp.kt index 921e71a..c404503 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/PrivateDNSApp.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/PrivateDNSApp.kt @@ -1,6 +1,7 @@ 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 @@ -13,5 +14,20 @@ class PrivateDNSApp : Application() { override fun onCreate() { super.onCreate() DynamicColors.applyToActivitiesIfAvailable(this) + + if (BuildConfig.DEBUG){ + StrictMode.setThreadPolicy( + StrictMode.ThreadPolicy.Builder() + .detectAll() + .penaltyLog() + .build() + ) + StrictMode.setVmPolicy( + StrictMode.VmPolicy.Builder() + .detectAll() + .penaltyLog() + .build() + ) + } } } \ No newline at end of file From 4f6dc13c1299c49464fd89857c92047a6fa2b9b9 Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Fri, 17 Jan 2025 15:50:14 +0300 Subject: [PATCH 29/52] Fix miscellaneous bugs and simplify code --- .../service/DnsTileService.kt | 41 +++++++++++-------- .../ui/DNSServerDialogFragment.kt | 8 +++- .../ui/OptionsDialogFragment.kt | 16 ++++---- .../privatednstoggle/util/BackupUtils.kt | 2 +- .../privatednstoggle/util/PrivateDNSUtils.kt | 15 ++----- 5 files changed, 44 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/service/DnsTileService.kt b/app/src/main/java/ru/karasevm/privatednstoggle/service/DnsTileService.kt index f1b25ae..1bb1944 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/service/DnsTileService.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/service/DnsTileService.kt @@ -8,6 +8,7 @@ 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 @@ -35,6 +36,8 @@ 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() @@ -53,9 +56,8 @@ class DnsTileService : TileService() { val dnsMode = Settings.Global.getString(contentResolver, "private_dns_mode") val dnsProvider = Settings.Global.getString(contentResolver, "private_dns_specifier") - val sharedPrefs = PreferenceHelper.defaultPreference(this) if (dnsMode.equals(DNS_MODE_OFF, ignoreCase = true)) { - if (sharedPrefs.autoMode == AUTO_MODE_OPTION_AUTO || sharedPrefs.autoMode == AUTO_MODE_OPTION_OFF_AUTO) { + 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) @@ -66,10 +68,10 @@ class DnsTileService : TileService() { } else if (dnsMode.equals(DNS_MODE_PRIVATE, ignoreCase = true)) { scope.launch { if (getNextAddress(dnsProvider) == null) { - if (sharedPrefs.autoMode == AUTO_MODE_OPTION_PRIVATE) { + if (sharedPreferences.autoMode == AUTO_MODE_OPTION_PRIVATE) { changeDNSServer(DNS_MODE_PRIVATE, null) } else { - if (sharedPrefs.autoMode == AUTO_MODE_OPTION_AUTO) { + if (sharedPreferences.autoMode == AUTO_MODE_OPTION_AUTO) { changeDNSServer(DNS_MODE_AUTO, dnsProvider) } else { changeDNSServer(DNS_MODE_OFF, dnsProvider) @@ -134,10 +136,9 @@ class DnsTileService : TileService() { if (!checkForPermission(this)) { return } - val sharedPrefs = PreferenceHelper.defaultPreference(this) // Require unlock to change mode according to user preference - val requireUnlock = sharedPrefs.requireUnlock + val requireUnlock = sharedPreferences.requireUnlock if (isLocked && requireUnlock) { unlockAndRun(this::cycleState) } else { @@ -151,12 +152,13 @@ class DnsTileService : TileService() { * 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, - Tile.STATE_INACTIVE, + if (!isPermissionGranted) Tile.STATE_UNAVAILABLE else Tile.STATE_INACTIVE, getString(R.string.dns_off), R.drawable.ic_off_black_24dp ) @@ -165,7 +167,7 @@ class DnsTileService : TileService() { DNS_MODE_AUTO -> { setTile( qsTile, - Tile.STATE_INACTIVE, + if (!isPermissionGranted) Tile.STATE_UNAVAILABLE else Tile.STATE_INACTIVE, getString(R.string.dns_auto), R.drawable.ic_auto_black_24dp ) @@ -178,7 +180,7 @@ class DnsTileService : TileService() { val dnsServer = repository.getFirstByServer(activeAddress) setTile( qsTile, - Tile.STATE_ACTIVE, + 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 @@ -189,7 +191,7 @@ class DnsTileService : TileService() { else -> { setTile( qsTile, - Tile.STATE_INACTIVE, + if (!isPermissionGranted) Tile.STATE_UNAVAILABLE else Tile.STATE_INACTIVE, getString(R.string.dns_unknown), R.drawable.ic_unknown_black_24dp ) @@ -205,12 +207,10 @@ class DnsTileService : TileService() { override fun onStartListening() { super.onStartListening() - if (!checkForPermission(this)) { - return - } // Prevent some crashes if (qsTile == null) { + Log.w(TAG, "onStartListening: qsTile is null") return } @@ -222,14 +222,17 @@ class DnsTileService : TileService() { IntentFilter("refresh_tile"), ContextCompat.RECEIVER_NOT_EXPORTED ) - + isBroadcastReceiverRegistered = true refreshTile() - } override fun onStopListening() { super.onStopListening() - unregisterReceiver(broadcastReceiver) + if (isBroadcastReceiverRegistered) { + unregisterReceiver(broadcastReceiver) + isBroadcastReceiverRegistered = false + } + } override fun onDestroy() { @@ -292,4 +295,8 @@ class DnsTileService : TileService() { repository.getNextByServer(currentAddress) } } -} \ No newline at end of file + + companion object { + private const val TAG = "DnsTileService" + } +} diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/ui/DNSServerDialogFragment.kt b/app/src/main/java/ru/karasevm/privatednstoggle/ui/DNSServerDialogFragment.kt index 5b8bf0f..b2fa284 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/ui/DNSServerDialogFragment.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/ui/DNSServerDialogFragment.kt @@ -17,6 +17,7 @@ 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() { @@ -68,7 +69,12 @@ class DNSServerDialogFragment : DialogFragment() { 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 -> { diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/ui/OptionsDialogFragment.kt b/app/src/main/java/ru/karasevm/privatednstoggle/ui/OptionsDialogFragment.kt index 094d1ba..2ca7b14 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/ui/OptionsDialogFragment.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/ui/OptionsDialogFragment.kt @@ -14,6 +14,7 @@ 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? @@ -34,8 +35,7 @@ class OptionsDialogFragment : DialogFragment() { override fun onStart() { super.onStart() - val sharedPrefs = PreferenceHelper.defaultPreference(requireContext()) - val autoModeOption = sharedPrefs.autoMode + val autoModeOption = sharedPreferences.autoMode when (autoModeOption) { PrivateDNSUtils.AUTO_MODE_OPTION_OFF -> binding.autoOptionRadioGroup.check(R.id.autoOptionOff) PrivateDNSUtils.AUTO_MODE_OPTION_AUTO -> binding.autoOptionRadioGroup.check(R.id.autoOptionAuto) @@ -44,20 +44,20 @@ class OptionsDialogFragment : DialogFragment() { } binding.autoOptionRadioGroup.setOnCheckedChangeListener { _, checkedId -> when (checkedId) { - R.id.autoOptionOff -> sharedPrefs.autoMode = PrivateDNSUtils.AUTO_MODE_OPTION_OFF - R.id.autoOptionAuto -> sharedPrefs.autoMode = PrivateDNSUtils.AUTO_MODE_OPTION_AUTO - R.id.autoOptionOffAuto -> sharedPrefs.autoMode = + R.id.autoOptionOff -> sharedPreferences.autoMode = PrivateDNSUtils.AUTO_MODE_OPTION_OFF + R.id.autoOptionAuto -> sharedPreferences.autoMode = PrivateDNSUtils.AUTO_MODE_OPTION_AUTO + R.id.autoOptionOffAuto -> sharedPreferences.autoMode = PrivateDNSUtils.AUTO_MODE_OPTION_OFF_AUTO - R.id.autoOptionPrivate -> sharedPrefs.autoMode = + R.id.autoOptionPrivate -> sharedPreferences.autoMode = PrivateDNSUtils.AUTO_MODE_OPTION_PRIVATE } } - val requireUnlock = sharedPrefs.requireUnlock + val requireUnlock = sharedPreferences.requireUnlock binding.requireUnlockSwitch.isChecked = requireUnlock binding.requireUnlockSwitch.setOnCheckedChangeListener { _, isChecked -> - sharedPrefs.requireUnlock = isChecked + sharedPreferences.requireUnlock = isChecked } } } \ No newline at end of file diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/util/BackupUtils.kt b/app/src/main/java/ru/karasevm/privatednstoggle/util/BackupUtils.kt index 549dd9c..304f1db 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/util/BackupUtils.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/util/BackupUtils.kt @@ -74,6 +74,6 @@ object BackupUtils { } } sharedPreferences.autoMode = legacyBackup.autoMode?: 0 - sharedPreferences.requireUnlock = legacyBackup.requireUnlock?: false + sharedPreferences.requireUnlock = legacyBackup.requireUnlock == true } } \ No newline at end of file diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/util/PrivateDNSUtils.kt b/app/src/main/java/ru/karasevm/privatednstoggle/util/PrivateDNSUtils.kt index 73a1bc9..9441421 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/util/PrivateDNSUtils.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/util/PrivateDNSUtils.kt @@ -5,9 +5,7 @@ import android.content.ContentResolver import android.content.Context import android.content.pm.PackageManager import android.provider.Settings -import android.widget.Toast import androidx.core.content.ContextCompat.checkSelfPermission -import ru.karasevm.privatednstoggle.R @Suppress("unused") object PrivateDNSUtils { @@ -45,15 +43,10 @@ object PrivateDNSUtils { } fun checkForPermission(context: Context): Boolean { - if (checkSelfPermission( - context, - Manifest.permission.WRITE_SECURE_SETTINGS - ) == PackageManager.PERMISSION_GRANTED - ) { - return true - } - Toast.makeText(context, R.string.permission_missing, Toast.LENGTH_SHORT).show() - return false + return checkSelfPermission( + context, + Manifest.permission.WRITE_SECURE_SETTINGS + ) == PackageManager.PERMISSION_GRANTED } } \ No newline at end of file From b39d7e3624bcba005b7fc1b8ae093efa518a2620 Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Fri, 17 Jan 2025 15:52:24 +0300 Subject: [PATCH 30/52] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index c216f78..8d9ff13 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,5 @@ fastlane/readme.md # kotlin .kotlin/ + +*~ From efd48b8984b649663467af5f0f3bb060fba53fb9 Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Fri, 17 Jan 2025 15:56:15 +0300 Subject: [PATCH 31/52] Add support for Shizuku on Android 14 QPR3+ (#45) --- .../privatednstoggle/ui/MainActivity.kt | 102 ++++++------------ .../privatednstoggle/util/ShizukuUtil.kt | 82 ++++++++++++++ app/src/main/res/values-ru/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 4 files changed, 119 insertions(+), 69 deletions(-) create mode 100644 app/src/main/java/ru/karasevm/privatednstoggle/util/ShizukuUtil.kt diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/ui/MainActivity.kt b/app/src/main/java/ru/karasevm/privatednstoggle/ui/MainActivity.kt index e029c25..26695a3 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/ui/MainActivity.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/ui/MainActivity.kt @@ -6,13 +6,11 @@ import android.content.ClipDescription.MIMETYPE_TEXT_PLAIN import android.content.ClipboardManager import android.content.Intent import android.content.SharedPreferences -import android.content.pm.IPackageManager import android.content.pm.PackageManager import android.graphics.Color import android.net.Uri import android.os.Build import android.os.Bundle -import android.permission.IPermissionManager import android.util.Log import android.view.Menu import android.view.View @@ -30,11 +28,8 @@ import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.launch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import org.lsposed.hiddenapibypass.HiddenApiBypass import rikka.shizuku.Shizuku -import rikka.shizuku.ShizukuBinderWrapper import rikka.shizuku.ShizukuProvider -import rikka.shizuku.SystemServiceHelper import ru.karasevm.privatednstoggle.PrivateDNSApp import ru.karasevm.privatednstoggle.R import ru.karasevm.privatednstoggle.data.DnsServerViewModel @@ -44,6 +39,7 @@ 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, @@ -51,7 +47,6 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi private lateinit var linearLayoutManager: LinearLayoutManager private lateinit var binding: ActivityMainBinding - private var items = mutableListOf() private lateinit var sharedPrefs: SharedPreferences private lateinit var adapter: ServerListRecyclerAdapter private lateinit var clipboard: ClipboardManager @@ -150,18 +145,20 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi * Migrate the SharedPreferences server list to Room */ private fun migrateServerList() { - 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])) + dnsServerViewModel.viewModelScope.launch { + if (sharedPrefs.dns_servers.isNotEmpty() && sharedPrefs.dns_servers[0] != "") { + Log.i( + "migrate", + "existing sharedPrefs list: ${sharedPrefs.dns_servers} ${sharedPrefs.dns_servers.size}" + ) + sharedPrefs.dns_servers.forEach { server -> + val parts = server.split(" : ").toMutableList() + if (parts.size != 2) parts.add(0, "") + Log.i("migrate", "migrating: $server -> $parts") + dnsServerViewModel.insert(DnsServer(0, parts[1], parts[0])) + } + sharedPrefs.dns_servers = emptyList().toMutableList() } - sharedPrefs.dns_servers = emptyList().toMutableList() } } @@ -182,11 +179,6 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi migrateServerList() - items = sharedPrefs.dns_servers - if (items[0] == "") { - items.removeAt(0) - } - adapter = ServerListRecyclerAdapter(true) binding.recyclerView.adapter = adapter @@ -363,7 +355,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi Shizuku.requestPermission(1) } } else { - grantPermissionWithShizuku() + grantPermission() } } else { if (checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) { @@ -371,6 +363,9 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi Intent.ACTION_VIEW, Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/") ) + Toast.makeText( + this, R.string.shizuku_failure_toast, Toast.LENGTH_SHORT + ).show() startActivity(browserIntent) finish() } @@ -454,58 +449,27 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi dnsServerViewModel.update(id, server, label, null, enabled) } - /** - * Attempts to grant WRITE_SECURE_SETTINGS permission with Shizuku - */ - private fun grantPermissionWithShizuku() { - val packageName = applicationContext.packageName - runCatching { - if (Build.VERSION.SDK_INT >= 31) { - HiddenApiBypass.addHiddenApiExemptions("Landroid/permission") - val binder = - ShizukuBinderWrapper(SystemServiceHelper.getSystemService("permissionmgr")) - val pm = IPermissionManager.Stub.asInterface(binder) - runCatching { - pm.grantRuntimePermission( - packageName, Manifest.permission.WRITE_SECURE_SETTINGS, 0 - ) - }.onFailure { _ -> - if (Build.VERSION.SDK_INT >= 34) { - pm.grantRuntimePermission( - packageName, - Manifest.permission.WRITE_SECURE_SETTINGS, - applicationContext.deviceId, - 0 - ) - } - } - } else { - val binder = ShizukuBinderWrapper(SystemServiceHelper.getSystemService("package")) - val pm = IPackageManager.Stub.asInterface(binder) - pm.grantRuntimePermission( - packageName, Manifest.permission.WRITE_SECURE_SETTINGS, 0 - ) - } - }.onFailure { e -> - Log.e("SHIZUKU", "onRequestPermissionResult: ", e) - }.also { - if (checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) { - val browserIntent = Intent( - Intent.ACTION_VIEW, Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/") - ) - startActivity(browserIntent) - finish() - } + private fun grantPermission() { + if (grantPermissionWithShizuku(this)) { + Toast.makeText( + this, R.string.shizuku_success_toast, Toast.LENGTH_SHORT + ).show() + } else { + Toast.makeText( + this, R.string.shizuku_failure_toast, Toast.LENGTH_SHORT + ).show() + val browserIntent = Intent( + Intent.ACTION_VIEW, Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/") + ) + startActivity(browserIntent) + finish() } - } override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) { val isGranted = grantResult == PackageManager.PERMISSION_GRANTED - if (isGranted) { - grantPermissionWithShizuku() - } else if (checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) { + if (!isGranted && checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) { val browserIntent = Intent( Intent.ACTION_VIEW, Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/") ) diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/util/ShizukuUtil.kt b/app/src/main/java/ru/karasevm/privatednstoggle/util/ShizukuUtil.kt new file mode 100644 index 0000000..fe9b5cb --- /dev/null +++ b/app/src/main/java/ru/karasevm/privatednstoggle/util/ShizukuUtil.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index c61ca11..cef59e3 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -48,4 +48,6 @@ Нет доступных серверов Нажмите на кнопку ниже, чтобы добавить сервер Включён + Разрешение получено, можно отозвать авторизацию Shizuku + Не удалось получить разрешение, предоставьте его вручную \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1d41b73..8124a26 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -48,4 +48,6 @@ No Servers Added Tap on the button below to add one Enabled + Permission granted, you can revoke the Shizuku permission now + Failed to acquire permission, please grant it manually \ No newline at end of file From 6cc99b78204fc91a891f9fe585d0ebb475c13e04 Mon Sep 17 00:00:00 2001 From: Purevbaatar Tuvshinjargal Date: Wed, 13 Nov 2024 09:42:20 +0100 Subject: [PATCH 32/52] Added translation using Weblate (Mongolian) --- app/src/main/res/values-mn/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/src/main/res/values-mn/strings.xml diff --git a/app/src/main/res/values-mn/strings.xml b/app/src/main/res/values-mn/strings.xml new file mode 100644 index 0000000..a6b3dae --- /dev/null +++ b/app/src/main/res/values-mn/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From 75413fddcdfd9a766240496d784717018683a8bd Mon Sep 17 00:00:00 2001 From: Purevbaatar Tuvshinjargal Date: Wed, 13 Nov 2024 09:06:22 +0000 Subject: [PATCH 33/52] Translated using Weblate (Mongolian) Currently translated at 93.8% (46 of 49 strings) Translation: PrivateDNSAndroid/Private DNS Quick Toggle Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/mn/ --- app/src/main/res/values-mn/strings.xml | 49 +++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-mn/strings.xml b/app/src/main/res/values-mn/strings.xml index a6b3dae..10de470 100644 --- a/app/src/main/res/values-mn/strings.xml +++ b/app/src/main/res/values-mn/strings.xml @@ -1,2 +1,49 @@ - \ No newline at end of file + + Хувийн DNS солих + Унтраах + Тодорхойгүй + Сервер нэмэх + Хадгалах + Болсон + Болих + Устгах + Та серверийг устгахдаа итгэлтэй байна уу? + Устгах + Серверийн хаяг хоосон байж болохгүй + DNS серверийн шошго (заавал биш) + DNS серверийн хаяг + Сонголтууд + ОК + Хавтан дээр ямар сонголтыг оруулахаа сонгоно уу + Зөвхөн унтарсан + Зөвхөн авто + Унтарсан болон авто + Хувийн DNS-г %1$s болгож тохируулсан + Серверийг өөрчлөхийн тулд төхөөрөмжийн түгжээг тайлах шаардлагатай + Бариулыг чирэх + Импорт + Импортолсон + Импорт хийж чадсангүй + Файлаас + Хадгалж чадсангүй + Амжилттай хадгалсан + Сервер засах + Хувийн DNS хурдан сэлгэх + Нууцлалын бодлого + Зөвшөөрөл олгоогүй. Үүнийг хэрхэн хийхийг харна уу + Авто + Нэмэх + Сервер сонгох + Зөвхөн хувийн DNS + Апп нээх + Түр санах ой руу + Хувийн DNS унтарсан + Хувийн DNS-г автоматаар тохируулсан + Экспорт + Хуулагдсан + Хуваалцах + Импорт хийж чадсангүй, алдаатай JSON + Түр санах ойноос + Файлруу + \ No newline at end of file From e4498ca64abd04c1710d518120d56ad597e071a6 Mon Sep 17 00:00:00 2001 From: Michal L Date: Sun, 1 Dec 2024 00:00:32 +0100 Subject: [PATCH 34/52] Added translation using Weblate (Polish) --- app/src/main/res/values-pl/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/src/main/res/values-pl/strings.xml diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml new file mode 100644 index 0000000..a6b3dae --- /dev/null +++ b/app/src/main/res/values-pl/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From 6164a35f04f86ec7e9e49a7370112b93a2b1fd41 Mon Sep 17 00:00:00 2001 From: Michal L Date: Sat, 30 Nov 2024 23:01:54 +0000 Subject: [PATCH 35/52] Translated using Weblate (Polish) Currently translated at 59.1% (29 of 49 strings) Translation: PrivateDNSAndroid/Private DNS Quick Toggle Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/pl/ --- app/src/main/res/values-pl/strings.xml | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index a6b3dae..656b95d 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -1,2 +1,25 @@ - \ No newline at end of file + + Usuń + Zapisz + Polityka prywatności + Wybierz serwer + Anuluj + Adres serwera DNS + OK + Otwórz aplikację + Importuj + Eksportuj + Skopiowano + Z pliku + Ze schowka + Do schowka + Udostępnij + Do pliku + Edytuj serwer + Brak dodanych serwerów + Włączone + Dodaj serwer + Dodaj + Usuń + \ No newline at end of file From a379c81cb95daf7df7c43f2326f5dd47961118dc Mon Sep 17 00:00:00 2001 From: Eryk Michalak Date: Sun, 1 Dec 2024 13:53:41 +0000 Subject: [PATCH 36/52] Translated using Weblate (Polish) Currently translated at 59.1% (29 of 49 strings) Translation: PrivateDNSAndroid/Private DNS Quick Toggle Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/pl/ --- app/src/main/res/values-pl/strings.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 656b95d..a1710dd 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -22,4 +22,11 @@ Dodaj serwer Dodaj Usuń + Nieznane + Gotowe + Opcje + Importowanie nie powiodło się + Automatycznie + Wyłącz + Zaimportowano \ No newline at end of file From e4b9e84f8c0de12dd6de12acb9877eb95bab530d Mon Sep 17 00:00:00 2001 From: Michal L Date: Mon, 2 Dec 2024 22:10:09 +0000 Subject: [PATCH 37/52] Translated using Weblate (Polish) Currently translated at 67.3% (33 of 49 strings) Translation: PrivateDNSAndroid/Private DNS Quick Toggle Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/pl/ --- app/src/main/res/values-pl/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index a1710dd..07a1554 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -29,4 +29,8 @@ Automatycznie Wyłącz Zaimportowano + Adres serwera nie może być pusty + Import nie powiódł się, zniekształcony plik JSON + Zapisano pomyślnie + Czy na pewno chcesz usunąć serwer? \ No newline at end of file From 4c6240bd348e4b9ea2708f6061013b9078cefdca Mon Sep 17 00:00:00 2001 From: Michal L Date: Wed, 4 Dec 2024 22:33:27 +0000 Subject: [PATCH 38/52] Translated using Weblate (Polish) Currently translated at 71.4% (35 of 49 strings) Translation: PrivateDNSAndroid/Private DNS Quick Toggle Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/pl/ --- app/src/main/res/values-pl/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 07a1554..60116c2 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -33,4 +33,6 @@ Import nie powiódł się, zniekształcony plik JSON Zapisano pomyślnie Czy na pewno chcesz usunąć serwer? + Private DNS Quick Toggle + Przełącznik prywatnego DNS \ No newline at end of file From 0e0e0bf9b4393479a9aed20d2d23fa32a238b66d Mon Sep 17 00:00:00 2001 From: Michal L Date: Mon, 23 Dec 2024 19:36:41 +0000 Subject: [PATCH 39/52] Translated using Weblate (Polish) Currently translated at 100.0% (49 of 49 strings) Translation: PrivateDNSAndroid/Private DNS Quick Toggle Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/pl/ --- app/src/main/res/values-pl/strings.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 60116c2..ea093f9 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -35,4 +35,18 @@ Czy na pewno chcesz usunąć serwer? Private DNS Quick Toggle Przełącznik prywatnego DNS + Nieprzydzielono uprawnienia, sprawdź w aplikacji, w jaki sposób można to zrobić + Opis serwera DNS (opcjonalnie) + Wybierz opcje, które będą dostępne w kafelku + Tylko wyłączenie + Tylko automatycznie + Wyłączenie i automatycznie + Tylko prywatny DNS + Prywatny DNS zmieniony na automatyczny + Prywatny DNS zmieniony na %1$s + Wymagaj odblokowania urządzenia do zmiany serwera + Wyłączono Prywatny DNS + Przeciągnij + Zapisywanie nie powiodło się + Kliknij na poniższy przycisk, aby dodać nowy \ No newline at end of file From 6a5f2af6f630e6d70b452ffcc293cd9a6d8a63fd Mon Sep 17 00:00:00 2001 From: ajan Date: Sat, 28 Dec 2024 12:03:37 +0100 Subject: [PATCH 40/52] Added translation using Weblate (Portuguese (Brazil)) --- app/src/main/res/values-pt-rBR/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/src/main/res/values-pt-rBR/strings.xml diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml new file mode 100644 index 0000000..a6b3dae --- /dev/null +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From 24800f7f2de72d12fbf0e297705918472c9a78df Mon Sep 17 00:00:00 2001 From: ajan Date: Sat, 28 Dec 2024 11:38:36 +0000 Subject: [PATCH 41/52] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (49 of 49 strings) Translation: PrivateDNSAndroid/Private DNS Quick Toggle Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/pt_BR/ --- app/src/main/res/values-pt-rBR/strings.xml | 52 +++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index a6b3dae..131ea24 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -1,2 +1,52 @@ - \ No newline at end of file + + Importado + Permissão não concedida, verifique o app para saber como prosseguir + Alteração rápida de DNS privado + Cancelar + Adicionar servidor + Endereço do servidor DNS + Escolha opções disponível em atalho + Alteração de DNS privado + Desativado + Automático + Indeterminado + Adicionar + Salvar + Política de privacidade + Concluído + Apagar + Tem certeza de que quer apagar o servidor? + Apagar + O endereço do servidor não pode estar em branco + Identificação do servidor DNS (opcional) + Opções + Ok + Somente desativado + Desativado e automático + Somente DNS privado + Abrir app + DNS privado desativado + DNS privado definido para automático + DNS privado definido para %1$s + Arrastre + Importar + Exportar + Falha na importação, JSON malformado + Copiado + Da memória + Compartilhar + Para arquivo + Salvo com sucesso + Editar servidor + Nenhum servidor adicionado + Toque no botão abaixo para adicionar + Ativado + Escolha servidor + Para memória + De arquivo + Falha ao importar + Falha ao salvar + Precisa desbloquear dispositivo para alterar servidor + Somente automático + \ No newline at end of file From e2104952bcf7b67f502e2d99626f28984caa05b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?tu=E1=BA=A5n=20nguy=E1=BB=85n?= Date: Sun, 12 Jan 2025 09:42:40 +0100 Subject: [PATCH 42/52] Added translation using Weblate (Vietnamese) --- app/src/main/res/values-vi/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/src/main/res/values-vi/strings.xml diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml new file mode 100644 index 0000000..a6b3dae --- /dev/null +++ b/app/src/main/res/values-vi/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From e155d17dd7a4d50e6e6dda4135ec13362932d2cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?tu=E1=BA=A5n=20nguy=E1=BB=85n?= Date: Sun, 12 Jan 2025 08:48:55 +0000 Subject: [PATCH 43/52] Translated using Weblate (Vietnamese) Currently translated at 100.0% (49 of 49 strings) Translation: PrivateDNSAndroid/Private DNS Quick Toggle Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/vi/ --- app/src/main/res/values-vi/strings.xml | 52 +++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index a6b3dae..1f423ae 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -1,2 +1,52 @@ - \ No newline at end of file + + Thêm máy chủ + Chỉ tự động + Chưa được cấp quyền, hãy kiểm tra ứng dụng để biết cách thực hiện + Tắt + Tự động + Thêm + Chính sách bảo mật + Chọn máy chủ + Hoàn thành + Hủy + Bạn có chắc chắn muốn xóa máy chủ không? + Xoá + Địa chỉ máy chủ DNS + Tùy chọn + OK + Chọn các tùy chọn để đưa vào ô + Chỉ tắt + Tắt và tự động + Mở ứng dụng + DNS cá nhân được thiết lập tự động + Yêu cầu mở khóa thiết bị để thay đổi máy chủ + Tay cầm kéo + Xuất + Đã nhập + Nhập thất bại + Nhập thất bại, JSON bị lỗi + Đã sao chép + Từ tập tin + Chia sẻ + Thành tập tin + Lưu không thành công + Chỉnh sửa máy chủ + Chưa có máy chủ nào + Nhấn vào nút bên dưới để thêm + Đã bật + Chuyển đổi DNS cá nhân + Xoá + Chuyển đổi nhanh DNS cá nhân + Không rõ + Nhãn máy chủ DNS (Không bắt buộc) + Lưu + Địa chỉ máy chủ không được để trống + Chỉ DNS cá nhân + Đã tắt DNS cá nhân + DNS cá nhân được đặt thành %1$s + Nhập + Từ bảng nhớ tạm + Vào bảng nhớ tạm + Đã lưu thành công + \ No newline at end of file From 9d62e91b60c481098cc4a9907b62293188dbf54b Mon Sep 17 00:00:00 2001 From: papaindiatango Date: Sat, 18 Jan 2025 15:40:12 +0000 Subject: [PATCH 44/52] Translated using Weblate (French) Currently translated at 90.1% (46 of 51 strings) Translation: PrivateDNSAndroid/Private DNS Quick Toggle Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/fr/ --- app/src/main/res/values-fr/strings.xml | 54 ++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 app/src/main/res/values-fr/strings.xml diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000..048dcdf --- /dev/null +++ b/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,54 @@ + + + Terminé + Private DNS Quick Toggle + Inconnu + Éteint + Ajouter un serveur + Ajouter + Politique de confidentialité + Êtes-vous sûr de vouloir supprimer le serveur ? + Supprimer + L\'adresse du serveur ne peut pas être vide + Étiquette du serveur DNS (Facultatif) + Adresse du serveur DNS + Options + OK + Seulement éteint + Seulement automatique + Éteint et automatique + Automatique + Commutateur de DNS privé + Enregistrer + Autorisation non accordée, vérifiez les instructions dans l\'application + Sélectionner le serveur + Définissez les options à inclure + DNS privé seulement + Ouvrir l\'application + DNS privé éteint + DNS privé réglé sur automatique + DNS privé réglé sur %1$s + Requiert le déverrouillage de l\'appareil pour changer de serveur + Activé + À partir du fichier + Aucun serveur ajouté + Copié + Éditer le serveur + Échec de la sauvegarde + Appuyez sur le bouton ci-dessous pour en ajouter un + Impossible d\'obtenir l\'autorisation, veuillez l\'accorder manuellement + Autorisation accordée, vous pouvez désormais révoquer l\'autorisation Shizuku + À partir du presse-papier + Vers le presse-papier + Partager + Vers le fichier + Sauvegarde réussie + Échec de l\'importation + Échec de l\'importation, le fichier JSON est incorrect + Importé + Exporter + Importer + Poignée + Supprimer + Annuler + From 3b09f605f9de4aaa27f23216bac1b53347de8ec3 Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Tue, 25 Feb 2025 11:27:46 +0300 Subject: [PATCH 45/52] Bump dependencies --- app/build.gradle.kts | 2 +- build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 438062a..c06d657 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -57,7 +57,7 @@ dependencies { 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.5") + 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") diff --git a/build.gradle.kts b/build.gradle.kts index 461e086..45827dc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ buildscript { mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:8.8.0") + 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 From bdd98109ed6bc2614dcb5b51d400fff5d121d2e7 Mon Sep 17 00:00:00 2001 From: Maksim Karasev Date: Tue, 25 Feb 2025 13:21:09 +0300 Subject: [PATCH 46/52] Update layout for Android 15 --- app/src/main/res/layout/activity_main.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index f918023..88e1536 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -4,7 +4,8 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".ui.MainActivity"> + tools:context=".ui.MainActivity" + android:fitsSystemWindows="true"> Date: Tue, 25 Feb 2025 13:29:11 +0300 Subject: [PATCH 47/52] Add 1.10.0 changelog --- fastlane/metadata/android/en-US/changelogs/18.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/18.txt diff --git a/fastlane/metadata/android/en-US/changelogs/18.txt b/fastlane/metadata/android/en-US/changelogs/18.txt new file mode 100644 index 0000000..067cc8f --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/18.txt @@ -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)) From e8885409b546038f88e7ca92a0d8655475979218 Mon Sep 17 00:00:00 2001 From: ajan Date: Tue, 18 Feb 2025 21:02:05 +0100 Subject: [PATCH 48/52] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (51 of 51 strings) Co-authored-by: ajan Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/pt_BR/ Translation: PrivateDNSAndroid/Private DNS Quick Toggle --- app/src/main/res/values-pt-rBR/strings.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 131ea24..b717561 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -2,7 +2,7 @@ Importado Permissão não concedida, verifique o app para saber como prosseguir - Alteração rápida de DNS privado + Alteração de DNS privado Cancelar Adicionar servidor Endereço do servidor DNS @@ -49,4 +49,6 @@ Falha ao salvar Precisa desbloquear dispositivo para alterar servidor Somente automático - \ No newline at end of file + Falha ao obter a permissão. Tente conceder manualmente + Permissão concedida, você pode revogar a permissão do Shizuku agora + From 3b24d24ba4e0c1687e3924c312230cfbbba4af01 Mon Sep 17 00:00:00 2001 From: Mustafa A Date: Tue, 18 Feb 2025 21:02:07 +0100 Subject: [PATCH 49/52] Translated using Weblate (Turkish) Currently translated at 98.0% (50 of 51 strings) Added translation using Weblate (Turkish) Co-authored-by: Mustafa A Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/tr/ Translation: PrivateDNSAndroid/Private DNS Quick Toggle --- app/src/main/res/values-tr/strings.xml | 54 ++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 app/src/main/res/values-tr/strings.xml diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml new file mode 100644 index 0000000..57f2c8d --- /dev/null +++ b/app/src/main/res/values-tr/strings.xml @@ -0,0 +1,54 @@ + + + Sunucuyu silmek istediğinizden emin misiniz? + Sadece kapalı + Sunucuyu değiştirmek için cihazın kilidini açmanız gerekiyor + Oto + Bilinmeyen + Tamam + İptal + DNS sunucusu Etiketi (İsteğe bağlı) + Seçenekler + OK + Sadece oto + Kapalı ve oto + Özel DNS kapandı + Uygulamayı açınız + İçe aktar + Dışa aktar + içe aktarıldı + Dosyaya + Kaydetme başarısız oldu + Hiç Sunucu Eklenmedi + Eklemek için aşağıdaki düğmeye dokunun + İzin alınamadı, lütfen manuel olarak verin + Karoya hangi seçeneklerin dahil edileceğini seçiniz + Kapalı + Gizlilik Politikası + Sil + Sunucu adresi boş olamaz + Sil + DNS sunucu adresi + tutacağı sürükle + Ekle + Sunucu Ekleyiniz + İzin verilmedi, nasıl yapıldığını görmek için uygulamayı kontrol ediniz + İçe aktarma başarısız oldu, hatalı biçimlendirilmiş JSON + Özel DNS Hızlı Geçiş + İzin verildi, şimdi Shizuku iznini iptal edebilirsiniz + Aktarma başarısız + Panodan + Başarıyla kaydedildi + Kaydet + Özel DNS otomatik olarak ayarlandı + Özel DNS %1$s olarak ayarlandı + Sadece Özel DNS + Sunucuyu Seçin + Kopyalandı + Sunucuyu düzenle + Etkin + Özel DNS Geçişi + Dosyadan + Panoya + Paylaş + From c375776575816c42d50fec5f961995fd71bd7693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=AE=A4=E0=AE=AE=E0=AE=BF=E0=AE=B4=E0=AF=8D=E0=AE=A8?= =?UTF-8?q?=E0=AF=87=E0=AE=B0=E0=AE=AE=E0=AF=8D?= Date: Tue, 18 Feb 2025 21:02:09 +0100 Subject: [PATCH 50/52] Translated using Weblate (Tamil) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (51 of 51 strings) Added translation using Weblate (Tamil) Co-authored-by: தமிழ்நேரம் Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/ta/ Translation: PrivateDNSAndroid/Private DNS Quick Toggle --- app/src/main/res/values-ta/strings.xml | 54 ++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 app/src/main/res/values-ta/strings.xml diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml new file mode 100644 index 0000000..1cb02d2 --- /dev/null +++ b/app/src/main/res/values-ta/strings.xml @@ -0,0 +1,54 @@ + + + சரி + தனியார் டி.என்.எச் விரைவாக மாற்று + தனியார் டி.என் கள் மாறுகின்றன + இசைவு வழங்கப்படவில்லை, அதை எப்படி செய்வது என்று பார்க்க பயன்பாட்டை சரிபார்க்கவும் + அணை + தானி + தெரியவில்லை + சேவையகத்தைச் சேர்க்கவும் + கூட்டு + சேமி + தனியுரிமைக் கொள்கை + சேவையகத்தைத் தேர்ந்தெடுக்கவும் + முடிந்தது + ரத்துசெய் + நீக்கு + சேவையகத்தை நீக்க விரும்புகிறீர்களா? + நீக்கு + டிஎன்எச் சேவையக முகவரி + விருப்பங்கள் + ஓடுகளில் எந்த விருப்பங்களைச் சேர்க்க வேண்டும் என்பதைத் தேர்வுசெய்க + மட்டுமே + ஆட்டோ மட்டுமே + ஆஃப் மற்றும் ஆட்டோ + தனியார் டி.என்.எச் மட்டுமே + திறந்த பயன்பாடு + தனியார் டி.என்.எச் அணைக்கப்பட்டது + தனியார் டி.என்.எச் ஆட்டோவாக அமைக்கப்பட்டுள்ளது + தனியார் டி.என்.எச் %1$s என அமைக்கப்பட்டுள்ளது + சேவையகத்தை மாற்ற சாதனத்தைத் திறக்க வேண்டும் + இழுவை கைப்பிடி + இறக்குமதி + ஏற்றுமதி + இறக்குமதி செய்யப்பட்டது + இறக்குமதி தோல்வியடைந்தது + இறக்குமதி தோல்வியுற்றது, தவறாக சாதொபொகு + நகலெடுக்கப்பட்டது + கோப்பிலிருந்து + கிளிப்போர்டிலிருந்து + இடைநிலைப்பலகைக்கு + பங்கு + தாக்கல் செய்ய + சேமிப்பு தோல்வியடைந்தது + வெற்றிகரமாக சேமிக்கப்பட்டது + சேவையகத்தைத் திருத்து + சேவையகங்கள் எதுவும் சேர்க்கப்படவில்லை + ஒன்றைச் சேர்க்க கீழே உள்ள பொத்தானைத் தட்டவும் + இயக்கப்பட்டது + இசைவு வழங்கப்பட்டது, நீங்கள் இப்போது சிசுகு அனுமதியை ரத்து செய்யலாம் + இசைவு பெறுவதில் தோல்வி, தயவுசெய்து அதை கைமுறையாக வழங்கவும் + சேவையக முகவரி காலியாக இருக்க முடியாது + டிஎன்எச் சேவையக சிட்டை (விரும்பினால்) + From 87fe66af20d44d09365e9f83da25537ecc55a2dc Mon Sep 17 00:00:00 2001 From: Michal L Date: Tue, 18 Feb 2025 21:02:11 +0100 Subject: [PATCH 51/52] Translated using Weblate (Polish) Currently translated at 100.0% (51 of 51 strings) Co-authored-by: Michal L Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/pl/ Translation: PrivateDNSAndroid/Private DNS Quick Toggle --- app/src/main/res/values-pl/strings.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index ea093f9..6c76614 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -49,4 +49,6 @@ Przeciągnij Zapisywanie nie powiodło się Kliknij na poniższy przycisk, aby dodać nowy - \ No newline at end of file + Udzielono zezwolenia, możesz teraz cofnąć zezwolenie w Shizuku + Uzyskanie uprawnień nie powiodło się, udziel ich ręcznie + From bd61fe6748c97f9348b402918a6097a523b8ac23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Assun=C3=A7=C3=A3o?= Date: Tue, 18 Feb 2025 21:02:13 +0100 Subject: [PATCH 52/52] Translated using Weblate (Portuguese (Brazil)) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (51 of 51 strings) Co-authored-by: Víctor Assunção Translate-URL: https://hosted.weblate.org/projects/privatednsandroid/private-dns-quick-toggle/pt_BR/ Translation: PrivateDNSAndroid/Private DNS Quick Toggle --- app/src/main/res/values-pt-rBR/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index b717561..c4691bd 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -47,7 +47,7 @@ De arquivo Falha ao importar Falha ao salvar - Precisa desbloquear dispositivo para alterar servidor + Necessário desbloquear o dispositivo para alterar servidor Somente automático Falha ao obter a permissão. Tente conceder manualmente Permissão concedida, você pode revogar a permissão do Shizuku agora