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