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

View file

@ -5,10 +5,8 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
import android.provider.Settings
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import android.util.Log
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -18,26 +16,20 @@ import kotlinx.coroutines.launch
import ru.karasevm.privatednstoggle.PrivateDNSApp import ru.karasevm.privatednstoggle.PrivateDNSApp
import ru.karasevm.privatednstoggle.R import ru.karasevm.privatednstoggle.R
import ru.karasevm.privatednstoggle.data.DnsServerRepository import ru.karasevm.privatednstoggle.data.DnsServerRepository
import ru.karasevm.privatednstoggle.model.DnsServer
import ru.karasevm.privatednstoggle.util.PreferenceHelper 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.PreferenceHelper.requireUnlock
import ru.karasevm.privatednstoggle.util.PrivateDNSUtils 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_AUTO
import ru.karasevm.privatednstoggle.util.PrivateDNSUtils.DNS_MODE_OFF import ru.karasevm.privatednstoggle.util.PrivateDNSUtils.DNS_MODE_OFF
import ru.karasevm.privatednstoggle.util.PrivateDNSUtils.DNS_MODE_PRIVATE import ru.karasevm.privatednstoggle.util.PrivateDNSUtils.DNS_MODE_PRIVATE
import ru.karasevm.privatednstoggle.util.PrivateDNSUtils.checkForPermission import ru.karasevm.privatednstoggle.util.PrivateDNSUtils.checkForPermission
import ru.karasevm.privatednstoggle.util.PrivateDNSUtils.getNextAddress
class DnsTileService : TileService() { class DnsTileService : TileService() {
private val repository: DnsServerRepository by lazy { (application as PrivateDNSApp).repository } private val repository: DnsServerRepository by lazy { (application as PrivateDNSApp).repository }
private val job = SupervisorJob() private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job) private val scope = CoroutineScope(Dispatchers.IO + job)
private val sharedPreferences by lazy { PreferenceHelper.defaultPreference(this) }
private var isBroadcastReceiverRegistered = false
override fun onTileAdded() { override fun onTileAdded() {
super.onTileAdded() super.onTileAdded()
@ -53,35 +45,11 @@ class DnsTileService : TileService() {
* Set's the state of the tile and system settings to the next state * Set's the state of the tile and system settings to the next state
*/ */
private fun cycleState() { private fun cycleState() {
val dnsMode = Settings.Global.getString(contentResolver, "private_dns_mode") val sharedPrefs = PreferenceHelper.defaultPreference(this)
val dnsProvider = Settings.Global.getString(contentResolver, "private_dns_specifier")
if (dnsMode.equals(DNS_MODE_OFF, ignoreCase = true)) { PrivateDNSUtils.getNextProvider(sharedPrefs, scope, repository, contentResolver, onNext = { mode, provider ->
if (sharedPreferences.autoMode == AUTO_MODE_OPTION_AUTO || sharedPreferences.autoMode == AUTO_MODE_OPTION_OFF_AUTO) { changeDNSServer(mode, provider)
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)
}
}
}
} }
/** /**
@ -115,7 +83,7 @@ class DnsTileService : TileService() {
DNS_MODE_PRIVATE -> { DNS_MODE_PRIVATE -> {
scope.launch { scope.launch {
val nextDnsServer = getNextAddress(dnsProvider) val nextDnsServer = getNextAddress(repository, dnsProvider)
if (nextDnsServer != null) { if (nextDnsServer != null) {
changeTileState( changeTileState(
qsTile, qsTile,
@ -123,7 +91,7 @@ class DnsTileService : TileService() {
nextDnsServer.label.ifEmpty { nextDnsServer.server }, nextDnsServer.label.ifEmpty { nextDnsServer.server },
R.drawable.ic_private_black_24dp, R.drawable.ic_private_black_24dp,
DNS_MODE_PRIVATE, DNS_MODE_PRIVATE,
getNextAddress(dnsProvider)?.server getNextAddress(repository, dnsProvider)?.server
) )
} }
} }
@ -136,9 +104,10 @@ class DnsTileService : TileService() {
if (!checkForPermission(this)) { if (!checkForPermission(this)) {
return return
} }
val sharedPrefs = PreferenceHelper.defaultPreference(this)
// Require unlock to change mode according to user preference // Require unlock to change mode according to user preference
val requireUnlock = sharedPreferences.requireUnlock val requireUnlock = sharedPrefs.requireUnlock
if (isLocked && requireUnlock) { if (isLocked && requireUnlock) {
unlockAndRun(this::cycleState) unlockAndRun(this::cycleState)
} else { } else {
@ -152,13 +121,12 @@ class DnsTileService : TileService() {
* Refreshes the state of the tile * Refreshes the state of the tile
*/ */
private fun refreshTile() { private fun refreshTile() {
val isPermissionGranted = checkForPermission(this) val dnsMode = PrivateDNSUtils.getPrivateMode(contentResolver)
val dnsMode = Settings.Global.getString(contentResolver, "private_dns_mode")
when (dnsMode?.lowercase()) { when (dnsMode?.lowercase()) {
DNS_MODE_OFF -> { DNS_MODE_OFF -> {
setTile( setTile(
qsTile, qsTile,
if (!isPermissionGranted) Tile.STATE_UNAVAILABLE else Tile.STATE_INACTIVE, Tile.STATE_INACTIVE,
getString(R.string.dns_off), getString(R.string.dns_off),
R.drawable.ic_off_black_24dp R.drawable.ic_off_black_24dp
) )
@ -167,7 +135,7 @@ class DnsTileService : TileService() {
DNS_MODE_AUTO -> { DNS_MODE_AUTO -> {
setTile( setTile(
qsTile, qsTile,
if (!isPermissionGranted) Tile.STATE_UNAVAILABLE else Tile.STATE_INACTIVE, Tile.STATE_INACTIVE,
getString(R.string.dns_auto), getString(R.string.dns_auto),
R.drawable.ic_auto_black_24dp R.drawable.ic_auto_black_24dp
) )
@ -176,11 +144,11 @@ class DnsTileService : TileService() {
DNS_MODE_PRIVATE -> { DNS_MODE_PRIVATE -> {
scope.launch { scope.launch {
val activeAddress = val activeAddress =
Settings.Global.getString(contentResolver, "private_dns_specifier") PrivateDNSUtils.getPrivateProvider(contentResolver)
val dnsServer = repository.getFirstByServer(activeAddress) val dnsServer = repository.getFirstByServer(activeAddress!!)
setTile( setTile(
qsTile, 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 // display server address if either there is no label or the server is not known
dnsServer?.label?.ifBlank { activeAddress } ?: activeAddress, dnsServer?.label?.ifBlank { activeAddress } ?: activeAddress,
R.drawable.ic_private_black_24dp R.drawable.ic_private_black_24dp
@ -191,7 +159,7 @@ class DnsTileService : TileService() {
else -> { else -> {
setTile( setTile(
qsTile, qsTile,
if (!isPermissionGranted) Tile.STATE_UNAVAILABLE else Tile.STATE_INACTIVE, Tile.STATE_INACTIVE,
getString(R.string.dns_unknown), getString(R.string.dns_unknown),
R.drawable.ic_unknown_black_24dp R.drawable.ic_unknown_black_24dp
) )
@ -207,10 +175,12 @@ class DnsTileService : TileService() {
override fun onStartListening() { override fun onStartListening() {
super.onStartListening() super.onStartListening()
if (!checkForPermission(this)) {
return
}
// Prevent some crashes // Prevent some crashes
if (qsTile == null) { if (qsTile == null) {
Log.w(TAG, "onStartListening: qsTile is null")
return return
} }
@ -222,17 +192,14 @@ class DnsTileService : TileService() {
IntentFilter("refresh_tile"), IntentFilter("refresh_tile"),
ContextCompat.RECEIVER_NOT_EXPORTED ContextCompat.RECEIVER_NOT_EXPORTED
) )
isBroadcastReceiverRegistered = true
refreshTile() refreshTile()
} }
override fun onStopListening() { override fun onStopListening() {
super.onStopListening() super.onStopListening()
if (isBroadcastReceiverRegistered) { unregisterReceiver(broadcastReceiver)
unregisterReceiver(broadcastReceiver)
isBroadcastReceiverRegistered = false
}
} }
override fun onDestroy() { override fun onDestroy() {
@ -280,23 +247,4 @@ class DnsTileService : TileService() {
PrivateDNSUtils.setPrivateProvider(contentResolver, dnsProvider) PrivateDNSUtils.setPrivateProvider(contentResolver, dnsProvider)
tile.updateTile() 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 package ru.karasevm.privatednstoggle.ui
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import ru.karasevm.privatednstoggle.service.ShortcutService
class SettingsDialogActivity : AppCompatActivity() { class SettingsDialogActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val newFragment = DNSServerDialogFragment() val newFragment = DNSServerDialogFragment()
newFragment.show(supportFragmentManager, DNSServerDialogFragment.TAG) 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.Manifest
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.provider.Settings import android.provider.Settings
import android.widget.Toast
import androidx.core.content.ContextCompat.checkSelfPermission 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 { object PrivateDNSUtils {
const val DNS_MODE_OFF = "off" const val DNS_MODE_OFF = "off"
const val DNS_MODE_AUTO = "opportunistic" const val DNS_MODE_AUTO = "opportunistic"
@ -23,12 +30,12 @@ object PrivateDNSUtils {
// Gets the system dns mode // Gets the system dns mode
fun getPrivateMode(contentResolver: ContentResolver): String { fun getPrivateMode(contentResolver: ContentResolver): String? {
return Settings.Global.getString(contentResolver, PRIVATE_DNS_MODE) return Settings.Global.getString(contentResolver, PRIVATE_DNS_MODE)
} }
// Gets the system dns provider // Gets the system dns provider
fun getPrivateProvider(contentResolver: ContentResolver): String { fun getPrivateProvider(contentResolver: ContentResolver): String? {
return Settings.Global.getString(contentResolver, PRIVATE_DNS_PROVIDER) return Settings.Global.getString(contentResolver, PRIVATE_DNS_PROVIDER)
} }
@ -43,10 +50,72 @@ object PrivateDNSUtils {
} }
fun checkForPermission(context: Context): Boolean { fun checkForPermission(context: Context): Boolean {
return checkSelfPermission( if (checkSelfPermission(
context, context,
Manifest.permission.WRITE_SECURE_SETTINGS Manifest.permission.WRITE_SECURE_SETTINGS
) == PackageManager.PERMISSION_GRANTED ) == 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="add_server_enabled">Enabled</string>
<string name="shizuku_success_toast">Permission granted, you can revoke the Shizuku permission now</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="shizuku_failure_toast">Failed to acquire permission, please grant it manually</string>
<string name="shortcut_switch_mode">Switch Mode</string>
</resources> </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>