Compare commits

...

213 Commits

Author SHA1 Message Date
Zane Schepke 9e0e17787d chore: bump version, update screenshots 2025-04-06 02:44:04 -04:00
Zane Schepke b4c5b51644 fix: tunnel service handling of multiple tunnels and tunnel job restarts 2025-04-06 01:46:22 -04:00
Zane Schepke 76191c46f3 fix: pop backstack on config save 2025-04-05 21:10:47 -04:00
Zane Schepke ecf5036f56 fix: nav backstack bug 2025-04-05 20:58:20 -04:00
Zane Schepke 1c0d968cfb fix: auto tunnel tile state bug 2025-04-05 20:17:16 -04:00
Zane Schepke bda1a2080a fix: tunnel tile default to previously active tunnel 2025-04-05 19:58:39 -04:00
Zane Schepke 14a71e3118 fix: stop kill switch on trusted auto tunnel bug 2025-04-05 19:12:55 -04:00
Zane Schepke 57391290c5 refactor: major ui refactor and bug fixes (#660)
refactor: improved error handling
fix: vpn kill switch with domain endpoints resolution
fix: bugs in ping restarts and config change restarts
feat: new nav bar
feat: expose network status info
feat: ui enhancements
2025-04-05 18:11:21 -04:00
Zane Schepke cd623c0c0c fix: ui bug 2025-04-02 00:54:44 -04:00
Zane Schepke 212c6cf088 feat: add screen security to config screen 2025-04-01 22:45:36 -04:00
Zane Schepke ca47127bff refactor: state management (#656) 2025-04-01 22:18:38 -04:00
Zane Schepke e63733286c refactor: advanced screen 2025-03-31 17:52:14 -04:00
Zane Schepke 36c76565f7 refactor: optimize log toggle 2025-03-31 17:08:39 -04:00
Zane Schepke 47f8de8c57 refactor: support screen, add matrix link 2025-03-31 16:40:40 -04:00
Zane Schepke 5740012101 chore: fmt and bump hilt 2025-03-31 14:30:51 -04:00
Zane Schepke 6f5bb24cfa fix: dns64 and ip version detection improvements 2025-03-31 13:13:44 -04:00
Zane Schepke 5f791ffda1 chore: bump ksp 2025-03-30 18:48:47 -04:00
Zane Schepke ec244eeda3 chore: bump deps 2025-03-30 18:46:26 -04:00
Hendrik Volkmer ff2a2cc082 feat: Add option to add config via URL (#623)
Co-authored-by: Zane Schepke <zanecschepke@gmail.com>
2025-03-30 18:37:07 -04:00
Zane Schepke a873546e9e fix: bugs in config changes and ping tunnel jobs (#650) 2025-03-30 18:31:26 -04:00
Zane Schepke 757669ddbe docs: update matrix link 2025-03-23 15:35:18 -04:00
Zane Schepke c71c4e5b29 chore: bump version and notes 2025-03-19 22:35:21 -04:00
Zane Schepke 7f0fea3766 fix: improve wifi monitoring to better handle permission changes 2025-03-19 21:51:54 -04:00
Zane Schepke 53c19762ef fix: attempt to improve tile sync 2025-03-16 23:12:39 -04:00
Zane Schepke c98fa04f73 fix: auto tunnel and tunnel regressions 2025-03-16 20:10:44 -04:00
Zane Schepke aba0f7d4d3 chore: bump deps 2025-03-16 02:05:55 -04:00
Zane Schepke fa517b2124 fix: race conditions (#621) 2025-03-16 02:04:09 -04:00
Zane Schepke d7e2648393 docs: update readme links 2025-03-15 19:22:34 -04:00
Zane Schepke 53ff3bb1e5 chore: bump version with notes 2025-03-15 13:10:53 -04:00
Zane Schepke 97ede3d5b4 fix: set prefer ipv4 as default to mimic old wg behavior 2025-03-15 12:44:56 -04:00
Zane Schepke dcd15f7bd8 fix: adding new peer bug
Closes #612
2025-03-15 11:30:10 -04:00
Zane Schepke 6031d85edd fix: re-enable shortcuts
Closes #616
2025-03-15 00:16:02 -04:00
Weblate (bot) a71f8f86b1 feat: Translations update from Hosted Weblate (#556)
Co-authored-by: Zane Schepke <zanecschepke@gmail.com>
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: lateweb <weblate@techkoala.net>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Kachelkaiser <kachelkaiser@outlook.com>
Co-authored-by: CyanWolf <hydemr@pm.me>
Co-authored-by: Henrik Sozzi <henrik_sozzi@hotmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: x86_64-pc-linux-gnu <x86_64-pc-linux-gnu@proton.me>
Co-authored-by: mak7im01 <mak7im02@gmail.com>
Co-authored-by: heykanspor <meingithub@heykan.de>
2025-03-14 22:51:26 -04:00
Zane Schepke 007c9f4c5d fix: static ipv6 endpoint bug 2025-03-14 22:49:08 -04:00
Zane Schepke e32a99db77 chore: fix fi 2025-03-08 23:37:16 -05:00
Zane Schepke 6670a62e2f chore: fix fastlane 2025-03-08 23:07:02 -05:00
Zane Schepke 34b20bd7f7 fix: pt region localization 2025-03-08 22:52:53 -05:00
Zane Schepke 01e15099ca chore: bump version with notes 2025-03-08 22:27:17 -05:00
Zane Schepke 8f2fd93e77 fix: improve local logging 2025-03-08 22:11:23 -05:00
𝗛𝗼𝗹𝗶 94197c9943 feat: Update Turkish Translation (#605) 2025-03-08 21:32:41 -05:00
Zane Schepke 39fc9014d8 fix: bump deps for dns fix 2025-03-08 21:31:31 -05:00
Zane Schepke 8860b45230 fix: service manager bug 2025-03-02 17:51:48 -05:00
Zane Schepke e220b26d88 fix: temporarily disallow start/stop of pinger while tunnel running 2025-03-02 17:25:29 -05:00
Zane Schepke 2302b473b4 refactor: tunnel toggling 2025-03-02 16:47:55 -05:00
Zane Schepke b01473073f fix: add restart support htc quickboot 2025-03-02 12:09:57 -05:00
Zane Schepke 3ae9a24ca4 refactor: restore on restart/update 2025-03-02 12:01:28 -05:00
Zane Schepke dc3f7fa736 refactor: improve service manager 2025-03-02 11:38:36 -05:00
Zane Schepke 93d6f8aa45 fix: module flavors 2025-03-01 21:07:47 -05:00
Zane Schepke 3ea4aea5cf fix: improve network status monitoring 2025-03-01 20:17:19 -05:00
Zane Schepke 68b41c8925 fix: startup/nav bugs 2025-03-01 12:15:37 -05:00
Zane Schepke 06de1f24c2 fix: tunnel race condition 2025-03-01 11:21:07 -05:00
Zane Schepke a39feeeea6 fix: proguard rules 2025-02-23 23:21:58 -05:00
Zane Schepke c1619ff012 temp disable proguard 2025-02-23 23:15:40 -05:00
Zane Schepke 2534b86005 fix: add back rules 2025-02-23 22:50:20 -05:00
Zane Schepke 15c550737c bump deps, remove redudant proguard rules 2025-02-23 22:39:15 -05:00
Zane Schepke e1e7e27bb5 fix: proguard (#590) 2025-02-23 16:41:51 -05:00
Zane Schepke 8021c133a5 fix: proguard 2025-02-23 15:58:08 -05:00
Zane Schepke 6009445a15 bump tunnel deps 2025-02-23 15:16:12 -05:00
Zane Schepke f80af9dd5e fix: tunnel state bug 2025-02-22 23:21:25 -05:00
Zane Schepke 3f912ed532 fix: tunnel change restart logic 2025-02-22 13:07:15 -05:00
Zane Schepke 13bb300f20 feat: add worker to restart killed services 2025-02-21 21:16:10 -05:00
Zane Schepke 1375a468fd feat: add export as amnezia or wg 2025-02-21 17:26:51 -05:00
Zane Schepke f52efd92ff fix: tunnel tile state 2025-02-14 19:05:02 -05:00
Zane Schepke ae665d3e61 feat: multi tunnel support (#566)
* add support endpoint resolve to ipv6 preference
2025-02-14 17:11:26 -05:00
Zane Schepke 1857977d9b ci: add verification step 2025-01-19 13:39:56 -05:00
Zane Schepke 4784b1f65c Merge branch 'main' of https://github.com/zaneschepke/wgtunnel 2025-01-19 12:38:32 -05:00
Zane Schepke f3debcfe45 feat: disable kill switch on trusted
fix: debounce ui bug closes #532
2025-01-19 12:37:43 -05:00
Weblate (bot) a5229c62b1 feat: Translations update from Hosted Weblate (#542)
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: CyanWolf <hydemr@pm.me>
2025-01-19 11:59:42 -05:00
Zane Schepke 3e6a2981cb chore: bump deps 2025-01-18 13:21:13 -05:00
Zane Schepke 00254874f0 refactor: connectivity monitoring (#553) 2025-01-18 11:56:04 -05:00
Zane Schepke 1b9560b601 fix: light mode text bugs 2025-01-18 07:16:12 -05:00
Zane Schepke ff97c65d4f fix: preshared key should be hidden
closes #547
2025-01-18 05:47:23 -05:00
Zane Schepke 02dea1e6b0 fix: include all ipv6 with exclusion of private ipv4 addresses
closes #550
2025-01-18 05:31:01 -05:00
Zane Schepke 548362cdde refactor: minor refactor of tunnel options 2025-01-18 04:42:20 -05:00
Zane Schepke 932d27edd7 feat: localizations (#540)
Co-authored-by: teemue <eemil.koivula@gmail.com>
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: MouaisTe44 <r.craft.212121@gmail.com>
Co-authored-by: Saturno <rodrigogigante4016@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Thomas Chuang <9euagazrg@mozmail.com>
2025-01-11 16:13:18 -05:00
Zane Schepke d89870e6de fix: tile titles not localized
closes #537
2025-01-11 15:37:39 -05:00
Zane Schepke 0dcee673e1 fix: back gesture 2025-01-11 14:52:29 -05:00
Zane Schepke 7b7c8f6e8c ci: fix fdroid dispatch 2025-01-02 22:58:04 -05:00
Zane Schepke 7cbe3311c9 chore: bump version 2025-01-02 22:45:43 -05:00
Zane Schepke a5898d4ad1 fix: make sure ping shuts down 2025-01-02 17:10:32 -05:00
Zane Schepke 48c01aa0e3 Merge branch 'main' of https://github.com/zaneschepke/wgtunnel 2025-01-02 16:47:30 -05:00
Zane Schepke b1dc6c5d59 fix: make ping aware of network availability
add basic tunnel error messages
2025-01-02 16:47:23 -05:00
Weblate (bot) 2a7397edba Translations update from Hosted Weblate (#531)
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: Kachelkaiser <kachelkaiser@outlook.com>
Co-authored-by: CyanWolf <hydemr@pm.me>
Co-authored-by: Deleted User <noreply+48943@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
2025-01-02 16:35:43 -05:00
Zane Schepke 53c09267df fix: ethernet monitoring and auto tunnel override 2025-01-01 19:14:56 -05:00
Zane Schepke f772dc0f8a feat!: move ping from auto tunnel to tunnel
Ping feature will not be tunnel specific and work without auto tunneling being active
2025-01-01 18:16:26 -05:00
Zane Schepke ba064b267f fix: try to query ssid if we don't have a valid one yet 2025-01-01 02:38:59 -05:00
Zane Schepke 2c04b9d69c fix: active tunnel bug 2025-01-01 01:32:24 -05:00
Zane Schepke 48adaae0a0 docs: fix readme 2025-01-01 00:43:28 -05:00
Zane Schepke 8e49a4d343 docs: fix readme title 2025-01-01 00:41:51 -05:00
Zane Schepke 67f53915cc feat(lang): weblate localization changes (#530)
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: Kachelkaiser <kachelkaiser@outlook.com>
Co-authored-by: L.y <lycidias@gmail.com>
2025-01-01 00:40:59 -05:00
Weblate (bot) 156c5478ae Translations update from Hosted Weblate (#522)
Co-authored-by: Zane Schepke <zanecschepke@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
2025-01-01 00:28:11 -05:00
Zane Schepke 85319ba874 refactor: navigation 2025-01-01 00:08:33 -05:00
Zane Schepke ab858ab59e feat: config screen peer action to exclude private ips
closes #402
2024-12-31 22:31:31 -05:00
Zane Schepke 4196a543b2 fix: location disclosure screen navigation bug 2024-12-31 19:13:15 -05:00
Zane Schepke 02a8db0f9a feat: add setting for debounce delay tuning
closes #493
2024-12-31 19:02:44 -05:00
Zane Schepke eb7c6ca7ba ci: fix apk upload github release 2024-12-31 15:25:07 -05:00
Zane Schepke 8ffe145ade fix: android 9 crash bug 2024-12-31 14:19:57 -05:00
Zane Schepke 4bdc43deb3 ci: move versionCode bump to build 2024-12-31 00:47:09 -05:00
Zane Schepke 83c0ff497b ci: fix github publish 2024-12-31 00:27:29 -05:00
Zane Schepke c8ac40d370 fix: improve tile sync
#491
2024-12-31 00:17:34 -05:00
Zane Schepke 4dc91b5fae fix: create tunnel from scratch bug
closes #524
2024-12-28 23:46:21 -05:00
Zane Schepke 7a3627bf6a fix: remove unused shortcuts dep 2024-12-27 15:03:38 -05:00
Zane Schepke e86bba6a0e docs: move chats 2024-12-27 13:52:20 -05:00
Zane Schepke ac846bfdc9 docs: update screenshots and readme 2024-12-27 13:49:09 -05:00
Zane Schepke 11e385d350 feat: toggle to set amnezia/wg compatibility values
closes #469
closes #518
2024-12-27 02:52:44 -05:00
Zane Schepke b2e266fc9f fix: toggle state bug 2024-12-26 22:56:09 -05:00
Zane Schepke 7cbbf00e52 fix: search case sensitive
closes #515
2024-12-26 22:42:43 -05:00
Zane Schepke 6a0b2b678f fix: banner background color removed 2024-12-26 22:39:55 -05:00
Zane Schepke 1bfea142ad ci: fix fdroid publish 2024-12-26 22:34:00 -05:00
Zane Schepke 6fdf80e84d chore: remove crowdin 2024-12-26 22:17:11 -05:00
Zane Schepke b7ec9e2696 chore: fix language mapping 2024-12-26 20:36:30 -05:00
Zane Schepke 6502f553ac Update Crowdin configuration file 2024-12-26 20:27:52 -05:00
Zane Schepke 946ac71fb1 Update Crowdin configuration file 2024-12-26 19:51:16 -05:00
Zane Schepke dca8b50083 chore: crowdin yaml fix 2024-12-26 18:44:59 -05:00
Zane Schepke 1ab611aa7f chore: cleanup unused strings 2024-12-26 14:33:08 -05:00
Zane Schepke 40a2732ec9 Update Crowdin configuration file 2024-12-26 13:41:42 -05:00
Zane Schepke ffcb4f6ed8 fix: weblate conflicts (#519)
Co-authored-by: lateweb <weblate@techkoala.net>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Tom <weblate.delicate088@passinbox.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: MouaisTe44 <r.craft.212121@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Julian Madsen <julian@sopyt.com>
Co-authored-by: tomo90 <ivanek.tomas@gmail.com>
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: Marco <16669146+RodoMa92@users.noreply.github.com>
2024-12-26 13:32:29 -05:00
Zane Schepke 7b6bdec133 fix!: notification channel id should not be localized 2024-12-26 12:49:00 -05:00
Zane Schepke 6ece224592 fix: translation bug 2024-12-26 12:23:36 -05:00
Zane Schepke 66051e4a0a ci: minor refactor 2024-12-25 23:19:31 -05:00
Zane Schepke c8298297e2 ci: refactor build and publish 2024-12-25 22:43:26 -05:00
Zane Schepke 83bec24ca9 fix: reproducibility 2024-12-25 21:47:56 -05:00
Zane Schepke 44353df400 chore: bump to 3.6.5 2024-12-22 02:41:05 -05:00
Zane Schepke 0da6e3e82c chore: bump deps 2024-12-22 02:26:45 -05:00
Zane Schepke dbaf0312f8 fix: split tunnel performance improvements 2024-12-22 02:11:42 -05:00
Zane Schepke 7cf7817cea fix: switch to dynamic shortcuts
closes #365
closes #508
2024-12-21 23:28:50 -05:00
GitHub Actions 133bd6caf7 Automated build update 2024-12-22 03:34:07 +00:00
Zane Schepke 25b986ef2f fix: improve location querying
closes #498
2024-12-21 22:14:31 -05:00
Zane Schepke fe45a2fad9 fix: minor ui improvements 2024-12-21 02:49:38 -05:00
Zane Schepke a3eb53f90f fix: tile tunnel start
closes #491
2024-12-21 00:47:58 -05:00
GitHub Actions 246916ab0d Automated build update 2024-12-21 03:30:12 +00:00
Zane Schepke 7f89dcaab0 fix: minor ui improvements
closes #473
2024-12-20 02:08:05 -05:00
GitHub Actions 1d2b305047 Automated build update 2024-12-20 03:32:37 +00:00
Zane Schepke 0201523262 fix: config screen performance (#495)
* improve config screen performance 
* add ability to edit tunnel scripts for rooted phones
* add new screen for split tunneling
* fix split tunneling reset bug
2024-12-19 19:52:31 -05:00
dependabot[bot] fcc1c1e7a1 build(deps): bump actions/upload-artifact from 4.4.3 to 4.5.0 (#503)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-19 19:48:19 -05:00
Zane Schepke cd82b83789 Merge branch 'main' of github.com:zaneschepke/wgtunnel 2024-12-19 11:33:02 -05:00
Zane Schepke 9f1af0140f fix: app launch on notification click
closes #504
2024-12-19 11:33:00 -05:00
GitHub Actions 14cd49983a Automated build update 2024-12-19 06:23:05 +00:00
Zane Schepke 40dd14ca16 fix: auto tunnel bug fixes
* add debounce to prevent rapid state changes
* fix get wifi from shell bug
* ignore default emitted state values
2024-12-19 01:07:25 -05:00
GitHub Actions 048bcd4ad7 Automated build update 2024-12-19 03:37:21 +00:00
Zane Schepke 94bd23f2a4 fix: bug starting app while no connectivity 2024-12-18 20:26:44 -05:00
Zane Schepke 7285af5c2d fix: location services wifi toggle 2024-12-18 20:19:38 -05:00
GitHub Actions 11efad703c Automated build update 2024-12-15 03:44:11 +00:00
Marko Zajc b81f43c7cf fix: getting the SSID via shell (#494) 2024-12-14 11:18:35 -05:00
GitHub Actions 68865843dc Automated build update 2024-12-14 03:41:42 +00:00
Zane Schepke 62daf138dd fix: add action and tunnel name to kernel notification (#490) 2024-12-12 23:54:08 -05:00
GitHub Actions 1120a8fc81 Automated build update 2024-12-12 03:42:47 +00:00
Zane Schepke ab10be7266 chore: bump version 2024-12-11 01:37:16 -05:00
Zane Schepke 88f9d43e51 chore: add release notes 2024-12-11 01:35:17 -05:00
Zane Schepke 2877757396 fix: kernel mode toggle bug 2024-12-11 01:32:39 -05:00
Zane Schepke c657c62640 fix: shut down backend if we enter kernel mode 2024-12-11 00:18:42 -05:00
Zane Schepke d84d9df57d fix: notification channel creation bug
Fixes a crash that is caused by a typo when creating a new notification channel.

Also makes VPN kill switch visible on TVs
2024-12-11 00:03:39 -05:00
Zane Schepke 6762d4733e chore: bump version 2024-12-09 22:46:27 -05:00
Zane Schepke add18d5cef chore: bump version w/notes 2024-12-09 22:44:37 -05:00
Zane Schepke c5b42b55c3 feat: improved notifications with actions (#481) 2024-12-09 22:38:55 -05:00
Zane Schepke 670d9d680c fix: make logging lifecycle aware 2024-12-09 21:41:59 -05:00
GitHub Actions bbfc0e2fab Automated build update 2024-12-09 03:44:20 +00:00
Zane Schepke 708b4c7646 fix: auto tunnel wifi whitelist bug
#472
2024-12-08 15:06:13 -05:00
Zane Schepke efba604c31 fix: screen padding/scroll bug
closes #479
2024-12-08 14:43:15 -05:00
GitHub Actions 1441488053 Automated build update 2024-12-08 03:46:39 +00:00
Zane Schepke bb3b05d224 fix: locale change bug and sort tunnels alphabetically 2024-12-07 22:34:45 -05:00
Zane Schepke cda747deee feat: add vpn kill switch (#476) 2024-12-07 18:10:03 -05:00
GitHub Actions c3a2e05eb2 Automated build update 2024-12-01 00:24:55 +00:00
Zane Schepke a992009c71 fix: restart on ping bugs
Fixes bug where restart on ping could kill itself or not start correctly given certain settings combinations.

This change also makes auto tunneling and all or nothing service as this intuitively makes the most sense with the way the global settings are presented.

This change also makes it so users can toggle tunnel on untrusted wifi without location permissions because location permissions are only required when they go to add trusted ssids.
2024-11-30 18:32:50 -05:00
Zane Schepke 57676bf4bb fix: copy tunnel bug 2024-11-30 13:18:47 -05:00
Zane Schepke 921e33cb70 feat: add ethernet tunnel, stop tunnel on offline
closes #460
2024-11-30 12:33:07 -05:00
Zane Schepke 70649383e0 fix: auto tunnel logic and speed
closes #466
2024-11-30 11:13:54 -05:00
GitHub Actions 64a7680b81 Automated build update 2024-11-29 04:35:33 +00:00
Weblate (bot) a81d3a8843 Translations update from Hosted Weblate (#459)
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: SeanChengN <54seancheng@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Kachelkaiser <kachelkaiser@outlook.com>
Co-authored-by: MouaisTe44 <r.craft.212121@gmail.com>
Co-authored-by: lateweb <weblate@techkoala.net>
2024-11-28 22:48:58 -05:00
GitHub Actions bad2f55121 Automated build update 2024-11-24 03:41:29 +00:00
Zane Schepke fecc55fe9c chore: add id title 2024-11-23 17:39:47 -05:00
Zane Schepke 4d2e9629f9 refactor: pt language code 2024-11-23 17:26:53 -05:00
Zane Schepke 347c79741f dist: 3.6.1 notes and bump 2024-11-23 16:06:05 -05:00
Zane Schepke 9bb30069fe feat: add tunnel from clipboard
closes #431
2024-11-23 15:48:11 -05:00
Zane Schepke 9a2d77c8bf minor: disable stats job when app is closed
Adds a slight battery performance improvement

#437
2024-11-23 15:00:06 -05:00
Zane Schepke f79f922838 refactor: language selection
fix: amnezia edit bug
closes #425
2024-11-23 00:35:09 -05:00
Weblate (bot) a9d5994070 chore: remove unused/outdated strings (#455) 2024-11-22 22:57:03 -05:00
Zane Schepke f967b38af1 feat: add weblate localizations (#454)
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: Kachelkaiser <kachelkaiser@outlook.com>
Co-authored-by: Kirill Isakov <k@isakov.net>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: Jasper <jasper@ennik.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Haerul Muttaqin <email.haerulmuttaqin@gmail.com>
Co-authored-by: GeneralTDog <kevin.duy.tran@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: day <grrrleon@gmail.com>
Co-authored-by: Henrik Sozzi <henrik_sozzi@hotmail.com>
Co-authored-by: MouaisTe44 <r.craft.212121@gmail.com>
Co-authored-by: TCH <teunish@outlook.com>
Co-authored-by: ragu <lobular.exports-02@icloud.com>
2024-11-22 22:40:23 -05:00
GitHub Actions a060f00490 Automated build update 2024-11-23 03:36:17 +00:00
Zane Schepke b15cdbce7c fix: tunnel control tile sync
closes #442
2024-11-22 22:07:41 -05:00
GitHub Actions 63d46c1817 Automated build update 2024-11-23 02:13:49 +00:00
Zane Schepke f9ef308f8a feat: user logger toggle control
Allow users to toggle on and off local logger. This will hopefully help address #437
Allow shortcuts activation on AndroidTV
closes #451
2024-11-22 21:06:58 -05:00
GitHub Actions b2938c3ac8 Automated build update 2024-11-18 03:39:50 +00:00
Zane Schepke 17b575b7f2 chore: update readme 2024-11-17 01:34:46 -05:00
Zane Schepke 777a948244 fix: add back per tunnel ping settings 2024-11-17 01:30:13 -05:00
Zane Schepke 72bf0a1979 fix: androidtv ui bugs and crash
closes #443
2024-11-16 23:22:25 -05:00
GitHub Actions c7a45845d6 Automated build update 2024-11-14 02:53:17 +00:00
Zane Schepke c2725bf066 chore: changelog to releases 2024-11-13 21:48:12 -05:00
Zane Schepke 2993f1db22 fix: delete tunnel bug
closes #435
2024-11-12 22:47:03 -05:00
Zane Schepke e9ae48f96c add: donation selection to support 2024-11-12 22:46:06 -05:00
GitHub Actions 5cc2ae0d01 Automated build update 2024-11-08 04:14:02 +00:00
Zane Schepke 7fdec509e9 Merge branch 'main' of https://github.com/zaneschepke/wgtunnel 2024-11-07 23:03:02 -05:00
Zane Schepke cab2945930 fix: auto tunnel bugs
Fixes auto tunnel bug that can happen on startup
Fixes auto tunnel bug that didn't allow manual toggle override of tunnel while auto tunnel is active
Add basic foreground persistent notifications (optionally turned off via android settings)
2024-11-07 23:03:00 -05:00
GitHub Actions 6a90cd02b9 Automated build update 2024-11-08 03:30:59 +00:00
Zane Schepke 7d810c7c3d fix: auto tunnel crash 2024-11-07 20:02:35 -05:00
GitHub Actions dad34b9e24 Automated build update 2024-11-04 03:36:36 +00:00
GitHub Actions fb33b8996f Automated build update 2024-11-03 22:22:13 +00:00
Zane Schepke ec3a5dcd65 fix: cd 2024-11-03 17:13:43 -05:00
Zane Schepke 7b443add3a fix: ci 2024-11-03 17:11:06 -05:00
Zane Schepke d70ef658e2 add notes 2024-11-03 02:15:42 -05:00
Zane Schepke c01b045022 bump target 2024-11-03 01:37:46 -05:00
Zane Schepke d3ea75869a more changes
bump versions
2024-11-03 01:32:42 -04:00
Zane Schepke 0784c96011 add more ui changes, androidtv improvements 2024-10-29 05:16:39 -04:00
Zane Schepke 553279ea76 initial ui changes 2024-10-21 23:52:20 -04:00
Zane Schepke 89f6dec357 fix: permission crash on Android 12 2024-10-20 23:53:55 -04:00
Zane Schepke ab7499a616 feat: auto toggle show amnezia props
closes #401
2024-10-20 16:41:02 -04:00
Zane Schepke 105c753c66 fix: copy bug
closes #403
2024-10-20 16:06:07 -04:00
Zane Schepke d9f0de2dd4 add top nav for lgos 2024-10-19 23:07:38 -04:00
Zane Schepke 82280091ad add top nav bar 2024-10-19 19:12:10 -04:00
Zane Schepke b97b7cf989 chore: add github sponsor support 2024-10-18 12:55:09 -04:00
Zane Schepke f83e40f6cc fix: release pipeline 2024-10-18 11:52:21 -04:00
Zane Schepke 1fab9dfdf2 add full description en for nl 2024-10-18 11:47:36 -04:00
Zane Schepke a670931b06 fix: qr scanner nav crash 2024-10-18 11:38:53 -04:00
615 changed files with 17469 additions and 8944 deletions
+2 -1
View File
@@ -1,2 +1,3 @@
ko_fi: zaneschepke
liberapay: zaneschepke
liberapay: zaneschepke
github: zaneschepke
+125
View File
@@ -0,0 +1,125 @@
name: build
on:
workflow_dispatch:
inputs:
build_type:
type: choice
description: "Build type"
required: true
default: debug
options:
- debug
- prerelease
- nightly
- release
secrets:
SIGNING_KEY_ALIAS:
required: false
SIGNING_KEY_PASSWORD:
required: false
SIGNING_STORE_PASSWORD:
required: false
SERVICE_ACCOUNT_JSON:
required: false
KEYSTORE:
required: false
workflow_call:
inputs:
build_type:
type: string
description: "Build type"
required: true
default: debug
secrets:
SIGNING_KEY_ALIAS:
required: false
SIGNING_KEY_PASSWORD:
required: false
SIGNING_STORE_PASSWORD:
required: false
SERVICE_ACCOUNT_JSON:
required: false
KEYSTORE:
required: false
env:
UPLOAD_DIR_ANDROID: android_artifacts
jobs:
build:
runs-on: ubuntu-latest
env:
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
KEY_STORE_FILE: 'android_keystore.jks'
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
outputs:
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
# Here we need to decode keystore.jks from base64 string and place it
# in the folder specified in the release signing configuration
- name: Decode Keystore
id: decode_keystore
uses: timheuer/base64-to-file@v1.2
with:
fileName: ${{ env.KEY_STORE_FILE }}
fileDir: ${{ env.KEY_STORE_LOCATION }}
encodedString: ${{ secrets.KEYSTORE }}
# create keystore path for gradle to read
- name: Create keystore path env var
if: ${{ inputs.build_type != 'debug' }}
run: |
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
- name: Create service_account.json
if: ${{ inputs.build_type != 'debug' }}
id: createServiceAccount
run: echo '${{ secrets.ANDROID_SERVICE_ACCOUNT_JSON }}' > service_account.json
- name: Build Fdroid Release APK
if: ${{ inputs.build_type == 'release' }}
run: ./gradlew :app:assembleFdroidRelease --info
- name: Build Fdroid Prerelease APK
if: ${{ inputs.build_type == 'prerelease' }}
run: ./gradlew :app:assembleFdroidPrerelease --info
- name: Build Fdroid Nightly APK
if: ${{ inputs.build_type == 'nightly' }}
run: ./gradlew :app:assembleFdroidNightly --info
- name: Build Debug APK
if: ${{ inputs.build_type == 'debug' }}
run: ./gradlew :app:assembleFdroidDebug --stacktrace
# bump versionCode for nightly and prerelease builds
- name: Commit and push versionCode changes
if: ${{ inputs.build_type == 'nightly' || inputs.build_type == 'prerelease' }}
run: |
git config --global user.name 'GitHub Actions'
git config --global user.email 'actions@github.com'
git add versionCode.txt
git commit -m "Automated build update"
- name: Get release apk path
id: apk-path
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
- name: Upload release apk
uses: actions/upload-artifact@v4
with:
name: ${{ env.UPLOAD_DIR_ANDROID }}
path: ${{github.workspace}}/${{ steps.apk-path.outputs.path }}
retention-days: 1
@@ -1,11 +1,11 @@
name: ci-android
name: on-pr
on:
workflow_dispatch:
pull_request:
jobs:
format:
format_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -1,4 +1,4 @@
name: release-android
name: publish
on:
schedule:
@@ -31,40 +31,46 @@ on:
required: false
default: nightly
workflow_call:
env:
UPLOAD_DIR_ANDROID: android_artifacts
jobs:
check_commits:
name: Check for New Commits
runs-on: ubuntu-latest
outputs:
has_new_commits: ${{ steps.check.outputs.new_commits }}
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Checkout Repository
uses: actions/checkout@v3
with:
fetch-depth: 0 # This fetches all history so we can check commits
- name: Check for new commits in the last 23 hours
id: check_commits
- name: Check for new commits
id: check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Get the current time and the time 23 hours ago in ISO 8601 format
now=$(date --utc +%Y-%m-%dT%H:%M:%SZ)
past=$(date --utc --date='23 hours ago' +%Y-%m-%dT%H:%M:%SZ)
# This script checks for commits newer than 23 hours ago
NEW_COMMITS=$(git rev-list --count --after="$(date -Iseconds -d '23 hours ago')" ${{ github.sha }})
echo "new_commits=$NEW_COMMITS" >> $GITHUB_OUTPUT
# Fetch commit history and check for commits in the last 23 hours
if git rev-list --since="$past" --count HEAD > /dev/null; then
echo "New commits found in the last 23 hours."
echo "::set-output name=new_commits::true"
else
echo "No new commits found in the last 23 hours."
echo "::set-output name=new_commits::false"
fi
build:
needs: check_commits
if: ${{ needs.check_commits.outputs.new_commits == 'true' || github.event_name != 'schedule'}}
name: Build Signed APK
if: ${{ inputs.release_type != 'none' }}
uses: ./.github/workflows/build.yml
secrets: inherit
with:
build_type: ${{ inputs.release_type == '' && 'nightly' || inputs.release_type }}
publish:
needs:
- check_commits
- build
if: ${{ needs.check_commits.outputs.has_new_commits > 0 && inputs.release_type != 'none' }}
name: publish-github
runs-on: ubuntu-latest
env:
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
KEY_STORE_FILE: 'android_keystore.jks'
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
GH_USER: ${{ secrets.GH_USER }}
# GH needed for gh cli
GH_TOKEN: ${{ secrets.GH_TOKEN }}
@@ -72,59 +78,35 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Install system dependencies
run: |
sudo apt update && sudo apt install -y gh apksigner
# Here we need to decode keystore.jks from base64 string and place it
# in the folder specified in the release signing configuration
- name: Decode Keystore
id: decode_keystore
uses: timheuer/base64-to-file@v1.2
# update latest tag
- name: Set latest tag
uses: rickstaa/action-create-tag@v1
id: tag_creation
with:
fileName: ${{ env.KEY_STORE_FILE }}
fileDir: ${{ env.KEY_STORE_LOCATION }}
encodedString: ${{ secrets.KEYSTORE }}
tag: "latest" # or any tag name you wish to use
message: "Automated tag for HEAD commit"
force_push_tag: true
tag_exists_error: false
# create keystore path for gradle to read
- name: Create keystore path env var
run: |
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
- name: Get latest release
id: latest_release
uses: kaliber5/action-get-release@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
latest: true
- name: Create service_account.json
id: createServiceAccount
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
# Build and sign APK ("-x test" argument is used to skip tests)
# add fdroid flavor for apk upload
- name: Build Fdroid Release APK
if: ${{ inputs.release_type != '' && inputs.release_type == 'release' }}
run: ./gradlew :app:assembleFdroidRelease -x test
- name: Build Fdroid Prerelease APK
if: ${{ inputs.release_type != '' && inputs.release_type == 'prerelease' }}
run: ./gradlew :app:assembleFdroidPrerelease -x test
- name: Build Fdroid Nightly APK
if: ${{ inputs.release_type == '' || inputs.release_type == 'nightly' }}
run: ./gradlew :app:assembleFdroidNightly -x test
- if: ${{ inputs.release_type == '' || inputs.release_type == 'nightly' }}
run: echo "APK_PATH=$(find . -regex '^.*/build/outputs/apk/fdroid/nightly/.*\.apk$' -type f | head -1)" >> $GITHUB_ENV
- if: ${{ inputs.release_type != '' && inputs.release_type == 'release' }}
run: echo "APK_PATH=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_ENV
- if: ${{ inputs.release_type != '' && inputs.release_type == 'prerelease' }}
run: echo "APK_PATH=$(find . -regex '^.*/build/outputs/apk/fdroid/prerelease/.*\.apk$' -type f | head -1)" >> $GITHUB_ENV
- name: Generate Changelog
id: changelog
uses: requarks/changelog-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
toTag: ${{ github.event_name == 'schedule' && 'nightly' || steps.latest_release.outputs.tag_name }}
fromTag: "latest"
writeToFile: false # we won't write to file, just output
- name: Get version code
if: ${{ inputs.release_type == 'release' }}
@@ -132,14 +114,6 @@ jobs:
version_code=$(grep "VERSION_CODE" buildSrc/src/main/kotlin/Constants.kt | awk '{print $5}' | tr -d '\n')
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
- name: Commit and push versionCode changes
if: ${{ inputs.release_type == '' || inputs.release_type == 'nightly' || inputs.release_type == 'prerelease' }}
run: |
git config --global user.name 'GitHub Actions'
git config --global user.email 'actions@github.com'
git add versionCode.txt
git commit -m "Automated build update"
- name: Push changes
if: ${{ inputs.release_type == '' || inputs.release_type == 'nightly' || inputs.release_type == 'prerelease' }}
uses: ad-m/github-push-action@master
@@ -147,25 +121,14 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }}
# Save the APK after the Build job is complete to publish it as a Github release in the next job
- name: Upload APK
uses: actions/upload-artifact@v4.4.3
with:
name: wgtunnel
path: ${{ env.APK_PATH }}
- name: Make download dir
run: mkdir ${{ github.workspace }}/temp
- name: Download APK from build
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: wgtunnel
- name: Repository Dispatch for my F-Droid repo
uses: peter-evans/repository-dispatch@v3
if: ${{ inputs.release_type == 'release' }}
with:
token: ${{ secrets.PAT }}
repository: zaneschepke/fdroid
event-type: fdroid-update
name: ${{ env.UPLOAD_DIR_ANDROID }}
path: ${{ github.workspace }}/temp
# Setup TAG_NAME, which is used as a general "name"
- if: github.event_name == 'workflow_dispatch'
@@ -196,7 +159,9 @@ jobs:
- name: Get checksum
id: checksum
run: echo "checksum=$(apksigner verify -print-certs ${{ env.APK_PATH }} | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
run: |
file_path=$(find ${{ github.workspace }}/temp -type f -iname "*.apk" | tail -n1)
echo "checksum=$(apksigner verify -print-certs $file_path | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
- name: Create Release with Fastlane changelog notes
@@ -208,14 +173,38 @@ jobs:
body: |
${{ env.RELEASE_NOTES }}
SHA256 fingerprint:
```${{ steps.checksum.outputs.checksum }}```
SHA-256 fingerprint for the 4096-bit signing certificate:
```sh
${{ steps.checksum.outputs.checksum }}
```
To verify fingerprint:
```sh
apksigner verify --print-certs [path to APK file] | grep SHA-256
```
### Changelog
${{ steps.changelog.outputs.changes }}
tag_name: ${{ env.TAG_NAME }}
name: ${{ env.TAG_NAME }}
draft: false
prerelease: ${{ inputs.release_type == 'prerelease' || inputs.release_type == '' || inputs.release_type == 'nightly' }}
make_latest: ${{ inputs.release_type == 'release' }}
files: ${{ github.workspace }}/${{ env.APK_PATH }}
files: |
${{ github.workspace }}/temp/*
publish-fdroid:
runs-on: ubuntu-latest
needs:
- build
if: inputs.release_type == 'release'
steps:
- name: Dispatch update for fdroid repo
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.PAT }}
repository: zaneschepke/fdroid
event-type: fdroid-update
publish-play:
if: ${{ inputs.track != 'none' && inputs.track != '' }}
+72 -58
View File
@@ -4,80 +4,107 @@ WG Tunnel
<div align="center">
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/rbRRNh6H7V)
[![X Community](https://img.shields.io/badge/X-000000?style=for-the-badge&logo=x&logoColor=white)](https://twitter.com/i/communities/1780655267685736818)
[![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/wgtunnel)
An alternative Android client app for [WireGuard](https://www.wireguard.com/)
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
<br />
<br />
<a href="https://github.com/zaneschepke/wgtunnel/issues/new?assignees=zaneschepke&labels=bug&projects=&template=bug_report.md&title=%5BBUG%5D+-+Problem+with+app">Report a Bug</a>
·
<a href="https://github.com/zaneschepke/wgtunnel/issues/new?assignees=zaneschepke&labels=enhancement&projects=&template=feature_request.md&title=%5BFEATURE%5D+-+New+feature+request">Request a Feature</a>
·
<a href="https://github.com/zaneschepke/wgtunnel/discussions">Ask a Question</a>
</div>
<div align="center">
<br/>
<div align="center">
[![Google Play](https://img.shields.io/badge/Google_Play-414141?style=for-the-badge&logo=google-play&logoColor=white)](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
[![F-Droid](https://img.shields.io/static/v1?style=for-the-badge&message=F-Droid&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
</div>
<div align="left">
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/)
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) with added
features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android)
library and [Jetpack Compose](https://developer.android.com/jetpack/compose), this application was
inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app.
[![Personal](https://img.shields.io/static/v1?style=for-the-badge&message=Personal&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://github.com/zaneschepke/fdroid)
[![Obtainium](https://img.shields.io/badge/Obtainium-414141?style=for-the-badge&logo=Obtainium&logoColor=white)](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.zaneschepke.wireguardautotunnel%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fzaneschepke%2Fwgtunnel%22%2C%22author%22%3A%22zaneschepke%22%2C%22name%22%3A%22WG%20Tunnel%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Atrue%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22WG%20Tunnel%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22Zane%20Schepke%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22overrideSource%22%3Anull%7D)
</div>
<div align="center">
[<img src="https://img.shields.io/badge/Telegram-26A5E4.svg?style=for-the-badge&logo=Telegram&logoColor=white">](https://t.me/wgtunnel)
[<img src="https://img.shields.io/badge/Matrix-000000.svg?style=for-the-badge&logo=Matrix&logoColor=white">](https://matrix.to/#/#wg-tunnel-space:matrix.org)
</div>
<details open="open">
<summary>Table of Contents</summary>
- [About](#about)
- [Acknowledgements](#acknowledgements)
- [Screenshots](#screenshots)
- [Features](#features)
- [Building](#building)
- [Translation](#translation)
- [Contributing](#contributing)
</details>
<div style="text-align: left;">
## About
Inspired by the official [wireguard-android](https://github.com/WireGuard/wireguard-android) app, WG Tunnel was created to address features and support missing from the official app. This app combines support for both [WireGuard](https://www.wireguard.com/)
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/), with its primary feature of auto-tunneling (on-demand tunneling).
</div>
<div style="text-align: left;">
## Acknowledgements
Thank you to the following:
- All of the users that have helped contribute to the project with ideas, translations, feedback, bug reports, testing, and donations.
- [WireGuard](https://www.wireguard.com/) - Jason A. Donenfeld (https://github.com/WireGuard/wireguard-android)
- [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) - Amnezia Team (https://github.com/amnezia-vpn/amneziawg-android)
## Screenshots
<p float="center">
<img label="Main" style="padding-right:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
<img label="Config" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png" width="200" />
<img label="Settings" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
<img label="Support" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/support_screen.png" width="200" />
</p>
</div>
<div style="display: flex; flex-wrap: wrap; justify-content: left; gap: 10px;">
<img label="Main" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
<img label="Settings" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
<img label="Auto" src="fastlane/metadata/android/en-US/images/phoneScreenshots/auto_screen.png" width="200" />
<img label="Config" src="fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png" width="200" />
</div>
<div align="left">
## Inspiration
The original inspiration for this app came from the inconvenience of having to manually turn VPN off
and on while on different networks. This app was created to offer a free solution to this problem.
<div style="text-align: left;">
## Features
* Add tunnels via .conf file, zip, manual entry, or QR code
* Auto connect to tunnels based on Wi-Fi SSID, ethernet, or mobile data
* Add tunnels via .conf file, zip, manual entry, clipboard, or QR code
* Auto-tunnel based on Wi-Fi SSID, ethernet, or mobile data
* Split tunneling by application with search
* WireGuard support for kernel and userspace modes
* Support for kernel and userspace modes
* Amnezia support for userspace mode for DPI/censorship protection
* Pre/Post Up/Down scripts support for all modes on a rooted device
* Always-On VPN support
* Export Amnezia and WireGuard tunnels to zip
* Export tunnels to zip
* Quick tile support for tunnel toggling, auto-tunneling
* Static shortcuts support for tunnel toggling, auto-tunneling
* Shortcuts support for tunnel toggling, auto-tunneling
* Intent automation support for all tunnels
* In app VPN kill switch with LAN bypass
* Automatic auto-tunneling service and/or tunnel restart after reboot or app update
* Battery preservation measures
* Restart tunnel on ping failure (beta)
* Restart tunnel on ping failure
## Fdroid
## Building
Want updates faster?
```sh
git clone https://github.com/zaneschepke/wgtunnel
cd wgtunnel
```
Check out my personal [fdroid repository](https://github.com/zaneschepke/fdroid) to get updates the
moment they are released.
## Docs
Information about features, behaviors, and answers to common questions can be found in the
app [documentation](https://zaneschepke.com/wgtunnel-docs/overview.html).
The repository for these docs can be found [here](https://github.com/zaneschepke/wgtunnel-docs).
```sh
./gradlew assembleDebug
```
## Translation
@@ -87,19 +114,6 @@ Help translate WG Tunnel into your language
at [Hosted Weblate](https://hosted.weblate.org/engage/wg-tunnel/).\
[![Translation status](https://hosted.weblate.org/widgets/wg-tunnel/-/multi-auto.svg)](https://hosted.weblate.org/engage/wg-tunnel/)
## Building
```
$ git clone https://github.com/zaneschepke/wgtunnel
$ cd wgtunnel
```
And then build the app:
```
$ ./gradlew assembleDebug
```
## Contributing
Any contributions in the form of feedback, issues, code, or translations are welcome and much
+22 -12
View File
@@ -14,7 +14,7 @@ val versionCodeIncrement = with(getBuildTaskName().lowercase()) {
when {
this.contains(Constants.NIGHTLY) || this.contains(Constants.PRERELEASE) -> {
if (versionFile.exists()) {
versionFile.readText().toInt() + 1
versionFile.readText().trim().toInt() + 1
} else {
1
}
@@ -31,6 +31,14 @@ android {
generateLocaleConfig = true
}
// reproducibility
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
defaultConfig {
applicationId = Constants.APP_ID
minSdk = Constants.MIN_SDK
@@ -44,6 +52,8 @@ android {
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
}
buildConfigField("String[]", "LANGUAGES", "new String[]{ ${languageList().joinToString(separator = ", ") { "\"$it\"" }} }")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { useSupportLibrary = true }
}
@@ -76,7 +86,6 @@ android {
}
debug {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
resValue("string", "app_name", "WG Tunnel - Debug")
isDebuggable = true
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.debug\"")
@@ -85,7 +94,6 @@ android {
create(Constants.PRERELEASE) {
initWith(buildTypes.getByName(Constants.RELEASE))
applicationIdSuffix = ".prerelease"
versionNameSuffix = "-pre"
resValue("string", "app_name", "WG Tunnel - Pre")
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.pre\"")
}
@@ -93,7 +101,6 @@ android {
create(Constants.NIGHTLY) {
initWith(buildTypes.getByName(Constants.RELEASE))
applicationIdSuffix = ".nightly"
versionNameSuffix = "-nightly"
resValue("string", "app_name", "WG Tunnel - Nightly")
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.nightly\"")
}
@@ -134,8 +141,8 @@ android {
}
dependencies {
implementation(project(":logcatter"))
implementation(project(":networkmonitor"))
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
@@ -149,6 +156,8 @@ dependencies {
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.storage)
// test
testImplementation(libs.junit)
@@ -161,8 +170,7 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.manifest)
// get tunnel lib from github packages or mavenLocal
// implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
// tunnel
implementation(libs.tunnel)
implementation(libs.amneziawg.android)
coreLibraryDesugaring(libs.desugar.jdk.libs)
@@ -177,10 +185,10 @@ dependencies {
// hilt
implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler)
// accompanist
implementation(libs.accompanist.permissions)
implementation(libs.accompanist.flowlayout)
implementation(libs.accompanist.drawablepainter)
// storage
@@ -194,13 +202,12 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.process)
// icons
implementation(libs.material.icons.extended)
// serialization
implementation(libs.kotlinx.serialization.json)
// barcode scanning
// ui
implementation(libs.zxing.android.embedded)
implementation(libs.material.icons.extended)
// bio
implementation(libs.androidx.biometric.ktx)
@@ -208,10 +215,13 @@ dependencies {
// shortcuts
implementation(libs.androidx.core)
implementation(libs.androidx.core.google.shortcuts)
// splash
implementation(libs.androidx.core.splashscreen)
// worker
implementation(libs.androidx.work.runtime)
implementation(libs.androidx.hilt.work)
}
fun determineVersionName(): String {
+38 -1
View File
@@ -2,4 +2,41 @@
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
<fields>;
}
}
# Keep all classes in the org.xbill.DNS package and subpackages
-keep class org.xbill.DNS.** { *; }
-dontwarn org.xbill.DNS.**
# Preserve JNA classes if used (e.g., for IPHlpAPI on Windows)
-keep class com.sun.jna.** { *; }
-dontwarn com.sun.jna.**
# Keep DNS resolver configuration classes that might be loaded dynamically
-keep class org.xbill.DNS.config.** { *; }
-dontwarn org.xbill.DNS.config.**
-keep class org.xbill.DNS.** { *; }
# Prevent optimization issues with native or reflection-based calls
-dontoptimize
-dontshrink
# Uncomment the above if errors persist, but use sparingly as theyre broad
# Suppress warnings about missing classes if not all features are used
-dontwarn java.lang.management.**
-dontwarn sun.nio.ch.**
-dontwarn com.google.api.client.http.GenericUrl
-dontwarn com.google.api.client.http.HttpHeaders
-dontwarn com.google.api.client.http.HttpRequest
-dontwarn com.google.api.client.http.HttpRequestFactory
-dontwarn com.google.api.client.http.HttpResponse
-dontwarn com.google.api.client.http.HttpTransport
-dontwarn com.google.api.client.http.javanet.NetHttpTransport$Builder
-dontwarn com.google.api.client.http.javanet.NetHttpTransport
-dontwarn javax.lang.model.element.Modifier
-dontwarn org.joda.time.Instant
-dontwarn org.slf4j.impl.StaticLoggerBinder
-dontwarn org.slf4j.impl.StaticMDCBinder
-dontwarn org.slf4j.impl.StaticMarkerBinder
+38 -1
View File
@@ -21,4 +21,41 @@
#-renamesourcefileattribute SourceFile
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
<fields>;
}
}
# Keep all classes in the org.xbill.DNS package and subpackages
-keep class org.xbill.DNS.** { *; }
-dontwarn org.xbill.DNS.**
# Preserve JNA classes if used (e.g., for IPHlpAPI on Windows)
-keep class com.sun.jna.** { *; }
-dontwarn com.sun.jna.**
# Keep DNS resolver configuration classes that might be loaded dynamically
-keep class org.xbill.DNS.config.** { *; }
-dontwarn org.xbill.DNS.config.**
-keep class org.xbill.DNS.** { *; }
# Prevent optimization issues with native or reflection-based calls
-dontoptimize
-dontshrink
# Uncomment the above if errors persist, but use sparingly as theyre broad
# Suppress warnings about missing classes if not all features are used
-dontwarn java.lang.management.**
-dontwarn sun.nio.ch.**
-dontwarn com.google.api.client.http.GenericUrl
-dontwarn com.google.api.client.http.HttpHeaders
-dontwarn com.google.api.client.http.HttpRequest
-dontwarn com.google.api.client.http.HttpRequestFactory
-dontwarn com.google.api.client.http.HttpResponse
-dontwarn com.google.api.client.http.HttpTransport
-dontwarn com.google.api.client.http.javanet.NetHttpTransport$Builder
-dontwarn com.google.api.client.http.javanet.NetHttpTransport
-dontwarn javax.lang.model.element.Modifier
-dontwarn org.joda.time.Instant
-dontwarn org.slf4j.impl.StaticLoggerBinder
-dontwarn org.slf4j.impl.StaticMDCBinder
-dontwarn org.slf4j.impl.StaticMarkerBinder
@@ -0,0 +1,232 @@
{
"formatVersion": 1,
"database": {
"version": 11,
"identityHash": "4c9418386f72dfac5d28ab96c1e5ea0b",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAmneziaEnabled",
"columnName": "is_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWifiNameByShellEnabled",
"columnName": "is_wifi_by_shell_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingInterval",
"columnName": "ping_interval",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingCooldown",
"columnName": "ping_cooldown",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingIp",
"columnName": "ping_ip",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "null"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4c9418386f72dfac5d28ab96c1e5ea0b')"
]
}
}
@@ -0,0 +1,246 @@
{
"formatVersion": 1,
"database": {
"version": 12,
"identityHash": "acf79ac5defacda5be6c3f976e777de3",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAmneziaEnabled",
"columnName": "is_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWifiNameByShellEnabled",
"columnName": "is_wifi_by_shell_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingInterval",
"columnName": "ping_interval",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingCooldown",
"columnName": "ping_cooldown",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingIp",
"columnName": "ping_ip",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'acf79ac5defacda5be6c3f976e777de3')"
]
}
}
@@ -0,0 +1,267 @@
{
"formatVersion": 1,
"database": {
"version": 13,
"identityHash": "ff209157b98a641c424f5086818ec585",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAmneziaEnabled",
"columnName": "is_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWifiNameByShellEnabled",
"columnName": "is_wifi_by_shell_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isVpnKillSwitchEnabled",
"columnName": "is_vpn_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelKillSwitchEnabled",
"columnName": "is_kernel_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingInterval",
"columnName": "ping_interval",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingCooldown",
"columnName": "ping_cooldown",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingIp",
"columnName": "ping_ip",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ff209157b98a641c424f5086818ec585')"
]
}
}
@@ -0,0 +1,274 @@
{
"formatVersion": 1,
"database": {
"version": 14,
"identityHash": "f2b260c389fb2e53216de40e4b1047f3",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAmneziaEnabled",
"columnName": "is_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWifiNameByShellEnabled",
"columnName": "is_wifi_by_shell_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isVpnKillSwitchEnabled",
"columnName": "is_vpn_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelKillSwitchEnabled",
"columnName": "is_kernel_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingInterval",
"columnName": "ping_interval",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingCooldown",
"columnName": "ping_cooldown",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingIp",
"columnName": "ping_ip",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f2b260c389fb2e53216de40e4b1047f3')"
]
}
}
@@ -0,0 +1,281 @@
{
"formatVersion": 1,
"database": {
"version": 15,
"identityHash": "4827f3b1ab5a4e5aa35937a0925d50e4",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_disable_kill_switch_on_trusted_enabled` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAmneziaEnabled",
"columnName": "is_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWifiNameByShellEnabled",
"columnName": "is_wifi_by_shell_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isVpnKillSwitchEnabled",
"columnName": "is_vpn_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelKillSwitchEnabled",
"columnName": "is_kernel_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "isDisableKillSwitchOnTrustedEnabled",
"columnName": "is_disable_kill_switch_on_trusted_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingInterval",
"columnName": "ping_interval",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingCooldown",
"columnName": "ping_cooldown",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingIp",
"columnName": "ping_ip",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4827f3b1ab5a4e5aa35937a0925d50e4')"
]
}
}
@@ -0,0 +1,288 @@
{
"formatVersion": 1,
"database": {
"version": 16,
"identityHash": "ae51793c4d09ea3194ecd26f0606f35c",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_disable_kill_switch_on_trusted_enabled` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAmneziaEnabled",
"columnName": "is_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWifiNameByShellEnabled",
"columnName": "is_wifi_by_shell_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isVpnKillSwitchEnabled",
"columnName": "is_vpn_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelKillSwitchEnabled",
"columnName": "is_kernel_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "isDisableKillSwitchOnTrustedEnabled",
"columnName": "is_disable_kill_switch_on_trusted_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `is_ipv4_preferred` INTEGER NOT NULL DEFAULT true)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingInterval",
"columnName": "ping_interval",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingCooldown",
"columnName": "ping_cooldown",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingIp",
"columnName": "ping_ip",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isIpv4Preferred",
"columnName": "is_ipv4_preferred",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "true"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ae51793c4d09ea3194ecd26f0606f35c')"
]
}
}
+41 -62
View File
@@ -3,26 +3,12 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
<uses-permission
android:name="android.permission.ACCESS_WIFI_STATE"
android:maxSdkVersion="30"
tools:ignore="LeanbackUsesWifi" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!--foreground service exempt android 14-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
tools:ignore="ProtectedPermissions" />
<!--foreground service permissions-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@@ -49,6 +35,12 @@
<uses-feature
android:name="android.hardware.screen.portrait"
android:required="false" />
<uses-feature
android:name="android.hardware.gamepad"
android:required="false"/>
<uses-feature android:name="android.hardware.wifi"
android:required="false"/>
<queries>
<intent>
@@ -69,19 +61,20 @@
android:theme="@style/Theme.App.Start"
tools:targetApi="tiramisu">
<activity
android:name=".ui.MainActivity"
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.WireguardAutoTunnel">
android:windowSoftInputMode="adjustNothing"
android:theme="@style/Theme.WireguardAutoTunnel"
android:configChanges="orientation|screenSize|keyboardHidden"
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.SHOW_APP_INFO" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
@@ -89,7 +82,7 @@
tools:replace="screenOrientation" />
<activity
android:name=".service.shortcut.ShortcutsActivity"
android:name=".core.shortcut.ShortcutsActivity"
android:enabled="true"
android:exported="true"
android:noHistory="true"
@@ -108,11 +101,18 @@
android:resource="@xml/file_paths" />
</provider>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:multiprocess="true"
tools:node="remove">
</provider>
<service
android:name=".service.tile.TunnelControlTile"
android:name=".core.service.tile.TunnelControlTile"
android:exported="true"
android:icon="@drawable/ic_launcher"
android:label="Tunnel control"
android:label="@string/tunnel_control"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data
android:name="android.service.quicksettings.ACTIVE_TILE"
@@ -126,10 +126,10 @@
</intent-filter>
</service>
<service
android:name=".service.tile.AutoTunnelControlTile"
android:name=".core.service.tile.AutoTunnelControlTile"
android:exported="true"
android:icon="@drawable/ic_launcher"
android:label="Auto-tunnel"
android:label="@string/auto_tunnel"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data
android:name="android.service.quicksettings.ACTIVE_TILE"
@@ -142,23 +142,8 @@
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".service.tunnel.AlwaysOnVpnService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="systemExempted"
android:permission="android.permission.BIND_VPN_SERVICE"
android:persistent="true"
tools:node="merge">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
<meta-data
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
android:value="true" />
</service>
<service
android:name=".service.foreground.AutoTunnelService"
android:name=".core.service.autotunnel.AutoTunnelService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="systemExempted"
@@ -167,8 +152,9 @@
tools:node="merge" />
<service
android:name=".service.foreground.TunnelBackgroundService"
android:name=".core.service.TunnelForegroundService"
android:exported="false"
android:persistent="true"
android:foregroundServiceType="systemExempted"
android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter>
@@ -177,36 +163,29 @@
</service>
<receiver
android:name=".receiver.BootReceiver"
android:name=".core.broadcast.RestartReceiver"
android:enabled="true"
android:exported="false">
android:exported="true">
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.BackgroundActionReceiver"
android:enabled="true"
android:exported="false"/>
<receiver
android:name=".receiver.AppUpdateReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.KernelReceiver"
android:name=".core.broadcast.KernelReceiver"
android:exported="false"
android:permission="${applicationId}.permission.CONTROL_TUNNELS">
<intent-filter>
<action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" />
</intent-filter>
</receiver>
<receiver
android:name=".core.broadcast.NotificationActionReceiver"
android:exported="false"
android:permission="${applicationId}.permission.CONTROL_TUNNELS">
</receiver>
</application>
</manifest>
@@ -0,0 +1,390 @@
package com.zaneschepke.wireguardautotunnel
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.activity.SystemBarStyle
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Bolt
import androidx.compose.material.icons.rounded.Home
import androidx.compose.material.icons.rounded.QuestionMark
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarData
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.CustomBottomNavbar
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.DynamicTopAppBar
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.currentNavBackStackEntryAsNavBarState
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.TunnelAutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.scanner.ScannerScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.SplitTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.TunnelOptionsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.pin.PinLockScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.AppearanceScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.AutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.AdvancedScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.disclosure.LocationDisclosureScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.KillSwitchScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.extensions.goFromRoot
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
import dagger.hilt.android.AndroidEntryPoint
import org.amnezia.awg.backend.GoBackend.VpnService
import timber.log.Timber
import javax.inject.Inject
import kotlin.system.exitProcess
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var appStateRepository: AppStateRepository
@Inject
lateinit var tunnelManager: TunnelManager
@Inject
lateinit var networkMonitor: NetworkMonitor
private var lastLocationPermissionState: Boolean? = null
@SuppressLint("BatteryLife")
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.Companion.auto(Color.TRANSPARENT, Color.TRANSPARENT),
navigationBarStyle = SystemBarStyle.Companion.auto(Color.TRANSPARENT, Color.TRANSPARENT),
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
super.onCreate(savedInstanceState)
val viewModel by viewModels<AppViewModel>()
installSplashScreen().apply {
setKeepOnScreenCondition {
!viewModel.appViewState.value.isAppReady
}
}
setContent {
val appUiState by viewModel.uiState.collectAsStateWithLifecycle()
val appViewState by viewModel.appViewState.collectAsStateWithLifecycle()
val navController = rememberNavController()
val backStackEntry by navController.currentBackStackEntryAsState()
val navBarState by currentNavBackStackEntryAsNavBarState(navController, backStackEntry, viewModel, appUiState)
val snackbar = remember { SnackbarHostState() }
var showVpnPermissionDialog by remember { mutableStateOf(false) }
var vpnPermissionDenied by remember { mutableStateOf(false) }
val vpnActivity =
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
onResult = {
if (it.resultCode != RESULT_OK) {
showVpnPermissionDialog = true
vpnPermissionDenied = true
} else {
vpnPermissionDenied = false
}
},
)
LaunchedEffect(appUiState.tunnels) {
if (!appViewState.isAppReady) {
viewModel.handleEvent(AppEvent.AppReadyCheck(appUiState.tunnels))
}
}
val batteryActivity = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
) { _: ActivityResult ->
viewModel.handleEvent(AppEvent.SetBatteryOptimizeDisableShown)
}
with(appViewState) {
LaunchedEffect(isConfigChanged) {
if (isConfigChanged) {
Intent(this@MainActivity, MainActivity::class.java).also {
startActivity(it)
exitProcess(0)
}
}
}
LaunchedEffect(errorMessage) {
errorMessage?.let {
snackbar.showSnackbar(it.asString(this@MainActivity))
viewModel.handleEvent(AppEvent.MessageShown)
}
}
LaunchedEffect(appUiState.activeTunnels) {
appUiState.activeTunnels
.mapNotNull { (tunnelConf, tunnelState) ->
(tunnelState.status as? TunnelStatus.Error)?.let { error ->
val message = error.error.toStringRes()
val context = this@MainActivity
snackbar.showSnackbar(context.getString(R.string.tunnel_error_template, context.getString(message)))
viewModel.handleEvent(AppEvent.ClearTunnelError(tunnelConf))
}
}
}
LaunchedEffect(popBackStack) {
if (popBackStack) {
navController.popBackStack()
viewModel.handleEvent(AppEvent.PopBackStack(false))
}
}
LaunchedEffect(requestVpnPermission) {
if (requestVpnPermission) {
if (!vpnPermissionDenied) {
vpnActivity.launch(VpnService.prepare(this@MainActivity))
} else {
showVpnPermissionDialog = true
}
viewModel.handleEvent(AppEvent.VpnPermissionRequested)
}
}
LaunchedEffect(requestBatteryPermission) {
if (requestBatteryPermission) {
batteryActivity.launch(
Intent().apply {
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
data = Uri.parse("package:${this@MainActivity.packageName}")
},
)
}
}
}
CompositionLocalProvider(LocalNavController provides navController) {
WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) {
VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false })
Scaffold(
modifier = Modifier.pointerInput(Unit) {
detectTapGestures {
viewModel.handleEvent(AppEvent.SetSelectedTunnel(null))
}
},
snackbarHost = {
SnackbarHost(snackbar) { snackbarData: SnackbarData ->
CustomSnackBar(
snackbarData.visuals.message,
isRtl = false,
containerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation(
2.dp,
),
)
}
},
topBar = {
DynamicTopAppBar(navBarState)
},
bottomBar = {
AnimatedVisibility(
visible = navBarState.showBottom,
enter = slideInVertically(initialOffsetY = { it }),
exit = slideOutVertically(targetOffsetY = { it }),
) {
CustomBottomNavbar(
listOf(
BottomNavItem(
name = stringResource(R.string.tunnels),
route = Route.Main,
icon = Icons.Rounded.Home,
onClick = { navController.goFromRoot(Route.Main) },
),
BottomNavItem(
name = stringResource(R.string.auto_tunnel),
route = Route.AutoTunnel,
icon = Icons.Rounded.Bolt,
onClick = {
val route = if (appUiState.generalState.isLocationDisclosureShown) Route.AutoTunnel else Route.LocationDisclosure
navController.goFromRoot(route)
},
active = appUiState.isAutoTunnelActive,
),
BottomNavItem(
name = stringResource(R.string.settings),
route = Route.Settings,
icon = Icons.Rounded.Settings,
onClick = { navController.goFromRoot(Route.Settings) },
),
BottomNavItem(
name = stringResource(R.string.support),
route = Route.Support,
icon = Icons.Rounded.QuestionMark,
onClick = { navController.goFromRoot(Route.Support) },
),
),
navBarState = navBarState,
)
}
},
) { padding ->
Box(
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.surface)
.padding(padding)
.consumeWindowInsets(padding)
.imePadding(),
) {
NavHost(
navController,
startDestination = (if (appUiState.generalState.isPinLockEnabled) Route.Lock else Route.Main),
) {
composable<Route.Main> {
MainScreen(appUiState, appViewState, viewModel)
}
composable<Route.Settings> {
SettingsScreen(appUiState, appViewState, viewModel)
}
composable<Route.LocationDisclosure> {
LocationDisclosureScreen(appUiState, viewModel)
}
composable<Route.AutoTunnel> {
AutoTunnelScreen(appUiState, viewModel)
}
composable<Route.Appearance> {
AppearanceScreen()
}
composable<Route.Language> {
LanguageScreen(appUiState, viewModel)
}
composable<Route.Display> {
DisplayScreen(appUiState, viewModel)
}
composable<Route.Support> {
SupportScreen()
}
composable<Route.AutoTunnelAdvanced> {
AdvancedScreen(appUiState)
}
composable<Route.Logs> {
LogsScreen(appViewState, viewModel)
}
composable<Route.Config> { backStack ->
val args = backStack.toRoute<Route.Config>()
val config = appUiState.tunnels.firstOrNull { it.id == args.id }
ConfigScreen(config, viewModel)
}
composable<Route.TunnelOptions> { backStack ->
val args = backStack.toRoute<Route.TunnelOptions>()
appUiState.tunnels.firstOrNull { it.id == args.id }?.let { config ->
TunnelOptionsScreen(config, appUiState, viewModel)
}
}
composable<Route.Lock> {
PinLockScreen(viewModel)
}
composable<Route.Scanner> {
ScannerScreen(viewModel)
}
composable<Route.KillSwitch> {
KillSwitchScreen(appUiState, viewModel)
}
composable<Route.SplitTunnel> {
SplitTunnelScreen(viewModel)
}
composable<Route.TunnelAutoTunnel> { backStack ->
val args = backStack.toRoute<Route.TunnelOptions>()
appUiState.tunnels.firstOrNull { it.id == args.id }?.let {
TunnelAutoTunnelScreen(it, appUiState.appSettings, viewModel)
}
}
}
BackHandler {
if (navController.currentDestination?.route != Route.Main::class.qualifiedName) {
navController.popBackStack()
} else {
this@MainActivity.finish()
}
}
}
}
}
}
}
}
override fun onResume() {
super.onResume()
checkPermissionAndNotify()
}
private fun checkPermissionAndNotify() {
val hasLocation = ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION,
) == PackageManager.PERMISSION_GRANTED
if (lastLocationPermissionState != hasLocation) {
Timber.d("Location permission changed to: $hasLocation")
if (hasLocation) {
networkMonitor.sendLocationPermissionsGrantedBroadcast()
}
lastLocationPermissionState = hasLocation
}
}
}
@@ -3,20 +3,40 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import androidx.hilt.work.HiltWorkerFactory
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.work.Configuration
import com.wireguard.android.backend.GoBackend
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
@HiltAndroidApp
class WireGuardAutoTunnel : Application() {
class WireGuardAutoTunnel : Application(), Configuration.Provider {
@Inject
lateinit var workerFactory: HiltWorkerFactory
override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
@Inject
@ApplicationScope
@@ -25,13 +45,24 @@ class WireGuardAutoTunnel : Application() {
@Inject
lateinit var logReader: LogReader
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
@Inject
@MainDispatcher
lateinit var mainDispatcher: CoroutineDispatcher
@Inject
lateinit var tunnelManager: TunnelManager
override fun onCreate() {
super.onCreate()
instance = this
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver())
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
StrictMode.setThreadPolicy(
@@ -45,14 +76,77 @@ class WireGuardAutoTunnel : Application() {
} else {
Timber.plant(ReleaseTree())
}
if (!isRunningOnTv()) {
applicationScope.launch(ioDispatcher) {
logReader.start()
GoBackend.setAlwaysOnCallback {
applicationScope.launch {
val settings = appDataRepository.settings.get()
if (settings.isAlwaysOnVpnEnabled) {
val tunnel = appDataRepository.getPrimaryOrFirstTunnel()
tunnel?.let {
tunnelManager.startTunnel(it)
}
} else {
Timber.w("Always-on VPN is not enabled in app settings")
}
}
}
ServiceWorker.start(this)
applicationScope.launch {
if (!appDataRepository.settings.get().isKernelEnabled) {
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
}
appDataRepository.appState.getLocale()?.let {
withContext(mainDispatcher) {
LocaleUtil.changeLocale(it)
}
}
appDataRepository.appState.isLocalLogsEnabled().let { enabled ->
if (enabled) logReader.start()
}
}
}
override fun onTerminate() {
applicationScope.launch {
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
}
super.onTerminate()
}
class AppLifecycleObserver : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
Timber.d("Application entered foreground")
foreground = true
}
override fun onPause(owner: LifecycleOwner) {
Timber.d("Application entered background")
foreground = false
}
}
companion object {
private var foreground = false
fun isForeground(): Boolean {
return foreground
}
@Volatile
private var lastActiveTunnels: List<Int> = emptyList()
@Synchronized
fun getLastActiveTunnels(): List<Int> {
return lastActiveTunnels
}
@Synchronized
fun setLastActiveTunnels(newTunnels: List<Int>) {
lastActiveTunnels = newTunnels
}
lateinit var instance: WireGuardAutoTunnel
private set
}
@@ -0,0 +1,49 @@
package com.zaneschepke.wireguardautotunnel.core.broadcast
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class KernelReceiver : BroadcastReceiver() {
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var tunnelRepository: TunnelRepository
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return
applicationScope.launch {
if (action == REFRESH_TUNNELS_ACTION) {
tunnelManager.runningTunnelNames().forEach { name ->
val tunnel = tunnelRepository.findByTunnelName(name)
tunnel?.let {
tunnelRepository.save(it.copy(isActive = true))
}
}
serviceManager.updateTunnelTile()
}
}
}
companion object {
const val REFRESH_TUNNELS_ACTION = "com.wireguard.android.action.REFRESH_TUNNEL_STATES"
}
}
@@ -0,0 +1,50 @@
package com.zaneschepke.wireguardautotunnel.core.broadcast
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class NotificationActionReceiver : BroadcastReceiver() {
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
@Inject
lateinit var tunnelRepository: TunnelRepository
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
override fun onReceive(context: Context, intent: Intent) {
applicationScope.launch {
when (intent.action) {
NotificationAction.AUTO_TUNNEL_OFF.name -> serviceManager.stopAutoTunnel()
NotificationAction.TUNNEL_OFF.name -> {
val tunnelId = intent.getIntExtra(NotificationManager.EXTRA_ID, 0)
if (tunnelId == STOP_ALL_TUNNELS_ID) return@launch tunnelManager.stopTunnel()
val tunnel = tunnelRepository.getById(tunnelId)
tunnelManager.stopTunnel(tunnel)
}
}
}
}
companion object {
const val STOP_ALL_TUNNELS_ID = 0
}
}
@@ -0,0 +1,64 @@
package com.zaneschepke.wireguardautotunnel.core.broadcast
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class RestartReceiver : BroadcastReceiver() {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return
if (action != Intent.ACTION_BOOT_COMPLETED &&
action != Intent.ACTION_MY_PACKAGE_REPLACED &&
action != "com.htc.intent.action.QUICKBOOT_POWERON"
) {
return
}
Timber.d("RestartReceiver triggered with action: ${intent.action}")
serviceManager.updateTunnelTile()
serviceManager.updateAutoTunnelTile()
applicationScope.launch(ioDispatcher) {
val settings = appDataRepository.settings.get()
if (settings.isRestoreOnBootEnabled) {
if (settings.isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) {
Timber.d("Starting auto-tunnel on boot/update")
serviceManager.startAutoTunnel()
} else {
Timber.d("Restoring previous tunnel state")
tunnelManager.restorePreviousState()
}
} else {
Timber.d("Restore on boot disabled, skipping")
}
}
}
}
@@ -0,0 +1,46 @@
package com.zaneschepke.wireguardautotunnel.core.notification
import android.app.Notification
import android.app.NotificationManager
import android.content.Context
import androidx.core.app.NotificationCompat
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification.NotificationChannels
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.util.StringValue
interface NotificationManager {
val context: Context
fun createNotification(
channel: NotificationChannels,
title: String = "",
actions: Collection<NotificationCompat.Action> = emptyList(),
description: String = "",
showTimestamp: Boolean = false,
importance: Int = NotificationManager.IMPORTANCE_HIGH,
onGoing: Boolean = true,
onlyAlertOnce: Boolean = true,
): Notification
fun createNotification(
channel: NotificationChannels,
title: StringValue,
actions: Collection<NotificationCompat.Action> = emptyList(),
description: StringValue,
showTimestamp: Boolean = false,
importance: Int = NotificationManager.IMPORTANCE_HIGH,
onGoing: Boolean = true,
onlyAlertOnce: Boolean = true,
): Notification
fun createNotificationAction(notificationAction: NotificationAction, extraId: Int? = null): NotificationCompat.Action
fun remove(notificationId: Int)
fun show(notificationId: Int, notification: Notification)
companion object {
const val AUTO_TUNNEL_NOTIFICATION_ID = 122
const val VPN_NOTIFICATION_ID = 100
const val EXTRA_ID = "id"
}
}
@@ -0,0 +1,169 @@
package com.zaneschepke.wireguardautotunnel.core.notification
import android.Manifest
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.zaneschepke.wireguardautotunnel.MainActivity
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.broadcast.NotificationActionReceiver
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager.Companion.EXTRA_ID
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class WireGuardNotification
@Inject
constructor(
@ApplicationContext override val context: Context,
) : com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager {
enum class NotificationChannels {
VPN,
AUTO_TUNNEL,
}
private val notificationManager = NotificationManagerCompat.from(context)
override fun createNotification(
channel: NotificationChannels,
title: String,
actions: Collection<NotificationCompat.Action>,
description: String,
showTimestamp: Boolean,
importance: Int,
onGoing: Boolean,
onlyAlertOnce: Boolean,
): Notification {
notificationManager.createNotificationChannel(channel.asChannel())
return channel.asBuilder().apply {
actions.forEach {
addAction(it)
}
setContentTitle(title)
setContentIntent(
PendingIntent.getActivity(
context,
0,
Intent(context, MainActivity::class.java),
PendingIntent.FLAG_IMMUTABLE,
),
)
setContentText(description)
setOnlyAlertOnce(onlyAlertOnce)
setOngoing(onGoing)
setPriority(NotificationCompat.PRIORITY_HIGH)
setShowWhen(showTimestamp)
setSmallIcon(R.drawable.ic_launcher)
}.build()
}
override fun createNotification(
channel: NotificationChannels,
title: StringValue,
actions: Collection<NotificationCompat.Action>,
description: StringValue,
showTimestamp: Boolean,
importance: Int,
onGoing: Boolean,
onlyAlertOnce: Boolean,
): Notification {
return createNotification(
channel,
title.asString(context),
actions,
description.asString(context),
showTimestamp,
importance,
onGoing,
onlyAlertOnce,
)
}
override fun createNotificationAction(notificationAction: NotificationAction, extraId: Int?): NotificationCompat.Action {
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
Intent(context, NotificationActionReceiver::class.java).apply {
action = notificationAction.name
if (extraId != null) putExtra(EXTRA_ID, extraId)
},
PendingIntent.FLAG_IMMUTABLE,
)
return NotificationCompat.Action.Builder(
R.drawable.ic_launcher,
notificationAction.title(context).uppercase(),
pendingIntent,
).build()
}
override fun remove(notificationId: Int) {
notificationManager.cancel(notificationId)
}
override fun show(notificationId: Int, notification: Notification) {
with(notificationManager) {
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
return
}
notify(notificationId, notification)
}
}
private fun NotificationChannels.asBuilder(): NotificationCompat.Builder {
return when (this) {
NotificationChannels.AUTO_TUNNEL -> {
NotificationCompat.Builder(
context,
context.getString(R.string.auto_tunnel_channel_id),
)
}
NotificationChannels.VPN -> {
NotificationCompat.Builder(
context,
context.getString(R.string.vpn_channel_id),
)
}
}
}
private fun NotificationChannels.asChannel(): NotificationChannel {
return when (this) {
NotificationChannels.VPN -> {
NotificationChannel(
context.getString(R.string.vpn_channel_id),
context.getString(R.string.vpn_channel_name),
NotificationManager.IMPORTANCE_HIGH,
).apply {
description = context.getString(R.string.vpn_channel_description)
enableLights(true)
lightColor = Color.WHITE
enableVibration(false)
vibrationPattern = longArrayOf(100, 200, 300)
}
}
NotificationChannels.AUTO_TUNNEL -> {
NotificationChannel(
context.getString(R.string.auto_tunnel_channel_id),
context.getString(R.string.auto_tunnel_channel_name),
NotificationManager.IMPORTANCE_HIGH,
).apply {
description = context.getString(R.string.auto_tunnel_channel_description)
enableLights(true)
lightColor = Color.WHITE
enableVibration(false)
vibrationPattern = longArrayOf(100, 200, 300)
}
}
}
}
}
@@ -0,0 +1,130 @@
package com.zaneschepke.wireguardautotunnel.core.service
import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.VpnService
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import jakarta.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import timber.log.Timber
class ServiceManager @Inject constructor(
private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@ApplicationScope private val applicationScope: CoroutineScope,
@MainDispatcher private val mainDispatcher: CoroutineDispatcher,
private val appDataRepository: AppDataRepository,
) {
private val autoTunnelMutex = Mutex()
private val _autoTunnelActive = MutableStateFlow(false)
val autoTunnelActive = _autoTunnelActive.asStateFlow()
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
var backgroundService = CompletableDeferred<TunnelForegroundService>()
private fun <T : Service> startService(cls: Class<T>, background: Boolean) {
runCatching {
val intent = Intent(context, cls)
if (background) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}.onFailure { Timber.e(it) }
}
fun hasVpnPermission(): Boolean {
return VpnService.prepare(context) == null
}
suspend fun startAutoTunnel() {
autoTunnelMutex.withLock {
val settings = appDataRepository.settings.get()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true))
if (autoTunnelService.isCompleted) {
_autoTunnelActive.update { true }
return
}
runCatching {
autoTunnelService = CompletableDeferred()
startService(AutoTunnelService::class.java, !WireGuardAutoTunnel.isForeground())
_autoTunnelActive.update { true }
}.onFailure {
Timber.e(it)
_autoTunnelActive.update { false }
}
withContext(mainDispatcher) { updateAutoTunnelTile() }
}
}
suspend fun stopAutoTunnel() {
autoTunnelMutex.withLock {
val settings = appDataRepository.settings.get()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false))
if (!autoTunnelService.isCompleted) return
runCatching {
val service = autoTunnelService.await()
service.stop()
_autoTunnelActive.update { false }
autoTunnelService = CompletableDeferred()
}.onFailure {
Timber.e(it)
}
withContext(mainDispatcher) { updateAutoTunnelTile() }
}
}
fun startTunnelForegroundService() {
if (backgroundService.isCompleted) return
runCatching {
backgroundService = CompletableDeferred()
startService(TunnelForegroundService::class.java, !WireGuardAutoTunnel.isForeground())
}.onFailure {
Timber.e(it)
}
}
suspend fun stopTunnelForegroundService() {
if (!backgroundService.isCompleted) return
runCatching {
val service = backgroundService.await()
service.stop()
backgroundService = CompletableDeferred()
}.onFailure {
Timber.e(it)
}
}
fun toggleAutoTunnel() {
applicationScope.launch(ioDispatcher) {
if (_autoTunnelActive.value) stopAutoTunnel() else startAutoTunnel()
}
}
fun updateAutoTunnelTile() {
context.requestAutoTunnelTileServiceUpdate()
}
fun updateTunnelTile() {
context.requestTunnelTileServiceStateUpdate()
}
}
@@ -0,0 +1,272 @@
package com.zaneschepke.wireguardautotunnel.core.service
import android.app.Notification
import android.content.Intent
import android.os.IBinder
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.networkmonitor.NetworkStatus
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
@AndroidEntryPoint
class TunnelForegroundService : LifecycleService() {
@Inject
lateinit var notificationManager: NotificationManager
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var networkMonitor: NetworkMonitor
@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
@Inject
lateinit var tunnelRepo: TunnelRepository
@Inject
lateinit var tunnelManager: TunnelManager
private val isNetworkConnected = MutableStateFlow(true)
private val tunnelJobs = ConcurrentHashMap<TunnelConf, Job>()
override fun onCreate() {
super.onCreate()
serviceManager.backgroundService.complete(this)
ServiceCompat.startForeground(
this@TunnelForegroundService,
NotificationManager.VPN_NOTIFICATION_ID,
onCreateNotification(),
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
serviceManager.backgroundService.complete(this)
ServiceCompat.startForeground(
this@TunnelForegroundService,
NotificationManager.VPN_NOTIFICATION_ID,
onCreateNotification(),
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
start()
return START_STICKY
}
fun start() = lifecycleScope.launch {
tunnelManager.activeTunnels.distinctByKeys().collect { tuns ->
if (tuns.isEmpty() && tunnelJobs.isEmpty()) return@collect
if (tuns.isEmpty() && tunnelJobs.isNotEmpty()) {
return@collect tunnelJobs.forEach { (key, _) ->
Timber.d("Stopping all tunnel jobs")
tunnelJobs[key]?.cancel()
tunnelJobs.remove(key)
}
}
val (jobsToStop, jobsToStart) = findMissingKeys(tuns, tunnelJobs)
if (jobsToStop.isEmpty() && jobsToStart.isEmpty()) return@collect
jobsToStop.forEach { tun ->
Timber.d("Stopping tunnel jobs for ${tun.tunName}")
tunnelJobs[tun]?.cancel()
tunnelJobs.remove(tun)
}
jobsToStart.forEach { tun ->
Timber.d("Starting tunnel jobs for ${tun.tunName}")
tunnelJobs += (tun to startTunnelJobs(tun))
}
updateServiceNotification()
}
}
// TODO Would be cool to have this include kill switch
// TODO also we need to include errors
private fun updateServiceNotification() {
val notification = when (tunnelJobs.size) {
0 -> onCreateNotification()
1 -> createTunnelNotification(tunnelJobs.keys.first())
else -> createTunnelsNotification()
}
ServiceCompat.startForeground(
this@TunnelForegroundService,
NotificationManager.VPN_NOTIFICATION_ID,
notification,
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
}
// use same scope so we can cancel all of these
private fun startTunnelJobs(tunnelConf: TunnelConf) = lifecycleScope.launch {
// monitor if we have internet connectivity
launch { startNetworkMonitorJob() }
// job to trigger stats emit on interval
launch { startTunnelStatsJob(tunnelConf) }
// monitor changes to the tunnel config
launch { startTunnelConfChangesJob(tunnelConf) }
// monitor tunnel ping
launch { startPingJob(tunnelConf) }
}
private fun findMissingKeys(map1: Map<TunnelConf, Any>, map2: Map<TunnelConf, Any>): Pair<Set<TunnelConf>, Set<TunnelConf>> {
val missingMap1 = map2.keys - map1.keys
val missingMap2 = map1.keys - map2.keys
return missingMap1 to missingMap2
}
private suspend fun startTunnelConfChangesJob(tunnelConf: TunnelConf) {
tunnelRepo.flow
.flowOn(ioDispatcher)
.map { storedTunnels ->
storedTunnels.firstOrNull { it.id == tunnelConf.id }
}
.filterNotNull()
// only emit when one of these 3 values change
.distinctUntilChanged { old, new ->
old == new
}
.collect { storedTunnel ->
if (tunnelConf != storedTunnel) {
Timber.d("Config changed for ${storedTunnel.tunName}, bouncing")
// let this complete, even after cancel
withContext(NonCancellable) {
tunnelManager.bounceTunnel(storedTunnel, TunnelStatus.StopReason.CONFIG_CHANGED)
}
}
}
}
private suspend fun startNetworkMonitorJob() {
networkMonitor.networkStatusFlow
.flowOn(ioDispatcher)
.collectLatest { status ->
val isAvailable = status !is NetworkStatus.Disconnected
isNetworkConnected.value = isAvailable
Timber.d("Network available: $status")
}
}
private suspend fun startTunnelStatsJob(tunnel: TunnelConf) = coroutineScope {
while (isActive) {
tunnelManager.updateTunnelStatistics(tunnel)
delay(STATS_DELAY)
}
}
// TODO fix cooldown
private suspend fun startPingJob(tunnel: TunnelConf) = coroutineScope {
delay(PING_START_DELAY)
while (isActive) {
val shouldBounce = shouldBounceTunnel(tunnel)
val delayMs = if (shouldBounce) {
// let this complete, even after cancel
withContext(NonCancellable) {
tunnelManager.bounceTunnel(tunnel, TunnelStatus.StopReason.PING)
}
tunnel.pingCooldown ?: Constants.PING_COOLDOWN
} else {
tunnel.pingInterval ?: Constants.PING_INTERVAL
}
delay(delayMs)
}
}
private suspend fun shouldBounceTunnel(tunnel: TunnelConf): Boolean {
if (!isNetworkConnected.value) {
Timber.d("Network disconnected, skipping ping for ${tunnel.tunName}")
return false
}
return runCatching {
!tunnel.isTunnelPingable(ioDispatcher)
}.onFailure { e ->
Timber.e(e, "Ping check failed for ${tunnel.tunName}")
}.getOrDefault(true)
}
fun stop() {
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
stopSelf()
}
override fun onDestroy() {
serviceManager.backgroundService = CompletableDeferred()
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
super.onDestroy()
}
private fun createTunnelNotification(tunnelConf: TunnelConf): Notification {
return notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = "${getString(R.string.tunnel_running)} - ${tunnelConf.tunName}",
actions = listOf(
notificationManager.createNotificationAction(NotificationAction.TUNNEL_OFF, tunnelConf.id),
),
)
}
private fun createTunnelsNotification(): Notification {
return notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = "${getString(R.string.tunnel_running)} - ${getString(R.string.multiple)}",
actions = listOf(
notificationManager.createNotificationAction(NotificationAction.TUNNEL_OFF, 0),
),
)
}
private fun onCreateNotification(): Notification {
return notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = getString(R.string.tunnel_starting),
)
}
// TODO add notification handling and optional log reading for restart on handshake failures
companion object {
const val STATS_DELAY = 1_000L
const val PING_START_DELAY = 30_000L
// ipv6 disabled or block on network
// const val userspaceStartFailed = "Failed to send handshake initiation: write udp [::]"
// const val ipv6Fails = "Failed to send data packets: write udp [::]"
// const val ipv4Fails = "Failed to send data packets: write udp 0.0.0.0:51820"
}
}
@@ -0,0 +1,257 @@
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
import android.content.Intent
import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.networkmonitor.NetworkStatus
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class AutoTunnelService : LifecycleService() {
@Inject
lateinit var networkMonitor: NetworkMonitor
@Inject
lateinit var appDataRepository: Provider<AppDataRepository>
@Inject
lateinit var notificationManager: NotificationManager
@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
private val defaultState = AutoTunnelState()
private val autoTunnelStateFlow = MutableStateFlow(defaultState)
private var wakeLock: PowerManager.WakeLock? = null
private var killSwitchJob: Job? = null
override fun onCreate() {
super.onCreate()
serviceManager.autoTunnelService.complete(this)
launchWatcherNotification()
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
Timber.d("onStartCommand executed with startId: $startId")
serviceManager.autoTunnelService.complete(this)
start()
return START_STICKY
}
fun start() {
kotlin.runCatching {
launchWatcherNotification()
initWakeLock()
startAutoTunnelJob()
startAutoTunnelStateJob()
killSwitchJob = startKillSwitchJob()
}.onFailure {
Timber.e(it)
}
}
fun stop() {
wakeLock?.let { if (it.isHeld) it.release() }
stopSelf()
}
override fun onDestroy() {
serviceManager.autoTunnelService = CompletableDeferred()
restoreVpnKillSwitch()
super.onDestroy()
}
private fun restoreVpnKillSwitch() {
with(autoTunnelStateFlow.value) {
if (settings.isVpnKillSwitchEnabled && tunnelManager.getBackendState() != BackendState.KILL_SWITCH_ACTIVE) {
killSwitchJob?.cancel()
val allowedIps = if (settings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS else emptyList()
tunnelManager.setBackendState(BackendState.KILL_SWITCH_ACTIVE, allowedIps)
}
}
}
private fun launchWatcherNotification(description: String = getString(R.string.monitoring_state_changes)) {
val notification =
notificationManager.createNotification(
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
title = getString(R.string.auto_tunnel_title),
description = description,
actions = listOf(
notificationManager.createNotificationAction(NotificationAction.AUTO_TUNNEL_OFF),
),
)
ServiceCompat.startForeground(
this,
NotificationManager.AUTO_TUNNEL_NOTIFICATION_ID,
notification,
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
}
private fun initWakeLock() {
wakeLock = (getSystemService(POWER_SERVICE) as PowerManager).run {
val tag = this.javaClass.name
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
try {
Timber.i("Initiating wakelock with 10 min timeout")
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
} finally {
release()
}
}
}
}
private fun buildNetworkState(networkStatus: NetworkStatus): NetworkState {
return with(autoTunnelStateFlow.value.networkState) {
val wifiName = when (networkStatus) {
is NetworkStatus.Connected -> {
networkStatus.wifiSsid
}
else -> null
}
copy(
isWifiConnected = networkStatus.wifiConnected,
isMobileDataConnected = networkStatus.cellularConnected,
isEthernetConnected = networkStatus.ethernetConnected,
wifiName = wifiName,
)
}
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) {
combine(
combineSettings(),
appDataRepository.get().settings.flow
.distinctUntilChanged { old, new -> old.isKernelEnabled == new.isKernelEnabled } // Only emit when isKernelEnabled changes
.flatMapLatest {
networkMonitor.networkStatusFlow
.flowOn(ioDispatcher)
.map { buildNetworkState(it) }
}
.distinctUntilChanged(),
) { double, networkState ->
AutoTunnelState(
tunnelManager.activeTunnels.value,
networkState,
double.first,
double.second,
)
}.collect { state ->
autoTunnelStateFlow.update {
it.copy(
activeTunnels = state.activeTunnels,
networkState = state.networkState,
settings = state.settings,
tunnels = state.tunnels,
)
}
}
}
private fun combineSettings(): Flow<Pair<AppSettings, Tunnels>> {
return combine(
appDataRepository.get().settings.flow,
appDataRepository.get().tunnels.flow.map { tunnels ->
// isActive is ignored for equality checks so user can manually toggle off tunnel with auto-tunnel
tunnels.map { it.copy(isActive = false) }
},
) { settings, tunnels ->
Pair(settings, tunnels)
}.distinctUntilChanged()
}
private fun startKillSwitchJob() = lifecycleScope.launch(ioDispatcher) {
autoTunnelStateFlow.collect {
if (it == defaultState) return@collect
when (val event = it.asKillSwitchEvent()) {
KillSwitchEvent.DoNothing -> Unit
is KillSwitchEvent.Start -> {
Timber.d("Starting kill switch")
tunnelManager.setBackendState(BackendState.KILL_SWITCH_ACTIVE, event.allowedIps)
}
KillSwitchEvent.Stop -> {
Timber.d("Stopping kill switch")
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptySet())
}
}
}
}
@OptIn(FlowPreview::class)
private fun startAutoTunnelJob() = lifecycleScope.launch(ioDispatcher) {
Timber.i("Starting auto-tunnel network event watcher")
val settings = appDataRepository.get().settings.get()
Timber.d("Starting with debounce delay of: ${settings.debounceDelaySeconds} seconds")
autoTunnelStateFlow.debounce(settings.debounceDelayMillis()).collect { watcherState ->
if (watcherState == defaultState) return@collect
Timber.d("New auto tunnel state emitted ${watcherState.networkState}")
when (val event = watcherState.asAutoTunnelEvent()) {
is AutoTunnelEvent.Start -> (event.tunnelConf ?: appDataRepository.get().getPrimaryOrFirstTunnel())?.let {
tunnelManager.startTunnel(it)
}
// TODO improve this to target specific tunnels to better support multi-tunnel
is AutoTunnelEvent.Stop -> tunnelManager.stopTunnel()
AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: no condition met")
}
}
}
}
@@ -0,0 +1,106 @@
package com.zaneschepke.wireguardautotunnel.core.service.tile
import android.content.Intent
import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class AutoTunnelControlTile : TileService(), LifecycleOwner {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var serviceManager: ServiceManager
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
override fun onCreate() {
super.onCreate()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
override fun onDestroy() {
super.onDestroy()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
override fun onStartListening() {
super.onStartListening()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
Timber.d("Start listening called for auto tunnel tile")
lifecycleScope.launch {
serviceManager.autoTunnelActive.collect {
if (it) return@collect setActive()
setInactive()
}
}
lifecycleScope.launch {
appDataRepository.tunnels.flow.collect {
if (it.isEmpty()) {
setUnavailable()
}
}
}
}
override fun onClick() {
super.onClick()
unlockAndRun {
lifecycleScope.launch {
if (serviceManager.autoTunnelActive.value) {
serviceManager.stopAutoTunnel()
setInactive()
} else {
serviceManager.startAutoTunnel()
setActive()
}
}
}
}
private fun setActive() {
runCatching {
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
}
}
private fun setInactive() {
runCatching {
qsTile.state = Tile.STATE_INACTIVE
qsTile.updateTile()
}
}
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (_: Throwable) {
Timber.e("Failed to bind to TunnelControlTile")
}
return ret
}
private fun setUnavailable() {
runCatching {
qsTile.state = Tile.STATE_UNAVAILABLE
qsTile.updateTile()
}
}
override val lifecycle: Lifecycle
get() = lifecycleRegistry
}
@@ -0,0 +1,191 @@
package com.zaneschepke.wireguardautotunnel.core.service.tile
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class TunnelControlTile : TileService(), LifecycleOwner {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
private var isCollecting = false
override fun onCreate() {
super.onCreate()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
override fun onDestroy() {
super.onDestroy()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
override fun onStartListening() {
super.onStartListening()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
Timber.d("Start listening called for tunnel tile")
if (isCollecting) return
isCollecting = true
lifecycleScope.launch {
tunnelManager.activeTunnels.collect {
updateTileState()
}
}
}
private suspend fun updateTileState() {
try {
val tunnels = appDataRepository.tunnels.getAll()
if (tunnels.isEmpty()) {
setUnavailable()
return
}
val activeTunnels = tunnelManager.activeTunnels.value
.filter { it.value.status.isUpOrStarting() }
when {
activeTunnels.isNotEmpty() -> {
val activeIds = activeTunnels.map { it.key.id }
// TODO improvements would be needed to make this work well with toggling multiple tunnels
// this would be better managed elsewhere
WireGuardAutoTunnel.setLastActiveTunnels(activeIds)
updateTileForActiveTunnels(activeTunnels)
}
else -> updateTileForLastActiveTunnels()
}
} catch (e: Exception) {
setUnavailable()
}
}
private fun updateTileForActiveTunnels(activeTunnels: Map<TunnelConf, TunnelState>) {
val tileName = when (activeTunnels.size) {
1 -> activeTunnels.keys.first().tunName
else -> getString(R.string.multiple)
}
updateTile(tileName, true)
}
private suspend fun updateTileForLastActiveTunnels() {
val lastActiveIds = WireGuardAutoTunnel.getLastActiveTunnels()
when {
lastActiveIds.isEmpty() -> {
appDataRepository.getStartTunnelConfig()?.let { config ->
updateTile(config.tunName, false)
} ?: setUnavailable()
}
lastActiveIds.size > 1 -> updateTile(getString(R.string.multiple), false)
else -> {
val tunnelId = lastActiveIds.first()
appDataRepository.tunnels.getById(tunnelId)?.let { tunnel ->
updateTile(tunnel.tunName, false)
} ?: setUnavailable()
}
}
}
override fun onClick() {
super.onClick()
unlockAndRun {
lifecycleScope.launch {
if (tunnelManager.activeTunnels.value.isNotEmpty()) return@launch tunnelManager.stopTunnel()
val lastActive = WireGuardAutoTunnel.getLastActiveTunnels()
if (lastActive.isEmpty()) {
appDataRepository.getStartTunnelConfig()?.let {
tunnelManager.startTunnel(it)
}
} else {
lastActive.forEach { id ->
appDataRepository.tunnels.getById(id)?.let {
tunnelManager.startTunnel(it)
}
}
}
}
}
}
private fun setActive() {
runCatching {
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
}
}
private fun setInactive() {
runCatching {
qsTile.state = Tile.STATE_INACTIVE
qsTile.updateTile()
}
}
private fun setUnavailable() {
runCatching {
qsTile.state = Tile.STATE_UNAVAILABLE
setTileDescription("")
qsTile.updateTile()
}
}
private fun setTileDescription(description: String) {
runCatching {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
qsTile.subtitle = description
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
qsTile.stateDescription = description
}
qsTile.updateTile()
}
}
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (_: Throwable) {
Timber.e("Failed to bind to TunnelControlTile")
}
return ret
}
private fun updateTile(name: String, active: Boolean) {
runCatching {
setTileDescription(name)
if (active) return setActive()
setInactive()
}.onFailure {
Timber.e(it)
}
}
override val lifecycle: Lifecycle
get() = lifecycleRegistry
}
@@ -0,0 +1,79 @@
package com.zaneschepke.wireguardautotunnel.core.shortcut
import android.content.Context
import android.content.Intent
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
class DynamicShortcutManager(private val context: Context, @IoDispatcher private val ioDispatcher: CoroutineDispatcher) : ShortcutManager {
override suspend fun addShortcuts() {
withContext(ioDispatcher) {
ShortcutManagerCompat.setDynamicShortcuts(context, createShortcuts())
}
}
override suspend fun removeShortcuts() {
withContext(ioDispatcher) {
ShortcutManagerCompat.removeDynamicShortcuts(context, createShortcuts().map { it.id })
}
}
private fun createShortcuts(): List<ShortcutInfoCompat> {
return listOf(
buildShortcut(
context.getString(R.string.vpn_off),
context.getString(R.string.vpn_off),
context.getString(R.string.vpn_off),
intent = Intent(context, ShortcutsActivity::class.java).apply {
putExtra("className", "WireGuardTunnelService")
action = ShortcutsActivity.Action.STOP.name
},
shortcutIcon = R.drawable.vpn_off,
),
buildShortcut(
context.getString(R.string.vpn_on),
context.getString(R.string.vpn_on),
context.getString(R.string.vpn_on),
intent = Intent(context, ShortcutsActivity::class.java).apply {
putExtra("className", "WireGuardTunnelService")
action = ShortcutsActivity.Action.START.name
},
shortcutIcon = R.drawable.vpn_on,
),
buildShortcut(
context.getString(R.string.start_auto),
context.getString(R.string.start_auto),
context.getString(R.string.start_auto),
intent = Intent(context, ShortcutsActivity::class.java).apply {
putExtra("className", "WireGuardConnectivityWatcherService")
action = ShortcutsActivity.Action.START.name
},
shortcutIcon = R.drawable.auto_play,
),
buildShortcut(
context.getString(R.string.stop_auto),
context.getString(R.string.stop_auto),
context.getString(R.string.stop_auto),
intent = Intent(context, ShortcutsActivity::class.java).apply {
putExtra("className", "WireGuardConnectivityWatcherService")
action = ShortcutsActivity.Action.STOP.name
},
shortcutIcon = R.drawable.auto_pause,
),
)
}
private fun buildShortcut(id: String, shortLabel: String, longLabel: String, intent: Intent, shortcutIcon: Int): ShortcutInfoCompat {
return ShortcutInfoCompat.Builder(context, id)
.setShortLabel(shortLabel)
.setLongLabel(longLabel)
.setIntent(intent)
.setIcon(IconCompat.createWithResource(context, shortcutIcon))
.build()
}
}
@@ -0,0 +1,6 @@
package com.zaneschepke.wireguardautotunnel.core.shortcut
interface ShortcutManager {
suspend fun addShortcuts()
suspend fun removeShortcuts()
}
@@ -1,20 +1,18 @@
package com.zaneschepke.wireguardautotunnel.service.shortcut
package com.zaneschepke.wireguardautotunnel.core.shortcut
import android.os.Bundle
import androidx.activity.ComponentActivity
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
import com.zaneschepke.wireguardautotunnel.util.extensions.stopTunnelBackground
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class ShortcutsActivity : ComponentActivity() {
@@ -22,7 +20,10 @@ class ShortcutsActivity : ComponentActivity() {
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var tunnelService: Provider<TunnelService>
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
@Inject
@ApplicationScope
@@ -31,39 +32,29 @@ class ShortcutsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
applicationScope.launch {
val settings = appDataRepository.settings.getSettings()
val settings = appDataRepository.settings.get()
if (settings.isShortcutsEnabled) {
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
LEGACY_TUNNEL_SERVICE_NAME, TunnelService::class.java.simpleName -> {
LEGACY_TUNNEL_SERVICE_NAME, TunnelProvider::class.java.simpleName -> {
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
Timber.d("Tunnel name extra: $tunnelName")
val tunnelConfig = tunnelName?.let {
appDataRepository.tunnels.getAll()
.firstOrNull { it.name == tunnelName }
.firstOrNull { it.tunName == tunnelName }
} ?: appDataRepository.getStartTunnelConfig()
Timber.d("Shortcut action on name: ${tunnelConfig?.name}")
Timber.d("Shortcut action on name: ${tunnelConfig?.tunName}")
tunnelConfig?.let {
when (intent.action) {
Action.START.name -> this@ShortcutsActivity.startTunnelBackground(it.id)
Action.STOP.name -> this@ShortcutsActivity.stopTunnelBackground(it.id)
Action.START.name -> tunnelManager.startTunnel(it)
Action.STOP.name -> tunnelManager.stopTunnel()
else -> Unit
}
}
}
AutoTunnelService::class.java.simpleName, LEGACY_AUTO_TUNNEL_SERVICE_NAME -> {
when (intent.action) {
Action.START.name ->
appDataRepository.settings.save(
settings.copy(
isAutoTunnelPaused = false,
),
)
Action.STOP.name ->
appDataRepository.settings.save(
settings.copy(
isAutoTunnelPaused = true,
),
)
Action.START.name -> serviceManager.startAutoTunnel()
Action.STOP.name -> serviceManager.stopAutoTunnel()
}
}
}
@@ -72,6 +63,11 @@ class ShortcutsActivity : ComponentActivity() {
finish()
}
enum class Action {
START,
STOP,
}
companion object {
const val LEGACY_TUNNEL_SERVICE_NAME = "WireGuardTunnelService"
const val LEGACY_AUTO_TUNNEL_SERVICE_NAME = "WireGuardConnectivityWatcherService"
@@ -0,0 +1,204 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.concurrent.thread
abstract class BaseTunnel(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@ApplicationScope private val applicationScope: CoroutineScope,
private val appDataRepository: AppDataRepository,
private val serviceManager: ServiceManager,
) : TunnelProvider {
private val activeTuns = MutableStateFlow<Map<TunnelConf, TunnelState>>(emptyMap())
private val tunThreads = ConcurrentHashMap<Int, Thread>()
override val activeTunnels = activeTuns.asStateFlow()
private val tunMutex = Mutex()
private val tunStatusMutex = Mutex()
private val isBouncing = AtomicBoolean(false)
abstract suspend fun startBackend(tunnel: TunnelConf)
abstract fun stopBackend(tunnel: TunnelConf)
override suspend fun clearError(tunnelConf: TunnelConf) = updateTunnelStatus(tunnelConf, TunnelStatus.Down)
override fun hasVpnPermission(): Boolean {
return serviceManager.hasVpnPermission()
}
protected suspend fun updateTunnelStatus(tunnelConf: TunnelConf, state: TunnelStatus? = null, stats: TunnelStatistics? = null) {
tunStatusMutex.withLock {
activeTuns.update { current ->
val originalConf = current.getKeyById(tunnelConf.id) ?: tunnelConf
val existingState = current.getValueById(tunnelConf.id) ?: TunnelState()
val newState = state ?: existingState.status
if (newState == TunnelStatus.Down) {
Timber.d("Removing tunnel ${tunnelConf.id} from activeTunnels as state is DOWN")
current - originalConf
} else if (existingState.status == newState && stats == null) {
Timber.d("Skipping redundant state update for ${tunnelConf.id}: $newState")
current
} else {
val updated = existingState.copy(
status = newState,
statistics = stats ?: existingState.statistics,
)
current + (originalConf to updated)
}
}
}
}
private suspend fun stopActiveTunnels() {
activeTunnels.value.forEach { (config, state) ->
if (state.status.isUpOrStarting()) {
stopTunnel(config)
delay(300)
}
}
}
private fun configureTunnelCallbacks(tunnelConf: TunnelConf) {
Timber.d("Configuring TunnelConf instance: ${tunnelConf.hashCode()}")
tunnelConf.setStateChangeCallback { state ->
Timber.d("State change callback triggered for tunnel ${tunnelConf.id}: ${tunnelConf.tunName} with state $state at ${System.currentTimeMillis()}")
when (state) {
is Tunnel.State -> applicationScope.launch { updateTunnelStatus(tunnelConf, state.asTunnelState()) }
is org.amnezia.awg.backend.Tunnel.State -> applicationScope.launch { updateTunnelStatus(tunnelConf, state.asTunnelState()) }
}
serviceManager.updateTunnelTile()
}
}
override suspend fun updateTunnelStatistics(tunnel: TunnelConf) {
val stats = getStatistics(tunnel)
updateTunnelStatus(tunnel, null, stats)
}
override suspend fun startTunnel(tunnelConf: TunnelConf) {
if (activeTuns.exists(tunnelConf.id) || tunThreads.containsKey(tunnelConf.id)) return
// stop active tunnels if we are userspace
if (this@BaseTunnel is UserspaceTunnel) stopActiveTunnels()
tunMutex.withLock {
// use thread to interrupt java backend if stuck (like in dns resolution)
tunThreads += tunnelConf.id to thread {
runBlocking {
try {
Timber.d("Starting tunnel ${tunnelConf.id}...")
startTunnelInner(tunnelConf)
Timber.d("Started complete for tunnel ${tunnelConf.name}...")
} catch (e: BackendError) {
Timber.e(e, "Failed to start tunnel ${tunnelConf.name} userspace")
updateTunnelStatus(tunnelConf, TunnelStatus.Error(e))
} catch (e: InterruptedException) {
Timber.i("Tunnel start has been interrupted as ${tunnelConf.name} failed to start")
}
}
}
}
}
private suspend fun startTunnelInner(tunnelConf: TunnelConf) {
configureTunnelCallbacks(tunnelConf)
Timber.d("Started backend for tunnel ${tunnelConf.id}...")
startBackend(tunnelConf)
updateTunnelStatus(tunnelConf, TunnelStatus.Up)
Timber.d("DONE for tun ${tunnelConf.id}...")
saveTunnelActiveState(tunnelConf, true)
serviceManager.startTunnelForegroundService()
}
private suspend fun saveTunnelActiveState(tunnelConf: TunnelConf, active: Boolean) {
val tunnelCopy = tunnelConf.copyWithCallback(isActive = active)
appDataRepository.tunnels.save(tunnelCopy)
}
override suspend fun stopTunnel(tunnelConf: TunnelConf?, reason: TunnelStatus.StopReason) {
if (tunnelConf == null) return stopActiveTunnels()
tunMutex.withLock {
try {
val stuckStarting = activeTuns.isStarting(tunnelConf.id)
handleTunnelThreadCleanup(tunnelConf)
if (stuckStarting) return Timber.d("Stuck in starting, so just shutting down tunnel thread")
updateTunnelStatus(tunnelConf, TunnelStatus.Stopping(reason))
stopTunnelInner(tunnelConf)
} catch (e: BackendError) {
Timber.e(e, "Failed to stop tunnel ${tunnelConf.id}")
updateTunnelStatus(tunnelConf, TunnelStatus.Error(e))
}
}
}
private suspend fun stopTunnelInner(tunnelConf: TunnelConf) {
val tunnel = activeTuns.findTunnel(tunnelConf.id) ?: return
stopBackend(tunnel)
saveTunnelActiveState(tunnelConf, false)
removeActiveTunnel(tunnel)
handleServiceChangesOnStop()
}
private suspend fun handleServiceChangesOnStop() {
if (activeTuns.value.isEmpty() && !isBouncing.get()) return serviceManager.stopTunnelForegroundService()
}
private suspend fun handleTunnelThreadCleanup(tunnel: TunnelConf) {
Timber.d("Cleaning up thread for ${tunnel.name}")
try {
tunThreads[tunnel.id]?.let {
if (it.state != Thread.State.TERMINATED) {
it.interrupt()
updateTunnelStatus(tunnel, TunnelStatus.Down)
} else {
Timber.d("Thread already terminated")
}
}
} catch (e: Exception) {
Timber.e(e, "Failed to stop tunnel thread for ${tunnel.name}")
}
Timber.d("Removing thread for ${tunnel.name}")
tunThreads -= tunnel.id
}
private fun removeActiveTunnel(tunnelConf: TunnelConf) {
activeTuns.update { current ->
current.toMutableMap().apply { remove(tunnelConf) }
}
}
override suspend fun bounceTunnel(tunnelConf: TunnelConf, reason: TunnelStatus.StopReason) {
Timber.i("Bounce tunnel ${tunnelConf.name}")
isBouncing.set(true)
stopTunnel(tunnelConf, reason)
startTunnel(tunnelConf)
isBouncing.set(false)
}
override suspend fun runningTunnelNames(): Set<String> = activeTuns.value.keys.map { it.tunName }.toSet()
}
@@ -0,0 +1,51 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import kotlinx.coroutines.flow.MutableStateFlow
fun Map<TunnelConf, TunnelState>.allDown(): Boolean {
return this.all { it.value.status.isDown() }
}
fun Map<TunnelConf, TunnelState>.hasActive(): Boolean {
return this.any { it.value.status.isUp() }
}
fun Map<TunnelConf, TunnelState>.getValueById(id: Int): TunnelState? {
val key = this.keys.find { it.id == id }
return key?.let { this@getValueById[it] }
}
fun Map<TunnelConf, TunnelState>.getKeyById(id: Int): TunnelConf? {
return this.keys.find { it.id == id }
}
fun Map<TunnelConf, TunnelState>.isUp(tunnelConf: TunnelConf): Boolean {
return this.getValueById(tunnelConf.id)?.status?.isUp() ?: false
}
fun MutableStateFlow<Map<TunnelConf, TunnelState>>.exists(id: Int): Boolean {
return this.value.any { it.key.id == id }
}
fun MutableStateFlow<Map<TunnelConf, TunnelState>>.isUp(id: Int): Boolean {
return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Up }
}
fun MutableStateFlow<Map<TunnelConf, TunnelState>>.isStarting(id: Int): Boolean {
return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Starting }
}
fun MutableStateFlow<Map<TunnelConf, TunnelState>>.findTunnel(id: Int): TunnelConf? {
return this.value.keys.find { it.id == id }
}
private val URL_PATTERN = Regex(
"""^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}:[0-9]{1,5}$""",
)
fun String.isUrl(): Boolean {
return URL_PATTERN.matches(this)
}
@@ -0,0 +1,67 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import timber.log.Timber
import javax.inject.Inject
class KernelTunnel @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@ApplicationScope private val applicationScope: CoroutineScope,
serviceManager: ServiceManager,
appDataRepository: AppDataRepository,
private val backend: Backend,
) : BaseTunnel(ioDispatcher, applicationScope, appDataRepository, serviceManager) {
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
return try {
WireGuardStatistics(backend.getStatistics(tunnelConf))
} catch (e: Exception) {
Timber.e(e)
null
}
}
override suspend fun startBackend(tunnel: TunnelConf) {
try {
updateTunnelStatus(tunnel, TunnelStatus.Starting)
backend.setState(tunnel, Tunnel.State.UP, tunnel.toWgConfig())
} catch (e: BackendException) {
throw e.toBackendError()
}
}
override fun stopBackend(tunnel: TunnelConf) {
Timber.i("Stopping tunnel ${tunnel.id} kernel")
try {
backend.setState(tunnel, Tunnel.State.DOWN, tunnel.toWgConfig())
} catch (e: BackendException) {
throw e.toBackendError()
}
}
override fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
Timber.w("Not yet implemented for kernel")
}
override fun getBackendState(): BackendState {
return BackendState.INACTIVE
}
override suspend fun runningTunnelNames(): Set<String> {
return backend.runningTunnelNames
}
}
@@ -0,0 +1,114 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.Kernel
import com.zaneschepke.wireguardautotunnel.di.Userspace
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import javax.inject.Inject
class TunnelManager @Inject constructor(
@Kernel private val kernelTunnel: TunnelProvider,
@Userspace private val userspaceTunnel: TunnelProvider,
private val appDataRepository: AppDataRepository,
@ApplicationScope private val applicationScope: CoroutineScope,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : TunnelProvider {
@OptIn(ExperimentalCoroutinesApi::class)
private val tunnelProviderFlow = appDataRepository.settings.flow
.filterNotNull()
.flatMapLatest { settings ->
MutableStateFlow(if (settings.isKernelEnabled) kernelTunnel else userspaceTunnel)
}
.stateIn(
scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly,
initialValue = userspaceTunnel,
)
@OptIn(ExperimentalCoroutinesApi::class)
override val activeTunnels = appDataRepository.settings.flow
.filterNotNull()
.flatMapLatest { settings ->
if (settings.isKernelEnabled) {
kernelTunnel.activeTunnels
} else {
userspaceTunnel.activeTunnels
}
}
.stateIn(
scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly,
initialValue = emptyMap(),
)
override fun hasVpnPermission(): Boolean {
return userspaceTunnel.hasVpnPermission()
}
override suspend fun clearError(tunnelConf: TunnelConf) {
tunnelProviderFlow.value.clearError(tunnelConf)
}
override suspend fun updateTunnelStatistics(tunnel: TunnelConf) {
tunnelProviderFlow.value.updateTunnelStatistics(tunnel)
}
override suspend fun startTunnel(tunnelConf: TunnelConf) {
tunnelProviderFlow.value.startTunnel(tunnelConf)
}
override suspend fun stopTunnel(tunnelConf: TunnelConf?, reason: TunnelStatus.StopReason) {
tunnelProviderFlow.value.stopTunnel(tunnelConf)
}
override suspend fun bounceTunnel(tunnelConf: TunnelConf, reason: TunnelStatus.StopReason) {
tunnelProviderFlow.value.bounceTunnel(tunnelConf)
}
override fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
tunnelProviderFlow.value.setBackendState(backendState, allowedIps)
}
override fun getBackendState(): BackendState {
return tunnelProviderFlow.value.getBackendState()
}
override suspend fun runningTunnelNames(): Set<String> {
return tunnelProviderFlow.value.runningTunnelNames()
}
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
return tunnelProviderFlow.value.getStatistics(tunnelConf)
}
fun restorePreviousState() = applicationScope.launch(ioDispatcher) {
val settings = appDataRepository.settings.get()
if (settings.isRestoreOnBootEnabled) {
val previouslyActiveTuns = appDataRepository.tunnels.getActive()
val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels.value.any { tun.id == it.key.id } }
if (settings.isKernelEnabled) {
return@launch tunsToStart.forEach {
startTunnel(it)
}
} else {
tunsToStart.firstOrNull()?.let { startTunnel(it) }
}
}
}
}
@@ -0,0 +1,22 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import kotlinx.coroutines.flow.StateFlow
interface TunnelProvider {
suspend fun startTunnel(tunnelConf: TunnelConf)
suspend fun stopTunnel(tunnelConf: TunnelConf? = null, reason: TunnelStatus.StopReason = TunnelStatus.StopReason.USER)
suspend fun bounceTunnel(tunnelConf: TunnelConf, reason: TunnelStatus.StopReason = TunnelStatus.StopReason.USER)
fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
fun getBackendState(): BackendState
suspend fun runningTunnelNames(): Set<String>
fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics?
val activeTunnels: StateFlow<Map<TunnelConf, TunnelState>>
fun hasVpnPermission(): Boolean
suspend fun clearError(tunnelConf: TunnelConf)
suspend fun updateTunnelStatistics(tunnel: TunnelConf)
}
@@ -0,0 +1,107 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.BackendException
import org.amnezia.awg.backend.Tunnel
import org.amnezia.awg.config.Config
import timber.log.Timber
import javax.inject.Inject
import kotlin.jvm.optionals.getOrNull
class UserspaceTunnel @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@ApplicationScope private val applicationScope: CoroutineScope,
val serviceManager: ServiceManager,
val appDataRepository: AppDataRepository,
private val backend: Backend,
) : BaseTunnel(ioDispatcher, applicationScope, appDataRepository, serviceManager) {
private var previousBackendState: Pair<BackendState, Boolean>? = null
override suspend fun startBackend(tunnel: TunnelConf) {
try {
updateTunnelStatus(tunnel, TunnelStatus.Starting)
val amConfig = tunnel.toAmConfig()
handleVpnKillSwitchWithDomainEndpoints(amConfig)
backend.setState(tunnel, Tunnel.State.UP, amConfig)
} catch (e: BackendException) {
Timber.e(e, "Failed to start up backend for tunnel ${tunnel.name}")
throw e.toBackendError()
}
}
override fun stopBackend(tunnel: TunnelConf) {
Timber.i("Stopping tunnel ${tunnel.name} userspace")
try {
backend.setState(tunnel, Tunnel.State.DOWN, tunnel.toAmConfig())
} catch (e: BackendException) {
Timber.e(e, "Failed to stop tunnel ${tunnel.id}")
throw e.toBackendError()
}
handlePreviouslyEnabledVpnKillSwitch()
}
// stop vpn kill switch if we need to resolve DNS for peer endpoints
private suspend fun handleVpnKillSwitchWithDomainEndpoints(config: Config) {
if (config.peers.any { it.endpoint.getOrNull()?.toString()?.isUrl() == true } &&
backend.backendState.asBackendState() == BackendState.KILL_SWITCH_ACTIVE
) {
val bypassLan = appDataRepository.settings.get().isLanOnKillSwitchEnabled
previousBackendState = Pair(BackendState.KILL_SWITCH_ACTIVE, bypassLan)
setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
}
}
// restore vpn kill switch if needed
private fun handlePreviouslyEnabledVpnKillSwitch() {
// let auto tunnel handle this if it is active
if (!serviceManager.autoTunnelActive.value) {
previousBackendState?.let { (state, lanEnabled) ->
Timber.d("Restoring kill switch configuration")
val lan = if (lanEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS else emptyList()
backend.setBackendState(state.asAmBackendState(), lan)
}
}
previousBackendState = null
}
override fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
Timber.d("Setting backend state: $backendState with allowedIps: $allowedIps")
try {
backend.setBackendState(backendState.asAmBackendState(), allowedIps)
} catch (e: BackendException) {
throw e.toBackendError()
}
}
override fun getBackendState(): BackendState {
return backend.backendState.asBackendState()
}
override suspend fun runningTunnelNames(): Set<String> {
return backend.runningTunnelNames
}
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
return try {
AmneziaStatistics(backend.getStatistics(tunnelConf))
} catch (e: Exception) {
Timber.e(e, "Failed to get stats for ${tunnelConf.tunName}")
null
}
}
}
@@ -0,0 +1,60 @@
package com.zaneschepke.wireguardautotunnel.core.worker
import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.concurrent.TimeUnit
@HiltWorker
class ServiceWorker @AssistedInject constructor(
@Assisted private val context: Context,
@Assisted private val params: WorkerParameters,
private val serviceManager: ServiceManager,
private val appDataRepository: AppDataRepository,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val tunnelManager: TunnelManager,
) : CoroutineWorker(context, params) {
companion object {
private const val TAG = "service_worker"
fun stop(context: Context) {
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
}
fun start(context: Context) {
val periodicWorkRequest = PeriodicWorkRequestBuilder<ServiceWorker>(
repeatInterval = 15,
repeatIntervalTimeUnit = TimeUnit.MINUTES,
).build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
TAG,
ExistingPeriodicWorkPolicy.KEEP,
periodicWorkRequest,
)
}
}
override suspend fun doWork(): Result = withContext(ioDispatcher) {
Timber.i("Service worker started")
with(appDataRepository.settings.get()) {
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@with serviceManager.startAutoTunnel()
if (tunnelManager.activeTunnels.value.isEmpty()) tunnelManager.restorePreviousState()
}
Result.success()
}
}
@@ -6,12 +6,14 @@ import androidx.room.DeleteColumn
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.AutoMigrationSpec
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
@Database(
entities = [Settings::class, TunnelConfig::class],
version = 10,
version = 16,
autoMigrations =
[
AutoMigration(from = 1, to = 2),
@@ -36,6 +38,31 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
AutoMigration(7, 8),
AutoMigration(8, 9),
AutoMigration(9, 10),
AutoMigration(
from = 10,
to = 11,
spec = RemoveTunnelPauseMigration::class,
),
AutoMigration(
from = 11,
to = 12,
),
AutoMigration(
from = 12,
to = 13,
),
AutoMigration(
from = 13,
to = 14,
),
AutoMigration(
from = 14,
to = 15,
),
AutoMigration(
from = 15,
to = 16,
),
],
exportSchema = true,
)
@@ -55,3 +82,9 @@ abstract class AppDatabase : RoomDatabase() {
columnName = "is_battery_saver_enabled",
)
class RemoveLegacySettingColumnsMigration : AutoMigrationSpec
@DeleteColumn(
tableName = "Settings",
columnName = "is_auto_tunnel_paused",
)
class RemoveTunnelPauseMigration : AutoMigrationSpec
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.data.datastore
package com.zaneschepke.wireguardautotunnel.data
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@@ -6,10 +6,11 @@ import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@@ -21,11 +22,14 @@ class DataStoreManager(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) {
companion object {
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
val CURRENT_SSID = stringPreferencesKey("CURRENT_SSID")
val IS_PIN_LOCK_ENABLED = booleanPreferencesKey("PIN_LOCK_ENABLED")
val IS_TUNNEL_STATS_EXPANDED = booleanPreferencesKey("TUNNEL_STATS_EXPANDED")
val locationDisclosureShown = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
val batteryDisableShown = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
val currentSSID = stringPreferencesKey("CURRENT_SSID")
val pinLockEnabled = booleanPreferencesKey("PIN_LOCK_ENABLED")
val tunnelStatsExpanded = booleanPreferencesKey("TUNNEL_STATS_EXPANDED")
val isLocalLogsEnabled = booleanPreferencesKey("LOCAL_LOGS_ENABLED")
val locale = stringPreferencesKey("LOCALE")
val theme = stringPreferencesKey("THEME")
}
// preferences
@@ -40,7 +44,7 @@ class DataStoreManager(
try {
context.dataStore.data.first()
} catch (e: IOException) {
Timber.e(e)
Timber.Forest.e(e)
}
}
}
@@ -50,9 +54,9 @@ class DataStoreManager(
try {
context.dataStore.edit { it[key] = value }
} catch (e: IOException) {
Timber.e(e)
Timber.Forest.e(e)
} catch (e: Exception) {
Timber.e(e)
Timber.Forest.e(e)
}
}
}
@@ -62,9 +66,9 @@ class DataStoreManager(
try {
context.dataStore.edit { it.remove(key) }
} catch (e: IOException) {
Timber.e(e)
Timber.Forest.e(e)
} catch (e: Exception) {
Timber.e(e)
Timber.Forest.e(e)
}
}
}
@@ -76,7 +80,7 @@ class DataStoreManager(
try {
context.dataStore.data.map { it[key] }.first()
} catch (e: IOException) {
Timber.e(e)
Timber.Forest.e(e)
null
}
}
@@ -86,5 +90,5 @@ class DataStoreManager(
context.dataStore.data.map { it[key] }.first()
}
val preferencesFlow: Flow<Preferences?> = context.dataStore.data
val preferencesFlow: Flow<Preferences?> = context.dataStore.data.flowOn(ioDispatcher)
}
@@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.data
import androidx.room.TypeConverter
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
class DatabaseListConverters {
@@ -16,7 +16,7 @@ object Queries {
VALUES
('false',
'false',
'sampleSSID1,sampleSSID2',
'',
'false',
'false',
'false',
@@ -1,11 +1,11 @@
package com.zaneschepke.wireguardautotunnel.data
package com.zaneschepke.wireguardautotunnel.data.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import kotlinx.coroutines.flow.Flow
@Dao
@@ -1,11 +1,11 @@
package com.zaneschepke.wireguardautotunnel.data
package com.zaneschepke.wireguardautotunnel.data.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import kotlinx.coroutines.flow.Flow
@@ -44,6 +44,9 @@ interface TunnelConfigDao {
@Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1")
suspend fun resetMobileDataTunnel()
@Query("UPDATE TunnelConfig SET is_ethernet_tunnel = 0 WHERE is_ethernet_tunnel =1")
suspend fun resetEthernetTunnel()
@Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1")
suspend fun findByPrimary(): TunnelConfigs
@@ -1,15 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.domain
data class GeneralState(
val isLocationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
val isTunnelStatsExpanded: Boolean = IS_TUNNEL_STATS_EXPANDED,
) {
companion object {
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
const val PIN_LOCK_ENABLED_DEFAULT = false
const val IS_TUNNEL_STATS_EXPANDED = false
}
}
@@ -1,58 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.domain
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Settings(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "is_tunnel_enabled") val isAutoTunnelEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled")
val isTunnelOnMobileDataEnabled: Boolean = false,
@ColumnInfo(name = "trusted_network_ssids")
val trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
@ColumnInfo(name = "is_always_on_vpn_enabled") val isAlwaysOnVpnEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled")
val isTunnelOnEthernetEnabled: Boolean = false,
@ColumnInfo(
name = "is_shortcuts_enabled",
defaultValue = "false",
)
val isShortcutsEnabled: Boolean = false,
@ColumnInfo(
name = "is_tunnel_on_wifi_enabled",
defaultValue = "false",
)
val isTunnelOnWifiEnabled: Boolean = false,
@ColumnInfo(
name = "is_kernel_enabled",
defaultValue = "false",
)
val isKernelEnabled: Boolean = false,
@ColumnInfo(
name = "is_restore_on_boot_enabled",
defaultValue = "false",
)
val isRestoreOnBootEnabled: Boolean = false,
@ColumnInfo(
name = "is_multi_tunnel_enabled",
defaultValue = "false",
)
val isMultiTunnelEnabled: Boolean = false,
@ColumnInfo(
name = "is_auto_tunnel_paused",
defaultValue = "false",
)
val isAutoTunnelPaused: Boolean = false,
@ColumnInfo(
name = "is_ping_enabled",
defaultValue = "false",
)
val isPingEnabled: Boolean = false,
@ColumnInfo(
name = "is_amnezia_enabled",
defaultValue = "false",
)
val isAmneziaEnabled: Boolean = false,
)
@@ -0,0 +1,46 @@
package com.zaneschepke.wireguardautotunnel.data.model
import com.zaneschepke.wireguardautotunnel.domain.entity.AppState
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
data class GeneralState(
val isLocationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
val isTunnelStatsExpanded: Boolean = IS_TUNNEL_STATS_EXPANDED,
val isLocalLogsEnabled: Boolean = IS_LOGS_ENABLED_DEFAULT,
val locale: String? = null,
val theme: Theme = Theme.AUTOMATIC,
) {
fun toAppState(): AppState = AppState(
isLocationDisclosureShown,
isBatteryOptimizationDisableShown,
isPinLockEnabled,
isTunnelStatsExpanded,
isLocationDisclosureShown,
locale,
theme,
)
companion object {
fun from(appState: AppState): GeneralState {
return with(appState) {
GeneralState(
isLocationDisclosureShown,
isBatteryOptimizationDisableShown,
isPinLockEnabled,
isTunnelStatsExpanded,
isLocationDisclosureShown,
locale,
theme,
)
}
}
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
const val PIN_LOCK_ENABLED_DEFAULT = false
const val IS_TUNNEL_STATS_EXPANDED = false
const val IS_LOGS_ENABLED_DEFAULT = false
}
}
@@ -0,0 +1,118 @@
package com.zaneschepke.wireguardautotunnel.data.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
@Entity
data class Settings(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "is_tunnel_enabled") val isAutoTunnelEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled")
val isTunnelOnMobileDataEnabled: Boolean = false,
@ColumnInfo(name = "trusted_network_ssids")
val trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
@ColumnInfo(name = "is_always_on_vpn_enabled") val isAlwaysOnVpnEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled")
val isTunnelOnEthernetEnabled: Boolean = false,
@ColumnInfo(
name = "is_shortcuts_enabled",
defaultValue = "false",
)
val isShortcutsEnabled: Boolean = false,
@ColumnInfo(
name = "is_tunnel_on_wifi_enabled",
defaultValue = "false",
)
val isTunnelOnWifiEnabled: Boolean = false,
@ColumnInfo(
name = "is_kernel_enabled",
defaultValue = "false",
)
val isKernelEnabled: Boolean = false,
@ColumnInfo(
name = "is_restore_on_boot_enabled",
defaultValue = "false",
)
val isRestoreOnBootEnabled: Boolean = false,
@ColumnInfo(
name = "is_multi_tunnel_enabled",
defaultValue = "false",
)
val isMultiTunnelEnabled: Boolean = false,
@ColumnInfo(
name = "is_ping_enabled",
defaultValue = "false",
)
val isPingEnabled: Boolean = false,
@ColumnInfo(
name = "is_amnezia_enabled",
defaultValue = "false",
)
val isAmneziaEnabled: Boolean = false,
@ColumnInfo(
name = "is_wildcards_enabled",
defaultValue = "false",
)
val isWildcardsEnabled: Boolean = false,
@ColumnInfo(
name = "is_wifi_by_shell_enabled",
defaultValue = "false",
)
val isWifiNameByShellEnabled: Boolean = false,
@ColumnInfo(
name = "is_stop_on_no_internet_enabled",
defaultValue = "false",
)
val isStopOnNoInternetEnabled: Boolean = false,
@ColumnInfo(
name = "is_vpn_kill_switch_enabled",
defaultValue = "false",
)
val isVpnKillSwitchEnabled: Boolean = false,
@ColumnInfo(
name = "is_kernel_kill_switch_enabled",
defaultValue = "false",
)
val isKernelKillSwitchEnabled: Boolean = false,
@ColumnInfo(
name = "is_lan_on_kill_switch_enabled",
defaultValue = "false",
)
val isLanOnKillSwitchEnabled: Boolean = false,
@ColumnInfo(
name = "debounce_delay_seconds",
defaultValue = "3",
)
val debounceDelaySeconds: Int = 3,
@ColumnInfo(
name = "is_disable_kill_switch_on_trusted_enabled",
defaultValue = "false",
)
val isDisableKillSwitchOnTrustedEnabled: Boolean = false,
) {
fun toAppSettings(): AppSettings {
return AppSettings(
id, isAutoTunnelEnabled, isTunnelOnMobileDataEnabled, trustedNetworkSSIDs, isAlwaysOnVpnEnabled, isTunnelOnEthernetEnabled,
isShortcutsEnabled, isTunnelOnWifiEnabled, isKernelEnabled, isRestoreOnBootEnabled, isMultiTunnelEnabled, isPingEnabled,
isAmneziaEnabled, isWildcardsEnabled, isWifiNameByShellEnabled, isStopOnNoInternetEnabled, isVpnKillSwitchEnabled,
isKernelKillSwitchEnabled, isLanOnKillSwitchEnabled, debounceDelaySeconds, isDisableKillSwitchOnTrustedEnabled,
)
}
companion object {
fun from(appSettings: AppSettings): Settings {
return with(appSettings) {
Settings(
id, isAutoTunnelEnabled, isTunnelOnMobileDataEnabled, trustedNetworkSSIDs.toMutableList(), isAlwaysOnVpnEnabled,
isTunnelOnEthernetEnabled, isShortcutsEnabled, isTunnelOnWifiEnabled, isKernelEnabled, isRestoreOnBootEnabled,
isMultiTunnelEnabled, isPingEnabled, isAmneziaEnabled, isWildcardsEnabled, isWifiNameByShellEnabled,
isStopOnNoInternetEnabled, isVpnKillSwitchEnabled, isKernelKillSwitchEnabled, isLanOnKillSwitchEnabled,
debounceDelaySeconds, isDisableKillSwitchOnTrustedEnabled,
)
}
}
}
}
@@ -1,11 +1,10 @@
package com.zaneschepke.wireguardautotunnel.data.domain
package com.zaneschepke.wireguardautotunnel.data.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import com.wireguard.config.Config
import java.io.InputStream
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
@Entity(indices = [Index(value = ["name"], unique = true)])
data class TunnelConfig(
@@ -57,28 +56,38 @@ data class TunnelConfig(
defaultValue = "null",
)
var pingIp: String? = null,
@ColumnInfo(
name = "is_ethernet_tunnel",
defaultValue = "false",
)
var isEthernetTunnel: Boolean = false,
@ColumnInfo(
name = "is_ipv4_preferred",
defaultValue = "true",
)
var isIpv4Preferred: Boolean = true,
) {
fun toAmConfig(): org.amnezia.awg.config.Config {
return configFromAmQuick(if (amQuick != "") amQuick else wgQuick)
fun toTunnel(): TunnelConf {
return TunnelConf(
id, name, wgQuick, tunnelNetworks, isMobileDataTunnel,
isPrimaryTunnel, amQuick, isActive, isPingEnabled, pingInterval,
pingCooldown, pingIp, isEthernetTunnel, isIpv4Preferred,
)
}
companion object {
fun configFromWgQuick(wgQuick: String): Config {
val inputStream: InputStream = wgQuick.byteInputStream()
return inputStream.bufferedReader(Charsets.UTF_8).use {
Config.parse(it)
}
}
fun configFromAmQuick(amQuick: String): org.amnezia.awg.config.Config {
val inputStream: InputStream = amQuick.byteInputStream()
return inputStream.bufferedReader(Charsets.UTF_8).use {
org.amnezia.awg.config.Config.parse(it)
}
}
const val AM_QUICK_DEFAULT = ""
fun from(tunnelConf: TunnelConf): TunnelConfig {
return with(tunnelConf) {
return TunnelConfig(
id, tunName, wgQuick, tunnelNetworks.toMutableList(), isMobileDataTunnel,
isPrimaryTunnel, amQuick, isActive, isPingEnabled, pingInterval,
pingCooldown, pingIp, isEthernetTunnel, isIpv4Preferred,
)
}
}
}
}
@@ -1,13 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
interface AppDataRepository {
suspend fun getPrimaryOrFirstTunnel(): TunnelConfig?
suspend fun getStartTunnelConfig(): TunnelConfig?
val settings: SettingsRepository
val tunnels: TunnelConfigRepository
val appState: AppStateRepository
}
@@ -1,20 +1,25 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import javax.inject.Inject
class AppDataRoomRepository
@Inject
constructor(
override val settings: SettingsRepository,
override val tunnels: TunnelConfigRepository,
override val settings: AppSettingRepository,
override val tunnels: TunnelRepository,
override val appState: AppStateRepository,
) : AppDataRepository {
override suspend fun getPrimaryOrFirstTunnel(): TunnelConfig? {
override suspend fun getPrimaryOrFirstTunnel(): TunnelConf? {
return tunnels.findPrimary().firstOrNull() ?: tunnels.getAll().firstOrNull()
}
override suspend fun getStartTunnelConfig(): TunnelConfig? {
override suspend fun getStartTunnelConfig(): TunnelConf? {
tunnels.getActive().let {
if (it.isNotEmpty()) return it.first()
return getPrimaryOrFirstTunnel()
@@ -1,7 +1,9 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import timber.log.Timber
@@ -11,64 +13,89 @@ class DataStoreAppStateRepository(
) :
AppStateRepository {
override suspend fun isLocationDisclosureShown(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN)
return dataStoreManager.getFromStore(DataStoreManager.locationDisclosureShown)
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT
}
override suspend fun setLocationDisclosureShown(shown: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown)
dataStoreManager.saveToDataStore(DataStoreManager.locationDisclosureShown, shown)
}
override suspend fun isPinLockEnabled(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.IS_PIN_LOCK_ENABLED)
return dataStoreManager.getFromStore(DataStoreManager.pinLockEnabled)
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT
}
override suspend fun setPinLockEnabled(enabled: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.IS_PIN_LOCK_ENABLED, enabled)
dataStoreManager.saveToDataStore(DataStoreManager.pinLockEnabled, enabled)
}
override suspend fun isBatteryOptimizationDisableShown(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN)
return dataStoreManager.getFromStore(DataStoreManager.batteryDisableShown)
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
}
override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, shown)
}
override suspend fun getCurrentSsid(): String? {
return dataStoreManager.getFromStore(DataStoreManager.CURRENT_SSID)
}
override suspend fun setCurrentSsid(ssid: String) {
dataStoreManager.saveToDataStore(DataStoreManager.CURRENT_SSID, ssid)
dataStoreManager.saveToDataStore(DataStoreManager.batteryDisableShown, shown)
}
override suspend fun isTunnelStatsExpanded(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.IS_TUNNEL_STATS_EXPANDED)
return dataStoreManager.getFromStore(DataStoreManager.tunnelStatsExpanded)
?: GeneralState.IS_TUNNEL_STATS_EXPANDED
}
override suspend fun setTunnelStatsExpanded(expanded: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.IS_TUNNEL_STATS_EXPANDED, expanded)
dataStoreManager.saveToDataStore(DataStoreManager.tunnelStatsExpanded, expanded)
}
override val generalStateFlow: Flow<GeneralState> =
override suspend fun setTheme(theme: Theme) {
dataStoreManager.saveToDataStore(DataStoreManager.theme, theme.name)
}
override suspend fun getTheme(): Theme {
return dataStoreManager.getFromStore(DataStoreManager.theme)?.let {
try {
Theme.valueOf(it)
} catch (_: IllegalArgumentException) {
Theme.AUTOMATIC
}
} ?: Theme.AUTOMATIC
}
override suspend fun isLocalLogsEnabled(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.isLocalLogsEnabled) ?: GeneralState.IS_LOGS_ENABLED_DEFAULT
}
override suspend fun setLocalLogsEnabled(enabled: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.isLocalLogsEnabled, enabled)
}
override suspend fun setLocale(localeTag: String) {
dataStoreManager.saveToDataStore(DataStoreManager.locale, localeTag)
}
override suspend fun getLocale(): String? {
return dataStoreManager.getFromStore(DataStoreManager.locale)
}
override val flow: Flow<GeneralState> =
dataStoreManager.preferencesFlow.map { prefs ->
prefs?.let { pref ->
try {
GeneralState(
isLocationDisclosureShown =
pref[DataStoreManager.LOCATION_DISCLOSURE_SHOWN]
pref[DataStoreManager.locationDisclosureShown]
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT,
isBatteryOptimizationDisableShown =
pref[DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN]
pref[DataStoreManager.batteryDisableShown]
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
isPinLockEnabled =
pref[DataStoreManager.IS_PIN_LOCK_ENABLED]
pref[DataStoreManager.pinLockEnabled]
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT,
isTunnelStatsExpanded = pref[DataStoreManager.IS_TUNNEL_STATS_EXPANDED] ?: GeneralState.IS_TUNNEL_STATS_EXPANDED,
isTunnelStatsExpanded = pref[DataStoreManager.tunnelStatsExpanded] ?: GeneralState.IS_TUNNEL_STATS_EXPANDED,
isLocalLogsEnabled = pref[DataStoreManager.isLocalLogsEnabled] ?: GeneralState.IS_LOGS_ENABLED_DEFAULT,
locale = pref[DataStoreManager.locale],
theme = getTheme(),
)
} catch (e: IllegalArgumentException) {
Timber.e(e)
@@ -1,30 +1,31 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class RoomSettingsRepository(private val settingsDoa: SettingsDao, @IoDispatcher private val ioDispatcher: CoroutineDispatcher) : SettingsRepository {
override suspend fun save(settings: Settings) {
class RoomSettingsRepository(
private val settingsDoa: SettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : AppSettingRepository {
override suspend fun save(appSettings: AppSettings) {
withContext(ioDispatcher) {
settingsDoa.save(settings)
settingsDoa.save(Settings.from(appSettings))
}
}
override fun getSettingsFlow(): Flow<Settings> {
return settingsDoa.getSettingsFlow()
}
override val flow = settingsDoa.getSettingsFlow().flowOn(ioDispatcher).map { it.toAppSettings() }
override suspend fun getSettings(): Settings {
override suspend fun get(): AppSettings {
return withContext(ioDispatcher) {
settingsDoa.getAll().firstOrNull() ?: Settings()
(settingsDoa.getAll().firstOrNull() ?: Settings()).toAppSettings()
}
}
override suspend fun getAll(): List<Settings> {
return withContext(ioDispatcher) { settingsDoa.getAll() }
}
}
@@ -1,97 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
class RoomTunnelConfigRepository(
private val tunnelConfigDao: TunnelConfigDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) :
TunnelConfigRepository {
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
return tunnelConfigDao.getAllFlow()
}
override suspend fun getAll(): TunnelConfigs {
return withContext(ioDispatcher) { tunnelConfigDao.getAll() }
}
override suspend fun save(tunnelConfig: TunnelConfig) {
withContext(ioDispatcher) {
tunnelConfigDao.save(tunnelConfig)
}.also {
WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
}
}
override suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetPrimaryTunnel()
tunnelConfig?.let {
save(
it.copy(
isPrimaryTunnel = true,
),
)
}
}
}
override suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetMobileDataTunnel()
tunnelConfig?.let {
save(
it.copy(
isMobileDataTunnel = true,
),
)
}
}
}
override suspend fun delete(tunnelConfig: TunnelConfig) {
withContext(ioDispatcher) {
tunnelConfigDao.delete(tunnelConfig)
}.also {
WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
}
}
override suspend fun getById(id: Int): TunnelConfig? {
return withContext(ioDispatcher) { tunnelConfigDao.getById(id.toLong()) }
}
override suspend fun getActive(): TunnelConfigs {
return withContext(ioDispatcher) {
tunnelConfigDao.getActive()
}
}
override suspend fun count(): Int {
return withContext(ioDispatcher) { tunnelConfigDao.count().toInt() }
}
override suspend fun findByTunnelName(name: String): TunnelConfig? {
return withContext(ioDispatcher) { tunnelConfigDao.getByName(name) }
}
override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs {
return withContext(ioDispatcher) { tunnelConfigDao.findByTunnelNetworkName(name) }
}
override suspend fun findByMobileDataTunnel(): TunnelConfigs {
return withContext(ioDispatcher) { tunnelConfigDao.findByMobileDataTunnel() }
}
override suspend fun findPrimary(): TunnelConfigs {
return withContext(ioDispatcher) { tunnelConfigDao.findByPrimary() }
}
}
@@ -0,0 +1,113 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class RoomTunnelRepository(
private val tunnelConfigDao: TunnelConfigDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : TunnelRepository {
override val flow = tunnelConfigDao.getAllFlow().flowOn(ioDispatcher).map { it.map { it.toTunnel() } }
override suspend fun getAll(): Tunnels {
return withContext(ioDispatcher) {
tunnelConfigDao.getAll().map { it.toTunnel() }
}
}
override suspend fun save(tunnelConf: TunnelConf) {
withContext(ioDispatcher) {
tunnelConfigDao.save(TunnelConfig.from(tunnelConf))
}
}
override suspend fun saveAll(tunnelConfs: List<TunnelConf>) {
withContext(ioDispatcher) {
tunnelConfigDao.saveAll(tunnelConfs.map(TunnelConfig::from))
}
}
override suspend fun updatePrimaryTunnel(tunnelConf: TunnelConf?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetPrimaryTunnel()
tunnelConf?.let {
save(
it.copy(
isPrimaryTunnel = true,
),
)
}
}
}
override suspend fun updateMobileDataTunnel(tunnelConf: TunnelConf?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetMobileDataTunnel()
tunnelConf?.let {
save(
it.copy(
isMobileDataTunnel = true,
),
)
}
}
}
override suspend fun updateEthernetTunnel(tunnelConf: TunnelConf?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetEthernetTunnel()
tunnelConf?.let {
save(
it.copy(
isEthernetTunnel = true,
),
)
}
}
}
override suspend fun delete(tunnelConf: TunnelConf) {
withContext(ioDispatcher) {
tunnelConfigDao.delete(TunnelConfig.from(tunnelConf))
}
}
override suspend fun getById(id: Int): TunnelConf? {
return withContext(ioDispatcher) { tunnelConfigDao.getById(id.toLong())?.toTunnel() }
}
override suspend fun getActive(): Tunnels {
return withContext(ioDispatcher) {
tunnelConfigDao.getActive().map { it.toTunnel() }
}
}
override suspend fun count(): Int {
return withContext(ioDispatcher) { tunnelConfigDao.count().toInt() }
}
override suspend fun findByTunnelName(name: String): TunnelConf? {
return withContext(ioDispatcher) { tunnelConfigDao.getByName(name)?.toTunnel() }
}
override suspend fun findByTunnelNetworksName(name: String): Tunnels {
return withContext(ioDispatcher) { tunnelConfigDao.findByTunnelNetworkName(name).map { it.toTunnel() } }
}
override suspend fun findByMobileDataTunnel(): Tunnels {
return withContext(ioDispatcher) { tunnelConfigDao.findByMobileDataTunnel().map { it.toTunnel() } }
}
override suspend fun findPrimary(): Tunnels {
return withContext(ioDispatcher) { tunnelConfigDao.findByPrimary().map { it.toTunnel() } }
}
}
@@ -1,14 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import kotlinx.coroutines.flow.Flow
interface SettingsRepository {
suspend fun save(settings: Settings)
fun getSettingsFlow(): Flow<Settings>
suspend fun getSettings(): Settings
suspend fun getAll(): List<Settings>
}
@@ -1,33 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import kotlinx.coroutines.flow.Flow
interface TunnelConfigRepository {
fun getTunnelConfigsFlow(): Flow<TunnelConfigs>
suspend fun getAll(): TunnelConfigs
suspend fun save(tunnelConfig: TunnelConfig)
suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?)
suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?)
suspend fun delete(tunnelConfig: TunnelConfig)
suspend fun getById(id: Int): TunnelConfig?
suspend fun getActive(): TunnelConfigs
suspend fun count(): Int
suspend fun findByTunnelName(name: String): TunnelConfig?
suspend fun findByTunnelNetworksName(name: String): TunnelConfigs
suspend fun findByMobileDataTunnel(): TunnelConfigs
suspend fun findPrimary(): TunnelConfigs
}
@@ -0,0 +1,46 @@
package com.zaneschepke.wireguardautotunnel.di
import android.content.Context
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.logcatter.LogcatReader
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.core.shortcut.DynamicShortcutManager
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class AppModule {
@Singleton
@ApplicationScope
@Provides
fun providesApplicationScope(@DefaultDispatcher defaultDispatcher: CoroutineDispatcher): CoroutineScope =
CoroutineScope(SupervisorJob() + defaultDispatcher)
@Singleton
@Provides
fun provideLogCollect(@ApplicationContext context: Context): LogReader {
return LogcatReader.init(storageDir = context.filesDir.absolutePath)
}
@Singleton
@Provides
fun provideNotificationService(@ApplicationContext context: Context): NotificationManager {
return WireGuardNotification(context)
}
@Singleton
@Provides
fun provideShortcutManager(@ApplicationContext context: Context, @IoDispatcher ioDispatcher: CoroutineDispatcher): ShortcutManager {
return DynamicShortcutManager(context, ioDispatcher)
}
}
@@ -0,0 +1,19 @@
package com.zaneschepke.wireguardautotunnel.di
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class TunnelShell
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AppShell
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Kernel
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Userspace
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.module
package com.zaneschepke.wireguardautotunnel.di
import javax.inject.Qualifier
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.module
package com.zaneschepke.wireguardautotunnel.di
import dagger.Module
import dagger.Provides
@@ -1,21 +1,21 @@
package com.zaneschepke.wireguardautotunnel.module
package com.zaneschepke.wireguardautotunnel.di
import android.content.Context
import androidx.room.Room
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRoomRepository
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.data.repository.DataStoreAppStateRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomSettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomTunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomTunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -54,13 +54,13 @@ class RepositoryModule {
@Singleton
@Provides
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao, @IoDispatcher ioDispatcher: CoroutineDispatcher): TunnelConfigRepository {
return RoomTunnelConfigRepository(tunnelConfigDao, ioDispatcher)
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao, @IoDispatcher ioDispatcher: CoroutineDispatcher): TunnelRepository {
return RoomTunnelRepository(tunnelConfigDao, ioDispatcher)
}
@Singleton
@Provides
fun provideSettingsRepository(settingsDao: SettingsDao, @IoDispatcher ioDispatcher: CoroutineDispatcher): SettingsRepository {
fun provideSettingsRepository(settingsDao: SettingsDao, @IoDispatcher ioDispatcher: CoroutineDispatcher): AppSettingRepository {
return RoomSettingsRepository(settingsDao, ioDispatcher)
}
@@ -79,10 +79,10 @@ class RepositoryModule {
@Provides
@Singleton
fun provideAppDataRepository(
settingsRepository: SettingsRepository,
tunnelConfigRepository: TunnelConfigRepository,
settingsRepository: AppSettingRepository,
tunnelRepository: TunnelRepository,
appStateRepository: AppStateRepository,
): AppDataRepository {
return AppDataRoomRepository(settingsRepository, tunnelConfigRepository, appStateRepository)
return AppDataRoomRepository(settingsRepository, tunnelRepository, appStateRepository)
}
}
@@ -0,0 +1,116 @@
package com.zaneschepke.wireguardautotunnel.di
import android.content.Context
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.KernelTunnel
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
import com.zaneschepke.wireguardautotunnel.core.tunnel.UserspaceTunnel
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.runBlocking
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.GoBackend
import org.amnezia.awg.backend.RootTunnelActionHandler
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class TunnelModule {
@Provides
@Singleton
@TunnelShell
fun provideTunnelRootShell(@ApplicationContext context: Context): RootShell {
return RootShell(context)
}
@Provides
@Singleton
@AppShell
fun provideAppRootShell(@ApplicationContext context: Context): RootShell {
return RootShell(context)
}
@Provides
@Singleton
fun provideAmneziaBackend(@ApplicationContext context: Context): Backend {
return GoBackend(context, RootTunnelActionHandler(org.amnezia.awg.util.RootShell(context)))
}
@Provides
@Singleton
fun provideKernelBackend(@ApplicationContext context: Context, @TunnelShell shell: RootShell): com.wireguard.android.backend.Backend {
return WgQuickBackend(context, shell, ToolsInstaller(context, shell), com.wireguard.android.backend.RootTunnelActionHandler(shell)).also {
it.setMultipleTunnels(true)
}
}
@Provides
@Singleton
@Kernel
fun provideKernelProvider(
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
serviceManager: ServiceManager,
appDataRepository: AppDataRepository,
backend: com.wireguard.android.backend.Backend,
): TunnelProvider {
return KernelTunnel(ioDispatcher, applicationScope, serviceManager, appDataRepository, backend)
}
@Provides
@Singleton
@Userspace
fun provideUserspaceProvider(
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
serviceManager: ServiceManager,
appDataRepository: AppDataRepository,
backend: Backend,
): TunnelProvider {
return UserspaceTunnel(ioDispatcher, applicationScope, serviceManager, appDataRepository, backend)
}
@Provides
@Singleton
fun provideTunnelManager(
@Kernel kernelTunnel: TunnelProvider,
@Userspace userspaceTunnel: TunnelProvider,
appDataRepository: AppDataRepository,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
): TunnelManager {
return TunnelManager(kernelTunnel, userspaceTunnel, appDataRepository, applicationScope, ioDispatcher)
}
@Provides
@Singleton
fun provideNetworkMonitor(@ApplicationContext context: Context, settingsRepository: AppSettingRepository): NetworkMonitor {
return AndroidNetworkMonitor(context) { runBlocking { settingsRepository.get().isWifiNameByShellEnabled } }
}
@Singleton
@Provides
fun provideServiceManager(
@ApplicationContext context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@MainDispatcher mainCoroutineDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
appDataRepository: AppDataRepository,
): ServiceManager {
return ServiceManager(context, ioDispatcher, applicationScope, mainCoroutineDispatcher, appDataRepository)
}
}
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.module
package com.zaneschepke.wireguardautotunnel.di
import android.content.Context
import com.zaneschepke.wireguardautotunnel.util.FileUtils
@@ -0,0 +1,29 @@
package com.zaneschepke.wireguardautotunnel.domain.entity
data class AppSettings(
val id: Int = 0,
val isAutoTunnelEnabled: Boolean = false,
val isTunnelOnMobileDataEnabled: Boolean = false,
val trustedNetworkSSIDs: List<String> = emptyList(),
val isAlwaysOnVpnEnabled: Boolean = false,
val isTunnelOnEthernetEnabled: Boolean = false,
val isShortcutsEnabled: Boolean = false,
val isTunnelOnWifiEnabled: Boolean = false,
val isKernelEnabled: Boolean = false,
val isRestoreOnBootEnabled: Boolean = false,
val isMultiTunnelEnabled: Boolean = false,
val isPingEnabled: Boolean = false,
val isAmneziaEnabled: Boolean = false,
val isWildcardsEnabled: Boolean = false,
val isWifiNameByShellEnabled: Boolean = false,
val isStopOnNoInternetEnabled: Boolean = false,
val isVpnKillSwitchEnabled: Boolean = false,
val isKernelKillSwitchEnabled: Boolean = false,
val isLanOnKillSwitchEnabled: Boolean = false,
val debounceDelaySeconds: Int = 3,
val isDisableKillSwitchOnTrustedEnabled: Boolean = false,
) {
fun debounceDelayMillis(): Long {
return debounceDelaySeconds * 1000L
}
}
@@ -0,0 +1,13 @@
package com.zaneschepke.wireguardautotunnel.domain.entity
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
data class AppState(
val isLocationDisclosureShown: Boolean,
val isBatteryOptimizationDisableShown: Boolean,
val isPinLockEnabled: Boolean,
val isTunnelStatsExpanded: Boolean,
val isLocalLogsEnabled: Boolean,
val locale: String?,
val theme: Theme,
)
@@ -0,0 +1,184 @@
package com.zaneschepke.wireguardautotunnel.domain.entity
import com.wireguard.android.backend.Tunnel
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.defaultName
import com.zaneschepke.wireguardautotunnel.util.extensions.extractNameAndNumber
import com.zaneschepke.wireguardautotunnel.util.extensions.hasNumberInParentheses
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.InputStream
import java.net.InetAddress
import java.nio.charset.StandardCharsets
import kotlin.coroutines.CoroutineContext
data class TunnelConf(
val id: Int = 0,
val tunName: String,
val wgQuick: String,
val tunnelNetworks: List<String> = emptyList(),
val isMobileDataTunnel: Boolean = false,
val isPrimaryTunnel: Boolean = false,
val amQuick: String,
val isActive: Boolean = false,
val isPingEnabled: Boolean = false,
val pingInterval: Long? = null,
val pingCooldown: Long? = null,
val pingIp: String? = null,
val isEthernetTunnel: Boolean = false,
val isIpv4Preferred: Boolean = true,
@Transient
private var stateChangeCallback: ((Any) -> Unit)? = null,
) : Tunnel, org.amnezia.awg.backend.Tunnel {
fun setStateChangeCallback(callback: (Any) -> Unit) {
stateChangeCallback = callback
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is TunnelConf) return false
return id == other.id && tunName == other.tunName && wgQuick == other.wgQuick && amQuick == other.amQuick &&
isPrimaryTunnel == other.isPrimaryTunnel && isMobileDataTunnel == other.isMobileDataTunnel &&
isEthernetTunnel == other.isEthernetTunnel && isPingEnabled == other.isPingEnabled && pingIp == other.pingIp &&
pingCooldown == other.pingCooldown && pingInterval == other.pingInterval && tunnelNetworks == other.tunnelNetworks &&
isIpv4Preferred == other.isIpv4Preferred
}
override fun hashCode(): Int {
var result = id
result = 31 * result + tunName.hashCode()
result = 31 * result + wgQuick.hashCode()
result = 31 * result + amQuick.hashCode()
return result
}
fun copyWithCallback(
id: Int = this.id,
tunName: String = this.tunName,
wgQuick: String = this.wgQuick,
tunnelNetworks: List<String> = this.tunnelNetworks,
isMobileDataTunnel: Boolean = this.isMobileDataTunnel,
isPrimaryTunnel: Boolean = this.isPrimaryTunnel,
amQuick: String = this.amQuick,
isActive: Boolean = this.isActive,
isPingEnabled: Boolean = this.isPingEnabled,
pingInterval: Long? = this.pingInterval,
pingCooldown: Long? = this.pingCooldown,
pingIp: String? = this.pingIp,
isEthernetTunnel: Boolean = this.isEthernetTunnel,
isIpv4Preferred: Boolean = this.isIpv4Preferred,
): TunnelConf {
return TunnelConf(
id, tunName, wgQuick, tunnelNetworks, isMobileDataTunnel, isPrimaryTunnel,
amQuick, isActive, isPingEnabled, pingInterval, pingCooldown, pingIp,
isEthernetTunnel, isIpv4Preferred,
).apply {
stateChangeCallback = this@TunnelConf.stateChangeCallback
// tunnelStatsCallback = this@TunnelConf.tunnelStatsCallback
// bounceTunnelCallback = this@TunnelConf.bounceTunnelCallback
}
}
// fun onUpdateStatistics() {
// tunnelStatsCallback?.invoke()
// }
//
// fun bounceTunnel(tunnelConf: TunnelConf, reason: TunnelStatus.StopReason) {
// bounceTunnelCallback?.invoke(tunnelConf, reason)
// }
fun toAmConfig(): org.amnezia.awg.config.Config {
return configFromAmQuick(amQuick.ifBlank { wgQuick })
}
fun toWgConfig(): Config {
return configFromWgQuick(wgQuick)
}
override fun getName(): String = tunName
override fun isIpv4ResolutionPreferred(): Boolean = isIpv4Preferred
override fun onStateChange(newState: org.amnezia.awg.backend.Tunnel.State) {
stateChangeCallback?.invoke(newState)
}
override fun onStateChange(newState: Tunnel.State) {
stateChangeCallback?.invoke(newState)
}
fun isTunnelConfigChanged(updatedConf: TunnelConf): Boolean {
return updatedConf.wgQuick != wgQuick || updatedConf.amQuick != amQuick || updatedConf.name != name
}
fun generateUniqueName(tunnelNames: List<String>): String {
var tunnelName = this.tunName
var num = 1
while (tunnelNames.any { it == tunnelName }) {
tunnelName = if (!tunnelName.hasNumberInParentheses()) {
"$name($num)"
} else {
val pair = tunnelName.extractNameAndNumber()
"${pair?.first}($num)"
}
num++
}
return tunnelName
}
suspend fun isTunnelPingable(context: CoroutineContext): Boolean {
return withContext(context) {
val config = toWgConfig()
if (pingIp != null) {
return@withContext InetAddress.getByName(pingIp)
.isReachable(Constants.PING_TIMEOUT.toInt()).also {
Timber.i("Ping reachable $pingIp: $it")
}
}
config.peers.map { peer ->
peer.isReachable()
}.all { true }.also {
Timber.i("Ping of all peers reachable: $it")
}
}
}
companion object {
fun configFromWgQuick(wgQuick: String): Config {
val inputStream: InputStream = wgQuick.byteInputStream()
return inputStream.bufferedReader(StandardCharsets.UTF_8).use {
Config.parse(it)
}
}
fun configFromAmQuick(amQuick: String): org.amnezia.awg.config.Config {
val inputStream: InputStream = amQuick.byteInputStream()
return inputStream.bufferedReader(StandardCharsets.UTF_8).use {
org.amnezia.awg.config.Config.parse(it)
}
}
fun tunnelConfigFromAmConfig(config: org.amnezia.awg.config.Config, name: String? = null): TunnelConf {
val amQuick = config.toAwgQuickString(true)
val wgQuick = config.toWgQuickString()
return TunnelConf(tunName = name ?: config.defaultName(), wgQuick = wgQuick, amQuick = amQuick)
}
private const val IPV6_ALL_NETWORKS = "::/0"
private const val IPV4_ALL_NETWORKS = "0.0.0.0/0"
val ALL_IPS = listOf(IPV4_ALL_NETWORKS, IPV6_ALL_NETWORKS)
private val IPV4_PUBLIC_NETWORKS = listOf(
"0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
"64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
"172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
"176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
"192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4",
)
val LAN_BYPASS_ALLOWED_IPS = listOf(IPV6_ALL_NETWORKS) + IPV4_PUBLIC_NETWORKS
}
}
@@ -0,0 +1,24 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
import com.zaneschepke.wireguardautotunnel.R
sealed class BackendError : Exception() {
data object DNS : BackendError()
data object Unauthorized : BackendError()
data object Config : BackendError()
data object KernelModuleName : BackendError()
data object InvalidConfig : BackendError()
data object NotAuthorized : BackendError()
data object ServiceNotRunning : BackendError()
data object Unknown : BackendError()
fun toStringRes() = when (this) {
Config -> R.string.config_error
DNS -> R.string.dns_resolve_error
InvalidConfig -> R.string.invalid_config_error
KernelModuleName -> R.string.kernel_name_error
NotAuthorized, Unauthorized -> R.string.auth_error
ServiceNotRunning -> R.string.service_running_error
Unknown -> R.string.unknown_error
}
}
@@ -0,0 +1,7 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
enum class BackendState {
KILL_SWITCH_ACTIVE,
SERVICE_ACTIVE,
INACTIVE,
}
@@ -0,0 +1,6 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
enum class ConfigType {
AMNEZIA,
WG,
}
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel
package com.zaneschepke.wireguardautotunnel.domain.enums
enum class HandshakeStatus {
HEALTHY,
@@ -0,0 +1,17 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
import android.content.Context
import com.zaneschepke.wireguardautotunnel.R
enum class NotificationAction {
TUNNEL_OFF,
AUTO_TUNNEL_OFF,
;
fun title(context: Context): String {
return when (this) {
TUNNEL_OFF -> context.getString(R.string.stop)
AUTO_TUNNEL_OFF -> context.getString(R.string.stop)
}
}
}
@@ -0,0 +1,31 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
sealed class TunnelStatus {
data class Error(val error: BackendError) : TunnelStatus()
data object Up : TunnelStatus()
data object Down : TunnelStatus()
data class Stopping(val reason: StopReason) : TunnelStatus()
data object Starting : TunnelStatus()
enum class StopReason {
USER,
PING,
CONFIG_CHANGED,
}
fun isDown(): Boolean {
return this == Down
}
fun isUp(): Boolean {
return this == Up
}
fun isUpOrStarting(): Boolean {
return this == Up || this == Starting
}
fun isDownOrStopping(): Boolean {
return this == Down || this is Stopping
}
}
@@ -0,0 +1,9 @@
package com.zaneschepke.wireguardautotunnel.domain.events
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
sealed class AutoTunnelEvent {
data class Start(val tunnelConf: TunnelConf? = null) : AutoTunnelEvent()
data object Stop : AutoTunnelEvent()
data object DoNothing : AutoTunnelEvent()
}
@@ -0,0 +1,7 @@
package com.zaneschepke.wireguardautotunnel.domain.events
sealed class KillSwitchEvent {
data class Start(val allowedIps: List<String>) : KillSwitchEvent()
data object Stop : KillSwitchEvent()
data object DoNothing : KillSwitchEvent()
}
@@ -0,0 +1,13 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
interface AppDataRepository {
suspend fun getPrimaryOrFirstTunnel(): TunnelConf?
suspend fun getStartTunnelConfig(): TunnelConf?
val settings: AppSettingRepository
val tunnels: TunnelRepository
val appState: AppStateRepository
}
@@ -0,0 +1,10 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import kotlinx.coroutines.flow.Flow
interface AppSettingRepository {
suspend fun save(appSettings: AppSettings)
val flow: Flow<AppSettings>
suspend fun get(): AppSettings
}
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.data.repository
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import kotlinx.coroutines.flow.Flow
interface AppStateRepository {
@@ -16,13 +17,21 @@ interface AppStateRepository {
suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
suspend fun getCurrentSsid(): String?
suspend fun setCurrentSsid(ssid: String)
suspend fun isTunnelStatsExpanded(): Boolean
suspend fun setTunnelStatsExpanded(expanded: Boolean)
val generalStateFlow: Flow<GeneralState>
suspend fun setTheme(theme: Theme)
suspend fun getTheme(): Theme
suspend fun isLocalLogsEnabled(): Boolean
suspend fun setLocalLogsEnabled(enabled: Boolean)
suspend fun setLocale(localeTag: String)
suspend fun getLocale(): String?
val flow: Flow<GeneralState>
}
@@ -0,0 +1,37 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import kotlinx.coroutines.flow.Flow
interface TunnelRepository {
val flow: Flow<List<TunnelConf>>
suspend fun getAll(): Tunnels
suspend fun save(tunnelConf: TunnelConf)
suspend fun saveAll(tunnelConfList: List<TunnelConf>)
suspend fun updatePrimaryTunnel(tunnelConf: TunnelConf?)
suspend fun updateMobileDataTunnel(tunnelConf: TunnelConf?)
suspend fun updateEthernetTunnel(tunnelConf: TunnelConf?)
suspend fun delete(tunnelConf: TunnelConf)
suspend fun getById(id: Int): TunnelConf?
suspend fun getActive(): Tunnels
suspend fun count(): Int
suspend fun findByTunnelName(name: String): TunnelConf?
suspend fun findByTunnelNetworksName(name: String): Tunnels
suspend fun findByMobileDataTunnel(): Tunnels
suspend fun findPrimary(): Tunnels
}
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
package com.zaneschepke.wireguardautotunnel.domain.state
import org.amnezia.awg.backend.Statistics
import org.amnezia.awg.crypto.Key
@@ -0,0 +1,159 @@
package com.zaneschepke.wireguardautotunnel.domain.state
import com.zaneschepke.wireguardautotunnel.core.tunnel.allDown
import com.zaneschepke.wireguardautotunnel.core.tunnel.hasActive
import com.zaneschepke.wireguardautotunnel.core.tunnel.isUp
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
data class AutoTunnelState(
val activeTunnels: Map<TunnelConf, TunnelState> = emptyMap(),
val networkState: NetworkState = NetworkState(),
val settings: AppSettings = AppSettings(),
val tunnels: List<TunnelConf> = emptyList(),
) {
private fun isMobileDataActive(): Boolean {
return !networkState.isEthernetConnected && !networkState.isWifiConnected && networkState.isMobileDataConnected
}
private fun isMobileTunnelDataChangeNeeded(): Boolean {
val preferredTunnel = preferredMobileDataTunnel()
return preferredTunnel != null &&
activeTunnels.isNotEmpty() && !activeTunnels.isUp(preferredTunnel)
}
private fun isEthernetTunnelChangeNeeded(): Boolean {
val preferredTunnel = preferredEthernetTunnel()
return preferredTunnel != null && activeTunnels.isNotEmpty() && !activeTunnels.isUp(preferredTunnel)
}
private fun preferredMobileDataTunnel(): TunnelConf? {
return tunnels.firstOrNull { it.isMobileDataTunnel } ?: tunnels.firstOrNull { it.isPrimaryTunnel }
}
private fun preferredEthernetTunnel(): TunnelConf? {
return tunnels.firstOrNull { it.isEthernetTunnel } ?: tunnels.firstOrNull { it.isPrimaryTunnel }
}
private fun preferredWifiTunnel(): TunnelConf? {
return getTunnelWithMatchingTunnelNetwork() ?: tunnels.firstOrNull { it.isPrimaryTunnel }
}
private fun isWifiActive(): Boolean {
return !networkState.isEthernetConnected && networkState.isWifiConnected
}
private fun startOnEthernet(): Boolean {
return networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled && activeTunnels.allDown()
}
private fun stopOnEthernet(): Boolean {
return networkState.isEthernetConnected && !settings.isTunnelOnEthernetEnabled && activeTunnels.hasActive()
}
// TODO test removed kill switch state check
private fun stopKillSwitchOnTrusted(): Boolean {
return networkState.isWifiConnected && settings.isVpnKillSwitchEnabled && settings.isDisableKillSwitchOnTrustedEnabled && isCurrentSSIDTrusted()
}
// TODO test, removed kill switch state check
private fun startKillSwitch(): Boolean {
return settings.isVpnKillSwitchEnabled && (!settings.isDisableKillSwitchOnTrustedEnabled || !isCurrentSSIDTrusted())
}
private fun isNoConnectivity(): Boolean {
return !networkState.isEthernetConnected && !networkState.isWifiConnected && !networkState.isMobileDataConnected
}
private fun stopOnMobileData(): Boolean {
return isMobileDataActive() && !settings.isTunnelOnMobileDataEnabled && activeTunnels.hasActive()
}
private fun startOnMobileData(): Boolean {
return isMobileDataActive() && settings.isTunnelOnMobileDataEnabled && activeTunnels.allDown()
}
private fun changeOnMobileData(): Boolean {
return isMobileDataActive() && settings.isTunnelOnMobileDataEnabled && isMobileTunnelDataChangeNeeded()
}
private fun changeOnEthernet(): Boolean {
return networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled && isEthernetTunnelChangeNeeded()
}
private fun stopOnWifi(): Boolean {
return isWifiActive() && !settings.isTunnelOnWifiEnabled && activeTunnels.hasActive()
}
private fun stopOnTrustedWifi(): Boolean {
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.hasActive() && isCurrentSSIDTrusted()
}
private fun startOnUntrustedWifi(): Boolean {
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.allDown() && !isCurrentSSIDTrusted()
}
private fun changeOnUntrustedWifi(): Boolean {
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.hasActive() && !isCurrentSSIDTrusted() && !isWifiTunnelPreferred()
}
private fun isWifiTunnelPreferred(): Boolean {
val preferred = preferredWifiTunnel()
return preferred?.let { activeTunnels.isUp(it) } ?: true
}
fun asAutoTunnelEvent(): AutoTunnelEvent {
return when {
// ethernet scenarios
stopOnEthernet() -> AutoTunnelEvent.Stop
startOnEthernet() || changeOnEthernet() -> AutoTunnelEvent.Start(preferredEthernetTunnel())
// mobile data scenarios
stopOnMobileData() -> AutoTunnelEvent.Stop
startOnMobileData() || changeOnMobileData() -> AutoTunnelEvent.Start(preferredMobileDataTunnel())
// wifi scenarios
stopOnWifi() -> AutoTunnelEvent.Stop
stopOnTrustedWifi() -> AutoTunnelEvent.Stop
startOnUntrustedWifi() || changeOnUntrustedWifi() -> AutoTunnelEvent.Start(preferredWifiTunnel())
// no connectivity
isNoConnectivity() && settings.isStopOnNoInternetEnabled -> AutoTunnelEvent.Stop
else -> AutoTunnelEvent.DoNothing
}
}
fun asKillSwitchEvent(): KillSwitchEvent {
return when {
stopKillSwitchOnTrusted() -> KillSwitchEvent.Stop
startKillSwitch() -> {
val allowedIps = if (settings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS else emptyList()
KillSwitchEvent.Start(allowedIps)
}
else -> KillSwitchEvent.DoNothing
}
}
private fun isCurrentSSIDTrusted(): Boolean {
return networkState.wifiName?.let {
hasTrustedWifiName(it)
} == true
}
private fun hasTrustedWifiName(wifiName: String, wifiNames: List<String> = settings.trustedNetworkSSIDs): Boolean {
return if (settings.isWildcardsEnabled) {
wifiNames.isMatchingToWildcardList(wifiName)
} else {
wifiNames.contains(wifiName)
}
}
private fun getTunnelWithMatchingTunnelNetwork(): TunnelConf? {
return networkState.wifiName?.let { wifiName ->
tunnels.firstOrNull {
hasTrustedWifiName(wifiName, it.tunnelNetworks)
}
}
}
}
@@ -0,0 +1,9 @@
package com.zaneschepke.wireguardautotunnel.domain.state
data class ConnectivityState(
val wifiAvailable: Boolean,
val ethernetAvailable: Boolean,
val cellularAvailable: Boolean,
) {
val allOffline = !wifiAvailable && !ethernetAvailable && !cellularAvailable
}
@@ -0,0 +1,12 @@
package com.zaneschepke.wireguardautotunnel.domain.state
data class NetworkState(
val isWifiConnected: Boolean = false,
val isMobileDataConnected: Boolean = false,
val isEthernetConnected: Boolean = false,
val wifiName: String? = null,
) {
fun hasNoCapabilities(): Boolean {
return !isWifiConnected && !isMobileDataConnected && !isEthernetConnected
}
}
@@ -0,0 +1,10 @@
package com.zaneschepke.wireguardautotunnel.domain.state
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
data class TunnelState(
val status: TunnelStatus = TunnelStatus.Down,
val backendState: BackendState = BackendState.INACTIVE,
val statistics: TunnelStatistics? = null,
)
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
package com.zaneschepke.wireguardautotunnel.domain.state
import org.amnezia.awg.crypto.Key
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
package com.zaneschepke.wireguardautotunnel.domain.state
import com.wireguard.android.backend.Statistics
import org.amnezia.awg.crypto.Key
@@ -1,30 +0,0 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.logcatter.LogcatCollector
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class AppModule {
@Singleton
@ApplicationScope
@Provides
fun providesApplicationScope(@DefaultDispatcher defaultDispatcher: CoroutineDispatcher): CoroutineScope =
CoroutineScope(SupervisorJob() + defaultDispatcher)
@Singleton
@Provides
fun provideLogCollect(@ApplicationContext context: Context): LogReader {
return LogcatCollector.init(context = context)
}
}
@@ -1,11 +0,0 @@
package com.zaneschepke.wireguardautotunnel.module
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Kernel
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Userspace
@@ -1,33 +0,0 @@
package com.zaneschepke.wireguardautotunnel.module
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ServiceComponent
import dagger.hilt.android.scopes.ServiceScoped
@Module
@InstallIn(ServiceComponent::class)
abstract class ServiceModule {
@Binds
@ServiceScoped
abstract fun provideNotificationService(wireGuardNotification: WireGuardNotification): NotificationService
@Binds
@ServiceScoped
abstract fun provideWifiService(wifiService: WifiService): NetworkService<WifiService>
@Binds
@ServiceScoped
abstract fun provideMobileDataService(mobileDataService: MobileDataService): NetworkService<MobileDataService>
@Binds
@ServiceScoped
abstract fun provideEthernetService(ethernetService: EthernetService): NetworkService<EthernetService>
}
@@ -1,78 +0,0 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.GoBackend
import com.wireguard.android.backend.RootTunnelActionHandler
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import javax.inject.Provider
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class TunnelModule {
@Provides
@Singleton
fun provideRootShell(@ApplicationContext context: Context): RootShell {
return RootShell(context)
}
@Provides
@Singleton
fun provideRootShellAm(@ApplicationContext context: Context): org.amnezia.awg.util.RootShell {
return org.amnezia.awg.util.RootShell(context)
}
@Provides
@Singleton
@Userspace
fun provideUserspaceBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
return GoBackend(context, RootTunnelActionHandler(rootShell))
}
@Provides
@Singleton
@Kernel
fun provideKernelBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell), RootTunnelActionHandler(rootShell))
}
@Provides
@Singleton
fun provideAmneziaBackend(@ApplicationContext context: Context, rootShell: org.amnezia.awg.util.RootShell): org.amnezia.awg.backend.Backend {
return org.amnezia.awg.backend.GoBackend(context, org.amnezia.awg.backend.RootTunnelActionHandler(rootShell))
}
@Provides
@Singleton
fun provideVpnService(
amneziaBackend: Provider<org.amnezia.awg.backend.Backend>,
@Kernel kernelBackend: Provider<Backend>,
appDataRepository: AppDataRepository,
tunnelConfigRepository: TunnelConfigRepository,
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): TunnelService {
return WireGuardTunnel(
amneziaBackend,
tunnelConfigRepository,
kernelBackend,
appDataRepository,
applicationScope,
ioDispatcher,
)
}
}
@@ -1,44 +0,0 @@
package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class AppUpdateReceiver : BroadcastReceiver() {
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var tunnelService: TunnelService
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return
applicationScope.launch {
val settings = appDataRepository.settings.getSettings()
if (settings.isAutoTunnelEnabled) {
Timber.i("Restarting services after upgrade")
ServiceManager.startWatcherServiceForeground(context)
}
if (!settings.isAutoTunnelEnabled || settings.isAutoTunnelPaused) {
val tunnels = appDataRepository.tunnels.getAll().filter { it.isActive }
if (tunnels.isNotEmpty()) context.startTunnelBackground(tunnels.first().id)
}
}
}
}
@@ -1,61 +0,0 @@
package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class BackgroundActionReceiver : BroadcastReceiver() {
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var tunnelService: Provider<TunnelService>
@Inject
lateinit var tunnelConfigRepository: TunnelConfigRepository
override fun onReceive(context: Context, intent: Intent) {
val id = intent.getIntExtra(TUNNEL_ID_EXTRA_KEY, 0)
if (id == 0) return
when (intent.action) {
ACTION_CONNECT -> {
Timber.d("Connect actions")
applicationScope.launch {
val tunnel = tunnelConfigRepository.getById(id)
tunnel?.let {
ServiceManager.startTunnelBackgroundService(context)
tunnelService.get().startTunnel(it)
}
}
}
ACTION_DISCONNECT -> {
applicationScope.launch {
val tunnel = tunnelConfigRepository.getById(id)
tunnel?.let {
ServiceManager.stopTunnelBackgroundService(context)
tunnelService.get().stopTunnel(it)
}
}
}
}
}
companion object {
const val ACTION_CONNECT = "ACTION_CONNECT"
const val ACTION_DISCONNECT = "ACTION_DISCONNECT"
const val TUNNEL_ID_EXTRA_KEY = "tunnelId"
}
}
@@ -1,50 +0,0 @@
package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class BootReceiver : BroadcastReceiver() {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var tunnelService: Provider<TunnelService>
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_BOOT_COMPLETED != intent.action) return
applicationScope.launch {
with(appDataRepository.settings.getSettings()) {
if (isRestoreOnBootEnabled) {
val activeTunnels = appDataRepository.tunnels.getActive()
val tunState = tunnelService.get().vpnState.value.status
if (activeTunnels.isNotEmpty() && tunState != TunnelState.UP) {
Timber.i("Starting previously active tunnel")
context.startTunnelBackground(activeTunnels.first().id)
}
if (isAutoTunnelEnabled) {
Timber.i("Starting watcher service from boot")
ServiceManager.startWatcherServiceForeground(context)
}
}
}
}
}
}
@@ -1,48 +0,0 @@
package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class KernelReceiver : BroadcastReceiver() {
@Inject
lateinit var tunnelService: Provider<TunnelService>
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var tunnelConfigRepository: TunnelConfigRepository
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return
applicationScope.launch {
if (action == REFRESH_TUNNELS_ACTION) {
tunnelService.get().runningTunnelNames().forEach { name ->
// TODO can optimize later
val tunnel = tunnelConfigRepository.findByTunnelName(name)
tunnel?.let {
tunnelConfigRepository.save(it.copy(isActive = true))
}
}
context.requestTunnelTileServiceStateUpdate()
}
}
}
companion object {
const val REFRESH_TUNNELS_ACTION = "com.wireguard.android.action.REFRESH_TUNNEL_STATES"
}
}
@@ -1,8 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
enum class Action {
START,
START_FOREGROUND,
STOP,
STOP_FOREGROUND,
}
@@ -1,589 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.content.Context
import android.content.Intent
import android.net.NetworkCapabilities
import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
import com.zaneschepke.wireguardautotunnel.util.extensions.onNotRunning
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.net.InetAddress
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class AutoTunnelService : LifecycleService() {
private val foregroundId = 122
@Inject
lateinit var rootShell: Provider<RootShell>
@Inject
lateinit var wifiService: NetworkService<WifiService>
@Inject
lateinit var mobileDataService: NetworkService<MobileDataService>
@Inject
lateinit var ethernetService: NetworkService<EthernetService>
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var notificationService: NotificationService
@Inject
lateinit var tunnelService: Provider<TunnelService>
@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
@Inject
@MainImmediateDispatcher
lateinit var mainImmediateDispatcher: CoroutineDispatcher
private val autoTunnelStateFlow = MutableStateFlow(AutoTunnelState())
private var wakeLock: PowerManager.WakeLock? = null
private var wifiJob: Job? = null
private var mobileDataJob: Job? = null
private var ethernetJob: Job? = null
private var pingJob: Job? = null
private var networkEventJob: Job? = null
@get:Synchronized @set:Synchronized
private var running: Boolean = false
override fun onCreate() {
super.onCreate()
lifecycleScope.launch(mainImmediateDispatcher) {
kotlin.runCatching {
launchNotification()
}.onFailure {
Timber.e(it)
}
}
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
// We don't provide binding, so return null
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Timber.d("onStartCommand executed with startId: $startId")
if (intent != null) {
val action = intent.action
when (action) {
Action.START.name,
Action.START_FOREGROUND.name,
-> startService()
Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
}
}
return super.onStartCommand(intent, flags, startId)
}
private suspend fun launchNotification() {
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
launchWatcherPausedNotification()
} else {
launchWatcherNotification()
}
}
private fun startService() {
if (running) return
running = true
kotlin.runCatching {
lifecycleScope.launch(mainImmediateDispatcher) {
launchNotification()
initWakeLock()
}
startSettingsJob()
startVpnStateJob()
}.onFailure {
Timber.e(it)
}
}
private fun stopService() {
wakeLock?.let {
if (it.isHeld) {
it.release()
}
}
stopSelf()
}
override fun onDestroy() {
cancelAndResetNetworkJobs()
cancelAndResetPingJob()
super.onDestroy()
}
private fun launchWatcherNotification(description: String = getString(R.string.watcher_notification_text_active)) {
val notification =
notificationService.createNotification(
channelId = getString(R.string.watcher_channel_id),
channelName = getString(R.string.watcher_channel_name),
title = getString(R.string.auto_tunnel_title),
description = description,
)
ServiceCompat.startForeground(
this,
foregroundId,
notification,
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
}
private fun launchWatcherPausedNotification() {
launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
}
private fun initWakeLock() {
wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
val tag = this.javaClass.name
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
try {
Timber.i("Initiating wakelock with 10 min timeout")
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
} finally {
release()
}
}
}
}
private fun startSettingsJob() = lifecycleScope.launch {
watchForSettingsChanges()
}
private fun startVpnStateJob() = lifecycleScope.launch {
watchForVpnStateChanges()
}
private fun startWifiJob() = lifecycleScope.launch {
watchForWifiConnectivityChanges()
}
private fun startMobileDataJob() = lifecycleScope.launch {
watchForMobileDataConnectivityChanges()
}
private fun startEthernetJob() = lifecycleScope.launch {
watchForEthernetConnectivityChanges()
}
private fun startPingJob() = lifecycleScope.launch {
watchForPingFailure()
}
private fun startNetworkEventJob() = lifecycleScope.launch {
handleNetworkEventChanges()
}
private suspend fun watchForMobileDataConnectivityChanges() {
withContext(ioDispatcher) {
Timber.i("Starting mobile data watcher")
mobileDataService.networkStatus.collect { status ->
when (status) {
is NetworkStatus.Available -> {
Timber.i("Gained Mobile data connection")
autoTunnelStateFlow.update {
it.copy(
isMobileDataConnected = true,
)
}
}
is NetworkStatus.CapabilitiesChanged -> {
autoTunnelStateFlow.update {
it.copy(
isMobileDataConnected = true,
)
}
Timber.i("Mobile data capabilities changed")
}
is NetworkStatus.Unavailable -> {
autoTunnelStateFlow.update {
it.copy(
isMobileDataConnected = false,
)
}
Timber.i("Lost mobile data connection")
}
}
}
}
}
private suspend fun watchForPingFailure() {
withContext(ioDispatcher) {
Timber.i("Starting ping watcher")
runCatching {
do {
val vpnState = tunnelService.get().vpnState.value
val settings = appDataRepository.settings.getSettings()
if (vpnState.status == TunnelState.UP && !settings.isAutoTunnelPaused) {
if (vpnState.tunnelConfig != null) {
val config = TunnelConfig.configFromWgQuick(vpnState.tunnelConfig.wgQuick)
val results = if (vpnState.tunnelConfig.pingIp != null) {
Timber.d("Pinging custom ip : ${vpnState.tunnelConfig.pingIp}")
listOf(InetAddress.getByName(vpnState.tunnelConfig.pingIp).isReachable(Constants.PING_TIMEOUT.toInt()))
} else {
Timber.d("Pinging all peers")
config.peers.map { peer ->
peer.isReachable()
}
}
Timber.i("Ping results reachable: $results")
if (results.contains(false)) {
Timber.i("Restarting VPN for ping failure")
val cooldown = vpnState.tunnelConfig.pingCooldown
tunnelService.get().bounceTunnel(vpnState.tunnelConfig)
delay(cooldown ?: Constants.PING_COOLDOWN)
continue
}
}
}
delay(vpnState.tunnelConfig?.pingInterval ?: Constants.PING_INTERVAL)
} while (true)
}.onFailure {
Timber.e(it)
}
}
}
private fun onAutoTunnelPause(paused: Boolean) {
if (autoTunnelStateFlow.value.settings.isAutoTunnelPaused
!= paused
) {
when (paused) {
true -> launchWatcherPausedNotification()
false -> launchWatcherNotification()
}
}
}
private suspend fun watchForSettingsChanges() {
Timber.i("Starting settings watcher")
withContext(ioDispatcher) {
appDataRepository.settings.getSettingsFlow().combine(
// ignore isActive changes to allow manual tunnel overrides
appDataRepository.tunnels.getTunnelConfigsFlow().distinctUntilChanged { old, new ->
old.map { it.isActive } != new.map { it.isActive }
},
) { settings, tunnels ->
autoTunnelStateFlow.value.copy(
settings = settings,
tunnels = tunnels,
)
}.collect {
onAutoTunnelPause(it.settings.isAutoTunnelPaused)
manageJobsBySettings(it.settings)
autoTunnelStateFlow.emit(it)
}
}
}
private suspend fun watchForVpnStateChanges() {
Timber.i("Starting vpn state watcher")
withContext(ioDispatcher) {
tunnelService.get().vpnState.collect { state ->
state.tunnelConfig?.let {
val settings = appDataRepository.settings.getSettings()
if (it.isPingEnabled && !settings.isPingEnabled) {
pingJob.onNotRunning { pingJob = startPingJob() }
}
if (!it.isPingEnabled && !settings.isPingEnabled) {
cancelAndResetPingJob()
}
}
}
}
}
private fun manageJobsBySettings(settings: Settings) {
with(settings) {
if (isPingEnabled) {
pingJob.onNotRunning { pingJob = startPingJob() }
} else {
cancelAndResetPingJob()
}
if (isTunnelOnWifiEnabled || isTunnelOnEthernetEnabled || isTunnelOnMobileDataEnabled) {
startNetworkJobs()
} else {
cancelAndResetNetworkJobs()
}
}
}
private fun startNetworkJobs() {
wifiJob.onNotRunning {
Timber.i("Wifi job starting")
wifiJob = startWifiJob()
}
ethernetJob.onNotRunning {
ethernetJob = startEthernetJob()
Timber.i("Ethernet job starting")
}
mobileDataJob.onNotRunning {
mobileDataJob = startMobileDataJob()
Timber.i("Mobile data job starting")
}
networkEventJob.onNotRunning {
Timber.i("Network event job starting")
networkEventJob = startNetworkEventJob()
}
}
private fun cancelAndResetPingJob() {
pingJob?.cancelWithMessage("Ping job canceled")
pingJob = null
}
private fun cancelAndResetNetworkJobs() {
networkEventJob?.cancelWithMessage("Network event job canceled")
wifiJob?.cancelWithMessage("Wifi job canceled")
ethernetJob?.cancelWithMessage("Ethernet job canceled")
mobileDataJob?.cancelWithMessage("Mobile data job canceled")
networkEventJob = null
wifiJob = null
ethernetJob = null
mobileDataJob = null
}
private fun updateEthernet(connected: Boolean) {
autoTunnelStateFlow.update {
it.copy(
isEthernetConnected = connected,
)
}
}
private fun updateWifi(connected: Boolean) {
autoTunnelStateFlow.update {
it.copy(
isWifiConnected = connected,
)
}
}
private suspend fun watchForEthernetConnectivityChanges() {
withContext(ioDispatcher) {
Timber.i("Starting ethernet data watcher")
ethernetService.networkStatus.collect { status ->
when (status) {
is NetworkStatus.Available -> {
Timber.i("Gained Ethernet connection")
updateEthernet(true)
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Ethernet capabilities changed")
updateEthernet(true)
}
is NetworkStatus.Unavailable -> {
updateEthernet(false)
Timber.i("Lost Ethernet connection")
}
}
}
}
}
private suspend fun watchForWifiConnectivityChanges() {
withContext(ioDispatcher) {
Timber.i("Starting wifi watcher")
wifiService.networkStatus.collect { status ->
when (status) {
is NetworkStatus.Available -> {
Timber.i("Gained Wi-Fi connection")
updateWifi(true)
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Wifi capabilities changed")
updateWifi(true)
val ssid = getWifiSSID(status.networkCapabilities)
ssid?.let { name ->
if (name.contains(Constants.UNREADABLE_SSID)) {
Timber.w("SSID unreadable: missing permissions")
} else {
Timber.i("Detected valid SSID")
}
appDataRepository.appState.setCurrentSsid(name)
autoTunnelStateFlow.update {
it.copy(
currentNetworkSSID = name,
)
}
} ?: Timber.w("Failed to read ssid")
}
is NetworkStatus.Unavailable -> {
updateWifi(false)
Timber.i("Lost Wi-Fi connection")
}
}
}
}
}
private suspend fun getWifiSSID(networkCapabilities: NetworkCapabilities): String? {
return withContext(ioDispatcher) {
try {
rootShell.get().getCurrentWifiName()
} catch (_: Exception) {
wifiService.getNetworkName(networkCapabilities)
}
}
}
private suspend fun getMobileDataTunnel(): TunnelConfig? {
return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull()
}
private fun isTunnelDown(): Boolean {
return tunnelService.get().vpnState.value.status == TunnelState.DOWN
}
private suspend fun handleNetworkEventChanges() {
withContext(ioDispatcher) {
Timber.i("Starting network event watcher")
autoTunnelStateFlow.collectLatest { watcherState ->
val autoTunnel = "Auto-tunnel watcher"
if (!watcherState.settings.isAutoTunnelPaused) {
// delay for rapid network state changes and then collect latest
delay(Constants.WATCHER_COLLECTION_DELAY)
val activeTunnel = tunnelService.get().vpnState.value.tunnelConfig
val defaultTunnel = appDataRepository.getPrimaryOrFirstTunnel()
when {
watcherState.isEthernetConditionMet() -> {
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
if (isTunnelDown()) {
defaultTunnel?.let {
tunnelService.get().startTunnel(it)
}
}
}
watcherState.isMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel on mobile data condition met")
val mobileDataTunnel = getMobileDataTunnel()
val tunnel =
mobileDataTunnel ?: defaultTunnel
if (isTunnelDown() || activeTunnel?.isMobileDataTunnel == false) {
tunnel?.let {
tunnelService.get().startTunnel(it)
}
}
}
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
if (!isTunnelDown()) {
activeTunnel?.let {
tunnelService.get().stopTunnel(it)
}
}
}
watcherState.isUntrustedWifiConditionMet() -> {
Timber.i("Untrusted wifi condition met")
if (activeTunnel?.tunnelNetworks?.isMatchingToWildcardList(watcherState.currentNetworkSSID) == false ||
activeTunnel == null || isTunnelDown()
) {
Timber.i(
"$autoTunnel - tunnel on ssid not associated with current tunnel condition met",
)
watcherState.tunnels.firstOrNull { it.tunnelNetworks.isMatchingToWildcardList(watcherState.currentNetworkSSID) }?.let {
Timber.i("Found tunnel associated with this SSID, bringing tunnel up: ${it.name}")
if (isTunnelDown() || activeTunnel?.id != it.id) {
tunnelService.get().startTunnel(it)
}
} ?: suspend {
Timber.i("No tunnel associated with this SSID, using defaults")
val default = appDataRepository.getPrimaryOrFirstTunnel()
if (default?.name != tunnelService.get().name || isTunnelDown()) {
default?.let {
tunnelService.get().startTunnel(it)
}
}
}.invoke()
}
}
watcherState.isTrustedWifiConditionMet() -> {
Timber.i(
"$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off",
)
if (!isTunnelDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
}
watcherState.isTunnelOffOnWifiConditionMet() -> {
Timber.i(
"$autoTunnel - tunnel off on wifi condition met, turning vpn off",
)
if (!isTunnelDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
}
watcherState.isTunnelOffOnNoConnectivityMet() -> {
Timber.i(
"$autoTunnel - tunnel off on no connectivity met, turning vpn off",
)
if (!isTunnelDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
}
else -> {
Timber.i("$autoTunnel - no condition met")
}
}
}
}
}
}
}
@@ -1,76 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
data class AutoTunnelState(
val isWifiConnected: Boolean = false,
val isEthernetConnected: Boolean = false,
val isMobileDataConnected: Boolean = false,
val currentNetworkSSID: String = "",
val settings: Settings = Settings(),
val tunnels: TunnelConfigs = emptyList(),
) {
fun isEthernetConditionMet(): Boolean {
return (
isEthernetConnected &&
settings.isTunnelOnEthernetEnabled
)
}
fun isMobileDataConditionMet(): Boolean {
return (
!isEthernetConnected &&
settings.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
isMobileDataConnected
)
}
fun isTunnelOffOnMobileDataConditionMet(): Boolean {
return (
!isEthernetConnected &&
!settings.isTunnelOnMobileDataEnabled &&
isMobileDataConnected &&
!isWifiConnected
)
}
fun isUntrustedWifiConditionMet(): Boolean {
return (
!isEthernetConnected &&
isWifiConnected &&
!settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID) &&
settings.isTunnelOnWifiEnabled
)
}
fun isTrustedWifiConditionMet(): Boolean {
return (
!isEthernetConnected &&
(
isWifiConnected &&
settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID)
)
)
}
fun isTunnelOffOnWifiConditionMet(): Boolean {
return (
!isEthernetConnected &&
(
isWifiConnected &&
!settings.isTunnelOnWifiEnabled
)
)
}
fun isTunnelOffOnNoConnectivityMet(): Boolean {
return (
!isEthernetConnected &&
!isWifiConnected &&
!isMobileDataConnected
)
}
}
@@ -1,71 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.VpnService
import timber.log.Timber
object ServiceManager {
private fun <T : Service> actionOnService(action: Action, context: Context, cls: Class<T>, extras: Map<String, Int>? = null) {
if (VpnService.prepare(context) != null) return
val intent =
Intent(context, cls).also {
it.action = action.name
extras?.forEach { (k, v) -> it.putExtra(k, v) }
}
intent.component?.javaClass
try {
when (action) {
Action.START_FOREGROUND, Action.STOP_FOREGROUND ->
context.startForegroundService(
intent,
)
Action.START, Action.STOP -> context.startService(intent)
}
} catch (e: Exception) {
Timber.e(e.message)
}
}
fun startWatcherServiceForeground(context: Context) {
actionOnService(
Action.START_FOREGROUND,
context,
AutoTunnelService::class.java,
)
}
fun startWatcherService(context: Context) {
actionOnService(
Action.START,
context,
AutoTunnelService::class.java,
)
}
fun stopWatcherService(context: Context) {
actionOnService(
Action.STOP,
context,
AutoTunnelService::class.java,
)
}
fun startTunnelBackgroundService(context: Context) {
actionOnService(
Action.START_FOREGROUND,
context,
TunnelBackgroundService::class.java,
)
}
fun stopTunnelBackgroundService(context: Context) {
actionOnService(
Action.STOP,
context,
TunnelBackgroundService::class.java,
)
}
}
@@ -1,61 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Notification
import android.content.Intent
import android.os.IBinder
import androidx.lifecycle.LifecycleService
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class TunnelBackgroundService : LifecycleService() {
@Inject
lateinit var notificationService: NotificationService
private val foregroundId = 123
override fun onCreate() {
super.onCreate()
startForeground(foregroundId, createNotification())
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
// We don't provide binding, so return null
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent != null) {
val action = intent.action
when (action) {
Action.START.name,
Action.START_FOREGROUND.name,
-> startService()
Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
}
}
return super.onStartCommand(intent, flags, startId)
}
private fun startService() {
startForeground(foregroundId, createNotification())
}
private fun stopService() {
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
private fun createNotification(): Notification {
return notificationService.createNotification(
getString(R.string.vpn_channel_id),
getString(R.string.vpn_channel_name),
getString(R.string.tunnel_start_text),
description = "",
)
}
}
@@ -1,101 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.network
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.wifi.WifiManager
import android.os.Build
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.map
abstract class BaseNetworkService<T : BaseNetworkService<T>>(
val context: Context,
networkCapability: Int,
) : NetworkService<T> {
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val wifiManager =
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
override val networkStatus =
callbackFlow {
val networkStatusCallback =
when (Build.VERSION.SDK_INT) {
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
object :
ConnectivityManager.NetworkCallback(
FLAG_INCLUDE_LOCATION_INFO,
) {
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network))
}
override fun onLost(network: Network) {
trySend(NetworkStatus.Unavailable(network))
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
trySend(
NetworkStatus.CapabilitiesChanged(
network,
networkCapabilities,
),
)
}
}
}
else -> {
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network))
}
override fun onLost(network: Network) {
trySend(NetworkStatus.Unavailable(network))
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
trySend(
NetworkStatus.CapabilitiesChanged(
network,
networkCapabilities,
),
)
}
}
}
}
val request =
NetworkRequest.Builder()
.addTransportType(networkCapability)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
}
}
inline fun <Result> Flow<NetworkStatus>.map(
crossinline onUnavailable: suspend (network: Network) -> Result,
crossinline onAvailable: suspend (network: Network) -> Result,
crossinline onCapabilitiesChanged:
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result,
): Flow<Result> = map { status ->
when (status) {
is NetworkStatus.Unavailable -> onUnavailable(status.network)
is NetworkStatus.Available -> onAvailable(status.network)
is NetworkStatus.CapabilitiesChanged ->
onCapabilitiesChanged(
status.network,
status.networkCapabilities,
)
}
}
@@ -1,18 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.network
import android.content.Context
import android.net.NetworkCapabilities
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class EthernetService
@Inject
constructor(
@ApplicationContext context: Context,
) :
BaseNetworkService<EthernetService>(context, NetworkCapabilities.TRANSPORT_ETHERNET) {
override fun isNetworkSecure(): Boolean {
return true
}
}

Some files were not shown because too many files have changed in this diff Show More