Loading app/src/main/AndroidManifest.xml +5 −0 Original line number Diff line number Diff line Loading @@ -3,6 +3,8 @@ xmlns:tools="http://schemas.android.com/tools" android:sharedUserId="android.uid.system"> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" /> <uses-permission android:name="android.permission.UPDATE_DEVICE_MANAGEMENT_RESOURCES" Loading Loading @@ -56,6 +58,7 @@ <action android:name="android.app.action.PROFILE_OWNER_CHANGED" /> <action android:name="android.app.action.DEVICE_OWNER_CHANGED" /> <action android:name="foundation.e.parental_control.RESTART_SERVICE" /> <action android:name="foundation.e.parental_control.RESET_PRIVATE_DNS" /> </intent-filter> </receiver> Loading @@ -67,6 +70,8 @@ </intent-filter> </receiver> <service android:name=".DnsCheckService"/> <provider android:name=".data.ParentalContentProvider" android:authorities="foundation.e.parentalcontrol.provider" Loading app/src/main/java/foundation/e/parentalcontrol/DeviceAdmin.kt +8 −0 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import android.content.Context import android.content.Intent import android.util.Log import android.widget.Toast import foundation.e.parentalcontrol.data.DnsManager import foundation.e.parentalcontrol.ui.view.MainUI import foundation.e.parentalcontrol.utils.Constants import java.util.Objects Loading Loading @@ -67,6 +68,12 @@ class DeviceAdmin : DeviceAdminReceiver() { } setSettings(context) } Constants.RESET_PRIVATE_DNS -> { if (isAdminActive(context)) { val mainUI = MainUI(context) mainUI.setPrivateDns(DnsManager.FORCE_UPDATE) } } else -> super.onReceive(context, intent) } } Loading @@ -81,6 +88,7 @@ class DeviceAdmin : DeviceAdminReceiver() { val mainUI = MainUI(context) mainUI.setDefaultRestrictions() mainUI.setDefaultMessages() context.startService(Intent(context, DnsCheckService::class.java)) } } Loading app/src/main/java/foundation/e/parentalcontrol/DnsCheckService.kt 0 → 100644 +114 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 MURENA SAS * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.parentalcontrol import android.app.Service import android.content.Context import android.content.Intent import android.net.ConnectivityManager import android.net.Network import android.os.IBinder import android.util.Log import foundation.e.parentalcontrol.utils.Constants import foundation.e.parentalcontrol.utils.PrefsUtils import foundation.e.parentalcontrol.utils.SystemUtils import java.net.InetSocketAddress import java.net.Socket import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class DnsCheckService : Service() { private val tag = "DnsCheckService" private val dnsPort = 53 private lateinit var connectivityManager: ConnectivityManager private lateinit var networkCallback: ConnectivityManager.NetworkCallback override fun onCreate() { super.onCreate() // Register the network callback networkCallback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { CoroutineScope(Dispatchers.IO).launch { delay(5000) // Wait for 5 seconds if (SystemUtils.isNetworkAvailable(this@DnsCheckService)) { val activeDns = when { isDnsReachable(Constants.DEFAULT_DNS_SERVER) -> Constants.DEFAULT_DNS_SERVER isDnsReachable(Constants.FALLBACK_DNS_SERVER) -> Constants.FALLBACK_DNS_SERVER else -> Constants.DEFAULT_DNS_SERVER } Log.d(tag, "Active DNS server: $activeDns") resetPrivateDns(activeDns) } else { Log.d(tag, "Internet is not working, no DNS check performed.") resetPrivateDns(Constants.DEFAULT_DNS_SERVER) } } } override fun onLost(network: Network) { Log.d(tag, "Network lost, stopping DNS checks.") } } connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager connectivityManager.registerDefaultNetworkCallback(networkCallback) PrefsUtils.init(this) } override fun onDestroy() { super.onDestroy() connectivityManager.unregisterNetworkCallback(networkCallback) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int = START_STICKY private fun resetPrivateDns(dnsHost: String) { PrefsUtils.setDefaultPrivateDns(dnsHost) val intent = Intent(this, DeviceAdmin::class.java) intent.action = Constants.RESET_PRIVATE_DNS this.sendBroadcast(intent) } private suspend fun isDnsReachable(host: String, port: Int = dnsPort): Boolean { return withContext(Dispatchers.IO) { try { Socket().use { socket -> val socketAddress = InetSocketAddress(host, port) socket.connect(socketAddress, 2000) // 2-second timeout true } } catch (e: Exception) { e.printStackTrace() false } } } override fun onBind(intent: Intent?): IBinder? = null } app/src/main/java/foundation/e/parentalcontrol/data/DnsManager.kt 0 → 100644 +30 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 MURENA SAS * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.parentalcontrol.data import androidx.annotation.IntDef class DnsManager { companion object { const val DEFAULT = 0 const val FORCE_UPDATE = 1 } @IntDef(DEFAULT, FORCE_UPDATE) @Retention(AnnotationRetention.SOURCE) annotation class DnsMode } app/src/main/java/foundation/e/parentalcontrol/ui/view/MainUI.kt +15 −12 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ import com.android.net.module.util.ConnectivitySettingsUtils import foundation.e.parentalcontrol.BuildConfig import foundation.e.parentalcontrol.DeviceAdmin import foundation.e.parentalcontrol.R import foundation.e.parentalcontrol.data.DnsManager import foundation.e.parentalcontrol.data.LoginStatus import foundation.e.parentalcontrol.providers.AppLoungeData import foundation.e.parentalcontrol.ui.buttons.ToggleWithText Loading Loading @@ -154,10 +155,12 @@ class MainUI(context: Context) { } } private fun setPrivateDns() { fun setPrivateDns(@DnsManager.DnsMode mode: Int = DnsManager.DEFAULT) { if (!dA.isAdminActive(mContext)) return // Set default private dns if (!isThisRestrictionSet(dnsSettingsRestriction)) { val cloudflareDnsHostName = mContext.getString(R.string.family_cloudflare_dns_com) if (!isThisRestrictionSet(dnsSettingsRestriction) || mode == DnsManager.FORCE_UPDATE) { val defaultDnsHostName = PrefsUtils.getDefaultPrivateDns() val currentDnsHostMode = Settings.Global.getString( mContext.contentResolver, Loading @@ -184,7 +187,7 @@ class MainUI(context: Context) { Settings.Global.putString( mContext.contentResolver, ConnectivitySettingsUtils.PRIVATE_DNS_SPECIFIER, cloudflareDnsHostName defaultDnsHostName ) } Loading Loading @@ -300,30 +303,30 @@ class MainUI(context: Context) { } ) val cloudflareDnsHostName = stringResource(R.string.family_cloudflare_dns_com) val cloudflareToggleState = remember { val defaultDnsHostName = Constants.DEFAULT_DNS_SERVER val defaultToggleState = remember { val isHostname = Settings.Global.getString( mContext.contentResolver, ConnectivitySettingsUtils.PRIVATE_DNS_MODE ) == ConnectivitySettingsUtils.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME_STRING val isCloudflareDns = val isDefaultDns = Settings.Global.getString( mContext.contentResolver, ConnectivitySettingsUtils.PRIVATE_DNS_SPECIFIER ) == cloudflareDnsHostName mutableStateOf(isHostname && isCloudflareDns && dnsSettingsToggleState) ) == defaultDnsHostName mutableStateOf(isHostname && isDefaultDns && dnsSettingsToggleState) } ToggleWithText( text = stringResource(R.string.block_malware_and_adult_content_with_cloudflare), isChecked = cloudflareToggleState.value, text = stringResource(R.string.block_malware_and_adult_content_with_dns), isChecked = defaultToggleState.value, onCheckedChange = { isActive -> if (isActive) { setPrivateDns() } else { removePrivateDns() } cloudflareToggleState.value = isActive defaultToggleState.value = isActive } ) Loading Loading
app/src/main/AndroidManifest.xml +5 −0 Original line number Diff line number Diff line Loading @@ -3,6 +3,8 @@ xmlns:tools="http://schemas.android.com/tools" android:sharedUserId="android.uid.system"> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" /> <uses-permission android:name="android.permission.UPDATE_DEVICE_MANAGEMENT_RESOURCES" Loading Loading @@ -56,6 +58,7 @@ <action android:name="android.app.action.PROFILE_OWNER_CHANGED" /> <action android:name="android.app.action.DEVICE_OWNER_CHANGED" /> <action android:name="foundation.e.parental_control.RESTART_SERVICE" /> <action android:name="foundation.e.parental_control.RESET_PRIVATE_DNS" /> </intent-filter> </receiver> Loading @@ -67,6 +70,8 @@ </intent-filter> </receiver> <service android:name=".DnsCheckService"/> <provider android:name=".data.ParentalContentProvider" android:authorities="foundation.e.parentalcontrol.provider" Loading
app/src/main/java/foundation/e/parentalcontrol/DeviceAdmin.kt +8 −0 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import android.content.Context import android.content.Intent import android.util.Log import android.widget.Toast import foundation.e.parentalcontrol.data.DnsManager import foundation.e.parentalcontrol.ui.view.MainUI import foundation.e.parentalcontrol.utils.Constants import java.util.Objects Loading Loading @@ -67,6 +68,12 @@ class DeviceAdmin : DeviceAdminReceiver() { } setSettings(context) } Constants.RESET_PRIVATE_DNS -> { if (isAdminActive(context)) { val mainUI = MainUI(context) mainUI.setPrivateDns(DnsManager.FORCE_UPDATE) } } else -> super.onReceive(context, intent) } } Loading @@ -81,6 +88,7 @@ class DeviceAdmin : DeviceAdminReceiver() { val mainUI = MainUI(context) mainUI.setDefaultRestrictions() mainUI.setDefaultMessages() context.startService(Intent(context, DnsCheckService::class.java)) } } Loading
app/src/main/java/foundation/e/parentalcontrol/DnsCheckService.kt 0 → 100644 +114 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 MURENA SAS * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.parentalcontrol import android.app.Service import android.content.Context import android.content.Intent import android.net.ConnectivityManager import android.net.Network import android.os.IBinder import android.util.Log import foundation.e.parentalcontrol.utils.Constants import foundation.e.parentalcontrol.utils.PrefsUtils import foundation.e.parentalcontrol.utils.SystemUtils import java.net.InetSocketAddress import java.net.Socket import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class DnsCheckService : Service() { private val tag = "DnsCheckService" private val dnsPort = 53 private lateinit var connectivityManager: ConnectivityManager private lateinit var networkCallback: ConnectivityManager.NetworkCallback override fun onCreate() { super.onCreate() // Register the network callback networkCallback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { CoroutineScope(Dispatchers.IO).launch { delay(5000) // Wait for 5 seconds if (SystemUtils.isNetworkAvailable(this@DnsCheckService)) { val activeDns = when { isDnsReachable(Constants.DEFAULT_DNS_SERVER) -> Constants.DEFAULT_DNS_SERVER isDnsReachable(Constants.FALLBACK_DNS_SERVER) -> Constants.FALLBACK_DNS_SERVER else -> Constants.DEFAULT_DNS_SERVER } Log.d(tag, "Active DNS server: $activeDns") resetPrivateDns(activeDns) } else { Log.d(tag, "Internet is not working, no DNS check performed.") resetPrivateDns(Constants.DEFAULT_DNS_SERVER) } } } override fun onLost(network: Network) { Log.d(tag, "Network lost, stopping DNS checks.") } } connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager connectivityManager.registerDefaultNetworkCallback(networkCallback) PrefsUtils.init(this) } override fun onDestroy() { super.onDestroy() connectivityManager.unregisterNetworkCallback(networkCallback) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int = START_STICKY private fun resetPrivateDns(dnsHost: String) { PrefsUtils.setDefaultPrivateDns(dnsHost) val intent = Intent(this, DeviceAdmin::class.java) intent.action = Constants.RESET_PRIVATE_DNS this.sendBroadcast(intent) } private suspend fun isDnsReachable(host: String, port: Int = dnsPort): Boolean { return withContext(Dispatchers.IO) { try { Socket().use { socket -> val socketAddress = InetSocketAddress(host, port) socket.connect(socketAddress, 2000) // 2-second timeout true } } catch (e: Exception) { e.printStackTrace() false } } } override fun onBind(intent: Intent?): IBinder? = null }
app/src/main/java/foundation/e/parentalcontrol/data/DnsManager.kt 0 → 100644 +30 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 MURENA SAS * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.parentalcontrol.data import androidx.annotation.IntDef class DnsManager { companion object { const val DEFAULT = 0 const val FORCE_UPDATE = 1 } @IntDef(DEFAULT, FORCE_UPDATE) @Retention(AnnotationRetention.SOURCE) annotation class DnsMode }
app/src/main/java/foundation/e/parentalcontrol/ui/view/MainUI.kt +15 −12 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ import com.android.net.module.util.ConnectivitySettingsUtils import foundation.e.parentalcontrol.BuildConfig import foundation.e.parentalcontrol.DeviceAdmin import foundation.e.parentalcontrol.R import foundation.e.parentalcontrol.data.DnsManager import foundation.e.parentalcontrol.data.LoginStatus import foundation.e.parentalcontrol.providers.AppLoungeData import foundation.e.parentalcontrol.ui.buttons.ToggleWithText Loading Loading @@ -154,10 +155,12 @@ class MainUI(context: Context) { } } private fun setPrivateDns() { fun setPrivateDns(@DnsManager.DnsMode mode: Int = DnsManager.DEFAULT) { if (!dA.isAdminActive(mContext)) return // Set default private dns if (!isThisRestrictionSet(dnsSettingsRestriction)) { val cloudflareDnsHostName = mContext.getString(R.string.family_cloudflare_dns_com) if (!isThisRestrictionSet(dnsSettingsRestriction) || mode == DnsManager.FORCE_UPDATE) { val defaultDnsHostName = PrefsUtils.getDefaultPrivateDns() val currentDnsHostMode = Settings.Global.getString( mContext.contentResolver, Loading @@ -184,7 +187,7 @@ class MainUI(context: Context) { Settings.Global.putString( mContext.contentResolver, ConnectivitySettingsUtils.PRIVATE_DNS_SPECIFIER, cloudflareDnsHostName defaultDnsHostName ) } Loading Loading @@ -300,30 +303,30 @@ class MainUI(context: Context) { } ) val cloudflareDnsHostName = stringResource(R.string.family_cloudflare_dns_com) val cloudflareToggleState = remember { val defaultDnsHostName = Constants.DEFAULT_DNS_SERVER val defaultToggleState = remember { val isHostname = Settings.Global.getString( mContext.contentResolver, ConnectivitySettingsUtils.PRIVATE_DNS_MODE ) == ConnectivitySettingsUtils.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME_STRING val isCloudflareDns = val isDefaultDns = Settings.Global.getString( mContext.contentResolver, ConnectivitySettingsUtils.PRIVATE_DNS_SPECIFIER ) == cloudflareDnsHostName mutableStateOf(isHostname && isCloudflareDns && dnsSettingsToggleState) ) == defaultDnsHostName mutableStateOf(isHostname && isDefaultDns && dnsSettingsToggleState) } ToggleWithText( text = stringResource(R.string.block_malware_and_adult_content_with_cloudflare), isChecked = cloudflareToggleState.value, text = stringResource(R.string.block_malware_and_adult_content_with_dns), isChecked = defaultToggleState.value, onCheckedChange = { isActive -> if (isActive) { setPrivateDns() } else { removePrivateDns() } cloudflareToggleState.value = isActive defaultToggleState.value = isActive } ) Loading