This commit is contained in:
Praveen Kumar 2025-02-25 20:36:27 +05:30 committed by GitHub
commit 78c98595d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 241 additions and 89 deletions

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:shortcutId="switch_mode"
android:enabled="true"
android:icon="@drawable/ic_outline_next_24dp"
android:shortcutShortLabel="@string/shortcut_switch_mode">
<intent
android:action="android.intent.action.VIEW"
android:targetPackage="ru.karasevm.privatednstoggle.dev"
android:targetClass="ru.karasevm.privatednstoggle.ui.SettingsDialogActivity"
android:data="privatednstoggle://switch_mode"/>
</shortcut>
</shortcuts>

View file

@ -1,7 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"
<uses-permission
android:name="android.permission.WRITE_SECURE_SETTINGS"
tools:ignore="ProtectedPermissions" />
<application
@ -11,6 +13,10 @@
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.Transparent">
<service
android:name=".service.ShortcutService"
android:enabled="true"
android:exported="true"></service>
<provider
android:name="rikka.shizuku.ShizukuProvider"
@ -18,23 +24,24 @@
android:enabled="true"
android:exported="true"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
<activity
android:name=".ui.MainActivity"
android:theme="@style/Theme.MyApplication"
android:taskAffinity="${applicationId}.main"
android:exported="true"
android:launchMode="singleInstance"
android:exported="true">
android:taskAffinity="${applicationId}.main"
android:theme="@style/Theme.MyApplication">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.SettingsDialogActivity"
android:theme="@style/Theme.Transparent"
android:excludeFromRecents="true"
android:exported="true">
android:exported="true"
android:theme="@style/Theme.Transparent">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter>

View file

