├── .github └── workflows │ ├── build-master.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── scripts ├── add-tun.sh ├── remove-tun.sh └── run-debugger.sh └── source ├── android ├── .gitignore ├── app │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ ├── release.keystore │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── jonforshort │ │ │ └── androidlocalvpn │ │ │ ├── AndroidLocalVpnApp.kt │ │ │ └── ui │ │ │ ├── main │ │ │ ├── ApplicationSettings.kt │ │ │ ├── ControlTab.kt │ │ │ ├── MainActivity.kt │ │ │ ├── MainScreen.kt │ │ │ ├── MainViewModel.kt │ │ │ ├── PolicyTab.kt │ │ │ └── TestTab.kt │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Shape.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── vpn │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── github │ │ └── jonforshort │ │ └── androidlocalvpn │ │ └── vpn │ │ ├── LocalVpnActivity.kt │ │ ├── LocalVpnConfiguration.kt │ │ └── LocalVpnService.kt │ └── rust │ └── vpn │ ├── .gitignore │ ├── .rustfmt.toml │ ├── .vscode │ ├── launch.json │ └── settings.json │ ├── Cargo.toml │ └── src │ ├── jni │ ├── jni_context.rs │ └── mod.rs │ ├── lib.rs │ └── socket_protector │ └── mod.rs ├── core ├── .gitignore ├── .rustfmt.toml ├── Cargo.toml └── src │ ├── error.rs │ ├── lib.rs │ └── vpn │ ├── buffers.rs │ ├── mio_socket.rs │ ├── mod.rs │ ├── processor.rs │ ├── session.rs │ ├── session_info.rs │ ├── smoltcp_socket.rs │ ├── utils.rs │ └── vpn_device.rs └── host ├── .gitignore ├── .rustfmt.toml ├── .vscode ├── launch.json └── settings.json ├── Cargo.toml └── src └── main.rs /.github/workflows/build-master.yml: -------------------------------------------------------------------------------- 1 | name: Build Master Branch 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build-host: 11 | runs-on: ubuntu-latest 12 | defaults: 13 | run: 14 | working-directory: ./source/host 15 | 16 | steps: 17 | - name: Checkout source code 18 | uses: actions/checkout@v3 19 | 20 | - name: Set up Rust 21 | uses: actions-rs/toolchain@v1 22 | with: 23 | toolchain: stable 24 | 25 | - name: Install Rust targets 26 | run: rustup target add x86_64-unknown-linux-gnu 27 | 28 | - name: Build 29 | run: cargo build --target x86_64-unknown-linux-gnu 30 | 31 | build-android: 32 | runs-on: ubuntu-latest 33 | defaults: 34 | run: 35 | working-directory: ./source/android 36 | 37 | steps: 38 | - name: Checkout source code 39 | uses: actions/checkout@v3 40 | 41 | - name: Set up Rust 42 | uses: actions-rs/toolchain@v1 43 | with: 44 | toolchain: stable 45 | 46 | - name: Update Rust 47 | run: rustup update 48 | 49 | - name: Install Rust targets 50 | run: rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android 51 | 52 | - name: Set up Android NDK 53 | run: $ANDROID_SDK_ROOT/tools/bin/sdkmanager --install "ndk;25.2.9519653" 54 | 55 | - name: Set up JDK 56 | uses: actions/setup-java@v3 57 | with: 58 | java-version: '17' 59 | distribution: 'temurin' 60 | cache: gradle 61 | 62 | - name: Grant execute permission for gradlew 63 | run: chmod +x ./gradlew 64 | 65 | - name: Build with Gradle 66 | run: ./gradlew clean build 67 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | publish: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | permissions: 11 | contents: read 12 | packages: write 13 | 14 | defaults: 15 | run: 16 | working-directory: ./source/android 17 | 18 | steps: 19 | - name: Checkout Source Code 20 | uses: actions/checkout@v3 21 | 22 | - name: Set Up Rust 23 | uses: actions-rs/toolchain@v1 24 | with: 25 | toolchain: stable 26 | 27 | - name: Install Rust Targets 28 | run: rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android 29 | 30 | - name: Set Up Android NDK 31 | run: $ANDROID_SDK_ROOT/tools/bin/sdkmanager --install "ndk;25.2.9519653" 32 | 33 | - name: Set Up JDK 34 | uses: actions/setup-java@v3 35 | with: 36 | java-version: '17' 37 | distribution: 'temurin' 38 | cache: gradle 39 | 40 | - name: Build With Gradle 41 | run: ./gradlew build 42 | 43 | - name: Publish Packages 44 | run: ./gradlew :android-local-vpn:publish 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .VSCodeCounter/ 3 | Cargo.lock 4 | target/ 5 | 6 | # Built application files 7 | *.apk 8 | *.aar 9 | *.ap_ 10 | *.aab 11 | 12 | # Files for the ART/Dalvik VM 13 | *.dex 14 | 15 | # Java class files 16 | *.class 17 | 18 | # Generated files 19 | bin/ 20 | gen/ 21 | out/ 22 | # Uncomment the following line in case you need and you don't have the release build type files in your app 23 | # release/ 24 | 25 | # Gradle files 26 | .gradle/ 27 | build/ 28 | 29 | # Local configuration file (sdk path, etc) 30 | local.properties 31 | 32 | # Proguard folder generated by Eclipse 33 | proguard/ 34 | 35 | # Log Files 36 | *.log 37 | 38 | # Android Studio Navigation editor temp files 39 | .navigation/ 40 | 41 | # Android Studio captures folder 42 | captures/ 43 | 44 | # IntelliJ 45 | *.iml 46 | .idea/workspace.xml 47 | .idea/tasks.xml 48 | .idea/gradle.xml 49 | .idea/assetWizardSettings.xml 50 | .idea/dictionaries 51 | .idea/libraries 52 | # Android Studio 3 in .gitignore file. 53 | .idea/caches 54 | .idea/modules.xml 55 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 56 | .idea/navEditor.xml 57 | 58 | # Keystore files 59 | # Uncomment the following lines if you do not want to check your keystore files in. 60 | #*.jks 61 | #*.keystore 62 | 63 | # External native build folder generated in Android Studio 2.2 and later 64 | .externalNativeBuild 65 | .cxx/ 66 | 67 | # Google Services (e.g. APIs or Firebase) 68 | # google-services.json 69 | 70 | # Freeline 71 | freeline.py 72 | freeline/ 73 | freeline_project_description.json 74 | 75 | # fastlane 76 | fastlane/report.xml 77 | fastlane/Preview.html 78 | fastlane/screenshots 79 | fastlane/test_output 80 | fastlane/readme.md 81 | 82 | # Version control 83 | vcs.xml 84 | 85 | # lint 86 | lint/intermediates/ 87 | lint/generated/ 88 | lint/outputs/ 89 | lint/tmp/ 90 | # lint/reports/ 91 | 92 | # Testing iptables backup file 93 | iptables.bak 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Master Branch](https://github.com/JonForShort/android-local-vpn/actions/workflows/build-master.yml/badge.svg)](https://github.com/JonForShort/android-local-vpn/actions/workflows/build-master.yml) 2 | 3 | # Android Local VPN 4 | 5 | This project is a simple [VPN](https://developer.android.com/guide/topics/connectivity/vpn) for Android written in the Rust programming language. It uses [smoltcp](https://github.com/smoltcp-rs/smoltcp) for its TCP/IP stack. 6 | 7 | ## Goals 8 | 9 | * Performant - Performance running local VPN should be comparable to performance without running VPN. 10 | * Small - Library should be less than 2 MB per architecture type. 11 | * Easy To Use - Integration should be simple. 12 | 13 | ## Planned Features 14 | 15 | * Support for protocols IPv4, IPv6, TCP and UDP. 16 | * Allow for monitoring all traffic. 17 | * Allow for blocking traffic by IP address, port number and application. 18 | 19 | ## Integration 20 | 21 | TBD 22 | 23 | ## Contributions 24 | 25 | All contributions are welcome. Please feel free to raise an issue if you have any questions or feature requests. 26 | 27 | ## Code of Conduct 28 | 29 | Contribution to this project is organized under the terms of the [Contributor Covenant](https://www.contributor-covenant.org/). 30 | 31 | ## License 32 | 33 | This project is licensed under the [unlicense](https://unlicense.org/) license. Note that this project has dependencies on several open source projects. It is important that the appropriate attributions be made to satisfy those licensing requirements. 34 | -------------------------------------------------------------------------------- /scripts/add-tun.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | 3 | # current directory 4 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 5 | 6 | main() { 7 | pushd ${SCRIPT_DIR} 8 | 9 | # create tun device and change state to 'up'. 10 | sudo ip tuntap add name tun0 mode tun user $USER 11 | sudo ip link set tun0 up 12 | 13 | # save routing table before modifying it. 14 | sudo iptables-save > iptables.bak 15 | 16 | # route everything through tun device. 17 | sudo ip route add 128.0.0.0/1 dev tun0 18 | sudo ip route add 0.0.0.0/1 dev tun0 19 | 20 | popd 21 | } 22 | 23 | main 24 | -------------------------------------------------------------------------------- /scripts/remove-tun.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | 3 | # current directory 4 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 5 | 6 | main() { 7 | pushd ${SCRIPT_DIR} 8 | 9 | # delete tun device. 10 | sudo ip link delete tun0 11 | 12 | # restore ip tables. 13 | sudo iptables-restore < iptables.bak 14 | 15 | popd 16 | } 17 | 18 | main 19 | -------------------------------------------------------------------------------- /scripts/run-debugger.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | NDK_VERSION=21.1.6352462 4 | PACKAGE_NAME=com.github.jonforshort.androidlocalvpn 5 | PORT=9999 6 | 7 | adb root 8 | 9 | adb forward tcp:${PORT} tcp:${PORT} 10 | 11 | adb push ${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}/toolchains/llvm/prebuilt/linux-x86_64/lib64/clang/9.0.8/lib/linux/x86_64/lldb-server /data/local/tmp/lldb-server 12 | 13 | adb shell chmod +x /data/local/tmp/lldb-server 14 | 15 | adb shell ps -A | grep ${PACKAGE_NAME} 16 | 17 | echo "running lldb server" 18 | 19 | adb shell /data/local/tmp/lldb-server platform --listen "*:${PORT}" --server 20 | 21 | echo "done" 22 | -------------------------------------------------------------------------------- /source/android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | -------------------------------------------------------------------------------- /source/android/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /source/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | namespace 'com.github.jonforshort.androidlocalvpn' 8 | 9 | compileSdk 34 10 | 11 | defaultConfig { 12 | applicationId "com.github.jonforshort.androidlocalvpn" 13 | minSdk 26 14 | targetSdk 34 15 | versionCode 1 16 | versionName "1.0" 17 | 18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 19 | vectorDrawables { 20 | useSupportLibrary true 21 | } 22 | } 23 | 24 | signingConfigs { 25 | release { 26 | storeFile file("${project.rootDir.absolutePath}/app/release.keystore") 27 | storePassword "localvpn" 28 | keyAlias "localvpn" 29 | keyPassword "localvpn" 30 | } 31 | } 32 | 33 | buildTypes { 34 | release { 35 | minifyEnabled true 36 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 37 | signingConfig signingConfigs.release 38 | } 39 | } 40 | 41 | compileOptions { 42 | sourceCompatibility JavaVersion.VERSION_17 43 | targetCompatibility JavaVersion.VERSION_17 44 | } 45 | 46 | kotlinOptions { 47 | jvmTarget = JavaVersion.VERSION_17.toString() 48 | } 49 | 50 | buildFeatures { 51 | compose true 52 | } 53 | 54 | composeOptions { 55 | kotlinCompilerExtensionVersion '1.5.7' 56 | } 57 | 58 | packagingOptions { 59 | resources { 60 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 61 | } 62 | } 63 | } 64 | 65 | dependencies { 66 | implementation project(':android-local-vpn') 67 | 68 | implementation 'androidx.core:core-ktx:1.12.0' 69 | implementation 'androidx.appcompat:appcompat:1.6.1' 70 | implementation 'com.google.android.material:material:1.11.0' 71 | implementation "androidx.compose.ui:ui:1.5.4" 72 | implementation "androidx.compose.material:material:1.5.4" 73 | implementation "androidx.compose.ui:ui-tooling-preview:1.5.4" 74 | implementation "androidx.compose.runtime:runtime-livedata:1.5.4" 75 | implementation "com.google.accompanist:accompanist-pager:0.25.0" 76 | implementation "com.google.accompanist:accompanist-pager-indicators:0.25.0" 77 | implementation 'org.jsoup:jsoup:1.15.2' 78 | implementation 'com.jakewharton.timber:timber:4.7.1' 79 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2' 80 | implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2" 81 | implementation 'androidx.activity:activity-compose:1.8.2' 82 | implementation 'dnsjava:dnsjava:3.5.1' 83 | 84 | debugImplementation "androidx.compose.ui:ui-tooling:1.5.4" 85 | } -------------------------------------------------------------------------------- /source/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | # Required since not available on Android 24 | -dontwarn com.sun.jna.Library 25 | -dontwarn com.sun.jna.Memory 26 | -dontwarn com.sun.jna.Native 27 | -dontwarn com.sun.jna.Pointer 28 | -dontwarn com.sun.jna.Structure$ByReference 29 | -dontwarn com.sun.jna.Structure$FieldOrder 30 | -dontwarn com.sun.jna.Structure 31 | -dontwarn com.sun.jna.WString 32 | -dontwarn com.sun.jna.platform.win32.Win32Exception 33 | -dontwarn com.sun.jna.ptr.IntByReference 34 | -dontwarn com.sun.jna.win32.W32APIOptions 35 | -dontwarn javax.annotation.Nullable 36 | -dontwarn javax.naming.NamingException 37 | -dontwarn javax.naming.directory.DirContext 38 | -dontwarn javax.naming.directory.InitialDirContext 39 | -dontwarn lombok.Generated 40 | -dontwarn org.slf4j.impl.StaticLoggerBinder 41 | -dontwarn sun.net.spi.nameservice.NameServiceDescriptor 42 | -------------------------------------------------------------------------------- /source/android/app/release.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonForShort/android-local-vpn/902126bd816471877bb9d8e9bbcb17978fb8ccff/source/android/app/release.keystore -------------------------------------------------------------------------------- /source/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /source/android/app/src/main/java/com/github/jonforshort/androidlocalvpn/AndroidLocalVpnApp.kt: -------------------------------------------------------------------------------- 1 | // 2 | // This is free and unencumbered software released into the public domain. 3 | // 4 | // Anyone is free to copy, modify, publish, use, compile, sell, or 5 | // distribute this software, either in source code form or as a compiled 6 | // binary, for any purpose, commercial or non-commercial, and by any 7 | // means. 8 | // 9 | // In jurisdictions that recognize copyright laws, the author or authors 10 | // of this software dedicate any and all copyright interest in the 11 | // software to the public domain. We make this dedication for the benefit 12 | // of the public at large and to the detriment of our heirs and 13 | // successors. We intend this dedication to be an overt act of 14 | // relinquishment in perpetuity of all present and future rights to this 15 | // software under copyright law. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 21 | // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 22 | // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | // OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | // For more information, please refer to 26 | // 27 | package com.github.jonforshort.androidlocalvpn 28 | 29 | import android.app.Application 30 | import timber.log.Timber 31 | import timber.log.Timber.DebugTree 32 | 33 | class AndroidLocalVpnApp : Application() { 34 | 35 | override fun onCreate() { 36 | super.onCreate() 37 | 38 | if (BuildConfig.DEBUG) { 39 | Timber.plant(DebugTree()) 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /source/android/app/src/main/java/com/github/jonforshort/androidlocalvpn/ui/main/ApplicationSettings.kt: -------------------------------------------------------------------------------- 1 | // 2 | // This is free and unencumbered software released into the public domain. 3 | // 4 | // Anyone is free to copy, modify, publish, use, compile, sell, or 5 | // distribute this software, either in source code form or as a compiled 6 | // binary, for any purpose, commercial or non-commercial, and by any 7 | // means. 8 | // 9 | // In jurisdictions that recognize copyright laws, the author or authors 10 | // of this software dedicate any and all copyright interest in the 11 | // software to the public domain. We make this dedication for the benefit 12 | // of the public at large and to the detriment of our heirs and 13 | // successors. We intend this dedication to be an overt act of 14 | // relinquishment in perpetuity of all present and future rights to this 15 | // software under copyright law. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 21 | // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 22 | // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | // OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | // For more information, please refer to 26 | // 27 | package com.github.jonforshort.androidlocalvpn.ui.main 28 | 29 | import android.graphics.drawable.Drawable 30 | 31 | internal data class ApplicationSettings( 32 | val appName: String, 33 | val appIcon: Drawable, 34 | val packageName: String, 35 | val policy: VpnPolicy 36 | ) 37 | 38 | internal enum class VpnPolicy { 39 | DEFAULT, 40 | ALLOW, 41 | DISALLOW 42 | } -------------------------------------------------------------------------------- /source/android/app/src/main/java/com/github/jonforshort/androidlocalvpn/ui/main/ControlTab.kt: -------------------------------------------------------------------------------- 1 | // 2 | // This is free and unencumbered software released into the public domain. 3 | // 4 | // Anyone is free to copy, modify, publish, use, compile, sell, or 5 | // distribute this software, either in source code form or as a compiled 6 | // binary, for any purpose, commercial or non-commercial, and by any 7 | // means. 8 | // 9 | // In jurisdictions that recognize copyright laws, the author or authors 10 | // of this software dedicate any and all copyright interest in the 11 | // software to the public domain. We make this dedication for the benefit 12 | // of the public at large and to the detriment of our heirs and 13 | // successors. We intend this dedication to be an overt act of 14 | // relinquishment in perpetuity of all present and future rights to this 15 | // software under copyright law. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 21 | // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 22 | // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | // OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | // For more information, please refer to 26 | // 27 | package com.github.jonforshort.androidlocalvpn.ui.main 28 | 29 | import androidx.compose.foundation.layout.* 30 | import androidx.compose.material.MaterialTheme 31 | import androidx.compose.material.Switch 32 | import androidx.compose.material.Text 33 | import androidx.compose.runtime.Composable 34 | import androidx.compose.ui.Alignment 35 | import androidx.compose.ui.Modifier 36 | import androidx.compose.ui.tooling.preview.Preview 37 | import com.github.jonforshort.androidlocalvpn.ui.theme.AndroidLocalVpnTheme 38 | 39 | @Composable 40 | internal fun ControlTab( 41 | isVpnEnabled: Boolean, 42 | onVpnEnabledChanged: (Boolean) -> Unit, 43 | modifier: Modifier = Modifier 44 | ) { 45 | Column(modifier = modifier) { 46 | EnableVpnToggle( 47 | isVpnEnabled, onVpnEnabledChanged 48 | ) 49 | } 50 | } 51 | 52 | @Composable 53 | private fun EnableVpnToggle( 54 | isVpnEnabled: Boolean, 55 | onVpnEnabledChanged: (Boolean) -> Unit 56 | ) { 57 | Row( 58 | modifier = Modifier 59 | .height(IntrinsicSize.Max) 60 | .fillMaxSize(), 61 | verticalAlignment = Alignment.CenterVertically, 62 | horizontalArrangement = Arrangement.SpaceBetween 63 | ) { 64 | Text( 65 | text = "Enable VPN", 66 | style = MaterialTheme.typography.h4 67 | ) 68 | Switch( 69 | checked = isVpnEnabled, 70 | onCheckedChange = { onVpnEnabledChanged(it) }, 71 | ) 72 | } 73 | } 74 | 75 | @Preview 76 | @Composable 77 | fun ControlsTabPreview() { 78 | AndroidLocalVpnTheme { 79 | ControlTab( 80 | isVpnEnabled = false, 81 | onVpnEnabledChanged = {}, 82 | ) 83 | } 84 | } -------------------------------------------------------------------------------- /source/android/app/src/main/java/com/github/jonforshort/androidlocalvpn/ui/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | // 2 | // This is free and unencumbered software released into the public domain. 3 | // 4 | // Anyone is free to copy, modify, publish, use, compile, sell, or 5 | // distribute this software, either in source code form or as a compiled 6 | // binary, for any purpose, commercial or non-commercial, and by any 7 | // means. 8 | // 9 | // In jurisdictions that recognize copyright laws, the author or authors 10 | // of this software dedicate any and all copyright interest in the 11 | // software to the public domain. We make this dedication for the benefit 12 | // of the public at large and to the detriment of our heirs and 13 | // successors. We intend this dedication to be an overt act of 14 | // relinquishment in perpetuity of all present and future rights to this 15 | // software under copyright law. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 21 | // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 22 | // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | // OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | // For more information, please refer to 26 | // 27 | package com.github.jonforshort.androidlocalvpn.ui.main 28 | 29 | import android.os.Bundle 30 | import androidx.activity.compose.setContent 31 | import androidx.compose.runtime.livedata.observeAsState 32 | import androidx.lifecycle.MutableLiveData 33 | import com.github.jonforshort.androidlocalvpn.vpn.LocalVpnActivity 34 | import com.github.jonforshort.androidlocalvpn.vpn.LocalVpnConfiguration 35 | import com.github.jonforshort.androidlocalvpn.vpn.PackageName 36 | 37 | class MainActivity : LocalVpnActivity() { 38 | 39 | private val vpnState = MutableLiveData(false) 40 | 41 | private lateinit var mainViewModel: MainViewModel 42 | 43 | override fun onCreate(savedInstanceState: Bundle?) { 44 | super.onCreate(savedInstanceState) 45 | 46 | mainViewModel = MainViewModel(application) 47 | 48 | setContent { 49 | MainScreen( 50 | mainViewModel = mainViewModel, 51 | isVpnEnabled = vpnState.observeAsState(), 52 | onVpnEnabledChanged = ::onVpnStateChanged 53 | ) 54 | } 55 | 56 | vpnState.postValue(isVpnRunning()) 57 | } 58 | 59 | override fun onResume() { 60 | super.onResume() 61 | mainViewModel.refresh() 62 | } 63 | 64 | private fun onVpnStateChanged(vpnEnabled: Boolean) = 65 | if (vpnEnabled) { 66 | val configuration = buildConfiguration() 67 | startVpn(configuration) 68 | } else { 69 | stopVpn() 70 | } 71 | 72 | private fun buildConfiguration(): LocalVpnConfiguration { 73 | val allowedApps = mutableListOf() 74 | val disallowedApps = mutableListOf() 75 | 76 | mainViewModel.applicationSettings.value.forEach { 77 | when (it.policy) { 78 | VpnPolicy.ALLOW -> allowedApps.add(PackageName(it.packageName)) 79 | VpnPolicy.DISALLOW -> disallowedApps.add(PackageName(it.packageName)) 80 | else -> {} 81 | } 82 | } 83 | 84 | return LocalVpnConfiguration(allowedApps, disallowedApps) 85 | } 86 | 87 | override fun onVpnStarted() = vpnState.postValue(true) 88 | 89 | override fun onVpnStopped() = vpnState.postValue(false) 90 | } 91 | 92 | -------------------------------------------------------------------------------- /source/android/app/src/main/java/com/github/jonforshort/androidlocalvpn/ui/main/MainScreen.kt: -------------------------------------------------------------------------------- 1 | // 2 | // This is free and unencumbered software released into the public domain. 3 | // 4 | // Anyone is free to copy, modify, publish, use, compile, sell, or 5 | // distribute this software, either in source code form or as a compiled 6 | // binary, for any purpose, commercial or non-commercial, and by any 7 | // means. 8 | // 9 | // In jurisdictions that recognize copyright laws, the author or authors 10 | // of this software dedicate any and all copyright interest in the 11 | // software to the public domain. We make this dedication for the benefit 12 | // of the public at large and to the detriment of our heirs and 13 | // successors. We intend this dedication to be an overt act of 14 | // relinquishment in perpetuity of all present and future rights to this 15 | // software under copyright law. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 21 | // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 22 | // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | // OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | // For more information, please refer to 26 | // 27 | package com.github.jonforshort.androidlocalvpn.ui.main 28 | 29 | import androidx.compose.foundation.layout.* 30 | import androidx.compose.material.* 31 | import androidx.compose.material.icons.Icons 32 | import androidx.compose.material.icons.filled.Build 33 | import androidx.compose.material.icons.filled.Send 34 | import androidx.compose.material.icons.filled.Settings 35 | import androidx.compose.runtime.* 36 | import androidx.compose.ui.Alignment 37 | import androidx.compose.ui.Modifier 38 | import androidx.compose.ui.graphics.vector.ImageVector 39 | import androidx.compose.ui.tooling.preview.Preview 40 | import androidx.compose.ui.unit.dp 41 | import androidx.lifecycle.viewmodel.compose.viewModel 42 | import com.github.jonforshort.androidlocalvpn.ui.theme.AndroidLocalVpnTheme 43 | import com.google.accompanist.pager.ExperimentalPagerApi 44 | import com.google.accompanist.pager.HorizontalPager 45 | import com.google.accompanist.pager.pagerTabIndicatorOffset 46 | import com.google.accompanist.pager.rememberPagerState 47 | import kotlinx.coroutines.launch 48 | import org.xbill.DNS.* 49 | 50 | @Composable 51 | internal fun MainScreen( 52 | mainViewModel: MainViewModel = viewModel(), 53 | isVpnEnabled: State, 54 | onVpnEnabledChanged: (Boolean) -> Unit 55 | ) { 56 | val tabs = listOf( 57 | controlTab(isVpnEnabled, onVpnEnabledChanged), 58 | policyTab(mainViewModel), 59 | testTab() 60 | ) 61 | 62 | AndroidLocalVpnTheme { 63 | Surface(color = MaterialTheme.colors.background) { 64 | MainView( 65 | tabData = tabs.map { it.tabName.uppercase() to it.tabIcon }, 66 | onTabDisplayed = { tabs[it].tab() } 67 | ) 68 | } 69 | } 70 | } 71 | 72 | @Composable 73 | private fun controlTab( 74 | isVpnEnabled: State, 75 | onVpnEnabledChanged: (Boolean) -> Unit 76 | ) = MainScreenTab( 77 | tabName = "Control", 78 | tabIcon = Icons.Filled.Settings, 79 | tab = { 80 | ControlTab( 81 | isVpnEnabled = isVpnEnabled.value ?: false, 82 | onVpnEnabledChanged = onVpnEnabledChanged, 83 | modifier = Modifier 84 | .padding(20.dp) 85 | .fillMaxWidth() 86 | ) 87 | } 88 | ) 89 | 90 | @Composable 91 | private fun policyTab( 92 | mainViewModel: MainViewModel 93 | ) = MainScreenTab( 94 | tabName = "Policy", 95 | tabIcon = Icons.Filled.Build, 96 | tab = { 97 | 98 | val selectedVpnPolicy = remember { 99 | mutableStateOf(mainViewModel.vpnPolicy.value) 100 | } 101 | 102 | val selectedApplicationsSettings = remember { 103 | val lastSavedVpnPolicy = mainViewModel.vpnPolicy.value 104 | mutableStateListOf( 105 | *mainViewModel.applicationSettings.value 106 | .filter { 107 | it.policy == lastSavedVpnPolicy && lastSavedVpnPolicy != VpnPolicy.DEFAULT 108 | } 109 | .toTypedArray() 110 | ) 111 | } 112 | 113 | PolicyTab( 114 | selectedVpnPolicy = selectedVpnPolicy, 115 | selectedApplicationsSettings = selectedApplicationsSettings, 116 | applicationsSettings = mainViewModel.applicationSettings.collectAsState(), 117 | onVpnPolicyTapped = { vpnPolicy -> 118 | selectedVpnPolicy.value = vpnPolicy 119 | }, 120 | onSaveApplicationSettings = { 121 | mainViewModel.adjustApplicationSettings( 122 | selectedVpnPolicy.value, 123 | selectedApplicationsSettings 124 | ) 125 | }, 126 | onResetApplicationSettings = { 127 | mainViewModel.reset() 128 | }, 129 | onApplicationSettingsTapped = { applicationSettings -> 130 | if (selectedApplicationsSettings.contains(applicationSettings)) { 131 | selectedApplicationsSettings.remove(applicationSettings) 132 | } else { 133 | selectedApplicationsSettings.add(applicationSettings) 134 | } 135 | }, 136 | modifier = Modifier 137 | .padding(20.dp) 138 | .fillMaxWidth() 139 | ) 140 | } 141 | ) 142 | 143 | @Composable 144 | private fun testTab() = MainScreenTab( 145 | tabName = "Test", 146 | tabIcon = Icons.Filled.Send, 147 | tab = { 148 | TestTab( 149 | modifier = Modifier 150 | .padding(20.dp) 151 | .fillMaxWidth() 152 | ) 153 | } 154 | ) 155 | 156 | private data class MainScreenTab( 157 | val tabName: String, 158 | val tabIcon: ImageVector, 159 | val tab: @Composable () -> Unit 160 | ) 161 | 162 | @OptIn(ExperimentalPagerApi::class) 163 | @Composable 164 | private fun MainView( 165 | tabData: List> = emptyList(), 166 | onTabDisplayed: @Composable (index: Int) -> Unit = {} 167 | ) { 168 | val pagerState = rememberPagerState() 169 | val tabIndex = pagerState.currentPage 170 | val coroutineScope = rememberCoroutineScope() 171 | Column { 172 | TabRow( 173 | selectedTabIndex = tabIndex, 174 | indicator = { tabPositions -> 175 | TabRowDefaults.Indicator( 176 | Modifier.pagerTabIndicatorOffset(pagerState, tabPositions) 177 | ) 178 | } 179 | ) { 180 | tabData.forEachIndexed { index, pair -> 181 | Tab(selected = tabIndex == index, 182 | onClick = { 183 | coroutineScope.launch { 184 | pagerState.animateScrollToPage(index) 185 | } 186 | }, 187 | text = { 188 | Text(text = pair.first) 189 | }, 190 | icon = { 191 | Icon(imageVector = pair.second, contentDescription = null) 192 | }) 193 | } 194 | } 195 | HorizontalPager( 196 | state = pagerState, 197 | modifier = Modifier.weight(1f), 198 | count = tabData.size 199 | ) { index -> 200 | Column( 201 | modifier = Modifier.fillMaxSize(), 202 | verticalArrangement = Arrangement.Top, 203 | horizontalAlignment = Alignment.Start 204 | ) { 205 | onTabDisplayed(index) 206 | } 207 | } 208 | } 209 | } 210 | 211 | @Preview(showBackground = true) 212 | @Composable 213 | private fun DefaultPreview() { 214 | AndroidLocalVpnTheme { 215 | MainView() 216 | } 217 | } -------------------------------------------------------------------------------- /source/android/app/src/main/java/com/github/jonforshort/androidlocalvpn/ui/main/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | // 2 | // This is free and unencumbered software released into the public domain. 3 | // 4 | // Anyone is free to copy, modify, publish, use, compile, sell, or 5 | // distribute this software, either in source code form or as a compiled 6 | // binary, for any purpose, commercial or non-commercial, and by any 7 | // means. 8 | // 9 | // In jurisdictions that recognize copyright laws, the author or authors 10 | // of this software dedicate any and all copyright interest in the 11 | // software to the public domain. We make this dedication for the benefit 12 | // of the public at large and to the detriment of our heirs and 13 | // successors. We intend this dedication to be an overt act of 14 | // relinquishment in perpetuity of all present and future rights to this 15 | // software under copyright law. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 21 | // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 22 | // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | // OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | // For more information, please refer to 26 | // 27 | package com.github.jonforshort.androidlocalvpn.ui.main 28 | 29 | import android.app.Application 30 | import android.content.Context 31 | import android.content.pm.PackageManager 32 | import android.content.pm.PackageManager.ApplicationInfoFlags 33 | import android.os.Build.VERSION 34 | import android.os.Build.VERSION_CODES 35 | import androidx.lifecycle.AndroidViewModel 36 | import kotlinx.coroutines.flow.MutableStateFlow 37 | import kotlinx.coroutines.flow.StateFlow 38 | import kotlinx.coroutines.flow.asStateFlow 39 | 40 | internal class MainViewModel(application: Application) : AndroidViewModel(application) { 41 | 42 | private val applicationSettingsStore = 43 | application.getSharedPreferences( 44 | SHARED_PREFERENCES_APPLICATION_SETTINGS_KEY, 45 | Context.MODE_PRIVATE 46 | ) 47 | 48 | private val vpnPolicySettingsStore = 49 | application.getSharedPreferences(SHARED_PREFERENCES_VPN_POLICY_KEY, Context.MODE_PRIVATE) 50 | 51 | private val _applicationsSettingsState = 52 | MutableStateFlow(emptyList()) 53 | 54 | val applicationSettings: StateFlow> 55 | get() = _applicationsSettingsState.asStateFlow() 56 | 57 | private val _vpnPolicy = 58 | MutableStateFlow(VpnPolicy.DEFAULT) 59 | 60 | val vpnPolicy: StateFlow 61 | get() = _vpnPolicy.asStateFlow() 62 | 63 | fun refresh() { 64 | _applicationsSettingsState.value = getInstalledApplications() 65 | _vpnPolicy.value = vpnPolicySettingsStore.getString(SHARED_PREFERENCES_VPN_POLICY_KEY, null) 66 | ?.let { VpnPolicy.valueOf(it) } 67 | ?: VpnPolicy.DEFAULT 68 | } 69 | 70 | fun reset() { 71 | applicationSettingsStore.edit().clear().apply() 72 | vpnPolicySettingsStore.edit().clear().apply() 73 | 74 | refresh() 75 | } 76 | 77 | private fun getInstalledApplications() = mutableListOf().apply { 78 | val packageManager = getApplication() 79 | .applicationContext 80 | .packageManager 81 | 82 | val installedApplications = packageManager.getInstalledApplicationsCompat() 83 | 84 | installedApplications.forEach { installedApplication -> 85 | add( 86 | ApplicationSettings( 87 | packageName = installedApplication.packageName, 88 | appIcon = installedApplication.loadIcon(packageManager), 89 | appName = installedApplication.loadLabel(packageManager).toString(), 90 | policy = applicationSettingsStore.getString( 91 | installedApplication.packageName, 92 | VpnPolicy.DEFAULT.name 93 | )!!.let { 94 | VpnPolicy.valueOf(it) 95 | } 96 | ) 97 | ) 98 | } 99 | } 100 | 101 | fun adjustApplicationSettings( 102 | vpnPolicy: VpnPolicy, 103 | applicationSettingsList: List 104 | ) { 105 | applicationSettingsStore.edit().apply { 106 | applicationSettingsList.forEach { 107 | putString(it.packageName, vpnPolicy.name) 108 | } 109 | }.apply() 110 | 111 | vpnPolicySettingsStore.edit() 112 | .putString(SHARED_PREFERENCES_VPN_POLICY_KEY, vpnPolicy.name) 113 | .apply() 114 | 115 | refresh() 116 | } 117 | 118 | companion object { 119 | private const val SHARED_PREFERENCES_VPN_POLICY_KEY = "VpnPolicy" 120 | private const val SHARED_PREFERENCES_APPLICATION_SETTINGS_KEY = "ApplicationSettings" 121 | } 122 | } 123 | 124 | private fun PackageManager.getInstalledApplicationsCompat() = when { 125 | VERSION.SDK_INT >= VERSION_CODES.TIRAMISU -> getInstalledApplications(ApplicationInfoFlags.of(0L)) 126 | else -> @Suppress("DEPRECATION") getInstalledApplications(PackageManager.GET_META_DATA) 127 | } 128 | -------------------------------------------------------------------------------- /source/android/app/src/main/java/com/github/jonforshort/androidlocalvpn/ui/main/PolicyTab.kt: -------------------------------------------------------------------------------- 1 | // 2 | // This is free and unencumbered software released into the public domain. 3 | // 4 | // Anyone is free to copy, modify, publish, use, compile, sell, or 5 | // distribute this software, either in source code form or as a compiled 6 | // binary, for any purpose, commercial or non-commercial, and by any 7 | // means. 8 | // 9 | // In jurisdictions that recognize copyright laws, the author or authors 10 | // of this software dedicate any and all copyright interest in the 11 | // software to the public domain. We make this dedication for the benefit 12 | // of the public at large and to the detriment of our heirs and 13 | // successors. We intend this dedication to be an overt act of 14 | // relinquishment in perpetuity of all present and future rights to this 15 | // software under copyright law. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 21 | // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 22 | // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | // OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | // For more information, please refer to 26 | // 27 | package com.github.jonforshort.androidlocalvpn.ui.main 28 | 29 | import androidx.appcompat.content.res.AppCompatResources 30 | import androidx.compose.foundation.Image 31 | import androidx.compose.foundation.layout.* 32 | import androidx.compose.foundation.lazy.LazyColumn 33 | import androidx.compose.material.* 34 | import androidx.compose.material.icons.Icons 35 | import androidx.compose.material.icons.filled.ArrowDropDown 36 | import androidx.compose.material.icons.filled.Delete 37 | import androidx.compose.material.icons.filled.Star 38 | import androidx.compose.runtime.* 39 | import androidx.compose.ui.Alignment 40 | import androidx.compose.ui.Modifier 41 | import androidx.compose.ui.graphics.asImageBitmap 42 | import androidx.compose.ui.platform.LocalContext 43 | import androidx.compose.ui.state.ToggleableState 44 | import androidx.compose.ui.text.style.TextAlign 45 | import androidx.compose.ui.text.style.TextOverflow 46 | import androidx.compose.ui.tooling.preview.Preview 47 | import androidx.compose.ui.unit.dp 48 | import androidx.core.graphics.drawable.toBitmap 49 | import com.github.jonforshort.androidlocalvpn.R 50 | import com.github.jonforshort.androidlocalvpn.ui.theme.AndroidLocalVpnTheme 51 | import kotlinx.coroutines.flow.MutableStateFlow 52 | import java.util.* 53 | 54 | @Composable 55 | internal fun PolicyTab( 56 | selectedApplicationsSettings: List, 57 | selectedVpnPolicy: State, 58 | applicationsSettings: State>, 59 | onVpnPolicyTapped: (VpnPolicy) -> Unit, 60 | onApplicationSettingsTapped: (ApplicationSettings) -> Unit, 61 | onSaveApplicationSettings: () -> Unit, 62 | onResetApplicationSettings: () -> Unit, 63 | modifier: Modifier = Modifier 64 | ) { 65 | val items = listOf( 66 | VpnPolicy.DEFAULT.capitalizedName(), 67 | VpnPolicy.ALLOW.capitalizedName(), 68 | VpnPolicy.DISALLOW.capitalizedName(), 69 | ) 70 | val isDropDownMenuExpanded = remember { mutableStateOf(false) } 71 | 72 | Column(modifier) { 73 | 74 | Box( 75 | modifier = Modifier 76 | .fillMaxWidth() 77 | .padding(horizontal = 10.dp) 78 | ) { 79 | AllowOrDisallowApplicationsDropDownMenu( 80 | selectedItem = selectedVpnPolicy.value.capitalizedName(), 81 | items = items, 82 | isExpanded = isDropDownMenuExpanded.value, 83 | onDismissRequest = { 84 | isDropDownMenuExpanded.value = false 85 | }, 86 | onItemSelected = { 87 | onVpnPolicyTapped(VpnPolicy.valueOf(it.uppercase())) 88 | }, 89 | onDropDownMenuClicked = { 90 | isDropDownMenuExpanded.value = !isDropDownMenuExpanded.value 91 | }) 92 | } 93 | 94 | Box( 95 | modifier = Modifier 96 | .fillMaxWidth() 97 | .padding(20.dp) 98 | .weight(1f) 99 | ) { 100 | ApplicationSettings( 101 | selectedVpnPolicy = selectedVpnPolicy, 102 | selectedApplicationsSettings = selectedApplicationsSettings, 103 | applicationsSettings = applicationsSettings, 104 | onApplicationSettingsTapped = onApplicationSettingsTapped, 105 | modifier = Modifier.matchParentSize() 106 | ) 107 | } 108 | 109 | Row( 110 | verticalAlignment = Alignment.CenterVertically, 111 | horizontalArrangement = Arrangement.spacedBy(10.dp), 112 | ) { 113 | OutlinedButton( 114 | onClick = { 115 | onSaveApplicationSettings() 116 | }, 117 | modifier = Modifier.weight(1f) 118 | ) { 119 | Image( 120 | imageVector = Icons.Filled.Star, 121 | contentDescription = "Save user selection" 122 | ) 123 | Text( 124 | text = "Save" 125 | ) 126 | } 127 | 128 | OutlinedButton( 129 | onClick = onResetApplicationSettings, 130 | modifier = Modifier.weight(1f) 131 | ) { 132 | Image( 133 | imageVector = Icons.Filled.Delete, 134 | contentDescription = "Reset user selection" 135 | ) 136 | Text( 137 | text = "Reset" 138 | ) 139 | } 140 | } 141 | } 142 | } 143 | 144 | private fun VpnPolicy.capitalizedName() = 145 | name.lowercase().replaceFirstChar { it.titlecase(Locale.ROOT) } 146 | 147 | @Composable 148 | private fun AllowOrDisallowApplicationsDropDownMenu( 149 | selectedItem: String, 150 | items: List, 151 | isExpanded: Boolean = false, 152 | onDismissRequest: () -> Unit, 153 | onItemSelected: (String) -> Unit, 154 | onDropDownMenuClicked: () -> Unit 155 | ) { 156 | Box(contentAlignment = Alignment.CenterStart) { 157 | OutlinedButton( 158 | modifier = Modifier.fillMaxWidth(), 159 | onClick = onDropDownMenuClicked, 160 | ) { 161 | Row( 162 | horizontalArrangement = Arrangement.Start, 163 | verticalAlignment = Alignment.CenterVertically 164 | ) { 165 | Image( 166 | imageVector = Icons.Filled.ArrowDropDown, 167 | contentDescription = "Drop down item" 168 | ) 169 | Text( 170 | text = selectedItem, 171 | textAlign = TextAlign.Start 172 | ) 173 | } 174 | } 175 | } 176 | 177 | DropdownMenu( 178 | expanded = isExpanded, 179 | onDismissRequest = onDismissRequest, 180 | modifier = Modifier 181 | .padding(horizontal = 10.dp), 182 | ) { 183 | items.forEach { 184 | DropdownMenuItem( 185 | onClick = { 186 | onItemSelected(it) 187 | onDismissRequest() 188 | }, 189 | modifier = Modifier 190 | .fillMaxWidth() 191 | .padding(horizontal = 10.dp), 192 | ) { 193 | Text( 194 | text = it, 195 | textAlign = TextAlign.Start 196 | ) 197 | } 198 | } 199 | } 200 | } 201 | 202 | @Composable 203 | private fun ApplicationSettings( 204 | selectedVpnPolicy: State, 205 | selectedApplicationsSettings: List, 206 | applicationsSettings: State>, 207 | onApplicationSettingsTapped: (ApplicationSettings) -> Unit, 208 | modifier: Modifier = Modifier 209 | ) { 210 | LazyColumn(modifier) { 211 | applicationsSettings.value.forEach { applicationSetting -> 212 | item(key = applicationSetting.packageName) { 213 | 214 | Box( 215 | modifier = Modifier 216 | .fillMaxSize() 217 | .padding(10.dp) 218 | ) { 219 | Row( 220 | modifier = Modifier 221 | .fillMaxSize() 222 | .padding(end = 10.dp), 223 | verticalAlignment = Alignment.CenterVertically 224 | ) { 225 | Image( 226 | modifier = Modifier.height(40.dp), 227 | bitmap = applicationSetting.appIcon.toBitmap().asImageBitmap(), 228 | contentDescription = "Application icon" 229 | ) 230 | 231 | Text( 232 | text = applicationSetting.appName, 233 | modifier = Modifier 234 | .fillMaxWidth() 235 | .padding(start = 10.dp, end = 40.dp), 236 | maxLines = 1, 237 | overflow = TextOverflow.Ellipsis 238 | ) 239 | 240 | Spacer( 241 | modifier = Modifier 242 | .fillMaxSize() 243 | .weight(1f) 244 | ) 245 | 246 | val triState = when { 247 | selectedVpnPolicy.value == VpnPolicy.DEFAULT -> ToggleableState.Indeterminate 248 | selectedApplicationsSettings.contains(applicationSetting) -> ToggleableState.On 249 | else -> ToggleableState.Off 250 | } 251 | 252 | TriStateCheckbox( 253 | enabled = selectedVpnPolicy.value != VpnPolicy.DEFAULT, 254 | state = triState, 255 | onClick = { 256 | onApplicationSettingsTapped(applicationSetting) 257 | }) 258 | } 259 | } 260 | } 261 | } 262 | } 263 | } 264 | 265 | @Preview 266 | @Composable 267 | fun PolicyTabPreview() { 268 | val context = LocalContext.current 269 | 270 | val selectedVpnPolicy = MutableStateFlow( 271 | VpnPolicy.ALLOW 272 | ).collectAsState() 273 | 274 | val applicationSettings = MutableStateFlow( 275 | listOf( 276 | ApplicationSettings( 277 | appName = "test test test", 278 | packageName = "com.test", 279 | policy = VpnPolicy.DEFAULT, 280 | appIcon = AppCompatResources.getDrawable( 281 | context, R.drawable.ic_launcher_background 282 | )!! 283 | ) 284 | ) 285 | ).collectAsState() 286 | 287 | AndroidLocalVpnTheme { 288 | PolicyTab( 289 | selectedApplicationsSettings = emptyList(), 290 | selectedVpnPolicy = selectedVpnPolicy, 291 | applicationsSettings = applicationSettings, 292 | onApplicationSettingsTapped = {}, 293 | onSaveApplicationSettings = {}, 294 | onResetApplicationSettings = {}, 295 | onVpnPolicyTapped = {}, 296 | ) 297 | } 298 | } -------------------------------------------------------------------------------- /source/android/app/src/main/java/com/github/jonforshort/androidlocalvpn/ui/main/TestTab.kt: -------------------------------------------------------------------------------- 1 | // 2 | // This is free and unencumbered software released into the public domain. 3 | // 4 | // Anyone is free to copy, modify, publish, use, compile, sell, or 5 | // distribute this software, either in source code form or as a compiled 6 | // binary, for any purpose, commercial or non-commercial, and by any 7 | // means. 8 | // 9 | // In jurisdictions that recognize copyright laws, the author or authors 10 | // of this software dedicate any and all copyright interest in the 11 | // software to the public domain. We make this dedication for the benefit 12 | // of the public at large and to the detriment of our heirs and 13 | // successors. We intend this dedication to be an overt act of 14 | // relinquishment in perpetuity of all present and future rights to this 15 | // software under copyright law. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 21 | // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 22 | // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | // OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | // For more information, please refer to 26 | // 27 | package com.github.jonforshort.androidlocalvpn.ui.main 28 | 29 | import androidx.compose.foundation.layout.Column 30 | import androidx.compose.foundation.layout.fillMaxWidth 31 | import androidx.compose.material.Button 32 | import androidx.compose.material.ButtonDefaults 33 | import androidx.compose.material.Text 34 | import androidx.compose.runtime.Composable 35 | import androidx.compose.runtime.mutableStateOf 36 | import androidx.compose.runtime.remember 37 | import androidx.compose.runtime.rememberCoroutineScope 38 | import androidx.compose.ui.Modifier 39 | import androidx.compose.ui.graphics.Color 40 | import kotlinx.coroutines.Dispatchers 41 | import kotlinx.coroutines.launch 42 | import org.jsoup.Connection 43 | import org.jsoup.Jsoup 44 | import org.xbill.DNS.* 45 | import timber.log.Timber 46 | import java.io.IOException 47 | import java.net.SocketTimeoutException 48 | 49 | @Composable 50 | internal fun TestTab(modifier: Modifier = Modifier) { 51 | Column(modifier = modifier) { 52 | TestHtmlQuery( 53 | text = "Google (HTTP)", 54 | url = "http://google.com/" 55 | ) 56 | 57 | TestHtmlQuery( 58 | text = "Google (HTTPS)", 59 | url = "https://google.com/" 60 | ) 61 | 62 | TestHtmlQuery( 63 | text = "HttpBin (HTTP)", 64 | url = "http://httpbin.org" 65 | ) 66 | 67 | TestHtmlQuery( 68 | text = "HttpBin (HTTPS)", 69 | url = "https://httpbin.org" 70 | ) 71 | 72 | TestHtmlQuery( 73 | text = "Kernel (HTTP)", 74 | url = "http://mirrors.edge.kernel.org/pub/site/README" 75 | ) 76 | 77 | TestHtmlQuery( 78 | text = "Kernel (HTTPS)", 79 | url = "https://mirrors.edge.kernel.org/pub/site/README" 80 | ) 81 | 82 | TestDnsQuery( 83 | text = "Google (DNS)", 84 | domain = "google.com." 85 | ) 86 | 87 | TestDnsQuery( 88 | text = "Non-Existent Server (DNS)", 89 | domain = "google.com.", 90 | server = "172.0.0.1" 91 | ) 92 | } 93 | } 94 | 95 | @Composable 96 | private fun TestHtmlQuery(text: String, url: String) { 97 | val coroutineScope = rememberCoroutineScope() 98 | 99 | fun performJsoupRequest(onRequestStarted: () -> Unit, onRequestFinished: (Boolean) -> Unit) { 100 | coroutineScope.launch(Dispatchers.IO) { 101 | val requestStartTime = System.currentTimeMillis() 102 | onRequestStarted() 103 | val conn = Jsoup 104 | .connect(url) 105 | .followRedirects(false) 106 | .method(Connection.Method.GET) 107 | try { 108 | val resp = conn.execute() 109 | val html = resp.body() 110 | val duration = System.currentTimeMillis() - requestStartTime 111 | 112 | Timber.d( 113 | """ 114 | |dumping html, count=[${html.length}] duration=[$duration] 115 | |$html 116 | |done dumping html 117 | """.trimMargin() 118 | ) 119 | onRequestFinished(true) 120 | } catch (e: SocketTimeoutException) { 121 | Timber.e(e, "Request timed out") 122 | onRequestFinished(false) 123 | } catch (e: IOException) { 124 | Timber.e(e) 125 | onRequestFinished(false) 126 | } catch (e: RuntimeException) { 127 | Timber.e(e) 128 | onRequestFinished(false) 129 | } 130 | } 131 | } 132 | 133 | val buttonColor = remember { mutableStateOf(Color.Magenta) } 134 | 135 | Button( 136 | modifier = Modifier.fillMaxWidth(), 137 | colors = ButtonDefaults.buttonColors(backgroundColor = buttonColor.value), 138 | onClick = { 139 | performJsoupRequest( 140 | onRequestStarted = { 141 | buttonColor.value = Color.LightGray 142 | }, 143 | onRequestFinished = { isSuccessful -> 144 | if (isSuccessful) { 145 | buttonColor.value = Color.Green 146 | } else { 147 | buttonColor.value = Color.Red 148 | } 149 | }) 150 | } 151 | ) { 152 | Text(text) 153 | } 154 | } 155 | 156 | @Composable 157 | private fun TestDnsQuery(text: String, domain: String, server: String = "8.8.8.8") { 158 | val coroutineScope = rememberCoroutineScope() 159 | 160 | fun performDnsLookup(onRequestStarted: () -> Unit, onRequestFinished: (Boolean) -> Unit) { 161 | coroutineScope.launch(Dispatchers.IO) { 162 | val requestStartTime = System.currentTimeMillis() 163 | onRequestStarted() 164 | 165 | try { 166 | val queryRecord = Record.newRecord(Name.fromString(domain), Type.A, DClass.IN) 167 | val queryMessage = Message.newQuery(queryRecord) 168 | SimpleResolver(server) 169 | .sendAsync(queryMessage) 170 | .whenComplete { answer, e -> 171 | if (e == null) { 172 | val duration = System.currentTimeMillis() - requestStartTime 173 | Timber.d( 174 | """ 175 | |dumping dns, duration=[$duration] 176 | |$answer 177 | |done dumping dns 178 | """.trimMargin() 179 | ) 180 | onRequestFinished(true) 181 | } else { 182 | Timber.e(e) 183 | onRequestFinished(false) 184 | } 185 | } 186 | .toCompletableFuture() 187 | .get() 188 | } catch (e: Exception) { 189 | Timber.e(e) 190 | onRequestFinished(false) 191 | } 192 | } 193 | } 194 | 195 | val buttonColor = remember { mutableStateOf(Color.Magenta) } 196 | 197 | Button( 198 | modifier = Modifier.fillMaxWidth(), 199 | colors = ButtonDefaults.buttonColors(backgroundColor = buttonColor.value), 200 | onClick = { 201 | performDnsLookup( 202 | onRequestStarted = { 203 | buttonColor.value = Color.LightGray 204 | }, 205 | onRequestFinished = { isSuccessful -> 206 | if (isSuccessful) { 207 | buttonColor.value = Color.Green 208 | } else { 209 | buttonColor.value = Color.Red 210 | } 211 | }) 212 | } 213 | ) { 214 | Text(text) 215 | } 216 | } -------------------------------------------------------------------------------- /source/android/app/src/main/java/com/github/jonforshort/androidlocalvpn/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | // 2 | // This is free and unencumbered software released into the public domain. 3 | // 4 | // Anyone is free to copy, modify, publish, use, compile, sell, or 5 | // distribute this software, either in source code form or as a compiled 6 | // binary, for any purpose, commercial or non-commercial, and by any 7 | // means. 8 | // 9 | // In jurisdictions that recognize copyright laws, the author or authors 10 | // of this software dedicate any and all copyright interest in the 11 | // software to the public domain. We make this dedication for the benefit 12 | // of the public at large and to the detriment of our heirs and 13 | // successors. We intend this dedication to be an overt act of 14 | // relinquishment in perpetuity of all present and future rights to this 15 | // software under copyright law. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 21 | // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 22 | // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | // OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | // For more information, please refer to 26 | // 27 | package com.github.jonforshort.androidlocalvpn.ui.theme 28 | 29 | import androidx.compose.ui.graphics.Color 30 | 31 | val Purple200 = Color(0xFFBB86FC) 32 | val Purple500 = Color(0xFF6200EE) 33 | val Purple700 = Color(0xFF3700B3) 34 | val Teal200 = Color(0xFF03DAC5) -------------------------------------------------------------------------------- /source/android/app/src/main/java/com/github/jonforshort/androidlocalvpn/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | // 2 | // This is free and unencumbered software released into the public domain. 3 | // 4 | // Anyone is free to copy, modify, publish, use, compile, sell, or 5 | // distribute this software, either in source code form or as a compiled 6 | // binary, for any purpose, commercial or non-commercial, and by any 7 | // means. 8 | // 9 | // In jurisdictions that recognize copyright laws, the author or authors 10 | // of this software dedicate any and all copyright interest in the 11 | // software to the public domain. We make this dedication for the benefit 12 | // of the public at large and to the detriment of our heirs and 13 | // successors. We intend this dedication to be an overt act of 14 | // relinquishment in perpetuity of all present and future rights to this 15 | // software under copyright law. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 21 | // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 22 | // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | // OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | // For more information, please refer to 26 | // 27 | package com.github.jonforshort.androidlocalvpn.ui.theme 28 | 29 | import androidx.compose.foundation.shape.RoundedCornerShape 30 | import androidx.compose.material.Shapes 31 | import androidx.compose.ui.unit.dp 32 | 33 | val Shapes = Shapes( 34 | small = RoundedCornerShape(4.dp), 35 | medium = RoundedCornerShape(4.dp), 36 | large = RoundedCornerShape(0.dp) 37 | ) -------------------------------------------------------------------------------- /source/android/app/src/main/java/com/github/jonforshort/androidlocalvpn/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | // 2 | // This is free and unencumbered software released into the public domain. 3 | // 4 | // Anyone is free to copy, modify, publish, use, compile, sell, or 5 | // distribute this software, either in source code form or as a compiled 6 | // binary, for any purpose, commercial or non-commercial, and by any 7 | // means. 8 | // 9 | // In jurisdictions that recognize copyright laws, the author or authors 10 | // of this software dedicate any and all copyright interest in the 11 | // software to the public domain. We make this dedication for the benefit 12 | // of the public at large and to the detriment of our heirs and 13 | // successors. We intend this dedication to be an overt act of 14 | // relinquishment in perpetuity of all present and future rights to this 15 | // software under copyright law. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 21 | // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 22 | // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | // OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | // For more information, please refer to 26 | // 27 | package com.github.jonforshort.androidlocalvpn.ui.theme 28 | 29 | import androidx.compose.foundation.isSystemInDarkTheme 30 | import androidx.compose.material.MaterialTheme 31 | import androidx.compose.material.darkColors 32 | import androidx.compose.material.lightColors 33 | import androidx.compose.runtime.Composable 34 | 35 | private val DarkColorPalette = darkColors( 36 | primary = Purple200, 37 | primaryVariant = Purple700, 38 | secondary = Teal200 39 | ) 40 | 41 | private val LightColorPalette = lightColors( 42 | primary = Purple500, 43 | primaryVariant = Purple700, 44 | secondary = Teal200 45 | 46 | /* Other default colors to override 47 | background = Color.White, 48 | surface = Color.White, 49 | onPrimary = Color.White, 50 | onSecondary = Color.Black, 51 | onBackground = Color.Black, 52 | onSurface = Color.Black, 53 | */ 54 | ) 55 | 56 | @Composable 57 | fun AndroidLocalVpnTheme( 58 | darkTheme: Boolean = isSystemInDarkTheme(), 59 | content: @Composable() () -> Unit 60 | ) { 61 | val colors = if (darkTheme) { 62 | DarkColorPalette 63 | } else { 64 | LightColorPalette 65 | } 66 | 67 | MaterialTheme( 68 | colors = colors, 69 | typography = Typography, 70 | shapes = Shapes, 71 | content = content 72 | ) 73 | } -------------------------------------------------------------------------------- /source/android/app/src/main/java/com/github/jonforshort/androidlocalvpn/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | // 2 | // This is free and unencumbered software released into the public domain. 3 | // 4 | // Anyone is free to copy, modify, publish, use, compile, sell, or 5 | // distribute this software, either in source code form or as a compiled 6 | // binary, for any purpose, commercial or non-commercial, and by any 7 | // means. 8 | // 9 | // In jurisdictions that recognize copyright laws, the author or authors 10 | // of this software dedicate any and all copyright interest in the 11 | // software to the public domain. We make this dedication for the benefit 12 | // of the public at large and to the detriment of our heirs and 13 | // successors. We intend this dedication to be an overt act of 14 | // relinquishment in perpetuity of all present and future rights to this 15 | // software under copyright law. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 21 | // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 22 | // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | // OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | // For more information, please refer to 26 | // 27 | package com.github.jonforshort.androidlocalvpn.ui.theme 28 | 29 | import androidx.compose.material.Typography 30 | import androidx.compose.ui.text.TextStyle 31 | import androidx.compose.ui.text.font.FontFamily 32 | import androidx.compose.ui.text.font.FontWeight 33 | import androidx.compose.ui.unit.sp 34 | 35 | // Set of Material typography styles to start with 36 | val Typography = Typography( 37 | 38 | defaultFontFamily = FontFamily.Default, 39 | 40 | h1 = TextStyle( 41 | fontWeight = FontWeight.Light, 42 | fontSize = 48.sp, 43 | letterSpacing = (-1.5).sp 44 | ), 45 | 46 | h2 = TextStyle( 47 | fontWeight = FontWeight.Light, 48 | fontSize = 36.sp, 49 | letterSpacing = (-0.5).sp 50 | ), 51 | 52 | h3 = TextStyle( 53 | fontWeight = FontWeight.Normal, 54 | fontSize = 20.sp, 55 | letterSpacing = 0.sp 56 | ), 57 | 58 | h4 = TextStyle( 59 | fontWeight = FontWeight.Normal, 60 | fontSize = 16.sp, 61 | letterSpacing = 0.25.sp 62 | ), 63 | 64 | h5 = TextStyle( 65 | fontWeight = FontWeight.Normal, 66 | fontSize = 14.sp, 67 | letterSpacing = 0.sp 68 | ), 69 | 70 | h6 = TextStyle( 71 | fontWeight = FontWeight.Medium, 72 | fontSize = 12.sp, 73 | letterSpacing = 0.15.sp 74 | ), 75 | 76 | body1 = TextStyle( 77 | fontWeight = FontWeight.Normal, 78 | fontSize = 16.sp, 79 | letterSpacing = 0.5.sp 80 | ), 81 | 82 | body2 = TextStyle( 83 | fontWeight = FontWeight.Normal, 84 | fontSize = 14.sp, 85 | letterSpacing = 0.25.sp 86 | ), 87 | 88 | subtitle1 = TextStyle( 89 | fontWeight = FontWeight.Normal, 90 | fontSize = 16.sp, 91 | letterSpacing = 0.15.sp 92 | ), 93 | 94 | subtitle2 = TextStyle( 95 | fontWeight = FontWeight.Medium, 96 | fontSize = 14.sp, 97 | letterSpacing = 0.1.sp 98 | ), 99 | 100 | caption = TextStyle( 101 | fontWeight = FontWeight.Normal, 102 | fontSize = 12.sp, 103 | letterSpacing = 0.4.sp 104 | ), 105 | 106 | overline = TextStyle( 107 | fontWeight = FontWeight.Normal, 108 | fontSize = 10.sp, 109 | letterSpacing = 1.5.sp 110 | ), 111 | 112 | button = TextStyle( 113 | fontWeight = FontWeight.Medium, 114 | fontSize = 14.sp, 115 | letterSpacing = 1.25.sp 116 | ) 117 | ) -------------------------------------------------------------------------------- /source/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /source/android/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /source/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /source/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /source/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonForShort/android-local-vpn/902126bd816471877bb9d8e9bbcb17978fb8ccff/source/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /source/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonForShort/android-local-vpn/902126bd816471877bb9d8e9bbcb17978fb8ccff/source/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /source/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonForShort/android-local-vpn/902126bd816471877bb9d8e9bbcb17978fb8ccff/source/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /source/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonForShort/android-local-vpn/902126bd816471877bb9d8e9bbcb17978fb8ccff/source/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /source/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonForShort/android-local-vpn/902126bd816471877bb9d8e9bbcb17978fb8ccff/source/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /source/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonForShort/android-local-vpn/902126bd816471877bb9d8e9bbcb17978fb8ccff/source/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /source/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonForShort/android-local-vpn/902126bd816471877bb9d8e9bbcb17978fb8ccff/source/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /source/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonForShort/android-local-vpn/902126bd816471877bb9d8e9bbcb17978fb8ccff/source/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /source/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonForShort/android-local-vpn/902126bd816471877bb9d8e9bbcb17978fb8ccff/source/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /source/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonForShort/android-local-vpn/902126bd816471877bb9d8e9bbcb17978fb8ccff/source/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /source/android/app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /source/android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /source/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AndroidLocalVpn 3 | -------------------------------------------------------------------------------- /source/android/app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 21 | 22 |