diff --git a/app/build/outputs/apk/debug/app-debug.apk b/app/build/outputs/apk/debug/app-debug.apk
new file mode 100644
index 0000000..ab774d2
Binary files /dev/null and b/app/build/outputs/apk/debug/app-debug.apk differ
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 97d56e3..e00eed5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,6 +5,8 @@
+
+
+
\ No newline at end of file
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 23e3496..a72bb64 100644
--- a/app/src/main/java/ru/karasevm/privatednstoggle/service/DnsTileService.kt
+++ b/app/src/main/java/ru/karasevm/privatednstoggle/service/DnsTileService.kt
@@ -19,6 +19,10 @@ import ru.karasevm.privatednstoggle.PrivateDNSApp
import ru.karasevm.privatednstoggle.R
import ru.karasevm.privatednstoggle.data.DnsServerRepository
import ru.karasevm.privatednstoggle.util.PreferenceHelper
+import ru.karasevm.privatednstoggle.util.PreferenceHelper.autoRevertEnabled
+import ru.karasevm.privatednstoggle.util.PreferenceHelper.autoRevertMinutes
+import ru.karasevm.privatednstoggle.util.PreferenceHelper.revertMode
+import ru.karasevm.privatednstoggle.util.PreferenceHelper.revertProvider
import ru.karasevm.privatednstoggle.util.PreferenceHelper.requireUnlock
import ru.karasevm.privatednstoggle.util.PrivateDNSUtils
import ru.karasevm.privatednstoggle.util.PrivateDNSUtils.DNS_MODE_AUTO
@@ -249,6 +253,10 @@ class DnsTileService : TileService() {
tile.label = label
tile.state = state
tile.icon = Icon.createWithResource(this, icon)
+
+ // Schedule auto-revert if enabled
+ PrivateDNSUtils.scheduleAutoRevertIfEnabled(this, contentResolver, sharedPreferences, dnsMode, dnsProvider)
+
PrivateDNSUtils.setPrivateMode(contentResolver, dnsMode)
PrivateDNSUtils.setPrivateProvider(contentResolver, dnsProvider)
tile.updateTile()
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/service/RevertReceiver.kt b/app/src/main/java/ru/karasevm/privatednstoggle/service/RevertReceiver.kt
new file mode 100644
index 0000000..da6289f
--- /dev/null
+++ b/app/src/main/java/ru/karasevm/privatednstoggle/service/RevertReceiver.kt
@@ -0,0 +1,95 @@
+package ru.karasevm.privatednstoggle.service
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import ru.karasevm.privatednstoggle.util.PreferenceHelper
+import ru.karasevm.privatednstoggle.util.PreferenceHelper.revertMode
+import ru.karasevm.privatednstoggle.util.PreferenceHelper.revertProvider
+import ru.karasevm.privatednstoggle.util.PrivateDNSUtils
+import ru.karasevm.privatednstoggle.util.PreferenceHelper.revertScheduledAt
+import android.widget.Toast
+import android.app.NotificationManager
+import android.app.NotificationChannel
+import androidx.core.app.NotificationCompat
+
+
+class RevertReceiver : BroadcastReceiver() {
+ companion object {
+ private const val TAG = "RevertReceiver"
+ }
+
+ private fun showNotification(context: Context, message: String) {
+ try {
+ val channelId = "revert_debug"
+ val notificationId = 12345
+
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+ // Create notification channel for Android 8+
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(channelId, "Revert Debug", NotificationManager.IMPORTANCE_HIGH)
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ val notification = NotificationCompat.Builder(context, channelId)
+ .setContentTitle("DNS Revert")
+ .setContentText(message)
+ .setSmallIcon(android.R.drawable.ic_dialog_info)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setAutoCancel(true)
+ .build()
+
+ notificationManager.notify(notificationId, notification)
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to show notification: ${e.message}")
+ }
+ }
+
+ override fun onReceive(context: Context, intent: Intent) {
+ Log.d(TAG, "onReceive: revert alarm fired - ENTERING BROADCAST RECEIVER")
+
+ val prefs = PreferenceHelper.defaultPreference(context)
+ val mode = prefs.revertMode
+ val provider = prefs.revertProvider
+ val scheduledAt = prefs.revertScheduledAt
+
+ val logMsg = "onReceive: stored revert mode=$mode provider=$provider scheduledAt=$scheduledAt now=${System.currentTimeMillis()}"
+ Log.d(TAG, logMsg)
+
+ if (mode.isNullOrBlank()) {
+ Log.d(TAG, "onReceive: nothing to revert")
+ return
+ }
+
+ // Apply provider first if private
+ if (mode.equals(PrivateDNSUtils.DNS_MODE_PRIVATE, true)) {
+ PrivateDNSUtils.setPrivateProvider(context.contentResolver, if (provider.isNullOrBlank()) null else provider)
+ } else {
+ // when reverting to non-private, preserve provider value
+ PrivateDNSUtils.setPrivateProvider(context.contentResolver, provider)
+ }
+
+ PrivateDNSUtils.setPrivateMode(context.contentResolver, mode)
+
+ val revertMsg = "Private DNS reverted to $mode"
+
+ // Show notification (persistent & visible)
+ showNotification(context, revertMsg)
+
+ // Notify user via toast for debugging
+ try {
+ Toast.makeText(context, revertMsg, Toast.LENGTH_LONG).show()
+ } catch (e: Exception) {
+ Log.w(TAG, "onReceive: failed to show toast: ${e.message}")
+ }
+
+ // clear saved revert info
+ prefs.revertMode = null
+ prefs.revertProvider = null
+
+ val finalMsg = "onReceive: reverted to mode=$mode provider=$provider"
+ Log.d(TAG, finalMsg)
+ }
+}
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/service/RevertScheduler.kt b/app/src/main/java/ru/karasevm/privatednstoggle/service/RevertScheduler.kt
new file mode 100644
index 0000000..283b941
--- /dev/null
+++ b/app/src/main/java/ru/karasevm/privatednstoggle/service/RevertScheduler.kt
@@ -0,0 +1,82 @@
+package ru.karasevm.privatednstoggle.service
+
+import android.app.AlarmManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.SystemClock
+import ru.karasevm.privatednstoggle.util.PreferenceHelper
+import java.util.concurrent.TimeUnit
+import ru.karasevm.privatednstoggle.util.PreferenceHelper.revertScheduledAt
+import android.util.Log
+
+object RevertScheduler {
+ private const val TAG = "RevertScheduler"
+ private const val ACTION_REVERT = "ru.karasevm.privatednstoggle.ACTION_REVERT"
+
+ fun scheduleRevert(context: Context, minutes: Int) {
+ val prefs = PreferenceHelper.defaultPreference(context)
+
+ val logMsg1 = "scheduleRevert: CALLED with minutes=$minutes"
+ Log.d(TAG, logMsg1)
+
+ // Build intent to fire our RevertReceiver
+ val intent = Intent(context, RevertReceiver::class.java).apply {
+ action = ACTION_REVERT
+ setPackage(context.packageName)
+ }
+
+ val pending = PendingIntent.getBroadcast(
+ context,
+ 0,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+
+ // Use elapsed realtime + minutes
+ val triggerAt = SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(minutes.toLong())
+ val triggerAtWall = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(minutes.toLong())
+
+ val logMsg2 = "scheduleRevert: setting alarm triggerAt(elapsed)=$triggerAt triggerAt(wall)=$triggerAtWall"
+ Log.d(TAG, logMsg2)
+
+ try {
+ alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAt, pending)
+
+ val logMsg3 = "scheduleRevert: alarm SET SUCCESSFULLY"
+ Log.d(TAG, logMsg3)
+ } catch (e: Exception) {
+ val logMsg3 = "scheduleRevert: ERROR setting alarm: ${e.message}"
+ Log.e(TAG, logMsg3)
+ }
+
+ // Persist scheduled time for debugging
+ prefs.revertScheduledAt = triggerAtWall
+ val logMsg4 = "scheduleRevert: persisted scheduled_at=$triggerAtWall"
+ Log.d(TAG, logMsg4)
+ }
+
+ fun cancelRevert(context: Context) {
+ val logMsg1 = "cancelRevert: CALLED"
+ Log.d(TAG, logMsg1)
+
+ val intent = Intent(context, RevertReceiver::class.java).apply {
+ action = ACTION_REVERT
+ setPackage(context.packageName)
+ }
+ val pending = PendingIntent.getBroadcast(
+ context,
+ 0,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+ val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+ alarmManager.cancel(pending)
+ val prefs = PreferenceHelper.defaultPreference(context)
+ prefs.revertScheduledAt = 0L
+ val logMsg2 = "cancelRevert: CANCELLED pending revert"
+ Log.d(TAG, logMsg2)
+ }
+}
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/service/ShortcutService.kt b/app/src/main/java/ru/karasevm/privatednstoggle/service/ShortcutService.kt
index 41048e9..69c867c 100644
--- a/app/src/main/java/ru/karasevm/privatednstoggle/service/ShortcutService.kt
+++ b/app/src/main/java/ru/karasevm/privatednstoggle/service/ShortcutService.kt
@@ -10,6 +10,10 @@ import kotlinx.coroutines.SupervisorJob
import ru.karasevm.privatednstoggle.PrivateDNSApp
import ru.karasevm.privatednstoggle.data.DnsServerRepository
import ru.karasevm.privatednstoggle.util.PreferenceHelper
+import ru.karasevm.privatednstoggle.util.PreferenceHelper.autoRevertEnabled
+import ru.karasevm.privatednstoggle.util.PreferenceHelper.autoRevertMinutes
+import ru.karasevm.privatednstoggle.util.PreferenceHelper.revertMode
+import ru.karasevm.privatednstoggle.util.PreferenceHelper.revertProvider
import ru.karasevm.privatednstoggle.util.PrivateDNSUtils
class ShortcutService : Service() {
@@ -34,6 +38,28 @@ class ShortcutService : Service() {
*/
private fun setDnsMode(dnsMode: String, dnsProvider: String? = null) {
Log.d(TAG, "setDnsMode: attempting to set dns mode to $dnsMode with provider $dnsProvider")
+
+ // Auto-revert: capture current state BEFORE change and schedule revert
+ try {
+ val prefs = PreferenceHelper.defaultPreference(this)
+ if (prefs.autoRevertEnabled) {
+ // Save CURRENT state as revert target (will revert back to it after X minutes)
+ val currentMode = PrivateDNSUtils.getPrivateMode(contentResolver)
+ val currentProvider = PrivateDNSUtils.getPrivateProvider(contentResolver)
+ prefs.revertMode = currentMode
+ prefs.revertProvider = currentProvider
+ RevertScheduler.scheduleRevert(this, prefs.autoRevertMinutes)
+ Log.d(TAG, "setDnsMode: auto-revert scheduled. Will revert FROM $dnsMode back TO $currentMode in ${prefs.autoRevertMinutes} minute(s)")
+ } else {
+ // Auto-revert disabled; cancel any pending revert
+ RevertScheduler.cancelRevert(this)
+ prefs.revertMode = null
+ prefs.revertProvider = null
+ }
+ } catch (e: Exception) {
+ Log.w(TAG, "setDnsMode: error with auto-revert: ${e.message}")
+ }
+
if (dnsMode == PrivateDNSUtils.DNS_MODE_PRIVATE) {
PrivateDNSUtils.setPrivateProvider(contentResolver, dnsProvider)
}
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 b2fa284..469c642 100644
--- a/app/src/main/java/ru/karasevm/privatednstoggle/ui/DNSServerDialogFragment.kt
+++ b/app/src/main/java/ru/karasevm/privatednstoggle/ui/DNSServerDialogFragment.kt
@@ -18,6 +18,7 @@ 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
+import ru.karasevm.privatednstoggle.util.PreferenceHelper
class DNSServerDialogFragment : DialogFragment() {
@@ -75,9 +76,15 @@ class DNSServerDialogFragment : DialogFragment() {
).show()
dialog!!.dismiss()
}
+ val sharedPreferences = PreferenceHelper.defaultPreference(requireContext())
+
adapter.onItemClick = { id ->
when (id) {
OFF_ID -> {
+ PrivateDNSUtils.scheduleAutoRevertIfEnabled(
+ requireContext(), contentResolver, sharedPreferences,
+ PrivateDNSUtils.DNS_MODE_OFF, null
+ )
PrivateDNSUtils.setPrivateMode(
contentResolver,
PrivateDNSUtils.DNS_MODE_OFF
@@ -89,6 +96,10 @@ class DNSServerDialogFragment : DialogFragment() {
}
AUTO_ID -> {
+ PrivateDNSUtils.scheduleAutoRevertIfEnabled(
+ requireContext(), contentResolver, sharedPreferences,
+ PrivateDNSUtils.DNS_MODE_AUTO, null
+ )
PrivateDNSUtils.setPrivateMode(
contentResolver,
PrivateDNSUtils.DNS_MODE_AUTO
@@ -102,6 +113,10 @@ class DNSServerDialogFragment : DialogFragment() {
else -> {
lifecycleScope.launch {
val server = servers.find { server -> server.id == id }
+ PrivateDNSUtils.scheduleAutoRevertIfEnabled(
+ requireContext(), contentResolver, sharedPreferences,
+ PrivateDNSUtils.DNS_MODE_PRIVATE, server?.server
+ )
PrivateDNSUtils.setPrivateMode(
contentResolver,
PrivateDNSUtils.DNS_MODE_PRIVATE
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 2ca7b14..42ed825 100644
--- a/app/src/main/java/ru/karasevm/privatednstoggle/ui/OptionsDialogFragment.kt
+++ b/app/src/main/java/ru/karasevm/privatednstoggle/ui/OptionsDialogFragment.kt
@@ -9,6 +9,8 @@ import ru.karasevm.privatednstoggle.databinding.DialogOptionsBinding
import ru.karasevm.privatednstoggle.util.PreferenceHelper
import ru.karasevm.privatednstoggle.util.PreferenceHelper.autoMode
import ru.karasevm.privatednstoggle.util.PreferenceHelper.requireUnlock
+import ru.karasevm.privatednstoggle.util.PreferenceHelper.autoRevertEnabled
+import ru.karasevm.privatednstoggle.util.PreferenceHelper.autoRevertMinutes
import ru.karasevm.privatednstoggle.util.PrivateDNSUtils
class OptionsDialogFragment : DialogFragment() {
@@ -59,5 +61,30 @@ class OptionsDialogFragment : DialogFragment() {
binding.requireUnlockSwitch.setOnCheckedChangeListener { _, isChecked ->
sharedPreferences.requireUnlock = isChecked
}
+
+ val autoRevertEnabled = sharedPreferences.autoRevertEnabled
+ binding.autoRevertSwitch.isChecked = autoRevertEnabled
+ binding.autoRevertSwitch.setOnCheckedChangeListener { _, isChecked ->
+ sharedPreferences.autoRevertEnabled = isChecked
+ }
+
+ val minutes = sharedPreferences.autoRevertMinutes
+ binding.autoRevertMinutesInput.setText(minutes.toString())
+ binding.autoRevertMinutesInput.setOnFocusChangeListener { _, hasFocus ->
+ if (!hasFocus) {
+ val text = binding.autoRevertMinutesInput.text?.toString() ?: ""
+ val value = text.toIntOrNull() ?: minutes
+ val finalValue = if (value <= 0) 1 else value
+ sharedPreferences.autoRevertMinutes = finalValue
+ binding.autoRevertMinutesInput.setText(finalValue.toString())
+ }
+ }
+ // Also save on dialog dismiss to catch any pending edits
+ dialog?.setOnDismissListener {
+ val text = binding.autoRevertMinutesInput.text?.toString() ?: ""
+ val value = text.toIntOrNull() ?: minutes
+ val finalValue = if (value <= 0) 1 else value
+ sharedPreferences.autoRevertMinutes = finalValue
+ }
}
}
\ 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 7fa2d07..90e636c 100644
--- a/app/src/main/java/ru/karasevm/privatednstoggle/util/PrivateDNSUtils.kt
+++ b/app/src/main/java/ru/karasevm/privatednstoggle/util/PrivateDNSUtils.kt
@@ -12,7 +12,12 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import ru.karasevm.privatednstoggle.data.DnsServerRepository
import ru.karasevm.privatednstoggle.model.DnsServer
+import ru.karasevm.privatednstoggle.service.RevertScheduler
import ru.karasevm.privatednstoggle.util.PreferenceHelper.autoMode
+import ru.karasevm.privatednstoggle.util.PreferenceHelper.autoRevertEnabled
+import ru.karasevm.privatednstoggle.util.PreferenceHelper.autoRevertMinutes
+import ru.karasevm.privatednstoggle.util.PreferenceHelper.revertMode
+import ru.karasevm.privatednstoggle.util.PreferenceHelper.revertProvider
object PrivateDNSUtils {
const val DNS_MODE_OFF = "off"
@@ -190,4 +195,42 @@ object PrivateDNSUtils {
}
}
+ /**
+ * Schedule auto-revert if enabled. Captures current DNS state as revert target.
+ *
+ * @param context Android context
+ * @param contentResolver content resolver to read current DNS state
+ * @param sharedPreferences shared preferences for storing revert state and reading config
+ * @param newMode the new DNS mode being set
+ * @param newProvider the new DNS provider being set
+ */
+ fun scheduleAutoRevertIfEnabled(
+ context: Context,
+ contentResolver: ContentResolver,
+ sharedPreferences: SharedPreferences,
+ newMode: String,
+ newProvider: String?
+ ) {
+ try {
+ if (sharedPreferences.autoRevertEnabled) {
+ // Capture current state BEFORE change as revert target
+ val currentMode = getPrivateMode(contentResolver)
+ val currentProvider = getPrivateProvider(contentResolver)
+ sharedPreferences.revertMode = currentMode
+ sharedPreferences.revertProvider = currentProvider
+ // Schedule revert
+ val minutes = sharedPreferences.autoRevertMinutes
+ RevertScheduler.scheduleRevert(context, minutes)
+ Log.d("PrivateDNSUtils", "scheduleAutoRevertIfEnabled: scheduled revert FROM $newMode back TO $currentMode in $minutes minute(s)")
+ } else {
+ // Auto-revert disabled; cancel any pending revert
+ RevertScheduler.cancelRevert(context)
+ sharedPreferences.revertMode = null
+ sharedPreferences.revertProvider = null
+ }
+ } catch (e: Exception) {
+ Log.w("PrivateDNSUtils", "scheduleAutoRevertIfEnabled: error: ${e.message}")
+ }
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/util/SharedPrefUtils.kt b/app/src/main/java/ru/karasevm/privatednstoggle/util/SharedPrefUtils.kt
index 21e4349..b34e05e 100644
--- a/app/src/main/java/ru/karasevm/privatednstoggle/util/SharedPrefUtils.kt
+++ b/app/src/main/java/ru/karasevm/privatednstoggle/util/SharedPrefUtils.kt
@@ -9,6 +9,11 @@ object PreferenceHelper {
const val DNS_SERVERS = "dns_servers"
const val AUTO_MODE = "auto_mode"
const val REQUIRE_UNLOCK = "require_unlock"
+ const val AUTO_REVERT_ENABLED = "auto_revert_enabled"
+ const val AUTO_REVERT_MINUTES = "auto_revert_minutes"
+ const val REVERT_MODE = "revert_mode"
+ const val REVERT_PROVIDER = "revert_provider"
+ const val REVERT_SCHEDULED_AT = "revert_scheduled_at"
fun defaultPreference(context: Context): SharedPreferences =
context.getSharedPreferences("app_prefs", 0)
@@ -48,6 +53,46 @@ object PreferenceHelper {
}
}
+ var SharedPreferences.autoRevertEnabled
+ get() = getBoolean(AUTO_REVERT_ENABLED, false)
+ set(value) {
+ editMe {
+ it.put(AUTO_REVERT_ENABLED to value)
+ }
+ }
+
+ var SharedPreferences.autoRevertMinutes
+ get() = getInt(AUTO_REVERT_MINUTES, 5)
+ set(value) {
+ editMe {
+ it.put(AUTO_REVERT_MINUTES to value)
+ }
+ }
+
+ var SharedPreferences.revertMode
+ get() = getString(REVERT_MODE, null)
+ set(value) {
+ editMe {
+ it.put(REVERT_MODE to (value ?: ""))
+ }
+ }
+
+ var SharedPreferences.revertProvider
+ get() = getString(REVERT_PROVIDER, null)
+ set(value) {
+ editMe {
+ it.put(REVERT_PROVIDER to (value ?: ""))
+ }
+ }
+
+ var SharedPreferences.revertScheduledAt
+ get() = getLong(REVERT_SCHEDULED_AT, 0L)
+ set(value) {
+ editMe {
+ it.put(REVERT_SCHEDULED_AT to value)
+ }
+ }
+
var SharedPreferences.requireUnlock
get() = getBoolean(REQUIRE_UNLOCK, false)
set(value) {
diff --git a/app/src/main/res/layout/dialog_options.xml b/app/src/main/res/layout/dialog_options.xml
index 11f6ce1..9b3a84c 100644
--- a/app/src/main/res/layout/dialog_options.xml
+++ b/app/src/main/res/layout/dialog_options.xml
@@ -62,4 +62,46 @@
android:text="@string/require_unlock_setting"
android:textSize="16sp" />
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 a187413..5dd9257 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -30,6 +30,9 @@
Private DNS set to auto
Private DNS set to %1$s
Require unlocking the device to change server
+ Auto-revert to previous DNS after timeout
+ Revert after
+ min
Drag handle
Import
Export