@ -5,10 +5,8 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.drawable.Icon
import android.provider.Settings
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import android.util.Log
import androidx.core.content.ContextCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -18,26 +16,20 @@ 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
import ru.karasevm.privatednstoggle.util.PrivateDNSUtils.getNextAddress
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,35 +45,11 @@ class DnsTileService : TileService() {
* Set's the state of the tile and system settings to the next state
*/
private fun cycleState() {
val dnsMode = Settings.Global.getString(contentResolver, "private_dns_mode")
val dnsProvider = Settings.Global.getString(contentResolver, "private_dns_specifier")
val sharedPrefs = PreferenceHelper.defaultPreference(this)
if (dnsMode.equals(DNS_MODE_OFF, ignoreCase = true)) {
if (sharedPreferences.autoMode == AUTO_MODE_OPTION_AUTO || sharedPreferences.autoMode == AUTO_MODE_OPTION_OFF_AUTO) {
changeDNSServer(DNS_MODE_AUTO, dnsProvider)
} else {
changeDNSServer(DNS_MODE_PRIVATE, dnsProvider)
}
} else if (dnsMode == null || dnsMode.equals(DNS_MODE_AUTO, ignoreCase = true)) {
changeDNSServer(DNS_MODE_PRIVATE, null)
} else if (dnsMode.equals(DNS_MODE_PRIVATE, ignoreCase = true)) {
scope.launch {
if (getNextAddress(dnsProvider) == null) {
if (sharedPreferences.autoMode == AUTO_MODE_OPTION_PRIVATE) {
changeDNSServer(DNS_MODE_PRIVATE, null)
} else {
if (sharedPreferences.autoMode == AUTO_MODE_OPTION_AUTO) {
changeDNSServer(DNS_MODE_AUTO, dnsProvider)
} else {
changeDNSServer(DNS_MODE_OFF, dnsProvider)
}
}
} else {
changeDNSServer(DNS_MODE_PRIVATE, dnsProvider)
}
}
}
PrivateDNSUtils.getNextProvider(sharedPrefs, scope, repository, contentResolver, onNext = { mode, provider ->
changeDNSServer(mode, provider)
})
}
/**
@ -115,7 +83,7 @@ class DnsTileService : TileService() {
DNS_MODE_PRIVATE -> {
scope.launch {
val nextDnsServer = getNextAddress(dnsProvider)
val nextDnsServer = getNextAddress(repository, dnsProvider)
if (nextDnsServer != null) {
changeTileState(
qsTile,
@ -123,7 +91,7 @@ class DnsTileService : TileService() {
nextDnsServer.label.ifEmpty { nextDnsServer.server },
R.drawable.ic_private_black_24dp,
DNS_MODE_PRIVATE,
getNextAddress(dnsProvider)?.server
getNextAddress(repository, dnsProvider)?.server
)
}
}
@ -136,9 +104,10 @@ class DnsTileService : TileService() {
if (!checkForPermission(this)) {
return
}
val sharedPrefs = PreferenceHelper.defaultPreference(this)
// Require unlock to change mode according to user preference
val requireUnlock = sharedPreferences.requireUnlock
val requireUnlock = sharedPrefs.requireUnlock
if (isLocked && requireUnlock) {
unlockAndRun(this::cycleState)
} else {
@ -152,13 +121,12 @@ 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")
val dnsMode = PrivateDNSUtils.getPrivateMode(contentResolver)
when (dnsMode?.lowercase()) {
DNS_MODE_OFF -> {
setTile(
qsTile,
if (!isPermissionGranted) Tile.STATE_UNAVAILABLE else Tile.STATE_INACTIVE,
Tile.STATE_INACTIVE,
getString(R.string.dns_off),
R.drawable.ic_off_black_24dp
)
@ -167,7 +135,7 @@ class DnsTileService : TileService() {
DNS_MODE_AUTO -> {
setTile(
qsTile,
if (!isPermissionGranted) Tile.STATE_UNAVAILABLE else Tile.STATE_INACTIVE,
Tile.STATE_INACTIVE,
getString(R.string.dns_auto),
R.drawable.ic_auto_black_24dp
)
@ -176,11 +144,11 @@ class DnsTileService : TileService() {
DNS_MODE_PRIVATE -> {
scope.launch {
val activeAddress =
Settings.Global.getString(contentResolver, "private_dns_specifier")
val dnsServer = repository.getFirstByServer(activeAddress)
PrivateDNSUtils.getPrivateProvider(contentResolver)
val dnsServer = repository.getFirstByServer(activeAddress!!)
setTile(
qsTile,
if (!isPermissionGranted) Tile.STATE_UNAVAILABLE else Tile.STATE_ACTIVE,
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
@ -191,7 +159,7 @@ class DnsTileService : TileService() {
else -> {
setTile(
qsTile,
if (!isPermissionGranted) Tile.STATE_UNAVAILABLE else Tile.STATE_INACTIVE,
Tile.STATE_INACTIVE,
getString(R.string.dns_unknown),
R.drawable.ic_unknown_black_24dp
)
@ -207,10 +175,12 @@ 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,17 +192,14 @@ class DnsTileService : TileService() {
IntentFilter("refresh_tile"),
ContextCompat.RECEIVER_NOT_EXPORTED
)
isBroadcastReceiverRegistered = true
refreshTile()
}
override fun onStopListening() {
super.onStopListening()
if (isBroadcastReceiverRegistered) {
unregisterReceiver(broadcastReceiver)
isBroadcastReceiverRegistered = false
}
unregisterReceiver(broadcastReceiver)
}
override fun onDestroy() {
@ -280,23 +247,4 @@ class DnsTileService : TileService() {
PrivateDNSUtils.setPrivateProvider(contentResolver, dnsProvider)
tile.updateTile()
}
/**
* Gets next dns address from the database,
* if current address is last or unknown returns null
*
* @param currentAddress currently set address
* @return next address
*/
private suspend fun getNextAddress(currentAddress: String?): DnsServer? {
return if (currentAddress.isNullOrEmpty()) {
repository.getFirstEnabled()
} else {
repository.getNextByServer(currentAddress)
}
}
companion object {
private const val TAG = "DnsTileService"
}
}
}

View file

