├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── COPYING ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── config │ └── detekt │ │ └── detekt.yml ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── kotlin │ │ └── dev │ │ │ └── clombardo │ │ │ └── dnsnet │ │ │ ├── ActionReceiver.kt │ │ │ ├── BootComplete.kt │ │ │ ├── DnsNetApplication.kt │ │ │ ├── MainActivity.kt │ │ │ └── tile │ │ │ └── DnsNetTileService.kt │ └── res │ │ └── resources.properties │ └── release │ └── generated │ └── baselineProfiles │ ├── baseline-prof.txt │ └── startup-prof.txt ├── assets ├── Icon.svg └── feature-graphic.png ├── baselineprofile ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── dev │ └── clombardo │ └── baselineprofile │ ├── BaselineProfileGenerator.kt │ └── StartupBenchmarks.kt ├── blocklogger ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── dev │ └── clombardo │ └── dnsnet │ └── blocklogger │ └── BlockLogger.kt ├── build.gradle.kts ├── file ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── dev │ └── clombardo │ └── dnsnet │ └── file │ ├── FileHelper.kt │ └── SingleWriterMultipleReaderFile.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── log ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── dev │ └── clombardo │ └── dnsnet │ └── log │ └── Log.kt ├── metadata ├── en-US │ ├── full_description.txt │ ├── images │ │ ├── feature-graphic.png │ │ ├── icon.png │ │ └── phoneScreenshots │ │ │ ├── apps-p9p.png │ │ │ ├── apps-pfp.png │ │ │ ├── apps-pt.png │ │ │ ├── dns-p9p.png │ │ │ ├── dns-pfp.png │ │ │ ├── dns-pt.png │ │ │ ├── hosts-p9p.png │ │ │ ├── hosts-pfp.png │ │ │ ├── hosts-pt.png │ │ │ ├── start-p9p.png │ │ │ ├── start-pfp.png │ │ │ └── start-pt.png │ └── short_description.txt ├── es-ES │ ├── full_description.txt │ └── short_description.txt ├── fa-IR │ ├── full_description.txt │ └── short_description.txt ├── id │ ├── full_description.txt │ └── short_description.txt ├── it-IT │ ├── full_description.txt │ └── short_description.txt ├── pl-PL │ └── short_description.txt ├── ru-RU │ ├── full_description.txt │ └── short_description.txt └── tr-TR │ ├── full_description.txt │ └── short_description.txt ├── notification ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── dev │ └── clombardo │ └── dnsnet │ └── notification │ └── NotificationChannels.kt ├── resources ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── res │ ├── drawable │ ├── ic_launcher_foreground.xml │ ├── ic_refresh.xml │ ├── ic_state_allow.xml │ ├── ic_state_deny.xml │ ├── ic_state_ignore.xml │ ├── ic_warning.xml │ └── icon_full.xml │ ├── font │ └── roboto_flex.ttf │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_banner.png │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── values-de │ └── strings.xml │ ├── values-es │ └── strings.xml │ ├── values-et │ └── strings.xml │ ├── values-fa │ └── strings.xml │ ├── values-fi │ └── strings.xml │ ├── values-fr │ └── strings.xml │ ├── values-hi │ └── strings.xml │ ├── values-in │ └── strings.xml │ ├── values-it │ └── strings.xml │ ├── values-ja │ └── strings.xml │ ├── values-nb │ └── strings.xml │ ├── values-night │ └── color.xml │ ├── values-nl │ └── strings.xml │ ├── values-pl │ └── strings.xml │ ├── values-pt-rBR │ └── strings.xml │ ├── values-ru │ └── strings.xml │ ├── values-tr │ └── strings.xml │ ├── values-zh │ └── strings.xml │ └── values │ ├── color.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── theme.xml ├── rust-toolchain.toml ├── service ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── libnet │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ ├── src │ │ ├── backend │ │ │ ├── doh3.rs │ │ │ ├── mod.rs │ │ │ └── standard.rs │ │ ├── database.rs │ │ ├── lib.rs │ │ ├── packet.rs │ │ ├── proxy.rs │ │ ├── validation.rs │ │ └── vpn.rs │ └── uniffi-bindgen.rs └── src │ ├── androidTest │ └── kotlin │ │ └── dev │ │ └── clombardo │ │ └── dnsnet │ │ └── service │ │ └── RuleDatabaseTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── kotlin │ │ └── dev │ │ └── clombardo │ │ └── dnsnet │ │ └── service │ │ ├── FilterUtil.kt │ │ ├── NativeBlockLoggerWrapper.kt │ │ ├── NativeFileHelperWrapper.kt │ │ ├── NetworkState.kt │ │ ├── db │ │ ├── RuleDatabaseItemUpdate.kt │ │ ├── RuleDatabaseManager.kt │ │ └── RuleDatabaseUpdateWorker.kt │ │ └── vpn │ │ ├── DnsNetVpnService.kt │ │ ├── NetworkUtil.kt │ │ ├── VpnExceptions.kt │ │ ├── VpnNetworkCallback.kt │ │ └── VpnThread.kt │ └── test │ └── kotlin │ └── dev │ └── clombardo │ └── dnsnet │ └── service │ └── NetworkStateTest.kt ├── settings.gradle.kts ├── settings ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── dev │ └── clombardo │ └── dnsnet │ └── settings │ ├── Configuration.kt │ └── Preferences.kt ├── ui-app ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── dev │ └── clombardo │ └── dnsnet │ └── ui │ └── app │ ├── About.kt │ ├── Apps.kt │ ├── BlockLog.kt │ ├── Credits.kt │ ├── Dns.kt │ ├── Filters.kt │ ├── Home.kt │ ├── Insets.kt │ ├── NavUtil.kt │ ├── Presets.kt │ ├── Setup.kt │ ├── Start.kt │ ├── coil │ └── AppImage.kt │ ├── model │ └── AppData.kt │ ├── state │ ├── AppListState.kt │ └── BlockLogListState.kt │ ├── util │ └── NumberFormatterCompat.kt │ └── viewmodel │ ├── AppListViewModel.kt │ ├── BlockLogListViewModel.kt │ ├── HomeViewModel.kt │ └── PersistableViewModel.kt └── ui-common ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro └── src └── main ├── AndroidManifest.xml └── kotlin └── dev └── clombardo └── dnsnet └── ui └── common ├── Dialog.kt ├── FloatingTopActions.kt ├── FullSizeClickable.kt ├── LinkUtil.kt ├── ListOptionItems.kt ├── LoadingIndicatorBox.kt ├── Menu.kt ├── MorphUtil.kt ├── PaddingUtil.kt ├── Paging.kt ├── RememberAtTop.kt ├── SaveableUtil.kt ├── Scaffold.kt ├── ScreenTitle.kt ├── ScrollUpIndicator.kt ├── SearchWidget.kt ├── Settings.kt ├── SplitContentContainer.kt ├── TooltipIconButton.kt ├── TriStateFab.kt ├── WindowUtil.kt ├── navigation ├── NavigationBar.kt ├── NavigationItem.kt ├── NavigationRail.kt ├── NavigationScaffold.kt └── NavigationScope.kt └── theme ├── Animation.kt ├── Color.kt ├── Dimension.kt ├── Theme.kt └── Type.kt /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: DNSNet Test 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - '**/baseline-prof.txt' 7 | - '**/startup-prof.txt' 8 | - '**/.github/**' 9 | push: 10 | branches: 11 | - a-couple-updates 12 | paths-ignore: 13 | - '**/baseline-prof.txt' 14 | - '**/startup-prof.txt' 15 | - '**/.github/**' 16 | 17 | jobs: 18 | test: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | api-level: [35] 23 | arch: [x86_64] 24 | target: [google_apis] 25 | steps: 26 | - name: checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Enable KVM 30 | run: | 31 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules 32 | sudo udevadm control --reload-rules 33 | sudo udevadm trigger --name-match=kvm 34 | 35 | - name: Set up Java 36 | uses: actions/setup-java@v4 37 | with: 38 | distribution: 'temurin' 39 | java-version: '17' 40 | 41 | - name: Set up Rust 42 | uses: actions-rust-lang/setup-rust-toolchain@v1 43 | - name: Set up Python 44 | uses: actions/setup-python@v5 45 | with: 46 | python-version: '3.13' 47 | 48 | - name: Set up NDK 49 | uses: nttld/setup-ndk@v1 50 | id: setup-ndk 51 | with: 52 | ndk-version: r28 53 | link-to-sdk: true 54 | 55 | - name: AVD cache 56 | uses: actions/cache@v4 57 | id: avd-cache 58 | with: 59 | path: | 60 | ~/.android/avd/* 61 | ~/.android/adb* 62 | key: avd-${{ matrix.api-level }} 63 | 64 | - name: create AVD and generate snapshot for caching 65 | if: steps.avd-cache.outputs.cache-hit != 'true' 66 | uses: reactivecircus/android-emulator-runner@v2 67 | with: 68 | api-level: ${{ matrix.api-level }} 69 | arch: ${{ matrix.arch }} 70 | target: ${{ matrix.target }} 71 | force-avd-creation: false 72 | emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none 73 | disable-animations: false 74 | script: echo "Generated AVD snapshot for caching." 75 | 76 | - name: run tests 77 | uses: reactivecircus/android-emulator-runner@v2 78 | env: 79 | ANDROID_NDK: ${{ steps.setup-ndk.outputs.ndk-path }} 80 | ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} 81 | ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} 82 | ANDROID_NDK_LATEST_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} 83 | with: 84 | api-level: ${{ matrix.api-level }} 85 | target: ${{ matrix.target }} 86 | arch: ${{ matrix.arch }} 87 | profile: Nexus 6 88 | script: | 89 | ./gradlew :service:test --stacktrace 90 | ./gradlew :service:connectedCheck --stacktrace && killall -INT crashpad_handler || true 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | /.kotlin 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at clombardo169@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | 78 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to DNSNet 2 | 3 | If you want to contribute code to DNSNet, you are more than welcome to do so. 4 | DNSNet uses GitHub pull requests for code contributions. 5 | 6 | ## Pull requests 7 | A pull request should consist of a series of logical commits leading from a 8 | point in the master branch to whatever you are implementing. The pull request 9 | should not contain any fixup commits -- rebase your branch instead, and squash 10 | the fixup commits into their respective commits. 11 | 12 | ### Commit messages 13 | Make sure you have good commit messages. In particular, the first line should 14 | be a very short description, followed by an empty line, followed by one or more 15 | paragraphs explaining the change. The last paragraph should mention any github 16 | issues being fixed in a syntax understood by github. 17 | 18 | If you squash changes together during a rebase, edit the commit message to 19 | describe the squashed commit as a whole - do not just stick the commit messages 20 | together. 21 | 22 | ## Coding style 23 | Code is usually automatically formatted with Android Studio using the default 24 | coding style. Sometimes it is forgotten, but please try to make sure to follow 25 | the same coding style for any of your contributions. 26 | 27 | ## Licensing 28 | Any code contribution will be considered to grant an implicit license under 29 | the GPL, version 3 or later, as stated below, or (on special exceptions) a 30 | different license explicitly included in the contribution that is compatible 31 | to the GPL 3. 32 | 33 | Icons are welcome too. Modifications of existing icons shall have the same 34 | license as the modified file. New icons may be contributed under the GPL, 35 | version 3 or later (as below), or the Apache license, version 2. 36 | 37 | ## Code of Conduct 38 | Please note that this project is released with a Contributor Code of 39 | Conduct. By participating in this project you agree to abide by its terms. 40 | 41 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/jak/Android/Sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Autogenerated by the Android Gradle Plugin 20 | -dontwarn sun.net.spi.nameservice.NameServiceDescriptor 21 | -dontwarn lombok.Generated 22 | -dontwarn java.awt.* 23 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 9 | 10 | 13 | 16 | 17 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 46 | 47 | 48 | 49 | 50 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/clombardo/dnsnet/ActionReceiver.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 12 | 13 | import android.content.BroadcastReceiver 14 | import android.content.Context 15 | import android.content.Intent 16 | import dev.clombardo.dnsnet.log.logDebug 17 | import dev.clombardo.dnsnet.log.logWarning 18 | import dev.clombardo.dnsnet.service.vpn.DnsNetVpnService 19 | 20 | class ActionReceiver : BroadcastReceiver() { 21 | override fun onReceive(context: Context?, intent: Intent?) { 22 | context ?: return 23 | val action = intent?.action ?: return 24 | logDebug("Got broadcast - $intent") 25 | when (action) { 26 | ACTION_START -> DnsNetVpnService.start(context) 27 | ACTION_STOP -> DnsNetVpnService.stop(context) 28 | else -> logWarning("Got unknown action: $action") 29 | } 30 | } 31 | 32 | companion object { 33 | private const val ACTION_START = "${BuildConfig.APPLICATION_ID}.START" 34 | private const val ACTION_STOP = "${BuildConfig.APPLICATION_ID}.STOP" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/clombardo/dnsnet/BootComplete.kt: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 Charles Lombardo 2 | * 3 | * Derived from DNS66: 4 | * Copyright (C) 2016-2019 Julian Andres Klode 5 | * 6 | * Derived from AdBuster: 7 | * Copyright (C) 2016 Daniel Brodie 8 | * 9 | * This program is free software: you can redistribute it and/or modify 10 | * it under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation, version 3. 12 | * 13 | * Contributions shall also be provided under any later versions of the 14 | * GPL. 15 | */ 16 | 17 | package dev.clombardo.dnsnet 18 | 19 | import android.content.BroadcastReceiver 20 | import android.content.Context 21 | import android.content.Intent 22 | import dagger.hilt.android.AndroidEntryPoint 23 | import dev.clombardo.dnsnet.service.vpn.DnsNetVpnService 24 | import dev.clombardo.dnsnet.settings.ConfigurationManager 25 | import dev.clombardo.dnsnet.settings.Preferences 26 | import javax.inject.Inject 27 | 28 | @AndroidEntryPoint 29 | class BootComplete : BroadcastReceiver() { 30 | @Inject 31 | lateinit var configuration: ConfigurationManager 32 | 33 | @Inject 34 | lateinit var preferences: Preferences 35 | 36 | override fun onReceive(context: Context, intent: Intent?) { 37 | if (intent?.action == Intent.ACTION_BOOT_COMPLETED) { 38 | DnsNetVpnService.checkStartVpnOnBoot(context, configuration, preferences) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/clombardo/dnsnet/DnsNetApplication.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 10 | 11 | import android.app.Application 12 | import androidx.hilt.work.HiltWorkerFactory 13 | import coil3.ImageLoader 14 | import coil3.SingletonImageLoader 15 | import coil3.disk.DiskCache 16 | import coil3.disk.directory 17 | import coil3.memory.MemoryCache 18 | import dagger.hilt.android.HiltAndroidApp 19 | import dev.clombardo.dnsnet.notification.NotificationChannels 20 | import dev.clombardo.dnsnet.service.FilterUtil 21 | import dev.clombardo.dnsnet.service.db.RuleDatabaseUpdateWorker 22 | import dev.clombardo.dnsnet.settings.Configuration 23 | import dev.clombardo.dnsnet.settings.ConfigurationManager 24 | import dev.clombardo.dnsnet.settings.Preferences 25 | import dev.clombardo.dnsnet.ui.app.coil.AppImageFetcher 26 | import dev.clombardo.dnsnet.ui.app.coil.AppImageKeyer 27 | import kotlinx.coroutines.CoroutineScope 28 | import kotlinx.coroutines.Dispatchers 29 | import kotlinx.coroutines.launch 30 | import uniffi.net.rustInit 31 | import java.io.File 32 | import javax.inject.Inject 33 | 34 | @HiltAndroidApp 35 | class DnsNetApplication : Application(), androidx.work.Configuration.Provider { 36 | @Inject 37 | lateinit var preferences: Preferences 38 | 39 | @Inject 40 | lateinit var configuration: ConfigurationManager 41 | 42 | override fun onCreate() { 43 | super.onCreate() 44 | 45 | rustInit(debug = BuildConfig.DEBUG) 46 | 47 | NotificationChannels.onCreate(this) 48 | 49 | SingletonImageLoader.setSafe { 50 | ImageLoader.Builder(applicationContext) 51 | .components { 52 | add(AppImageKeyer()) 53 | add(AppImageFetcher.Factory()) 54 | } 55 | .memoryCache { 56 | MemoryCache.Builder() 57 | .maxSizePercent(applicationContext) 58 | .build() 59 | } 60 | .diskCache { 61 | DiskCache.Builder() 62 | .directory(applicationContext.cacheDir.resolve("image_cache")) 63 | .maxSizePercent(0.02) 64 | .build() 65 | } 66 | .build() 67 | } 68 | 69 | // Prevent existing users (pre-1.1.9) from seeing the setup screen 70 | if (preferences.NotificationPermissionActedUpon) { 71 | preferences.SetupComplete = true 72 | } 73 | 74 | CoroutineScope(Dispatchers.IO).launch { 75 | if (!FilterUtil.areFilterFilesExistent(this@DnsNetApplication, configuration)) { 76 | RuleDatabaseUpdateWorker.runNow(this@DnsNetApplication) 77 | } 78 | } 79 | } 80 | 81 | @Inject 82 | lateinit var workerFactory: HiltWorkerFactory 83 | 84 | override val workManagerConfiguration: androidx.work.Configuration 85 | get() = androidx.work.Configuration.Builder() 86 | .setWorkerFactory(workerFactory) 87 | .build() 88 | } 89 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/clombardo/dnsnet/tile/DnsNetTileService.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.tile 10 | 11 | import android.annotation.SuppressLint 12 | import android.net.VpnService 13 | import android.os.Build 14 | import android.service.quicksettings.Tile 15 | import android.service.quicksettings.TileService 16 | import dev.clombardo.dnsnet.MainActivity 17 | import dev.clombardo.dnsnet.service.vpn.DnsNetVpnService 18 | import dev.clombardo.dnsnet.service.vpn.VpnStatus 19 | import kotlinx.coroutines.CoroutineScope 20 | import kotlinx.coroutines.Dispatchers 21 | import kotlinx.coroutines.cancel 22 | import kotlinx.coroutines.flow.collectLatest 23 | import kotlinx.coroutines.launch 24 | 25 | class DnsNetTileService : TileService() { 26 | private lateinit var tileCoroutineScope: CoroutineScope 27 | 28 | override fun onStartListening() { 29 | super.onStartListening() 30 | tileCoroutineScope = CoroutineScope(Dispatchers.IO) 31 | tileCoroutineScope.launch { 32 | DnsNetVpnService.status.collectLatest { 33 | update(it) 34 | } 35 | } 36 | } 37 | 38 | override fun onStopListening() { 39 | super.onStopListening() 40 | tileCoroutineScope.cancel() 41 | } 42 | 43 | override fun onClick() { 44 | super.onClick() 45 | if (isSecure) { 46 | unlockAndRun(::toggleService) 47 | } else { 48 | toggleService() 49 | } 50 | } 51 | 52 | private fun update(status: VpnStatus) { 53 | qsTile?.apply { 54 | val statusString = applicationContext.getString(status.toTextId()) 55 | contentDescription = statusString 56 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 57 | subtitle = statusString 58 | } 59 | 60 | state = 61 | if (status != VpnStatus.STOPPED) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE 62 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 63 | stateDescription = statusString 64 | } 65 | updateTile() 66 | } 67 | } 68 | 69 | @SuppressLint("StartActivityAndCollapseDeprecated") 70 | private fun toggleService() { 71 | val prepareIntent = VpnService.prepare(applicationContext) 72 | if (prepareIntent != null) { 73 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 74 | startActivityAndCollapse(MainActivity.getPendingIntent(applicationContext)) 75 | } else { 76 | startActivityAndCollapse(MainActivity.getIntent(applicationContext)) 77 | } 78 | return 79 | } 80 | 81 | DnsNetVpnService.toggle(applicationContext) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/res/resources.properties: -------------------------------------------------------------------------------- 1 | unqualifiedResLocale=en-US 2 | -------------------------------------------------------------------------------- /assets/Icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /assets/feature-graphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/assets/feature-graphic.png -------------------------------------------------------------------------------- /baselineprofile/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /baselineprofile/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.test) 11 | alias(libs.plugins.kotlin.android) 12 | alias(libs.plugins.androidx.baselineprofile) 13 | } 14 | 15 | kotlin { 16 | jvmToolchain(libs.versions.java.get().toInt()) 17 | } 18 | 19 | android { 20 | namespace = "dev.clombardo.baselineprofile" 21 | compileSdk = libs.versions.compileSdk.get().toInt() 22 | 23 | defaultConfig { 24 | minSdk = libs.versions.minSdkBaselineProfile.get().toInt() 25 | targetSdk = libs.versions.targetSdk.get().toInt() 26 | 27 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 28 | } 29 | 30 | targetProjectPath = ":app" 31 | } 32 | 33 | baselineProfile { 34 | useConnectedDevices = true 35 | } 36 | 37 | dependencies { 38 | implementation(libs.androidx.test.junit) 39 | implementation(libs.androidx.test.espresso.core) 40 | implementation(libs.androidx.test.uiautomator) 41 | implementation(libs.androidx.benchmark.macro.junit4) 42 | 43 | implementation(project(":ui-app")) 44 | } 45 | 46 | androidComponents { 47 | onVariants { v -> 48 | val artifactsLoader = v.artifacts.getBuiltArtifactsLoader() 49 | v.instrumentationRunnerArguments.put( 50 | "targetAppId", 51 | v.testedApks.map { artifactsLoader.load(it)?.applicationId ?: "" } 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /baselineprofile/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /baselineprofile/src/main/java/dev/clombardo/baselineprofile/StartupBenchmarks.kt: -------------------------------------------------------------------------------- 1 | package dev.clombardo.baselineprofile 2 | 3 | import androidx.benchmark.macro.BaselineProfileMode 4 | import androidx.benchmark.macro.CompilationMode 5 | import androidx.benchmark.macro.StartupMode 6 | import androidx.benchmark.macro.StartupTimingMetric 7 | import androidx.benchmark.macro.junit4.MacrobenchmarkRule 8 | import androidx.test.ext.junit.runners.AndroidJUnit4 9 | import androidx.test.filters.LargeTest 10 | import androidx.test.platform.app.InstrumentationRegistry 11 | import org.junit.Rule 12 | import org.junit.Test 13 | import org.junit.runner.RunWith 14 | 15 | /** 16 | * This test class benchmarks the speed of app startup. 17 | * Run this benchmark to verify how effective a Baseline Profile is. 18 | * It does this by comparing [CompilationMode.None], which represents the app with no Baseline 19 | * Profiles optimizations, and [CompilationMode.Partial], which uses Baseline Profiles. 20 | * 21 | * Run this benchmark to see startup measurements and captured system traces for verifying 22 | * the effectiveness of your Baseline Profiles. You can run it directly from Android 23 | * Studio as an instrumentation test, or run all benchmarks for a variant, for example benchmarkRelease, 24 | * with this Gradle task: 25 | * ``` 26 | * ./gradlew :baselineprofile:connectedBenchmarkReleaseAndroidTest 27 | * ``` 28 | * 29 | * You should run the benchmarks on a physical device, not an Android emulator, because the 30 | * emulator doesn't represent real world performance and shares system resources with its host. 31 | * 32 | * For more information, see the [Macrobenchmark documentation](https://d.android.com/macrobenchmark#create-macrobenchmark) 33 | * and the [instrumentation arguments documentation](https://d.android.com/topic/performance/benchmarking/macrobenchmark-instrumentation-args). 34 | **/ 35 | @RunWith(AndroidJUnit4::class) 36 | @LargeTest 37 | class StartupBenchmarks { 38 | 39 | @get:Rule 40 | val rule = MacrobenchmarkRule() 41 | 42 | @Test 43 | fun startupCompilationNone() = 44 | benchmark(CompilationMode.None()) 45 | 46 | @Test 47 | fun startupCompilationBaselineProfiles() = 48 | benchmark(CompilationMode.Partial(BaselineProfileMode.Require)) 49 | 50 | private fun benchmark(compilationMode: CompilationMode) { 51 | // The application id for the running build variant is read from the instrumentation arguments. 52 | rule.measureRepeated( 53 | packageName = InstrumentationRegistry.getArguments().getString("targetAppId") 54 | ?: throw Exception("targetAppId not passed as instrumentation runner arg"), 55 | metrics = listOf(StartupTimingMetric()), 56 | compilationMode = compilationMode, 57 | startupMode = StartupMode.COLD, 58 | iterations = 10, 59 | setupBlock = { 60 | pressHome() 61 | }, 62 | measureBlock = { 63 | startActivityAndWait() 64 | 65 | // TODO Add interactions to wait for when your app is fully drawn. 66 | // The app is fully drawn when Activity.reportFullyDrawn is called. 67 | // For Jetpack Compose, you can use ReportDrawn, ReportDrawnWhen and ReportDrawnAfter 68 | // from the AndroidX Activity library. 69 | 70 | // Check the UiAutomator documentation for more information on how to 71 | // interact with the app. 72 | // https://d.android.com/training/testing/other-components/ui-automator 73 | } 74 | ) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /blocklogger/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /blocklogger/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.ksp) 14 | alias(libs.plugins.hilt) 15 | } 16 | 17 | android { 18 | namespace = "dev.clombardo.dnsnet.blocklogger" 19 | compileSdk = libs.versions.compileSdk.get().toInt() 20 | 21 | defaultConfig { 22 | minSdk = libs.versions.minSdk.get().toInt() 23 | 24 | consumerProguardFiles("consumer-rules.pro") 25 | } 26 | 27 | buildTypes { 28 | create("benchmark") 29 | } 30 | } 31 | 32 | kotlin { 33 | jvmToolchain(libs.versions.java.get().toInt()) 34 | } 35 | 36 | dependencies { 37 | implementation(libs.kotlinx.serialization.json) 38 | 39 | implementation(libs.hilt) 40 | ksp(libs.hilt.compiler) 41 | 42 | implementation(project(":file")) 43 | implementation(project(":log")) 44 | } 45 | -------------------------------------------------------------------------------- /blocklogger/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/blocklogger/consumer-rules.pro -------------------------------------------------------------------------------- /blocklogger/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /blocklogger/src/main/kotlin/dev/clombardo/dnsnet/blocklogger/BlockLogger.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.blocklogger 10 | 11 | import android.content.Context 12 | import dagger.Module 13 | import dagger.Provides 14 | import dagger.hilt.InstallIn 15 | import dagger.hilt.android.qualifiers.ApplicationContext 16 | import dagger.hilt.components.SingletonComponent 17 | import dev.clombardo.dnsnet.file.FileHelper 18 | import dev.clombardo.dnsnet.log.logError 19 | import kotlinx.serialization.ExperimentalSerializationApi 20 | import kotlinx.serialization.Serializable 21 | import kotlinx.serialization.Transient 22 | import kotlinx.serialization.json.Json 23 | import kotlinx.serialization.json.decodeFromStream 24 | import kotlinx.serialization.json.encodeToStream 25 | import javax.inject.Singleton 26 | 27 | @Module 28 | @InstallIn(SingletonComponent::class) 29 | class BlockLoggerModule { 30 | @Provides 31 | @Singleton 32 | fun provideBlockLogger(@ApplicationContext context: Context): BlockLogger { 33 | return BlockLogger.load(context) 34 | } 35 | } 36 | 37 | @Serializable 38 | data class BlockLogger(val connections: MutableMap = HashMap()) { 39 | @Transient 40 | private var onConnection: ((name: String, connection: LoggedConnection) -> Unit)? = null 41 | 42 | fun setOnConnectionListener(listener: ((name: String, connection: LoggedConnection) -> Unit)?) { 43 | onConnection = listener 44 | } 45 | 46 | fun newConnection(name: String, allowed: Boolean) { 47 | val connection = connections[name] 48 | val now = System.currentTimeMillis() 49 | if (connection != null) { 50 | if (connection.allowed != allowed) { 51 | connections.remove(name) 52 | connections[name] = LoggedConnection(allowed, 1, now) 53 | } else { 54 | connection.attempt(now) 55 | } 56 | } else { 57 | connections[name] = LoggedConnection(allowed, 1, now) 58 | } 59 | onConnection?.invoke(name, connections[name]!!) 60 | } 61 | 62 | @OptIn(ExperimentalSerializationApi::class) 63 | fun save(context: Context, name: String = DEFAULT_LOG_FILENAME) { 64 | try { 65 | val outputStream = FileHelper.openWrite(context, name) 66 | json.encodeToStream(this, outputStream) 67 | } catch (e: Exception) { 68 | logError("Failed to write connection history", e) 69 | } 70 | } 71 | 72 | fun clear(context: Context) { 73 | connections.clear() 74 | context.getFileStreamPath(DEFAULT_LOG_FILENAME).delete() 75 | } 76 | 77 | companion object { 78 | private const val DEFAULT_LOG_FILENAME = "connections.json" 79 | 80 | private val json by lazy { 81 | Json { 82 | ignoreUnknownKeys = true 83 | } 84 | } 85 | 86 | @OptIn(ExperimentalSerializationApi::class) 87 | internal fun load(context: Context, name: String = DEFAULT_LOG_FILENAME): BlockLogger { 88 | val inputStream = 89 | FileHelper.openRead(context, name) ?: return BlockLogger() 90 | return try { 91 | json.decodeFromStream(inputStream) 92 | } catch (e: Exception) { 93 | logError("Failed to load connection history", e) 94 | BlockLogger() 95 | } 96 | } 97 | } 98 | } 99 | 100 | @Serializable 101 | data class LoggedConnection( 102 | val allowed: Boolean = true, 103 | var attempts: Long = 0, 104 | var lastAttemptTime: Long = 0, 105 | ) { 106 | fun attempt(now: Long) { 107 | attempts++ 108 | lastAttemptTime = now 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | alias(libs.plugins.android.application) apply false 4 | alias(libs.plugins.android.library) apply false 5 | alias(libs.plugins.android.test) apply false 6 | 7 | alias(libs.plugins.kotlin.android) apply false 8 | alias(libs.plugins.kotlin.compose) apply false 9 | alias(libs.plugins.kotlin.parcelize) apply false 10 | alias(libs.plugins.kotlin.serialization) apply false 11 | 12 | alias(libs.plugins.kotlinx.atomicfu) apply false 13 | 14 | alias(libs.plugins.androidx.baselineprofile) apply false 15 | 16 | alias(libs.plugins.accrescent.bundletool) apply false 17 | 18 | alias(libs.plugins.aboutLibraries) apply false 19 | 20 | alias(libs.plugins.arturbosch.detekt) apply false 21 | 22 | alias(libs.plugins.rust.android.gradle) apply false 23 | 24 | alias(libs.plugins.ksp) apply false 25 | alias(libs.plugins.hilt) apply false 26 | 27 | alias(libs.plugins.gradle.play.publisher) apply false 28 | } 29 | 30 | val versionCode by extra { 59 } 31 | val versionName by extra { "1.3.0" } 32 | -------------------------------------------------------------------------------- /file/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /file/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, version 3. 6 | * 7 | * Contributions shall also be provided under any later versions of the 8 | * GPL. 9 | */ 10 | 11 | plugins { 12 | alias(libs.plugins.android.library) 13 | alias(libs.plugins.kotlin.android) 14 | } 15 | 16 | android { 17 | namespace = "dev.clombardo.dnsnet.file" 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 | 31 | kotlin { 32 | jvmToolchain(libs.versions.java.get().toInt()) 33 | } 34 | 35 | dependencies { 36 | implementation(libs.androidx.core.ktx) 37 | 38 | implementation(project(":log")) 39 | } 40 | -------------------------------------------------------------------------------- /file/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t895/DNSNet/92bae7b84ba7a19ffa9d450621d04fa2d9383c79/file/consumer-rules.pro -------------------------------------------------------------------------------- /file/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /file/src/main/kotlin/dev/clombardo/dnsnet/file/SingleWriterMultipleReaderFile.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.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 | --------------------------------------------------------------------------------