Compare commits

...

116 Commits

Author SHA1 Message Date
zaneschepke c130247df1 chore: release 4.3.0 2026-03-09 19:54:14 -04:00
zaneschepke 074229b6b4 fix: doze mode tunnel connectivity bug 2026-03-09 16:30:03 -04:00
zaneschepke 585176f08d chore: fmt 2026-03-07 22:13:38 -05:00
Max Grakov 2ed06728e3 feat: show consumed traffic in notification (#1165) 2026-03-07 21:58:32 -05:00
Naonak 2d9c5ece4a fix(core): always poll WireGuard stats regardless of Doze mode (#1177) 2026-03-07 18:48:15 -05:00
Weblate (bot) 3b69f620fb Translations update from Hosted Weblate (#1124)
Co-authored-by: Prefill add-on <noreply-addon-prefill@weblate.org>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: EESF-2 <eesf-2@users.noreply.hosted.weblate.org>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: Kachelkaiser <kachelkaiser@htpst.de>
Co-authored-by: Aleksandre Ghvineria <Ghvinerias@gmail.com>
Co-authored-by: Fill read-only add-on <noreply-addon-fill@weblate.org>
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: lateweb <weblate@techkoala.net>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Alvar Kusma <kaabuta@gmail.com>
Co-authored-by: Denny Schwender <denny.schwender@gmail.com>
Co-authored-by: Salizan <sohrab.sy1@gmail.com>
Co-authored-by: Jacob <jacob.venborg@gmail.com>
Co-authored-by: CyanWolf <hydemr@pm.me>
Co-authored-by: Mostafa Kian <mostafakian77@gmail.com>
Co-authored-by: Tomáš Pernekr <leostreamer@gmail.com>
Co-authored-by: PhillyMay <mein.alias@outlook.com>
Co-authored-by: Vitaliy Bilyk <vitaliy.bilyk@gmail.com>
Co-authored-by: SanctumSaturn <oraculogithub@gmail.com>
Co-authored-by: Saeid Sadat <9773heeytgege@gmail.com>
Co-authored-by: catelixor <catelixor+weblate@proton.me>
Co-authored-by: HalfMolar <weblate@reitsmas.nl>
Co-authored-by: apemay <aperezmayol@gmail.com>
Co-authored-by: Quang cai Boong <a0974284912@gmail.com>
Co-authored-by: jaime-grj <weblate.4ljj9@aleeas.com>
Co-authored-by: Zaparojine <passerby.feiz@yahoo.com>
Co-authored-by: ojiouigyutycghjbknljioui <weblate@victordrijkoningen.nl>
Co-authored-by: กวาง ครับ <mr.krisda2542@gmail.com>
Co-authored-by: Arif Budiman <arifpedia@gmail.com>
Co-authored-by: justcontributor <kty5663@gmail.com>
Co-authored-by: spreedawd <cosmos895@proton.me>
Co-authored-by: Zane Schepke <zanecschepke@gmail.com>
2026-03-07 12:32:13 -05:00
Henry Essinghigh 6369d8975c feat: support for filtering by endpoint latency (#1155) 2026-03-07 12:22:30 -05:00
zaneschepke 0c57bea2ff Merge branch 'master' of github.com:wgtunnel/wgtunnel 2026-03-07 12:02:45 -05:00
zaneschepke 5f8f699ab5 chore: bump deps 2026-03-07 12:02:31 -05:00
dependabot[bot] d0f58615b0 chore(deps): bump actions/upload-artifact from 6 to 7 (#1178)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-07 01:36:54 -05:00
dependabot[bot] 35982aa345 chore(deps): bump actions/download-artifact from 7 to 8 (#1179)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-07 01:36:43 -05:00
zaneschepke 0344b8fde8 fix: optional I2-5 params and socks5/lockdown crash on heavy load 2026-03-07 01:36:14 -05:00
zaneschepke bdd7c9689c fix: create from scratch bug, proxy mode UDP crash
Bump versions

closes #1149
closes #1141
2026-01-24 21:26:04 -05:00
zaneschepke b641539af8 chore: release v4.2.2 2025-12-26 00:58:08 -05:00
Weblate (bot) 9f9a15a97c feat(lang): translations from Weblate (#1115)
Co-authored-by: Fill read-only add-on <noreply-addon-fill@weblate.org>
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Qotsa1984 <carlominzi@inwind.it>
Co-authored-by: jaime-grj <weblate.4ljj9@aleeas.com>
Co-authored-by: catelixor <catelixor+weblate@proton.me>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: Prefill add-on <noreply-addon-prefill@weblate.org>
Co-authored-by: Kachelkaiser <kachelkaiser@htpst.de>
Co-authored-by: Henrik Sozzi <henrik_sozzi@hotmail.com>
Co-authored-by: EESF-2 <eesf-2@users.noreply.hosted.weblate.org>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: Aleksandre Ghvineria <Ghvinerias@gmail.com>
Co-authored-by: lateweb <weblate@techkoala.net>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Alvar Kusma <kaabuta@gmail.com>
Co-authored-by: Denny Schwender <denny.schwender@gmail.com>
Co-authored-by: Salizan <sohrab.sy1@gmail.com>
Co-authored-by: Jacob <jacob.venborg@gmail.com>
Co-authored-by: CyanWolf <hydemr@pm.me>
Co-authored-by: Mostafa Kian <mostafakian77@gmail.com>
Co-authored-by: Tomáš Pernekr <leostreamer@gmail.com>
Co-authored-by: Zane Schepke <zanecschepke@gmail.com>
2025-12-26 00:38:26 -05:00
zaneschepke eeeec5613f fix: fdroid check update, in app messages bug 2025-12-26 00:31:13 -05:00
zaneschepke af21a6a3cf refactor: remove redundant dispatchers 2025-12-25 22:57:21 -05:00
Zane Schepke 0bf52ad378 fix: koin refactor param bug 2025-12-25 12:47:25 -05:00
Zane Schepke 0cf39fed68 fix: multiple profiles endpoint updates and lockdown bug
closes #962
2025-12-25 03:30:13 -05:00
Zane Schepke 590985d5cd refactor: make clear kernel is wg only
closes #1103
2025-12-25 02:05:21 -05:00
Zane Schepke c16a1b9b55 fix: pop backstack crash in certain scenarios 2025-12-25 01:18:06 -05:00
Zane Schepke 679f6abbcb fix: race after recent tunnel manager refactor
Optimize restore logic
2025-12-25 00:52:04 -05:00
Zane Schepke bbc62a26e7 refactor: ui state optimizations 2025-12-25 00:06:53 -05:00
Zane Schepke e475fd27d9 fix: race in vpn activation after amnezia 2.0 changes
Refactor to tunnel jobs handlers to make them more modular and efficient.

closes #1113
2025-12-24 02:59:14 -05:00
Zane Schepke e2dd27e70c refactor: dagger/hilt to koin for kmp 2025-12-22 12:47:57 -05:00
Zane Schepke a994e8e2c1 chore: bump agp 2025-12-19 13:52:04 -05:00
Zane Schepke 16d0642a51 chore: release 4.2.1 2025-12-19 11:35:43 -05:00
Zane Schepke eac674c996 fix: auto-tunnel screen not loading without wifi
Fixes auto tunnel screen failing to load if you haven't connected to wifi once.

Fixes import via url.

Closes #1108
Closes #1105
2025-12-19 11:30:39 -05:00
Zane Schepke 394188b55f chore: release 4.2.0 2025-12-19 01:10:11 -05:00
Weblate (bot) d61a4213cf Translations update from Hosted Weblate (#1030)
Co-authored-by: Fill read-only add-on <noreply-addon-fill@weblate.org>
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Qotsa1984 <carlominzi@inwind.it>
Co-authored-by: jaime-grj <weblate.4ljj9@aleeas.com>
Co-authored-by: catelixor <catelixor+weblate@proton.me>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: Prefill add-on <noreply-addon-prefill@weblate.org>
Co-authored-by: Kachelkaiser <kachelkaiser@htpst.de>
Co-authored-by: Henrik Sozzi <henrik_sozzi@hotmail.com>
Co-authored-by: EESF-2 <eesf-2@users.noreply.hosted.weblate.org>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: Aleksandre Ghvineria <Ghvinerias@gmail.com>
Co-authored-by: lateweb <weblate@techkoala.net>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Alvar Kusma <kaabuta@gmail.com>
Co-authored-by: Denny Schwender <denny.schwender@gmail.com>
Co-authored-by: Salizan <sohrab.sy1@gmail.com>
Co-authored-by: Jacob <jacob.venborg@gmail.com>
Co-authored-by: CyanWolf <hydemr@pm.me>
2025-12-18 22:05:31 -05:00
Zane Schepke 90a693abb4 feat: copy split config from existing 2025-12-18 16:28:44 -05:00
Patryk 05b950fbb3 fix: add check if kernel supports WG (#993) 2025-12-18 10:25:31 -05:00
Zane Schepke 5310cf17e6 chore: bump deps 2025-12-17 23:19:55 -05:00
dependabot[bot] 3ffc8c5e39 chore(deps): bump actions/upload-artifact from 5 to 6 (#1096)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 22:50:36 -05:00
dependabot[bot] e538ec80d0 chore(deps): bump actions/download-artifact from 6 to 7 (#1097)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 22:50:12 -05:00
Zane Schepke 6b24cca551 feat: improve amnezia label names
closes #1089
2025-12-15 12:36:50 -05:00
Zane Schepke f0c885708b fix: amnezia 2.0 header ranges 2025-12-14 22:30:54 -05:00
Zane Schepke c028b9b5b6 fix: logger start and clear 2025-12-09 10:45:34 -05:00
Zane Schepke de8b39d848 fix: amnezia 2.0 backwards compat, bump deps 2025-12-08 11:02:08 -05:00
Zane Schepke a573208db6 feat: amnezia 2.0 2025-12-03 00:53:35 -05:00
dependabot[bot] 0e9a63c850 chore(deps): bump actions/checkout from 5 to 6 (#1065)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-25 00:38:39 -05:00
Zane Schepke 341a806f29 chore: bump deps 2025-11-25 00:37:32 -05:00
Zane Schepke 8d257a604c fix: improve quickly state sync, nightly icon
closes #1056
closes #1055
2025-11-24 23:57:13 -05:00
Zane Schepke 0b674684ba fix: redundant location queries in legacy mode
closes #1062
2025-11-18 23:37:02 -05:00
Zane Schepke 85a27f48a2 chore: release v4.1.8 2025-11-14 14:11:22 -05:00
Zane Schepke 1f978cdf96 fix: rapid network changes race in network monitoring 2025-11-14 14:06:59 -05:00
Zane Schepke 4f816fa175 chore: release v4.1.7 2025-11-12 15:12:53 -05:00
Zane Schepke ee4ac4e968 fix: improve qr device support and scanner
#844
closes #1040
2025-11-12 14:13:35 -05:00
Zane Schepke ff53454966 fix: underlying network detection race
#1052
2025-11-12 11:59:20 -05:00
Zane Schepke 22c17ef66b fix: tile update crash when triggerd from non-user profile 2025-11-11 17:43:30 -05:00
Zane Schepke 7a60b90d2b fix: qr scanning scanning can cause crash 2025-11-11 17:29:05 -05:00
Zane Schepke 5fd3f89a59 feat: show tunnel uptime, improve duration display
closes #820
2025-11-11 16:20:08 -05:00
Zane Schepke 9510f43252 fix: global overrides regression, support prompt bug 2025-11-10 20:51:41 -05:00
Zane Schepke 064aa6aa74 fix: error notification bug 2025-11-10 00:56:56 -05:00
Zane Schepke 0c09add0e4 chore: add custom funding link 2025-11-09 12:42:21 -05:00
Zane Schepke fd0fd33f71 chore: release v4.1.6 2025-11-08 20:23:09 -05:00
Zane Schepke aaeb251bbf chore: shorten ur short description 2025-11-08 20:11:48 -05:00
Zane Schepke e563608e49 chore: bump deps 2025-11-08 20:06:09 -05:00
Zane Schepke 584f0386b6 fix: network monitor ignoring valid states for underlying networks 2025-11-08 14:00:14 -05:00
Zane Schepke cf49c34bff ci: simplify publish 2025-11-08 00:43:47 -05:00
Zane Schepke a0f89d40f5 chore: DE short description length too long 2025-11-08 00:17:29 -05:00
Zane Schepke 4da05e23f1 chore: release v4.1.5 2025-11-07 23:58:45 -05:00
Zane Schepke 6749719e21 chore: bump deps, update app description 2025-11-07 23:50:07 -05:00
Zane Schepke 1c160ff5f9 fix: network monitor should ignore default network VPN events
#1038
2025-11-07 21:54:16 -05:00
Zane Schepke 861440b7db fix: disable metered option for Android 9 and lower
closes #1044

#1031
2025-11-07 20:49:32 -05:00
Zane Schepke bdb0d27b53 ci: add aab build workflow 2025-11-05 00:47:46 -05:00
Zane Schepke 9b3283a2b1 chore: release 4.1.4 2025-11-04 20:20:41 -05:00
Zane Schepke 78def29980 fix: keep network monitor for full app lifecyle 2025-11-04 20:16:23 -05:00
Zane Schepke e83bbdf23a fix: tunnel service bind race 2025-11-04 19:59:30 -05:00
Zane Schepke 4beeb4e01e fix: network monitoring bug 2025-11-04 17:48:40 -05:00
Zane Schepke 4bcd810b38 chore: release 4.1.3 2025-11-04 03:57:24 -05:00
Zane Schepke e71174995b fix: tab back navigation bug 2025-11-04 03:39:23 -05:00
Zane Schepke f256a32bda fix: restore proper metered tunnel default
closes #1035
2025-11-04 03:03:24 -05:00
Zane Schepke c49666303a fix: network monitor changes for Android 10 2025-11-04 02:00:58 -05:00
Zane Schepke 3a9b435e50 fix: default wifi method needs flag 2025-11-03 11:52:34 -05:00
Zane Schepke 0993f60977 fix: auto tunnel service binder 2025-11-03 10:55:57 -05:00
Zane Schepke 3d88feb97c fix: r8 ip parsing bug
closes #1031
2025-11-03 09:45:56 -05:00
Zane Schepke f61e6d6c6e fix: network detection bug
closes #1032
2025-11-03 08:20:35 -05:00
Zane Schepke df864ade95 fix: binder leak 2025-11-03 02:24:19 -05:00
Zane Schepke 0abe3f67ef chore: fix fastlane deploy 2025-11-02 03:30:16 -05:00
Zane Schepke 880d30fdfa fix: snackbar and restarts bug 2025-11-02 02:54:03 -05:00
Zane Schepke 765785ff41 chore: release v4.1.2 2025-11-02 02:12:09 -05:00
Zane Schepke 9991e06c44 fix: updater after abi split change 2025-11-02 00:48:13 -04:00
Weblate (bot) 433d383b0b feat(lang): update localization from Hosted Weblate (#1029)
Co-authored-by: CyanWolf <hydemr@pm.me>
Co-authored-by: MouaisTe44 <r.craft.212121@gmail.com>
Co-authored-by: teemue <eemil.koivula@gmail.com>
Co-authored-by: Hamed Ap <hamed.ap1366@gmail.com>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: Qotsa1984 <carlominzi@inwind.it>
Co-authored-by: kometchtech <kometch@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Jan-Pascal van Best <janpascal@vanbest.org>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: Patrik <patrik1305@binternet.eu>
Co-authored-by: mak7im01 <mak7im02@gmail.com>
Co-authored-by: தமிழ்நேரம் <tamilneram247@gmail.com>
Co-authored-by: 翻譯得真好下次別翻了 <x86_64-pc-linux-gnu@proton.me>
Co-authored-by: Prefill add-on <noreply-addon-prefill@weblate.org>
2025-11-02 00:29:50 -04:00
Zane Schepke 041f12dc77 chore: remove orphaned strings 2025-11-01 23:35:20 -04:00
Zane Schepke afdc49629c refactor: snackbar, support prompt on update 2025-11-01 22:16:57 -04:00
Zane Schepke f846d54d78 chore: update screenshots 2025-11-01 02:45:17 -04:00
Zane Schepke ffd9c4192a feat: add translations link 2025-11-01 02:21:40 -04:00
Weblate (bot) d6138b80eb Translations update from Hosted Weblate (#950)
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Kachelkaiser <kachelkaiser@htpst.de>
Co-authored-by: Qotsa1984 <carlominzi@inwind.it>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: catelixor <catelixor+weblate@proton.me>
Co-authored-by: EESF-2 <eesf-2@users.noreply.hosted.weblate.org>
Co-authored-by: Faisal Gull <mail.faisalrehman.345@gmail.com>
Co-authored-by: angrybb <lijadolija@gmail.com>
Co-authored-by: Noureddine <noureddinex@protonmail.com>
Co-authored-by: Patrik <patrik1305@binternet.eu>
Co-authored-by: weblate4ljj9 <weblate.4ljj9@aleeas.com>
Co-authored-by: Zane Schepke <zanecschepke@gmail.com>
2025-11-01 01:58:56 -04:00
Zane Schepke 0874db8bbf refactor: restart modal on config changes for proxy and tuns 2025-11-01 01:24:19 -04:00
Zane Schepke 6d483459a6 refactor: add restart to lockdown on config change 2025-11-01 00:06:40 -04:00
Zane Schepke 7e4f055833 fix: app start kills other vpns bug
closes #1020
2025-10-31 20:57:38 -04:00
Zane Schepke 8ea432b4f6 build: split abis 2025-10-31 02:20:05 -04:00
Zane Schepke 6637539d1f fix: android tv hide kernel mode, ping toggle, import sheet
closes #1008

#1008
2025-10-31 00:34:01 -04:00
Zane Schepke bfe3533030 fix: android tv long press behavior
#1008
2025-10-30 00:53:56 -04:00
Zane Schepke b61d49469f chore: bump ksp 2025-10-29 15:30:03 -04:00
Zane Schepke 349b56b2e2 fix: prevent backup restore issues by resetting active tuns storage 2025-10-29 15:20:08 -04:00
dependabot[bot] 42aa378938 chore(deps): bump actions/download-artifact from 5 to 6 (#1022)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 03:45:19 -04:00
dependabot[bot] 062f59aa33 chore(deps): bump actions/upload-artifact from 4 to 5 (#1021)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 03:45:07 -04:00
Zane Schepke 883b9f7dae refactor: wifi detection, bump deps
#1005
2025-10-29 03:43:50 -04:00
Zane Schepke 1f8d24c704 refactor: show disabled feature by mode 2025-10-29 01:44:25 -04:00
Zane Schepke 68ab3fdc52 fix: split tunnel missing packages
closes #750
2025-10-28 22:36:12 -04:00
Zane Schepke b4b96a7e77 feat!: dual-stack kill switch support, metered tunnels
Adds dual-stack option for kill switch.

Add metered option for kill switch and individual tunnels.

closes #966
closes #962
2025-10-28 21:43:35 -04:00
Zane Schepke 59a70e53ff fix: mtu parser errors
closes #890
2025-10-24 03:20:01 -04:00
Zane Schepke 170b12ab79 fix: endpoints should be optional
closes #808
2025-10-24 02:00:52 -04:00
Zane Schepke 0f365e2ef8 chore: update description, fdroid link 2025-10-23 21:32:39 -04:00
Zane Schepke 950d75b57f ci: fix duplicate notifications 2025-10-23 00:58:31 -04:00
Zane Schepke ea90896061 ci: optimize nightly ci 2025-10-23 00:33:47 -04:00
Zane Schepke c62b328187 fix: kernel tunnel symbols error handling
closes #1017
2025-10-23 00:31:33 -04:00
Zane Schepke 46a962a730 ci: switch to pat, improve notifications to include nightly 2025-10-21 21:09:48 -04:00
Zane Schepke 6631ebcf49 ci: fix notifications 2025-10-21 15:13:39 -04:00
Zane Schepke c28e157616 fix: proxy mode vpn permission 2025-10-21 00:31:34 -04:00
Zane Schepke b2bd8574ec chore: release 4.1.1 2025-10-19 02:45:22 -04:00
Zane Schepke e8146f0b97 fix: peer stats ui bug
closes #1007
2025-10-19 02:41:33 -04:00
Zane Schepke 706513a5b0 fix: auto tunnel start show battery optimization 2025-10-19 02:15:00 -04:00
Zane Schepke a77aa4d92f ci: fix token 2025-10-17 18:35:14 -04:00
884 changed files with 20308 additions and 4862 deletions
-22
View File
@@ -1,22 +0,0 @@
# Contributor Code of Conduct
## Pledge
We as individuals involved in this project, pledge to participate in this
community in a respectful, constructive, and civil manner as we work towards a common goal
of delivering free, open source, and value adding software for all.
## Standard
The standard for this community is the Golden Rule.
> “Do unto others as you would have them do unto you.”
## Scope
This Code of Conduct applies to all spaces related to WG Tunnel.
## Incidents or Concerns
For any incidents or concerns, reach out to Zane at
<support@zaneschepke.com>.
+1
View File
@@ -1,3 +1,4 @@
ko_fi: zaneschepke
liberapay: zaneschepke
github: zaneschepke
custom: ["https://wgtunnel.com/donate/"]
+130
View File
@@ -0,0 +1,130 @@
name: build-aab
permissions:
contents: read
on:
workflow_dispatch:
inputs:
build_type:
type: choice
description: "Build type"
required: true
default: release
options:
- release
flavor:
type: choice
description: "Product flavor"
required: true
default: google
options:
- google
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: release
flavor:
type: string
description: "Product flavor"
required: false
default: google
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@v6
with:
fetch-depth: 0
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '17'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- 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 }}
- 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: Build AAB (noSplits=true)
run: |
flavor=${{ inputs.flavor }}
build_type=${{ inputs.build_type }}
case $build_type in
"release")
./gradlew :app:bundle${flavor^}Release \
-PnoSplits=true \
--info
;;
esac
- name: Get release AAB path
id: aab-path
run: |
AAB_PATH=$(find app/build/outputs/bundle -iname "*google*release*.aab" -type f | head -1)
if [ -z "$AAB_PATH" ]; then
echo "Error: AAB not found!" >&2
exit 1
fi
echo "Found AAB: $AAB_PATH"
echo "path=$AAB_PATH" >> $GITHUB_OUTPUT
- name: Upload AAB Artifact
uses: actions/upload-artifact@v7
with:
name: google-play-aab
path: ${{ steps.aab-path.outputs.path }}
retention-days: 7
if-no-files-found: error
+4 -8
View File
@@ -72,7 +72,7 @@ jobs:
outputs:
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up JDK 17
@@ -114,15 +114,11 @@ jobs:
- name: Get release apk path
id: apk-path
run: echo "path=$(find . -regex '^.*/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
- name: Upload APK
uses: actions/upload-artifact@v4
- name: Upload All APK Artifacts
uses: actions/upload-artifact@v7
with:
name: android_artifacts_${{ inputs.flavor }}
path: >-
app/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/${{
inputs.flavor == 'fdroid' && inputs.build_type == 'release'
&& 'wgtunnel-fdroid-release-*.apk'
|| format('wgtunnel-{0}-v*.apk', inputs.flavor)
}}
app/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/*.apk
retention-days: 1
if-no-files-found: warn
+7 -5
View File
@@ -16,7 +16,7 @@ jobs:
has_new_commits: ${{ steps.check.outputs.new_commits }}
steps:
- name: Checkout Repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Check for new commits
id: check
env:
@@ -26,6 +26,9 @@ jobs:
echo "new_commits=$NEW_COMMITS" >> $GITHUB_OUTPUT
build-standalone-nightly:
needs:
- check_commits
if: ${{ needs.check_commits.outputs.has_new_commits > 0 && inputs.release_type != 'none' }}
uses: ./.github/workflows/build.yml
secrets: inherit
with:
@@ -34,14 +37,13 @@ jobs:
publish:
needs:
- check_commits
- build-standalone-nightly
if: ${{ needs.check_commits.outputs.has_new_commits > 0 && inputs.release_type != 'none' }}
name: publish-nightly
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Install system dependencies
run: |
@@ -69,7 +71,7 @@ jobs:
run: mkdir ${{ github.workspace }}/temp
- name: Download artifacts
uses: actions/download-artifact@v5
uses: actions/download-artifact@v8
with:
pattern: android_artifacts_*
path: ${{ github.workspace }}/temp
@@ -124,4 +126,4 @@ jobs:
files: |
${{ github.workspace }}/temp/**/*.apk
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ secrets.PAT }}
+115 -40
View File
@@ -1,73 +1,148 @@
name: notifications
permissions:
contents: write
packages: write
on:
issues:
types: [opened, closed]
release:
types: [published]
types: [published, prereleased]
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Send to Telegram
- name: Send to Telegram - New Issue
if: github.event_name == 'issues' && github.event.action == 'opened'
env:
TITLE: ${{ github.event.issue.title }}
NUMBER: ${{ github.event.issue.number }}
USER: ${{ github.event.issue.user.login }}
BODY: ${{ github.event.issue.body || 'No body provided' }}
URL: ${{ github.event.issue.html_url }}
run: |
if [[ "${{ github.event_name }}" == "issues" && "${{ github.event.action }}" == "opened" ]]; then
TITLE="${{ github.event.issue.title }}"
NUMBER=${{ github.event.issue.number }}
USER="${{ github.event.issue.user.login }}"
BODY="${{ github.event.issue.body || 'No body provided' }}"
BODY_TRUNC="${BODY:0:200}" # Truncate to avoid spam
URL="${{ github.event.issue.html_url }}"
TEXT=$(echo -e "🆕 New Issue #$NUMBER: *$TITLE* by $USER\n\n$BODY_TRUNC\n\n[View Issue]($URL)")
elif [[ "${{ github.event_name }}" == "issues" && "${{ github.event.action }}" == "closed" ]]; then
TITLE="${{ github.event.issue.title }}"
NUMBER=${{ github.event.issue.number }}
USER="${{ github.event.issue.user.login }}"
URL="${{ github.event.issue.html_url }}"
TEXT=$(echo -e "✅ Issue Closed #$NUMBER: *$TITLE* by $USER\n\n[View Issue]($URL)")
elif [[ "${{ github.event_name }}" == "release" && "${{ github.event.action }}" == "published" ]]; then
NAME="${{ github.event.release.name }}"
TAG="${{ github.event.release.tag_name }}"
BODY="${{ github.event.release.body || 'No notes provided' }}"
BODY_TRUNC="${BODY:0:200}" # Truncate to avoid spam
URL="${{ github.event.release.html_url }}"
TEXT=$(echo -e "🚀 New Release *$NAME* ($TAG)\n\n$BODY_TRUNC\n\n[View Release]($URL)")
fi
BODY_TRUNC="${BODY:0:200}" # Truncate to avoid spam
TEXT=$(echo -e "🆕 New Issue #$NUMBER: *$TITLE* by $USER\n\n$BODY_TRUNC\n\n[View Issue]($URL)")
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage" \
-d chat_id="${{ vars.TELEGRAM_CHAT_ID }}" \
${{ vars.TELEGRAM_THREAD_ID && format('-d message_thread_id="{0}"', vars.TELEGRAM_THREAD_ID) || '' }} \
-d parse_mode="Markdown" \
--data-urlencode "text=$TEXT"
- name: Send to Matrix
- name: Send to Telegram - Closed Issue
if: github.event_name == 'issues' && github.event.action == 'closed'
env:
TITLE: ${{ github.event.issue.title }}
NUMBER: ${{ github.event.issue.number }}
USER: ${{ github.event.issue.user.login }}
URL: ${{ github.event.issue.html_url }}
run: |
PLAIN_MESSAGE=""
HTML_MESSAGE=""
if [[ "${{ github.event_name }}" == "issues" && "${{ github.event.action }}" == "opened" ]]; then
PLAIN_MESSAGE=$(echo -e "🆕 New Issue #${{ github.event.issue.number }}: ${{ github.event.issue.title }} by ${{ github.event.issue.user.login }}\n\n${{ github.event.issue.body || 'No body provided' }}\n\nView Issue: ${{ github.event.issue.html_url }}")
HTML_MESSAGE=$(echo -e "<p>🆕 New Issue #${{ github.event.issue.number }}: <strong>${{ github.event.issue.title }}</strong> by ${{ github.event.issue.user.login }}</p><p>${{ github.event.issue.body || 'No body provided' }}</p><p><a href=\"${{ github.event.issue.html_url }}\">View Issue</a></p>")
elif [[ "${{ github.event_name }}" == "issues" && "${{ github.event.action }}" == "closed" ]]; then
PLAIN_MESSAGE=$(echo -e "✅ Issue Closed #${{ github.event.issue.number }}: ${{ github.event.issue.title }} by ${{ github.event.issue.user.login }}\n\nView Issue: ${{ github.event.issue.html_url }}")
HTML_MESSAGE=$(echo -e "<p>✅ Issue Closed #${{ github.event.issue.number }}: <strong>${{ github.event.issue.title }}</strong> by ${{ github.event.issue.user.login }}</p><p><a href=\"${{ github.event.issue.html_url }}\">View Issue</a></p>")
elif [[ "${{ github.event_name }}" == "release" && "${{ github.event.action }}" == "published" ]]; then
PLAIN_MESSAGE=$(echo -e "🚀 New Release ${{ github.event.release.name }} (${{ github.event.release.tag_name }})\n\n${{ github.event.release.body || 'No notes provided' }}\n\nView Release: ${{ github.event.release.html_url }}")
HTML_MESSAGE=$(echo -e "<p>🚀 New Release <strong>${{ github.event.release.name }}</strong> (${{ github.event.release.tag_name }})</p><p>${{ github.event.release.body || 'No notes provided' }}</p><p><a href=\"${{ github.event.release.html_url }}\">View Release</a></p>")
TEXT=$(echo -e "✅ Issue Closed #$NUMBER: *$TITLE* by $USER\n\n[View Issue]($URL)")
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage" \
-d chat_id="${{ vars.TELEGRAM_CHAT_ID }}" \
${{ vars.TELEGRAM_THREAD_ID && format('-d message_thread_id="{0}"', vars.TELEGRAM_THREAD_ID) || '' }} \
-d parse_mode="Markdown" \
--data-urlencode "text=$TEXT"
- name: Send to Telegram - New Release
if: github.event_name == 'release' && ((github.event.action == 'published' && !github.event.release.prerelease) || (github.event.action == 'prereleased' && github.event.release.prerelease && github.event.release.name == 'nightly'))
env:
NAME: ${{ github.event.release.name }}
TAG: ${{ github.event.release.tag_name }}
BODY: ${{ github.event.release.body || 'No notes provided' }}
URL: ${{ github.event.release.html_url }}
ACTION: ${{ github.event.action }}
run: |
BODY_TRUNC="${BODY:0:200}" # Truncate to avoid spam
if [ "$ACTION" == "prereleased" ]; then
ICON="🌙"
PREFIX="New Nightly Release"
else
ICON="🚀"
PREFIX="New Release"
fi
TEXT=$(echo -e "$ICON $PREFIX *$NAME* ($TAG)\n\n$BODY_TRUNC\n\n[View Release]($URL)")
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage" \
-d chat_id="${{ vars.TELEGRAM_CHAT_ID }}" \
${{ vars.TELEGRAM_THREAD_ID && format('-d message_thread_id="{0}"', vars.TELEGRAM_THREAD_ID) || '' }} \
-d parse_mode="Markdown" \
--data-urlencode "text=$TEXT"
- name: Send to Matrix - New Issue
if: github.event_name == 'issues' && github.event.action == 'opened'
env:
NUMBER: ${{ github.event.issue.number }}
TITLE: ${{ github.event.issue.title }}
USER: ${{ github.event.issue.user.login }}
BODY: ${{ github.event.issue.body || 'No body provided' }}
URL: ${{ github.event.issue.html_url }}
run: |
PLAIN_MESSAGE=$(echo -e "🆕 New Issue #$NUMBER: $TITLE by $USER\n\n$BODY\n\nView Issue: $URL")
HTML_MESSAGE=$(echo -e "<p>🆕 New Issue #$NUMBER: <strong>$TITLE</strong> by $USER</p><p>$BODY</p><p><a href=\"$URL\">View Issue</a></p>")
PLAIN_MESSAGE="${PLAIN_MESSAGE:0:220}"
PAYLOAD=$(jq -n --arg body "$PLAIN_MESSAGE" --arg formatted "$HTML_MESSAGE" '{
"msgtype": "m.text",
"body": $body,
"format": "org.matrix.custom.html",
"formatted_body": $formatted
}')
TXN_ID="${{ github.run_id }}-${{ github.run_attempt }}"
curl -s -X PUT "https://${{ vars.MATRIX_HOMESERVER }}/_matrix/client/v3/rooms/${{ vars.MATRIX_ROOM_ID }}/send/m.room.message/$TXN_ID" \
-H "Authorization: Bearer ${{ secrets.MATRIX_ACCESS_TOKEN }}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD"
- name: Send to Matrix - Closed Issue
if: github.event_name == 'issues' && github.event.action == 'closed'
env:
NUMBER: ${{ github.event.issue.number }}
TITLE: ${{ github.event.issue.title }}
USER: ${{ github.event.issue.user.login }}
URL: ${{ github.event.issue.html_url }}
run: |
PLAIN_MESSAGE=$(echo -e "✅ Issue Closed #$NUMBER: $TITLE by $USER\n\nView Issue: $URL")
HTML_MESSAGE=$(echo -e "<p>✅ Issue Closed #$NUMBER: <strong>$TITLE</strong> by $USER</p><p><a href=\"$URL\">View Issue</a></p>")
PLAIN_MESSAGE="${PLAIN_MESSAGE:0:220}"
PAYLOAD=$(jq -n --arg body "$PLAIN_MESSAGE" --arg formatted "$HTML_MESSAGE" '{
"msgtype": "m.text",
"body": $body,
"format": "org.matrix.custom.html",
"formatted_body": $formatted
}')
TXN_ID="${{ github.run_id }}-${{ github.run_attempt }}"
curl -s -X PUT "https://${{ vars.MATRIX_HOMESERVER }}/_matrix/client/v3/rooms/${{ vars.MATRIX_ROOM_ID }}/send/m.room.message/$TXN_ID" \
-H "Authorization: Bearer ${{ secrets.MATRIX_ACCESS_TOKEN }}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD"
- name: Send to Matrix - New Release
if: github.event_name == 'release' && ((github.event.action == 'published' && !github.event.release.prerelease) || (github.event.action == 'prereleased' && github.event.release.prerelease && github.event.release.name == 'nightly'))
env:
NAME: ${{ github.event.release.name }}
TAG: ${{ github.event.release.tag_name }}
BODY: ${{ github.event.release.body || 'No notes provided' }}
URL: ${{ github.event.release.html_url }}
ACTION: ${{ github.event.action }}
run: |
if [ "$ACTION" == "prereleased" ]; then
ICON="🌙"
PREFIX="New Nightly Release"
else
ICON="🚀"
PREFIX="New Release"
fi
PLAIN_MESSAGE=$(echo -e "$ICON $PREFIX $NAME ($TAG)\n\n$BODY\n\nView Release: $URL")
HTML_MESSAGE=$(echo -e "<p>$ICON $PREFIX <strong>$NAME</strong> ($TAG)</p><p>$BODY</p><p><a href=\"$URL\">View Release</a></p>")
PLAIN_MESSAGE="${PLAIN_MESSAGE:0:220}"
PAYLOAD=$(jq -n --arg body "$PLAIN_MESSAGE" --arg formatted "$HTML_MESSAGE" '{
"msgtype": "m.text",
"body": $body,
"format": "org.matrix.custom.html",
"formatted_body": $formatted
}')
TXN_ID="${{ github.run_id }}-${{ github.run_attempt }}"
curl -s -X PUT "https://${{ vars.MATRIX_HOMESERVER }}/_matrix/client/v3/rooms/${{ vars.MATRIX_ROOM_ID }}/send/m.room.message/$TXN_ID" \
-H "Authorization: Bearer ${{ secrets.MATRIX_ACCESS_TOKEN }}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD"
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
format_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
+31 -21
View File
@@ -32,14 +32,6 @@ on:
description: "Tag name for release"
required: false
default: 1.1.1
flavor:
type: choice
description: "Product flavor"
required: true
default: standalone
options:
- fdroid
- standalone
workflow_call:
inputs:
flavor:
@@ -51,7 +43,11 @@ on:
jobs:
build-fdroid:
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' || inputs.flavor == 'fdroid' }}
if: >-
${{
github.event_name == 'push' ||
inputs.release_type != 'none'
}}
uses: ./.github/workflows/build.yml
secrets: inherit
with:
@@ -59,20 +55,30 @@ jobs:
flavor: fdroid
build-standalone:
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' || inputs.release_type == 'debug' || inputs.flavor == 'standalone' }}
if: >-
${{
github.event_name == 'push' ||
inputs.release_type != 'none'
}}
uses: ./.github/workflows/build.yml
secrets: inherit
with:
build_type: ${{ github.event_name == 'push' && 'release' || inputs.release_type }}
flavor: standalone
publish:
publish-github:
if: >-
${{
github.event_name == 'push' ||
inputs.release_type != 'none'
}}
needs:
- build-standalone
- build-fdroid
- build-standalone
name: publish-github
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
- name: Install system dependencies
@@ -93,7 +99,7 @@ jobs:
id: latest_release
uses: kaliber5/action-get-release@v1
with:
token: ${{ secrets.PAT }}
token: ${{ github.token }}
latest: true
- name: Generate Changelog
@@ -109,7 +115,7 @@ jobs:
run: mkdir ${{ github.workspace }}/temp
- name: Download artifacts
uses: actions/download-artifact@v5
uses: actions/download-artifact@v8
with:
pattern: android_artifacts_*
path: ${{ github.workspace }}/temp
@@ -118,8 +124,8 @@ jobs:
- name: Set version release notes
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' }}
run: |
VERSION_NAME=$(grep "const val VERSION_NAME" buildSrc/src/main/kotlin/Constants.kt | awk -F'"' '{print $2}')
RELEASE_NOTES="$(cat ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${VERSION_NAME}.txt || echo "No changelog found for ${VERSION_NAME}")"
VERSION_CODE=$(sed -nE 's/.*const val VERSION_CODE[[:space:]]*=[[:space:]]*([0-9]+).*/\1/p' buildSrc/src/main/kotlin/Constants.kt)
RELEASE_NOTES="$(cat ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${VERSION_CODE}.txt || echo "No changelog found for ${VERSION_CODE}")"
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
echo "$RELEASE_NOTES" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
@@ -162,13 +168,17 @@ jobs:
files: |
${{ github.workspace }}/temp/**/*.apk
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ secrets.PAT }}
publish-fdroid-public:
runs-on: ubuntu-latest
if: >-
${{
github.event_name == 'push' ||
inputs.release_type != 'none'
}}
needs:
- build-fdroid
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' }}
- publish-github
steps:
- name: Dispatch update for fdroid repo
uses: peter-evans/repository-dispatch@v4
@@ -190,7 +200,7 @@ jobs:
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
+5 -8
View File
@@ -21,8 +21,7 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
<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/)
[![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)
[![F-Droid](https://img.shields.io/static/v1?style=for-the-badge&message=F-Droid&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>
@@ -60,14 +59,12 @@ WG Tunnel is an alternative Android client for WireGuard and AmneziaWG, inspired
</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" />
<img label="Main" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" alt="Main"/>
<img label="Config" src="fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png" width="200" alt="Config"/>
<img label="Settings" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" alt="Settings"/>
<img label="Auto-tunnel" src="fastlane/metadata/android/en-US/images/phoneScreenshots/auto_screen.png" width="200" alt="Auto-tunnel"/>
</div>
<div style="text-align: left;">
## Features
- **Tunnel Import Methods**: Easily add tunnels using .conf files, ZIP archives, manual entry, or QR code scanning.
+50 -18
View File
@@ -1,9 +1,9 @@
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.hilt.android)
alias(libs.plugins.kotlinxSerialization)
alias(libs.plugins.ksp)
alias(libs.plugins.compose.compiler)
@@ -27,6 +27,15 @@ android {
// fix okhttp proguard issue
packaging { resources { pickFirsts.add("okhttp3/internal/publicsuffix/publicsuffixes.gz") } }
splits {
abi {
isEnable = !project.hasProperty("noSplits")
reset()
include("armeabi-v7a", "arm64-v8a")
isUniversalApk = !project.hasProperty("noSplits")
}
}
defaultConfig {
applicationId = Constants.APP_ID
minSdk = Constants.MIN_SDK
@@ -126,21 +135,37 @@ android {
licensee {
allowedLicenses().forEach { allow(it) }
allowedLicenseUrls().forEach { allowUrl(it) }
// foss, but missing license
ignoreDependencies("com.github.T8RIN.QuickieExtended")
}
applicationVariants.all {
android.applicationVariants.all {
val variant = this
variant.outputs
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
.forEach { output ->
val outputFileName =
if (variant.flavorName == "fdroid" && variant.buildType.name == "release") {
"${Constants.APP_NAME}-fdroid-release-${variant.versionName}.apk"
} else {
"${Constants.APP_NAME}-${variant.flavorName}-v${variant.versionName}.apk"
}
output.outputFileName = outputFileName
}
val abiNameMap =
mapOf(
"armeabi-v7a" to "armv7",
"arm64-v8a" to "arm64",
"x86" to "x86",
"x86_64" to "x64",
)
variant.outputs.all {
val output = this as BaseVariantOutputImpl
val abi = output.getFilter("ABI")
val baseFileName = "${Constants.APP_NAME}-${variant.flavorName}-v${variant.versionName}"
val outputFileName =
if (!abi.isNullOrEmpty()) {
val shortAbiName = abiNameMap.getOrDefault(abi, abi)
"${baseFileName}-${shortAbiName}.apk"
} else {
"${baseFileName}.apk"
}
output.outputFileName = outputFileName
}
}
}
@@ -163,7 +188,6 @@ dependencies {
// Navigation
implementation(libs.bundles.androidx.navigation3)
implementation(libs.bundles.navigation.lifecycle)
implementation(libs.bundles.androidx.hilt)
// Material and icons
implementation(libs.bundles.google.material)
@@ -174,11 +198,7 @@ dependencies {
implementation(libs.bundles.androidx.datastore)
ksp(libs.androidx.room.compiler)
// DI and work
implementation(libs.bundles.hilt.android)
implementation(libs.bundles.androidx.work)
ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler)
// Networking and serialization
implementation(libs.bundles.ktor.client)
@@ -218,10 +238,22 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.manifest)
debugImplementation(libs.leakcanary.android)
// Room database backup
implementation(libs.roomdatabasebackup) {
exclude(group = "org.reactivestreams", module = "reactive-streams")
}
// DI
implementation(platform(libs.koin.bom))
implementation(libs.koin.core)
implementation(libs.koin.android)
implementation(libs.koin.compose.viewmodel)
implementation(libs.koin.androidx.compose)
implementation(libs.koin.androidx.navigation)
implementation(libs.koin.lazy)
implementation(libs.koin.worker)
}
tasks.register<Copy>("copyLicenseeJsonToAssets") {
@@ -0,0 +1,509 @@
{
"formatVersion": 1,
"database": {
"version": 26,
"identityHash": "a420594a08fff58ecda3e0424fb43e47",
"entities": [
{
"tableName": "tunnel_config",
"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, `restart_on_ping_failure` INTEGER NOT NULL DEFAULT false, `ping_target` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `is_ipv4_preferred` INTEGER NOT NULL DEFAULT true, `position` INTEGER NOT NULL DEFAULT 0, `auto_tunnel_apps` TEXT NOT NULL DEFAULT '[]')",
"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": "restartOnPingFailure",
"columnName": "restart_on_ping_failure",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingTarget",
"columnName": "ping_target",
"affinity": "TEXT",
"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"
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "autoTunnelApps",
"columnName": "auto_tunnel_apps",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'[]'"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_tunnel_config_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tunnel_config_name` ON `${TABLE_NAME}` (`name`)"
}
]
},
{
"tableName": "proxy_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `socks5_proxy_enabled` INTEGER NOT NULL DEFAULT 0, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT 0, `http_proxy_bind_address` TEXT, `proxy_username` TEXT, `proxy_password` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "socks5ProxyEnabled",
"columnName": "socks5_proxy_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "socks5ProxyBindAddress",
"columnName": "socks5_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "httpProxyEnabled",
"columnName": "http_proxy_enable",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "httpProxyBindAddress",
"columnName": "http_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "proxyUsername",
"columnName": "proxy_username",
"affinity": "TEXT"
},
{
"fieldPath": "proxyPassword",
"columnName": "proxy_password",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "general_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_globals_enabled` INTEGER NOT NULL DEFAULT 0, `app_mode` INTEGER NOT NULL DEFAULT 0, `theme` TEXT NOT NULL DEFAULT 'AUTOMATIC', `locale` TEXT, `remote_key` TEXT, `is_remote_control_enabled` INTEGER NOT NULL DEFAULT 0, `is_pin_lock_enabled` INTEGER NOT NULL DEFAULT 0, `is_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `custom_split_packages` TEXT NOT NULL DEFAULT '{}')",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelGlobalsEnabled",
"columnName": "is_tunnel_globals_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "appMode",
"columnName": "app_mode",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "theme",
"columnName": "theme",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'AUTOMATIC'"
},
{
"fieldPath": "locale",
"columnName": "locale",
"affinity": "TEXT"
},
{
"fieldPath": "remoteKey",
"columnName": "remote_key",
"affinity": "TEXT"
},
{
"fieldPath": "isRemoteControlEnabled",
"columnName": "is_remote_control_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isPinLockEnabled",
"columnName": "is_pin_lock_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "customSplitPackages",
"columnName": "custom_split_packages",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'{}'"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "auto_tunnel_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL DEFAULT 0, `trusted_network_ssids` TEXT NOT NULL DEFAULT '', `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `start_on_boot` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "isTunnelOnUnsecureEnabled",
"columnName": "is_tunnel_on_unsecure_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "wifiDetectionMethod",
"columnName": "wifi_detection_method",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "startOnBoot",
"columnName": "start_on_boot",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "monitoring_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_ping_enabled` INTEGER NOT NULL DEFAULT 0, `is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30, `tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3, `tunnel_ping_timeout_sec` INTEGER, `show_detailed_ping_stats` INTEGER NOT NULL DEFAULT 0, `is_local_logs_enabled` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isPingMonitoringEnabled",
"columnName": "is_ping_monitoring_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "tunnelPingIntervalSeconds",
"columnName": "tunnel_ping_interval_sec",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "30"
},
{
"fieldPath": "tunnelPingAttempts",
"columnName": "tunnel_ping_attempts",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "tunnelPingTimeoutSeconds",
"columnName": "tunnel_ping_timeout_sec",
"affinity": "INTEGER"
},
{
"fieldPath": "showDetailedPingStats",
"columnName": "show_detailed_ping_stats",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isLocalLogsEnabled",
"columnName": "is_local_logs_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "dns_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dnsProtocol",
"columnName": "dns_protocol",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsEndpoint",
"columnName": "dns_endpoint",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "lockdown_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bypass_lan` INTEGER NOT NULL DEFAULT 0, `metered` INTEGER NOT NULL DEFAULT 0, `dual_stack` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bypassLan",
"columnName": "bypass_lan",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "metered",
"columnName": "metered",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dualStack",
"columnName": "dual_stack",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
}
],
"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, 'a420594a08fff58ecda3e0424fb43e47')"
]
}
}
@@ -0,0 +1,523 @@
{
"formatVersion": 1,
"database": {
"version": 27,
"identityHash": "98452d8160a1ae66c852ec8cd739e675",
"entities": [
{
"tableName": "tunnel_config",
"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, `restart_on_ping_failure` INTEGER NOT NULL DEFAULT false, `ping_target` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `is_ipv4_preferred` INTEGER NOT NULL DEFAULT true, `position` INTEGER NOT NULL DEFAULT 0, `auto_tunnel_apps` TEXT NOT NULL DEFAULT '[]', `is_metered` 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": "restartOnPingFailure",
"columnName": "restart_on_ping_failure",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingTarget",
"columnName": "ping_target",
"affinity": "TEXT",
"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"
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "autoTunnelApps",
"columnName": "auto_tunnel_apps",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'[]'"
},
{
"fieldPath": "isMetered",
"columnName": "is_metered",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "true"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_tunnel_config_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tunnel_config_name` ON `${TABLE_NAME}` (`name`)"
}
]
},
{
"tableName": "proxy_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `socks5_proxy_enabled` INTEGER NOT NULL DEFAULT 0, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT 0, `http_proxy_bind_address` TEXT, `proxy_username` TEXT, `proxy_password` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "socks5ProxyEnabled",
"columnName": "socks5_proxy_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "socks5ProxyBindAddress",
"columnName": "socks5_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "httpProxyEnabled",
"columnName": "http_proxy_enable",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "httpProxyBindAddress",
"columnName": "http_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "proxyUsername",
"columnName": "proxy_username",
"affinity": "TEXT"
},
{
"fieldPath": "proxyPassword",
"columnName": "proxy_password",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "general_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `global_split_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `app_mode` INTEGER NOT NULL DEFAULT 0, `theme` TEXT NOT NULL DEFAULT 'AUTOMATIC', `locale` TEXT, `remote_key` TEXT, `is_remote_control_enabled` INTEGER NOT NULL DEFAULT 0, `is_pin_lock_enabled` INTEGER NOT NULL DEFAULT 0, `is_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `custom_split_packages` TEXT NOT NULL DEFAULT '{}')",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isGlobalSplitTunnelEnabled",
"columnName": "global_split_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "appMode",
"columnName": "app_mode",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "theme",
"columnName": "theme",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'AUTOMATIC'"
},
{
"fieldPath": "locale",
"columnName": "locale",
"affinity": "TEXT"
},
{
"fieldPath": "remoteKey",
"columnName": "remote_key",
"affinity": "TEXT"
},
{
"fieldPath": "isRemoteControlEnabled",
"columnName": "is_remote_control_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isPinLockEnabled",
"columnName": "is_pin_lock_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "customSplitPackages",
"columnName": "custom_split_packages",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'{}'"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "auto_tunnel_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL DEFAULT 0, `trusted_network_ssids` TEXT NOT NULL DEFAULT '', `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `start_on_boot` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "isTunnelOnUnsecureEnabled",
"columnName": "is_tunnel_on_unsecure_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "wifiDetectionMethod",
"columnName": "wifi_detection_method",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "startOnBoot",
"columnName": "start_on_boot",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "monitoring_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_ping_enabled` INTEGER NOT NULL DEFAULT 0, `is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30, `tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3, `tunnel_ping_timeout_sec` INTEGER, `show_detailed_ping_stats` INTEGER NOT NULL DEFAULT 0, `is_local_logs_enabled` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isPingMonitoringEnabled",
"columnName": "is_ping_monitoring_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "tunnelPingIntervalSeconds",
"columnName": "tunnel_ping_interval_sec",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "30"
},
{
"fieldPath": "tunnelPingAttempts",
"columnName": "tunnel_ping_attempts",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "tunnelPingTimeoutSeconds",
"columnName": "tunnel_ping_timeout_sec",
"affinity": "INTEGER"
},
{
"fieldPath": "showDetailedPingStats",
"columnName": "show_detailed_ping_stats",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isLocalLogsEnabled",
"columnName": "is_local_logs_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "dns_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT, `global_tunnel_dns_enabled` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dnsProtocol",
"columnName": "dns_protocol",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsEndpoint",
"columnName": "dns_endpoint",
"affinity": "TEXT"
},
{
"fieldPath": "isGlobalTunnelDnsEnabled",
"columnName": "global_tunnel_dns_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "lockdown_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bypass_lan` INTEGER NOT NULL DEFAULT 0, `metered` INTEGER NOT NULL DEFAULT 0, `dual_stack` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bypassLan",
"columnName": "bypass_lan",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "metered",
"columnName": "metered",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dualStack",
"columnName": "dual_stack",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
}
],
"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, '98452d8160a1ae66c852ec8cd739e675')"
]
}
}
@@ -0,0 +1,523 @@
{
"formatVersion": 1,
"database": {
"version": 28,
"identityHash": "4792d0cc61a527c69962b5e58463e6da",
"entities": [
{
"tableName": "tunnel_config",
"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, `restart_on_ping_failure` INTEGER NOT NULL DEFAULT false, `ping_target` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `is_ipv4_preferred` INTEGER NOT NULL DEFAULT true, `position` INTEGER NOT NULL DEFAULT 0, `auto_tunnel_apps` TEXT NOT NULL DEFAULT '[]', `is_metered` 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": "restartOnPingFailure",
"columnName": "restart_on_ping_failure",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingTarget",
"columnName": "ping_target",
"affinity": "TEXT",
"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"
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "autoTunnelApps",
"columnName": "auto_tunnel_apps",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'[]'"
},
{
"fieldPath": "isMetered",
"columnName": "is_metered",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "true"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_tunnel_config_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tunnel_config_name` ON `${TABLE_NAME}` (`name`)"
}
]
},
{
"tableName": "proxy_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `socks5_proxy_enabled` INTEGER NOT NULL DEFAULT 0, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT 0, `http_proxy_bind_address` TEXT, `proxy_username` TEXT, `proxy_password` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "socks5ProxyEnabled",
"columnName": "socks5_proxy_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "socks5ProxyBindAddress",
"columnName": "socks5_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "httpProxyEnabled",
"columnName": "http_proxy_enable",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "httpProxyBindAddress",
"columnName": "http_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "proxyUsername",
"columnName": "proxy_username",
"affinity": "TEXT"
},
{
"fieldPath": "proxyPassword",
"columnName": "proxy_password",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "general_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `global_split_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `app_mode` INTEGER NOT NULL DEFAULT 0, `theme` TEXT NOT NULL DEFAULT 'AUTOMATIC', `locale` TEXT, `remote_key` TEXT, `is_remote_control_enabled` INTEGER NOT NULL DEFAULT 0, `is_pin_lock_enabled` INTEGER NOT NULL DEFAULT 0, `is_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `already_donated` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isGlobalSplitTunnelEnabled",
"columnName": "global_split_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "appMode",
"columnName": "app_mode",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "theme",
"columnName": "theme",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'AUTOMATIC'"
},
{
"fieldPath": "locale",
"columnName": "locale",
"affinity": "TEXT"
},
{
"fieldPath": "remoteKey",
"columnName": "remote_key",
"affinity": "TEXT"
},
{
"fieldPath": "isRemoteControlEnabled",
"columnName": "is_remote_control_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isPinLockEnabled",
"columnName": "is_pin_lock_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "alreadyDonated",
"columnName": "already_donated",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "auto_tunnel_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL DEFAULT 0, `trusted_network_ssids` TEXT NOT NULL DEFAULT '', `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `start_on_boot` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "isTunnelOnUnsecureEnabled",
"columnName": "is_tunnel_on_unsecure_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "wifiDetectionMethod",
"columnName": "wifi_detection_method",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "startOnBoot",
"columnName": "start_on_boot",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "monitoring_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_ping_enabled` INTEGER NOT NULL DEFAULT 0, `is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30, `tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3, `tunnel_ping_timeout_sec` INTEGER, `show_detailed_ping_stats` INTEGER NOT NULL DEFAULT 0, `is_local_logs_enabled` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isPingMonitoringEnabled",
"columnName": "is_ping_monitoring_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "tunnelPingIntervalSeconds",
"columnName": "tunnel_ping_interval_sec",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "30"
},
{
"fieldPath": "tunnelPingAttempts",
"columnName": "tunnel_ping_attempts",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "tunnelPingTimeoutSeconds",
"columnName": "tunnel_ping_timeout_sec",
"affinity": "INTEGER"
},
{
"fieldPath": "showDetailedPingStats",
"columnName": "show_detailed_ping_stats",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isLocalLogsEnabled",
"columnName": "is_local_logs_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "dns_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT, `global_tunnel_dns_enabled` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dnsProtocol",
"columnName": "dns_protocol",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsEndpoint",
"columnName": "dns_endpoint",
"affinity": "TEXT"
},
{
"fieldPath": "isGlobalTunnelDnsEnabled",
"columnName": "global_tunnel_dns_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "lockdown_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bypass_lan` INTEGER NOT NULL DEFAULT 0, `metered` INTEGER NOT NULL DEFAULT 0, `dual_stack` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bypassLan",
"columnName": "bypass_lan",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "metered",
"columnName": "metered",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dualStack",
"columnName": "dual_stack",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
}
],
"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, '4792d0cc61a527c69962b5e58463e6da')"
]
}
}
@@ -0,0 +1,523 @@
{
"formatVersion": 1,
"database": {
"version": 29,
"identityHash": "345471c118dee1b7688afa81d835e62c",
"entities": [
{
"tableName": "tunnel_config",
"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, `restart_on_ping_failure` INTEGER NOT NULL DEFAULT false, `ping_target` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `is_ipv4_preferred` INTEGER NOT NULL DEFAULT true, `position` INTEGER NOT NULL DEFAULT 0, `auto_tunnel_apps` TEXT NOT NULL DEFAULT '[]', `is_metered` 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": "restartOnPingFailure",
"columnName": "restart_on_ping_failure",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingTarget",
"columnName": "ping_target",
"affinity": "TEXT",
"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"
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "autoTunnelApps",
"columnName": "auto_tunnel_apps",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'[]'"
},
{
"fieldPath": "isMetered",
"columnName": "is_metered",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_tunnel_config_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tunnel_config_name` ON `${TABLE_NAME}` (`name`)"
}
]
},
{
"tableName": "proxy_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `socks5_proxy_enabled` INTEGER NOT NULL DEFAULT 0, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT 0, `http_proxy_bind_address` TEXT, `proxy_username` TEXT, `proxy_password` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "socks5ProxyEnabled",
"columnName": "socks5_proxy_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "socks5ProxyBindAddress",
"columnName": "socks5_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "httpProxyEnabled",
"columnName": "http_proxy_enable",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "httpProxyBindAddress",
"columnName": "http_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "proxyUsername",
"columnName": "proxy_username",
"affinity": "TEXT"
},
{
"fieldPath": "proxyPassword",
"columnName": "proxy_password",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "general_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `global_split_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `app_mode` INTEGER NOT NULL DEFAULT 0, `theme` TEXT NOT NULL DEFAULT 'AUTOMATIC', `locale` TEXT, `remote_key` TEXT, `is_remote_control_enabled` INTEGER NOT NULL DEFAULT 0, `is_pin_lock_enabled` INTEGER NOT NULL DEFAULT 0, `is_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `already_donated` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isGlobalSplitTunnelEnabled",
"columnName": "global_split_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "appMode",
"columnName": "app_mode",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "theme",
"columnName": "theme",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'AUTOMATIC'"
},
{
"fieldPath": "locale",
"columnName": "locale",
"affinity": "TEXT"
},
{
"fieldPath": "remoteKey",
"columnName": "remote_key",
"affinity": "TEXT"
},
{
"fieldPath": "isRemoteControlEnabled",
"columnName": "is_remote_control_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isPinLockEnabled",
"columnName": "is_pin_lock_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "alreadyDonated",
"columnName": "already_donated",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "auto_tunnel_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL DEFAULT 0, `trusted_network_ssids` TEXT NOT NULL DEFAULT '', `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `start_on_boot` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "isTunnelOnUnsecureEnabled",
"columnName": "is_tunnel_on_unsecure_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "wifiDetectionMethod",
"columnName": "wifi_detection_method",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "startOnBoot",
"columnName": "start_on_boot",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "monitoring_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_ping_enabled` INTEGER NOT NULL DEFAULT 0, `is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30, `tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3, `tunnel_ping_timeout_sec` INTEGER, `show_detailed_ping_stats` INTEGER NOT NULL DEFAULT 0, `is_local_logs_enabled` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isPingMonitoringEnabled",
"columnName": "is_ping_monitoring_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "tunnelPingIntervalSeconds",
"columnName": "tunnel_ping_interval_sec",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "30"
},
{
"fieldPath": "tunnelPingAttempts",
"columnName": "tunnel_ping_attempts",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "tunnelPingTimeoutSeconds",
"columnName": "tunnel_ping_timeout_sec",
"affinity": "INTEGER"
},
{
"fieldPath": "showDetailedPingStats",
"columnName": "show_detailed_ping_stats",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isLocalLogsEnabled",
"columnName": "is_local_logs_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "dns_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT, `global_tunnel_dns_enabled` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dnsProtocol",
"columnName": "dns_protocol",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsEndpoint",
"columnName": "dns_endpoint",
"affinity": "TEXT"
},
{
"fieldPath": "isGlobalTunnelDnsEnabled",
"columnName": "global_tunnel_dns_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "lockdown_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bypass_lan` INTEGER NOT NULL DEFAULT 0, `metered` INTEGER NOT NULL DEFAULT 0, `dual_stack` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bypassLan",
"columnName": "bypass_lan",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "metered",
"columnName": "metered",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dualStack",
"columnName": "dual_stack",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
}
],
"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, '345471c118dee1b7688afa81d835e62c')"
]
}
}
@@ -4,10 +4,10 @@ import androidx.room.testing.MigrationTestHelper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import java.io.IOException
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.io.IOException
@RunWith(AndroidJUnit4::class)
class MigrationTest {
+52 -22
View File
@@ -5,6 +5,9 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!--for split tunneling-->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<!--foreground service special use for non VPN service tunnels, android 14-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<!--foreground service special use for VPN service tunnels, android 14-->
@@ -14,6 +17,7 @@
<!--foreground service permissions-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!--start service on boot permission-->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
@@ -42,17 +46,6 @@
<uses-feature android:name="android.hardware.wifi"
android:required="false"/>
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent>
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent>
</queries>
<application
android:name=".WireGuardAutoTunnel"
android:allowBackup="false"
@@ -66,10 +59,7 @@
android:supportsRtl="true"
android:theme="@style/Theme.App.Start"
tools:targetApi="tiramisu">
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="portrait"
tools:replace="screenOrientation" />
<activity
android:name=".MainActivity"
android:exported="true"
@@ -113,13 +103,50 @@
android:resource="@xml/file_paths" />
</provider>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:multiprocess="true"
tools:node="remove">
</provider>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
<service
android:name=".core.service.tile.TunnelControlTile"
android:exported="true"
android:icon="@drawable/ic_notification"
android:label="@string/tunnel_control"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data
android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
<meta-data
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".core.service.tile.AutoTunnelControlTile"
android:exported="true"
android:icon="@drawable/ic_notification"
android:label="@string/auto_tunnel"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data
android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
<meta-data
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".core.service.tile.TunnelControlTile"
android:exported="true"
@@ -205,7 +232,10 @@
<action android:name="android.intent.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>
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<receiver
@@ -11,41 +11,69 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.*
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withLink
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.banner.AppAlertBanner
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarInfo
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.rememberCustomSnackbarState
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.navigation.Tab
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.BottomNavbar
@@ -64,8 +92,8 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.Appear
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.settings.dns.DnsSettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.globals.TunnelGlobalsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.integrations.AndroidIntegrationsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.lockdown.LockdownSettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.TunnelMonitoringScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.ping.PingTargetScreen
@@ -75,34 +103,39 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.crypto.Addr
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.LicenseScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.TunnelsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.TunnelSettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.sort.SortScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.SplitTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.tunneloptions.TunnelOptionsScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.AlertRed
import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.util.extensions.*
import com.zaneschepke.wireguardautotunnel.util.extensions.installApk
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.restartApp
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
import dagger.hilt.android.AndroidEntryPoint
import de.raphaelebner.roomdatabasebackup.core.RoomBackup
import java.util.*
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.androidx.compose.koinViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import xyz.teamgravity.pin_lock_compose.PinManager
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var appStateRepository: AppStateRepository
@Inject lateinit var tunnelManager: TunnelManager
@Inject lateinit var networkMonitor: NetworkMonitor
@Inject lateinit var appDatabase: AppDatabase
private val appStateRepository: AppStateRepository by inject()
private val tunnelRepository: TunnelRepository by inject()
private val appDatabase: AppDatabase by inject()
private val networkMonitor: NetworkMonitor by inject()
val viewModel by viewModel<SharedAppViewModel>()
private lateinit var roomBackup: RoomBackup
@OptIn(ExperimentalMaterial3Api::class)
@@ -118,8 +151,6 @@ class MainActivity : AppCompatActivity() {
roomBackup = RoomBackup(this)
val viewModel by viewModels<SharedAppViewModel>()
installSplashScreen().apply {
setKeepOnScreenCondition { !viewModel.container.stateFlow.value.isAppLoaded }
}
@@ -127,16 +158,16 @@ class MainActivity : AppCompatActivity() {
setContent {
val context = LocalContext.current
val isTv = isRunningOnTv()
val appState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope()
LaunchedEffect(appState.isAppLoaded) {
if (appState.isAppLoaded) {
appState.locale.let { LocaleUtil.changeLocale(it) }
LaunchedEffect(uiState.isAppLoaded) {
if (uiState.isAppLoaded) {
uiState.locale.let { LocaleUtil.changeLocale(it) }
}
}
val snackbar = remember { SnackbarHostState() }
val snackbarState = rememberCustomSnackbarState()
var showVpnPermissionDialog by remember { mutableStateOf(false) }
var vpnPermissionDenied by remember { mutableStateOf(false) }
var requestingAppMode by remember {
@@ -146,17 +177,19 @@ class MainActivity : AppCompatActivity() {
val startingStack = buildList {
add(Route.Tunnels)
if (intent?.action == Intent.ACTION_APPLICATION_PREFERENCES) add(Route.Settings)
if (appState.pinLockEnabled) add(Route.Lock)
if (uiState.pinLockEnabled) add(Route.Lock)
}
val backStack = rememberNavBackStack(*startingStack.toTypedArray())
var previousRoute by remember { mutableStateOf<Route?>(null) }
val navController =
rememberNavController<NavKey>(backStack, appState.isLocationDisclosureShown) {
previousKey ->
previousRoute = previousKey as? Route
}
rememberNavController(
backStack,
uiState.isLocationDisclosureShown,
onChange = { previousKey -> previousRoute = previousKey as? Route },
onExitApp = { finish() },
)
val vpnActivity =
rememberLauncherForActivityResult(
@@ -189,10 +222,20 @@ class MainActivity : AppCompatActivity() {
vpnActivity.launch(VpnService.prepare(this@MainActivity))
}
is GlobalSideEffect.Snackbar ->
is GlobalSideEffect.Snackbar -> {
scope.launch {
snackbar.showSnackbar(sideEffect.message.asString(context))
snackbarState.showSnackbar(
SnackbarInfo(
message =
buildAnnotatedString {
append(sideEffect.message.asString(context))
},
type = sideEffect.type ?: SnackbarType.INFO,
durationMs = sideEffect.durationMs ?: 4000L,
)
)
}
}
is GlobalSideEffect.Toast ->
scope.launch { context.showToast(sideEffect.message.asString(context)) }
@@ -203,19 +246,18 @@ class MainActivity : AppCompatActivity() {
}
}
if (!appState.isAppLoaded) return@setContent
if (!uiState.isAppLoaded) return@setContent
var showLock by remember {
mutableStateOf(appState.pinLockEnabled && !appState.isPinVerified)
mutableStateOf(uiState.pinLockEnabled && !uiState.isPinVerified)
}
LaunchedEffect(appState.isPinVerified) { if (appState.isPinVerified) showLock = false }
LaunchedEffect(uiState.isPinVerified) { if (uiState.isPinVerified) showLock = false }
CompositionLocalProvider(
LocalIsAndroidTV provides isTv,
LocalSharedVm provides viewModel,
LocalNavController provides navController,
) {
WireguardAutoTunnelTheme(theme = appState.theme) {
WireguardAutoTunnelTheme(theme = uiState.theme) {
VpnDeniedDialog(
showVpnPermissionDialog,
onDismiss = {
@@ -224,6 +266,53 @@ class MainActivity : AppCompatActivity() {
},
)
val annotatedMessage = buildAnnotatedString {
append(context.getString(R.string.donation_prompt_prefix))
append(" ")
withLink(
LinkAnnotation.Clickable(
tag = context.getString(R.string.support),
styles =
TextLinkStyles(
style =
SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colorScheme.primary,
),
focusedStyle =
SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colorScheme.primary,
background =
MaterialTheme.colorScheme.primary.copy(
alpha = 0.2f
),
),
),
) {
snackbarState.dismissCurrent()
navController.push(Route.Donate)
}
) {
append(context.getString(R.string.donation_prompt_link))
}
append(" ")
append(context.getString(R.string.donation_prompt_suffix))
}
LaunchedEffect(Unit) {
if (uiState.shouldShowDonationSnackbar && !uiState.alreadyDonated) {
viewModel.setShouldShowDonationSnackbar(false)
snackbarState.showSnackbar(
SnackbarInfo(
message = annotatedMessage,
type = SnackbarType.THANK_YOU,
durationMs = 30_000L,
)
)
}
}
if (showLock) {
PinManager.initialize(context = this@MainActivity)
PinLockScreen()
@@ -236,17 +325,17 @@ class MainActivity : AppCompatActivity() {
}
val navState by
currentRouteAsNavbarState(
appState,
uiState,
viewModel,
currentRoute,
navController,
)
Box(modifier = Modifier.fillMaxSize()) {
if (appState.settings.appMode == AppMode.LOCK_DOWN) {
if (uiState.appMode == AppMode.LOCK_DOWN) {
AppAlertBanner(
stringResource(R.string.locked_down)
.uppercase(Locale.getDefault()),
.uppercase(Locale.current.platformLocale),
OffWhite,
AlertRed,
modifier = Modifier.fillMaxWidth().zIndex(2f),
@@ -254,14 +343,25 @@ class MainActivity : AppCompatActivity() {
}
Scaffold(
snackbarHost = {
SnackbarHost(snackbar) { snackbarData ->
snackbarState.SnackbarHost(
modifier =
Modifier.align(Alignment.BottomCenter)
.padding(
bottom =
if (LocalIsAndroidTV.current) 120.dp
else 80.dp
)
) { info ->
CustomSnackBar(
snackbarData.visuals.message,
isRtl = false,
message = info.message,
type = info.type,
onDismiss = { snackbarState.dismissCurrent() },
containerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation(
2.dp
),
modifier =
Modifier.wrapContentHeight(align = Alignment.Top),
)
}
},
@@ -269,7 +369,7 @@ class MainActivity : AppCompatActivity() {
bottomBar = {
if (navState.showBottomItems) {
BottomNavbar(
appState.isAutoTunnelActive,
uiState.isAutoTunnelActive,
currentTab,
onTabSelected = { tab ->
navController.popUpTo(tab.startRoute)
@@ -331,39 +431,24 @@ class MainActivity : AppCompatActivity() {
}
entry<Route.Tunnels> { TunnelsScreen() }
entry<Route.Sort> { SortScreen() }
entry<Route.TunnelOptions> { key ->
val viewModel =
hiltViewModel<
TunnelViewModel,
TunnelViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(key.id)
}
entry<Route.TunnelSettings> { key ->
val viewModel: TunnelViewModel =
koinViewModel(
parameters = { parametersOf(key.id) }
)
TunnelOptionsScreen(viewModel)
TunnelSettingsScreen(viewModel)
}
entry<Route.SplitTunnel> { key ->
val viewModel =
hiltViewModel<
SplitTunnelViewModel,
SplitTunnelViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(key.id)
}
val viewModel: SplitTunnelViewModel =
koinViewModel(
parameters = { parametersOf(key.id) }
)
SplitTunnelScreen(viewModel)
}
entry<Route.Config> { key ->
val viewModel =
hiltViewModel<
ConfigViewModel,
ConfigViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(key.id)
}
val viewModel: ConfigViewModel =
koinViewModel(
parameters = { parametersOf(key.id) }
)
ConfigScreen(viewModel)
}
@@ -388,33 +473,23 @@ class MainActivity : AppCompatActivity() {
AndroidIntegrationsScreen()
}
entry<Route.Dns> { DnsSettingsScreen() }
entry<Route.TunnelGlobals> { key ->
TunnelGlobalsScreen(key.id)
}
entry<Route.ConfigGlobal> { key ->
val viewModel =
hiltViewModel<
ConfigViewModel,
ConfigViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(key.id)
}
val viewModel: ConfigViewModel =
koinViewModel(
parameters = { parametersOf(key.id) }
)
ConfigScreen(viewModel)
}
entry<Route.SplitTunnelGlobal> { key ->
val viewModel =
hiltViewModel<
SplitTunnelViewModel,
SplitTunnelViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(key.id)
}
val viewModel: SplitTunnelViewModel =
koinViewModel(
parameters = { parametersOf(key.id) }
)
SplitTunnelScreen(viewModel)
}
entry<Route.LockdownSettings> {
LockdownSettingsScreen()
}
entry<Route.ProxySettings> { ProxySettingsScreen() }
entry<Route.Appearance> { AppearanceScreen() }
entry<Route.Language> { LanguageScreen() }
@@ -441,8 +516,8 @@ class MainActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
WireGuardAutoTunnel.setUiActive(true)
networkMonitor.checkPermissionsAndUpdateState()
WireGuardAutoTunnel.setUiActive(true)
}
override fun onPause() {
@@ -452,6 +527,9 @@ class MainActivity : AppCompatActivity() {
fun performBackup() =
lifecycleScope.launch {
// reset active tuns before backup to prevent trying to start them without permission on
// restore
tunnelRepository.resetActiveTunnels()
roomBackup
.database(appDatabase)
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
@@ -2,85 +2,87 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationMonitor
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.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
import com.zaneschepke.wireguardautotunnel.di.Scope
import com.zaneschepke.wireguardautotunnel.di.appModule
import com.zaneschepke.wireguardautotunnel.di.databaseModule
import com.zaneschepke.wireguardautotunnel.di.dispatchersModule
import com.zaneschepke.wireguardautotunnel.di.networkModule
import com.zaneschepke.wireguardautotunnel.di.tunnelModule
import com.zaneschepke.wireguardautotunnel.di.workerModule
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.androidx.workmanager.koin.workManagerFactory
import org.koin.core.component.KoinComponent
import org.koin.core.context.GlobalContext.startKoin
import org.koin.core.lazyModules
import org.koin.core.option.viewModelScopeFactory
import org.koin.core.qualifier.named
import timber.log.Timber
@HiltAndroidApp
class WireGuardAutoTunnel : Application(), Configuration.Provider {
class WireGuardAutoTunnel : Application(), KoinComponent {
@Inject lateinit var workerFactory: HiltWorkerFactory
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
private val ioDispatcher: CoroutineDispatcher by inject(named(Dispatcher.IO))
private val logReader: LogReader by inject()
override val workManagerConfiguration: Configuration
get() = Configuration.Builder().setWorkerFactory(workerFactory).build()
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
@Inject lateinit var logReader: LogReader
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
@Inject lateinit var monitoringRepository: MonitoringSettingsRepository
@Inject lateinit var notificationMonitor: NotificationMonitor
@Inject lateinit var tunnelManager: TunnelManager
private val monitoringRepository: MonitoringSettingsRepository by inject()
private val notificationMonitor: NotificationMonitor by inject()
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@WireGuardAutoTunnel)
if (BuildConfig.DEBUG) androidLogger()
workManagerFactory()
modules(dispatchersModule, appModule, databaseModule, tunnelModule, workerModule)
options(viewModelScopeFactory())
lazyModules(networkModule)
}
instance = this
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
StrictMode.setThreadPolicy(
ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.penaltyFlashScreen()
.build()
)
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build())
} else {
Timber.plant(ReleaseTree())
}
applicationScope.launch(ioDispatcher) {
launch {
val monitoringSettings = monitoringRepository.getMonitoringSettings()
if (monitoringSettings.isLocalLogsEnabled) logReader.start()
monitoringRepository.flow
.distinctUntilChangedBy { it.isLocalLogsEnabled }
.collect { settings ->
if (settings.isLocalLogsEnabled) {
logReader.start()
} else {
logReader.stop()
}
}
}
launch { notificationMonitor.handleApplicationNotifications() }
}
ServiceWorker.start(this)
}
override fun onTerminate() {
applicationScope.cancel()
tunnelManager.setBackendMode(BackendMode.Inactive)
super.onTerminate()
}
companion object {
private val _uiActive = MutableStateFlow(false)
val uiActive: StateFlow<Boolean>
@@ -4,21 +4,19 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.Scope
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.qualifier.named
@AndroidEntryPoint
class KernelReceiver : BroadcastReceiver() {
class KernelReceiver : BroadcastReceiver(), KoinComponent {
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
@Inject lateinit var tunnelRepository: TunnelRepository
@Inject lateinit var tunnelManager: TunnelManager
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
private val tunnelRepository: TunnelRepository by inject()
private val tunnelManager: TunnelManager by inject()
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return
@@ -5,25 +5,21 @@ import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.Scope
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.component.inject
import org.koin.core.qualifier.named
@AndroidEntryPoint
class NotificationActionReceiver : BroadcastReceiver() {
class NotificationActionReceiver : BroadcastReceiver(), KoinComponent {
@Inject lateinit var tunnelManager: TunnelManager
@Inject lateinit var tunnelRepository: TunnelRepository
@Inject lateinit var autoTunnelRepository: AutoTunnelSettingsRepository
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
private val tunnelManager: TunnelManager by inject()
private val autoTunnelRepository: AutoTunnelSettingsRepository by inject()
private val applicationScope: CoroutineScope = get(named(Scope.APPLICATION))
override fun onReceive(context: Context, intent: Intent) {
applicationScope.launch {
@@ -4,27 +4,25 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.Scope
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.qualifier.named
import timber.log.Timber
@AndroidEntryPoint
class RemoteControlReceiver : BroadcastReceiver() {
class RemoteControlReceiver : BroadcastReceiver(), KoinComponent {
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
@Inject lateinit var settingsRepository: GeneralSettingRepository
@Inject lateinit var tunnelsRepository: TunnelRepository
@Inject lateinit var autoTunnelSettingsRepository: AutoTunnelSettingsRepository
@Inject lateinit var tunnelManager: TunnelManager
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
private val settingsRepository: GeneralSettingRepository by inject()
private val tunnelsRepository: TunnelRepository by inject()
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository by inject()
private val tunnelManager: TunnelManager by inject()
enum class Action(private val suffix: String) {
START_TUNNEL("START_TUNNEL"),
@@ -5,21 +5,25 @@ import android.content.Context
import android.content.Intent
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import com.zaneschepke.wireguardautotunnel.di.Scope
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.component.inject
import org.koin.core.qualifier.named
import timber.log.Timber
@AndroidEntryPoint
class RestartReceiver : BroadcastReceiver() {
class RestartReceiver : BroadcastReceiver(), KoinComponent {
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
private val applicationScope: CoroutineScope = get(named(Scope.APPLICATION))
@Inject lateinit var tunnelManager: TunnelManager
private val tunnelManager: TunnelManager by inject()
@Inject lateinit var logReader: LogReader
private val appStateRepository: AppStateRepository by inject()
private val logReader: LogReader by inject()
override fun onReceive(context: Context, intent: Intent) {
Timber.d("RestartReceiver triggered with action: ${intent.action}")
@@ -31,8 +35,10 @@ class RestartReceiver : BroadcastReceiver() {
tunnelManager.handleReboot()
}
Intent.ACTION_MY_PACKAGE_REPLACED -> {
Timber.i("Restoring state on package upgrade")
tunnelManager.handleRestore()
logReader.deleteAndClearLogs()
appStateRepository.setShouldShowDonationSnackbar(true)
}
}
}
@@ -14,6 +14,7 @@ interface NotificationManager {
fun createNotification(
channel: NotificationChannels,
title: String = "",
subText: String? = null,
actions: Collection<NotificationCompat.Action> = emptyList(),
description: String = "",
showTimestamp: Boolean = true,
@@ -27,6 +28,7 @@ interface NotificationManager {
fun createNotification(
channel: NotificationChannels,
title: StringValue,
subText: String? = null,
actions: Collection<NotificationCompat.Action> = emptyList(),
description: StringValue,
showTimestamp: Boolean = true,
@@ -4,14 +4,11 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.util.StringValue
import jakarta.inject.Inject
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class NotificationMonitor
@Inject
constructor(
class NotificationMonitor(
private val tunnelManager: TunnelManager,
private val notificationManager: NotificationManager,
) {
@@ -32,7 +29,7 @@ constructor(
description =
StringValue.StringResource(
R.string.tunnel_error_template,
error.toStringRes(),
error.stringRes,
),
groupKey = NotificationManager.VPN_GROUP_KEY,
)
@@ -16,11 +16,8 @@ import com.zaneschepke.wireguardautotunnel.core.broadcast.NotificationActionRece
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) :
NotificationManager {
class WireGuardNotification(override val context: Context) : NotificationManager {
enum class NotificationChannels {
VPN,
@@ -32,6 +29,7 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
override fun createNotification(
channel: NotificationChannels,
title: String,
subText: String?,
actions: Collection<NotificationCompat.Action>,
description: String,
showTimestamp: Boolean,
@@ -47,6 +45,7 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
.apply {
actions.forEach { addAction(it) }
setContentTitle(title)
setSubText(subText)
setContentIntent(
PendingIntent.getActivity(
context,
@@ -75,6 +74,7 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
override fun createNotification(
channel: NotificationChannels,
title: StringValue,
subText: String?,
actions: Collection<NotificationCompat.Action>,
description: StringValue,
showTimestamp: Boolean,
@@ -87,6 +87,7 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
return createNotification(
channel,
title.asString(context),
subText,
actions,
description.asString(context),
showTimestamp,
@@ -112,7 +113,7 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
)
return NotificationCompat.Action.Builder(
R.drawable.ic_notification,
notificationAction.title(context).uppercase(),
notificationAction.title(context),
pendingIntent,
)
.build()
@@ -144,6 +145,7 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
context.getString(R.string.auto_tunnel_channel_id),
)
}
NotificationChannels.VPN -> {
NotificationCompat.Builder(context, context.getString(R.string.vpn_channel_id))
}
@@ -160,6 +162,7 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
)
.apply { description = context.getString(R.string.vpn_channel_description) }
}
NotificationChannels.AUTO_TUNNEL -> {
NotificationChannel(
context.getString(R.string.auto_tunnel_channel_id),
@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.core.service
import android.app.Notification
import android.content.Intent
import android.os.IBinder
import android.text.format.Formatter
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
@@ -10,38 +11,36 @@ 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.core.tunnel.TunnelMonitor
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.core.qualifier.named
import timber.log.Timber
@AndroidEntryPoint
abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
@Inject lateinit var notificationManager: NotificationManager
@Inject lateinit var serviceManager: ServiceManager
@Inject lateinit var tunnelManager: TunnelManager
@Inject lateinit var tunnelMonitor: TunnelMonitor
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
@Inject lateinit var settingsRepository: GeneralSettingRepository
@Inject lateinit var tunnelsRepository: TunnelRepository
private val notificationManager: NotificationManager by inject()
private val serviceManager: ServiceManager by inject()
private val tunnelManager: TunnelManager by inject()
private val ioDispatcher: CoroutineDispatcher by inject(named(Dispatcher.IO))
private val settingsRepository: GeneralSettingRepository by inject()
private val tunnelsRepository: TunnelRepository by inject()
protected abstract val fgsType: Int
private var currentSingleTunnelId: Int? = null
private var statsJob: Job? = null
override fun onBind(intent: Intent): IBinder {
super.onBind(intent)
return LocalBinder(this)
@@ -59,12 +58,14 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
ServiceCompat.startForeground(
this,
NotificationManager.VPN_NOTIFICATION_ID,
onCreateNotification(),
fgsType,
)
if (
intent == null ||
intent.component == null ||
@@ -83,28 +84,68 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
} else {
start()
}
return START_STICKY
}
override fun start() {
lifecycleScope.launch(ioDispatcher) {
tunnelManager.activeTunnels.distinctByKeys().collect { activeTunnels ->
val activeTunConfigs = activeTunnels.keys
val activeTunIds = activeTunnels.keys
val tunnels = tunnelsRepository.getAll()
val activeConfigs = tunnels.filter { activeTunConfigs.contains(it.id) }
val activeConfigs = tunnels.filter { activeTunIds.contains(it.id) }
updateServiceNotification(activeConfigs)
restartStatsUpdaterIfNeeded(activeConfigs)
}
}
}
// TODO Would be cool to have this include kill switch
private fun restartStatsUpdaterIfNeeded(activeConfigs: List<TunnelConfig>) {
val single = activeConfigs.singleOrNull()
if (single == null) {
statsJob?.cancel()
statsJob = null
currentSingleTunnelId = null
return
}
if (currentSingleTunnelId == single.id && statsJob?.isActive == true) return
statsJob?.cancel()
statsJob = null
currentSingleTunnelId = single.id
statsJob =
lifecycleScope.launch(ioDispatcher) {
while (isActive) {
val traffic = readTraffic(single.id)
notificationManager.show(
NotificationManager.VPN_NOTIFICATION_ID,
createTunnelNotification(single, consumedTraffic = traffic),
)
delay(1000)
}
}
}
private fun readTraffic(tunnelId: Int): Pair<Long, Long>? {
val active = tunnelManager.activeTunnels.value[tunnelId] ?: return null
val stats = active.statistics ?: return null
return stats.rx() to stats.tx()
}
private fun updateServiceNotification(activeConfigs: List<TunnelConfig>) {
val notification =
when (activeConfigs.size) {
0 -> onCreateNotification()
1 -> createTunnelNotification(activeConfigs.first())
1 -> createTunnelNotification(activeConfigs.first(), consumedTraffic = null)
else -> createTunnelsNotification()
}
ServiceCompat.startForeground(
this,
NotificationManager.VPN_NOTIFICATION_ID,
@@ -115,21 +156,43 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
override fun stop() {
Timber.d("Stop called")
statsJob?.cancel()
statsJob = null
currentSingleTunnelId = null
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
stopSelf()
}
override fun onDestroy() {
serviceManager.handleTunnelServiceDestroy()
statsJob?.cancel()
statsJob = null
currentSingleTunnelId = null
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
Timber.d("onDestroy")
super.onDestroy()
}
private fun createTunnelNotification(tunnelConfig: TunnelConfig): Notification {
private fun createTunnelNotification(
tunnelConfig: TunnelConfig,
consumedTraffic: Pair<Long, Long>?,
): Notification {
val subText =
consumedTraffic?.let { traffic ->
val formattedRx = "${formatBytes(traffic.first)}"
val formattedTx = "${formatBytes(traffic.second)}"
"$formattedRx $formattedTx"
}
return notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = "${getString(R.string.tunnel_running)} - ${tunnelConfig.name}",
title = tunnelConfig.name,
description = getString(R.string.tunnel_running),
subText = subText,
actions =
listOf(
notificationManager.createNotificationAction(
@@ -164,4 +227,6 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
isGroupSummary = true,
)
}
private fun formatBytes(bytes: Long) = Formatter.formatFileSize(this, bytes)
}
@@ -1,5 +1,11 @@
package com.zaneschepke.wireguardautotunnel.core.service
import android.os.Binder
import java.lang.ref.WeakReference
class LocalBinder(val service: TunnelService) : Binder()
class LocalBinder(service: TunnelService) : Binder() {
private val serviceRef = WeakReference(service)
val service: TunnelService?
get() = serviceRef.get()
}
@@ -8,27 +8,31 @@ import android.net.VpnService
import android.os.IBinder
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import jakarta.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber
class ServiceManager
@Inject
constructor(
class ServiceManager(
private val context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
ioDispatcher: CoroutineDispatcher,
applicationScope: CoroutineScope,
private val mainDispatcher: CoroutineDispatcher,
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
) {
@@ -137,17 +141,25 @@ constructor(
suspend fun startTunnelService(appMode: AppMode) =
tunnelMutex.withLock {
if (_tunnelService.value != null) return@withLock
val serviceClass =
when (appMode) {
AppMode.VPN,
AppMode.LOCK_DOWN -> VpnForegroundService::class.java
AppMode.KERNEL,
AppMode.PROXY -> TunnelForegroundService::class.java
}
val intent = Intent(context, serviceClass)
context.startForegroundService(intent)
context.bindService(intent, tunnelServiceConnection, Context.BIND_AUTO_CREATE)
if (_tunnelService.value != null) {
Timber.d("Service already exists, waiting for disconnect")
withTimeoutOrNull(2000L) { _tunnelService.first { it == null } }
?: Timber.w("Timeout waiting for existing service to disconnect")
}
if (_tunnelService.value == null) {
val serviceClass =
when (appMode) {
AppMode.VPN,
AppMode.LOCK_DOWN -> VpnForegroundService::class.java
AppMode.KERNEL,
AppMode.PROXY -> TunnelForegroundService::class.java
}
val intent = Intent(context, serviceClass)
context.startForegroundService(intent)
context.bindService(intent, tunnelServiceConnection, Context.BIND_AUTO_CREATE)
} else {
Timber.e("Service still not null after timeout")
}
}
suspend fun stopTunnelService() =
@@ -157,7 +169,7 @@ constructor(
try {
context.unbindService(tunnelServiceConnection)
} catch (e: Exception) {
Timber.e(e, "Failed to stop Tunnel Service")
Timber.e(e, "Failed to unbind Tunnel Service")
}
}
}
@@ -1,8 +1,6 @@
package com.zaneschepke.wireguardautotunnel.core.service
import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class TunnelForegroundService(override val fgsType: Int = Constants.SPECIAL_USE_SERVICE_TYPE_ID) :
BaseTunnelForegroundService()
@@ -1,8 +1,6 @@
package com.zaneschepke.wireguardautotunnel.core.service
import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class VpnForegroundService(override val fgsType: Int = Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID) :
BaseTunnelForegroundService()
@@ -15,7 +15,7 @@ import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotificati
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
@@ -24,35 +24,49 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsR
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
import com.zaneschepke.wireguardautotunnel.domain.state.toDomain
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.to
import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.lang.ref.WeakReference
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
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.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.koin.android.ext.android.inject
import org.koin.core.qualifier.named
import timber.log.Timber
@AndroidEntryPoint
class AutoTunnelService : LifecycleService() {
@Inject lateinit var networkMonitor: NetworkMonitor
private val networkMonitor: NetworkMonitor by inject()
@Inject lateinit var notificationManager: NotificationManager
private val notificationManager: NotificationManager by inject()
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
private val ioDispatcher: CoroutineDispatcher by inject(named(Dispatcher.IO))
@Inject lateinit var serviceManager: ServiceManager
private val serviceManager: ServiceManager by inject()
@Inject lateinit var tunnelManager: TunnelManager
private val tunnelManager: TunnelManager by inject()
@Inject lateinit var autoTunnelRepository: Provider<AutoTunnelSettingsRepository>
@Inject lateinit var settingsRepository: GeneralSettingRepository
@Inject lateinit var tunnelsRepository: TunnelRepository
private val autoTunnelRepository: AutoTunnelSettingsRepository by inject()
private val settingsRepository: GeneralSettingRepository by inject()
private val tunnelsRepository: TunnelRepository by inject()
private val defaultState = AutoTunnelState()
@@ -60,7 +74,16 @@ class AutoTunnelService : LifecycleService() {
private val autoTunnelStateFlow = MutableStateFlow(defaultState)
class LocalBinder(val service: AutoTunnelService) : Binder()
private var autoTunnelJob: Job? = null
private var permissionsJob: Job? = null
private var autoTunnelFailoverJob: Job? = null
class LocalBinder(service: AutoTunnelService) : Binder() {
private val serviceRef = WeakReference(service)
val service: AutoTunnelService?
get() = serviceRef.get()
}
private val binder = LocalBinder(this)
@@ -83,8 +106,10 @@ class AutoTunnelService : LifecycleService() {
fun start() {
launchWatcherNotification()
startAutoTunnelStateJob()
startLocationPermissionsNotificationJob()
autoTunnelJob?.cancel()
autoTunnelJob = startAutoTunnelStateJob()
permissionsJob?.cancel()
permissionsJob = startLocationPermissionsNotificationJob()
}
fun stop() {
@@ -123,12 +148,12 @@ class AutoTunnelService : LifecycleService() {
)
}
private fun startAutoTunnelStateJob() =
private fun startAutoTunnelStateJob(): Job =
lifecycleScope.launch(ioDispatcher) {
val networkFlow =
debouncedConnectivityStateFlow
.flowOn(ioDispatcher)
.map(NetworkState::from)
.map { it.toDomain() }
.map(::NetworkChange)
.distinctUntilChanged()
@@ -201,6 +226,7 @@ class AutoTunnelService : LifecycleService() {
handleAutoTunnelEvent(autoTunnelStateFlow.value.determineAutoTunnelEvent(change))
// re-evaluate network state after a short duration to prevent missed state changes
reevaluationJob = launch {
val snapshotNetwork = autoTunnelStateFlow.value.networkState
delay(REEVALUATE_CHECK_DELAY)
@@ -222,8 +248,8 @@ class AutoTunnelService : LifecycleService() {
private fun combineSettings(): Flow<Triple<AppMode, AutoTunnelSettings, List<TunnelConfig>>> {
return combine(
settingsRepository.flow.map { it.appMode }.distinctUntilChanged(),
autoTunnelRepository.get().flow,
tunnelsRepository.flow.map { tunnels ->
autoTunnelRepository.flow,
tunnelsRepository.userTunnelsFlow.map { tunnels ->
// isActive is ignored for equality checks so user can manually toggle off
// tunnel with auto-tunnel
tunnels.map { it.copy(isActive = false) }
@@ -265,8 +291,8 @@ class AutoTunnelService : LifecycleService() {
.map {
NetworkPermissionState(
it.settings.wifiDetectionMethod.to(),
it.networkState.locationServicesEnabled == true,
it.networkState.locationPermissionGranted == true,
it.networkState.locationServicesEnabled,
it.networkState.locationPermissionGranted,
(it.tunnels.any { tunnel -> tunnel.tunnelNetworks.isNotEmpty() } ||
it.settings.trustedNetworkSSIDs.isNotEmpty()),
)
@@ -339,7 +365,10 @@ class AutoTunnelService : LifecycleService() {
) {
is AutoTunnelEvent.Start ->
(event.tunnelConfig ?: tunnelsRepository.getDefaultTunnel())?.let {
tunnelManager.startTunnel(it)
tunnelManager.startTunnel(it).onFailure { e ->
Timber.e(e, "Auto-tunnel start failed for ${it.name}")
// TODO notify or retry
}
}
is AutoTunnelEvent.Stop -> tunnelManager.stopActiveTunnels()
AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: nothing to do")
@@ -347,11 +376,10 @@ class AutoTunnelService : LifecycleService() {
}
}
// restart network flow on debounce changes
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
private val debouncedConnectivityStateFlow: Flow<ConnectivityState> by lazy {
autoTunnelRepository
.get()
.flow
autoTunnelRepository.flow
.map { it.debounceDelaySeconds.toMillis() }
.distinctUntilChanged()
.flatMapLatest { debounceMillis ->
@@ -4,25 +4,22 @@ 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 androidx.lifecycle.*
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import timber.log.Timber
@AndroidEntryPoint
class AutoTunnelControlTile : TileService(), LifecycleOwner {
@Inject lateinit var autoTunnelSettingsRepository: AutoTunnelSettingsRepository
@Inject lateinit var tunnelsRepository: TunnelRepository
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository by inject()
@Inject lateinit var serviceManager: ServiceManager
private val serviceManager: ServiceManager by inject()
@OptIn(ExperimentalAtomicApi::class) val isCollecting = AtomicBoolean(false)
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
@@ -36,25 +33,37 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
override fun onStartListening() {
super.onStartListening()
override fun onTileAdded() {
super.onTileAdded()
initTileState()
}
override fun onStopListening() {
super.onStopListening()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
}
@OptIn(ExperimentalAtomicApi::class)
private fun initTileState() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
Timber.d("Start listening called for auto tunnel tile")
lifecycleScope.launch {
serviceManager.autoTunnelService.collect {
if (it != null) return@collect setActive()
setInactive()
}
}
lifecycleScope.launch {
tunnelsRepository.flow.collect {
if (it.isEmpty()) {
setUnavailable()
if (isCollecting.compareAndSet(expectedValue = false, newValue = true)) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
serviceManager.autoTunnelService.collect {
if (it != null) return@collect setActive()
setInactive()
}
}
}
}
}
override fun onStartListening() {
super.onStartListening()
initTileState()
}
override fun onClick() {
super.onClick()
unlockAndRun {
@@ -71,23 +80,16 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
}
private fun setActive() {
runCatching {
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
qsTile?.let {
it.state = Tile.STATE_ACTIVE
it.updateTile()
}
}
private fun setInactive() {
runCatching {
qsTile.state = Tile.STATE_INACTIVE
qsTile.updateTile()
}
}
private fun setUnavailable() {
runCatching {
qsTile.state = Tile.STATE_UNAVAILABLE
qsTile.updateTile()
qsTile?.let {
it.state = Tile.STATE_INACTIVE
it.updateTile()
}
}
@@ -9,29 +9,35 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
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.repository.TunnelRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.koin.android.ext.android.inject
import timber.log.Timber
@AndroidEntryPoint
class TunnelControlTile : TileService(), LifecycleOwner {
@Inject lateinit var tunnelsRepository: TunnelRepository
private val tunnelsRepository: TunnelRepository by inject()
@Inject lateinit var serviceManager: ServiceManager
private val serviceManager: ServiceManager by inject()
@Inject lateinit var tunnelManager: TunnelManager
private val tunnelManager: TunnelManager by inject()
@OptIn(ExperimentalAtomicApi::class) val isCollecting = AtomicBoolean(false)
private val startLock = Mutex()
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
private var isCollecting = false
override fun onCreate() {
super.onCreate()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
@@ -42,13 +48,34 @@ class TunnelControlTile : TileService(), LifecycleOwner {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
override fun onStartListening() {
super.onStartListening()
override fun onTileAdded() {
super.onTileAdded()
initTileState()
}
@OptIn(ExperimentalAtomicApi::class)
private fun initTileState() {
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() } }
if (isCollecting.compareAndSet(expectedValue = false, newValue = true)) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
tunnelManager.activeTunnels
.distinctUntilChangedBy { it.size }
.collect { updateTileState() }
}
}
}
}
override fun onStartListening() {
super.onStartListening()
initTileState()
}
override fun onStopListening() {
super.onStopListening()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
}
private suspend fun updateTileState() {
@@ -76,6 +103,7 @@ class TunnelControlTile : TileService(), LifecycleOwner {
else -> updateTileForLastActiveTunnels()
}
} catch (e: Exception) {
Timber.e(e, "Failed to update tunnel state")
setUnavailable()
}
}
@@ -110,14 +138,16 @@ class TunnelControlTile : TileService(), LifecycleOwner {
super.onClick()
unlockAndRun {
lifecycleScope.launch {
if (tunnelManager.activeTunnels.value.isNotEmpty())
return@launch tunnelManager.stopActiveTunnels()
val lastActive = WireGuardAutoTunnel.getLastActiveTunnels()
if (lastActive.isEmpty()) {
tunnelsRepository.getStartTunnel()?.let { tunnelManager.startTunnel(it) }
} else {
lastActive.forEach { id ->
tunnelsRepository.getById(id)?.let { tunnelManager.startTunnel(it) }
startLock.withLock {
if (tunnelManager.activeTunnels.value.isNotEmpty())
return@launch tunnelManager.stopActiveTunnels()
val lastActive = WireGuardAutoTunnel.getLastActiveTunnels()
if (lastActive.isEmpty()) {
tunnelsRepository.getStartTunnel()?.let { tunnelManager.startTunnel(it) }
} else {
lastActive.forEach { id ->
tunnelsRepository.getById(id)?.let { tunnelManager.startTunnel(it) }
}
}
}
}
@@ -125,37 +155,36 @@ class TunnelControlTile : TileService(), LifecycleOwner {
}
private fun setActive() {
runCatching {
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
qsTile?.let {
it.state = Tile.STATE_ACTIVE
it.updateTile()
}
}
private fun setInactive() {
runCatching {
qsTile.state = Tile.STATE_INACTIVE
qsTile.updateTile()
qsTile?.let {
it.state = Tile.STATE_INACTIVE
it.updateTile()
}
}
private fun setUnavailable() {
runCatching {
qsTile.state = Tile.STATE_UNAVAILABLE
qsTile?.let {
it.state = Tile.STATE_UNAVAILABLE
setTileDescription("")
qsTile.updateTile()
it.updateTile()
}
}
private fun setTileDescription(description: String) {
runCatching {
if (qsTile == null) return@runCatching
qsTile?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
qsTile.subtitle = description
qsTile.stateDescription = description
it.subtitle = description
it.stateDescription = description
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
qsTile.subtitle = description
it.subtitle = description
}
qsTile.updateTile()
it.updateTile()
}
}
@@ -6,13 +6,12 @@ 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,
private val ioDispatcher: CoroutineDispatcher,
) : ShortcutManager {
override suspend fun addShortcuts() {
withContext(ioDispatcher) {
@@ -5,26 +5,23 @@ import androidx.activity.ComponentActivity
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.di.Scope
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.core.qualifier.named
import timber.log.Timber
@AndroidEntryPoint
class ShortcutsActivity : ComponentActivity() {
@Inject lateinit var settingsRepository: GeneralSettingRepository
@Inject lateinit var autoTunnelSettingsRepository: AutoTunnelSettingsRepository
@Inject lateinit var tunnelsRepository: TunnelRepository
@Inject lateinit var tunnelManager: TunnelManager
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
private val settingsRepository: GeneralSettingRepository by inject()
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository by inject()
private val tunnelsRepository: TunnelRepository by inject()
private val tunnelManager: TunnelManager by inject()
private val applicationScope: CoroutineScope by inject(named(Scope.APPLICATION))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -1,158 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import java.util.concurrent.ConcurrentHashMap
import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
abstract class BaseTunnel(
@ApplicationScope protected val applicationScope: CoroutineScope,
@IoDispatcher protected val ioDispatcher: CoroutineDispatcher,
) : TunnelProvider {
protected val errors = MutableSharedFlow<Pair<String, BackendCoreException>>()
override val errorEvents = errors.asSharedFlow()
private val _messageEvents = MutableSharedFlow<Pair<String, BackendMessage>>()
override val messageEvents = _messageEvents.asSharedFlow()
protected val activeTuns = MutableStateFlow<Map<Int, TunnelState>>(emptyMap())
override val activeTunnels = activeTuns.asStateFlow()
protected val tunJobs = ConcurrentHashMap<Int, Job>()
private val tunMutex = Mutex()
private val tunStatusMutex = Mutex()
abstract fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus>
abstract override fun setBackendMode(backendMode: BackendMode)
abstract override fun getBackendMode(): BackendMode
abstract override suspend fun forceStopTunnel(tunnelId: Int)
abstract override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean
abstract override fun getStatistics(tunnelId: Int): TunnelStatistics?
override suspend fun updateTunnelStatus(
tunnelId: Int,
status: TunnelStatus?,
stats: TunnelStatistics?,
pingStates: Map<String, PingState>?,
logHealthState: LogHealthState?,
) {
tunStatusMutex.withLock {
activeTuns.update { currentTuns ->
if (!currentTuns.containsKey(tunnelId) && status != TunnelStatus.Starting) {
Timber.d("Ignoring update for inactive tunnel $tunnelId")
return@update currentTuns
}
val existingState = currentTuns[tunnelId] ?: TunnelState()
val newStatus = status ?: existingState.status
if (newStatus == TunnelStatus.Down) {
Timber.d("Removing tunnel $tunnelId from activeTunnels as state is DOWN")
cleanUpTunJob(tunnelId)
currentTuns - tunnelId
} else if (
existingState.status == newStatus &&
stats == null &&
pingStates == null &&
logHealthState == null
) {
Timber.d("Skipping redundant state update for ${tunnelId}: $newStatus")
currentTuns
} else {
val updated =
existingState.copy(
status = newStatus,
statistics = stats ?: existingState.statistics,
pingStates = pingStates ?: existingState.pingStates,
logHealthState = logHealthState ?: existingState.logHealthState,
)
currentTuns + (tunnelId to updated)
}
}
}
}
override suspend fun stopActiveTunnels() {
activeTunnels.value.forEach { (config, state) ->
if (state.status.isUpOrStarting()) {
stopTunnel(config)
}
}
}
override suspend fun startTunnel(tunnelConfig: TunnelConfig) {
tunMutex.withLock {
if (
activeTuns.value.containsKey(tunnelConfig.id) ||
tunJobs.containsKey(tunnelConfig.id)
) {
return Timber.w("Tunnel is already running: ${tunnelConfig.name}")
}
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting)
val job =
applicationScope.launch(ioDispatcher) {
try {
tunnelStateFlow(tunnelConfig).collect { status ->
updateTunnelStatus(tunnelConfig.id, status)
}
} catch (e: BackendCoreException) {
errors.emit(tunnelConfig.name to e)
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Down)
} catch (_: CancellationException) {}
}
tunJobs[tunnelConfig.id] = job
job.invokeOnCompletion {
tunJobs.remove(tunnelConfig.id)
activeTuns.update { it - tunnelConfig.id }
}
}
}
override suspend fun stopTunnel(tunnelId: Int) {
tunMutex.withLock {
val currentState = activeTuns.value[tunnelId]?.status ?: return@withLock
updateTunnelStatus(tunnelId, TunnelStatus.Stopping)
tunJobs[tunnelId]?.cancel()
withTimeoutOrNull(STOP_TIMEOUT_MS) {
activeTunnels.first {
!it.containsKey(tunnelId) || it[tunnelId]!!.status == TunnelStatus.Down
}
}
?: run {
Timber.w("Stop timeout for $tunnelId (was $currentState); forcing kill")
forceStopTunnel(tunnelId)
}
}
}
private fun cleanUpTunJob(tunnelId: Int) {
Timber.d("Removing job for $tunnelId")
tunJobs -= tunnelId
}
companion object {
const val STARTUP_TIMEOUT_MS: Long = 15_000L
const val STOP_TIMEOUT_MS: Long = 5_000L
}
}
@@ -31,7 +31,7 @@ fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.exists(id: Int): Boolean {
}
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.isUp(id: Int): Boolean {
return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Up }
return this.value.any { it.key.id == id && it.value.status is TunnelStatus.Up }
}
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.isStarting(id: Int): Boolean {
@@ -0,0 +1,187 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.TunnelBackend
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
import com.zaneschepke.wireguardautotunnel.domain.events.UnknownError
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber
class TunnelLifecycleManager(
private val backend: TunnelBackend,
private val applicationScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher,
private val sharedActiveTunnels: MutableStateFlow<Map<Int, TunnelState>>,
) : TunnelProvider {
override val activeTunnels: StateFlow<Map<Int, TunnelState>> = sharedActiveTunnels.asStateFlow()
private val _errorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
override val errorEvents: SharedFlow<Pair<String?, BackendCoreException>> =
_errorEvents.asSharedFlow()
private val _messageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
override val messageEvents: SharedFlow<Pair<String?, BackendMessage>> =
_messageEvents.asSharedFlow()
private val tunnelJobs = ConcurrentHashMap<Int, Job>()
private val tunMutex = Mutex()
private val tunStatusMutex = Mutex()
override suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<Unit> =
tunMutex.withLock {
val id = tunnelConfig.id
if (sharedActiveTunnels.value.containsKey(id)) {
Timber.w("Tunnel is already running: ${tunnelConfig.name}")
return Result.failure(IllegalStateException("Tunnel already running"))
}
val startupCompleted = CompletableDeferred<Result<Unit>>()
val job =
applicationScope.launch(ioDispatcher) {
try {
updateTunnelStatus(id, TunnelStatus.Starting)
backend.tunnelStateFlow(tunnelConfig).collect { status ->
updateTunnelStatus(id, status)
if (status != TunnelStatus.Starting && !startupCompleted.isCompleted) {
if (status is TunnelStatus.Up) {
startupCompleted.complete(Result.success(Unit))
} else {
startupCompleted.complete(Result.failure(UnknownError()))
}
}
}
} catch (e: BackendCoreException) {
_errorEvents.emit(tunnelConfig.name to e)
updateTunnelStatus(id, TunnelStatus.Down)
startupCompleted.complete(Result.failure(e))
} catch (_: CancellationException) {} finally {
tunnelJobs.remove(id)
sharedActiveTunnels.update { it - id }
}
}
tunnelJobs[id] = job
job.invokeOnCompletion { tunnelJobs.remove(id) }
try {
startupCompleted.await()
} catch (e: Throwable) {
job.cancel()
Result.failure(e)
}
}
override suspend fun stopTunnel(tunnelId: Int) =
tunMutex.withLock {
val currentState = sharedActiveTunnels.value[tunnelId]?.status ?: return@withLock
updateTunnelStatus(tunnelId, TunnelStatus.Stopping)
tunnelJobs[tunnelId]?.cancel()
withTimeoutOrNull(STOP_TIMEOUT_MS) {
activeTunnels.first {
!it.containsKey(tunnelId) || it[tunnelId]!!.status == TunnelStatus.Down
}
}
?: run {
Timber.w("Stop timeout for $tunnelId (was $currentState); forcing kill")
forceStopTunnel(tunnelId)
}
}
override suspend fun forceStopTunnel(tunnelId: Int) {
backend.forceStopTunnel(tunnelId)
tunnelJobs[tunnelId]?.cancel()
tunnelJobs.remove(tunnelId)
sharedActiveTunnels.update { it - tunnelId }
updateTunnelStatus(tunnelId, TunnelStatus.Down)
}
override suspend fun stopActiveTunnels() {
sharedActiveTunnels.value.forEach { (id, state) ->
if (state.status.isUpOrStarting()) {
stopTunnel(id)
}
}
}
override suspend fun updateTunnelStatus(
tunnelId: Int,
status: TunnelStatus?,
stats: TunnelStatistics?,
pingStates: Map<String, PingState>?,
logHealthState: LogHealthState?,
) =
tunStatusMutex.withLock {
sharedActiveTunnels.update { currentTuns ->
if (!currentTuns.containsKey(tunnelId) && status != TunnelStatus.Starting) {
Timber.d("Ignoring update for inactive tunnel $tunnelId")
return@update currentTuns
}
val existingState = currentTuns[tunnelId] ?: TunnelState()
val newStatus = status ?: existingState.status
if (newStatus == TunnelStatus.Down) {
Timber.d("Removing tunnel $tunnelId from activeTunnels as state is DOWN")
currentTuns - tunnelId
} else if (
existingState.status == newStatus &&
stats == null &&
pingStates == null &&
logHealthState == null
) {
Timber.d("Skipping redundant state update for ${tunnelId}: $newStatus")
currentTuns
} else {
val updated =
existingState.copy(
status = newStatus,
statistics = stats ?: existingState.statistics,
pingStates = pingStates ?: existingState.pingStates,
logHealthState = logHealthState ?: existingState.logHealthState,
)
currentTuns + (tunnelId to updated)
}
}
}
override fun setBackendMode(backendMode: BackendMode) = backend.setBackendMode(backendMode)
override fun getBackendMode(): BackendMode = backend.getBackendMode()
override suspend fun runningTunnelNames(): Set<String> = backend.runningTunnelNames()
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean =
backend.handleDnsReresolve(tunnelConfig)
override fun getStatistics(tunnelId: Int): TunnelStatistics? = backend.getStatistics(tunnelId)
companion object {
const val STOP_TIMEOUT_MS: Long = 5_000L
}
}
@@ -1,280 +1,137 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import android.os.PowerManager
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig as Entity
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.TunnelBackend
import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.DynamicDnsHandler
import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.TunnelActiveStatePersister
import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.TunnelMonitorHandler
import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.TunnelServiceHandler
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.di.*
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
import com.zaneschepke.wireguardautotunnel.domain.events.NotAuthorized
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.AtomicReference
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.withContext
import timber.log.Timber
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalAtomicApi::class)
class TunnelManager
@Inject
constructor(
@Kernel private val kernelTunnel: TunnelProvider,
@Userspace private val userspaceTunnel: TunnelProvider,
@ProxyUserspace private val proxyUserspaceTunnel: TunnelProvider,
class TunnelManager(
kernelBackend: TunnelBackend,
userspaceBackend: TunnelBackend,
proxyUserspaceBackend: TunnelBackend,
networkMonitor: NetworkMonitor,
networkUtils: NetworkUtils,
powerManager: PowerManager,
logReader: LogReader,
monitoringSettingsRepository: MonitoringSettingsRepository,
private val serviceManager: ServiceManager,
private val settingsRepository: GeneralSettingRepository,
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
private val lockdownSettingsRepository: LockdownSettingsRepository,
private val tunnelsRepository: TunnelRepository,
private val tunnelMonitor: TunnelMonitor,
@ApplicationScope private val applicationScope: CoroutineScope,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val applicationScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher,
) : TunnelProvider {
@OptIn(ExperimentalCoroutinesApi::class)
private val localErrorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
private val _activeTunnels = MutableStateFlow<Map<Int, TunnelState>>(emptyMap())
override val activeTunnels: StateFlow<Map<Int, TunnelState>> = _activeTunnels.asStateFlow()
@OptIn(ExperimentalCoroutinesApi::class)
private val localMessageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
@OptIn(ExperimentalAtomicApi::class) val currentAppMode = AtomicReference(AppMode.VPN)
private val monitoringMutex = Mutex()
private val monitoringJobs = ConcurrentHashMap<Int, Job>()
private val defaultManager =
TunnelLifecycleManager(userspaceBackend, applicationScope, ioDispatcher, _activeTunnels)
private val ddnsMutex = Mutex()
private val ddnsJobs = ConcurrentHashMap<Int, Job>()
private val lifecycleManagers: Map<AppMode, TunnelLifecycleManager> =
mapOf(
AppMode.KERNEL to
TunnelLifecycleManager(
kernelBackend,
applicationScope,
ioDispatcher,
_activeTunnels,
),
AppMode.VPN to defaultManager,
AppMode.PROXY to
TunnelLifecycleManager(
proxyUserspaceBackend,
applicationScope,
ioDispatcher,
_activeTunnels,
),
AppMode.LOCK_DOWN to
TunnelLifecycleManager(
proxyUserspaceBackend,
applicationScope,
ioDispatcher,
_activeTunnels,
),
)
private data class SideEffectState(
val activeTuns: Map<Int, TunnelState>,
val tuns: List<TunnelConfig>,
val settings: GeneralSettings,
val previouslyActive: Map<Int, TunnelState>,
)
private data class SideEffectWithCondition(
val effect: suspend (SideEffectState) -> Unit,
val condition: (SideEffectState) -> Boolean,
)
private suspend fun getSettings(): GeneralSettings =
settingsRepository.flow.filterNotNull().first { it != GeneralSettings() }
private suspend fun getTunnels(): List<TunnelConfig> =
tunnelsRepository.flow.first { it.isNotEmpty() }
private val tunnelProviderFlow: StateFlow<TunnelProvider> = run {
val currentBackend = AtomicReference(userspaceTunnel)
val currentSettings = AtomicReference(GeneralSettings())
val initialEmit = AtomicBoolean(true)
settingsRepository.flow
.filterNotNull()
// ignore default state
.filterNot { it == GeneralSettings() }
.distinctUntilChanged { old, new ->
old.appMode == new.appMode &&
old.isLanOnKillSwitchEnabled == new.isLanOnKillSwitchEnabled
}
.map { settings ->
Timber.d("App mode changes with ${settings.appMode}")
val backend =
when (settings.appMode) {
AppMode.VPN -> userspaceTunnel
AppMode.PROXY -> proxyUserspaceTunnel
AppMode.LOCK_DOWN -> proxyUserspaceTunnel
AppMode.KERNEL -> kernelTunnel
}
settings to backend
}
.onEach { (settings, newBackend) ->
val isInitialEmit = initialEmit.exchange(false)
val previousBackend = currentBackend.exchange(newBackend)
val previousSettings = currentSettings.exchange(settings)
if ((previousSettings.appMode != settings.appMode) && !isInitialEmit) {
handleModeChangeCleanup(previousBackend, previousSettings.appMode)
}
if (settings.appMode == AppMode.LOCK_DOWN) {
handleLockDownModeInit(settings.isLanOnKillSwitchEnabled)
}
}
.map { (_, backend) -> backend }
.stateIn(
scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly,
initialValue = userspaceTunnel,
)
@OptIn(ExperimentalAtomicApi::class)
private fun getProvider(): TunnelProvider {
return lifecycleManagers[currentAppMode.load()] ?: defaultManager
}
override val activeTunnels: StateFlow<Map<Int, TunnelState>> = run {
val activeTunsReference: AtomicReference<Map<Int, TunnelState>> =
AtomicReference(emptyMap())
override suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<Unit> =
getProvider().startTunnel(tunnelConfig)
tunnelProviderFlow
.flatMapLatest { backend ->
combine(
backend.activeTunnels,
tunnelsRepository.flow,
settingsRepository.flow.filterNotNull(),
) { activeTuns, tuns, settings ->
Triple(activeTuns, tuns, settings)
}
}
.onStart { handleRestore() }
.onEach { (activeTuns, tuns, settings) ->
val previouslyActive = activeTunsReference.exchange(activeTuns)
val state = SideEffectState(activeTuns, tuns, settings, previouslyActive)
override suspend fun stopTunnel(tunnelId: Int) = getProvider().stopTunnel(tunnelId)
applicationScope.launch(ioDispatcher) {
supervisorScope {
val sideEffects =
listOf(
SideEffectWithCondition(
effect = { s ->
handleTunnelServiceChange(s.settings.appMode, s.activeTuns)
},
condition = { s ->
s.activeTuns.size != s.previouslyActive.size
},
),
SideEffectWithCondition(
effect = { s ->
handleTunnelsActiveChange(
s.previouslyActive,
s.activeTuns,
s.tuns,
)
},
condition = { s ->
s.activeTuns.size != s.previouslyActive.size
},
),
SideEffectWithCondition(
effect = { s ->
handleDynamicDnsMonitoring(s.activeTuns, s.tuns, s.settings)
},
condition = { s ->
s.activeTuns.keys != s.previouslyActive.keys
},
),
SideEffectWithCondition(
effect = { s ->
handleFullTunnelMonitoring(s.activeTuns, s.tuns, s.settings)
},
condition = { s ->
s.activeTuns.keys != s.previouslyActive.keys
},
),
)
override suspend fun forceStopTunnel(tunnelId: Int) = getProvider().forceStopTunnel(tunnelId)
sideEffects
.filter { it.condition(state) }
.forEach { sideEffect ->
launch {
try {
sideEffect.effect(state)
} catch (e: Exception) {
Timber.e(e, "Side effect failed")
}
}
}
}
}
}
.map { (activeTuns, _, _) -> activeTuns }
.stateIn(
scope = applicationScope,
started = SharingStarted.Eagerly,
initialValue = emptyMap(),
)
}
override suspend fun stopActiveTunnels() = getProvider().stopActiveTunnels()
@OptIn(ExperimentalCoroutinesApi::class)
override val errorEvents: SharedFlow<Pair<String?, BackendCoreException>> =
merge(localErrorEvents, tunnelProviderFlow.flatMapLatest { it.errorEvents })
.shareIn(
scope = applicationScope + ioDispatcher,
started = SharingStarted.Eagerly,
replay = 0,
)
override fun setBackendMode(backendMode: BackendMode) =
getProvider().setBackendMode(backendMode)
@OptIn(ExperimentalCoroutinesApi::class)
override val messageEvents: SharedFlow<Pair<String?, BackendMessage>> =
merge(localMessageEvents, tunnelProviderFlow.flatMapLatest { it.messageEvents })
.shareIn(
scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly,
replay = 0,
)
override fun getBackendMode(): BackendMode = getProvider().getBackendMode()
override fun getStatistics(tunnelId: Int): TunnelStatistics? {
return tunnelProviderFlow.value.getStatistics(tunnelId)
}
override suspend fun runningTunnelNames(): Set<String> = getProvider().runningTunnelNames()
override suspend fun startTunnel(tunnelConfig: TunnelConfig) {
if (activeTunnels.value.containsKey(tunnelConfig.id)) return
val provider = tunnelProviderFlow.value
val isKernel = provider is KernelTunnel
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean =
getProvider().handleDnsReresolve(tunnelConfig)
if (!isKernel && activeTunnels.value.isNotEmpty()) {
stopActiveTunnels()
withTimeoutOrNull(BaseTunnel.STARTUP_TIMEOUT_MS) {
activeTunnels.first { it.isEmpty() }
} ?: run { activeTunnels.value.keys.forEach { id -> provider.forceStopTunnel(id) } }
}
val runConfig =
tunnelConfig.run {
if (getSettings().isTunnelGlobalsEnabled) {
val globalTunnel =
getTunnels().firstOrNull { it.name == Entity.GLOBAL_CONFIG_NAME }
?: return@run this
return@run copyWithGlobalValues(globalTunnel)
}
this
}
tunnelProviderFlow.value.startTunnel(runConfig)
}
override suspend fun stopTunnel(tunnelId: Int) {
tunnelProviderFlow.value.stopTunnel(tunnelId)
}
override suspend fun forceStopTunnel(tunnelId: Int) {
tunnelProviderFlow.value.forceStopTunnel(tunnelId)
}
override suspend fun stopActiveTunnels() {
tunnelProviderFlow.value.stopActiveTunnels()
}
override fun setBackendMode(backendMode: BackendMode) {
tunnelProviderFlow.value.setBackendMode(backendMode)
}
override fun getBackendMode(): BackendMode {
return tunnelProviderFlow.value.getBackendMode()
}
override suspend fun runningTunnelNames(): Set<String> {
return tunnelProviderFlow.value.runningTunnelNames()
}
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean {
return tunnelProviderFlow.value.handleDnsReresolve(tunnelConfig)
}
override fun getStatistics(tunnelId: Int): TunnelStatistics? =
getProvider().getStatistics(tunnelId)
override suspend fun updateTunnelStatus(
tunnelId: Int,
@@ -282,68 +139,151 @@ constructor(
stats: TunnelStatistics?,
pingStates: Map<String, PingState>?,
logHealthState: LogHealthState?,
) {
tunnelProviderFlow.value.updateTunnelStatus(
tunnelId,
status,
stats,
pingStates,
logHealthState,
) = getProvider().updateTunnelStatus(tunnelId, status, stats, pingStates, logHealthState)
@OptIn(ExperimentalCoroutinesApi::class)
private val localErrorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
@OptIn(ExperimentalCoroutinesApi::class)
private val localMessageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
override val errorEvents: SharedFlow<Pair<String?, BackendCoreException>> =
merge(localErrorEvents, *lifecycleManagers.values.map { it.errorEvents }.toTypedArray())
.shareIn(
scope = applicationScope + ioDispatcher,
started = SharingStarted.Eagerly,
replay = 0,
)
override val messageEvents: SharedFlow<Pair<String?, BackendMessage>> =
merge(localMessageEvents, *lifecycleManagers.values.map { it.messageEvents }.toTypedArray())
.shareIn(
scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly,
replay = 0,
)
private val tunnelServiceHandler =
TunnelServiceHandler(
activeTunnels = activeTunnels,
settingsRepository = settingsRepository,
serviceManager = serviceManager,
applicationScope = applicationScope,
ioDispatcher = ioDispatcher,
)
private val tunnelActiveStatePersister =
TunnelActiveStatePersister(
activeTunnels = activeTunnels,
tunnelsRepository = tunnelsRepository,
applicationScope = applicationScope,
ioDispatcher = ioDispatcher,
)
private val dynamicDnsHandler =
DynamicDnsHandler(
activeTunnels = activeTunnels,
tunnelsRepository = tunnelsRepository,
settingsRepository = settingsRepository,
localMessageEvents = localMessageEvents,
handleDnsReresolve = { config -> handleDnsReresolve(config) },
applicationScope = applicationScope,
ioDispatcher = ioDispatcher,
)
private val fullTunnelMonitorHandler =
TunnelMonitorHandler(
activeTunnels = activeTunnels,
tunnelsRepository = tunnelsRepository,
settingsRepository = settingsRepository,
monitoringSettingsRepository = monitoringSettingsRepository,
networkMonitor = networkMonitor,
networkUtils = networkUtils,
powerManager = powerManager,
logReader = logReader,
getStatistics = { id -> getStatistics(id) },
updateTunnelStatus = { id, status, stats, pings, logHealth ->
updateTunnelStatus(id, status, stats, pings, logHealth)
},
applicationScope = applicationScope,
ioDispatcher = ioDispatcher,
)
init {
applicationScope.launch(ioDispatcher) {
val initialEmit = AtomicBoolean(true)
settingsRepository.flow
.filterNotNull()
.filterNot { it == GeneralSettings() }
.distinctUntilChangedBy { it.appMode }
.collect { settings ->
val isInitialEmit = initialEmit.exchange(false)
val previousMode = currentAppMode.exchange(settings.appMode)
if (isInitialEmit) {
return@collect handleRestore(settings)
}
if (previousMode != settings.appMode) {
handleModeChangeCleanup(previousMode)
}
if (settings.appMode == AppMode.LOCK_DOWN) {
handleLockDownModeInit()
}
}
}
}
private suspend fun handleTunnelServiceChange(
appMode: AppMode,
activeTuns: Map<Int, TunnelState>,
) {
if (activeTuns.isEmpty()) serviceManager.stopTunnelService()
if (activeTuns.isNotEmpty() && serviceManager.tunnelService.value == null)
serviceManager.startTunnelService(appMode)
serviceManager.updateTunnelTile()
}
private fun handleLockDownModeInit(withLanBypass: Boolean) {
val allowedIps = if (withLanBypass) TunnelConfig.IPV4_PUBLIC_NETWORKS else emptySet()
// TODO this can crash if we haven't started foreground service yet, especially for
// workerManager
private suspend fun handleLockDownModeInit() {
val lockdownSettings = lockdownSettingsRepository.getLockdownSettings()
val allowedIps =
if (lockdownSettings.bypassLan) TunnelConfig.IPV4_PUBLIC_NETWORKS else emptySet()
try {
if (serviceManager.hasVpnPermission()) {
proxyUserspaceTunnel.setBackendMode(BackendMode.KillSwitch(allowedIps))
setBackendMode(
BackendMode.KillSwitch(
allowedIps,
lockdownSettings.metered,
lockdownSettings.dualStack,
)
)
} else {
throw BackendCoreException.NotAuthorized
throw NotAuthorized()
}
} catch (e: BackendCoreException) {
localErrorEvents.tryEmit(null to e)
}
}
private suspend fun handleModeChangeCleanup(
previousBackend: TunnelProvider,
previousAppMode: AppMode,
) {
previousBackend.stopActiveTunnels()
// stop lockdown if we switch from that mode
if (previousAppMode == AppMode.LOCK_DOWN)
proxyUserspaceTunnel.setBackendMode(BackendMode.Inactive)
private suspend fun handleModeChangeCleanup(previousAppMode: AppMode) {
lifecycleManagers[previousAppMode]?.stopActiveTunnels()
if (previousAppMode == AppMode.LOCK_DOWN) {
lifecycleManagers[previousAppMode]?.setBackendMode(BackendMode.Inactive)
}
}
suspend fun handleRestore() =
suspend fun handleRestore(settings: GeneralSettings? = null) =
withContext(ioDispatcher) {
val settings = settingsRepository.getGeneralSettings()
val currentSettings = settings ?: settingsRepository.getGeneralSettings()
val autoTunnelSettings = autoTunnelSettingsRepository.getAutoTunnelSettings()
val tunnels = tunnelsRepository.getAll()
val tunnels = tunnelsRepository.userTunnelsFlow.firstOrNull()
if (autoTunnelSettings.isAutoTunnelEnabled)
return@withContext restoreAutoTunnel(autoTunnelSettings)
when (val mode = settings.appMode) {
AppMode.VPN,
AppMode.PROXY,
AppMode.LOCK_DOWN -> {
if (serviceManager.hasVpnPermission()) {
if (mode == AppMode.LOCK_DOWN)
handleLockDownModeInit(settings.isLanOnKillSwitchEnabled)
if (currentSettings.appMode == AppMode.LOCK_DOWN) handleLockDownModeInit()
if (tunnels?.any { it.isActive } == true) {
if (currentSettings.appMode == AppMode.VPN && !serviceManager.hasVpnPermission())
return@withContext localErrorEvents.emit(null to NotAuthorized())
when (currentSettings.appMode) {
AppMode.VPN,
AppMode.PROXY,
AppMode.LOCK_DOWN -> {
tunnels.firstOrNull { it.isActive }?.let { startTunnel(it) }
} else localErrorEvents.emit(null to BackendCoreException.NotAuthorized)
}
AppMode.KERNEL ->
tunnels.filter { it.isActive }.forEach { conf -> startTunnel(conf) }
}
AppMode.KERNEL ->
tunnels.filter { it.isActive }.forEach { conf -> startTunnel(conf) }
}
}
@@ -356,174 +296,64 @@ constructor(
withContext(ioDispatcher) {
val settings = settingsRepository.getGeneralSettings()
val autoTunnelSettings = autoTunnelSettingsRepository.getAutoTunnelSettings()
val defaultTunnel = tunnelsRepository.getStartTunnel()
val defaultTunnel = tunnelsRepository.getDefaultTunnel()
if (autoTunnelSettings.startOnBoot)
return@withContext restoreAutoTunnel(autoTunnelSettings)
if (settings.isRestoreOnBootEnabled) {
tunnelsRepository.resetActiveTunnels()
when (val mode = settings.appMode) {
AppMode.VPN,
AppMode.PROXY,
AppMode.LOCK_DOWN -> {
if (serviceManager.hasVpnPermission()) {
if (mode == AppMode.LOCK_DOWN)
handleLockDownModeInit(settings.isLanOnKillSwitchEnabled)
} else
return@withContext localErrorEvents.emit(
null to BackendCoreException.NotAuthorized
)
}
AppMode.KERNEL -> Unit
when (settings.appMode) {
AppMode.LOCK_DOWN -> handleLockDownModeInit()
AppMode.VPN ->
if (!serviceManager.hasVpnPermission())
return@withContext localErrorEvents.emit(null to NotAuthorized())
AppMode.KERNEL,
AppMode.PROXY -> Unit
}
defaultTunnel?.let { startTunnel(it) }
}
}
private suspend fun handleTunnelsActiveChange(
previousActiveTuns: Map<Int, TunnelState>,
activeTuns: Map<Int, TunnelState>,
tuns: List<TunnelConfig>,
) {
val relevantTunnels = previousActiveTuns.keys + activeTuns.keys
relevantTunnels.forEach { tunnelId ->
val wasActive = previousActiveTuns.containsKey(tunnelId)
val isActiveNow = activeTuns.containsKey(tunnelId)
when {
!wasActive && isActiveNow -> {
tuns
.find { it.id == tunnelId }
?.let { dbTunnelConf ->
tunnelsRepository.save(dbTunnelConf.copy(isActive = true))
}
}
wasActive && !isActiveNow -> {
tuns
.find { it.id == tunnelId }
?.let { dbTunnelConf ->
tunnelsRepository.save(dbTunnelConf.copy(isActive = false))
}
}
}
suspend fun restartActiveTunnel(id: Int) =
withContext(ioDispatcher) {
val activeIds = activeTunnels.value.keys.toList()
if (activeIds.isEmpty()) return@withContext
if (!activeIds.contains(id)) return@withContext
val tunnel = tunnelsRepository.getById(id) ?: return@withContext
restartTunnel(tunnel)
}
}
private suspend fun handleDynamicDnsMonitoring(
activeTuns: Map<Int, TunnelState>,
configs: List<TunnelConfig>,
settings: GeneralSettings,
) =
ddnsMutex.withLock {
val activeIds =
activeTuns.keys
.filter { id ->
configs.find { it.id == id }?.restartOnPingFailure == true &&
settings.appMode != AppMode.KERNEL
}
.toSet()
suspend fun restartActiveTunnels() =
withContext(ioDispatcher) {
val activeIds = activeTunnels.value.keys.toList()
if (activeIds.isEmpty()) return@withContext
val currentJobs = ddnsJobs.keys.toSet()
val obsoleteIds = currentJobs - activeIds
val tunnels = tunnelsRepository.getAll()
if (tunnels.isEmpty()) return@withContext
Timber.d(
"DDNS Monitoring: Active IDs: $activeIds, Obsolete IDs: $obsoleteIds, Total jobs before: ${ddnsJobs.size}"
)
obsoleteIds.forEach { id ->
ddnsJobs[id]?.cancel()
ddnsJobs.remove(id)
}
activeIds.forEach { id ->
if (ddnsJobs.containsKey(id)) return@forEach // Skip if already monitored
val conf = configs.find { it.id == id } ?: return@forEach
val tunStateFlow =
activeTunnels.map { it[id] }.stateIn(applicationScope + ioDispatcher)
val newJob =
applicationScope.launch(ioDispatcher) {
var backoff = 30_000L
while (isActive) {
val state = tunStateFlow.value ?: break
if (state.health() != TunnelState.Health.UNHEALTHY) {
backoff = BASE_BACKOFF
tunStateFlow.first {
it?.health() == TunnelState.Health.UNHEALTHY || it == null
}
continue
supervisorScope {
activeIds.forEach { id ->
val tunnel =
tunnels.find { it.id == id }
?: run {
Timber.w("Tunnel config $id not found; skipping restart")
return@forEach
}
runCatching {
val updated = handleDnsReresolve(conf)
if (updated) {
localMessageEvents.emit(
conf.name to BackendMessage.DynamicDnsSuccess
)
backoff = BASE_BACKOFF
} else {
Timber.i(
"Dynamic DNS check completed, current endpoint address is already up to date."
)
}
}
.onFailure {
Timber.e(
it,
"Failed to handle dns re-resolution for ${conf.name}",
)
}
delay(backoff)
backoff = (backoff * 1.5).toLong().coerceAtMost(MAX_BACKOFF_TIME)
}
}
ddnsJobs[id] = newJob
restartTunnel(tunnel)
}
}
}
private suspend fun handleFullTunnelMonitoring(
activeTuns: Map<Int, TunnelState>,
configs: List<TunnelConfig>,
settings: GeneralSettings,
) =
monitoringMutex.withLock {
val activeIds = activeTuns.keys.toSet()
val currentJobs = monitoringJobs.keys.toSet()
val obsoleteIds = currentJobs - activeIds
private suspend fun restartTunnel(tunnel: TunnelConfig) {
runCatching { stopTunnel(tunnel.id) }
.onFailure { e -> Timber.e(e, "Failed to stop tunnel ${tunnel.id} during restart") }
Timber.d(
"Monitoring: Active IDs: $activeIds, Obsolete IDs: $obsoleteIds, Total jobs before: ${monitoringJobs.size}"
)
delay(RESTART_TUNNEL_DELAY)
obsoleteIds.forEach { id ->
monitoringJobs[id]?.cancel()
monitoringJobs.remove(id)
}
activeIds.forEach { id ->
if (monitoringJobs.containsKey(id)) return@forEach // Skip if already monitored
configs.find { it.id == id } ?: return@forEach
val tunStateFlow =
activeTunnels.map { it[id] }.stateIn(applicationScope + ioDispatcher)
val newJob =
applicationScope.launch(ioDispatcher) {
tunnelMonitor.startMonitoring(
id,
withLogs = settings.appMode != AppMode.KERNEL,
tunStateFlow = tunStateFlow,
getStatistics = { tunnelId -> getStatistics(tunnelId) },
updateTunnelStatus = { tid, _, stats, pings, logHealth ->
updateTunnelStatus(tid, null, stats, pings, logHealth)
},
)
}
monitoringJobs[id] = newJob
}
}
runCatching { startTunnel(tunnel) }
.onFailure { e -> Timber.e(e, "Failed to restart tunnel ${tunnel.id}") }
}
companion object {
const val BASE_BACKOFF = 30_000L
const val MAX_BACKOFF_TIME = 300_000L
const val RESTART_TUNNEL_DELAY = 300L
}
}
@@ -13,19 +13,12 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
interface TunnelProvider {
/** Starts the specified tunnel configuration. */
suspend fun startTunnel(tunnelConfig: TunnelConfig)
suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<Unit>
/**
* Stops the specified tunnel.
*
* @param tunnelId The tunnelConf to stop.
*/
suspend fun stopTunnel(tunnelId: Int)
suspend fun forceStopTunnel(tunnelId: Int)
/** Stops all active tunnels. */
suspend fun stopActiveTunnels()
fun setBackendMode(backendMode: BackendMode)
@@ -39,9 +32,7 @@ interface TunnelProvider {
fun getStatistics(tunnelId: Int): TunnelStatistics?
val activeTunnels: StateFlow<Map<Int, TunnelState>>
val errorEvents: SharedFlow<Pair<String?, BackendCoreException>>
val messageEvents: SharedFlow<Pair<String?, BackendMessage>>
suspend fun updateTunnelStatus(
@@ -1,193 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendMode
import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendMode
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException
import java.io.IOException
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.update
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.BackendException
import org.amnezia.awg.backend.ProxyGoBackend
import org.amnezia.awg.backend.Tunnel as AwgTunnel
import org.amnezia.awg.config.Config
import org.amnezia.awg.config.DnsSettings
import org.amnezia.awg.config.proxy.HttpProxy
import org.amnezia.awg.config.proxy.Proxy
import org.amnezia.awg.config.proxy.Socks5Proxy
import timber.log.Timber
class UserspaceTunnel
@Inject
constructor(
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
private val proxySettingsRepository: ProxySettingsRepository,
private val dnsSettingsRepository: DnsSettingsRepository,
private val backend: Backend,
) : BaseTunnel(applicationScope, ioDispatcher) {
private val runtimeTunnels = ConcurrentHashMap<Int, AwgTunnel>()
override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow {
val stateChannel = Channel<AwgTunnel.State>()
val runtimeTunnel = RuntimeAwgTunnel(tunnelConfig, stateChannel)
runtimeTunnels[tunnelConfig.id] = runtimeTunnel
val consumerJob = launch {
stateChannel.consumeAsFlow().collect { awgState -> trySend(awgState.asTunnelState()) }
}
try {
withTimeout(STARTUP_TIMEOUT_MS) {
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting)
val proxies: List<Proxy> =
when (backend) {
is ProxyGoBackend -> {
val proxySettings = proxySettingsRepository.getProxySettings()
Timber.d("Adding proxy configs")
buildList {
if (proxySettings.socks5ProxyEnabled) {
add(
Socks5Proxy(
proxySettings.socks5ProxyBindAddress
?: ProxySettings.DEFAULT_SOCKS_BIND_ADDRESS,
proxySettings.proxyUsername,
proxySettings.proxyPassword,
)
)
}
if (proxySettings.httpProxyEnabled) {
add(
HttpProxy(
proxySettings.httpProxyBindAddress
?: ProxySettings.DEFAULT_HTTP_BIND_ADDRESS,
proxySettings.proxyUsername,
proxySettings.proxyPassword,
)
)
}
}
}
else -> emptyList()
}
val setting = dnsSettingsRepository.getDnsSettings()
val config = tunnelConfig.toAmConfig()
val updatedConfig =
Config.Builder()
.apply {
setInterface(config.`interface`)
addPeers(config.peers)
addProxies(proxies)
setDnsSettings(
DnsSettings(
setting.dnsProtocol == DnsProtocol.DOH,
Optional.ofNullable(setting.dnsEndpoint),
)
)
}
.build()
backend.setState(runtimeTunnel, AwgTunnel.State.UP, updatedConfig)
}
} catch (e: TimeoutCancellationException) {
Timber.e("Startup timed out for ${tunnelConfig.name} (likely DNS hang)")
errors.emit(tunnelConfig.name to BackendCoreException.DNS)
forceStopTunnel(tunnelConfig.id)
close()
} catch (e: BackendException) {
close(e.toBackendCoreException())
} catch (e: IllegalArgumentException) {
close(BackendCoreException.Config)
} catch (e: Exception) {
Timber.e(e, "Error while setting tunnel state")
close(BackendCoreException.Unknown)
}
awaitClose {
try {
backend.setState(runtimeTunnel, AwgTunnel.State.DOWN, null)
} catch (e: BackendException) {
errors.tryEmit(tunnelConfig.name to e.toBackendCoreException())
} finally {
consumerJob.cancel()
stateChannel.close()
runtimeTunnels.remove(tunnelConfig.id)
trySend(TunnelStatus.Down)
close()
}
}
}
override fun setBackendMode(backendMode: BackendMode) {
Timber.d("Setting backend mode: $backendMode")
try {
backend.backendMode = backendMode.asAmBackendMode()
} catch (e: BackendException) {
throw e.toBackendCoreException()
// TODO this should be mapped to BackendException in the lib
} catch (e: IOException) {
throw BackendCoreException.NotAuthorized
}
}
override fun getBackendMode(): BackendMode {
return backend.backendMode.asBackendMode()
}
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean {
val tunnel = runtimeTunnels[tunnelConfig.id] ?: throw BackendCoreException.ServiceNotRunning
return backend.resolveDDNS(tunnelConfig.toAmConfig(), tunnel.isIpv4ResolutionPreferred)
}
override suspend fun runningTunnelNames(): Set<String> {
return backend.runningTunnelNames
}
override fun getStatistics(tunnelId: Int): TunnelStatistics? {
return try {
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return null
AmneziaStatistics(backend.getStatistics(runtimeTunnel))
} catch (e: Exception) {
Timber.e(e, "Failed to get stats for $tunnelId")
null
}
}
override suspend fun forceStopTunnel(tunnelId: Int) {
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return
try {
backend.setState(runtimeTunnel, AwgTunnel.State.DOWN, null)
} catch (e: BackendException) {
Timber.e(e, "Force stop failed for $tunnelId")
} finally {
tunJobs[tunnelId]?.cancel()
runtimeTunnels.remove(tunnelId)
tunJobs.remove(tunnelId)
activeTuns.update { it - tunnelId }
updateTunnelStatus(tunnelId, TunnelStatus.Down)
}
}
}
@@ -1,45 +1,56 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel as WgTunnel
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.Kernel
import com.wireguard.android.backend.Tunnel
import com.wireguard.android.backend.WgQuickBackend
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
import com.zaneschepke.wireguardautotunnel.domain.events.DnsFailure
import com.zaneschepke.wireguardautotunnel.domain.events.InvalidConfig
import com.zaneschepke.wireguardautotunnel.domain.events.KernelTunnelName
import com.zaneschepke.wireguardautotunnel.domain.events.KernelWireguardNotSupported
import com.zaneschepke.wireguardautotunnel.domain.events.UnknownError
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import kotlinx.coroutines.*
import java.util.regex.Pattern
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
class KernelTunnel
@Inject
constructor(
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@Kernel private val backend: Backend,
) : BaseTunnel(applicationScope, ioDispatcher) {
class KernelTunnel(private val runConfigHelper: RunConfigHelper, private val backend: Backend) :
TunnelBackend {
private val runtimeTunnels = ConcurrentHashMap<Int, WgTunnel>()
private val runtimeTunnels = ConcurrentHashMap<Int, Tunnel>()
private fun validateWireGuardInterfaceName(name: String): Result<Unit> {
if (name.isEmpty() || name.length > 15)
return Result.failure(KernelTunnelName(R.string.kernel_name_error))
if (name == "." || name == "..") {
return Result.failure(KernelTunnelName(R.string.kernel_name_dots))
}
val pattern = Pattern.compile("^[a-zA-Z0-9_=+.-]{1,15}$")
if (!pattern.matcher(name).matches()) {
return Result.failure(KernelTunnelName(R.string.kernel_name_special_characters))
}
return Result.success(Unit)
}
// TODO Add DNS settings
override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow {
if (!tunnelConfig.isNameKernelCompatible) close(BackendCoreException.TunnelNameTooLong)
if (!WgQuickBackend.hasKernelSupport()) throw KernelWireguardNotSupported()
validateWireGuardInterfaceName(tunnelConfig.name).onFailure { throw it }
val stateChannel = Channel<WgTunnel.State>()
val stateChannel = Channel<Tunnel.State>()
val runtimeTunnel = RuntimeWgTunnel(tunnelConfig, stateChannel)
runtimeTunnels[tunnelConfig.id] = runtimeTunnel
@@ -49,36 +60,31 @@ constructor(
}
try {
withTimeout(STARTUP_TIMEOUT_MS) {
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting)
backend.setState(runtimeTunnel, WgTunnel.State.UP, tunnelConfig.toWgConfig())
}
val runConfig = runConfigHelper.buildWgRunConfig(tunnelConfig)
backend.setState(runtimeTunnel, Tunnel.State.UP, runConfig)
} catch (e: TimeoutCancellationException) {
Timber.e("Startup timed out for ${tunnelConfig.name}")
errors.emit(tunnelConfig.name to BackendCoreException.DNS)
forceStopTunnel(tunnelConfig.id)
close()
Timber.Forest.e("Startup timed out for ${tunnelConfig.name}")
throw DnsFailure()
} catch (e: BackendException) {
close(e.toBackendCoreException())
throw e.toBackendCoreException()
} catch (e: IllegalArgumentException) {
Timber.e(e, "Invalid backend arguments")
close(BackendCoreException.Config)
Timber.Forest.e(e, "Invalid backend arguments")
throw InvalidConfig()
} catch (e: Exception) {
Timber.e(e, "Error while setting tunnel state")
close(BackendCoreException.Unknown)
Timber.Forest.e(e, "Error while setting tunnel state")
throw UnknownError()
}
awaitClose {
try {
backend.setState(runtimeTunnel, WgTunnel.State.DOWN, null)
backend.setState(runtimeTunnel, Tunnel.State.DOWN, null)
} catch (e: BackendException) {
errors.tryEmit(tunnelConfig.name to e.toBackendCoreException())
// Errors are emitted by caller (lifecycle manager)
} finally {
consumerJob.cancel()
stateChannel.close()
runtimeTunnels.remove(tunnelConfig.id)
trySend(TunnelStatus.Down)
close()
}
}
}
@@ -88,13 +94,13 @@ constructor(
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return null
WireGuardStatistics(backend.getStatistics(runtimeTunnel))
} catch (e: Exception) {
Timber.e(e, "Failed to get stats for $tunnelId")
Timber.Forest.e(e, "Failed to get stats for $tunnelId")
null
}
}
override fun setBackendMode(backendMode: BackendMode) {
Timber.w("Not yet implemented for kernel")
Timber.Forest.w("Not yet implemented for kernel")
}
override fun getBackendMode(): BackendMode {
@@ -112,15 +118,11 @@ constructor(
override suspend fun forceStopTunnel(tunnelId: Int) {
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return
try {
backend.setState(runtimeTunnel, WgTunnel.State.DOWN, null)
backend.setState(runtimeTunnel, Tunnel.State.DOWN, null)
} catch (e: BackendException) {
Timber.e(e, "Force stop failed for $tunnelId")
Timber.Forest.e(e, "Force stop failed for $tunnelId")
} finally {
tunJobs[tunnelId]?.cancel()
runtimeTunnels.remove(tunnelId)
tunJobs.remove(tunnelId)
activeTuns.update { it - tunnelId }
updateTunnelStatus(tunnelId, TunnelStatus.Down)
}
}
}
@@ -0,0 +1,101 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.domain.events.InvalidConfig
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import java.util.Optional
import kotlinx.coroutines.flow.firstOrNull
import org.amnezia.awg.config.Config
import org.amnezia.awg.config.proxy.HttpProxy
import org.amnezia.awg.config.proxy.Socks5Proxy
class RunConfigHelper(
private val settingsRepository: GeneralSettingRepository,
private val proxySettingsRepository: ProxySettingsRepository,
private val dnsSettingsRepository: DnsSettingsRepository,
private val tunnelsRepository: TunnelRepository,
) {
private data class PrepResult(
val effectiveConfig: TunnelConfig,
val generalSettings: GeneralSettings,
val dnsSettings: DnsSettings,
)
private suspend fun prepare(tunnelConfig: TunnelConfig): PrepResult {
val generalSettings = settingsRepository.getGeneralSettings()
val dnsSettings = dnsSettingsRepository.getDnsSettings()
val effectiveConfig =
if (
generalSettings.isGlobalSplitTunnelEnabled || dnsSettings.isGlobalTunnelDnsEnabled
) {
val globalConfig =
tunnelsRepository.globalTunnelFlow.firstOrNull() ?: throw InvalidConfig()
tunnelConfig.copyWithGlobalValues(
globalConfig,
dnsSettings.isGlobalTunnelDnsEnabled,
generalSettings.isGlobalSplitTunnelEnabled,
)
} else {
tunnelConfig
}
return PrepResult(effectiveConfig, generalSettings, dnsSettings)
}
suspend fun buildAmRunConfig(tunnelConfig: TunnelConfig): Config {
val prep = prepare(tunnelConfig)
val proxies =
if (prep.generalSettings.appMode == AppMode.PROXY) {
val proxySettings = proxySettingsRepository.getProxySettings()
buildList {
if (proxySettings.socks5ProxyEnabled) {
add(
Socks5Proxy(
proxySettings.socks5ProxyBindAddress
?: ProxySettings.DEFAULT_SOCKS_BIND_ADDRESS,
proxySettings.proxyUsername,
proxySettings.proxyPassword,
)
)
}
if (proxySettings.httpProxyEnabled) {
add(
HttpProxy(
proxySettings.httpProxyBindAddress
?: ProxySettings.DEFAULT_HTTP_BIND_ADDRESS,
proxySettings.proxyUsername,
proxySettings.proxyPassword,
)
)
}
}
} else {
emptyList()
}
val amConfig = prep.effectiveConfig.toAmConfig()
return Config.Builder()
.setInterface(amConfig.`interface`)
.addPeers(amConfig.peers)
.addProxies(proxies)
.setDnsSettings(
org.amnezia.awg.config.DnsSettings(
prep.dnsSettings.dnsProtocol == DnsProtocol.DOH,
Optional.ofNullable(prep.dnsSettings.dnsEndpoint),
)
)
.build()
}
suspend fun buildWgRunConfig(tunnelConfig: TunnelConfig): com.wireguard.config.Config {
val prep = prepare(tunnelConfig)
return prep.effectiveConfig.toWgConfig()
}
}
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import kotlinx.coroutines.channels.Channel
@@ -16,4 +16,6 @@ class RuntimeAwgTunnel(
}
override fun isIpv4ResolutionPreferred() = tunnelConfig.isIpv4Preferred
override fun isMetered() = tunnelConfig.isMetered
}
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
@@ -0,0 +1,23 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import kotlinx.coroutines.flow.Flow
interface TunnelBackend {
fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus>
fun getStatistics(tunnelId: Int): TunnelStatistics?
fun setBackendMode(backendMode: BackendMode)
fun getBackendMode(): BackendMode
fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean
suspend fun runningTunnelNames(): Set<String>
suspend fun forceStopTunnel(tunnelId: Int)
}
@@ -0,0 +1,119 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel.backend
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.DnsFailure
import com.zaneschepke.wireguardautotunnel.domain.events.InvalidConfig
import com.zaneschepke.wireguardautotunnel.domain.events.ServiceNotRunning
import com.zaneschepke.wireguardautotunnel.domain.events.UnknownError
import com.zaneschepke.wireguardautotunnel.domain.events.VpnUnauthorized
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendMode
import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendMode
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException
import java.io.IOException
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.BackendException
import org.amnezia.awg.backend.Tunnel
import timber.log.Timber
class UserspaceTunnel(private val backend: Backend, private val runConfigHelper: RunConfigHelper) :
TunnelBackend {
private val runtimeTunnels = ConcurrentHashMap<Int, Tunnel>()
override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow {
val stateChannel = Channel<Tunnel.State>()
val runtimeTunnel = RuntimeAwgTunnel(tunnelConfig, stateChannel)
runtimeTunnels[tunnelConfig.id] = runtimeTunnel
val consumerJob = launch {
stateChannel.consumeAsFlow().collect { awgState -> trySend(awgState.asTunnelState()) }
}
try {
val runConfig = runConfigHelper.buildAmRunConfig(tunnelConfig)
backend.setState(runtimeTunnel, Tunnel.State.UP, runConfig)
} catch (_: TimeoutCancellationException) {
Timber.e("Startup timed out for ${tunnelConfig.name} (likely DNS hang)")
throw DnsFailure()
} catch (e: BackendException) {
throw e.toBackendCoreException()
} catch (_: IllegalArgumentException) {
throw InvalidConfig()
} catch (e: Exception) {
Timber.e(e, "Error while setting tunnel state")
throw UnknownError()
}
awaitClose {
try {
backend.setState(runtimeTunnel, Tunnel.State.DOWN, null)
} catch (e: BackendException) {
// Errors emitted by caller
} finally {
consumerJob.cancel()
stateChannel.close()
runtimeTunnels.remove(tunnelConfig.id)
trySend(TunnelStatus.Down)
}
}
}
override fun setBackendMode(backendMode: BackendMode) {
Timber.d("Setting backend mode: $backendMode")
try {
backend.backendMode = backendMode.asAmBackendMode()
} catch (e: BackendException) {
throw e.toBackendCoreException()
} catch (_: IOException) {
throw VpnUnauthorized()
}
}
override fun getBackendMode(): BackendMode {
return backend.backendMode.asBackendMode()
}
override fun handleDnsReresolve(tunnelConfig: TunnelConfig): Boolean {
val tunnel = runtimeTunnels[tunnelConfig.id] ?: throw ServiceNotRunning()
return backend.resolveDDNS(tunnelConfig.toAmConfig(), tunnel.isIpv4ResolutionPreferred)
}
override suspend fun runningTunnelNames(): Set<String> {
return backend.runningTunnelNames
}
override fun getStatistics(tunnelId: Int): TunnelStatistics? {
return try {
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return null
AmneziaStatistics(backend.getStatistics(runtimeTunnel))
} catch (e: Exception) {
Timber.e(e, "Failed to get stats for $tunnelId")
null
}
}
override suspend fun forceStopTunnel(tunnelId: Int) {
val runtimeTunnel = runtimeTunnels[tunnelId] ?: return
try {
backend.setState(runtimeTunnel, Tunnel.State.DOWN, null)
} catch (e: BackendException) {
Timber.e(e, "Force stop failed for $tunnelId")
} finally {
runtimeTunnels.remove(tunnelId)
}
}
}
@@ -0,0 +1,114 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel.handler
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
class DynamicDnsHandler(
private val activeTunnels: StateFlow<Map<Int, TunnelState>>,
private val tunnelsRepository: TunnelRepository,
private val settingsRepository: GeneralSettingRepository,
private val localMessageEvents: MutableSharedFlow<Pair<String?, BackendMessage>>,
private val handleDnsReresolve: (TunnelConfig) -> Boolean,
private val applicationScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher,
) {
private val mutex = Mutex()
private val jobs = ConcurrentHashMap<Int, Job>()
init {
applicationScope.launch(ioDispatcher) {
combine(activeTunnels, settingsRepository.flow.filterNotNull()) { active, settings ->
active to settings
}
.collect { (activeTuns, settings) ->
mutex.withLock {
val activeIds =
activeTuns.keys
.filter { id ->
val config =
tunnelsRepository.getById(id) ?: return@filter false
config.restartOnPingFailure &&
settings.appMode != AppMode.KERNEL
}
.toSet()
(jobs.keys - activeIds).forEach { id ->
Timber.d("Shutting down Dynamic DNS monitoring job for tunnelId: $id")
jobs.remove(id)?.cancel()
}
activeIds.forEach { id ->
if (jobs.containsKey(id)) return@forEach
val config = tunnelsRepository.getById(id) ?: return@forEach
val tunStateFlow =
activeTunnels
.map { it[id] }
.stateIn(applicationScope + ioDispatcher)
Timber.d("Starting Dynamic DNS monitoring job for tunnelId: $id")
jobs[id] =
applicationScope.launch(ioDispatcher) {
monitorDynamicDns(config, tunStateFlow)
}
}
}
}
}
}
private suspend fun monitorDynamicDns(
config: TunnelConfig,
tunStateFlow: StateFlow<TunnelState?>,
) {
var backoff = BASE_BACKOFF
while (true) {
val state = tunStateFlow.value ?: break
if (state.health() != TunnelState.Health.UNHEALTHY) {
backoff = BASE_BACKOFF
tunStateFlow.first { it?.health() == TunnelState.Health.UNHEALTHY || it == null }
continue
}
runCatching {
val updated = handleDnsReresolve(config)
if (updated) {
localMessageEvents.emit(config.name to BackendMessage.DynamicDnsSuccess)
backoff = BASE_BACKOFF
} else {
Timber.i(
"Dynamic DNS check completed, current endpoint address is already up to date."
)
}
}
.onFailure { Timber.e(it, "Failed to handle dns re-resolution for ${config.name}") }
delay(backoff)
backoff = (backoff * 1.5).toLong().coerceAtMost(MAX_BACKOFF_TIME)
}
}
companion object {
const val BASE_BACKOFF = 30_000L
const val MAX_BACKOFF_TIME = 300_000L
}
}
@@ -0,0 +1,47 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel.handler
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
class TunnelActiveStatePersister(
private val activeTunnels: StateFlow<Map<Int, TunnelState>>,
private val tunnelsRepository: TunnelRepository,
applicationScope: CoroutineScope,
ioDispatcher: CoroutineDispatcher,
) {
private var previousActiveIds: Set<Int> = emptySet()
init {
applicationScope.launch(ioDispatcher) {
activeTunnels.collect { currentActive ->
val currentActiveIds = currentActive.keys
if (currentActiveIds == previousActiveIds) return@collect
val tunnels = tunnelsRepository.userTunnelsFlow.firstOrNull() ?: return@collect
val tunnelsById = tunnels.associateBy { it.id }
val relevantIds = previousActiveIds + currentActiveIds
supervisorScope {
relevantIds.forEach { id ->
launch {
val config = tunnelsById[id] ?: return@launch
val wasActive = previousActiveIds.contains(id)
val isActive = currentActiveIds.contains(id)
if (wasActive != isActive) {
tunnelsRepository.save(config.copy(isActive = isActive))
}
}
}
}
previousActiveIds = currentActiveIds.toSet()
}
}
}
}
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
package com.zaneschepke.wireguardautotunnel.core.tunnel.handler
import android.os.PowerManager
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
@@ -8,34 +9,107 @@ import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.*
import com.zaneschepke.wireguardautotunnel.domain.state.FailureReason
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
import dagger.hilt.android.scopes.ServiceScoped
import inet.ipaddr.AddressValueException
import inet.ipaddr.IPAddress
import inet.ipaddr.IPAddressString
import io.ktor.util.collections.*
import javax.inject.Inject
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import io.ktor.util.collections.ConcurrentMap
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeout
import timber.log.Timber
@ServiceScoped
class TunnelMonitor
@Inject
constructor(
private val settingsRepository: GeneralSettingRepository,
class TunnelMonitorHandler(
private val activeTunnels: StateFlow<Map<Int, TunnelState>>,
private val tunnelsRepository: TunnelRepository,
private val settingsRepository: GeneralSettingRepository,
private val monitoringSettingsRepository: MonitoringSettingsRepository,
private val networkMonitor: NetworkMonitor,
private val networkUtils: NetworkUtils,
private val logReader: LogReader,
private val powerManager: PowerManager,
private val getStatistics: (Int) -> TunnelStatistics?,
private val updateTunnelStatus:
suspend (
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
) -> Unit,
private val applicationScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher,
) {
private val mutex = Mutex()
private val jobs = ConcurrentHashMap<Int, Job>()
init {
applicationScope.launch(ioDispatcher) {
activeTunnels.collect { activeTuns ->
mutex.withLock {
val activeIds = activeTuns.keys.toSet()
(jobs.keys - activeIds).forEach { id ->
Timber.d("Shutting down tunnel monitoring job for tunnelId: $id")
jobs.remove(id)?.cancel()
}
val tunnels = tunnelsRepository.flow.firstOrNull() ?: return@collect
val tunnelsById = tunnels.associateBy { it.id }
activeIds.forEach { id ->
if (jobs.containsKey(id)) return@forEach
val config = tunnelsById[id] ?: return@forEach
val settings = settingsRepository.flow.filterNotNull().first()
val tunStateFlow =
activeTunnels.map { it[id] }.stateIn(applicationScope + ioDispatcher)
jobs[id] =
applicationScope.launch(ioDispatcher) {
Timber.d("Starting tunnel monitoring job for tunnelId: $id")
startMonitoring(
config = config,
withLogs = settings.appMode != AppMode.KERNEL,
tunStateFlow = tunStateFlow,
getStatistics = { tunnelId -> getStatistics(tunnelId) },
updateTunnelStatus = { tid, _, stats, pings, logHealth ->
updateTunnelStatus(tid, null, stats, pings, logHealth)
},
)
}
}
}
}
}
}
@OptIn(FlowPreview::class)
suspend fun startMonitoring(
tunnelId: Int,
private suspend fun startMonitoring(
config: TunnelConfig,
withLogs: Boolean,
tunStateFlow: StateFlow<TunnelState?>,
getStatistics: suspend (Int) -> TunnelStatistics?,
@@ -43,13 +117,10 @@ constructor(
suspend (
Int, TunnelStatus?, TunnelStatistics?, Map<String, PingState>?, LogHealthState?,
) -> Unit,
): Job = coroutineScope {
launch {
val config = tunnelsRepository.getById(tunnelId) ?: return@launch
launch { startPingMonitor(config, tunStateFlow, updateTunnelStatus) }
launch { startWgStatsPoll(tunnelId, getStatistics, updateTunnelStatus) }
if (withLogs) launch { startLogsMonitor(config, updateTunnelStatus) }
}
) = coroutineScope {
launch { startPingMonitor(config, tunStateFlow, updateTunnelStatus) }
launch { startWgStatsPoll(config.id, getStatistics, updateTunnelStatus) }
if (withLogs) launch { startLogsMonitor(config, updateTunnelStatus) }
}
private suspend fun startLogsMonitor(
@@ -74,7 +145,7 @@ constructor(
else -> null
}
}
.distinctUntilChangedBy { it.isHealthy } // Only emit when health changes
.distinctUntilChangedBy { it.isHealthy }
.collect { logHealthState ->
Timber.d("Tunnel log health updated for ${tunnelConfig.name}: $logHealthState")
updateTunnelStatus(tunnelConfig.id, null, null, null, logHealthState)
@@ -93,26 +164,7 @@ constructor(
val connectivityStateFlow = networkMonitor.connectivityStateFlow.stateIn(this)
val isNetworkConnected = connectivityStateFlow.map { it.hasConnectivity() }.stateIn(this)
data class NetworkChangeKey(
val ethernetConnected: Boolean,
val wifiConnected: Boolean,
val cellularConnected: Boolean,
val wifiSsid: String?,
)
connectivityStateFlow
.map {
NetworkChangeKey(
ethernetConnected = it.ethernetConnected,
wifiConnected = it.wifiState.connected,
cellularConnected = it.cellularConnected,
wifiSsid = if (it.wifiState.connected) it.wifiState.ssid else null,
)
}
.distinctUntilChanged()
.stateIn(this)
val isNetworkConnected = connectivityStateFlow.map { it.hasInternet() }.stateIn(this)
combine(
settingsRepository.flow.distinctUntilChangedBy { it.appMode },
@@ -199,6 +251,7 @@ constructor(
}
val attemptTime = System.currentTimeMillis()
val timeout = settings.tunnelPingTimeoutSeconds?.toMillis() ?: 5000L
runCatching {
withTimeout(
settings.tunnelPingTimeoutSeconds?.toMillis() ?: 5000L
@@ -263,27 +316,35 @@ constructor(
}
// Wait for the tunnel to be fully active
tunStateFlow.filter { state -> state?.status == TunnelStatus.Up }.first()
tunStateFlow.filter { state -> state?.status is TunnelStatus.Up }.first()
// small delay to make sure tunnel is fully up before we actively monitor
delay(3_000L)
delay(PING_MONITOR_START_DELAY)
while (isActive) {
ensureActive()
if (isNetworkConnected.value) {
performPing()
} else {
pingStatsFlow.update { current ->
current.mapValues { entry ->
entry.value.copy(
isReachable = false,
failureReason = FailureReason.NoConnectivity,
lastPingAttemptMillis = System.currentTimeMillis(),
)
if (!powerManager.isDeviceIdleMode) {
if (isNetworkConnected.value) {
performPing()
} else {
pingStatsFlow.update { current ->
current.mapValues { entry ->
entry.value.copy(
isReachable = false,
failureReason = FailureReason.NoConnectivity,
lastPingAttemptMillis = System.currentTimeMillis(),
)
}
}
ensureActive()
updateTunnelStatus(
tunnelConfig.id,
null,
null,
pingStatsFlow.value,
null,
)
}
ensureActive()
updateTunnelStatus(tunnelConfig.id, null, null, pingStatsFlow.value, null)
}
delay(settings.tunnelPingIntervalSeconds.toMillis())
}
@@ -308,7 +369,6 @@ constructor(
}
companion object {
private val successLogRegex =
Regex("Received handshake response|Receiving keepalive packet", RegexOption.IGNORE_CASE)
@@ -323,5 +383,6 @@ constructor(
const val CLOUDFLARE_IPV6_IP = "2606:4700:4700::1111"
const val CLOUDFLARE_IPV4_IP = "1.1.1.1"
const val STATS_DELAY = 1_000L
const val PING_MONITOR_START_DELAY = 5_000L
}
}
@@ -0,0 +1,36 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel.handler
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import timber.log.Timber
class TunnelServiceHandler(
private val activeTunnels: StateFlow<Map<Int, TunnelState>>,
private val settingsRepository: GeneralSettingRepository,
private val serviceManager: ServiceManager,
applicationScope: CoroutineScope,
ioDispatcher: CoroutineDispatcher,
) {
init {
applicationScope.launch(ioDispatcher) {
activeTunnels.collect { activeTuns ->
if (activeTuns.isEmpty()) {
Timber.d("Stopping tunnel service, no tunnels active.")
serviceManager.stopTunnelService()
} else if (serviceManager.tunnelService.value == null) {
val settings = settingsRepository.flow.firstOrNull() ?: GeneralSettings()
Timber.d("Starting tunnel foreground service for active tunnel.")
serviceManager.startTunnelService(settings.appMode)
}
serviceManager.updateTunnelTile()
}
}
}
}
@@ -1,31 +1,25 @@
package com.zaneschepke.wireguardautotunnel.core.worker
import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.*
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.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import timber.log.Timber
@HiltWorker
class ServiceWorker
@AssistedInject
constructor(
@Assisted private val context: Context,
@Assisted private val params: WorkerParameters,
class ServiceWorker(
context: Context,
params: WorkerParameters,
private val serviceManager: ServiceManager,
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : CoroutineWorker(context, params) {
companion object {
private const val TAG = "service_worker"
private const val TAG = "auto_tunnel_service_monitor"
fun stop(context: Context) {
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
@@ -47,16 +41,15 @@ constructor(
}
}
override suspend fun doWork(): Result =
withContext(ioDispatcher) {
Timber.i("Service worker started")
with(autoTunnelSettingsRepository.getAutoTunnelSettings()) {
Timber.i("Checking to see if auto-tunnel has been killed by system")
if (isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null) {
Timber.i("Service has been killed by system, restoring.")
serviceManager.startAutoTunnelService()
}
override suspend fun doWork(): Result {
Timber.i("Service worker started")
with(autoTunnelSettingsRepository.getAutoTunnelSettings()) {
Timber.i("Checking to see if auto-tunnel has been killed by system")
if (isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null) {
Timber.i("Service has been killed by system, restoring.")
serviceManager.startAutoTunnelService()
}
Result.success()
return Result.success()
}
}
}
@@ -15,8 +15,9 @@ import com.zaneschepke.wireguardautotunnel.data.entity.*
AutoTunnelSettings::class,
MonitoringSettings::class,
DnsSettings::class,
LockdownSettings::class,
],
version = 25,
version = 29,
autoMigrations =
[
AutoMigration(from = 1, to = 2),
@@ -42,6 +43,8 @@ import com.zaneschepke.wireguardautotunnel.data.entity.*
AutoMigration(from = 21, to = 22),
AutoMigration(from = 22, to = 23),
AutoMigration(from = 24, to = 25),
AutoMigration(from = 26, to = 27, spec = GlobalsMigration::class),
AutoMigration(from = 27, to = 28, spec = DonationMigration::class),
],
exportSchema = true,
)
@@ -57,6 +60,8 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun monitoringSettingsDao(): MonitoringSettingsDao
abstract fun lockdownSettingsDao(): LockdownSettingsDao
abstract fun dnsSettingsDao(): DnsSettingsDao
}
@@ -112,3 +117,15 @@ class FixProxySettingsMigration : AutoMigrationSpec {
}
}
}
@RenameColumn.Entries(
RenameColumn(
tableName = "general_settings",
fromColumnName = "is_tunnel_globals_enabled",
toColumnName = "global_split_tunnel_enabled",
)
)
class GlobalsMigration : AutoMigrationSpec
@DeleteColumn(tableName = "general_settings", columnName = "custom_split_packages")
class DonationMigration : AutoMigrationSpec
@@ -5,7 +5,6 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStore
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import java.io.IOException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
@@ -17,7 +16,7 @@ import timber.log.Timber
class DataStoreManager(
private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val ioDispatcher: CoroutineDispatcher,
) {
private val preferencesKey = "preferences"
val Context.dataStore by preferencesDataStore(name = preferencesKey)
@@ -26,6 +25,7 @@ class DataStoreManager(
companion object {
val locationDisclosureShown = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
val batteryDisableShown = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
val shouldShowDonationSnackbar = booleanPreferencesKey("SHOW_DONATION_SNACK")
}
suspend fun init() {
@@ -2,12 +2,9 @@ package com.zaneschepke.wireguardautotunnel.data
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import javax.inject.Inject
import javax.inject.Provider
import timber.log.Timber
class DatabaseCallback @Inject constructor(private val databaseProvider: Provider<AppDatabase>) :
RoomDatabase.Callback() {
class DatabaseCallback(private val databaseProvider: Lazy<AppDatabase>) : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
Timber.d("Database created, inserting default rows")
@@ -4,6 +4,7 @@ import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import kotlinx.coroutines.flow.Flow
@Dao
@@ -15,4 +16,16 @@ interface GeneralSettingsDao {
@Query("SELECT * FROM general_settings LIMIT 1")
fun getGeneralSettingsFlow(): Flow<GeneralSettings?>
@Query("UPDATE general_settings SET theme = :theme WHERE id = 1")
suspend fun updateTheme(theme: String)
@Query("UPDATE general_settings SET locale = :locale WHERE id = 1")
suspend fun updateLocale(locale: String)
@Query("UPDATE general_settings SET is_pin_lock_enabled = :enabled WHERE id = 1")
suspend fun updatePinLockEnabled(enabled: Boolean)
@Query("UPDATE general_settings SET app_mode = :appMode WHERE id = 1")
suspend fun updateAppMode(appMode: AppMode)
}
@@ -0,0 +1,18 @@
package com.zaneschepke.wireguardautotunnel.data.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import com.zaneschepke.wireguardautotunnel.data.entity.LockdownSettings
import kotlinx.coroutines.flow.Flow
@Dao
interface LockdownSettingsDao {
@Query("SELECT * FROM lockdown_settings LIMIT 1")
suspend fun getLockdownSettings(): LockdownSettings?
@Upsert suspend fun upsert(lockdownSettings: LockdownSettings)
@Query("SELECT * FROM lockdown_settings LIMIT 1")
fun getLockdownSettingsFlow(): Flow<LockdownSettings?>
}
@@ -50,23 +50,27 @@ interface TunnelConfigDao {
@Query(
"""
SELECT * FROM tunnel_config
ORDER BY
CASE WHEN is_primary_tunnel = 1 THEN 0 ELSE 1 END,
position ASC
LIMIT 1"""
SELECT * FROM tunnel_config
WHERE name != '${TunnelConfig.GLOBAL_CONFIG_NAME}'
ORDER BY
CASE WHEN is_primary_tunnel = 1 THEN 0 ELSE 1 END,
position ASC
LIMIT 1
"""
)
suspend fun getDefaultTunnel(): TunnelConfig?
@Query(
"""
SELECT * FROM tunnel_config
ORDER BY
CASE WHEN is_Active = 1 THEN 0
WHEN is_primary_tunnel = 1 THEN 1
ELSE 2 END,
position ASC
LIMIT 1"""
SELECT * FROM tunnel_config
WHERE name != '${TunnelConfig.GLOBAL_CONFIG_NAME}'
ORDER BY
CASE WHEN is_Active = 1 THEN 0
WHEN is_primary_tunnel = 1 THEN 1
ELSE 2 END,
position ASC
LIMIT 1
"""
)
suspend fun getStartTunnel(): TunnelConfig?
@@ -3,4 +3,5 @@ package com.zaneschepke.wireguardautotunnel.data.entity
data class AppState(
val isLocationDisclosureShown: Boolean = false,
val isBatteryOptimizationDisableShown: Boolean = false,
val shouldShowDonationSnackbar: Boolean = false,
)
@@ -11,4 +11,6 @@ data class DnsSettings(
@ColumnInfo(name = "dns_protocol", defaultValue = "0")
val dnsProtocol: DnsProtocol = DnsProtocol.fromValue(0),
@ColumnInfo(name = "dns_endpoint") val dnsEndpoint: String? = null,
@ColumnInfo(name = "global_tunnel_dns_enabled", defaultValue = "0")
val isGlobalTunnelDnsEnabled: Boolean = false,
)
@@ -14,8 +14,8 @@ data class GeneralSettings(
val isRestoreOnBootEnabled: Boolean = false,
@ColumnInfo(name = "is_multi_tunnel_enabled", defaultValue = "0")
val isMultiTunnelEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_globals_enabled", defaultValue = "0")
val isTunnelGlobalsEnabled: Boolean = false,
@ColumnInfo(name = "global_split_tunnel_enabled", defaultValue = "0")
val isGlobalSplitTunnelEnabled: Boolean = false,
@ColumnInfo(name = "app_mode", defaultValue = "0") val appMode: AppMode = AppMode.fromValue(0),
@ColumnInfo(name = "theme", defaultValue = "AUTOMATIC") val theme: String = "AUTOMATIC",
@ColumnInfo(name = "locale") val locale: String? = null,
@@ -26,8 +26,5 @@ data class GeneralSettings(
val isPinLockEnabled: Boolean = false,
@ColumnInfo(name = "is_always_on_vpn_enabled", defaultValue = "0")
val isAlwaysOnVpnEnabled: Boolean = false,
@ColumnInfo(name = "is_lan_on_kill_switch_enabled", defaultValue = "0")
val isLanOnKillSwitchEnabled: Boolean = false,
@ColumnInfo(name = "custom_split_packages", defaultValue = "{}")
val customSplitPackages: Map<String, String> = emptyMap(),
@ColumnInfo(name = "already_donated", defaultValue = "0") val alreadyDonated: Boolean = false,
)
@@ -0,0 +1,13 @@
package com.zaneschepke.wireguardautotunnel.data.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "lockdown_settings")
data class LockdownSettings(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo(name = "bypass_lan", defaultValue = "0") val bypassLan: Boolean = false,
@ColumnInfo(name = "metered", defaultValue = "0") val metered: Boolean = false,
@ColumnInfo(name = "dual_stack", defaultValue = "0") val dualStack: Boolean = false,
)
@@ -28,8 +28,8 @@ data class TunnelConfig(
@ColumnInfo(name = "position", defaultValue = "0") val position: Int = 0,
@ColumnInfo(name = "auto_tunnel_apps", defaultValue = "[]")
val autoTunnelApps: Set<String> = emptySet(),
@ColumnInfo(name = "is_metered", defaultValue = "false") val isMetered: Boolean = false,
) {
companion object {
const val GLOBAL_CONFIG_NAME = "4675ab06-903a-438b-8485-6ea4187a9512"
}
@@ -4,7 +4,17 @@ import com.zaneschepke.wireguardautotunnel.data.entity.DnsSettings as Entity
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings as Domain
fun Entity.toDomain(): Domain =
Domain(id = id, dnsProtocol = dnsProtocol, dnsEndpoint = dnsEndpoint)
Domain(
id = id,
dnsProtocol = dnsProtocol,
dnsEndpoint = dnsEndpoint,
isGlobalTunnelDnsEnabled = isGlobalTunnelDnsEnabled,
)
fun Domain.toEntity(): Entity =
Entity(id = id, dnsProtocol = dnsProtocol, dnsEndpoint = dnsEndpoint)
Entity(
id = id,
dnsProtocol = dnsProtocol,
dnsEndpoint = dnsEndpoint,
isGlobalTunnelDnsEnabled = isGlobalTunnelDnsEnabled,
)
@@ -7,4 +7,5 @@ fun Entity.toDomain(): Domain =
Domain(
isLocationDisclosureShown = isLocationDisclosureShown,
isBatteryOptimizationDisableShown = isBatteryOptimizationDisableShown,
shouldShowDonationSnackbar = shouldShowDonationSnackbar,
)
@@ -0,0 +1,10 @@
package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.LockdownSettings as Entity
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings as Domain
fun Entity.toDomain(): Domain =
Domain(id = id, bypassLan = bypassLan, metered = metered, dualStack = dualStack)
fun Domain.toEntity(): Entity =
Entity(id = id, bypassLan = bypassLan, metered = metered, dualStack = dualStack)
@@ -10,7 +10,7 @@ fun Entity.toDomain(): Domain =
isShortcutsEnabled = isShortcutsEnabled,
isRestoreOnBootEnabled = isRestoreOnBootEnabled,
isMultiTunnelEnabled = isMultiTunnelEnabled,
isTunnelGlobalsEnabled = isTunnelGlobalsEnabled,
isGlobalSplitTunnelEnabled = isGlobalSplitTunnelEnabled,
appMode = appMode,
theme = Theme.valueOf(theme.uppercase()),
locale = locale,
@@ -18,8 +18,7 @@ fun Entity.toDomain(): Domain =
isRemoteControlEnabled = isRemoteControlEnabled,
isPinLockEnabled = isPinLockEnabled,
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
isLanOnKillSwitchEnabled = isLanOnKillSwitchEnabled,
customSplitPackages = customSplitPackages,
alreadyDonated = alreadyDonated,
)
fun Domain.toEntity(): Entity =
@@ -28,7 +27,7 @@ fun Domain.toEntity(): Entity =
isShortcutsEnabled = isShortcutsEnabled,
isRestoreOnBootEnabled = isRestoreOnBootEnabled,
isMultiTunnelEnabled = isMultiTunnelEnabled,
isTunnelGlobalsEnabled = isTunnelGlobalsEnabled,
isGlobalSplitTunnelEnabled = isGlobalSplitTunnelEnabled,
appMode = appMode,
theme = theme.name,
locale = locale,
@@ -36,6 +35,5 @@ fun Domain.toEntity(): Entity =
isRemoteControlEnabled = isRemoteControlEnabled,
isPinLockEnabled = isPinLockEnabled,
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
isLanOnKillSwitchEnabled = isLanOnKillSwitchEnabled,
customSplitPackages = customSplitPackages,
alreadyDonated = alreadyDonated,
)
@@ -19,6 +19,7 @@ fun Entity.toDomain(): Domain =
isIpv4Preferred = isIpv4Preferred,
position = position,
autoTunnelApps = autoTunnelApps,
isMetered = isMetered,
)
fun Domain.toEntity(): Entity =
@@ -37,4 +38,5 @@ fun Domain.toEntity(): Entity =
isIpv4Preferred = isIpv4Preferred,
position = position,
autoTunnelApps = autoTunnelApps,
isMetered = isMetered,
)
@@ -316,3 +316,151 @@ val MIGRATION_23_24 =
}
}
}
val MIGRATION_25_26 =
object : Migration(25, 26) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS `lockdown_settings` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`bypass_lan` INTEGER NOT NULL DEFAULT 0,
`metered` INTEGER NOT NULL DEFAULT 0,
`dual_stack` INTEGER NOT NULL DEFAULT 0
)
"""
.trimIndent()
)
val cursor =
db.query("SELECT `is_lan_on_kill_switch_enabled` FROM `general_settings` LIMIT 1")
var bypassLan = 0
if (cursor.moveToFirst()) {
bypassLan = if (cursor.getInt(0) != 0) 1 else 0
}
cursor.close()
db.execSQL(
"""
INSERT INTO `lockdown_settings` (`bypass_lan`, `metered`, `dual_stack`)
VALUES (?, 0, 0)
"""
.trimIndent(),
arrayOf(bypassLan),
)
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS `general_settings_new` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0,
`is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0,
`is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT 0,
`is_tunnel_globals_enabled` INTEGER NOT NULL DEFAULT 0,
`app_mode` INTEGER NOT NULL DEFAULT 0,
`theme` TEXT NOT NULL DEFAULT 'AUTOMATIC',
`locale` TEXT,
`remote_key` TEXT,
`is_remote_control_enabled` INTEGER NOT NULL DEFAULT 0,
`is_pin_lock_enabled` INTEGER NOT NULL DEFAULT 0,
`is_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0,
`custom_split_packages` TEXT NOT NULL DEFAULT '{}'
)
"""
.trimIndent()
)
db.execSQL(
"""
INSERT INTO `general_settings_new` (
`id`,
`is_shortcuts_enabled`,
`is_restore_on_boot_enabled`,
`is_multi_tunnel_enabled`,
`is_tunnel_globals_enabled`,
`app_mode`,
`theme`,
`locale`,
`remote_key`,
`is_remote_control_enabled`,
`is_pin_lock_enabled`,
`is_always_on_vpn_enabled`,
`custom_split_packages`
)
SELECT
`id`,
`is_shortcuts_enabled`,
`is_restore_on_boot_enabled`,
`is_multi_tunnel_enabled`,
`is_tunnel_globals_enabled`,
`app_mode`,
`theme`,
`locale`,
`remote_key`,
`is_remote_control_enabled`,
`is_pin_lock_enabled`,
`is_always_on_vpn_enabled`,
`custom_split_packages`
FROM `general_settings`
"""
.trimIndent()
)
db.execSQL("DROP TABLE `general_settings`")
db.execSQL("ALTER TABLE `general_settings_new` RENAME TO `general_settings`")
}
}
val MIGRATION_28_29 =
object : Migration(28, 29) {
override fun migrate(database: SupportSQLiteDatabase) {
// Migrate tunnel_config table
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `tunnel_config_new` (
`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,
`restart_on_ping_failure` INTEGER NOT NULL DEFAULT false,
`ping_target` TEXT DEFAULT null,
`is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false,
`is_ipv4_preferred` INTEGER NOT NULL DEFAULT true,
`position` INTEGER NOT NULL DEFAULT 0,
`auto_tunnel_apps` TEXT NOT NULL DEFAULT '[]',
`is_metered` INTEGER NOT NULL DEFAULT false
)
"""
.trimIndent()
)
database.execSQL(
"""
INSERT INTO `tunnel_config_new` (
`id`, `name`, `wg_quick`, `tunnel_networks`, `is_mobile_data_tunnel`,
`is_primary_tunnel`, `am_quick`, `is_Active`, `restart_on_ping_failure`,
`ping_target`, `is_ethernet_tunnel`, `is_ipv4_preferred`, `position`,
`auto_tunnel_apps`, `is_metered`
)
SELECT
`id`, `name`, `wg_quick`, `tunnel_networks`, `is_mobile_data_tunnel`,
`is_primary_tunnel`, `am_quick`, `is_Active`, `restart_on_ping_failure`,
`ping_target`, `is_ethernet_tunnel`, `is_ipv4_preferred`, `position`,
`auto_tunnel_apps`, 0 AS `is_metered`
FROM `tunnel_config`
"""
.trimIndent()
)
database.execSQL("DROP TABLE `tunnel_config`")
database.execSQL("ALTER TABLE `tunnel_config_new` RENAME TO `tunnel_config`")
database.execSQL(
"CREATE UNIQUE INDEX IF NOT EXISTS `index_tunnel_config_name` ON `tunnel_config` (`name`)"
)
}
}
@@ -20,10 +20,7 @@ enum class DnsProtocol(val value: Int) {
}
}
data class DnsSettings(
val protocol: DnsProtocol = DnsProtocol.SYSTEM,
val endpoint: String? = null,
)
data class DnsSettings(val protocol: DnsProtocol = DnsProtocol.SYSTEM, val endpoint: String? = null)
enum class DnsProvider(private val systemAddress: String, private val dohAddress: String) {
CLOUDFLARE("1.1.1.1", "https://1.1.1.1/dns-query"),
@@ -3,8 +3,6 @@ package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.entity.AppState as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.AppState as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import kotlinx.coroutines.CoroutineDispatcher
@@ -18,8 +16,8 @@ import timber.log.Timber
class DataStoreAppStateRepository(
private val dataStoreManager: DataStoreManager,
@ApplicationScope private val applicationScope: CoroutineScope,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
applicationScope: CoroutineScope,
ioDispatcher: CoroutineDispatcher,
) : AppStateRepository {
override suspend fun isLocationDisclosureShown(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.locationDisclosureShown) ?: false
@@ -37,6 +35,14 @@ class DataStoreAppStateRepository(
dataStoreManager.saveToDataStore(DataStoreManager.batteryDisableShown, shown)
}
override suspend fun setShouldShowDonationSnackbar(show: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.shouldShowDonationSnackbar, show)
}
override suspend fun shouldShowDonationSnackbar(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.shouldShowDonationSnackbar) ?: false
}
override val flow: Flow<Domain> =
dataStoreManager.preferencesFlow
.map { prefs ->
@@ -47,6 +53,8 @@ class DataStoreAppStateRepository(
pref[DataStoreManager.locationDisclosureShown] ?: false,
isBatteryOptimizationDisableShown =
pref[DataStoreManager.batteryDisableShown] ?: false,
shouldShowDonationSnackbar =
pref[DataStoreManager.shouldShowDonationSnackbar] ?: false,
)
} catch (e: IllegalArgumentException) {
Timber.e(e)
@@ -4,16 +4,17 @@ import android.content.Context
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.data.mapper.GitHubReleaseMapper
import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.AppUpdate
import com.zaneschepke.wireguardautotunnel.domain.repository.UpdateRepository
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.utils.io.*
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsChannel
import io.ktor.http.contentLength
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.readAvailable
import java.io.File
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
@@ -25,8 +26,9 @@ class GitHubUpdateRepository(
private val githubOwner: String,
private val githubRepo: String,
private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val ioDispatcher: CoroutineDispatcher,
) : UpdateRepository {
override suspend fun checkForUpdate(currentVersion: String): Result<AppUpdate?> =
withContext(ioDispatcher) {
Timber.i("Checking for update")
@@ -38,27 +40,40 @@ class GitHubUpdateRepository(
gitHubApi.getLatestRelease(githubOwner, githubRepo).onFailure(Timber::e)
}
release.map { release ->
val standaloneApkAsset =
val universalApkAsset =
release.assets.find { asset ->
asset.name.startsWith("wgtunnel-${Constants.STANDALONE_FLAVOR}-v") &&
asset.name.endsWith(".apk")
val prefix = "wgtunnel-${Constants.STANDALONE_FLAVOR}-v"
val apkSuffix = ".apk"
asset.name.startsWith(prefix) &&
asset.name.endsWith(apkSuffix) &&
!asset.name.endsWith("-arm64$apkSuffix") &&
!asset.name.endsWith("-armv7$apkSuffix")
}
val newVersion =
standaloneApkAsset
universalApkAsset
?.name
?.removePrefix("wgtunnel-${Constants.STANDALONE_FLAVOR}-v")
?.removeSuffix(".apk") ?: return@map null
Timber.i("Latest version: $newVersion, current version: $currentVersion")
if (isNightly && newVersion != currentVersion)
return@map GitHubReleaseMapper.toAppUpdate(release, newVersion)
if (NumberUtils.compareVersions(newVersion, currentVersion) > 0) {
GitHubReleaseMapper.toAppUpdate(
release.copy(assets = listOf(standaloneApkAsset)),
newVersion,
)
if (isNightly) {
if (newVersion != currentVersion) {
GitHubReleaseMapper.toAppUpdate(
release.copy(assets = listOf(universalApkAsset)),
newVersion,
)
} else {
null
}
} else {
null
if (NumberUtils.compareVersions(newVersion, currentVersion) > 0) {
GitHubReleaseMapper.toAppUpdate(
release.copy(assets = listOf(universalApkAsset)),
newVersion,
)
} else {
null
}
}
}
}
@@ -1,58 +1,21 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.InstalledPackage
import com.zaneschepke.wireguardautotunnel.domain.repository.InstalledPackageRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.getAllInternetCapablePackages
import com.zaneschepke.wireguardautotunnel.util.extensions.getFriendlyAppName
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
@Singleton
class InstalledAndroidPackageRepository(
private val context: Context,
@ApplicationScope val applicationScope: CoroutineScope,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val ioDispatcher: CoroutineDispatcher,
) : InstalledPackageRepository {
private var cachedPackages: List<InstalledPackage>? = null
init {
val receiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
Intent.ACTION_PACKAGE_ADDED,
Intent.ACTION_PACKAGE_REMOVED,
Intent.ACTION_PACKAGE_CHANGED -> {
// don't update if we have nothing cached
if (cachedPackages == null) return
Timber.d("Updating installed packages cache")
applicationScope.launch { refreshInstalledPackages() }
}
}
}
}
val filter =
IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addAction(Intent.ACTION_PACKAGE_CHANGED)
addDataScheme("package")
}
context.registerReceiver(receiver, filter)
}
override suspend fun getInstalledPackages(): List<InstalledPackage> =
withContext(ioDispatcher) {
cachedPackages?.let {
@@ -63,7 +26,7 @@ class InstalledAndroidPackageRepository(
override suspend fun refreshInstalledPackages(): List<InstalledPackage> =
withContext(ioDispatcher) {
val packages = context.getAllInternetCapablePackages()
val packages = context.packageManager.getInstalledPackages(0)
val installedPackages =
packages.mapNotNull { packageInfo ->
@@ -4,28 +4,20 @@ import com.zaneschepke.wireguardautotunnel.data.dao.AutoTunnelSettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.AutoTunnelSettings as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
class RoomAutoTunnelSettingsRepository(
private val autoTunnelSettingsDao: AutoTunnelSettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : AutoTunnelSettingsRepository {
class RoomAutoTunnelSettingsRepository(private val autoTunnelSettingsDao: AutoTunnelSettingsDao) :
AutoTunnelSettingsRepository {
override suspend fun upsert(autoTunnelSettings: Domain) {
autoTunnelSettingsDao.upsert(autoTunnelSettings.toEntity())
}
override val flow: Flow<Domain>
get() =
autoTunnelSettingsDao
.getAutoTunnelSettingsFlow()
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
autoTunnelSettingsDao.getAutoTunnelSettingsFlow().map { (it ?: Entity()).toDomain() }
override suspend fun getAutoTunnelSettings(): Domain {
return (autoTunnelSettingsDao.getAutoTunnelSettings() ?: Entity()).toDomain()
@@ -4,28 +4,19 @@ import com.zaneschepke.wireguardautotunnel.data.dao.DnsSettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.DnsSettings as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
class RoomDnsSettingsRepository(
private val dnsSettingsDao: DnsSettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : DnsSettingsRepository {
class RoomDnsSettingsRepository(private val dnsSettingsDao: DnsSettingsDao) :
DnsSettingsRepository {
override suspend fun upsert(dnsSettings: Domain) {
dnsSettingsDao.upsert(dnsSettings.toEntity())
}
override val flow: Flow<Domain>
get() =
dnsSettingsDao
.getDnsSettingsFlow()
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
get() = dnsSettingsDao.getDnsSettingsFlow().map { (it ?: Entity()).toDomain() }
override suspend fun getDnsSettings(): Domain {
return (dnsSettingsDao.getDnsSettings() ?: Entity()).toDomain()
@@ -0,0 +1,23 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.dao.LockdownSettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.LockdownSettings as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
import kotlinx.coroutines.flow.map
class RoomLockdownSettingsRepository(private val lockdownSettingsDao: LockdownSettingsDao) :
LockdownSettingsRepository {
override suspend fun upsert(lockdownSettings: Domain) {
lockdownSettingsDao.upsert(lockdownSettings.toEntity())
}
override val flow =
lockdownSettingsDao.getLockdownSettingsFlow().map { (it ?: Entity()).toDomain() }
override suspend fun getLockdownSettings(): Domain {
return (lockdownSettingsDao.getLockdownSettings() ?: Entity()).toDomain()
}
}
@@ -4,28 +4,20 @@ import com.zaneschepke.wireguardautotunnel.data.dao.MonitoringSettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.MonitoringSettings as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.MonitoringSettings as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
class RoomMonitoringSettingsRepository(
private val monitoringSettingsDao: MonitoringSettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : MonitoringSettingsRepository {
class RoomMonitoringSettingsRepository(private val monitoringSettingsDao: MonitoringSettingsDao) :
MonitoringSettingsRepository {
override suspend fun upsert(monitoringSettings: Domain) {
monitoringSettingsDao.upsert(monitoringSettings.toEntity())
}
override val flow: Flow<Domain>
get() =
monitoringSettingsDao
.getMonitoringSettingsFlow()
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
monitoringSettingsDao.getMonitoringSettingsFlow().map { (it ?: Entity()).toDomain() }
override suspend fun getMonitoringSettings(): Domain {
return (monitoringSettingsDao.getMonitoringSettings() ?: Entity()).toDomain()
@@ -4,32 +4,20 @@ import com.zaneschepke.wireguardautotunnel.data.dao.ProxySettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class RoomProxySettingsRepository(
private val proxySettingsDao: ProxySettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ProxySettingsRepository {
class RoomProxySettingsRepository(private val proxySettingsDao: ProxySettingsDao) :
ProxySettingsRepository {
override suspend fun upsert(proxySettings: Domain) {
withContext(ioDispatcher) { proxySettingsDao.upsert(proxySettings.toEntity()) }
proxySettingsDao.upsert(proxySettings.toEntity())
}
override val flow =
proxySettingsDao
.getProxySettingsFlow()
.flowOn(ioDispatcher)
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
override val flow = proxySettingsDao.getProxySettingsFlow().map { (it ?: Entity()).toDomain() }
override suspend fun getProxySettings(): Domain {
return withContext(ioDispatcher) {
(proxySettingsDao.getProxySettings() ?: Entity()).toDomain()
}
return (proxySettingsDao.getProxySettings() ?: Entity()).toDomain()
}
}
@@ -4,32 +4,37 @@ import com.zaneschepke.wireguardautotunnel.data.dao.GeneralSettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.flowOn
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class RoomSettingsRepository(
private val settingsDoa: GeneralSettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : GeneralSettingRepository {
class RoomSettingsRepository(private val settingsDao: GeneralSettingsDao) :
GeneralSettingRepository {
override suspend fun upsert(generalSettings: Domain) {
withContext(ioDispatcher) { settingsDoa.upsert(generalSettings.toEntity()) }
settingsDao.upsert(generalSettings.toEntity())
}
override val flow =
settingsDoa
.getGeneralSettingsFlow()
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
override val flow = settingsDao.getGeneralSettingsFlow().map { (it ?: Entity()).toDomain() }
override suspend fun getGeneralSettings(): Domain {
return withContext(ioDispatcher) {
(settingsDoa.getGeneralSettings() ?: Entity()).toDomain()
}
return (settingsDao.getGeneralSettings() ?: Entity()).toDomain()
}
override suspend fun updateTheme(theme: Theme) {
settingsDao.updateTheme(theme.name)
}
override suspend fun updateLocale(locale: String) {
settingsDao.updateLocale(locale)
}
override suspend fun updatePinLockEnabled(enabled: Boolean) {
settingsDao.updatePinLockEnabled(enabled)
}
override suspend fun updateAppMode(appMode: AppMode) {
settingsDao.updateAppMode(appMode)
}
}
@@ -3,119 +3,96 @@ package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
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 {
class RoomTunnelRepository(private val tunnelConfigDao: TunnelConfigDao) : TunnelRepository {
override val flow =
tunnelConfigDao.getAllFlow().flowOn(ioDispatcher).map {
it.map { tunnelConfig -> tunnelConfig.toDomain() }
}
tunnelConfigDao.getAllFlow().map { it.map { tunnelConfig -> tunnelConfig.toDomain() } }
override val userTunnelsFlow =
tunnelConfigDao.getAllTunnelsExceptGlobal().flowOn(ioDispatcher).map {
tunnelConfigDao.getAllTunnelsExceptGlobal().map {
it.map { tunnelConfig -> tunnelConfig.toDomain() }
}
override val globalTunnelFlow: Flow<Domain?> =
tunnelConfigDao.getGlobalTunnel().flowOn(ioDispatcher).map { it?.toDomain() }
tunnelConfigDao.getGlobalTunnel().map { it?.toDomain() }
override suspend fun getAll(): List<Domain> {
return withContext(ioDispatcher) { tunnelConfigDao.getAll().map { it.toDomain() } }
return tunnelConfigDao.getAll().map { it.toDomain() }
}
override suspend fun save(tunnelConfig: Domain) {
withContext(ioDispatcher) { tunnelConfigDao.upsert(tunnelConfig.toEntity()) }
tunnelConfigDao.upsert(tunnelConfig.toEntity())
}
override suspend fun saveAll(tunnelConfigList: List<Domain>) {
withContext(ioDispatcher) {
tunnelConfigDao.saveAll(
tunnelConfigList.map { tunnelConfig -> tunnelConfig.toEntity() }
)
}
tunnelConfigDao.saveAll(tunnelConfigList.map { tunnelConfig -> tunnelConfig.toEntity() })
}
override suspend fun updatePrimaryTunnel(tunnelConfig: Domain?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetPrimaryTunnel()
tunnelConfig?.let { save(it.copy(isPrimaryTunnel = true)) }
}
tunnelConfigDao.resetPrimaryTunnel()
tunnelConfig?.let { save(it.copy(isPrimaryTunnel = true)) }
}
override suspend fun resetActiveTunnels() {
withContext(ioDispatcher) { tunnelConfigDao.resetActiveTunnels() }
tunnelConfigDao.resetActiveTunnels()
}
override suspend fun updateMobileDataTunnel(tunnelConfig: Domain?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetMobileDataTunnel()
tunnelConfig?.let { save(it.copy(isMobileDataTunnel = true)) }
}
tunnelConfigDao.resetMobileDataTunnel()
tunnelConfig?.let { save(it.copy(isMobileDataTunnel = true)) }
}
override suspend fun updateEthernetTunnel(tunnelConfig: Domain?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetEthernetTunnel()
tunnelConfig?.let { save(it.copy(isEthernetTunnel = true)) }
}
tunnelConfigDao.resetEthernetTunnel()
tunnelConfig?.let { save(it.copy(isEthernetTunnel = true)) }
}
override suspend fun delete(tunnelConfig: Domain) {
withContext(ioDispatcher) { tunnelConfigDao.delete(tunnelConfig.toEntity()) }
tunnelConfigDao.delete(tunnelConfig.toEntity())
}
override suspend fun getById(id: Int): Domain? {
return withContext(ioDispatcher) { tunnelConfigDao.getById(id.toLong())?.toDomain() }
return tunnelConfigDao.getById(id.toLong())?.toDomain()
}
override suspend fun getActive(): List<Domain> {
return withContext(ioDispatcher) { tunnelConfigDao.getActive().map { it.toDomain() } }
return tunnelConfigDao.getActive().map { it.toDomain() }
}
override suspend fun getDefaultTunnel(): Domain? {
return withContext(ioDispatcher) { tunnelConfigDao.getDefaultTunnel()?.toDomain() }
return tunnelConfigDao.getDefaultTunnel()?.toDomain()
}
override suspend fun getStartTunnel(): Domain? {
return withContext(ioDispatcher) { tunnelConfigDao.getStartTunnel()?.toDomain() }
return tunnelConfigDao.getStartTunnel()?.toDomain()
}
override suspend fun count(): Int {
return withContext(ioDispatcher) { tunnelConfigDao.count().toInt() }
return tunnelConfigDao.count().toInt()
}
override suspend fun findByTunnelName(name: String): Domain? {
return withContext(ioDispatcher) { tunnelConfigDao.getByName(name)?.toDomain() }
return tunnelConfigDao.getByName(name)?.toDomain()
}
override suspend fun findByTunnelNetworksName(name: String): List<Domain> {
return withContext(ioDispatcher) {
tunnelConfigDao.findByTunnelNetworkName(name).map { it.toDomain() }
}
return tunnelConfigDao.findByTunnelNetworkName(name).map { it.toDomain() }
}
override suspend fun findByMobileDataTunnel(): List<Domain> {
return withContext(ioDispatcher) {
tunnelConfigDao.findByMobileDataTunnel().map { it.toDomain() }
}
return tunnelConfigDao.findByMobileDataTunnel().map { it.toDomain() }
}
override suspend fun findPrimary(): List<Domain> {
return withContext(ioDispatcher) { tunnelConfigDao.findByPrimary().map { it.toDomain() } }
return tunnelConfigDao.findByPrimary().map { it.toDomain() }
}
override suspend fun delete(tunnels: List<Domain>) {
withContext(ioDispatcher) { tunnelConfigDao.delete(tunnels.map { it.toEntity() }) }
tunnelConfigDao.delete(tunnels.map { it.toEntity() })
}
}
@@ -1,69 +1,74 @@
package com.zaneschepke.wireguardautotunnel.di
import android.content.Context
import android.os.PowerManager
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.logcatter.LogcatReader
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationMonitor
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.shortcut.DynamicShortcutManager
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.SelectedTunnelsRepository
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import com.zaneschepke.wireguardautotunnel.viewmodel.*
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import org.koin.android.ext.koin.androidContext
import org.koin.core.annotation.KoinExperimentalAPI
import org.koin.core.module.dsl.singleOf
import org.koin.core.module.dsl.viewModel
import org.koin.core.module.dsl.viewModelOf
import org.koin.core.qualifier.named
import org.koin.dsl.bind
import org.koin.dsl.module
@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)
@OptIn(KoinExperimentalAPI::class)
val appModule = module {
single<CoroutineScope>(named(Scope.APPLICATION)) {
CoroutineScope(SupervisorJob() + get<CoroutineDispatcher>(named(Dispatcher.DEFAULT)))
}
@Singleton
@Provides
fun provideNotificationService(@ApplicationContext context: Context): NotificationManager {
return WireGuardNotification(context)
single<LogReader> { LogcatReader.init(storageDir = androidContext().filesDir.absolutePath) }
single<PowerManager> {
androidContext().getSystemService(Context.POWER_SERVICE) as PowerManager
}
singleOf(::NotificationMonitor)
singleOf(::WireGuardNotification) bind NotificationManager::class
single {
ServiceManager(
androidContext(),
get(named(Dispatcher.IO)),
get(named(Scope.APPLICATION)),
get(named(Dispatcher.MAIN)),
get(),
)
}
@Singleton
@Provides
fun provideShortcutManager(
@ApplicationContext context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): ShortcutManager {
return DynamicShortcutManager(context, ioDispatcher)
}
singleOf(::GlobalEffectRepository)
@Singleton
@Provides
fun provideNetworkUtils(@IoDispatcher ioDispatcher: CoroutineDispatcher): NetworkUtils {
return NetworkUtils(ioDispatcher)
}
single { FileUtils(androidContext(), get(named(Dispatcher.IO))) }
single<ShortcutManager> { DynamicShortcutManager(androidContext(), get(named(Dispatcher.IO))) }
singleOf(::SelectedTunnelsRepository)
@Singleton
@Provides
fun provideNotificationMonitor(
tunnelManager: TunnelManager,
notificationManager: NotificationManager,
): NotificationMonitor {
return NotificationMonitor(tunnelManager, notificationManager)
}
single { NetworkUtils(get(named(Dispatcher.IO))) }
viewModelOf(::AutoTunnelViewModel)
viewModel { (id: Int?) -> ConfigViewModel(get(), get(), get(), id) }
viewModelOf(::DnsViewModel)
viewModelOf(::LicenseViewModel)
viewModelOf(::LockdownViewModel)
viewModelOf(::LoggerViewModel)
viewModelOf(::MonitoringViewModel)
viewModelOf(::ProxySettingsViewModel)
viewModelOf(::SettingsViewModel)
viewModelOf(::SharedAppViewModel)
viewModel { (id: Int) -> SplitTunnelViewModel(get(), get(), get(), id) }
viewModel { SupportViewModel(get(), get(named(Dispatcher.MAIN)), get()) }
viewModel { (id: Int) -> TunnelViewModel(get(), get(), id) }
}
@@ -1,13 +0,0 @@
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
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class ProxyUserspace
@@ -1,15 +0,0 @@
package com.zaneschepke.wireguardautotunnel.di
import javax.inject.Qualifier
@Retention(AnnotationRetention.BINARY) @Qualifier annotation class DefaultDispatcher
@Retention(AnnotationRetention.BINARY) @Qualifier annotation class IoDispatcher
@Retention(AnnotationRetention.BINARY) @Qualifier annotation class MainDispatcher
@Retention(AnnotationRetention.BINARY) @Qualifier annotation class MainImmediateDispatcher
@Retention(AnnotationRetention.BINARY) @Qualifier annotation class ApplicationScope
@Retention(AnnotationRetention.BINARY) @Qualifier annotation class ServiceScope
@@ -1,24 +0,0 @@
package com.zaneschepke.wireguardautotunnel.di
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
@Module
@InstallIn(SingletonComponent::class)
object CoroutinesDispatchersModule {
@DefaultDispatcher
@Provides
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
@IoDispatcher @Provides fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
@MainDispatcher @Provides fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
@MainImmediateDispatcher
@Provides
fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
}
@@ -0,0 +1,79 @@
package com.zaneschepke.wireguardautotunnel.di
import android.content.Context
import androidx.room.Room
import androidx.room.RoomDatabase
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.migrations.MIGRATION_23_24
import com.zaneschepke.wireguardautotunnel.data.migrations.MIGRATION_25_26
import com.zaneschepke.wireguardautotunnel.data.migrations.MIGRATION_28_29
import com.zaneschepke.wireguardautotunnel.data.repository.DataStoreAppStateRepository
import com.zaneschepke.wireguardautotunnel.data.repository.InstalledAndroidPackageRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomAutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomDnsSettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomLockdownSettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomMonitoringSettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomProxySettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomSettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomTunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.InstalledPackageRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import org.koin.android.ext.koin.androidContext
import org.koin.core.module.dsl.singleOf
import org.koin.core.qualifier.named
import org.koin.dsl.bind
import org.koin.dsl.module
val databaseModule = module {
single<RoomDatabase.Callback> { DatabaseCallback(lazy { get() }) }
single {
Room.databaseBuilder(
androidContext(),
AppDatabase::class.java,
get<Context>().getString(R.string.db_name),
)
.addMigrations(
MIGRATION_23_24(get<DataStoreManager>().dataStore),
MIGRATION_25_26,
MIGRATION_28_29,
)
.fallbackToDestructiveMigration(true)
.addCallback(get())
.build()
}
single { get<AppDatabase>().generalSettingsDao() }
single { get<AppDatabase>().lockdownSettingsDao() }
single { get<AppDatabase>().dnsSettingsDao() }
single { get<AppDatabase>().autoTunnelSettingsDao() }
single { get<AppDatabase>().monitoringSettingsDao() }
single { get<AppDatabase>().proxySettingsDoa() }
single { get<AppDatabase>().tunnelConfigDoa() }
single { DataStoreManager(androidContext(), get(named(Dispatcher.IO))) }
single<AppStateRepository> {
DataStoreAppStateRepository(get(), get(named(Scope.APPLICATION)), get(named(Dispatcher.IO)))
}
singleOf(::RoomAutoTunnelSettingsRepository) bind AutoTunnelSettingsRepository::class
singleOf(::RoomDnsSettingsRepository) bind DnsSettingsRepository::class
singleOf(::RoomLockdownSettingsRepository) bind LockdownSettingsRepository::class
singleOf(::RoomMonitoringSettingsRepository) bind MonitoringSettingsRepository::class
singleOf(::RoomProxySettingsRepository) bind ProxySettingsRepository::class
singleOf(::RoomSettingsRepository) bind GeneralSettingRepository::class
singleOf(::RoomTunnelRepository) bind TunnelRepository::class
single<InstalledPackageRepository> {
InstalledAndroidPackageRepository(androidContext(), get(named(Dispatcher.IO)))
}
}
@@ -0,0 +1,13 @@
package com.zaneschepke.wireguardautotunnel.di
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import org.koin.core.qualifier.named
import org.koin.dsl.module
val dispatchersModule = module {
single<CoroutineDispatcher>(named(Dispatcher.DEFAULT)) { Dispatchers.Default }
single<CoroutineDispatcher>(named(Dispatcher.IO)) { Dispatchers.IO }
single<CoroutineDispatcher>(named(Dispatcher.MAIN)) { Dispatchers.Main }
single<CoroutineDispatcher>(named(Dispatcher.MAIN_IMMEDIATE)) { Dispatchers.Main.immediate }
}
@@ -0,0 +1,29 @@
package com.zaneschepke.wireguardautotunnel.di
import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi
import com.zaneschepke.wireguardautotunnel.data.network.KtorClient
import com.zaneschepke.wireguardautotunnel.data.network.KtorGitHubApi
import com.zaneschepke.wireguardautotunnel.data.repository.GitHubUpdateRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.UpdateRepository
import org.koin.android.ext.koin.androidContext
import org.koin.core.module.dsl.singleOf
import org.koin.core.qualifier.named
import org.koin.dsl.bind
import org.koin.dsl.lazyModule
val networkModule = lazyModule {
single { KtorClient.create() }
singleOf(::KtorGitHubApi) bind GitHubApi::class
single<UpdateRepository> {
val appName = "wgtunnel"
GitHubUpdateRepository(
get(),
get(),
appName,
appName,
androidContext(),
get(named(Dispatcher.IO)),
)
}
}
@@ -0,0 +1,25 @@
package com.zaneschepke.wireguardautotunnel.di
// Dispatchers
enum class Dispatcher {
MAIN,
IO,
DEFAULT,
MAIN_IMMEDIATE,
}
// Scopes
enum class Scope {
APPLICATION
}
enum class Shell {
APP,
TUNNEL,
}
enum class Core {
KERNEL,
PROXY_USERSPACE,
USERSPACE,
}
@@ -1,202 +0,0 @@
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.dao.*
import com.zaneschepke.wireguardautotunnel.data.migrations.MIGRATION_23_24
import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi
import com.zaneschepke.wireguardautotunnel.data.network.KtorClient
import com.zaneschepke.wireguardautotunnel.data.network.KtorGitHubApi
import com.zaneschepke.wireguardautotunnel.data.repository.*
import com.zaneschepke.wireguardautotunnel.domain.repository.*
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import io.ktor.client.*
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
@Module
@InstallIn(SingletonComponent::class)
class RepositoryModule {
@Provides
@Singleton
fun provideGlobalEffectRepository(): GlobalEffectRepository {
return GlobalEffectRepository()
}
@Provides
@Singleton
fun provideInstalledPackageRepository(
@ApplicationContext context: Context,
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): InstalledPackageRepository {
return InstalledAndroidPackageRepository(context, applicationScope, ioDispatcher)
}
@Provides
@Singleton
fun provideDatabase(
@ApplicationContext context: Context,
callback: DatabaseCallback,
dataStoreManager: DataStoreManager,
): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
context.getString(R.string.db_name),
)
.addMigrations(MIGRATION_23_24(dataStoreManager.dataStore))
.fallbackToDestructiveMigration(true)
.addCallback(callback)
.build()
}
@Singleton
@Provides
fun provideSettingsDoa(appDatabase: AppDatabase): GeneralSettingsDao {
return appDatabase.generalSettingsDao()
}
@Singleton
@Provides
fun provideDnsSettingsDao(appDatabase: AppDatabase): DnsSettingsDao {
return appDatabase.dnsSettingsDao()
}
@Singleton
@Provides
fun provideAutoTunnelDao(appDatabase: AppDatabase): AutoTunnelSettingsDao {
return appDatabase.autoTunnelSettingsDao()
}
@Singleton
@Provides
fun provideMonitoringDao(appDatabase: AppDatabase): MonitoringSettingsDao {
return appDatabase.monitoringSettingsDao()
}
@Singleton
@Provides
fun provideProxyDoa(appDatabase: AppDatabase): ProxySettingsDao {
return appDatabase.proxySettingsDoa()
}
@Singleton
@Provides
fun provideTunnelConfigDoa(appDatabase: AppDatabase): TunnelConfigDao {
return appDatabase.tunnelConfigDoa()
}
@Singleton
@Provides
fun provideTunnelConfigRepository(
tunnelConfigDao: TunnelConfigDao,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): TunnelRepository {
return RoomTunnelRepository(tunnelConfigDao, ioDispatcher)
}
@Singleton
@Provides
fun provideGeneralSettingsRepository(
settingsDao: GeneralSettingsDao,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): GeneralSettingRepository {
return RoomSettingsRepository(settingsDao, ioDispatcher)
}
@Singleton
@Provides
fun provideMonitoringSettingsRepository(
monitoringSettingsDao: MonitoringSettingsDao,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): MonitoringSettingsRepository {
return RoomMonitoringSettingsRepository(monitoringSettingsDao, ioDispatcher)
}
@Singleton
@Provides
fun provideDnsSettingsRepository(
dnsSettingsDao: DnsSettingsDao,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): DnsSettingsRepository {
return RoomDnsSettingsRepository(dnsSettingsDao, ioDispatcher)
}
@Singleton
@Provides
fun provideAutoTunnelSettingsRepository(
autoTunnelSettingsDao: AutoTunnelSettingsDao,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): AutoTunnelSettingsRepository {
return RoomAutoTunnelSettingsRepository(autoTunnelSettingsDao, ioDispatcher)
}
@Singleton
@Provides
fun provideProxySettingsRepository(
proxySettingsDao: ProxySettingsDao,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): ProxySettingsRepository {
return RoomProxySettingsRepository(proxySettingsDao, ioDispatcher)
}
@Singleton
@Provides
fun providePreferencesDataStore(
@ApplicationContext context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): DataStoreManager {
return DataStoreManager(context, ioDispatcher)
}
@Provides
@Singleton
fun provideGeneralStateRepository(
dataStoreManager: DataStoreManager,
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): AppStateRepository {
return DataStoreAppStateRepository(dataStoreManager, applicationScope, ioDispatcher)
}
@Provides
@Singleton
fun provideHttpClient(): HttpClient {
return KtorClient.create()
}
@Provides
@Singleton
fun provideGitHubApi(client: HttpClient): GitHubApi {
return KtorGitHubApi(client)
}
@Provides
@Singleton
fun provideUpdateRepository(
gitHubApi: GitHubApi,
client: HttpClient,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationContext context: Context,
): UpdateRepository {
return GitHubUpdateRepository(
gitHubApi,
client,
"wgtunnel",
"wgtunnel",
context,
ioDispatcher,
)
}
}
@@ -1,219 +1,108 @@
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.logcatter.LogReader
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.*
import com.zaneschepke.wireguardautotunnel.domain.repository.*
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.KernelTunnel
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.RunConfigHelper
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.TunnelBackend
import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.UserspaceTunnel
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.util.RootShellUtils
import com.zaneschepke.wireguardautotunnel.util.extensions.to
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.map
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.GoBackend
import org.amnezia.awg.backend.ProxyGoBackend
import org.amnezia.awg.backend.RootTunnelActionHandler
import org.koin.android.ext.koin.androidContext
import org.koin.core.module.dsl.singleOf
import org.koin.core.qualifier.named
import org.koin.dsl.module
@Module
@InstallIn(SingletonComponent::class)
class TunnelModule {
val tunnelModule = module {
single(named(Shell.TUNNEL)) { RootShell(androidContext()) }
single(named(Shell.APP)) { RootShell(androidContext()) }
@Provides
@Singleton
@TunnelShell
fun provideTunnelRootShell(@ApplicationContext context: Context): RootShell {
return RootShell(context)
}
single { RootShellUtils(get(named(Shell.APP)), get(named(Dispatcher.IO))) }
@Provides
@Singleton
@AppShell
fun provideAppRootShell(@ApplicationContext context: Context): RootShell {
return RootShell(context)
}
singleOf(::RunConfigHelper)
@Provides
@Singleton
@Userspace
fun provideAmneziaBackend(@ApplicationContext context: Context): Backend {
return GoBackend(context, RootTunnelActionHandler(org.amnezia.awg.util.RootShell(context)))
}
@Provides
@Singleton
@ProxyUserspace
fun provideAmneziaProxyBackend(@ApplicationContext context: Context): Backend {
return ProxyGoBackend(
context,
RootTunnelActionHandler(org.amnezia.awg.util.RootShell(context)),
single<Backend>(named(Core.USERSPACE)) {
GoBackend(
androidContext(),
RootTunnelActionHandler(org.amnezia.awg.util.RootShell(androidContext())),
)
}
@Provides
@Singleton
fun provideKernelBackend(
@ApplicationContext context: Context,
@TunnelShell shell: RootShell,
): com.wireguard.android.backend.Backend {
return WgQuickBackend(
context,
single<Backend>(named(Core.PROXY_USERSPACE)) {
ProxyGoBackend(
androidContext(),
RootTunnelActionHandler(org.amnezia.awg.util.RootShell(androidContext())),
)
}
single<com.wireguard.android.backend.Backend> {
val shell = get<RootShell>(named(Shell.TUNNEL))
WgQuickBackend(
androidContext(),
shell,
ToolsInstaller(context, shell),
ToolsInstaller(androidContext(), shell),
com.wireguard.android.backend.RootTunnelActionHandler(shell),
)
.also { it.setMultipleTunnels(true) }
.apply { setMultipleTunnels(true) }
}
@Provides
@Singleton
@Kernel
fun provideKernelProvider(
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
backend: com.wireguard.android.backend.Backend,
): TunnelProvider {
return KernelTunnel(applicationScope, ioDispatcher, backend)
single<TunnelBackend>(named(Core.KERNEL)) {
KernelTunnel(get(), get<com.wireguard.android.backend.Backend>())
}
@Provides
@Singleton
@Userspace
fun provideUserspaceProvider(
@ApplicationScope applicationScope: CoroutineScope,
proxySettingsRepository: ProxySettingsRepository,
dnsSettingsRepository: DnsSettingsRepository,
@Userspace backend: Backend,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): TunnelProvider {
return UserspaceTunnel(
applicationScope,
ioDispatcher,
proxySettingsRepository,
dnsSettingsRepository,
backend,
)
single<TunnelBackend>(qualifier = named(Core.USERSPACE)) {
UserspaceTunnel(get<Backend>(named(Core.USERSPACE)), get())
}
@Provides
@Singleton
@ProxyUserspace
fun provideProxyUserspaceProvider(
@ApplicationScope applicationScope: CoroutineScope,
dnsSettingsRepository: DnsSettingsRepository,
proxySettingsRepository: ProxySettingsRepository,
@ProxyUserspace backend: Backend,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): TunnelProvider {
return UserspaceTunnel(
applicationScope,
ioDispatcher,
proxySettingsRepository,
dnsSettingsRepository,
backend,
)
single<TunnelBackend>(qualifier = named(Core.PROXY_USERSPACE)) {
UserspaceTunnel(get<Backend>(named(Core.PROXY_USERSPACE)), get())
}
@Provides
@Singleton
fun provideTunnelManager(
@Kernel kernelTunnel: TunnelProvider,
@Userspace userspaceTunnel: TunnelProvider,
@ProxyUserspace proxyTunnel: TunnelProvider,
serviceManager: ServiceManager,
tunnelRepository: TunnelRepository,
settingsRepository: GeneralSettingRepository,
autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
tunnelMonitor: TunnelMonitor,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
): TunnelManager {
return TunnelManager(
kernelTunnel,
userspaceTunnel,
proxyTunnel,
serviceManager,
settingsRepository,
autoTunnelSettingsRepository,
tunnelRepository,
tunnelMonitor,
applicationScope,
ioDispatcher,
)
}
@Provides
@Singleton
fun provideNetworkMonitor(
@ApplicationContext context: Context,
autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
@ApplicationScope applicationScope: CoroutineScope,
@AppShell appShell: RootShell,
): NetworkMonitor {
return AndroidNetworkMonitor(
context,
single<NetworkMonitor> {
AndroidNetworkMonitor(
androidContext(),
object : AndroidNetworkMonitor.ConfigurationListener {
override val detectionMethod: Flow<AndroidNetworkMonitor.WifiDetectionMethod>
get() =
autoTunnelSettingsRepository.flow
.distinctUntilChangedBy { it.wifiDetectionMethod }
.map { it.wifiDetectionMethod.to() }
override val detectionMethod =
get<AutoTunnelSettingsRepository>()
.flow
.distinctUntilChangedBy { it.wifiDetectionMethod }
.map { it.wifiDetectionMethod.to() }
override val rootShell: RootShell
get() = appShell
override val rootShell = get<RootShell>(named(Shell.APP))
},
applicationScope,
get<CoroutineScope>(named(Scope.APPLICATION)),
)
}
@Singleton
@Provides
fun provideServiceManager(
@ApplicationContext context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@MainDispatcher mainCoroutineDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
): ServiceManager {
return ServiceManager(
context,
ioDispatcher,
applicationScope,
mainCoroutineDispatcher,
autoTunnelSettingsRepository,
)
}
@Singleton
@Provides
fun provideTunnelMonitor(
networkMonitor: NetworkMonitor,
networkUtils: NetworkUtils,
logReader: LogReader,
tunnelsRepository: TunnelRepository,
settingsRepository: GeneralSettingRepository,
monitoringSettingsRepository: MonitoringSettingsRepository,
): TunnelMonitor {
return TunnelMonitor(
settingsRepository,
tunnelsRepository,
monitoringSettingsRepository,
networkMonitor,
networkUtils,
logReader,
single {
TunnelManager(
get(named(Core.KERNEL)),
get(named(Core.USERSPACE)),
get(named(Core.PROXY_USERSPACE)),
get(),
get(),
get(),
get(),
get(),
get(),
get(),
get(),
get(),
get(),
get(named(Scope.APPLICATION)),
get(named(Dispatcher.IO)),
)
}
}
@@ -1,36 +0,0 @@
package com.zaneschepke.wireguardautotunnel.di
import android.content.Context
import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.RootShellUtils
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ViewModelScoped
import javax.inject.Provider
import kotlinx.coroutines.CoroutineDispatcher
@Module
@InstallIn(ViewModelComponent::class)
class ViewModelModule {
@ViewModelScoped
@Provides
fun provideFileUtils(
@ApplicationContext context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): FileUtils {
return FileUtils(context, ioDispatcher)
}
@ViewModelScoped
@Provides
fun provideRootShellUtils(
@AppShell rootShell: Provider<RootShell>,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): RootShellUtils {
return RootShellUtils(rootShell, ioDispatcher)
}
}
@@ -0,0 +1,7 @@
package com.zaneschepke.wireguardautotunnel.di
import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
import org.koin.androidx.workmanager.dsl.workerOf
import org.koin.dsl.module
val workerModule = module { workerOf(::ServiceWorker) }
@@ -3,5 +3,9 @@ package com.zaneschepke.wireguardautotunnel.domain.enums
sealed class BackendMode {
data object Inactive : BackendMode()
data class KillSwitch(val allowedIps: Set<String>) : BackendMode()
data class KillSwitch(
val allowedIps: Set<String>,
val isMetered: Boolean,
val dualStack: Boolean,
) : BackendMode()
}
@@ -1,8 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
enum class NetworkType {
WIFI,
ETHERNET,
MOBILE_DATA,
NONE,
}
@@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.domain.enums
sealed class TunnelStatus {
data object Up : TunnelStatus()
data class Up(val startTime: Long) : TunnelStatus()
data object Down : TunnelStatus()
@@ -15,11 +15,11 @@ sealed class TunnelStatus {
}
fun isUp(): Boolean {
return this == Up
return this is Up
}
fun isUpOrStarting(): Boolean {
return this == Up || this == Starting
return this is Up || this == Starting
}
fun isDownOrStopping(): Boolean {
@@ -1,11 +1,12 @@
package com.zaneschepke.wireguardautotunnel.domain.events
import androidx.annotation.Keep
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
sealed class AutoTunnelEvent {
data class Start(val tunnelConfig: TunnelConfig? = null) : AutoTunnelEvent()
@Keep data class Start(val tunnelConfig: TunnelConfig? = null) : AutoTunnelEvent()
data object Stop : AutoTunnelEvent()
@Keep data object Stop : AutoTunnelEvent()
data object DoNothing : AutoTunnelEvent()
@Keep data object DoNothing : AutoTunnelEvent()
}
@@ -4,38 +4,43 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.util.StringValue
sealed class BackendCoreException : Exception() {
data object DNS : BackendCoreException()
data object Unauthorized : BackendCoreException()
data object Config : BackendCoreException()
data object KernelModuleName : BackendCoreException()
data object NotAuthorized : BackendCoreException()
data object ServiceNotRunning : BackendCoreException()
data object Unknown : BackendCoreException()
data object TunnelNameTooLong : BackendCoreException()
data object UapiUpdateFailed : BackendCoreException()
fun toStringRes() =
when (this) {
Config -> R.string.config_error
DNS -> R.string.dns_resolve_error
KernelModuleName -> R.string.kernel_name_error
NotAuthorized,
Unauthorized -> R.string.auth_error
ServiceNotRunning -> R.string.service_running_error
Unknown -> R.string.unknown_error
TunnelNameTooLong -> R.string.error_tunnel_name
UapiUpdateFailed -> R.string.active_tunnel_update_failed
}
abstract val stringRes: Int
fun toStringValue(): StringValue {
return StringValue.StringResource(toStringRes())
return StringValue.StringResource(stringRes)
}
}
class DnsFailure : BackendCoreException() {
override val stringRes = R.string.dns_resolve_error
}
class VpnUnauthorized : BackendCoreException() {
override val stringRes = R.string.auth_error
}
class InvalidConfig : BackendCoreException() {
override val stringRes = R.string.config_error
}
class KernelTunnelName(override val stringRes: Int) : BackendCoreException() {}
class NotAuthorized : BackendCoreException() {
override val stringRes = R.string.auth_error
}
class ServiceNotRunning : BackendCoreException() {
override val stringRes = R.string.service_running_error
}
class UnknownError : BackendCoreException() {
override val stringRes = R.string.unknown_error
}
class UapiUpdateFailed : BackendCoreException() {
override val stringRes = R.string.active_tunnel_update_failed
}
class KernelWireguardNotSupported : BackendCoreException() {
override val stringRes = R.string.kernel_wireguard_unsupported
}
@@ -3,4 +3,5 @@ package com.zaneschepke.wireguardautotunnel.domain.model
data class AppState(
val isLocationDisclosureShown: Boolean = false,
val isBatteryOptimizationDisableShown: Boolean = false,
val shouldShowDonationSnackbar: Boolean = false,
)
@@ -6,4 +6,5 @@ data class DnsSettings(
val id: Int = 0,
val dnsProtocol: DnsProtocol = DnsProtocol.fromValue(0),
val dnsEndpoint: String? = null,
val isGlobalTunnelDnsEnabled: Boolean = false,
)
@@ -8,7 +8,7 @@ data class GeneralSettings(
val isShortcutsEnabled: Boolean = false,
val isRestoreOnBootEnabled: Boolean = false,
val isMultiTunnelEnabled: Boolean = false,
val isTunnelGlobalsEnabled: Boolean = false,
val isGlobalSplitTunnelEnabled: Boolean = false,
val appMode: AppMode = AppMode.fromValue(0),
val theme: Theme = Theme.AUTOMATIC,
val locale: String? = null,
@@ -16,6 +16,6 @@ data class GeneralSettings(
val isRemoteControlEnabled: Boolean = false,
val isPinLockEnabled: Boolean = false,
val isAlwaysOnVpnEnabled: Boolean = false,
val isLanOnKillSwitchEnabled: Boolean = false,
val customSplitPackages: Map<String, String> = emptyMap(),
val isKillSwitchMetered: Boolean = true,
val alreadyDonated: Boolean = false,
)

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