@ -0,0 +1,73 @@
package ru.karasevm.privatednstoggle.service
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.widget.Toast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import ru.karasevm.privatednstoggle.PrivateDNSApp
import ru.karasevm.privatednstoggle.data.DnsServerRepository
import ru.karasevm.privatednstoggle.util.PreferenceHelper
import ru.karasevm.privatednstoggle.util.PrivateDNSUtils
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 ShortcutService : Service() {
private val repository: DnsServerRepository by lazy { (application as PrivateDNSApp).repository }
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job)
companion object {
private const val ACTION_SWITCH_MODE = "privatednstoggle://switch_mode"
}
private fun setDnsModeAndShowToast(dnsMode: String) {
if(dnsMode == DNS_MODE_PRIVATE) {
val provider = PrivateDNSUtils.getPrivateProvider(contentResolver)
if (provider == null) {
PrivateDNSUtils.setPrivateProvider(contentResolver, "dns.google")
}
}
PrivateDNSUtils.setPrivateMode(contentResolver, dnsMode)
val text = when (dnsMode) {
DNS_MODE_OFF -> "DNS set to Off"
DNS_MODE_AUTO -> "DNS set to Auto"
DNS_MODE_PRIVATE -> "DNS set to Private Provider"
else -> "Unknown"
}
scope.launch {
launch(Dispatchers.Main) {
Toast.makeText(applicationContext, text, Toast.LENGTH_SHORT).show()
}
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val data = intent?.data.toString()
val sharedPrefs = PreferenceHelper.defaultPreference(this)
if(checkForPermission(this)) {
if (data == ACTION_SWITCH_MODE) {
PrivateDNSUtils.getNextProvider(
sharedPrefs,
scope,
repository,
contentResolver,
skipProvider = true,
onNext = { dnsMode, _ ->
setDnsModeAndShowToast(dnsMode)
})
}
}
return START_NOT_STICKY
}
override fun onBind(intent: Intent): IBinder? {
return null
}
}

View file

@ -1,12 +1,25 @@
package ru.karasevm.privatednstoggle.ui
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import ru.karasevm.privatednstoggle.service.ShortcutService
class SettingsDialogActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val newFragment = DNSServerDialogFragment()
newFragment.show(supportFragmentManager, DNSServerDialogFragment.TAG)
if (intent != null && intent.data != null) {
// Start the service when the shortcut is clicked
val serviceIntent = Intent(this, ShortcutService::class.java)
serviceIntent.data = intent.data
startService(serviceIntent)
finish()
} else {
val newFragment = DNSServerDialogFragment()
newFragment.show(supportFragmentManager, DNSServerDialogFragment.TAG)
}
}
}

View file

