Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added app/build/outputs/apk/debug/app-debug.apk
Binary file not shown.
5 changes: 5 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<uses-permission
android:name="android.permission.WRITE_SECURE_SETTINGS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer not using restricted permissions. To quote the API reference "This is only intended for use by apps that rely on exact alarms for their core functionality. You should continue using SCHEDULE_EXACT_ALARM if your app needs exact alarms for a secondary feature that users may or may not use within your app."


<application
android:name=".PrivateDNSApp"
Expand Down Expand Up @@ -79,6 +81,9 @@
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<receiver
android:name=".service.RevertReceiver"
android:exported="true" />
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for the receiver being exported? It does not have to be to handle calls from the same package.

</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The app targets the latest recommended sdk, please read the notification api reference and ensure it works properly on modern versions of android. Don't request the notification permission unless user explicitly turns on the revert feature.

try {
val channelId = "revert_debug"
val notificationId = 12345
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace the placeholder IDs.


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) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Private DNS feature was added in A9, minSdk is 28, this check is useless.

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"
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't hardcode strings. If you need formatting, see set_to_provider_toast.


// 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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment in the manifest, replace either with set or setAndAllowWhileIdle.


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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably would be good to cleanup any active alarms if user disables the feature.

}

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
}
}
}
Loading