5 | *
6 | * This program is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | */
11 |
12 | package dev.clombardo.dnsnet.file
13 |
14 | import java.io.File
15 | import java.io.FileInputStream
16 | import java.io.FileNotFoundException
17 | import java.io.FileOutputStream
18 | import java.io.IOException
19 | import java.io.InputStream
20 |
21 | /**
22 | * A file that multiple readers can safely read from and a single
23 | * writer thread can safely write too, without any synchronisation.
24 | *
25 | * Implements the same API as AtomicFile, but avoids modifications
26 | * in openRead(), so it is safe to open files for reading while
27 | * writing, without losing the writes.
28 | *
29 | * It uses two files: The specified one, and a work file with a suffix. On
30 | * failure, the work file is deleted; on success, it rename()ed to the specified
31 | * one, causing it to replace that atomically.
32 | */
33 | class SingleWriterMultipleReaderFile(file: File) {
34 | val activeFile = file.absoluteFile
35 | val workFile = File(activeFile.absolutePath + ".dnsnet-new")
36 |
37 | /**
38 | * Opens the known-good file for reading.
39 | * @return A {@link FileInputStream} to read from
40 | * @throws FileNotFoundException See {@link FileInputStream}
41 | */
42 | @Throws(FileNotFoundException::class)
43 | fun openRead(): InputStream = FileInputStream(activeFile)
44 |
45 | /**
46 | * Starts a write.
47 | * @return A writable stream.
48 | * @throws IOException If the work file cannot be replaced or opened for writing.
49 | */
50 | @Throws(IOException::class)
51 | fun startWrite(): FileOutputStream {
52 | if (workFile.exists() && !workFile.delete()) {
53 | throw IOException("Cannot delete working file")
54 | }
55 | return FileOutputStream(workFile)
56 | }
57 |
58 | /**
59 | * Atomically replaces the active file with the work file, and closes the stream.
60 | * @param stream
61 | * @throws IOException
62 | */
63 | @Throws(IOException::class)
64 | fun finishWrite(stream: FileOutputStream) {
65 | try {
66 | stream.close()
67 | } catch (e: IOException) {
68 | failWrite(stream)
69 | throw e
70 | }
71 | if (!workFile.renameTo(activeFile)) {
72 | failWrite(stream)
73 | throw IOException("Cannot commit transaction")
74 | }
75 | }
76 |
77 | /**
78 | * Atomically replaces the active file with the work file, and closes the stream.
79 | * @param stream
80 | * @throws IOException
81 | */
82 | @Throws(IOException::class)
83 | fun failWrite(stream: FileOutputStream) {
84 | FileHelper.closeOrWarn(stream, "Cannot close working file")
85 | if (!workFile.delete()) {
86 | throw IOException("Cannot delete working file")
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | android.enableJetifier=false
10 | android.nonFinalResIds=false
11 | android.nonTransitiveRClass=false
12 | android.useAndroidX=true
13 | org.gradle.jvmargs=-Xmx1536m
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 |
19 | # The Rust plugin is broken when the configuration cache is enabled
20 | # org.gradle.configuration-cache=true
21 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionSha256Sum=61ad310d3c7d3e5da131b76bbf22b5a4c0786e9d892dae8c1658d4b484de3caa
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
5 | networkTimeout=10000
6 | validateDistributionUrl=true
7 | zipStoreBase=GRADLE_USER_HOME
8 | zipStorePath=wrapper/dists
9 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/log/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/log/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | plugins {
10 | alias(libs.plugins.android.library)
11 | alias(libs.plugins.kotlin.android)
12 | }
13 |
14 | android {
15 | namespace = "dev.clombardo.dnsnet.log"
16 | compileSdk = libs.versions.compileSdk.get().toInt()
17 |
18 | defaultConfig {
19 | minSdk = libs.versions.minSdk.get().toInt()
20 |
21 | consumerProguardFiles("consumer-rules.pro")
22 | }
23 |
24 | buildTypes {
25 | create("benchmark")
26 | }
27 |
28 | buildFeatures {
29 | buildConfig = true
30 | }
31 | }
32 |
33 | kotlin {
34 | jvmToolchain(libs.versions.java.get().toInt())
35 | }
36 |
--------------------------------------------------------------------------------
/log/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/log/consumer-rules.pro
--------------------------------------------------------------------------------
/log/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/log/src/main/kotlin/dev/clombardo/dnsnet/log/Log.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.log
10 |
11 | import android.util.Log
12 |
13 | fun Any.className(): String = this::class.java.simpleName
14 |
15 | fun Any.logDebug(message: String, error: Throwable? = null) {
16 | if (BuildConfig.DEBUG) {
17 | Log.d(this.className(), message, error)
18 | }
19 | }
20 |
21 | inline fun Any.logDebug(error: Throwable? = null, crossinline lazyMessage: () -> String) {
22 | if (BuildConfig.DEBUG) {
23 | Log.d(this.className(), lazyMessage(), error)
24 | }
25 | }
26 |
27 | fun Any.logVerbose(message: String, error: Throwable? = null) =
28 | Log.v(this.className(), message, error)
29 | inline fun Any.logVerbose(error: Throwable? = null, crossinline lazyMessage: () -> String) =
30 | logVerbose(lazyMessage(), error)
31 |
32 | fun Any.logInfo(message: String, error: Throwable? = null) =
33 | Log.i(this.className(), message, error)
34 | inline fun Any.logInfo(error: Throwable? = null, crossinline lazyMessage: () -> String) =
35 | logInfo(lazyMessage(), error)
36 |
37 | fun Any.logWarning(message: String, error: Throwable? = null) =
38 | Log.w(this.className(), message, error)
39 | inline fun Any.logWarning(error: Throwable? = null, crossinline lazyMessage: () -> String) =
40 | logWarning(lazyMessage(), error)
41 |
42 | fun Any.logError(message: String, error: Throwable? = null) =
43 | Log.e(this.className(), message, error)
44 | inline fun Any.logError(error: Throwable? = null, crossinline lazyMessage: () -> String) =
45 | logError(lazyMessage(), error)
46 |
47 | fun Any.logwtf(message: String, error: Throwable? = null) =
48 | Log.wtf(this.className(), message, error)
49 | inline fun Any.logwtf(error: Throwable? = null, crossinline lazyMessage: () -> String) =
50 | logwtf(lazyMessage(), error)
51 |
--------------------------------------------------------------------------------
/metadata/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | DNSNet allows you to take more control over what internet traffic goes in and out of your device. You can download host files to block a set of known advertising or malicious host names and then create exemptions where you see fit.
2 |
3 | It works by creating a lightweight VPN service that filters your internet traffic as you use your device. If you ever have trouble with connecting to a site or using an app, you can always exempt an app from filtering or create an exception for a specific host name.
4 |
--------------------------------------------------------------------------------
/metadata/en-US/images/feature-graphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/metadata/en-US/images/feature-graphic.png
--------------------------------------------------------------------------------
/metadata/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/metadata/en-US/images/icon.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/apps-p9p.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/metadata/en-US/images/phoneScreenshots/apps-p9p.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/apps-pfp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/metadata/en-US/images/phoneScreenshots/apps-pfp.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/apps-pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/metadata/en-US/images/phoneScreenshots/apps-pt.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/dns-p9p.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/metadata/en-US/images/phoneScreenshots/dns-p9p.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/dns-pfp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/metadata/en-US/images/phoneScreenshots/dns-pfp.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/dns-pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/metadata/en-US/images/phoneScreenshots/dns-pt.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/hosts-p9p.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/metadata/en-US/images/phoneScreenshots/hosts-p9p.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/hosts-pfp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/metadata/en-US/images/phoneScreenshots/hosts-pfp.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/hosts-pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/metadata/en-US/images/phoneScreenshots/hosts-pt.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/start-p9p.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/metadata/en-US/images/phoneScreenshots/start-p9p.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/start-pfp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/metadata/en-US/images/phoneScreenshots/start-pfp.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/start-pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/metadata/en-US/images/phoneScreenshots/start-pt.png
--------------------------------------------------------------------------------
/metadata/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | Lightweight ad and content blocker
2 |
--------------------------------------------------------------------------------
/metadata/es-ES/full_description.txt:
--------------------------------------------------------------------------------
1 | DNSNet te permite tener un mayor control sobre el tráfico de Internet que entra y sale de tu dispositivo. Puedes descargar archivos host para bloquear un conjunto de dominios maliciosos o publicitarios conocidos y luego crear exenciones cuando lo consideres necesario.
2 |
3 | Funciona creando un servicio VPN ligero que filtra tu tráfico de Internet mientras utilizas tu dispositivo. Si alguna vez tienes problemas para conectarte a un sitio o utilizar una aplicación, siempre puedes eximir una aplicación del filtrado o crear una excepción para un dominio específico.
4 |
--------------------------------------------------------------------------------
/metadata/es-ES/short_description.txt:
--------------------------------------------------------------------------------
1 | Bloqueador ligero de anuncios y contenido
2 |
--------------------------------------------------------------------------------
/metadata/fa-IR/full_description.txt:
--------------------------------------------------------------------------------
1 | DNSNet به شما این امکان را می دهد که کنترل بیشتری بر ترافیک اینترنتی که از دستگاه شما وارد و خارج می شود داشته باشید. می توانید فایل های host را دانلود کنید تا مجموعه ای از سرویس های تبلیغاتی یا خطر ناک شناخته شده را مسدود کنید و سپس برکناری هایی را در جایی که مناسب می دانید ایجاد کنید.
2 |
3 | این سرویس با ایجاد یک تانل VPN سبک وزن که ترافیک اینترنت شما را هنگام استفاده از دستگاه خود فیلتر می کند، کار می کند. اگر زمانی در اتصال به یک سایت یا استفاده از یک برنامه مشکل داشتید، همیشه می توانید یک برنامه را از فیلتر کردن بر کنار کنید یا برای اسم host خاصی استثنا قائل شوید.
4 |
--------------------------------------------------------------------------------
/metadata/fa-IR/short_description.txt:
--------------------------------------------------------------------------------
1 | مسدود کننده ی تبلیغات و محتوای سبک وزن
2 |
--------------------------------------------------------------------------------
/metadata/id/full_description.txt:
--------------------------------------------------------------------------------
1 | DNSNet memungkinkan Anda untuk mengambil kendali penuh atas lalu lintas internet yang masuk dan keluar dari perangkat Anda. Anda dapat mengunduh berkas host untuk memblokir sekumpulan nama host yang diketahui beriklan atau berbahaya, lalu membuat pengecualian sesuai keinginan Anda.
2 |
3 | Aplikasi ini bekerja dengan membuat layanan VPN ringan yang menyaring lalu lintas internet saat Anda menggunakan perangkat. Jika Anda mengalami masalah saat menyambung ke sebuah situs atau menggunakan aplikasi, Anda selalu bisa mengecualikan aplikasi dari penyaringan atau membuat pengecualian untuk nama host tertentu.
4 |
--------------------------------------------------------------------------------
/metadata/id/short_description.txt:
--------------------------------------------------------------------------------
1 | Pemblokir iklan dan konten yang ringan
2 |
--------------------------------------------------------------------------------
/metadata/it-IT/full_description.txt:
--------------------------------------------------------------------------------
1 | DNSNet ti permette di avere più controllo sul traffico internet in entrata e in uscita dal tuo dispositivo. Puoi scaricare file host per bloccare un insieme di nomi host pubblicitari o malevoli conosciuti, e poi creare delle eccezioni dove lo ritieni opportuno.
2 |
3 | Funziona creando un servizio VPN leggero che filtra il tuo traffico internet mentre usi il tuo dispositivo. Se dovessi mai avere problemi a connetterti a un sito o a usare un'app, puoi sempre escludere un'app dal filtraggio o creare un'eccezione per uno specifico nome host.
4 |
--------------------------------------------------------------------------------
/metadata/it-IT/short_description.txt:
--------------------------------------------------------------------------------
1 | Blocco leggero per pubblicità e contenuti
2 |
--------------------------------------------------------------------------------
/metadata/pl-PL/short_description.txt:
--------------------------------------------------------------------------------
1 | Szybkie blokowanie reklam i innych niepożądanych treści
2 |
--------------------------------------------------------------------------------
/metadata/ru-RU/full_description.txt:
--------------------------------------------------------------------------------
1 | DNSNet позволяет вам управлять как интернет-данные входят и выходят из вашего устройства. Вы можете загрузить файл имён или адресов серверов, чтобы заблокировать набор известных рекламных или вредных хостов, а затем создать исключения, где вы считаете нужно.
2 |
3 | Он работает создавая лёгкую VPN службу которая фильтрует ваш интернет-трафик при использовании вашего устройства. Если у вас есть проблемы при подключении к сайту или использовании приложения, вы всегда можете отключить фильтрацию для приложения или создать исключение для опредёленного сервера.
4 |
--------------------------------------------------------------------------------
/metadata/ru-RU/short_description.txt:
--------------------------------------------------------------------------------
1 | Лёгкий блокировщик рекламы
2 |
--------------------------------------------------------------------------------
/metadata/tr-TR/full_description.txt:
--------------------------------------------------------------------------------
1 | DNSNet cihazınızdan hangi internet trafiğinin girip çıktığına dair daha fazla kontrol ele almanızı sağlar. Bilinen bir dizi reklam ya da kötü niyetli sunucu adreslerini engellemek için "host dosyaları" indirip, gerek gördüğünüz yerde istisnalar oluşturabilirsiniz.
2 |
3 |
4 | Cihazınızı kullandıkça internet trafiğinizi süzgüden geçiren hafif bir VPN hizmeti oluşturarak çalışır. Eğer bir siteye bağlanmakta ya da bir uygulamayı açmakta sorun yaşarsanız, bir uygulamayı ya da sunucu adresini süzgüye istisna olarak ayarlayabilirsiniz.
5 |
--------------------------------------------------------------------------------
/metadata/tr-TR/short_description.txt:
--------------------------------------------------------------------------------
1 | Hafif reklam ve içerik engelleyicisi
2 |
--------------------------------------------------------------------------------
/notification/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/notification/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | plugins {
10 | alias(libs.plugins.android.library)
11 | alias(libs.plugins.kotlin.android)
12 | }
13 |
14 | android {
15 | namespace = "dev.clombardo.dnsnet.notification"
16 | compileSdk = libs.versions.compileSdk.get().toInt()
17 |
18 | defaultConfig {
19 | minSdk = libs.versions.minSdk.get().toInt()
20 |
21 | consumerProguardFiles("consumer-rules.pro")
22 | }
23 |
24 | buildTypes {
25 | create("benchmark")
26 | }
27 | }
28 |
29 | kotlin {
30 | jvmToolchain(libs.versions.java.get().toInt())
31 | }
32 |
33 | dependencies {
34 | implementation(project(":resources"))
35 | }
36 |
--------------------------------------------------------------------------------
/notification/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/notification/consumer-rules.pro
--------------------------------------------------------------------------------
/notification/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/notification/src/main/kotlin/dev/clombardo/dnsnet/notification/NotificationChannels.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * Derived from DNS66:
4 | * Copyright (C) 2017 Julian Andres Klode
5 | *
6 | * This program is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | */
11 |
12 | package dev.clombardo.dnsnet.notification
13 |
14 | import android.app.NotificationChannel
15 | import android.app.NotificationChannelGroup
16 | import android.app.NotificationManager
17 | import android.content.Context
18 | import android.os.Build
19 |
20 | /**
21 | * Helper object containing IDs of notification channels and code to create them.
22 | */
23 | object NotificationChannels {
24 | const val GROUP_SERVICE = "dev.clombardo.dnsnet.notifications.service"
25 | const val SERVICE_RUNNING = "dev.clombardo.dnsnet.notifications.service.running"
26 | const val SERVICE_PAUSED = "dev.clombardo.dnsnet.notifications.service.paused"
27 | const val GROUP_UPDATE = "dev.clombardo.dnsnet.notifications.update"
28 | const val UPDATE_STATUS = "dev.clombardo.dnsnet.notifications.update.status"
29 |
30 | fun onCreate(context: Context) {
31 | val notificationManager =
32 | context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
33 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
34 | return
35 | }
36 |
37 | notificationManager.createNotificationChannelGroup(
38 | NotificationChannelGroup(
39 | GROUP_SERVICE,
40 | context.getString(R.string.notifications_group_service)
41 | )
42 | )
43 | notificationManager.createNotificationChannelGroup(
44 | NotificationChannelGroup(
45 | GROUP_UPDATE,
46 | context.getString(R.string.notifications_group_updates)
47 | )
48 | )
49 |
50 | val runningChannel = NotificationChannel(
51 | SERVICE_RUNNING,
52 | context.getString(R.string.notifications_running),
53 | NotificationManager.IMPORTANCE_LOW
54 | ).apply {
55 | description = context.getString(R.string.notifications_running_desc)
56 | group = GROUP_SERVICE
57 | setShowBadge(false)
58 | }
59 | notificationManager.createNotificationChannel(runningChannel)
60 |
61 | val pausedChannel = NotificationChannel(
62 | SERVICE_PAUSED,
63 | context.getString(R.string.notifications_paused),
64 | NotificationManager.IMPORTANCE_LOW
65 | ).apply {
66 | description = context.getString(R.string.notifications_paused_desc)
67 | group = GROUP_SERVICE
68 | setShowBadge(false)
69 | }
70 | notificationManager.createNotificationChannel(pausedChannel)
71 |
72 | val updateChannel = NotificationChannel(
73 | UPDATE_STATUS,
74 | context.getString(R.string.notifications_update),
75 | NotificationManager.IMPORTANCE_LOW
76 | ).apply {
77 | description = context.getString(R.string.notifications_update_desc)
78 | group = GROUP_UPDATE
79 | setShowBadge(false)
80 | }
81 | notificationManager.createNotificationChannel(updateChannel)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/resources/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/resources/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | plugins {
10 | alias(libs.plugins.android.library)
11 | alias(libs.plugins.kotlin.android)
12 | }
13 |
14 | android {
15 | namespace = "dev.clombardo.dnsnet.resources"
16 | compileSdk = libs.versions.compileSdk.get().toInt()
17 |
18 | defaultConfig {
19 | minSdk = libs.versions.minSdk.get().toInt()
20 |
21 | consumerProguardFiles("consumer-rules.pro")
22 | }
23 |
24 | buildTypes {
25 | create("benchmark")
26 | }
27 |
28 | lint {
29 | disable.apply {
30 | add("MissingTranslation")
31 | add("ExtraTranslation")
32 | }
33 | }
34 | }
35 |
36 | dependencies {
37 | implementation(libs.androidx.core.splashscreen)
38 | implementation(libs.androidx.appcompat)
39 | }
40 |
--------------------------------------------------------------------------------
/resources/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/resources/consumer-rules.pro
--------------------------------------------------------------------------------
/resources/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/resources/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
11 |
16 |
21 |
26 |
31 |
36 |
41 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/resources/src/main/res/drawable/ic_refresh.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
12 |
14 |
15 |
--------------------------------------------------------------------------------
/resources/src/main/res/drawable/ic_state_allow.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
13 |
14 |
--------------------------------------------------------------------------------
/resources/src/main/res/drawable/ic_state_deny.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/resources/src/main/res/drawable/ic_state_ignore.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/resources/src/main/res/drawable/ic_warning.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/resources/src/main/res/drawable/icon_full.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
11 |
12 |
16 |
17 |
21 |
22 |
26 |
27 |
31 |
32 |
36 |
37 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/resources/src/main/res/font/roboto_flex.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/resources/src/main/res/font/roboto_flex.ttf
--------------------------------------------------------------------------------
/resources/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/resources/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/resources/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/resources/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/resources/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/resources/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/resources/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/resources/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/resources/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/resources/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/resources/src/main/res/mipmap-xhdpi/ic_banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/resources/src/main/res/mipmap-xhdpi/ic_banner.png
--------------------------------------------------------------------------------
/resources/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/resources/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/resources/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/resources/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/resources/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/resources/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/resources/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/resources/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/resources/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/resources/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/resources/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/resources/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/resources/src/main/res/values-et/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Hostide blokeerimine ja kohandatud DNS-serverid
4 | Kohandatud DNS-serverid
5 | Algus
6 | DNS
7 | Ekspordi seaded
8 | Impordi seaded
9 | Laadi vaikimisi seaded
10 | Teave
11 | See programm on vaba tarkvara: sa võid seda edasi levitada ja/või muuta litsensi GNU General Public License tingimuste alusel, nagu on postitanud Free Software Foundation, kas litsensi 3. versioon või (omal valikul) mõni hilisem versioon.
12 | Jätka süsteemi käivitusel
13 | Pealkiri
14 | Asukoht (URL või host)
15 | Tegevus
16 | Konfiguratsiooni ei saa kirjutada: %s
17 | Värskenda hostifaile
18 | Käivitun
19 | Aktiivne
20 | Peatan
21 | Ootan võrgu järel
22 | Taasühendun
23 | Taasühendamise viga
24 | Peatatud
25 | Keela
26 | Luba
27 | Ignoreeri
28 |
29 | - @string/deny
30 | - @string/allow
31 | - @string/ignore
32 |
33 |
34 | Eesti keelde tõlkinud Madis0
35 | Versioon: %s
36 | Ei
37 | Jah
38 | Rakendused
39 | Luba oma eelistatud DNS-serverid
40 | DNS-serveri IP-aadress (IPv4 või IPv6)
41 | Lubatud
42 | Uuendus teostamata
43 | Mõndasid hostifaile ei suudetud alla laadida. Kui failid olid eelnevalt alla laaditud, kasutatakse jätkuvalt vanu versioone.
44 | Kasuta faili
45 | Taotletud faili ei saa kasutada: pole õigust soovitud faili püsivaks kasutamiseks
46 | Luba tühistatud
47 | Sobimatu URL: %1$s
48 | Faili ei leitud
49 | Ootamatu viga: %1$s
50 | Taotluse ajalõpp
51 | Kustuta
52 | Peata
53 | Käivita
54 | Värskenda igapäevaselt
55 | Igapäevased uuendused toimuvad siis, kui seade laeb, on ebaaktiivne ja ühendatud mahupiiranguta võrku.
56 | Jälgi ühendust
57 | Kontrolli ühendust pidevalt pikenevate intervallidega ja taasühenda, kui see lakkas töötamast
58 | Muuda DNS-serverit
59 | Paus
60 | Keela see, kui su võrk ei kasuta IPv6-te või sa ei saa teenust käivitada.
61 | IPv6-tugi
62 | Logcat
63 |
64 | - Ära ignoreeri vaikimisi ühtegi rakendust
65 | - Ignoreeri vaikimisi kõiki rakendusi
66 | - Ignoreeri vaikimisi süsteemirakendusi
67 |
68 |
69 |
--------------------------------------------------------------------------------
/resources/src/main/res/values-fa/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | مجاز
4 | اطلاعات درباره ی DNSNet
5 | بسته
6 | خوش آمدید به
7 | توقف
8 | فعال
9 | خطای غیر منتظره:%1$s
10 | این %s را به صورت همیشه گی پاک می کند
11 | برای ادامه یک گزینه را انتخاب کنید
12 | شروع
13 | سرور DNS سفارشی
14 | DNS
15 | خارج کردن تنظیمات
16 | درباره
17 | عنوان
18 | ساخت یک نسخه ی پشتیبان از تنظیمات شما
19 | بارگذاری یک فایل تنظیمات
20 | بارگذاری تنظیمات پیش فرض
21 | ادامه در هنگام بالا امدن سیستم
22 | مکان (URL یا host)
23 | در انتظار برای شبکه
24 | تازه سازی فایل های host
25 | در حال توقف
26 | اتصال مجدد
27 | خطا در هنگام اتصال مجدد
28 | متوقف شد
29 | نسخه:%s
30 | مشاهده ی کد مبدا
31 | برنامه ها
32 | فعال
33 | بر پایه ی DN66 ساخته شده توسط Julian Andres Klode
34 | ادرس IP سرور DNS (IPv4 یا IPv6)
35 | فایل پیدا نشد
36 | شروع
37 | اضافه کردن سرور DNS
38 | اضافه کردن ادرس
39 | پشتیبانی از IPv6
40 | تلاش مجدد
41 | ویرایش فهرست
42 | جست و جو
43 | فیلتر
44 | مرتب کردن
45 | تمام
46 | DNS بر روی HTTP/3
47 | حداقل یک سرور باید انتخاب شود
48 | ویرایش سرور DNS
49 | وارد کردن تنظیمات
50 | اقدام
51 | خیر
52 | برنامه های سیستم
53 | بله
54 | حذف
55 | ضبط هر اتصال برای مشاهده و انالیز
56 | باز گردانی تمامی تنظیمات به مقدار اولیه
57 | غیر فعال
58 | ادامه
59 | تلاش ها
60 | این به صورت همیشه گی تنضیمات فعلی شما را حذف می کند.
61 | کمک
62 | بیشتر یاد بگیرید
63 | فارسی، مترجم : سید علی هاشمی نژاد
64 | در حال راه اندازی
65 | برکناری DNSNet برای برنامه های علامت گذاری شده
66 | به روز رسانی ناقص
67 | برخی فایل های میزبان را نمی توان دانلود کرد. اگر فایل ها قبلا دانلود شده بودند، نسخه های قدیمی همچنان مورد استفاده قرار خواهند گرفت.
68 | استفاده از فایل
69 |
70 |
--------------------------------------------------------------------------------
/resources/src/main/res/values-hi/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/resources/src/main/res/values-ja/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | ホストのブロックとカスタム DNS サーバー
4 | カスタム DNS サーバーを有効にする
5 | 開始
6 | DNS サーバー
7 | 設定をエクスポート
8 | 設定をインポート
9 | デフォルトの設定を読み込み
10 | アプリについて
11 | このプログラムはフリーソフトウェアです: フリーソフトウェア財団が発行した GNU 一般公衆利用許諾契約書、バージョン 3 のライセンス、または(オプションで)それ以降のバージョンのいずれかの条件で、再配布および/または変更することができます。
12 | 起動時に自動的に開始する
13 | タイトル
14 | ロケーション (URL またはホスト)
15 | 操作
16 | 設定を書き込みできません: %s
17 | ホストファイルを更新
18 | 開始中
19 | 実行中
20 | 停止中
21 | ネットワークの待機中
22 | 再接続中
23 | 再接続エラー
24 | 停止しました
25 | 拒否
26 | 許可
27 | 無視
28 |
29 | - @string/deny
30 | - @string/allow
31 | - @string/ignore
32 |
33 |
34 | 日本語翻訳: Naofumi Fukue
35 | バージョン: %s
36 | いいえ
37 | はい
38 | アプリ
39 | 優先の DNS サーバーを有効にする
40 | DNS サーバーの IP アドレス (IPv4 または IPv6)
41 | 有効
42 | 更新が未完了
43 | 一部のホストファイルがダウンロードできませんでした。 ファイルを以前ダウンロードしている場合は、古いバージョンが引き続き使用されます。
44 | ファイルを使用
45 | 要求したファイルを使用できません: 要求したファイルを永続的に使用できません
46 | アクセスが拒否されました
47 | 無効な URL: %1$s
48 | ファイルが見つかりません
49 | 予期しないエラー: %1$s
50 | リクエストタイムアウト
51 | 削除
52 | 停止
53 | 開始
54 | 日次更新
55 | 日次の更新は、充電中、アイドル、およびネットワークが従量課金ではない場合に行います。
56 | 接続を監視
57 | より長い間隔で接続を確認し、動作が停止した場合は再接続します
58 | DNS サーバーを編集
59 | 一時停止
60 | ネットワークが IPv6 を使用していない場合や、サービスを開始できない場合は、これを無効にしてください。
61 | IPv6 サポート
62 |
63 | - デフォルトでアプリをバイパスしません
64 | - デフォルトですべてのアプリをバイパスします
65 | - デフォルトでシステムアプリをバイパスします
66 |
67 |
68 |
--------------------------------------------------------------------------------
/resources/src/main/res/values-nb/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Vertsblokkering og tilpassede DNS-tjenere
4 | Tilpassede DNS-tjenere
5 | Start
6 | DNS
7 | Eksporter innstillinger
8 | Importer innstillinger
9 | Last standardinnstillingene
10 | Om DNSNet
11 | Dette programmet er fri programvare: du kan distribuere det rundt og/eller modifisere den under betingelsene til GNU General Public License som publisert av Free Software Foundation, enten versjon 3 av den lisensen eller (Valgfritt) enhver senere versjon.
12 | Fortsett ved systemoppstart
13 | Tittel
14 | Plassering (Nettadresse eller vert)
15 | Handling
16 | Klarte ikke å lagre oppsettet: %s
17 | Oppfrisk vertsfilene
18 | Starter opp
19 | Aktiv
20 | Stopper
21 | Venter på nettverk
22 | Kobler til på nytt
23 | Feil under tilkobling på nytt
24 | Stoppet
25 | Nekt
26 | Tillat
27 | Ignorer
28 |
29 | - @string/deny
30 | - @string/allow
31 | - @string/ignore
32 |
33 | Versjon: %s
34 | Nei
35 | Ja
36 | Apper
37 | Aktiver dine foretrukkede DNS-tjenere
38 | IP-adressen til DNS-tjeneren (IPv6 eller IPv4)
39 | Aktivert
40 | Oppdateringen ble ikke fullført
41 | Noen vertsfiler kunne ikke bli lastet ned. Dersom filene ble lastet ned tidligere, vil de gamle versjonene fortsatt bli brukt.
42 | Bruk fil
43 | Kan ikke bruke den ønskede filen: Har ikke tillatelse til å kontinuerlig bruke den ønskede filen
44 | Tillatelse avslåttPermission denied
45 | Ugyldig nettadresse: %1$s
46 | Filen ble ikke funnet
47 | Uventet feil: %1$s
48 | Forespørselen brukte for lang tid
49 | Slett
50 | Stopp
51 | Start
52 | Oppfrisk daglig
53 | Daglige oppdateringer skjer når enheten lader, ikke brukes, og er på et ikke-datamengdebegrenset nettverk.
54 | Overvåk tilkoblingen
55 | Sjekk tilkoblingen med stadig lengre intervaller, og koble til på nytt dersom den sluttet å fungere
56 | Rediger DNS-tjeneren
57 | Pause
58 | Skru av dette dersom nettverket ditt ikke bruker IPv6, eller hvis du ikke får startet tjenesten.
59 | IPv6-støtte
60 | Logcat
61 |
62 | - Ingen apper slipper gjennom som standard
63 | - Alle apper slipper gjennom som standard
64 | - Systemapper slipper gjennom som standard
65 |
66 |
67 |
--------------------------------------------------------------------------------
/resources/src/main/res/values-night/color.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FF181114
4 |
5 |
--------------------------------------------------------------------------------
/resources/src/main/res/values-nl/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Hostblokkering en aangepaste DNS-servers
4 | Aangepaste DNS-servers
5 | Start
6 | DNS
7 | Instellingen exporteren
8 | Instellingen importeren
9 | Standaardinstellingen laden
10 | Over
11 | Dit programma is gratis software: je mag het aanpassen en distribueren onder de voorwaarden van de GNU General Public License zoals gepubliceerd bij de Free Software Foundation, ofwel versie 3 van de licentie, of (naar keuze) iedere latere versie.
12 | Hervatten bij systeem opstarten
13 | Titel
14 | Locatie (URL of host)
15 | Actie
16 | Kan configuratie niet schrijven: %s
17 | Hosts-bestanden verversen
18 | Beginnen
19 | Actief
20 | Stoppen
21 | Wachten op netwerk
22 | Opnieuw verbinden
23 | Opnieuw verbinden fout
24 | Gestopt
25 | Weigeren
26 | Toestaan
27 | Negeren
28 |
29 | - @string/deny
30 | - @string/allow
31 | - @string/ignore
32 |
33 |
34 | Nederlandse vertaling: Roy Schutte
35 | Versie: %s
36 | Nee
37 | Ja
38 | Apps
39 | Jouw voorkeurs-DNS-servers inschakelen
40 | IP-adres van DNS-server (IPv4 of IPv6)
41 | Ingeschakeld
42 | Update onvolledig
43 | Sommige hosts-bestanden konden niet worden gedownload. Als er eerder gedownloade bestanden waren, worden deze oudere versies gebruikt.
44 | Bestand gebruiken
45 | Kan verzochte bestand niet gebruiken: Niet gemachtigd om het verzochte bestand persistent te gebruiken
46 | Toestemming niet verleend
47 | Ongeldige URL: %1$s
48 | Bestand niet gevonden
49 | Onverwachte fout: %1$s
50 | Verzoektijd verlopen
51 | Verwijderen
52 | Stop
53 | Begin
54 | Dagelijks verversen
55 | Dagelijkse updates gebeuren tijdens opladen, sluimeren, en op een ongelimiteerd netwerk.
56 | Verbinding bewaken
57 | Controleer de verbinding in toenemende tussenpozen en verbind opnieuw als het stopt met werken
58 | DNS-server bewerken
59 | Pauzeren
60 | Uitschakelen als je netwerk geen IPv6 gebruikt, of je de service niet kunt starten.
61 | IPv6-ondersteuning
62 | Logcat
63 |
64 | - Standaard geen apps omzeilen
65 | - Standaard alle apps omzeilen
66 | - Standaard systeemapps omzeilen
67 |
68 |
69 |
--------------------------------------------------------------------------------
/resources/src/main/res/values-zh/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 导出
4 | 关于
5 | 导入
6 | Host广告屏蔽和自定义DNS服务器
7 | 自定义DNS服务器
8 | DNS
9 | 启动/停止
10 | 行为
11 | 位置 (URL 或 Host)
12 | 标题
13 | 拒绝
14 | 允许
15 | 忽略
16 |
17 | - @string/deny
18 | - @string/allow
19 | - @string/ignore
20 |
21 | 无法写入设置: %s
22 | 重新连接中
23 | 正在运行
24 | 正在启动
25 | 已停止
26 | 正在停止
27 | 等待网络
28 | 更新Hosts文件
29 | 载入初始化设置
30 | 重新连接错误
31 | 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.
32 | 简体中文:Kevin Jiang
33 | 删除
34 | 启动
35 | 停止
36 | 修改 DNS 服务器
37 | 版本: %s
38 | 每日更新
39 | 当使用不计费的网络且处于空闲和充电状态时会触发自动更新。
40 | 否
41 | 是
42 | 启用你的自定义DNS服务器
43 | 无法找到文件
44 | URL 无效: %1$s
45 | IPv6 支持
46 | 如果你的网络未启用 IPv6 或你无法启动服务时禁用此选项。
47 | DNS 服务器的 IP 地址 ( IPv4 或 IPv6)
48 | 暂停
49 | 拒绝访问
50 | 无法使用所选择的文件: 该文件拒绝被长时间访问
51 | 请求超时
52 | 启用
53 | 系统启动时继续
54 | 未知错误r: %1$s
55 | 更新未完成
56 | 某些 hosts 文件未被下载. 如果这些文件曾被下载, 则会继续使用旧版本.
57 | 监视连接
58 | 使用较长的间隔检查连接并在其停止工作时重连
59 | 程序
60 | 使用文件
61 |
62 |
--------------------------------------------------------------------------------
/resources/src/main/res/values/color.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFF8F8
4 |
5 |
--------------------------------------------------------------------------------
/resources/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 |
--------------------------------------------------------------------------------
/resources/src/main/res/values/theme.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
--------------------------------------------------------------------------------
/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | channel = "1.85.0"
3 | targets = [ "x86_64-linux-android", "i686-linux-android", "aarch64-linux-android", "armv7-linux-androideabi" ]
4 |
--------------------------------------------------------------------------------
/service/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/service/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | import com.android.build.gradle.tasks.MergeSourceSetFolders
10 | import com.nishtahir.CargoBuildTask
11 |
12 | plugins {
13 | alias(libs.plugins.android.library)
14 | alias(libs.plugins.kotlin.android)
15 | alias(libs.plugins.rust.android.gradle)
16 | alias(libs.plugins.kotlinx.atomicfu)
17 | alias(libs.plugins.ksp)
18 | alias(libs.plugins.hilt)
19 | }
20 |
21 | val libnet = "libnet"
22 |
23 | // Required for reproducible builds on F-Droid
24 | val remapCargo = listOf(
25 | "--config",
26 | "build.rustflags = [ '--remap-path-prefix=${System.getenv("CARGO_HOME")}=/rust/cargo' ]",
27 | )
28 |
29 | cargo {
30 | module = libnet
31 | libname = "net"
32 |
33 | targets = listOf("arm64", "arm", "x86_64")
34 |
35 | pythonCommand = "python3"
36 |
37 | val isDebug = gradle.startParameter.taskNames.any {
38 | it.lowercase().contains("debug")
39 | }
40 | if (!isDebug) {
41 | profile = "release"
42 | }
43 | }
44 |
45 | val uniffiBindgen = tasks.register("uniffiBindgen") {
46 | val s = File.separatorChar
47 | workingDir = file("${projectDir}${s}$libnet")
48 | commandLine(
49 | "cargo",
50 | "run",
51 | "--bin",
52 | "uniffi-bindgen",
53 | "generate",
54 | "--library",
55 | "${projectDir}${s}build${s}rustJniLibs${s}android${s}arm64-v8a${s}$libnet.so",
56 | "--language",
57 | "kotlin",
58 | "--out-dir",
59 | layout.buildDirectory.dir("generated${s}kotlin").get().asFile.path
60 | )
61 | }
62 |
63 | uniffiBindgen.configure {
64 | dependsOn.add(tasks.withType(CargoBuildTask::class.java))
65 | }
66 |
67 | project.afterEvaluate {
68 | tasks.withType(CargoBuildTask::class)
69 | .forEach { buildTask ->
70 | tasks.withType(MergeSourceSetFolders::class)
71 | .configureEach {
72 | inputs.dir(layout.buildDirectory.dir("rustJniLibs" + File.separatorChar + buildTask.toolchain!!.folder))
73 | dependsOn(buildTask)
74 | }
75 | }
76 | }
77 |
78 | tasks.preBuild.configure {
79 | dependsOn.add(tasks.withType(CargoBuildTask::class.java))
80 | dependsOn.add(uniffiBindgen)
81 | }
82 |
83 | tasks.getByName("clean") {
84 | doFirst {
85 | delete(layout.projectDirectory.dir(libnet + File.separatorChar + "target"))
86 | }
87 | }
88 |
89 | android {
90 | namespace = "dev.clombardo.dnsnet.service"
91 | compileSdk = libs.versions.compileSdk.get().toInt()
92 |
93 | defaultConfig {
94 | minSdk = libs.versions.minSdk.get().toInt()
95 |
96 | consumerProguardFiles("consumer-rules.pro")
97 |
98 | ndk {
99 | abiFilters += listOf("x86_64", "arm64-v8a", "armeabi-v7a")
100 | }
101 |
102 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
103 | }
104 |
105 | ndkVersion = "28.0.13004108"
106 |
107 | sourceSets {
108 | getByName("main") {
109 | java.srcDir("build/generated/kotlin")
110 | jniLibs.srcDir("build/rustJniLibs")
111 | }
112 | }
113 |
114 | buildTypes {
115 | create("benchmark")
116 | }
117 | }
118 |
119 | kotlin {
120 | jvmToolchain(libs.versions.java.get().toInt())
121 | }
122 |
123 | dependencies {
124 | implementation(libs.androidx.work.runtime.ktx)
125 |
126 | implementation(libs.atomicfu)
127 |
128 | implementation(libs.androidx.core.ktx)
129 |
130 | implementation(libs.jna) {
131 | artifact {
132 | type = "aar"
133 | }
134 | }
135 |
136 | implementation(libs.hilt)
137 | implementation(libs.androidx.hilt.work)
138 | ksp(libs.hilt.compiler)
139 | ksp(libs.hilt.extensions.compiler)
140 |
141 | testImplementation(libs.junit)
142 | testImplementation(libs.androidx.test.core)
143 |
144 | androidTestImplementation(libs.androidx.test.core)
145 | androidTestImplementation(libs.androidx.test.runner)
146 | androidTestImplementation(libs.androidx.test.rules)
147 |
148 | implementation(project(":log"))
149 | implementation(project(":file"))
150 | implementation(project(":ui-common"))
151 | implementation(project(":resources"))
152 | implementation(project(":settings"))
153 | implementation(project(":blocklogger"))
154 | implementation(project(":notification"))
155 | }
156 |
--------------------------------------------------------------------------------
/service/consumer-rules.pro:
--------------------------------------------------------------------------------
1 | -keep class com.sun.jna.** { *; }
2 | -keepclassmembers class * extends com.sun.jna.* { public *; }
3 |
4 | -keep class uniffi.net.*
5 |
--------------------------------------------------------------------------------
/service/libnet/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 |
--------------------------------------------------------------------------------
/service/libnet/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "net"
3 | version = "1.0.0"
4 | edition = "2024"
5 |
6 | [lib]
7 | crate-type = ["cdylib"]
8 | name = "net"
9 |
10 | [[bin]]
11 | name = "uniffi-bindgen"
12 | path = "uniffi-bindgen.rs"
13 |
14 | [dependencies]
15 | base64 = "0.22.1"
16 | etherparse = "0.17.0"
17 | getrandom = "0.3.2"
18 | libc = "0.2.172"
19 | log = "0.4.27"
20 | mio = { version="1.0.3", features=["net","os-poll","os-ext"] }
21 | quiche = "0.23.7"
22 | simple-dns = "0.10.1"
23 | thiserror = "2.0.12"
24 | uniffi = { version = "0.29.1", features = ["cli"] }
25 | url = "2.5.4"
26 |
27 | [target.'cfg(target_os = "android")'.dependencies]
28 | android_logger = "0.14.1"
29 |
30 | [profile.release]
31 | lto = "fat"
32 |
--------------------------------------------------------------------------------
/service/libnet/src/backend/mod.rs:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | pub mod doh3;
10 | pub mod standard;
11 |
12 | use std::time::Duration;
13 |
14 | use mio::{Poll, event::Source};
15 |
16 | use crate::{Vpn, VpnCallback};
17 |
18 | #[derive(Debug)]
19 | pub enum DnsBackendError {
20 | SocketFailure,
21 | InvalidAddress,
22 | RandomGenerationFailure,
23 | }
24 |
25 | pub trait DnsBackend {
26 | /// Returns the max number of events that the events object will be initialized with.
27 | fn get_max_events_count(&self) -> usize;
28 |
29 | fn get_poll_timeout(&self) -> Option;
30 |
31 | /// Register sources with the poller.
32 | /// Returns the number of sources that were registered.
33 | /// You MUST NOT register sources that have a token value of [usize::MAX] or [usize::MAX] - 1.
34 | fn register_sources(&mut self, poll: &mut Poll) -> usize;
35 |
36 | fn forward_packet(
37 | &mut self,
38 | android_vpn_service: &Box,
39 | packet: &[u8],
40 | request_packet: &[u8],
41 | destination_address: Vec,
42 | destination_port: u16,
43 | ) -> Result<(), DnsBackendError>;
44 |
45 | /// Process all events from the poller and send any processed packets to the [DnsPacketProxy].
46 | /// Return a [Source] if it should be removed from the poller and [None] if it should be kept.
47 | fn process_events(
48 | &mut self,
49 | ad_vpn: &mut Vpn,
50 | events: Vec<&mio::event::Event>,
51 | ) -> Result>, DnsBackendError>;
52 | }
53 |
--------------------------------------------------------------------------------
/service/libnet/src/lib.rs:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | mod backend;
10 | mod database;
11 | mod packet;
12 | mod proxy;
13 | mod validation;
14 | mod vpn;
15 |
16 | use std::{
17 | net::{Ipv6Addr, SocketAddr, SocketAddrV6},
18 | str::FromStr,
19 | sync::Arc,
20 | time::{Duration, SystemTime, UNIX_EPOCH},
21 | };
22 |
23 | use android_logger::Config;
24 | use database::RuleDatabase;
25 | use log::LevelFilter;
26 | use mio::net::UdpSocket;
27 | use vpn::{Vpn, VpnConfigurationResult, VpnController, VpnError, VpnResult};
28 |
29 | #[macro_use]
30 | extern crate log;
31 | extern crate android_logger;
32 |
33 | uniffi::setup_scaffolding!();
34 |
35 | /// Initializes the logger for the Rust side of the VPN
36 | ///
37 | /// This should be called before any other Rust functions in the Kotlin code
38 | #[uniffi::export]
39 | pub fn rust_init(debug: bool) {
40 | android_logger::init_once(
41 | Config::default()
42 | .with_max_level(if debug {
43 | LevelFilter::Trace
44 | } else {
45 | LevelFilter::Info
46 | }) // limit log level
47 | .with_tag("DNSNet Native"), // logs will show under mytag tag
48 | );
49 | }
50 |
51 | /// Entrypoint for starting the VPN from Kotlin
52 | ///
53 | /// Runs the main loop for the service based on the descriptor given
54 | /// by the Android system.
55 | #[uniffi::export]
56 | pub fn run_vpn_native(
57 | ad_vpn_callback: Box,
58 | block_logger_callback: Option>,
59 | vpn_controller: Arc,
60 | rule_database: Arc,
61 | ) -> Result {
62 | let mut vpn = Vpn::new(vpn_controller);
63 | let result = vpn.run(ad_vpn_callback, block_logger_callback, rule_database);
64 | info!("run_vpn_native: Stopped");
65 | return result;
66 | }
67 |
68 | #[uniffi::export]
69 | pub fn network_has_ipv6_support() -> bool {
70 | let socket = match UdpSocket::bind(SocketAddr::new(
71 | std::net::IpAddr::V6(Ipv6Addr::UNSPECIFIED),
72 | 0,
73 | )) {
74 | Ok(value) => value,
75 | Err(error) => {
76 | error!("has_ipv6_support: Failed to create socket! - {:?}", error);
77 | return false;
78 | }
79 | };
80 |
81 | let target_socket_address = SocketAddr::V6(SocketAddrV6::new(
82 | Ipv6Addr::from_str("2001:2::").unwrap(),
83 | 53,
84 | 0,
85 | 0,
86 | ));
87 | if let Err(error) = socket.send_to(&mut vec![1; 1], target_socket_address) {
88 | debug!("has_ipv6_support: Error during IPv6 test - {:?}", error);
89 | return false;
90 | }
91 |
92 | return true;
93 | }
94 |
95 | /// Convenience function to get the [Duration] since the Unix epoch
96 | fn get_epoch() -> Duration {
97 | SystemTime::now().duration_since(UNIX_EPOCH).unwrap()
98 | }
99 |
100 | /// Callback interface to be implemented by a Kotlin class and then passed into the main loop
101 | #[uniffi::export(callback_interface)]
102 | pub trait VpnCallback: Send + Sync {
103 | fn configure(&self, vpn_controller: Arc) -> VpnConfigurationResult;
104 |
105 | fn protect_raw_socket_fd(&self, socket_fd: i32) -> bool;
106 |
107 | fn update_status(&self, native_status: i32);
108 | }
109 |
110 | /// Callback interface for accessing our filter files from the Android system
111 | #[uniffi::export(callback_interface)]
112 | pub trait AndroidFileHelper {
113 | fn get_filter_file_fd(&self, path: String) -> Option;
114 | }
115 |
116 | /// Callback interface for logging connections that we've blocked for the block logger
117 | #[uniffi::export(callback_interface)]
118 | pub trait BlockLoggerCallback: Send + Sync {
119 | fn log(&self, connection_name: String, allowed: bool);
120 | }
121 |
--------------------------------------------------------------------------------
/service/libnet/uniffi-bindgen.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | uniffi::uniffi_bindgen_main()
3 | }
4 |
--------------------------------------------------------------------------------
/service/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
19 |
20 |
21 |
22 |
24 |
25 |
26 |
27 |
32 |
33 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/service/src/main/kotlin/dev/clombardo/dnsnet/service/FilterUtil.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.service
10 |
11 | import android.content.Context
12 | import dev.clombardo.dnsnet.file.FileHelper
13 | import dev.clombardo.dnsnet.log.logInfo
14 | import dev.clombardo.dnsnet.settings.ConfigurationManager
15 | import dev.clombardo.dnsnet.settings.Filter
16 | import dev.clombardo.dnsnet.settings.FilterState
17 | import uniffi.net.NativeFilter
18 | import uniffi.net.NativeFilterState
19 | import java.io.IOException
20 |
21 | object FilterUtil {
22 | /**
23 | * Check if all configured filter files exist.
24 | *
25 | * @return true if all filter files exist or no filter files were configured.
26 | */
27 | fun areFilterFilesExistent(context: Context, configuration: ConfigurationManager): Boolean {
28 | return configuration.read {
29 | for (item in this.filters.files) {
30 | if (item.state != FilterState.IGNORE) {
31 | try {
32 | val reader =
33 | FileHelper.openPath(context, item.data) ?: return@read false
34 | reader.close()
35 | } catch (e: IOException) {
36 | logInfo("areFilterFilesExistent: Failed to open file {$item}", e)
37 | return@read false
38 | }
39 | }
40 | }
41 | return@read true
42 | }
43 | }
44 | }
45 |
46 | fun FilterState.toNative(): NativeFilterState =
47 | try {
48 | NativeFilterState.entries[ordinal]
49 | } catch (e: IndexOutOfBoundsException) {
50 | NativeFilterState.IGNORE
51 | }
52 |
53 | fun Filter.toNative(): NativeFilter = NativeFilter(title, data, state.toNative())
54 |
--------------------------------------------------------------------------------
/service/src/main/kotlin/dev/clombardo/dnsnet/service/NativeBlockLoggerWrapper.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.service
10 |
11 | import dev.clombardo.dnsnet.blocklogger.BlockLogger
12 | import uniffi.net.BlockLoggerCallback
13 |
14 | class NativeBlockLoggerWrapper(private val logger: BlockLogger): BlockLoggerCallback {
15 | override fun log(connectionName: String, allowed: Boolean) =
16 | logger.newConnection(connectionName, allowed)
17 | }
18 |
--------------------------------------------------------------------------------
/service/src/main/kotlin/dev/clombardo/dnsnet/service/NativeFileHelperWrapper.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.service
10 |
11 | import android.content.Context
12 | import dev.clombardo.dnsnet.file.FileHelper
13 | import uniffi.net.AndroidFileHelper
14 |
15 | class NativeFileHelperWrapper(private val context: Context) : AndroidFileHelper {
16 | override fun getFilterFileFd(path: String): Int? =
17 | FileHelper.getDetachedReadOnlyFd(context, path)
18 | }
19 |
--------------------------------------------------------------------------------
/service/src/main/kotlin/dev/clombardo/dnsnet/service/db/RuleDatabaseManager.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.service.db
10 |
11 | import android.content.Context
12 | import dev.clombardo.dnsnet.log.logInfo
13 | import dev.clombardo.dnsnet.log.logWarning
14 | import dev.clombardo.dnsnet.service.NativeFileHelperWrapper
15 | import dev.clombardo.dnsnet.service.toNative
16 | import dev.clombardo.dnsnet.settings.ConfigurationManager
17 | import kotlinx.atomicfu.atomic
18 | import kotlinx.coroutines.CoroutineScope
19 | import kotlinx.coroutines.Dispatchers
20 | import kotlinx.coroutines.launch
21 | import kotlinx.coroutines.sync.Semaphore
22 | import kotlinx.coroutines.withContext
23 | import uniffi.net.RuleDatabase
24 | import uniffi.net.RuleDatabaseController
25 | import uniffi.net.RuleDatabaseException
26 |
27 | class RuleDatabaseManager(
28 | private val context: Context,
29 | private val configuration: ConfigurationManager,
30 | ) {
31 | private val reloadLock = Semaphore(1)
32 | private val pendingReloadLock = Semaphore(1)
33 | private var destroyed by atomic(false)
34 |
35 | private val ruleDatabaseController = RuleDatabaseController()
36 | val ruleDatabase = RuleDatabase(ruleDatabaseController)
37 |
38 | private suspend fun initialize() = withContext(Dispatchers.IO) {
39 | try {
40 | ruleDatabase.initialize(
41 | androidFileHelper = NativeFileHelperWrapper(context),
42 | filterFiles = configuration.read { this.filters.files.map { it.toNative() } },
43 | singleFilters = configuration.read { filters.singleFilters.map { it.toNative() } },
44 | )
45 | } catch (e: RuleDatabaseException) {
46 | when (e) {
47 | is RuleDatabaseException.Interrupted -> logInfo("Interrupted", e)
48 | else -> throw IllegalStateException("Failed to initialize rule database", e)
49 | }
50 | }
51 | }
52 |
53 | fun reload() {
54 | logInfo("Reloading")
55 | if (!pendingReloadLock.tryAcquire()) {
56 | logInfo("Reload already pending")
57 | return
58 | }
59 |
60 | CoroutineScope(Dispatchers.IO).launch {
61 | reloadLock.acquire()
62 | pendingReloadLock.release()
63 |
64 | if (destroyed) {
65 | logWarning("Tried to initialize destroyed database")
66 | reloadLock.release()
67 | return@launch
68 | }
69 |
70 | if (ruleDatabaseController.isInitialized()) {
71 | ruleDatabase.waitOnInit()
72 | }
73 |
74 | logInfo("Initializing after wait")
75 | try {
76 | initialize()
77 | } catch (e: Exception) {
78 | throw e
79 | } finally {
80 | reloadLock.release()
81 | }
82 | }
83 | }
84 |
85 | fun waitOnInit() = ruleDatabase.waitOnInit()
86 |
87 | fun setShouldStop(shouldStop: Boolean) = ruleDatabaseController.setShouldStop(shouldStop)
88 |
89 | fun destroy() {
90 | destroyed = true
91 | waitOnInit()
92 | ruleDatabase.destroy()
93 | ruleDatabaseController.destroy()
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/service/src/main/kotlin/dev/clombardo/dnsnet/service/vpn/NetworkUtil.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.service.vpn
10 |
11 | import android.net.NetworkCapabilities
12 | import android.os.Build
13 | import android.os.ext.SdkExtensions
14 |
15 | private val TRANSPORT_NAMES: Array = arrayOf(
16 | "CELLULAR",
17 | "WIFI",
18 | "BLUETOOTH",
19 | "ETHERNET",
20 | "VPN",
21 | "WIFI_AWARE",
22 | "LOWPAN",
23 | "TEST",
24 | "USB",
25 | "THREAD",
26 | "SATELLITE",
27 | )
28 |
29 | data class NetworkDetails(
30 | var networkId: Int,
31 | var transports: IntArray?,
32 | ) {
33 | override fun toString(): String {
34 | val builder = StringBuilder()
35 | builder.append("NetworkDetails { networkId: $networkId, ")
36 | if (transports != null) {
37 | builder.append("transports: ")
38 | transports!!.forEach {
39 | builder.append("${TRANSPORT_NAMES[it]}, ")
40 | }
41 | }
42 | builder.append("}")
43 | return builder.toString()
44 | }
45 |
46 | override fun equals(other: Any?): Boolean {
47 | if (this === other) return true
48 | if (javaClass != other?.javaClass) return false
49 |
50 | other as NetworkDetails
51 |
52 | if (networkId != other.networkId) return false
53 | if (transports != null) {
54 | if (other.transports == null) return false
55 | if (!transports.contentEquals(other.transports)) return false
56 | } else if (other.transports != null) return false
57 |
58 | return true
59 | }
60 |
61 | override fun hashCode(): Int {
62 | var result = networkId.hashCode()
63 | result = 31 * result + (transports?.contentHashCode() ?: 0)
64 | return result
65 | }
66 | }
67 |
68 | fun NetworkCapabilities.getTransportTypes(): IntArray {
69 | val types = mutableListOf()
70 | if (hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
71 | types.add(NetworkCapabilities.TRANSPORT_CELLULAR)
72 | }
73 | if (hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
74 | types.add(NetworkCapabilities.TRANSPORT_WIFI)
75 | }
76 | if (hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH)) {
77 | types.add(NetworkCapabilities.TRANSPORT_BLUETOOTH)
78 | }
79 | if (hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
80 | types.add(NetworkCapabilities.TRANSPORT_ETHERNET)
81 | }
82 | if (hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
83 | types.add(NetworkCapabilities.TRANSPORT_VPN)
84 | }
85 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
86 | if (hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)) {
87 | types.add(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
88 | }
89 | }
90 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
91 | if (hasTransport(NetworkCapabilities.TRANSPORT_LOWPAN)) {
92 | types.add(NetworkCapabilities.TRANSPORT_LOWPAN)
93 | }
94 | }
95 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
96 | if (hasTransport(NetworkCapabilities.TRANSPORT_USB)) {
97 | types.add(NetworkCapabilities.TRANSPORT_USB)
98 | }
99 | }
100 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && SdkExtensions.getExtensionVersion(
101 | Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 7) {
102 | if (hasTransport(NetworkCapabilities.TRANSPORT_THREAD)) {
103 | types.add(NetworkCapabilities.TRANSPORT_THREAD)
104 | }
105 | }
106 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && SdkExtensions.getExtensionVersion(
107 | Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 12) {
108 | if (hasTransport(NetworkCapabilities.TRANSPORT_SATELLITE)) {
109 | types.add(NetworkCapabilities.TRANSPORT_SATELLITE)
110 | }
111 | }
112 | return types.toIntArray()
113 | }
114 |
--------------------------------------------------------------------------------
/service/src/main/kotlin/dev/clombardo/dnsnet/service/vpn/VpnExceptions.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, version 3.
6 | *
7 | * Contributions shall also be provided under any later versions of the
8 | * GPL.
9 | */
10 |
11 | package dev.clombardo.dnsnet.service.vpn
12 |
13 | class NoNetworkException : Exception {
14 | constructor(s: String?) : super(s)
15 | constructor(s: String?, t: Throwable?) : super(s, t)
16 | }
17 |
--------------------------------------------------------------------------------
/service/src/main/kotlin/dev/clombardo/dnsnet/service/vpn/VpnNetworkCallback.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, version 3.
6 | *
7 | * Contributions shall also be provided under any later versions of the
8 | * GPL.
9 | */
10 |
11 | package dev.clombardo.dnsnet.service.vpn
12 |
13 | import android.net.ConnectivityManager.NetworkCallback
14 | import android.net.Network
15 | import android.net.NetworkCapabilities
16 | import dev.clombardo.dnsnet.log.logDebug
17 | import dev.clombardo.dnsnet.service.NetworkState
18 |
19 | class VpnNetworkCallback(
20 | private val networkState: NetworkState,
21 | private val onDefaultNetworkChanged: (NetworkDetails?) -> Unit
22 | ) : NetworkCallback() {
23 | override fun onCapabilitiesChanged(
24 | network: Network,
25 | networkCapabilities: NetworkCapabilities
26 | ) {
27 | super.onCapabilitiesChanged(network, networkCapabilities)
28 | logDebug("onCapabilitiesChanged")
29 | val networkId = network.toString()
30 | val networkDetails = networkState.getConnectedNetwork(networkId)
31 | if (networkDetails == null) {
32 | val newNetwork = NetworkDetails(
33 | networkId = networkId.toInt(),
34 | transports = networkCapabilities.getTransportTypes(),
35 | )
36 | onDefaultNetworkChanged(newNetwork)
37 | } else {
38 | onDefaultNetworkChanged(
39 | networkDetails.copy(
40 | networkId = networkId.toInt(),
41 | transports = networkCapabilities.getTransportTypes(),
42 | )
43 | )
44 | }
45 | }
46 |
47 | override fun onLost(network: Network) {
48 | super.onLost(network)
49 | logDebug("onLost")
50 | val networkString = network.toString()
51 | val lostNetwork = networkState.getConnectedNetwork(networkString)
52 | if (lostNetwork != null) {
53 | val defaultNetwork = networkState.getDefaultNetwork()
54 | if (defaultNetwork != null && lostNetwork.networkId == defaultNetwork.networkId) {
55 | onDefaultNetworkChanged(null)
56 | }
57 | networkState.removeNetwork(lostNetwork)
58 | }
59 | logDebug(networkState.toString())
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/service/src/test/kotlin/dev/clombardo/dnsnet/service/NetworkStateTest.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.service
10 |
11 | import android.net.NetworkCapabilities
12 | import dev.clombardo.dnsnet.service.vpn.NetworkDetails
13 | import dev.clombardo.dnsnet.service.vpn.VpnStatus
14 | import org.junit.Assert.assertFalse
15 | import org.junit.Assert.assertTrue
16 | import org.junit.Test
17 |
18 | class NetworkStateTest {
19 | private val plainCellularNetwork = NetworkDetails(
20 | networkId = 1,
21 | transports = arrayOf(NetworkCapabilities.TRANSPORT_CELLULAR).toIntArray()
22 | )
23 |
24 | private val vpnCellularNetwork = NetworkDetails(
25 | networkId = 2,
26 | transports = arrayOf(
27 | NetworkCapabilities.TRANSPORT_CELLULAR,
28 | NetworkCapabilities.TRANSPORT_VPN
29 | ).toIntArray()
30 | )
31 |
32 | @Test
33 | fun networkState_switching_test() {
34 | val networkState = NetworkState()
35 |
36 | // No networks to one cellular network
37 | assertFalse(networkState.shouldReconnect(plainCellularNetwork, VpnStatus.RUNNING))
38 | networkState.setDefaultNetwork(plainCellularNetwork)
39 |
40 | // Cellular network to VPN cellular network
41 | assertFalse(networkState.shouldReconnect(vpnCellularNetwork, VpnStatus.RUNNING))
42 | networkState.setDefaultNetwork(vpnCellularNetwork)
43 |
44 | // VPN cellular network to cellular network
45 | networkState.removeNetwork(plainCellularNetwork)
46 | assertTrue(networkState.shouldReconnect(plainCellularNetwork, VpnStatus.RUNNING))
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | pluginManagement {
10 | repositories {
11 | google()
12 | mavenCentral()
13 | maven {
14 | url = uri("https://plugins.gradle.org/m2/")
15 | }
16 | }
17 | }
18 |
19 | dependencyResolutionManagement {
20 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
21 | repositories {
22 | google()
23 | mavenCentral()
24 | }
25 | }
26 |
27 | include(":app")
28 | include(":baselineprofile")
29 | include(":ui-common")
30 | include(":ui-app")
31 | include(":settings")
32 | include(":log")
33 | include(":file")
34 | include(":resources")
35 | include(":service")
36 | include(":blocklogger")
37 | include(":notification")
38 |
--------------------------------------------------------------------------------
/settings/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | plugins {
10 | alias(libs.plugins.android.library)
11 | alias(libs.plugins.kotlin.android)
12 | alias(libs.plugins.kotlin.parcelize)
13 | alias(libs.plugins.kotlin.serialization)
14 | alias(libs.plugins.kotlinx.atomicfu)
15 | alias(libs.plugins.ksp)
16 | alias(libs.plugins.hilt)
17 | }
18 |
19 | android {
20 | namespace = "dev.clombardo.dnsnet.settings"
21 | compileSdk = libs.versions.compileSdk.get().toInt()
22 |
23 | defaultConfig {
24 | minSdk = libs.versions.minSdk.get().toInt()
25 |
26 | consumerProguardFiles("consumer-rules.pro")
27 | }
28 |
29 | buildTypes {
30 | create("benchmark")
31 | }
32 | }
33 |
34 | kotlin {
35 | jvmToolchain(libs.versions.java.get().toInt())
36 | }
37 |
38 | dependencies {
39 | implementation(libs.androidx.preference.ktx)
40 |
41 | implementation(libs.kotlinx.serialization.json)
42 |
43 | implementation(libs.atomicfu)
44 |
45 | implementation(libs.hilt)
46 | ksp(libs.hilt.compiler)
47 |
48 | implementation(project(":log"))
49 | implementation(project(":file"))
50 | implementation(project(":resources"))
51 | }
52 |
--------------------------------------------------------------------------------
/settings/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/settings/consumer-rules.pro
--------------------------------------------------------------------------------
/settings/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/settings/src/main/kotlin/dev/clombardo/dnsnet/settings/Preferences.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.settings
10 |
11 | import android.content.Context
12 | import android.content.SharedPreferences
13 | import androidx.preference.PreferenceManager
14 | import kotlin.reflect.KProperty
15 | import androidx.core.content.edit
16 | import dagger.Module
17 | import dagger.Provides
18 | import dagger.hilt.InstallIn
19 | import dagger.hilt.android.qualifiers.ApplicationContext
20 | import dagger.hilt.components.SingletonComponent
21 | import javax.inject.Singleton
22 |
23 | @Module
24 | @InstallIn(SingletonComponent::class)
25 | class PreferencesModule {
26 | @Provides
27 | @Singleton
28 | fun providePreferences(@ApplicationContext context: Context): Preferences {
29 | return Preferences(PreferenceManager.getDefaultSharedPreferences(context))
30 | }
31 | }
32 |
33 | class Preferences(val sharedPreferences: SharedPreferences) {
34 | /**
35 | * Old preference that is no longer used to see if the user interacted with the notification
36 | * permission dialog. Now it's just used to make sure that we don't show existing users the
37 | * setup screen.
38 | */
39 | var NotificationPermissionActedUpon by BooleanPreference(
40 | preferences = sharedPreferences,
41 | key = "NotificationPermissionDenied",
42 | defaultValue = false,
43 | )
44 |
45 | /**
46 | * Tracks whether the VPN is running and is meant to tell the service if it was running when
47 | * the device was last on. On the next device boot, this is checked and if it is true and
48 | * the user enabled "Resume on system start-up," the service is started.
49 | */
50 | var VpnIsActive by BooleanPreference(
51 | preferences = sharedPreferences,
52 | key = "isActive",
53 | defaultValue = false,
54 | )
55 |
56 | var SetupComplete by BooleanPreference(
57 | preferences = sharedPreferences,
58 | key = "setupComplete",
59 | defaultValue = false,
60 | )
61 |
62 | /**
63 | * Single fire preference to tell users to select a preset when they upgrade from
64 | * config v1.1 to v1.2 if they have no block lists.
65 | */
66 | var ShouldShowPresetsWhenNoBlockLists by BooleanPreference(
67 | preferences = sharedPreferences,
68 | key = "shouldShowPresetsWhenNoBlockLists",
69 | defaultValue = false,
70 | )
71 | }
72 |
73 | interface Preference {
74 | val preferences: SharedPreferences
75 | val key: String
76 | val defaultValue: T
77 |
78 | operator fun getValue(thisRef: Any?, property: KProperty<*>): T
79 | operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T)
80 | }
81 |
82 | class BooleanPreference(
83 | override val preferences: SharedPreferences,
84 | override val key: String,
85 | override val defaultValue: Boolean,
86 | ) : Preference {
87 | override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean {
88 | return preferences.getBoolean(key, defaultValue)
89 | }
90 |
91 | override fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) {
92 | preferences.edit { putBoolean(key, value) }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/ui-app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/ui-app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | plugins {
10 | alias(libs.plugins.android.library)
11 | alias(libs.plugins.kotlin.android)
12 | alias(libs.plugins.kotlin.compose)
13 | alias(libs.plugins.kotlin.parcelize)
14 | alias(libs.plugins.kotlin.serialization)
15 | alias(libs.plugins.kotlinx.atomicfu)
16 | alias(libs.plugins.aboutLibraries)
17 | alias(libs.plugins.ksp)
18 | alias(libs.plugins.hilt)
19 | }
20 |
21 | android {
22 | namespace = "dev.clombardo.dnsnet.ui.app"
23 | compileSdk = libs.versions.compileSdk.get().toInt()
24 |
25 | defaultConfig {
26 | minSdk = libs.versions.minSdk.get().toInt()
27 |
28 | consumerProguardFiles("consumer-rules.pro")
29 |
30 | val versionName: String by rootProject.extra
31 | buildConfigField(
32 | type = "String",
33 | name = "VERSION_NAME",
34 | value = "\"$versionName\"",
35 | )
36 | }
37 |
38 | buildTypes {
39 | create("benchmark")
40 | }
41 |
42 | buildFeatures {
43 | compose = true
44 | buildConfig = true
45 | }
46 | }
47 |
48 | kotlin {
49 | jvmToolchain(libs.versions.java.get().toInt())
50 | }
51 |
52 | dependencies {
53 | val composeBom = platform(libs.compose.bom)
54 | implementation(composeBom)
55 | debugImplementation(composeBom)
56 | androidTestImplementation(composeBom)
57 | implementation(libs.androidx.material3)
58 | implementation(libs.androidx.ui.tooling.preview)
59 | debugImplementation(libs.androidx.ui.tooling)
60 | implementation(libs.androidx.material.icons.core)
61 | implementation(libs.androidx.material.icons.extended)
62 |
63 | implementation(libs.androidx.navigation.compose)
64 |
65 | implementation(libs.accompanist.permissions)
66 |
67 | implementation(libs.kotlinx.serialization.json)
68 |
69 | implementation(libs.string.similarity.kotlin)
70 |
71 | implementation(libs.coil.compose)
72 |
73 | implementation(libs.atomicfu)
74 |
75 | implementation(libs.hilt)
76 | implementation(libs.androidx.hilt.navigation.compose)
77 | ksp(libs.hilt.compiler)
78 |
79 | implementation(libs.aboutlibraries.core)
80 | implementation(libs.aboutlibraries.compose.core)
81 |
82 | implementation(project(":ui-common"))
83 | implementation(project(":settings"))
84 | implementation(project(":log"))
85 | implementation(project(":blocklogger"))
86 | }
87 |
88 | aboutLibraries {
89 | export {
90 | outputFile = File("src/main/res/raw/aboutlibraries.json")
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/ui-app/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/ui-app/consumer-rules.pro
--------------------------------------------------------------------------------
/ui-app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/ui-app/src/main/kotlin/dev/clombardo/dnsnet/ui/app/Insets.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.app
10 |
11 | import androidx.compose.foundation.layout.WindowInsets
12 | import androidx.compose.foundation.layout.WindowInsetsSides
13 | import androidx.compose.foundation.layout.displayCutout
14 | import androidx.compose.foundation.layout.only
15 | import androidx.compose.foundation.layout.systemBars
16 | import androidx.compose.foundation.layout.union
17 | import androidx.compose.runtime.Composable
18 |
19 | val topAppBarInsets: WindowInsets
20 | @Composable
21 | get() = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)
22 | .union(WindowInsets.systemBars.only(WindowInsetsSides.Top))
23 | .union(WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal))
24 |
--------------------------------------------------------------------------------
/ui-app/src/main/kotlin/dev/clombardo/dnsnet/ui/app/NavUtil.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.app
10 |
11 | import android.annotation.SuppressLint
12 | import androidx.navigation.NavController
13 | import androidx.navigation.NavBackStackEntry
14 | import androidx.navigation.NavDestination.Companion.hasRoute
15 |
16 | /**
17 | * Wrapper around [NavController.popBackStack] that prevents you from popping every item in the backstack.
18 | *
19 | * @param id ID from [NavBackStackEntry.id]
20 | */
21 | @SuppressLint("RestrictedApi")
22 | fun NavController.tryPopBackstack(id: String): Boolean {
23 | if (currentBackStack.value.size > 2) {
24 | if (currentBackStack.value.any { it.id == id }) {
25 | return popBackStack()
26 | }
27 | }
28 | return false
29 | }
30 |
31 | /**
32 | * Wrapper around [NavController.navigate] that navigates to a destination and removes all previous
33 | * backstack entries while saving state.
34 | *
35 | * @param route Typed route to navigate to
36 | */
37 | fun NavController.popNavigate(route: T) {
38 | navigate(route) {
39 | popUpTo(0) {
40 | saveState = true
41 | inclusive = true
42 | }
43 | launchSingleTop = true
44 | restoreState = true
45 | }
46 | }
47 |
48 | /**
49 | * Checks if the current backstack contains a typed route.
50 | *
51 | * @param T Type of route to search for
52 | */
53 | @SuppressLint("RestrictedApi")
54 | inline fun NavController.containsRoute(): Boolean =
55 | currentBackStack.value.any { it.destination.hasRoute() }
56 |
--------------------------------------------------------------------------------
/ui-app/src/main/kotlin/dev/clombardo/dnsnet/ui/app/coil/AppImage.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.app.coil
10 |
11 | import coil3.ImageLoader
12 | import coil3.asImage
13 | import coil3.decode.DataSource
14 | import coil3.fetch.FetchResult
15 | import coil3.fetch.Fetcher
16 | import coil3.fetch.ImageFetchResult
17 | import coil3.key.Keyer
18 | import coil3.request.Options
19 | import dev.clombardo.dnsnet.ui.app.model.AppData
20 |
21 | class AppImageKeyer : Keyer {
22 | override fun key(data: AppData, options: Options): String? = data.info.packageName
23 | }
24 |
25 | class AppImageFetcher(private val appData: AppData) : Fetcher {
26 | override suspend fun fetch(): FetchResult {
27 | val icon = appData.loadIcon()
28 | return ImageFetchResult(
29 | image = icon.asImage(),
30 | isSampled = true,
31 | dataSource = DataSource.DISK,
32 | )
33 | }
34 |
35 | class Factory : Fetcher.Factory {
36 | override fun create(data: AppData, options: Options, imageLoader: ImageLoader): Fetcher =
37 | AppImageFetcher(data)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/ui-app/src/main/kotlin/dev/clombardo/dnsnet/ui/app/model/AppData.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.app.model
10 |
11 | import android.content.pm.ApplicationInfo
12 | import android.content.pm.PackageManager
13 | import android.graphics.drawable.Drawable
14 | import java.lang.ref.WeakReference
15 |
16 | data class AppData(
17 | val packageManager: PackageManager,
18 | val info: ApplicationInfo,
19 | val label: String,
20 | var enabled: Boolean,
21 | val isSystem: Boolean,
22 | ) {
23 | private var weakIcon: WeakReference? = null
24 |
25 | fun loadIcon(): Drawable {
26 | var icon = weakIcon?.get()
27 | if (icon == null) {
28 | icon = info.loadIcon(packageManager)
29 | weakIcon = WeakReference(icon)
30 | }
31 | return icon!!
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/ui-app/src/main/kotlin/dev/clombardo/dnsnet/ui/app/state/AppListState.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.app.state
10 |
11 | import dev.clombardo.dnsnet.ui.app.R
12 | import dev.clombardo.dnsnet.ui.common.FilterMode
13 | import dev.clombardo.dnsnet.ui.common.ListFilter
14 | import dev.clombardo.dnsnet.ui.common.ListFilterType
15 | import dev.clombardo.dnsnet.ui.common.ListSort
16 | import dev.clombardo.dnsnet.ui.common.ListSortType
17 | import kotlinx.serialization.Serializable
18 |
19 | object AppListState {
20 | enum class SortType(override val labelRes: Int) : ListSortType {
21 | Alphabetical(R.string.alphabetical),
22 | }
23 |
24 | @Serializable
25 | data class Sort(
26 | override val selectedType: SortType = SortType.Alphabetical,
27 | override val ascending: Boolean = true,
28 | ) : ListSort()
29 |
30 | enum class FilterType(override val labelRes: Int) : ListFilterType {
31 | SystemApps(R.string.system_apps),
32 | }
33 |
34 | @Serializable
35 | data class Filter(
36 | override val filters: Map =
37 | mapOf(FilterType.SystemApps to FilterMode.Exclude)
38 | ) : ListFilter()
39 | }
40 |
--------------------------------------------------------------------------------
/ui-app/src/main/kotlin/dev/clombardo/dnsnet/ui/app/state/BlockLogListState.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.app.state
10 |
11 | import dev.clombardo.dnsnet.ui.app.R
12 | import dev.clombardo.dnsnet.ui.common.FilterMode
13 | import dev.clombardo.dnsnet.ui.common.ListFilter
14 | import dev.clombardo.dnsnet.ui.common.ListFilterType
15 | import dev.clombardo.dnsnet.ui.common.ListSort
16 | import dev.clombardo.dnsnet.ui.common.ListSortType
17 | import kotlinx.serialization.Serializable
18 |
19 | object BlockLogListState {
20 | enum class SortType(override val labelRes: Int) : ListSortType {
21 | Attempts(R.string.attempts),
22 | LastConnected(R.string.last_connected),
23 | Alphabetical(R.string.alphabetical),
24 | }
25 |
26 | @Serializable
27 | data class Sort(
28 | override val selectedType: SortType = SortType.Attempts,
29 | override val ascending: Boolean = true,
30 | ) : ListSort()
31 |
32 | enum class FilterType(override val labelRes: Int) : ListFilterType {
33 | Blocked(R.string.blocked),
34 | }
35 |
36 | @Serializable
37 | data class Filter(
38 | override val filters: Map = emptyMap()
39 | ) : ListFilter()
40 | }
41 |
--------------------------------------------------------------------------------
/ui-app/src/main/kotlin/dev/clombardo/dnsnet/ui/app/util/NumberFormatterCompat.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.app.util
10 |
11 | import android.icu.number.Notation
12 | import android.icu.number.NumberFormatter
13 | import android.icu.number.Precision
14 | import android.icu.text.CompactDecimalFormat
15 | import android.icu.util.ULocale
16 | import android.os.Build
17 |
18 | object NumberFormatterCompat {
19 | fun formatCompact(value: Long): String =
20 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
21 | NumberFormatter.with()
22 | .notation(Notation.compactShort())
23 | .precision(Precision.maxSignificantDigits(3))
24 | .locale(ULocale.getDefault())
25 | .format(value)
26 | .toString()
27 | } else {
28 | CompactDecimalFormat.getInstance().format(value)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/ui-app/src/main/kotlin/dev/clombardo/dnsnet/ui/app/viewmodel/AppListViewModel.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.app.viewmodel
10 |
11 | import androidx.compose.runtime.getValue
12 | import androidx.compose.runtime.mutableStateOf
13 | import androidx.compose.runtime.setValue
14 | import dagger.hilt.android.lifecycle.HiltViewModel
15 | import dev.clombardo.dnsnet.settings.Preferences
16 | import dev.clombardo.dnsnet.ui.app.model.AppData
17 | import dev.clombardo.dnsnet.ui.app.state.AppListState
18 | import dev.clombardo.dnsnet.ui.common.FilterMode
19 | import javax.inject.Inject
20 | import kotlin.collections.sortedBy
21 | import kotlin.collections.sortedByDescending
22 |
23 | @HiltViewModel
24 | class AppListViewModel @Inject constructor(
25 | override val preferences: Preferences
26 | ) : PersistableViewModel() {
27 | override val tag = "AppListViewModel"
28 |
29 | var searchValue by mutableStateOf("")
30 |
31 | var searchWidgetExpanded by mutableStateOf(false)
32 | var showModifyListSheet by mutableStateOf(false)
33 |
34 | var sort by mutableStateOf(
35 | getInitialPersistedValue(SORT_KEY, AppListState.Sort())
36 | )
37 |
38 | fun onSortClick(type: AppListState.SortType) {
39 | sort = if (sort.selectedType == type) {
40 | AppListState.Sort(
41 | selectedType = type,
42 | ascending = !sort.ascending,
43 | )
44 | } else {
45 | AppListState.Sort(
46 | selectedType = type,
47 | ascending = true,
48 | )
49 | }
50 | persistValue(SORT_KEY, sort)
51 | }
52 |
53 | var filter by mutableStateOf(
54 | getInitialPersistedValue(FILTER_KEY, AppListState.Filter())
55 | )
56 |
57 | fun onFilterClick(type: AppListState.FilterType) {
58 | val newFilters = filter.filters.toMutableMap()
59 | val currentState = filter.filters[type]
60 | when (currentState) {
61 | FilterMode.Include ->
62 | newFilters[type] = FilterMode.Exclude
63 |
64 | FilterMode.Exclude -> newFilters.remove(type)
65 | null -> newFilters[type] = FilterMode.Include
66 | }
67 | filter = AppListState.Filter(newFilters)
68 | persistValue(FILTER_KEY, filter)
69 | }
70 |
71 | fun getList(initialList: List): List {
72 | val sortedList = when (sort.selectedType) {
73 | AppListState.SortType.Alphabetical -> if (sort.ascending) {
74 | initialList.sortedBy { it.label }
75 | } else {
76 | initialList.sortedByDescending { it.label }
77 | }
78 | }
79 |
80 | val filteredList = sortedList.filter {
81 | var result = true
82 | filter.filters.forEach { (type, mode) ->
83 | when (type) {
84 | AppListState.FilterType.SystemApps -> {
85 | result = when (mode) {
86 | FilterMode.Include -> it.isSystem
87 | FilterMode.Exclude -> !it.isSystem
88 | }
89 | }
90 | }
91 | }
92 | result
93 | }
94 |
95 | return if (searchValue.isEmpty()) {
96 | filteredList
97 | } else {
98 | val adjustedSearchValue = searchValue.trim().lowercase()
99 | filteredList.mapNotNull {
100 | val similarity =
101 | cosineSimilarity.similarity(it.label.lowercase(), adjustedSearchValue)
102 | if (similarity > 0) {
103 | similarity to it
104 | } else {
105 | null
106 | }
107 | }.sortedByDescending {
108 | it.first
109 | }.map { it.second }
110 | }
111 | }
112 |
113 | companion object {
114 | private const val SORT_KEY = "sort"
115 | private const val FILTER_KEY = "filter"
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/ui-app/src/main/kotlin/dev/clombardo/dnsnet/ui/app/viewmodel/BlockLogListViewModel.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.app.viewmodel
10 |
11 | import androidx.compose.runtime.getValue
12 | import androidx.compose.runtime.mutableStateOf
13 | import androidx.compose.runtime.setValue
14 | import dagger.hilt.android.lifecycle.HiltViewModel
15 | import dev.clombardo.dnsnet.settings.Preferences
16 | import dev.clombardo.dnsnet.ui.app.LoggedConnectionState
17 | import dev.clombardo.dnsnet.ui.app.state.BlockLogListState
18 | import dev.clombardo.dnsnet.ui.common.FilterMode
19 | import javax.inject.Inject
20 | import kotlin.collections.component1
21 | import kotlin.collections.component2
22 | import kotlin.collections.set
23 |
24 | @HiltViewModel
25 | class BlockLogListViewModel @Inject constructor(
26 | override val preferences: Preferences
27 | ) : PersistableViewModel() {
28 | override val tag = "BlockLogListViewModel"
29 |
30 | var searchValue by mutableStateOf("")
31 |
32 | var sort by mutableStateOf(
33 | getInitialPersistedValue(SORT_KEY, BlockLogListState.Sort())
34 | )
35 |
36 | fun onSortClick(type: BlockLogListState.SortType) {
37 | sort = if (sort.selectedType == type) {
38 | BlockLogListState.Sort(
39 | selectedType = type,
40 | ascending = !sort.ascending,
41 | )
42 | } else {
43 | BlockLogListState.Sort(
44 | selectedType = type,
45 | ascending = true,
46 | )
47 | }
48 | persistValue(SORT_KEY, sort)
49 | }
50 |
51 | var filter by mutableStateOf(
52 | getInitialPersistedValue(FILTER_KEY, BlockLogListState.Filter())
53 | )
54 |
55 | fun onFilterClick(type: BlockLogListState.FilterType) {
56 | val newFilters = filter.filters.toMutableMap()
57 | val currentState = filter.filters[type]
58 | when (currentState) {
59 | FilterMode.Include ->
60 | newFilters[type] = FilterMode.Exclude
61 |
62 | FilterMode.Exclude -> newFilters.remove(type)
63 | null -> newFilters[type] = FilterMode.Include
64 | }
65 | filter = BlockLogListState.Filter(newFilters)
66 | persistValue(FILTER_KEY, filter)
67 | }
68 |
69 | fun getList(list: Collection): List {
70 | val sortedList = when (sort.selectedType) {
71 | BlockLogListState.SortType.Alphabetical -> if (sort.ascending) {
72 | list.sortedByDescending { it.hostname }
73 | } else {
74 | list.sortedBy { it.hostname }
75 | }
76 |
77 | BlockLogListState.SortType.LastConnected -> if (sort.ascending) {
78 | list.sortedByDescending { it.lastAttemptTime }
79 | } else {
80 | list.sortedBy { it.lastAttemptTime }
81 | }
82 |
83 | BlockLogListState.SortType.Attempts -> if (sort.ascending) {
84 | list.sortedByDescending { it.attempts }
85 | } else {
86 | list.sortedBy { it.attempts }
87 | }
88 | }
89 |
90 | val filteredList = sortedList.filter {
91 | var result = true
92 | filter.filters.forEach { (type, mode) ->
93 | when (type) {
94 | BlockLogListState.FilterType.Blocked -> {
95 | result = when (mode) {
96 | FilterMode.Include -> !it.allowed
97 | FilterMode.Exclude -> it.allowed
98 | }
99 | }
100 | }
101 | }
102 | result
103 | }
104 |
105 | return if (searchValue.isEmpty()) {
106 | filteredList
107 | } else {
108 | val adjustedSearchValue = searchValue.trim().lowercase()
109 | filteredList.mapNotNull {
110 | val similarity = cosineSimilarity.similarity(it.hostname, adjustedSearchValue)
111 | if (similarity > 0) {
112 | similarity to it
113 | } else {
114 | null
115 | }
116 | }.sortedByDescending {
117 | it.first
118 | }.map { it.second }
119 | }
120 | }
121 |
122 | companion object {
123 | private const val SORT_KEY = "sort"
124 | private const val FILTER_KEY = "filter"
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/ui-app/src/main/kotlin/dev/clombardo/dnsnet/ui/app/viewmodel/PersistableViewModel.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.app.viewmodel
10 |
11 | import androidx.core.content.edit
12 | import androidx.lifecycle.ViewModel
13 | import com.aallam.similarity.Cosine
14 | import dev.clombardo.dnsnet.settings.Preferences
15 | import kotlinx.serialization.json.Json
16 |
17 | abstract class PersistableViewModel : ViewModel() {
18 | abstract val preferences: Preferences
19 | abstract val tag: String
20 | protected val cosineSimilarity = Cosine()
21 |
22 | internal inline fun getInitialPersistedValue(key: String, defaultValue: T): T {
23 | val key = "$tag:$key"
24 | return if (preferences.sharedPreferences.contains(key)) {
25 | try {
26 | Json.decodeFromString(preferences.sharedPreferences.getString(key, "")!!)
27 | } catch (_: Exception) {
28 | defaultValue
29 | }
30 | } else {
31 | defaultValue
32 | }
33 | }
34 |
35 | internal inline fun persistValue(key: String, value: T) {
36 | try {
37 | preferences.sharedPreferences.edit { putString("$tag:$key", Json.encodeToString(value)) }
38 | } catch (_: Exception) {
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/ui-common/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/ui-common/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | plugins {
10 | alias(libs.plugins.android.library)
11 | alias(libs.plugins.kotlin.android)
12 | alias(libs.plugins.kotlin.serialization)
13 | alias(libs.plugins.kotlin.compose)
14 | }
15 |
16 | android {
17 | namespace = "dev.clombardo.dnsnet.ui.common"
18 | compileSdk = libs.versions.compileSdk.get().toInt()
19 |
20 | defaultConfig {
21 | minSdk = libs.versions.minSdk.get().toInt()
22 |
23 | consumerProguardFiles("consumer-rules.pro")
24 | }
25 |
26 | buildTypes {
27 | create("benchmark")
28 | }
29 |
30 | buildFeatures {
31 | compose = true
32 | }
33 | }
34 |
35 | kotlin {
36 | jvmToolchain(libs.versions.java.get().toInt())
37 | }
38 |
39 | dependencies {
40 | val composeBom = platform(libs.compose.bom)
41 | implementation(composeBom)
42 | debugImplementation(composeBom)
43 | androidTestImplementation(composeBom)
44 | implementation(libs.androidx.material3)
45 | implementation(libs.androidx.ui.tooling.preview)
46 | debugImplementation(libs.androidx.ui.tooling)
47 | implementation(libs.androidx.material.icons.core)
48 | implementation(libs.androidx.material.icons.extended)
49 | implementation(libs.androidx.graphics.shapes)
50 | implementation(libs.androidx.material3.adaptive.navigation.suite)
51 |
52 | implementation(libs.materialswitch)
53 |
54 | implementation(libs.kotlinx.serialization.json)
55 |
56 | implementation(project(":log"))
57 | implementation(project(":resources"))
58 | }
59 |
--------------------------------------------------------------------------------
/ui-common/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/ui-common/consumer-rules.pro
--------------------------------------------------------------------------------
/ui-common/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/ui-common/src/main/kotlin/dev/clombardo/dnsnet/ui/common/Dialog.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.common
10 |
11 | import androidx.compose.material3.AlertDialog
12 | import androidx.compose.material3.Text
13 | import androidx.compose.material3.TextButton
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.remember
16 | import androidx.compose.ui.Modifier
17 |
18 | data class DialogButton(
19 | val modifier: Modifier = Modifier,
20 | val text: String,
21 | val onClick: () -> Unit,
22 | )
23 |
24 | @Composable
25 | fun BasicDialog(
26 | modifier: Modifier = Modifier,
27 | title: String,
28 | text: String,
29 | primaryButton: DialogButton,
30 | secondaryButton: DialogButton? = null,
31 | tertiaryButton: DialogButton? = null,
32 | onDismissRequest: () -> Unit,
33 | ) {
34 | val primaryButtonState = remember { primaryButton }
35 | val secondaryButtonState = remember { secondaryButton }
36 | val tertiaryButtonState = remember { tertiaryButton }
37 |
38 | AlertDialog(
39 | modifier = modifier,
40 | onDismissRequest = onDismissRequest,
41 | confirmButton = {
42 | if (tertiaryButtonState != null) {
43 | TextButton(
44 | modifier = tertiaryButtonState.modifier,
45 | onClick = tertiaryButtonState.onClick,
46 | ) {
47 | Text(text = tertiaryButtonState.text)
48 | }
49 | }
50 | if (secondaryButtonState != null) {
51 | TextButton(
52 | modifier = secondaryButtonState.modifier,
53 | onClick = secondaryButtonState.onClick,
54 | ) {
55 | Text(text = secondaryButtonState.text)
56 | }
57 | }
58 | TextButton(
59 | modifier = primaryButtonState.modifier,
60 | onClick = primaryButtonState.onClick,
61 | ) {
62 | Text(text = primaryButtonState.text)
63 | }
64 | },
65 | title = { Text(text = title) },
66 | text = { Text(text = text) },
67 | )
68 | }
69 |
--------------------------------------------------------------------------------
/ui-common/src/main/kotlin/dev/clombardo/dnsnet/ui/common/FullSizeClickable.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.common
10 |
11 | import androidx.compose.foundation.clickable
12 | import androidx.compose.material3.minimumInteractiveComponentSize
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.semantics.Role
15 |
16 | /**
17 | * A [Modifier] that adds [minimumInteractiveComponentSize] and [clickable] without clipping the indication
18 | *
19 | * Intended to be used over [androidx.compose.material3.IconButton] in certain circumstances
20 | */
21 | fun Modifier.fullSizeClickable(
22 | enabled: Boolean = true,
23 | onClickLabel: String? = null,
24 | role: Role? = null,
25 | onClick: () -> Unit
26 | ) = this.then(
27 | Modifier
28 | .clickable(
29 | enabled = enabled,
30 | onClickLabel = onClickLabel,
31 | role = role,
32 | onClick = onClick,
33 | )
34 | .minimumInteractiveComponentSize()
35 | )
36 |
--------------------------------------------------------------------------------
/ui-common/src/main/kotlin/dev/clombardo/dnsnet/ui/common/LinkUtil.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.common
10 |
11 | import android.content.Context
12 | import android.net.Uri
13 | import android.widget.Toast
14 | import androidx.compose.ui.platform.UriHandler
15 | import dev.clombardo.dnsnet.log.logWarning
16 |
17 | /**
18 | * This prevents a rare crash where a user does not have a web browser installed to open a link.
19 | * This only happens when someone is messing around with root/custom roms but I'd prefer that they
20 | * get a friendly error message instead of crashing.
21 | */
22 | fun UriHandler.tryOpenUri(context: Context, uri: Uri) {
23 | try {
24 | openUri(uri.toString())
25 | } catch (e: Exception) {
26 | logWarning("Failed to open link: $uri", e)
27 | Toast.makeText(context, R.string.failed_to_open_link, Toast.LENGTH_SHORT).show()
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/ui-common/src/main/kotlin/dev/clombardo/dnsnet/ui/common/ListOptionItems.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.common
10 |
11 | import androidx.annotation.StringRes
12 | import androidx.compose.animation.core.animateFloatAsState
13 | import androidx.compose.foundation.clickable
14 | import androidx.compose.foundation.layout.BoxScope
15 | import androidx.compose.foundation.layout.padding
16 | import androidx.compose.foundation.selection.triStateToggleable
17 | import androidx.compose.material.icons.Icons
18 | import androidx.compose.material.icons.filled.ArrowUpward
19 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
20 | import androidx.compose.material3.Icon
21 | import androidx.compose.material3.MaterialTheme
22 | import androidx.compose.material3.TriStateCheckbox
23 | import androidx.compose.runtime.Composable
24 | import androidx.compose.runtime.getValue
25 | import androidx.compose.ui.Modifier
26 | import androidx.compose.ui.draw.rotate
27 | import androidx.compose.ui.res.stringResource
28 | import androidx.compose.ui.semantics.Role
29 | import androidx.compose.ui.state.ToggleableState
30 | import androidx.compose.ui.unit.dp
31 | import kotlinx.serialization.Serializable
32 |
33 | interface ListSortType {
34 | @get:StringRes
35 | val labelRes: Int
36 | }
37 |
38 | @Serializable
39 | abstract class ListSort {
40 | abstract val selectedType: ListSortType
41 | abstract val ascending: Boolean
42 | }
43 |
44 | interface ListFilterType {
45 | @get:StringRes
46 | val labelRes: Int
47 | }
48 |
49 | @Serializable
50 | abstract class ListFilter {
51 | abstract val filters: Map
52 | }
53 |
54 | @Composable
55 | fun ListOptionItem(
56 | modifier: Modifier = Modifier,
57 | text: String,
58 | endContent: @Composable BoxScope.() -> Unit,
59 | ) {
60 | ContentSetting(
61 | modifier = modifier.padding(horizontal = 16.dp),
62 | title = text,
63 | endContent = endContent
64 | )
65 | }
66 |
67 | @OptIn(ExperimentalMaterial3ExpressiveApi::class)
68 | @Composable
69 | fun SortItem(
70 | modifier: Modifier = Modifier,
71 | selected: Boolean,
72 | ascending: Boolean,
73 | label: String,
74 | onClick: () -> Unit,
75 | ) {
76 | ListOptionItem(
77 | modifier = modifier
78 | .clickable(
79 | role = Role.Button,
80 | onClick = onClick,
81 | ),
82 | text = label,
83 | ) {
84 | if (selected) {
85 | val animatedRotation by animateFloatAsState(
86 | targetValue = if (ascending) 0f else -180f,
87 | animationSpec = MaterialTheme.motionScheme.fastSpatialSpec(),
88 | label = "animatedRotation",
89 | )
90 | Icon(
91 | modifier = Modifier.rotate(animatedRotation),
92 | imageVector = Icons.Filled.ArrowUpward,
93 | contentDescription = if (ascending) {
94 | stringResource(R.string.ascending)
95 | } else {
96 | stringResource(R.string.descending)
97 | },
98 | )
99 | }
100 | }
101 | }
102 |
103 | enum class FilterMode {
104 | Include,
105 | Exclude,
106 | }
107 |
108 | @Composable
109 | fun FilterItem(
110 | modifier: Modifier = Modifier,
111 | label: String,
112 | mode: FilterMode?,
113 | onClick: () -> Unit,
114 | ) {
115 | val state = when (mode) {
116 | FilterMode.Include -> ToggleableState.On
117 | FilterMode.Exclude -> ToggleableState.Indeterminate
118 | null -> ToggleableState.Off
119 | }
120 | ListOptionItem(
121 | modifier = modifier
122 | .triStateToggleable(
123 | state = state,
124 | role = Role.Checkbox,
125 | onClick = onClick,
126 | ),
127 | text = label,
128 | ) {
129 | TriStateCheckbox(
130 | state = state,
131 | onClick = onClick,
132 | )
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/ui-common/src/main/kotlin/dev/clombardo/dnsnet/ui/common/Menu.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.common
10 |
11 | import androidx.compose.foundation.layout.Arrangement
12 | import androidx.compose.foundation.layout.Row
13 | import androidx.compose.foundation.layout.Spacer
14 | import androidx.compose.foundation.layout.padding
15 | import androidx.compose.foundation.layout.sizeIn
16 | import androidx.compose.material3.DropdownMenuItem
17 | import androidx.compose.material3.ExperimentalMaterial3Api
18 | import androidx.compose.material3.ExposedDropdownMenuDefaults
19 | import androidx.compose.material3.Icon
20 | import androidx.compose.material3.MaterialTheme
21 | import androidx.compose.material3.Text
22 | import androidx.compose.runtime.Composable
23 | import androidx.compose.ui.Alignment
24 | import androidx.compose.ui.Modifier
25 | import androidx.compose.ui.graphics.painter.Painter
26 | import androidx.compose.ui.unit.dp
27 |
28 | @OptIn(ExperimentalMaterial3Api::class)
29 | @Composable
30 | fun MenuItem(
31 | modifier: Modifier = Modifier,
32 | text: String,
33 | painter: Painter? = null,
34 | enabled: Boolean = true,
35 | onClick: () -> Unit,
36 | ) {
37 | DropdownMenuItem(
38 | modifier = modifier
39 | .sizeIn(minWidth = 112.dp, minHeight = 48.dp, maxWidth = 280.dp),
40 | text = {
41 | Row(
42 | modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp),
43 | horizontalArrangement = Arrangement.Start,
44 | verticalAlignment = Alignment.CenterVertically,
45 | ) {
46 | if (painter != null) {
47 | Icon(
48 | painter = painter,
49 | contentDescription = text,
50 | )
51 | Spacer(modifier = Modifier.padding(horizontal = 8.dp))
52 | }
53 | Text(
54 | text = text,
55 | style = MaterialTheme.typography.bodyLarge,
56 | )
57 | }
58 | },
59 | enabled = enabled,
60 | onClick = onClick,
61 | contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
62 | )
63 | }
64 |
--------------------------------------------------------------------------------
/ui-common/src/main/kotlin/dev/clombardo/dnsnet/ui/common/MorphUtil.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2024 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package dev.clombardo.dnsnet.ui.common
18 |
19 | import androidx.compose.ui.geometry.Size
20 | import androidx.compose.ui.graphics.Matrix
21 | import androidx.compose.ui.graphics.Outline
22 | import androidx.compose.ui.graphics.Shape
23 | import androidx.compose.ui.graphics.asComposePath
24 | import androidx.compose.ui.unit.Density
25 | import androidx.compose.ui.unit.LayoutDirection
26 | import androidx.graphics.shapes.Morph
27 | import androidx.graphics.shapes.toPath
28 |
29 | class RotatingMorphShape(
30 | private val morph: Morph,
31 | private val percentage: Float,
32 | private val rotation: Float
33 | ) : Shape {
34 | private val matrix = Matrix()
35 |
36 | override fun createOutline(
37 | size: Size,
38 | layoutDirection: LayoutDirection,
39 | density: Density
40 | ): Outline {
41 | // Below assumes that you haven't changed the default radius of 1f, nor the centerX and centerY of 0f
42 | // By default this stretches the path to the size of the container, if you don't want stretching, use the same size.width for both x and y.
43 | matrix.scale(size.width / 2f, size.height / 2f)
44 | matrix.translate(1f, 1f)
45 | matrix.rotateZ(rotation)
46 |
47 | val path = morph.toPath(progress = percentage).asComposePath()
48 | path.transform(matrix)
49 |
50 | return Outline.Generic(path)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/ui-common/src/main/kotlin/dev/clombardo/dnsnet/ui/common/PaddingUtil.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.common
10 |
11 | import androidx.compose.foundation.layout.PaddingValues
12 | import androidx.compose.foundation.layout.calculateEndPadding
13 | import androidx.compose.foundation.layout.calculateStartPadding
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.platform.LocalLayoutDirection
16 | import androidx.compose.ui.unit.Dp
17 | import androidx.compose.ui.unit.dp
18 |
19 | @Composable
20 | fun PaddingValues.add(
21 | start: Dp = Dp.Unspecified,
22 | top: Dp = Dp.Unspecified,
23 | end: Dp = Dp.Unspecified,
24 | bottom: Dp = Dp.Unspecified,
25 | ): PaddingValues {
26 | val layoutDirection = LocalLayoutDirection.current
27 |
28 | var currentStart = this.calculateStartPadding(layoutDirection)
29 | if (start != Dp.Unspecified) {
30 | currentStart += start
31 | }
32 |
33 | var currentTop = this.calculateTopPadding()
34 | if (top != Dp.Unspecified) {
35 | currentTop += top
36 | }
37 |
38 | var currentEnd = this.calculateEndPadding(layoutDirection)
39 | if (end != Dp.Unspecified) {
40 | currentEnd += end
41 | }
42 |
43 | var currentBottom = this.calculateBottomPadding()
44 | if (bottom != Dp.Unspecified) {
45 | currentBottom += bottom
46 | }
47 |
48 | return PaddingValues(
49 | start = currentStart,
50 | top = currentTop,
51 | end = currentEnd,
52 | bottom = currentBottom,
53 | )
54 | }
55 |
56 | @Composable
57 | operator fun PaddingValues.plus(other: PaddingValues): PaddingValues {
58 | val layoutDirection = LocalLayoutDirection.current
59 | return PaddingValues(
60 | start = this.calculateStartPadding(layoutDirection) + other.calculateStartPadding(layoutDirection),
61 | top = this.calculateTopPadding() + other.calculateTopPadding(),
62 | end = this.calculateEndPadding(layoutDirection) + other.calculateEndPadding(layoutDirection),
63 | bottom = this.calculateBottomPadding() + other.calculateBottomPadding(),
64 | )
65 | }
66 |
--------------------------------------------------------------------------------
/ui-common/src/main/kotlin/dev/clombardo/dnsnet/ui/common/RememberAtTop.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.common
10 |
11 | import androidx.compose.foundation.lazy.LazyListState
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.State
14 | import androidx.compose.runtime.derivedStateOf
15 | import androidx.compose.runtime.remember
16 |
17 | @Composable
18 | fun rememberAtTop(state: LazyListState): State {
19 | return remember {
20 | derivedStateOf {
21 | state.firstVisibleItemIndex == 0 && state.firstVisibleItemScrollOffset == 0
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/ui-common/src/main/kotlin/dev/clombardo/dnsnet/ui/common/SaveableUtil.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.common
10 |
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.saveable.listSaver
13 | import androidx.compose.runtime.saveable.rememberSaveable
14 | import androidx.compose.runtime.snapshots.SnapshotStateList
15 | import androidx.compose.runtime.toMutableStateList
16 |
17 | @Composable
18 | fun rememberMutableStateListOf(builderAction: MutableList.() -> Unit = {}): SnapshotStateList {
19 | return rememberSaveable(saver = snapshotStateListSaver()) {
20 | val elements = mutableListOf()
21 | builderAction(elements)
22 | elements.toMutableStateList()
23 | }
24 | }
25 |
26 | private fun snapshotStateListSaver() = listSaver, T>(
27 | save = { stateList -> stateList.toList() },
28 | restore = { it.toMutableStateList() },
29 | )
30 |
--------------------------------------------------------------------------------
/ui-common/src/main/kotlin/dev/clombardo/dnsnet/ui/common/Scaffold.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.common
10 |
11 | import androidx.compose.foundation.layout.PaddingValues
12 | import androidx.compose.foundation.layout.WindowInsets
13 | import androidx.compose.foundation.layout.WindowInsetsSides
14 | import androidx.compose.foundation.layout.add
15 | import androidx.compose.foundation.layout.displayCutout
16 | import androidx.compose.foundation.layout.only
17 | import androidx.compose.foundation.layout.systemBars
18 | import androidx.compose.foundation.layout.union
19 | import androidx.compose.material3.FabPosition
20 | import androidx.compose.material3.MaterialTheme
21 | import androidx.compose.material3.Scaffold
22 | import androidx.compose.material3.contentColorFor
23 | import androidx.compose.runtime.Composable
24 | import androidx.compose.ui.Modifier
25 | import androidx.compose.ui.graphics.Color
26 |
27 | private val scaffoldContentInsets: WindowInsets
28 | @Composable
29 | get() = WindowInsets.systemBars
30 | .union(WindowInsets.displayCutout.only(WindowInsetsSides.Start))
31 | .union(WindowInsets.displayCutout.only(WindowInsetsSides.End))
32 |
33 | @Composable
34 | fun InsetScaffold(
35 | modifier: Modifier = Modifier,
36 | topBar: @Composable () -> Unit = {},
37 | bottomBar: @Composable () -> Unit = {},
38 | snackbarHost: @Composable () -> Unit = {},
39 | floatingActionButton: @Composable () -> Unit = {},
40 | floatingActionButtonPosition: FabPosition = FabPosition.End,
41 | containerColor: Color = MaterialTheme.colorScheme.surfaceContainerLowest,
42 | contentColor: Color = contentColorFor(containerColor),
43 | contentWindowInsets: WindowInsets = scaffoldContentInsets,
44 | content: @Composable (PaddingValues) -> Unit,
45 | ) {
46 | Scaffold(
47 | modifier = modifier,
48 | topBar = topBar,
49 | bottomBar = bottomBar,
50 | snackbarHost = snackbarHost,
51 | floatingActionButton = floatingActionButton,
52 | floatingActionButtonPosition = floatingActionButtonPosition,
53 | containerColor = containerColor,
54 | contentColor = contentColor,
55 | contentWindowInsets = contentWindowInsets,
56 | content = content,
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/ui-common/src/main/kotlin/dev/clombardo/dnsnet/ui/common/ScreenTitle.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.common
10 |
11 | import androidx.compose.foundation.layout.Box
12 | import androidx.compose.foundation.layout.fillMaxWidth
13 | import androidx.compose.foundation.layout.padding
14 | import androidx.compose.foundation.lazy.LazyColumn
15 | import androidx.compose.foundation.lazy.rememberLazyListState
16 | import androidx.compose.material.icons.Icons
17 | import androidx.compose.material.icons.automirrored.filled.ArrowBack
18 | import androidx.compose.material3.MaterialTheme
19 | import androidx.compose.material3.Text
20 | import androidx.compose.runtime.Composable
21 | import androidx.compose.runtime.getValue
22 | import androidx.compose.ui.Alignment
23 | import androidx.compose.ui.Modifier
24 | import androidx.compose.ui.res.stringResource
25 | import androidx.compose.ui.tooling.preview.Preview
26 | import androidx.compose.ui.unit.dp
27 | import androidx.compose.ui.unit.sp
28 | import dev.clombardo.dnsnet.ui.common.theme.DnsNetTheme
29 |
30 | @Composable
31 | fun ScreenTitle(
32 | modifier: Modifier = Modifier,
33 | text: String,
34 | ) {
35 | Box(
36 | modifier = modifier.fillMaxWidth().padding(bottom = 24.dp),
37 | contentAlignment = Alignment.Center
38 | ) {
39 | Text(
40 | text = text,
41 | style = MaterialTheme.typography.displaySmall,
42 | fontSize = 32.sp,
43 | )
44 | }
45 | }
46 |
47 | @Preview
48 | @Composable
49 | private fun ScreenTitlePreview() {
50 | DnsNetTheme {
51 | val state = rememberLazyListState()
52 | InsetScaffold(
53 | topBar = {
54 | val isAtTop by rememberAtTop(state)
55 | FloatingTopActions(
56 | elevated = !isAtTop,
57 | navigationIcon = {
58 | BasicTooltipButton(
59 | icon = Icons.AutoMirrored.Filled.ArrowBack,
60 | contentDescription = stringResource(R.string.navigate_up),
61 | onClick = {},
62 | )
63 | }
64 | )
65 | }
66 | ) { contentPadding ->
67 | LazyColumn(
68 | state = state,
69 | contentPadding = contentPadding
70 | ) {
71 | item {
72 | ScreenTitle(text = "Screen Title")
73 | }
74 |
75 | repeat(50) {
76 | item {
77 | Text(text = "Item $it")
78 | }
79 | }
80 | }
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/ui-common/src/main/kotlin/dev/clombardo/dnsnet/ui/common/ScrollUpIndicator.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.common
10 |
11 | import androidx.compose.animation.AnimatedVisibility
12 | import androidx.compose.animation.EnterTransition
13 | import androidx.compose.animation.ExitTransition
14 | import androidx.compose.animation.slideInVertically
15 | import androidx.compose.animation.slideOutVertically
16 | import androidx.compose.foundation.background
17 | import androidx.compose.foundation.clickable
18 | import androidx.compose.foundation.layout.Box
19 | import androidx.compose.foundation.layout.BoxScope
20 | import androidx.compose.foundation.layout.WindowInsets
21 | import androidx.compose.foundation.layout.asPaddingValues
22 | import androidx.compose.foundation.layout.displayCutout
23 | import androidx.compose.foundation.layout.padding
24 | import androidx.compose.foundation.layout.size
25 | import androidx.compose.foundation.layout.systemBars
26 | import androidx.compose.foundation.layout.union
27 | import androidx.compose.foundation.shape.CircleShape
28 | import androidx.compose.material.icons.Icons
29 | import androidx.compose.material.icons.filled.ArrowUpward
30 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
31 | import androidx.compose.material3.Icon
32 | import androidx.compose.material3.MaterialTheme
33 | import androidx.compose.runtime.Composable
34 | import androidx.compose.runtime.rememberCoroutineScope
35 | import androidx.compose.ui.Alignment
36 | import androidx.compose.ui.Modifier
37 | import androidx.compose.ui.draw.clip
38 | import androidx.compose.ui.draw.shadow
39 | import androidx.compose.ui.res.stringResource
40 | import androidx.compose.ui.semantics.Role
41 | import androidx.compose.ui.unit.dp
42 | import kotlinx.coroutines.CoroutineScope
43 | import kotlinx.coroutines.launch
44 |
45 | @OptIn(ExperimentalMaterial3ExpressiveApi::class)
46 | object ScrollUpIndicatorDefaults {
47 | val windowInsets: WindowInsets
48 | @Composable get() = WindowInsets.systemBars.union(WindowInsets.displayCutout)
49 |
50 | val EnterTransition: EnterTransition
51 | @Composable get() {
52 | return slideInVertically(animationSpec = MaterialTheme.motionScheme.slowSpatialSpec()) {
53 | it
54 | }
55 | }
56 | val ExitTransition: ExitTransition
57 | @Composable get() {
58 | return slideOutVertically(animationSpec = MaterialTheme.motionScheme.slowSpatialSpec()) {
59 | it
60 | }
61 | }
62 | }
63 |
64 | object ScrollUpIndicator {
65 | val padding = 16.dp
66 | val size = 48.dp
67 | }
68 |
69 | @Composable
70 | fun BoxScope.ScrollUpIndicator(
71 | enabled: Boolean = true,
72 | visible: Boolean,
73 | enterTransition: EnterTransition = ScrollUpIndicatorDefaults.EnterTransition,
74 | exitTransition: ExitTransition = ScrollUpIndicatorDefaults.ExitTransition,
75 | windowInsets: WindowInsets = ScrollUpIndicatorDefaults.windowInsets,
76 | alignment: Alignment = Alignment.BottomEnd,
77 | onClick: suspend CoroutineScope.() -> Unit,
78 | ) {
79 | val scope = rememberCoroutineScope()
80 | val scrollUpButtonColor = MaterialTheme.colorScheme.tertiaryContainer
81 | AnimatedVisibility(
82 | modifier = Modifier.align(alignment),
83 | visible = visible,
84 | enter = enterTransition,
85 | exit = exitTransition,
86 | ) {
87 | Box(
88 | modifier = Modifier
89 | .padding(ScrollUpIndicator.padding)
90 | .padding(windowInsets.asPaddingValues())
91 | .size(ScrollUpIndicator.size)
92 | .shadow(
93 | elevation = 2.dp,
94 | shape = CircleShape,
95 | )
96 | .clip(CircleShape)
97 | .background(color = scrollUpButtonColor)
98 | .clickable(
99 | enabled = enabled,
100 | role = Role.Button,
101 | ) {
102 | scope.launch(block = onClick)
103 | },
104 | contentAlignment = Alignment.Center,
105 | ) {
106 | Icon(
107 | imageVector = Icons.Default.ArrowUpward,
108 | contentDescription = stringResource(R.string.scroll_up),
109 | tint = MaterialTheme.colorScheme.onTertiaryContainer,
110 | )
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/ui-common/src/main/kotlin/dev/clombardo/dnsnet/ui/common/WindowUtil.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.common
10 |
11 | import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
12 | import androidx.compose.runtime.Composable
13 | import androidx.window.core.layout.WindowSizeClass
14 |
15 | @Composable
16 | fun isSmallScreen(): Boolean {
17 | return !currentWindowAdaptiveInfo().windowSizeClass
18 | .isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND)
19 | }
20 |
--------------------------------------------------------------------------------
/ui-common/src/main/kotlin/dev/clombardo/dnsnet/ui/common/navigation/NavigationBar.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.common.navigation
10 |
11 | import androidx.compose.foundation.background
12 | import androidx.compose.foundation.clickable
13 | import androidx.compose.foundation.layout.Arrangement
14 | import androidx.compose.foundation.layout.Row
15 | import androidx.compose.foundation.layout.WindowInsets
16 | import androidx.compose.foundation.layout.asPaddingValues
17 | import androidx.compose.foundation.layout.calculateEndPadding
18 | import androidx.compose.foundation.layout.calculateStartPadding
19 | import androidx.compose.foundation.layout.fillMaxWidth
20 | import androidx.compose.foundation.layout.padding
21 | import androidx.compose.foundation.layout.systemBars
22 | import androidx.compose.material.icons.Icons
23 | import androidx.compose.material.icons.filled.Android
24 | import androidx.compose.material.icons.filled.Dns
25 | import androidx.compose.material.icons.filled.VpnKey
26 | import androidx.compose.material3.MaterialTheme
27 | import androidx.compose.runtime.Composable
28 | import androidx.compose.runtime.derivedStateOf
29 | import androidx.compose.runtime.getValue
30 | import androidx.compose.runtime.mutableIntStateOf
31 | import androidx.compose.runtime.remember
32 | import androidx.compose.runtime.rememberUpdatedState
33 | import androidx.compose.runtime.setValue
34 | import androidx.compose.ui.Alignment
35 | import androidx.compose.ui.Modifier
36 | import androidx.compose.ui.platform.LocalLayoutDirection
37 | import androidx.compose.ui.tooling.preview.Preview
38 | import androidx.compose.ui.unit.dp
39 | import dev.clombardo.dnsnet.ui.common.theme.DnsNetTheme
40 |
41 | object NavigationBar {
42 | val height = 64.dp
43 | }
44 |
45 | @Composable
46 | fun NavigationBar(
47 | modifier: Modifier = Modifier,
48 | windowInsets: WindowInsets = WindowInsets.systemBars,
49 | content: NavigationScope.() -> Unit,
50 | ) {
51 | val latestContent = rememberUpdatedState(content)
52 | val scope by remember { derivedStateOf { NavigationScopeImpl().apply(latestContent.value) } }
53 |
54 | val insets = windowInsets.asPaddingValues()
55 | val layoutDirection = LocalLayoutDirection.current
56 | Row(
57 | modifier = modifier
58 | .clickable(
59 | enabled = false,
60 | interactionSource = null,
61 | indication = null,
62 | onClick = {},
63 | )
64 | .fillMaxWidth()
65 | .background(MaterialTheme.colorScheme.surfaceContainer)
66 | .padding(
67 | start = insets.calculateStartPadding(layoutDirection),
68 | end = insets.calculateEndPadding(layoutDirection),
69 | bottom = insets.calculateBottomPadding(),
70 | ),
71 | verticalAlignment = Alignment.CenterVertically,
72 | horizontalArrangement = Arrangement.SpaceAround,
73 | ) {
74 | scope.itemList.forEach {
75 | NavigationItem(
76 | modifier = Modifier.weight(1f),
77 | layoutType = LayoutType.NavigationBar,
78 | item = it,
79 | )
80 | }
81 | }
82 | }
83 |
84 | @Preview
85 | @Composable
86 | private fun NavigationBarPreview() {
87 | DnsNetTheme {
88 | var selectedIndex by remember { mutableIntStateOf(0) }
89 | NavigationBar {
90 | item(
91 | selected = selectedIndex == 0,
92 | icon = Icons.Default.VpnKey,
93 | text = "Start",
94 | onClick = { selectedIndex = 0 },
95 | )
96 | item(
97 | selected = selectedIndex == 1,
98 | icon = Icons.Default.Dns,
99 | text = "DNS",
100 | onClick = { selectedIndex = 1 },
101 | )
102 | item(
103 | selected = selectedIndex == 2,
104 | icon = Icons.Default.Android,
105 | text = "Apps",
106 | onClick = { selectedIndex = 2 },
107 | )
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/ui-common/src/main/kotlin/dev/clombardo/dnsnet/ui/common/navigation/NavigationRail.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.common.navigation
10 |
11 | import androidx.compose.foundation.background
12 | import androidx.compose.foundation.layout.Arrangement
13 | import androidx.compose.foundation.layout.Column
14 | import androidx.compose.foundation.layout.Spacer
15 | import androidx.compose.foundation.layout.WindowInsets
16 | import androidx.compose.foundation.layout.asPaddingValues
17 | import androidx.compose.foundation.layout.calculateStartPadding
18 | import androidx.compose.foundation.layout.fillMaxHeight
19 | import androidx.compose.foundation.layout.padding
20 | import androidx.compose.foundation.layout.systemBars
21 | import androidx.compose.material.icons.Icons
22 | import androidx.compose.material.icons.filled.Android
23 | import androidx.compose.material.icons.filled.Dns
24 | import androidx.compose.material.icons.filled.VpnKey
25 | import androidx.compose.material3.MaterialTheme
26 | import androidx.compose.runtime.Composable
27 | import androidx.compose.runtime.derivedStateOf
28 | import androidx.compose.runtime.getValue
29 | import androidx.compose.runtime.mutableIntStateOf
30 | import androidx.compose.runtime.remember
31 | import androidx.compose.runtime.rememberUpdatedState
32 | import androidx.compose.runtime.setValue
33 | import androidx.compose.ui.Alignment
34 | import androidx.compose.ui.Modifier
35 | import androidx.compose.ui.platform.LocalLayoutDirection
36 | import androidx.compose.ui.tooling.preview.Preview
37 | import androidx.compose.ui.unit.dp
38 | import dev.clombardo.dnsnet.ui.common.theme.DnsNetTheme
39 |
40 | object NavigationRail {
41 | val width = 80.dp
42 | }
43 |
44 | @Composable
45 | fun NavigationRail(
46 | modifier: Modifier = Modifier,
47 | windowInsets: WindowInsets = WindowInsets.systemBars,
48 | verticalArrangement: Arrangement. Vertical = Arrangement.Top,
49 | content: NavigationScope.() -> Unit,
50 | ) {
51 | val latestContent = rememberUpdatedState(content)
52 | val scope by remember { derivedStateOf { NavigationScopeImpl().apply(latestContent.value) } }
53 |
54 | val insets = windowInsets.asPaddingValues()
55 | val layoutDirection = LocalLayoutDirection.current
56 | Column(
57 | modifier = modifier
58 | .fillMaxHeight()
59 | .background(color = MaterialTheme.colorScheme.surfaceContainerLowest)
60 | .padding(
61 | start = insets.calculateStartPadding(layoutDirection),
62 | top = insets.calculateTopPadding(),
63 | bottom = insets.calculateBottomPadding(),
64 | )
65 | .padding(horizontal = 12.dp),
66 | verticalArrangement = verticalArrangement,
67 | horizontalAlignment = Alignment.CenterHorizontally,
68 | ) {
69 | scope.itemList.forEach {
70 | Spacer(Modifier.padding(top = 12.dp))
71 | NavigationItem(
72 | layoutType = LayoutType.NavigationRail,
73 | item = it,
74 | )
75 | }
76 | }
77 | }
78 |
79 | @Preview
80 | @Composable
81 | private fun NavigationRailPreview() {
82 | DnsNetTheme {
83 | var selectedIndex by remember { mutableIntStateOf(0) }
84 | NavigationRail {
85 | item(
86 | selected = selectedIndex == 0,
87 | icon = Icons.Default.VpnKey,
88 | text = "Start",
89 | onClick = { selectedIndex = 0 },
90 | )
91 | item(
92 | selected = selectedIndex == 1,
93 | icon = Icons.Default.Dns,
94 | text = "DNS",
95 | onClick = { selectedIndex = 1 },
96 | )
97 | item(
98 | selected = selectedIndex == 2,
99 | icon = Icons.Default.Android,
100 | text = "Apps",
101 | onClick = { selectedIndex = 2 },
102 | )
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/ui-common/src/main/kotlin/dev/clombardo/dnsnet/ui/common/navigation/NavigationScope.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.common.navigation
10 |
11 | import androidx.annotation.StringRes
12 | import androidx.compose.runtime.collection.MutableVector
13 | import androidx.compose.runtime.collection.mutableVectorOf
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.graphics.vector.ImageVector
16 |
17 | sealed interface NavigationScope {
18 | val itemList: MutableVector
19 |
20 | fun item(
21 | modifier: Modifier = Modifier,
22 | selected: Boolean,
23 | icon: ImageVector,
24 | text: String,
25 | onClick: () -> Unit,
26 | )
27 | }
28 |
29 | class NavigationScopeImpl : NavigationScope {
30 | override val itemList: MutableVector = mutableVectorOf()
31 |
32 | override fun item(
33 | modifier: Modifier,
34 | selected: Boolean,
35 | icon: ImageVector,
36 | text: String,
37 | onClick: () -> Unit
38 | ) {
39 | itemList.add(
40 | NavigationItem(
41 | modifier = modifier,
42 | selected = selected,
43 | icon = icon,
44 | text = text,
45 | onClick = onClick,
46 | )
47 | )
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/ui-common/src/main/kotlin/dev/clombardo/dnsnet/ui/common/theme/Animation.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.common.theme
10 |
11 | import androidx.compose.animation.EnterTransition
12 | import androidx.compose.animation.ExitTransition
13 | import androidx.compose.animation.core.CubicBezierEasing
14 | import androidx.compose.animation.expandHorizontally
15 | import androidx.compose.animation.fadeIn
16 | import androidx.compose.animation.fadeOut
17 | import androidx.compose.animation.shrinkHorizontally
18 | import androidx.compose.foundation.lazy.LazyListScope
19 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
20 | import androidx.compose.material3.MaterialTheme
21 | import androidx.compose.runtime.Composable
22 | import androidx.compose.ui.Alignment
23 | import androidx.compose.ui.Modifier
24 |
25 | @OptIn(ExperimentalMaterial3ExpressiveApi::class)
26 | object Animation {
27 | val EmphasizedDecelerateEasing by lazy { CubicBezierEasing(0.05f, 0.7f, 0.1f, 1f) }
28 | val EmphasizedAccelerateEasing by lazy { CubicBezierEasing(0.3f, 0f, 0.8f, 0.15f) }
29 |
30 | val ShowSpinnerHorizontal: EnterTransition
31 | @Composable get() {
32 | return fadeIn(
33 | animationSpec = MaterialTheme.motionScheme.defaultSpatialSpec(),
34 | ) + expandHorizontally(
35 | animationSpec = MaterialTheme.motionScheme.defaultSpatialSpec(),
36 | expandFrom = Alignment.Start,
37 | clip = false,
38 | )
39 | }
40 |
41 | val HideSpinnerHorizontal: ExitTransition
42 | @Composable get() {
43 | return fadeOut(
44 | animationSpec = MaterialTheme.motionScheme.defaultSpatialSpec(),
45 | ) + shrinkHorizontally(
46 | animationSpec = MaterialTheme.motionScheme.defaultSpatialSpec(),
47 | shrinkTowards = Alignment.Start,
48 | clip = false,
49 | )
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/ui-common/src/main/kotlin/dev/clombardo/dnsnet/ui/common/theme/Dimension.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.common.theme
10 |
11 | import androidx.compose.ui.unit.dp
12 |
13 | val DefaultFabSize = 56.dp
14 | val FabPadding = 16.dp
15 |
16 | val ListPadding = 16.dp
17 |
--------------------------------------------------------------------------------
/ui-common/src/main/kotlin/dev/clombardo/dnsnet/ui/common/theme/Type.kt:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 Charles Lombardo
2 | *
3 | * This program is free software: you can redistribute it and/or modify
4 | * it under the terms of the GNU General Public License as published by
5 | * the Free Software Foundation, either version 3 of the License, or
6 | * (at your option) any later version.
7 | */
8 |
9 | package dev.clombardo.dnsnet.ui.common.theme
10 |
11 | import android.os.Build
12 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
13 | import androidx.compose.material3.Typography
14 | import androidx.compose.ui.text.ExperimentalTextApi
15 | import androidx.compose.ui.text.TextStyle
16 | import androidx.compose.ui.text.font.Font
17 | import androidx.compose.ui.text.font.FontFamily
18 | import androidx.compose.ui.text.font.FontVariation
19 | import dev.clombardo.dnsnet.resources.R
20 |
21 | @OptIn(ExperimentalTextApi::class)
22 | val displayEmphasizedFontFamily = FontFamily(
23 | Font(
24 | resId = R.font.roboto_flex,
25 | variationSettings = FontVariation.Settings(
26 | FontVariation.weight(1000),
27 | FontVariation.grade(150),
28 | FontVariation.slant(-10f),
29 | FontVariation.width(60f),
30 | FontVariation.Setting("XOPQ", 27f),
31 | FontVariation.Setting("YOPQ", 90f),
32 | FontVariation.Setting("XTRA", 540f),
33 | )
34 | )
35 | )
36 |
37 | @OptIn(ExperimentalMaterial3ExpressiveApi::class)
38 | val AppTypography = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
39 | Typography(
40 | displayLargeEmphasized = TextStyle(fontFamily = displayEmphasizedFontFamily),
41 | displayMediumEmphasized = TextStyle(fontFamily = displayEmphasizedFontFamily),
42 | displaySmallEmphasized = TextStyle(fontFamily = displayEmphasizedFontFamily),
43 | )
44 | } else {
45 | Typography()
46 | }
47 |
--------------------------------------------------------------------------------