@ -3,11 +3,18 @@ package ru.karasevm.privatednstoggle.util
import android.Manifest
import android.content.ContentResolver
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.provider.Settings
import android.widget.Toast
import androidx.core.content.ContextCompat.checkSelfPermission
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import ru.karasevm.privatednstoggle.R
import ru.karasevm.privatednstoggle.data.DnsServerRepository
import ru.karasevm.privatednstoggle.model.DnsServer
import ru.karasevm.privatednstoggle.util.PreferenceHelper.autoMode
@Suppress("unused")
object PrivateDNSUtils {
const val DNS_MODE_OFF = "off"
const val DNS_MODE_AUTO = "opportunistic"
@ -23,12 +30,12 @@ object PrivateDNSUtils {
// Gets the system dns mode
fun getPrivateMode(contentResolver: ContentResolver): String {
fun getPrivateMode(contentResolver: ContentResolver): String? {
return Settings.Global.getString(contentResolver, PRIVATE_DNS_MODE)
}
// Gets the system dns provider
fun getPrivateProvider(contentResolver: ContentResolver): String {
fun getPrivateProvider(contentResolver: ContentResolver): String? {
return Settings.Global.getString(contentResolver, PRIVATE_DNS_PROVIDER)
}
@ -43,10 +50,72 @@ object PrivateDNSUtils {
}
fun checkForPermission(context: Context): Boolean {
return checkSelfPermission(
context,
Manifest.permission.WRITE_SECURE_SETTINGS
) == PackageManager.PERMISSION_GRANTED
if (checkSelfPermission(
context,
Manifest.permission.WRITE_SECURE_SETTINGS
) == PackageManager.PERMISSION_GRANTED
) {
return true
}
Toast.makeText(context, R.string.permission_missing, Toast.LENGTH_SHORT).show()
return false
}
/**
* Gets next dns address from the database,
* if current address is last or unknown returns null
*
* @param currentAddress currently set address
* @return next address
*/
suspend fun getNextAddress(
repository: DnsServerRepository,
currentAddress: String?
): DnsServer? {
return if (currentAddress.isNullOrEmpty()) {
repository.getFirstEnabled()
} else {
repository.getNextByServer(currentAddress)
}
}
fun getNextProvider(
sharedPrefs: SharedPreferences,
scope: CoroutineScope,
repository: DnsServerRepository,
contentResolver: ContentResolver,
skipProvider: Boolean = false,
onNext: ((String, String?) -> Unit)
) {
val dnsMode = getPrivateMode(contentResolver)
val dnsProvider = getPrivateProvider(contentResolver)
if (dnsMode.equals(DNS_MODE_OFF, ignoreCase = true)) {
if (sharedPrefs.autoMode == AUTO_MODE_OPTION_AUTO || sharedPrefs.autoMode == AUTO_MODE_OPTION_OFF_AUTO) {
onNext.invoke(DNS_MODE_AUTO, dnsProvider)
} else {
onNext.invoke(DNS_MODE_PRIVATE, dnsProvider)
}
} else if (dnsMode == null || dnsMode.equals(DNS_MODE_AUTO, ignoreCase = true)) {
onNext.invoke(DNS_MODE_PRIVATE, null)
} else if (dnsMode.equals(DNS_MODE_PRIVATE, ignoreCase = true)) {
scope.launch {
if (getNextAddress(repository, dnsProvider) == null) {
if (!skipProvider && sharedPrefs.autoMode == AUTO_MODE_OPTION_PRIVATE) {
onNext.invoke(DNS_MODE_PRIVATE, null)
} else {
if (sharedPrefs.autoMode == AUTO_MODE_OPTION_AUTO) {
onNext.invoke(DNS_MODE_AUTO, dnsProvider)
} else {
onNext.invoke(DNS_MODE_OFF, dnsProvider)
}
}
} else {
onNext.invoke(DNS_MODE_OFF, dnsProvider)
}
}
}
}
}

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:autoMirrored="true">
<path
android:fillColor="@android:color/black"
android:pathData="M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10s10,-4.48 10,-10C22,6.48 17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8s8,3.58 8,8S16.42,20 12,20z"/>
<path
android:fillColor="@android:color/black"
android:pathData="M15.97,11.03C14.87,9.79 13.28,9 11.5,9c-2.82,0 -5.18,1.95 -5.82,4.56l0.96,0.32C7.15,11.66 9.13,10 11.5,10c1.51,0 2.85,0.68 3.76,1.74L13,14h5V9L15.97,11.03z"/>
</vector>

View file

@ -50,4 +50,5 @@
<string name="add_server_enabled">Enabled</string>
<string name="shizuku_success_toast">Permission granted, you can revoke the Shizuku permission now</string>
<string name="shizuku_failure_toast">Failed to acquire permission, please grant it manually</string>
<string name="shortcut_switch_mode">Switch Mode</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:shortcutId="switch_mode"
android:enabled="true"
android:icon="@drawable/ic_outline_next_24dp"
android:shortcutShortLabel="@string/shortcut_switch_mode">
<intent
android:action="android.intent.action.VIEW"
android:targetPackage="ru.karasevm.privatednstoggle"
android:targetClass="ru.karasevm.privatednstoggle.ui.SettingsDialogActivity"
android:data="privatednstoggle://switch_mode"/>
</shortcut>
</shortcuts>