Compare commits

..

107 Commits

Author SHA1 Message Date
Zane Schepke 1952ff1b02 Merge remote-tracking branch 'weblate/main' into localize-sync
# Conflicts:
#	app/src/main/res/values-cs/strings.xml
#	app/src/main/res/values-fr/strings.xml
#	app/src/main/res/values-hu/strings.xml
#	app/src/main/res/values-ru/strings.xml
#	app/src/main/res/values-ur/strings.xml
#	app/src/main/res/values-zh-rTW/strings.xml
#	fastlane/metadata/android/zh-TW/short_description.txt
2025-07-11 12:57:56 -04:00
Andras 99f6c0bda5 Translated using Weblate (Hungarian)
Currently translated at 5.3% (14 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/hu/
2025-07-11 18:48:04 +02:00
Priit Jõerüüt 28b77d33ca Translated using Weblate (Estonian)
Currently translated at 90.1% (237 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/et/
2025-07-06 06:05:19 +02:00
Priit Jõerüüt b0b070f6aa Translated using Weblate (Estonian)
Currently translated at 90.1% (237 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/et/
2025-07-06 06:05:19 +02:00
Priit Jõerüüt 89b1f314b1 Translated using Weblate (Estonian)
Currently translated at 90.1% (237 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/et/
2025-07-06 06:05:19 +02:00
EESF-2 020263eba6 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 88.2% (232 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/zh_Hant/
2025-07-01 22:02:05 +02:00
EESF-2 d359215b20 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 25.0% (3 of 12 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/zh_Hant/
2025-07-01 22:02:03 +02:00
Priit Jõerüüt a6a0bba569 Translated using Weblate (Estonian)
Currently translated at 81.7% (215 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/et/
2025-06-29 23:04:36 +02:00
Priit Jõerüüt f729e924cd Translated using Weblate (Estonian)
Currently translated at 81.7% (215 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/et/
2025-06-29 23:04:35 +02:00
teemue 3c1d0f893a Translated using Weblate (Finnish)
Currently translated at 52.8% (139 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/fi/
2025-06-29 23:04:34 +02:00
teemue b184331258 Translated using Weblate (Finnish)
Currently translated at 52.8% (139 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/fi/
2025-06-29 23:04:33 +02:00
Priit Jõerüüt 36603ab542 Translated using Weblate (Estonian)
Currently translated at 76.8% (202 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/et/
2025-06-28 13:04:37 +00:00
Priit Jõerüüt d0e21247b5 Translated using Weblate (Estonian)
Currently translated at 76.8% (202 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/et/
2025-06-28 13:04:35 +00:00
Priit Jõerüüt 786a8b54ce Translated using Weblate (Estonian)
Currently translated at 76.8% (202 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/et/
2025-06-28 13:04:34 +00:00
Priit Jõerüüt d762fe7283 Translated using Weblate (Estonian)
Currently translated at 76.8% (202 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/et/
2025-06-28 13:04:33 +00:00
Jan-Erik Moen ed9cab3251 Translated using Weblate (Norwegian Bokmål)
Currently translated at 5.3% (14 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/nb_NO/
2025-06-28 13:04:32 +00:00
Priit Jõerüüt 8e1e754a87 Translated using Weblate (Estonian)
Currently translated at 6.0% (16 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/et/
2025-06-27 07:01:52 +02:00
Priit Jõerüüt c6eaaa1f37 Translated using Weblate (Estonian)
Currently translated at 33.3% (4 of 12 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/et/
2025-06-27 07:01:50 +02:00
Languages add-on 5077ec9a46 Added translation using Weblate (Estonian) 2025-06-26 06:22:22 +02:00
catelixor e4a808c6ce Translated using Weblate (Czech)
Currently translated at 100.0% (263 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/cs/
2025-06-26 00:04:44 +00:00
EESF-2 52efdfa288 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 86.3% (227 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/zh_Hant/
2025-06-19 01:04:27 +02:00
igor a4f413e79e Translated using Weblate (French)
Currently translated at 76.4% (201 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/fr/
2025-06-10 17:01:52 +02:00
igor 71ae3864fc Translated using Weblate (French)
Currently translated at 58.3% (7 of 12 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/fr/
2025-06-10 17:01:50 +02:00
Hamed Ap 008bc476e0 Translated using Weblate (Persian)
Currently translated at 11.0% (29 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/fa/
2025-06-09 10:02:02 +02:00
Hamed Ap 3947b04e21 Translated using Weblate (Persian)
Currently translated at 100.0% (12 of 12 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/fa/
2025-06-09 10:01:59 +02:00
Languages add-on acef2637c8 Added translation using Weblate (Persian) 2025-06-08 07:10:21 +00:00
Noureddine ba8b09cdc9 Translated using Weblate (Arabic)
Currently translated at 8.3% (1 of 12 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/ar/
2025-06-07 07:01:47 +02:00
Languages add-on 79e5ba0cb0 Added translation using Weblate (Arabic) 2025-06-06 04:41:28 +00:00
catelixor 165fda0352 Translated using Weblate (Czech)
Currently translated at 98.8% (260 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/cs/
2025-05-24 09:54:40 +02:00
Faisal Gull c4dfb4d591 Translated using Weblate (Urdu)
Currently translated at 100.0% (12 of 12 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/ur/
2025-05-19 05:03:13 +02:00
Faisal Gull e80e0b7f94 Translated using Weblate (Urdu)
Currently translated at 100.0% (263 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/ur/
2025-05-19 05:03:10 +02:00
François-Xavier Choinière b856fc2230 Translated using Weblate (French)
Currently translated at 75.6% (199 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/fr/
2025-05-16 04:02:07 +02:00
Deleted User 202ac52e25 Translated using Weblate (Spanish)
Currently translated at 100.0% (263 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/es/
2025-05-13 19:02:26 +02:00
Saratoga79 2b1a8af998 Translated using Weblate (Spanish)
Currently translated at 100.0% (263 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/es/
2025-05-13 19:02:25 +02:00
solokot b924845835 Translated using Weblate (Russian)
Currently translated at 99.6% (262 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/ru/
2025-05-11 08:01:43 +02:00
catelixor 0f8d7fed97 Translated using Weblate (Czech)
Currently translated at 97.7% (257 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/cs/
2025-05-09 23:01:51 +00:00
angrybb 20ff172055 Translated using Weblate (Serbian)
Currently translated at 16.6% (2 of 12 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/sr/
2025-05-07 23:01:49 +02:00
Faisal Gull 42671f616f Translated using Weblate (Urdu)
Currently translated at 100.0% (12 of 12 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/ur/
2025-05-07 23:01:48 +02:00
Faisal Gull ecca99828f Translated using Weblate (Urdu)
Currently translated at 100.0% (263 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/ur/
2025-05-07 23:01:46 +02:00
Languages add-on 7f40df9d36 Added translation using Weblate (Serbian) 2025-05-06 22:16:27 +02:00
dct 9a497f7892 Translated using Weblate (Vietnamese)
Currently translated at 5.7% (15 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/vi/
2025-05-06 22:16:26 +02:00
Tommaso dc5a10ebc9 Translated using Weblate (Italian)
Currently translated at 100.0% (12 of 12 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/it/
2025-05-06 14:06:03 +00:00
catelixor d1a4c7f133 Translated using Weblate (Czech)
Currently translated at 92.7% (244 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/cs/
2025-05-06 14:06:01 +00:00
catelixor 93ebf299e6 Translated using Weblate (Czech)
Currently translated at 100.0% (12 of 12 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/cs/
2025-05-06 14:06:00 +00:00
大王叫我来巡山 1d83da9d44 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (263 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/zh_Hans/
2025-05-05 10:01:55 +02:00
Matthaiks d2abaad5b4 Translated using Weblate (Polish)
Currently translated at 100.0% (263 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/pl/
2025-05-05 10:01:54 +02:00
Matthaiks 8bf88a89bd Translated using Weblate (Polish)
Currently translated at 100.0% (12 of 12 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/pl/
2025-05-05 10:01:52 +02:00
Kachelkaiser 96cfd04450 Translated using Weblate (German)
Currently translated at 100.0% (263 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/de/
2025-05-05 10:01:51 +02:00
Kachelkaiser c5f39ec906 Translated using Weblate (German)
Currently translated at 100.0% (12 of 12 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/de/
2025-05-05 10:01:49 +02:00
solokot 5cd8f9c2f2 Translated using Weblate (Russian)
Currently translated at 99.6% (262 of 263 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/ru/
2025-05-05 10:01:47 +02:00
Tommaso 2d11712fdc Translated using Weblate (Italian)
Currently translated at 96.5% (250 of 259 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/it/
2025-05-04 09:03:30 +02:00
Tommaso 9528850873 Translated using Weblate (Italian)
Currently translated at 100.0% (11 of 11 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/it/
2025-05-04 09:03:30 +02:00
Faisal Gull a0d5647f23 Translated using Weblate (Urdu)
Currently translated at 100.0% (259 of 259 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/ur/
2025-05-04 09:03:30 +02:00
Faisal Gull 5ff810a6ef Translated using Weblate (Urdu)
Currently translated at 100.0% (11 of 11 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/ur/
2025-05-04 09:03:30 +02:00
Faisal Gull 3dc314d79a Translated using Weblate (Urdu)
Currently translated at 100.0% (259 of 259 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/ur/
2025-05-04 09:03:30 +02:00
Jasper 466a5cecbe Translated using Weblate (Dutch)
Currently translated at 70.6% (183 of 259 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/nl/
2025-05-04 09:03:30 +02:00
VertekPlus a13d70227f Translated using Weblate (Russian)
Currently translated at 99.6% (258 of 259 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/ru/
2025-05-04 09:03:30 +02:00
adkostatt 519981d681 Translated using Weblate (Russian)
Currently translated at 99.6% (258 of 259 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/ru/
2025-05-04 09:03:30 +02:00
Valentin 0d17bf28e0 Translated using Weblate (Russian)
Currently translated at 99.6% (258 of 259 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/ru/
2025-05-04 09:03:30 +02:00
ssantos fc1d4b22a6 Translated using Weblate (Portuguese)
Currently translated at 18.1% (2 of 11 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/pt/
2025-05-04 09:03:30 +02:00
翻譯得真好下次別翻了 d2f5be6e19 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 72.5% (188 of 259 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/zh_Hant/
2025-05-04 09:03:30 +02:00
Kachelkaiser 27a5b6b9f2 Translated using Weblate (German)
Currently translated at 100.0% (11 of 11 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/de/
2025-05-04 09:03:30 +02:00
大王叫我来巡山 2ae048b1de Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (259 of 259 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/zh_Hans/
2025-05-04 09:03:30 +02:00
Matthaiks a890c83088 Translated using Weblate (Polish)
Currently translated at 100.0% (259 of 259 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/pl/
2025-05-04 09:03:30 +02:00
Kachelkaiser 3eedbdccba Translated using Weblate (German)
Currently translated at 100.0% (259 of 259 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/de/
2025-05-04 09:03:30 +02:00
solokot 73b3d03a25 Translated using Weblate (Russian)
Currently translated at 100.0% (259 of 259 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/ru/
2025-05-04 09:03:30 +02:00
catelixor b74f84abc2 Translated using Weblate (Czech)
Currently translated at 89.9% (232 of 258 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/cs/
2025-05-04 09:03:30 +02:00
Matthaiks bbb056e9d1 Translated using Weblate (Polish)
Currently translated at 100.0% (258 of 258 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/pl/
2025-05-04 09:03:30 +02:00
தமிழ்நேரம் 0d953a32be Translated using Weblate (Tamil)
Currently translated at 100.0% (256 of 256 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/ta/
2025-05-04 09:03:30 +02:00
sgauthiertremblay 3383bb6b45 Translated using Weblate (French)
Currently translated at 77.3% (198 of 256 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/fr/
2025-05-04 09:03:30 +02:00
தமிழ்நேரம் afcb801806 Translated using Weblate (Tamil)
Currently translated at 100.0% (11 of 11 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/ta/
2025-05-04 09:03:30 +02:00
大王叫我来巡山 494835a5a4 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (256 of 256 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/zh_Hans/
2025-05-04 09:03:30 +02:00
catelixor 2f784f563f Translated using Weblate (Czech)
Currently translated at 89.8% (230 of 256 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/cs/
2025-05-04 09:03:30 +02:00
catelixor 11d6fd2ed9 Translated using Weblate (Czech)
Currently translated at 63.6% (7 of 11 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/cs/
2025-05-04 09:03:30 +02:00
Matthaiks 0e8138ba6b Translated using Weblate (Polish)
Currently translated at 100.0% (256 of 256 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/pl/
2025-05-04 09:03:30 +02:00
Matthaiks 0eb3719972 Translated using Weblate (Polish)
Currently translated at 100.0% (11 of 11 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/pl/
2025-05-04 09:03:30 +02:00
solokot 196cd34ef7 Translated using Weblate (Russian)
Currently translated at 100.0% (256 of 256 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/ru/
2025-05-04 09:03:30 +02:00
Matthaiks ed42a2f7df Translated using Weblate (Polish)
Currently translated at 99.2% (254 of 256 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/pl/
2025-05-04 09:03:30 +02:00
vm 9b944bf51e Translated using Weblate (Hungarian)
Currently translated at 20.0% (2 of 10 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/hu/
2025-05-04 09:03:30 +02:00
Faisal Gull b8c792418d Translated using Weblate (Urdu)
Currently translated at 100.0% (254 of 254 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/ur/
2025-05-04 09:03:30 +02:00
vm 6d8a38c65b Translated using Weblate (Hungarian)
Currently translated at 5.1% (13 of 254 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/hu/
2025-05-04 09:03:30 +02:00
翻譯得真好下次別翻了 09c1c68aad Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 72.4% (184 of 254 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/zh_Hant/
2025-05-04 09:03:30 +02:00
大王叫我来巡山 36b8353b07 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (254 of 254 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/zh_Hans/
2025-05-04 09:03:30 +02:00
catelixor f10ed1269e Translated using Weblate (Czech)
Currently translated at 87.7% (223 of 254 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/cs/
2025-05-04 09:03:30 +02:00
Matthaiks e6e35f01d7 Translated using Weblate (Polish)
Currently translated at 100.0% (254 of 254 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/pl/
2025-05-04 09:03:30 +02:00
Kachelkaiser 35737bdd84 Translated using Weblate (German)
Currently translated at 100.0% (254 of 254 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/de/
2025-05-04 09:03:30 +02:00
solokot 5a6960bffd Translated using Weblate (Russian)
Currently translated at 100.0% (254 of 254 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/ru/
2025-05-04 09:03:30 +02:00
翻譯得真好下次別翻了 10cefe2ab0 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 70.8% (170 of 240 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/zh_Hant/
2025-05-04 09:03:30 +02:00
翻譯得真好下次別翻了 add53834e7 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 30.0% (3 of 10 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/zh_Hant/
2025-05-04 09:03:30 +02:00
catelixor f740ec57ea Translated using Weblate (Czech)
Currently translated at 80.0% (192 of 240 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/cs/
2025-05-04 09:03:30 +02:00
catelixor 7eadfe2af2 Translated using Weblate (Czech)
Currently translated at 50.0% (5 of 10 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/cs/
2025-05-04 09:03:30 +02:00
Faisal Gull 001a4358e1 Translated using Weblate (Urdu)
Currently translated at 100.0% (10 of 10 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/ur/
2025-05-04 09:03:30 +02:00
Faisal Gull 31abe64513 Translated using Weblate (Urdu)
Currently translated at 100.0% (240 of 240 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/ur/
2025-05-04 09:03:30 +02:00
翻譯得真好下次別翻了 900aa86c29 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 68.7% (165 of 240 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/zh_Hant/
2025-05-04 09:03:29 +02:00
大王叫我来巡山 264e17f30b Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (240 of 240 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/zh_Hans/
2025-05-04 09:03:29 +02:00
catelixor fbc3ff3276 Translated using Weblate (Czech)
Currently translated at 73.7% (177 of 240 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/cs/
2025-05-04 09:03:29 +02:00
Kachelkaiser 151cdd4e03 Translated using Weblate (German)
Currently translated at 100.0% (240 of 240 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/de/
2025-05-04 09:03:29 +02:00
Kachelkaiser f0deb1294a Translated using Weblate (German)
Currently translated at 100.0% (10 of 10 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/de/
2025-05-04 09:03:29 +02:00
solokot 19efb08cb9 Translated using Weblate (Russian)
Currently translated at 100.0% (240 of 240 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/ru/
2025-05-04 09:03:29 +02:00
Matthaiks 9f4b9ca771 Translated using Weblate (Polish)
Currently translated at 100.0% (240 of 240 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/pl/
2025-05-04 09:03:29 +02:00
翻譯得真好下次別翻了 a60e480be0 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 63.8% (152 of 238 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/zh_Hant/
2025-05-04 09:03:29 +02:00
kometchtech f28e21ef79 Translated using Weblate (Japanese)
Currently translated at 41.1% (98 of 238 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/ja/
2025-05-04 09:03:29 +02:00
Matthaiks b4ca1e63c4 Translated using Weblate (Polish)
Currently translated at 100.0% (10 of 10 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/pl/
2025-05-04 09:03:29 +02:00
翻譯得真好下次別翻了 e35f4bbb52 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 63.4% (151 of 238 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/zh_Hant/
2025-05-04 09:03:29 +02:00
kometchtech f0e575da69 Translated using Weblate (Japanese)
Currently translated at 39.0% (93 of 238 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/ja/
2025-05-04 09:03:29 +02:00
kometchtech a6b7d2091a Translated using Weblate (Japanese)
Currently translated at 100.0% (9 of 9 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/ja/
2025-05-04 09:03:29 +02:00
Matthaiks 324a1a66c5 Translated using Weblate (Polish)
Currently translated at 100.0% (9 of 9 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/pl/
2025-05-04 09:03:29 +02:00
52 changed files with 670 additions and 1038 deletions
-2
View File
@@ -223,8 +223,6 @@ dependencies {
// shizuku // shizuku
implementation(libs.shizuku.api) implementation(libs.shizuku.api)
implementation(libs.shizuku.provider) implementation(libs.shizuku.provider)
implementation(libs.reorderable)
} }
tasks.register<Copy>("copyLicenseeJsonToAssets") { tasks.register<Copy>("copyLicenseeJsonToAssets") {
@@ -1,302 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 18,
"identityHash": "505728bad740c12bab998a066b569333",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_disable_kill_switch_on_trusted_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT false, `split_tunnel_apps` TEXT NOT NULL DEFAULT '', `wifi_detection_method` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAmneziaEnabled",
"columnName": "is_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isVpnKillSwitchEnabled",
"columnName": "is_vpn_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelKillSwitchEnabled",
"columnName": "is_kernel_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "isDisableKillSwitchOnTrustedEnabled",
"columnName": "is_disable_kill_switch_on_trusted_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnUnsecureEnabled",
"columnName": "is_tunnel_on_unsecure_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "splitTunnelApps",
"columnName": "split_tunnel_apps",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "wifiDetectionMethod",
"columnName": "wifi_detection_method",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `is_ipv4_preferred` INTEGER NOT NULL DEFAULT true, `position` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingInterval",
"columnName": "ping_interval",
"affinity": "INTEGER",
"defaultValue": "null"
},
{
"fieldPath": "pingCooldown",
"columnName": "ping_cooldown",
"affinity": "INTEGER",
"defaultValue": "null"
},
{
"fieldPath": "pingIp",
"columnName": "ping_ip",
"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"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
]
}
],
"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, '505728bad740c12bab998a066b569333')"
]
}
}
@@ -1,7 +1,9 @@
package com.zaneschepke.wireguardautotunnel package com.zaneschepke.wireguardautotunnel
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@@ -25,6 +27,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -51,7 +54,6 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.Loca
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.TunnelAutoTunnelScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.TunnelAutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.ConfigScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.sort.SortScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.SplitTunnelScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.SplitTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.TunnelOptionsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.TunnelOptionsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.pin.PinLockScreen import com.zaneschepke.wireguardautotunnel.ui.screens.pin.PinLockScreen
@@ -72,6 +74,7 @@ import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject
import kotlin.system.exitProcess import kotlin.system.exitProcess
import org.amnezia.awg.backend.GoBackend.VpnService import org.amnezia.awg.backend.GoBackend.VpnService
import timber.log.Timber
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@@ -319,7 +322,6 @@ class MainActivity : AppCompatActivity() {
) )
} }
} }
composable<Route.Sort> { SortScreen(appUiState, viewModel) }
} }
} }
} }
@@ -328,4 +330,22 @@ class MainActivity : AppCompatActivity() {
} }
} }
} }
override fun onResume() {
super.onResume()
checkPermissionAndNotify()
}
private fun checkPermissionAndNotify() {
val hasLocation =
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
if (lastLocationPermissionState != hasLocation) {
Timber.d("Location permission changed to: $hasLocation")
if (hasLocation) {
networkMonitor.sendLocationPermissionsGrantedBroadcast()
}
lastLocationPermissionState = hasLocation
}
}
} }
@@ -8,6 +8,7 @@ import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.zaneschepke.networkmonitor.NetworkMonitor import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.networkmonitor.NetworkStatus
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
@@ -216,8 +217,9 @@ class TunnelForegroundService : LifecycleService() {
} }
private suspend fun startNetworkMonitorJob() { private suspend fun startNetworkMonitorJob() {
networkMonitor.connectivityStateFlow.flowOn(ioDispatcher).collectLatest { status -> networkMonitor.networkStatusFlow.flowOn(ioDispatcher).collectLatest { status ->
isNetworkConnected.value = status.hasConnectivity() val isAvailable = status !is NetworkStatus.Disconnected
isNetworkConnected.value = isAvailable
Timber.d("Network available: $status") Timber.d("Network available: $status")
} }
} }
@@ -7,8 +7,8 @@ import android.os.PowerManager
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.zaneschepke.networkmonitor.ConnectivityState
import com.zaneschepke.networkmonitor.NetworkMonitor import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.networkmonitor.NetworkStatus
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
@@ -158,13 +158,20 @@ class AutoTunnelService : LifecycleService() {
} }
} }
private fun buildNetworkState(connectivityState: ConnectivityState): NetworkState { private fun buildNetworkState(networkStatus: NetworkStatus): NetworkState {
return with(autoTunnelStateFlow.value.networkState) { return with(autoTunnelStateFlow.value.networkState) {
val wifiName =
when (networkStatus) {
is NetworkStatus.Connected -> {
networkStatus.wifiSsid
}
else -> null
}
copy( copy(
isWifiConnected = connectivityState.wifiState.connected, isWifiConnected = networkStatus.wifiConnected,
isMobileDataConnected = connectivityState.cellularConnected, isMobileDataConnected = networkStatus.cellularConnected,
isEthernetConnected = connectivityState.ethernetConnected, isEthernetConnected = networkStatus.ethernetConnected,
wifiName = connectivityState.wifiState.ssid, wifiName = wifiName,
) )
} }
} }
@@ -182,7 +189,7 @@ class AutoTunnelService : LifecycleService() {
old.isKernelEnabled == new.isKernelEnabled old.isKernelEnabled == new.isKernelEnabled
} // Only emit when isKernelEnabled changes } // Only emit when isKernelEnabled changes
.flatMapLatest { .flatMapLatest {
networkMonitor.connectivityStateFlow.flowOn(ioDispatcher).map { networkMonitor.networkStatusFlow.flowOn(ioDispatcher).map {
buildNetworkState(it) buildNetworkState(it)
} }
} }
@@ -13,7 +13,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
@Database( @Database(
entities = [Settings::class, TunnelConfig::class], entities = [Settings::class, TunnelConfig::class],
version = 18, version = 17,
autoMigrations = autoMigrations =
[ [
AutoMigration(from = 1, to = 2), AutoMigration(from = 1, to = 2),
@@ -32,7 +32,6 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
AutoMigration(from = 14, to = 15), AutoMigration(from = 14, to = 15),
AutoMigration(from = 15, to = 16), AutoMigration(from = 15, to = 16),
AutoMigration(from = 16, to = 17, spec = WifiDetectionMigration::class), AutoMigration(from = 16, to = 17, spec = WifiDetectionMigration::class),
AutoMigration(from = 17, to = 18),
], ],
exportSchema = true, exportSchema = true,
) )
@@ -46,6 +46,5 @@ interface TunnelConfigDao {
@Query("SELECT * FROM TUNNELCONFIG WHERE is_mobile_data_tunnel=1") @Query("SELECT * FROM TUNNELCONFIG WHERE is_mobile_data_tunnel=1")
suspend fun findByMobileDataTunnel(): TunnelConfigs suspend fun findByMobileDataTunnel(): TunnelConfigs
@Query("SELECT * FROM tunnelconfig ORDER BY position ASC") @Query("SELECT * FROM tunnelconfig") fun getAllFlow(): Flow<MutableList<TunnelConfig>>
fun getAllFlow(): Flow<MutableList<TunnelConfig>>
} }
@@ -24,10 +24,9 @@ data class TunnelConfig(
@ColumnInfo(name = "ping_cooldown", defaultValue = "null") val pingCooldown: Long? = null, @ColumnInfo(name = "ping_cooldown", defaultValue = "null") val pingCooldown: Long? = null,
@ColumnInfo(name = "ping_ip", defaultValue = "null") var pingIp: String? = null, @ColumnInfo(name = "ping_ip", defaultValue = "null") var pingIp: String? = null,
@ColumnInfo(name = "is_ethernet_tunnel", defaultValue = "false") @ColumnInfo(name = "is_ethernet_tunnel", defaultValue = "false")
val isEthernetTunnel: Boolean = false, var isEthernetTunnel: Boolean = false,
@ColumnInfo(name = "is_ipv4_preferred", defaultValue = "true") @ColumnInfo(name = "is_ipv4_preferred", defaultValue = "true")
val isIpv4Preferred: Boolean = true, var isIpv4Preferred: Boolean = true,
@ColumnInfo(name = "position", defaultValue = "0") val position: Int = 0,
) { ) {
companion object { companion object {
@@ -21,7 +21,6 @@ object TunnelConfigMapper {
pingIp, pingIp,
isEthernetTunnel, isEthernetTunnel,
isIpv4Preferred, isIpv4Preferred,
position,
) )
} }
} }
@@ -43,7 +42,6 @@ object TunnelConfigMapper {
pingIp, pingIp,
isEthernetTunnel, isEthernetTunnel,
isIpv4Preferred, isIpv4Preferred,
position,
) )
} }
} }
@@ -26,7 +26,6 @@ data class TunnelConf(
val pingIp: String? = null, val pingIp: String? = null,
val isEthernetTunnel: Boolean = false, val isEthernetTunnel: Boolean = false,
val isIpv4Preferred: Boolean = true, val isIpv4Preferred: Boolean = true,
val position: Int = 0,
@Transient private var stateChangeCallback: ((Any) -> Unit)? = null, @Transient private var stateChangeCallback: ((Any) -> Unit)? = null,
) : Tunnel, org.amnezia.awg.backend.Tunnel { ) : Tunnel, org.amnezia.awg.backend.Tunnel {
@@ -45,6 +45,4 @@ sealed class Route {
@Serializable data class TunnelAutoTunnel(val id: Int) : Route() @Serializable data class TunnelAutoTunnel(val id: Int) : Route()
@Serializable data object Logs : Route() @Serializable data object Logs : Route()
@Serializable data object Sort : Route()
} }
@@ -3,41 +3,68 @@ package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun ExpandingRowListItem( fun ExpandingRowListItem(
leading: @Composable () -> Unit, leading: @Composable () -> Unit,
text: String, text: String,
onHold: () -> Unit,
onClick: () -> Unit,
onDoubleClick: () -> Unit,
trailing: @Composable () -> Unit, trailing: @Composable () -> Unit,
isSelected: Boolean, isSelected: Boolean,
expanded: @Composable () -> Unit, expanded: (@Composable () -> Unit)?,
modifier: Modifier = Modifier,
) { ) {
val isTv = LocalIsAndroidTV.current
val haptic = LocalHapticFeedback.current
val interactionSource = remember { MutableInteractionSource() }
Box( Box(
modifier = modifier =
modifier Modifier.animateContentSize()
.animateContentSize()
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(8.dp))
.background( .background(
if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
else Color.Transparent else Color.Transparent
) )
.then(
if (!isTv) {
Modifier.combinedClickable(
interactionSource = interactionSource,
indication = ripple(),
onClick = onClick,
onLongClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
onHold()
},
onDoubleClick = onDoubleClick,
)
} else Modifier
)
) { ) {
Column { Column {
Row( Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp).height(48.dp), modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
@@ -57,7 +84,7 @@ fun ExpandingRowListItem(
} }
trailing() trailing()
} }
expanded() expanded?.invoke()
} }
} }
} }
@@ -10,6 +10,7 @@ import androidx.compose.material.icons.rounded.QuestionMark
import androidx.compose.material.icons.rounded.Settings import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -67,42 +68,53 @@ fun BottomNavbar(appUiState: AppUiState) {
onClick = { navController.goFromRoot(Route.Support) }, onClick = { navController.goFromRoot(Route.Support) },
), ),
) )
// Define ripple configuration based on platform
val rippleConfiguration =
if (isTv) {
RippleConfiguration()
} else {
null
}
NavigationBar(containerColor = MaterialTheme.colorScheme.surface) { // Apply ripple configuration only if needed
items.forEach { item -> CompositionLocalProvider(LocalRippleConfiguration provides rippleConfiguration) {
val isSelected = navBackStackEntry.isCurrentRoute(item.route::class) NavigationBar(containerColor = MaterialTheme.colorScheme.surface) {
val interactionSource = remember { MutableInteractionSource() } items.forEach { item ->
val isSelected = navBackStackEntry.isCurrentRoute(item.route::class)
val interactionSource = remember { MutableInteractionSource() }
NavigationBarItem( NavigationBarItem(
icon = { icon = {
if (item.active) { if (item.active) {
BadgedBox( BadgedBox(
badge = { badge = {
Badge( Badge(
modifier = Modifier.offset(x = 8.dp, y = (-8).dp).size(6.dp), modifier =
containerColor = SilverTree, Modifier.offset(x = 8.dp, y = (-8).dp).size(6.dp),
) containerColor = SilverTree,
)
}
) {
Icon(imageVector = item.icon, contentDescription = item.name)
} }
) { } else {
Icon(imageVector = item.icon, contentDescription = item.name) Icon(imageVector = item.icon, contentDescription = item.name)
} }
} else { },
Icon(imageVector = item.icon, contentDescription = item.name) onClick = { navController.goFromRoot(item.route) },
} selected = isSelected,
}, enabled = true,
onClick = { navController.goFromRoot(item.route) }, label = null,
selected = isSelected, alwaysShowLabel = false,
enabled = true, colors =
label = null, NavigationBarItemDefaults.colors(
alwaysShowLabel = false, selectedIconColor = MaterialTheme.colorScheme.primary,
colors = unselectedIconColor = MaterialTheme.colorScheme.onBackground,
NavigationBarItemDefaults.colors( indicatorColor = Color.Transparent,
selectedIconColor = MaterialTheme.colorScheme.primary, ),
unselectedIconColor = MaterialTheme.colorScheme.onBackground, interactionSource = interactionSource,
indicatorColor = Color.Transparent, )
), }
interactionSource = interactionSource,
)
} }
} }
} }
@@ -4,7 +4,6 @@ import android.os.Build
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.Sort
import androidx.compose.material.icons.rounded.* import androidx.compose.material.icons.rounded.*
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@@ -12,7 +11,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -30,7 +28,6 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
import com.zaneschepke.wireguardautotunnel.viewmodel.event.UiEvent
@Composable @Composable
fun currentNavBackStackEntryAsNavBarState( fun currentNavBackStackEntryAsNavBarState(
@@ -63,40 +60,35 @@ fun currentNavBackStackEntryAsNavBarState(
Row { Row {
if (selectedCount == 0) { if (selectedCount == 0) {
val showSort = remember(uiState.tunnels) { uiState.tunnels.size > 1 }
if (showSort)
ActionIconButton(Icons.AutoMirrored.Rounded.Sort, R.string.sort) {
navController.navigate(Route.Sort)
}
ActionIconButton(Icons.Rounded.Add, R.string.add_tunnel) { ActionIconButton(Icons.Rounded.Add, R.string.add_tunnel) {
viewModel.handleEvent( viewModel.handleEvent(
AppEvent.SetBottomSheet(AppViewState.BottomSheet.IMPORT_TUNNELS) AppEvent.SetBottomSheet(AppViewState.BottomSheet.IMPORT_TUNNELS)
) )
} }
return@Row } else {
} ActionIconButton(Icons.Rounded.SelectAll, R.string.select_all) {
ActionIconButton(Icons.Rounded.SelectAll, R.string.select_all) { viewModel.handleEvent(AppEvent.ToggleSelectAllTunnels)
viewModel.handleEvent(AppEvent.ToggleSelectAllTunnels)
}
// due to permissions, and SAF issues on TV, not support less than Android 10 on
// Android TV for file exports
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ActionIconButton(Icons.Rounded.Download, R.string.download) {
viewModel.handleEvent(
AppEvent.SetBottomSheet(AppViewState.BottomSheet.EXPORT_TUNNELS)
)
} }
} // due to permissions, and SAF issues on TV, not support less than Android 10 on
// Android TV for file exports
if (selectedCount == 1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ActionIconButton(Icons.Rounded.CopyAll, R.string.copy) { ActionIconButton(Icons.Rounded.Download, R.string.download) {
viewModel.handleEvent(AppEvent.CopySelectedTunnel) viewModel.handleEvent(
AppEvent.SetBottomSheet(AppViewState.BottomSheet.EXPORT_TUNNELS)
)
}
} }
}
if (showDelete) { if (selectedCount == 1) {
ActionIconButton(Icons.Rounded.Delete, R.string.delete_tunnel) { ActionIconButton(Icons.Rounded.CopyAll, R.string.copy) {
viewModel.handleEvent(AppEvent.SetShowModal(AppViewState.ModalType.DELETE)) viewModel.handleEvent(AppEvent.CopySelectedTunnel)
}
}
if (showDelete) {
ActionIconButton(Icons.Rounded.Delete, R.string.delete_tunnel) {
viewModel.handleEvent(AppEvent.SetShowModal(AppViewState.ModalType.DELETE))
}
} }
} }
} }
@@ -220,25 +212,6 @@ fun currentNavBackStackEntryAsNavBarState(
route = Route.Support, route = Route.Support,
) )
backStackEntry.isCurrentRoute(Route.Sort::class) -> {
NavBarState(
showTop = true,
showBottom = true,
topTitle = { Text(stringResource(R.string.sort)) },
topTrailing = {
Row {
ActionIconButton(Icons.Rounded.SortByAlpha, R.string.sort) {
viewModel.handleUiEvent(UiEvent.SortTunnels)
}
ActionIconButton(Icons.Rounded.Save, R.string.save) {
viewModel.handleEvent(AppEvent.InvokeScreenAction)
}
}
},
route = Route.Sort,
)
}
backStackEntry.isCurrentRoute(Route.License::class) -> { backStackEntry.isCurrentRoute(Route.License::class) -> {
NavBarState( NavBarState(
showTop = true, showTop = true,
@@ -40,8 +40,8 @@ fun NetworkTunnelingItems(uiState: AppUiState, viewModel: AppViewModel): List<Se
}, },
description = { description = {
val cellularActive = val cellularActive =
remember(uiState.connectivityState) { remember(uiState.networkStatus) {
uiState.connectivityState?.cellularConnected ?: false uiState.networkStatus?.cellularConnected ?: false
} }
Text( Text(
text = text =
@@ -77,8 +77,8 @@ fun NetworkTunnelingItems(uiState: AppUiState, viewModel: AppViewModel): List<Se
}, },
description = { description = {
val ethernetActive = val ethernetActive =
remember(uiState.connectivityState) { remember(uiState.networkStatus) {
uiState.connectivityState?.ethernetConnected ?: false uiState.networkStatus?.ethernetConnected ?: false
} }
Text( Text(
text = text =
@@ -18,6 +18,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
import com.zaneschepke.networkmonitor.NetworkStatus
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
@@ -67,12 +68,11 @@ fun WifiTunnelingItems(
}, },
description = { description = {
val wifiInfo by val wifiInfo by
remember(uiState.connectivityState) { remember(uiState.networkStatus) {
derivedStateOf { derivedStateOf {
uiState.connectivityState (uiState.networkStatus as? NetworkStatus.Connected)
?.wifiState ?.takeIf { it.wifiConnected }
?.takeIf { it.connected } .let { Pair(it?.wifiSsid, it?.securityType) }
.let { Pair(it?.ssid, it?.securityType) }
} }
} }
val (wifiName, securityType) = wifiInfo val (wifiName, securityType) = wifiInfo
@@ -1,9 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@@ -11,7 +9,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.overscroll import androidx.compose.foundation.overscroll
import androidx.compose.foundation.rememberOverscrollEffect import androidx.compose.foundation.rememberOverscrollEffect
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -28,6 +25,8 @@ import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
import java.text.Collator
import java.util.*
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@@ -41,10 +40,17 @@ fun TunnelList(
val isTv = LocalIsAndroidTV.current val isTv = LocalIsAndroidTV.current
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
val collator = Collator.getInstance(Locale.getDefault())
val lazyListState = rememberLazyListState() val sortedTunnels =
remember(appUiState.tunnels) {
val sortedTunnels = remember(appUiState.tunnels) { appUiState.tunnels.sortedBy { it.position } } appUiState.tunnels.sortedWith(
compareBy(
// primary tunnel first
{ !it.isPrimaryTunnel },
{ collator.compare(it.tunName, "") },
)
)
}
LazyColumn( LazyColumn(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
@@ -53,7 +59,7 @@ fun TunnelList(
modifier modifier
.pointerInput(Unit) { if (appUiState.tunnels.isEmpty()) return@pointerInput } .pointerInput(Unit) { if (appUiState.tunnels.isEmpty()) return@pointerInput }
.overscroll(rememberOverscrollEffect()), .overscroll(rememberOverscrollEffect()),
state = lazyListState, state = rememberLazyListState(0, appUiState.tunnels.count()),
userScrollEnabled = true, userScrollEnabled = true,
reverseLayout = false, reverseLayout = false,
flingBehavior = ScrollableDefaults.flingBehavior(), flingBehavior = ScrollableDefaults.flingBehavior(),
@@ -69,36 +75,26 @@ fun TunnelList(
val selected = remember(selectedTunnels) { selectedTunnels.any { it.id == tunnel.id } } val selected = remember(selectedTunnels) { selectedTunnels.any { it.id == tunnel.id } }
TunnelRowItem( TunnelRowItem(
state = tunnelState, state = tunnelState,
expanded = appUiState.appState.expandedTunnelIds.contains(tunnel.id),
isSelected = selected, isSelected = selected,
tunnel = tunnel, tunnel = tunnel,
tunnelState = tunnelState, tunnelState = tunnelState,
onTvClick = { onClick = {
navController.navigate(Route.TunnelOptions(tunnel.id)) if (selectedTunnels.isNotEmpty() && !isTv) {
viewModel.handleEvent(AppEvent.ClearSelectedTunnels) viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(tunnel))
} else {
navController.navigate(Route.TunnelOptions(tunnel.id))
viewModel.handleEvent(AppEvent.ClearSelectedTunnels)
}
},
onDoubleClick = {
viewModel.handleEvent(AppEvent.ToggleTunnelStatsExpanded(tunnel.id))
}, },
onToggleSelectedTunnel = { onToggleSelectedTunnel = {
viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(it)) viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(it))
}, },
onSwitchClick = { checked -> onToggleTunnel(tunnel, checked) }, onSwitchClick = { checked -> onToggleTunnel(tunnel, checked) },
isTv = isTv, isTv = isTv,
modifier =
if (!isTv)
Modifier.combinedClickable(
onClick = {
if (selectedTunnels.isNotEmpty()) {
viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(tunnel))
} else {
navController.navigate(Route.TunnelOptions(tunnel.id))
viewModel.handleEvent(AppEvent.ClearSelectedTunnels)
}
},
onLongClick = {
viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(tunnel))
},
interactionSource = remember { MutableInteractionSource() },
indication = null,
)
else Modifier,
) )
} }
} }
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Circle import androidx.compose.material.icons.rounded.Circle
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.Settings import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.icons.rounded.SettingsEthernet import androidx.compose.material.icons.rounded.SettingsEthernet
import androidx.compose.material.icons.rounded.Smartphone import androidx.compose.material.icons.rounded.Smartphone
@@ -21,7 +22,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
@@ -32,13 +32,14 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.asColor
fun TunnelRowItem( fun TunnelRowItem(
state: TunnelState, state: TunnelState,
isSelected: Boolean, isSelected: Boolean,
expanded: Boolean,
tunnel: TunnelConf, tunnel: TunnelConf,
tunnelState: TunnelState, tunnelState: TunnelState,
onTvClick: () -> Unit, onClick: () -> Unit,
onDoubleClick: () -> Unit,
onToggleSelectedTunnel: (TunnelConf) -> Unit, onToggleSelectedTunnel: (TunnelConf) -> Unit,
onSwitchClick: (Boolean) -> Unit, onSwitchClick: (Boolean) -> Unit,
isTv: Boolean, isTv: Boolean,
modifier: Modifier = Modifier,
) { ) {
val leadingIconColor = val leadingIconColor =
remember(state) { remember(state) {
@@ -77,10 +78,13 @@ fun TunnelRowItem(
} }
}, },
text = tunnel.tunName, text = tunnel.tunName,
onHold = { if (!isTv) onToggleSelectedTunnel(tunnel) },
onClick = { if (!isTv) onClick() },
onDoubleClick = { if (!isTv) onDoubleClick() },
expanded = { expanded = {
if (tunnelState.status != TunnelStatus.Down) { if (expanded) {
TunnelStatisticsRow(tunnelState.statistics, tunnel) TunnelStatisticsRow(tunnelState.statistics, tunnel)
} } else null
}, },
trailing = { trailing = {
Row( Row(
@@ -88,7 +92,13 @@ fun TunnelRowItem(
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End), horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
) { ) {
if (isTv) { if (isTv) {
IconButton(onClick = onTvClick) { IconButton(onClick = onDoubleClick) {
Icon(
Icons.Rounded.KeyboardArrowDown,
contentDescription = stringResource(R.string.info),
)
}
IconButton(onClick = onClick) {
Icon( Icon(
Icons.Rounded.Settings, Icons.Rounded.Settings,
contentDescription = stringResource(R.string.settings), contentDescription = stringResource(R.string.settings),
@@ -99,6 +109,5 @@ fun TunnelRowItem(
} }
}, },
isSelected = isSelected, isSelected = isSelected,
modifier = modifier,
) )
} }
@@ -1,7 +1,10 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -94,17 +97,15 @@ fun TunnelStatisticsRow(statistics: TunnelStatistics?, tunnelConf: TunnelConf) {
) )
} }
if (endpoint != null) { if (endpoint != null) {
AnimatedVisibility(visible = true) { Row(
Row( verticalAlignment = Alignment.CenterVertically,
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.Start),
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.Start), ) {
) { Text(
Text( "endpoint: $endpoint",
stringResource(R.string.endpoint).lowercase() + ": $endpoint", style = MaterialTheme.typography.bodySmall,
style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline,
color = MaterialTheme.colorScheme.outline, )
)
}
} }
} }
} }
@@ -1,168 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main.sort
import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.overscroll
import androidx.compose.foundation.rememberOverscrollEffect
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.util.extensions.isSortedBy
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
import com.zaneschepke.wireguardautotunnel.viewmodel.event.UiEvent
import sh.calvin.reorderable.DragGestureDetector
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
@Composable
fun SortScreen(appUiState: AppUiState, viewModel: AppViewModel) {
val hapticFeedback = LocalHapticFeedback.current
val isTv = LocalIsAndroidTV.current
var sortAscending by remember { mutableStateOf<Boolean?>(null) }
var sortedTunnels by remember { mutableStateOf(appUiState.tunnels.sortedBy { it.position }) }
LaunchedEffect(Unit) {
viewModel.uiEvent.collect { uiEvent ->
when (uiEvent) {
UiEvent.SortTunnels -> {
sortAscending =
when (sortAscending) {
null -> !sortedTunnels.isSortedBy { it.name }
true -> false
false -> null
}
sortedTunnels =
when (sortAscending) {
true -> sortedTunnels.sortedBy { it.name }
false -> sortedTunnels.sortedByDescending { it.name }
null -> sortedTunnels.sortedBy { it.position }
}
}
}
}
}
LaunchedEffect(Unit) {
viewModel.handleEvent(
AppEvent.SetScreenAction {
viewModel.handleEvent(
AppEvent.SaveAllConfigs(
sortedTunnels.mapIndexed { index, conf -> conf.copy(position = index) }
)
)
viewModel.handleEvent(AppEvent.PopBackStack(true))
}
)
}
val lazyListState = rememberLazyListState()
val reorderableLazyListState =
rememberReorderableLazyListState(
lazyListState,
scrollThresholdPadding = WindowInsets.systemBars.asPaddingValues(),
) { from, to ->
sortedTunnels =
sortedTunnels.toMutableList().apply { add(to.index, removeAt(from.index)) }
hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick)
}
LazyColumn(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(5.dp, Alignment.Top),
modifier =
Modifier.pointerInput(Unit) { if (appUiState.tunnels.isEmpty()) return@pointerInput }
.overscroll(rememberOverscrollEffect())
.padding(horizontal = 16.dp, vertical = 24.dp),
state = lazyListState,
userScrollEnabled = true,
reverseLayout = false,
flingBehavior = ScrollableDefaults.flingBehavior(),
) {
itemsIndexed(sortedTunnels, key = { _, tunnel -> tunnel.id }) { index, tunnel ->
ReorderableItem(reorderableLazyListState, tunnel.id) { isDragging ->
ExpandingRowListItem(
leading = {},
text = tunnel.name,
trailing = {
if (!isTv)
Icon(
Icons.Default.DragHandle,
stringResource(
com.zaneschepke.wireguardautotunnel.R.string.drag_handle
),
)
else
Row {
IconButton(
onClick = {
sortedTunnels =
sortedTunnels.toMutableList().apply {
add(index - 1, removeAt(index))
}
},
enabled = index != 0,
) {
Icon(
Icons.Default.ArrowUpward,
stringResource(
com.zaneschepke.wireguardautotunnel.R.string.move_up
),
)
}
IconButton(
onClick = {
sortedTunnels =
sortedTunnels.toMutableList().apply {
add(index + 1, removeAt(index))
}
},
enabled = index != sortedTunnels.count() - 1,
) {
Icon(
Icons.Default.ArrowDownward,
stringResource(R.string.move_down),
)
}
}
},
isSelected = isDragging,
expanded = {},
modifier =
Modifier.draggableHandle(
onDragStarted = {
hapticFeedback.performHapticFeedback(
HapticFeedbackType.GestureThresholdActivate
)
},
onDragStopped = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureEnd)
},
dragGestureDetector = DragGestureDetector.LongPress,
),
)
}
}
}
}
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.state package com.zaneschepke.wireguardautotunnel.ui.state
import com.zaneschepke.networkmonitor.ConnectivityState import com.zaneschepke.networkmonitor.NetworkStatus
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralState import com.zaneschepke.wireguardautotunnel.data.entity.GeneralState
import com.zaneschepke.wireguardautotunnel.data.mapper.GeneralStateMapper import com.zaneschepke.wireguardautotunnel.data.mapper.GeneralStateMapper
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
@@ -16,5 +16,5 @@ data class AppUiState(
val isAutoTunnelActive: Boolean = false, val isAutoTunnelActive: Boolean = false,
val appConfigurationChange: Boolean = false, val appConfigurationChange: Boolean = false,
val isAppLoaded: Boolean = false, val isAppLoaded: Boolean = false,
val connectivityState: ConnectivityState? = null, val networkStatus: NetworkStatus? = null,
) )
@@ -3,17 +3,18 @@ package com.zaneschepke.wireguardautotunnel.ui.theme
import android.app.Activity import android.app.Activity
import android.os.Build import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.ripple.RippleAlpha import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.* import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
private val DarkColorScheme = private val DarkColorScheme =
darkColorScheme( darkColorScheme(
@@ -48,11 +49,9 @@ enum class Theme {
DYNAMIC, DYNAMIC,
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun WireguardAutoTunnelTheme(theme: Theme = Theme.AUTOMATIC, content: @Composable () -> Unit) { fun WireguardAutoTunnelTheme(theme: Theme = Theme.AUTOMATIC, content: @Composable () -> Unit) {
val context = LocalContext.current val context = LocalContext.current
val isTv = LocalIsAndroidTV.current
var isDark = isSystemInDarkTheme() var isDark = isSystemInDarkTheme()
val autoTheme = if (isDark) DarkColorScheme else LightColorScheme val autoTheme = if (isDark) DarkColorScheme else LightColorScheme
val colorScheme = val colorScheme =
@@ -106,22 +105,5 @@ fun WireguardAutoTunnelTheme(theme: Theme = Theme.AUTOMATIC, content: @Composabl
} }
} }
// Make hover/ripple more obvious on TV MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content)
val rippleConfig =
if (isTv) {
RippleConfiguration(
color = colorScheme.outline.copy(alpha = 0.12f),
rippleAlpha =
RippleAlpha(
pressedAlpha = 0.7f,
focusedAlpha = 0.6f,
draggedAlpha = 0.9f,
hoveredAlpha = 0.3f,
),
)
} else null
CompositionLocalProvider(LocalRippleConfiguration provides rippleConfig) {
MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content)
}
} }
@@ -24,7 +24,3 @@ typealias Packages = List<PackageInfo>
fun <T> MutableList<T>.addAllUnique(elements: Collection<T>, comparator: (T, T) -> Boolean) { fun <T> MutableList<T>.addAllUnique(elements: Collection<T>, comparator: (T, T) -> Boolean) {
addAll(elements.filterNot { new -> this.any { existing -> comparator(existing, new) } }) addAll(elements.filterNot { new -> this.any { existing -> comparator(existing, new) } })
} }
fun <T, R : Comparable<R>> List<T>.isSortedBy(selector: (T) -> R): Boolean {
return zipWithNext().all { (a, b) -> selector(a) <= selector(b) }
}
@@ -10,8 +10,8 @@ import com.wireguard.android.util.RootShell
import com.zaneschepke.logcatter.LogReader import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.logcatter.model.LogMessage import com.zaneschepke.logcatter.model.LogMessage
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
import com.zaneschepke.networkmonitor.ConnectivityState
import com.zaneschepke.networkmonitor.NetworkMonitor import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.networkmonitor.NetworkStatus
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
@@ -34,7 +34,6 @@ import com.zaneschepke.wireguardautotunnel.util.*
import com.zaneschepke.wireguardautotunnel.util.extensions.addAllUnique import com.zaneschepke.wireguardautotunnel.util.extensions.addAllUnique
import com.zaneschepke.wireguardautotunnel.util.extensions.withFirstState import com.zaneschepke.wireguardautotunnel.util.extensions.withFirstState
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
import com.zaneschepke.wireguardautotunnel.viewmodel.event.UiEvent
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import java.io.IOException import java.io.IOException
import java.net.URL import java.net.URL
@@ -80,9 +79,6 @@ constructor(
private val _appViewState = MutableStateFlow(AppViewState()) private val _appViewState = MutableStateFlow(AppViewState())
val appViewState = _appViewState.asStateFlow() val appViewState = _appViewState.asStateFlow()
private val _uiEvent = MutableSharedFlow<UiEvent>()
val uiEvent: SharedFlow<UiEvent> = _uiEvent.asSharedFlow()
private val _logs = MutableStateFlow<List<LogMessage>>(emptyList()) private val _logs = MutableStateFlow<List<LogMessage>>(emptyList())
val logs: StateFlow<List<LogMessage>> = _logs.asStateFlow() val logs: StateFlow<List<LogMessage>> = _logs.asStateFlow()
private val maxLogSize = Constants.MAX_LOG_SIZE private val maxLogSize = Constants.MAX_LOG_SIZE
@@ -94,14 +90,14 @@ constructor(
appDataRepository.appState.flow, appDataRepository.appState.flow,
tunnelManager.activeTunnels, tunnelManager.activeTunnels,
serviceManager.autoTunnelService.map { it != null }, serviceManager.autoTunnelService.map { it != null },
networkMonitor.connectivityStateFlow, networkMonitor.networkStatusFlow,
) { array -> ) { array ->
val settings = array[0] as AppSettings val settings = array[0] as AppSettings
val tunnels = array[1] as List<TunnelConf> val tunnels = array[1] as List<TunnelConf>
val appState = array[2] as AppState val appState = array[2] as AppState
val activeTunnels = array[3] as Map<TunnelConf, TunnelState> val activeTunnels = array[3] as Map<TunnelConf, TunnelState>
val autoTunnel = array[4] as Boolean val autoTunnel = array[4] as Boolean
val network = array[5] as ConnectivityState val network = array[5] as NetworkStatus
AppUiState( AppUiState(
appSettings = settings, appSettings = settings,
@@ -110,7 +106,7 @@ constructor(
appState = appState, appState = appState,
isAutoTunnelActive = autoTunnel, isAutoTunnelActive = autoTunnel,
isAppLoaded = true, isAppLoaded = true,
connectivityState = network, networkStatus = network,
) )
} }
.stateIn( .stateIn(
@@ -130,9 +126,6 @@ constructor(
} }
} }
fun handleUiEvent(event: UiEvent) =
viewModelScope.launch(mainDispatcher) { _uiEvent.emit(event) }
fun handleEvent(event: AppEvent) = fun handleEvent(event: AppEvent) =
viewModelScope.launch(ioDispatcher) { viewModelScope.launch(ioDispatcher) {
uiState.withFirstState { state -> uiState.withFirstState { state ->
@@ -222,15 +215,10 @@ constructor(
is AppEvent.SetDetectionMethod -> is AppEvent.SetDetectionMethod ->
handleSetDetectionMethod(event.detectionMethod, state.appSettings) handleSetDetectionMethod(event.detectionMethod, state.appSettings)
is AppEvent.SaveAllConfigs -> saveAllTunnels(event.tunnels)
} }
} }
} }
private suspend fun saveAllTunnels(tunnels: List<TunnelConf>) {
appDataRepository.tunnels.saveAll(tunnels)
}
private suspend fun handleSetDetectionMethod( private suspend fun handleSetDetectionMethod(
detectionMethod: AndroidNetworkMonitor.WifiDetectionMethod, detectionMethod: AndroidNetworkMonitor.WifiDetectionMethod,
appSettings: AppSettings, appSettings: AppSettings,
@@ -125,6 +125,4 @@ sealed class AppEvent {
data class SetShowModal(val modalType: AppViewState.ModalType) : AppEvent() data class SetShowModal(val modalType: AppViewState.ModalType) : AppEvent()
data object ToggleSelectAllTunnels : AppEvent() data object ToggleSelectAllTunnels : AppEvent()
data class SaveAllConfigs(val tunnels: List<TunnelConf>) : AppEvent()
} }
@@ -1,5 +0,0 @@
package com.zaneschepke.wireguardautotunnel.viewmodel.event
sealed class UiEvent {
data object SortTunnels : UiEvent()
}
+70
View File
@@ -163,6 +163,76 @@
<string name="learn_more">Zjistit více</string> <string name="learn_more">Zjistit více</string>
<string name="stop">zastavit</string> <string name="stop">zastavit</string>
<string name="server_ipv4">Překlad názvu hostitele IPv4</string> <string name="server_ipv4">Překlad názvu hostitele IPv4</string>
<string name="select_all">Vybrat vše</string>
<string name="share">Sdílet</string>
<string name="trusted_ssid_value_description">Odeslat SSID</string>
<string name="app_settings">nastavení aplikace</string>
<string name="debounce_delay">Zpoždění odezvy</string>
<string name="always_on_message">Autorizace připojení VPN byla zamítnuta. Zkontrolujte prosím</string>
<string name="bio_not_created">Biometrické údaje nebyly vytvořeny</string>
<string name="bio_not_supported">Biometrie není podporována</string>
<string name="bio_subtitle">Přihlášení pomocí biometrických údajů</string>
<string name="config_error">Chybná konfigurace</string>
<string name="prominent_background_location_title">Zpřístupnění stávající polohy na pozadí</string>
<string name="vpn_denied_dialog_title">Povolení zamítnuto</string>
<string name="app_permission_title">Řídicí most pro WG tunely</string>
<string name="app_permission_description">Ovládání funkcí tunelu a automatického tunelu.</string>
<string name="enable_remote_app_control">Povolit vzdálené ovládání aplikace</string>
<string name="tunnel_starting">Spuštění tunelu</string>
<string name="bio_auth_title">Biometrické ověření</string>
<string name="nothing_here_yet">Zatím zde nic není!</string>
<string name="export_success">Export byl úspěšně dokončen</string>
<string name="download">Stáhnout</string>
<string name="check_for_update">Zkontrolovat aktualizaci</string>
<string name="update_check_failed">Kontrola aktualizace se nezdařila.</string>
<string name="version_template">Verze: %1$s</string>
<string name="update_download_failed">Stažení aktualizace se nezdařilo.</string>
<string name="update_available">Dostupná aktualizace!</string>
<string name="download_and_install">Stáhnout a nainstalovat</string>
<string name="allow">Povolit</string>
<string name="permission_required">Je vyžadováno oprávnění</string>
<string name="licenses">Licence</string>
<string name="latest_installed">Již používáte nejnovější verzi.</string>
<string name="install_updated_permission">Tato aplikace potřebuje oprávnění k instalaci aktualizací.</string>
<string name="checking_for_update">Kontrola aktualizací</string>
<string name="add_from_url">Přidat z adresy URL</string>
<string name="inactive">Neaktivní</string>
<string name="auth_error">Chyba: neautorizováno</string>
<string name="kernel_name_error">Chyba názvu modulu jádra</string>
<string name="export_failed">Export se nezdařil</string>
<string name="delete">Smazat</string>
<string name="export_tunnels_wireguard">Exportovat tunely jako WireGuard</string>
<string name="export_tunnels_amnezia">Exportovat tunely jako Amnezia</string>
<string name="remote_key_template">Klíč: %1$s</string>
<string name="active">Aktivní</string>
<string name="service_running_error">Chyba: Služba není spuštěna</string>
<string name="wifi_name_template">Aktivní: %1$s</string>
<string name="tunnel_error_template">Tunel selhal s: %1$s</string>
<string name="camera_permission_required">Vyžadováno oprávnění k použití fotoaparátu</string>
<string name="info">Informace</string>
<string name="copy">Kopírovat</string>
<string name="status">Stav</string>
<string name="launch_app_settings">Spustit nastavení aplikace</string>
<string name="tunnel_running">Tunel je v provozu</string>
<string name="wildcards_active">Zástupné znaky(wildcards) aktivní</string>
<string name="root_accepted">Root shell přijata</string>
<string name="background_location_message">Autorizace povolit vždy polohu a/nebo přesná poloha je vyžadováno pro tuto funkci. Viz</string>
<string name="update_check_unsupported">Kontrola aktualizací není u tohoto typu sestavení podporována.</string>
<string name="background_location_message2">abyste se ujistili, že jsou tato oprávnění povolena</string>
<string name="darker">Tmavší</string>
<string name="amoled">AMOLED</string>
<string name="default_ping_ip">(nepovinné, výchozí hodnota je peers)</string>
<string name="monitoring_state_changes">Monitorování změn stavu</string>
<string name="pre_up">Před aktivací</string>
<string name="pre_down">Před deaktivací</string>
<string name="post_up">Po aktivaci</string>
<string name="optional_default">"nepovinné, výchozí: "</string>
<string name="flavor_template">Varianta: %1$s</string>
<string name="security_template">Zabezpečení: %1$s</string>
<string name="done">Hotovo</string>
<string name="wireguard">WireGuard</string>
<string name="amnezia">Amnezia</string>
<string name="show_qr">Zobrazit QR kód</string>
<string name="always_on_message2">ujistěte se, že je pro všechny ostatní aplikace vypnutá funkce trvalé připojení VPN, a zkuste to znovu</string> <string name="always_on_message2">ujistěte se, že je pro všechny ostatní aplikace vypnutá funkce trvalé připojení VPN, a zkuste to znovu</string>
<string name="use_wildcards">Použít zástupné znaky(wildcards) pro názvy</string> <string name="use_wildcards">Použít zástupné znaky(wildcards) pro názvy</string>
<string name="multiple">Několik</string> <string name="multiple">Několik</string>
+16
View File
@@ -190,4 +190,20 @@
<string name="allow">Autoriser</string> <string name="allow">Autoriser</string>
<string name="app_permission_title">Pont de contrôle du tunnel WG</string> <string name="app_permission_title">Pont de contrôle du tunnel WG</string>
<string name="app_permission_description">Contrôler les tunnels et les fonctions automatiques des tunnels.</string> <string name="app_permission_description">Contrôler les tunnels et les fonctions automatiques des tunnels.</string>
<string name="select">Sélectionner</string>
<string name="join_telegram">Rejoindre la communauté Telegram</string>
<string name="join_matrix">Rejoindre la communauté Matrix</string>
<string name="auto_tunnel_channel_description">Un canal pour les notifications de l\'état du tunnel automatique</string>
<string name="tunnel_control">Contrôle du tunnel</string>
<string name="auto_tunnel">Tunnel automatique</string>
<string name="add_tunnel">Ajouter un tunnel</string>
<string name="error_download_failed">Le téléchargement de la configuration a échouée</string>
<string name="multiple">Multiple</string>
<string name="add_from_url">Ajouter depuis un URL</string>
<string name="enter_config_url">Saisissez l\'URL de configuration</string>
<string name="search">Rechercher</string>
<string name="save">Sauvegarder</string>
<string name="copy">Copier</string>
<string name="info">Informations</string>
<string name="prefer_ipv4">Préférer une connexion IPv4</string>
</resources> </resources>
+4
View File
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">WG Tunnel</string>
</resources>
<resources> <resources>
<string name="app_name">WG Tunnel</string> <string name="app_name">WG Tunnel</string>
<string name="app_permission_description">Alagutak és automatikus alagút funkciók vezérlése.</string> <string name="app_permission_description">Alagutak és automatikus alagút funkciók vezérlése.</string>
+163
View File
@@ -0,0 +1,163 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="tunnel_name">Nome do Túnel</string>
<string name="exclude">Excluir</string>
<string name="include">Incluir</string>
<string name="config_changes_saved">Mudanças nas configurações gravadas.</string>
<string name="public_key">Chave pública</string>
<string name="app_name">WG Tunnel</string>
<string name="vpn_channel_name">Canal de notificações VPN</string>
<string name="error_file_extension">O ficheiro não é .conf ou .zip</string>
<string name="turn_off_tunnel">Esta ação só é possível com o túnel inativo</string>
<string name="no_tunnels">Nenhum túnel foi adicionado!</string>
<string name="tunnels">Túneis</string>
<string name="tunnel_mobile_data">Túnel em dados móveis</string>
<string name="privacy_policy">Ver a Política de Privacidade</string>
<string name="okay">OK</string>
<string name="tunnel_on_ethernet">Túnel na ethernet</string>
<string name="prominent_background_location_message">Este recurso precisa de permissões de localização em segundo plano para ativar o monitoramento do SSID da rede Wi-Fi mesmo quando a aplicação está fechado. Para mais pormenores, por favor veja a Política de Privacidade no ecrã de Suporte.</string>
<string name="prominent_background_location_title">Revelar a localização em segundo plano</string>
<string name="thank_you">Obrigado por usar o WG Tunnel!</string>
<string name="trusted_ssid_value_description">Envie o SSID</string>
<string name="add_tunnels_text">Adicionar a partir de ficheiro ou zip</string>
<string name="open_file">Abrir Ficheiro</string>
<string name="add_from_qr">Adicionar a partir de código QR</string>
<string name="qr_scan">Escanear o código QR</string>
<string name="addresses">Endereços</string>
<string name="dns_servers">Servidores DNS</string>
<string name="mtu">MTU</string>
<string name="peer">Par</string>
<string name="allowed_ips">IPs Permitidos</string>
<string name="name">Nome</string>
<string name="always_on_vpn_support">Permitir VPN sempre ligada</string>
<string name="location_services_not_detected">Serviço de localização não foi detetado</string>
<string name="auto_tunneling">Auto-túnel</string>
<string name="vpn_on">VPN ligada</string>
<string name="vpn_off">VPN desligada</string>
<string name="create_import">Criar do zero</string>
<string name="turn_on_tunnel">Esta ação precisa um túnel ativo</string>
<string name="add_peer">Adicionar par</string>
<string name="interface_">Interface</string>
<string name="rotate_keys">Revezar chaves</string>
<string name="private_key">Chave privada</string>
<string name="copy_public_key">Copiar chave pública</string>
<string name="base64_key">Chave base64</string>
<string name="comma_separated_list">Lista separada por vírgulas</string>
<string name="listen_port">Porta de escuta</string>
<string name="random">(aleatório)</string>
<string name="optional">(opcional)</string>
<string name="preshared_key">Chave pré-partilhada</string>
<string name="seconds">segundos</string>
<string name="persistent_keepalive">Manter a conexão persistente (keepalive)</string>
<string name="cancel">Cancelar</string>
<string name="error_authentication_failed">Autenticação falhou</string>
<string name="error_authorization_failed">Autorização falhou</string>
<string name="enabled_app_shortcuts">Ativar atalhos de aplicações</string>
<string name="unknown_error">Ocorreu um erro desconhecido</string>
<string name="tunnel_on_wifi">Túnel em Wi-Fi não confiável</string>
<string name="email_subject">Apoio para o WG Tunnel</string>
<string name="email_chooser">Enviar um email…</string>
<string name="docs_description">Ler a documentação</string>
<string name="email_description">Me envie um email</string>
<string name="use_kernel">Usar o módulo do kernel</string>
<string name="error_ssid_exists">SSID já existe</string>
<string name="error_root_denied">Shell Root negado</string>
<string name="error_no_file_explorer">Nenhum explorador de ficheiros instalado</string>
<string name="location_services_missing_message">A aplicação não detetou o serviço de localização ativado no seu dispositivo. Dependendo do dispositivo, isto pode causar que a função de Wi-Fi não confiável falhe em ler o nome do Wi-Fi. Quer continuar mesmo assim?</string>
<string name="auto_tunnel_title">Serviço de Auto-túnel</string>
<string name="delete_tunnel">Apagar túnel</string>
<string name="delete_tunnel_message">Tem certeza que quer apagar este túnel?</string>
<string name="yes">Sim</string>
<string name="all">todos</string>
<string name="no_email_detected">Nenhuma aplicação de email detetado</string>
<string name="no_browser_detected">Nenhum navegador detetado</string>
<string name="open_issue">Abrir um problema</string>
<string name="read_logs">Ler os registos</string>
<string name="auto">(automático)</string>
<string name="incorrect_pin">O Pin está errado</string>
<string name="pin_created">Pin criado com sucesso</string>
<string name="enter_pin">Digite o seu pin</string>
<string name="create_pin">Criar um pin</string>
<string name="enable_app_lock">Ligar bloqueio de aplicação</string>
<string name="restart_on_ping">Reiniciar em falha de ping (beta)</string>
<string name="mobile_data_tunnel">Selecionar como túnel em dados móveis</string>
<string name="set_primary_tunnel">Selecionar como túnel principal</string>
<string name="use_tunnel_on_wifi_name">Usar túnel em wifi com nome</string>
<string name="edit_tunnel">Editar túnel</string>
<string name="version">Versão</string>
<string name="settings">Configurações</string>
<string name="support">Suporte</string>
<string name="kernel">Kernel</string>
<string name="junk_packet_count">Quantidade de pacotes-lixo</string>
<string name="junk_packet_minimum_size">Tamanho mínimo de pacote-lixo</string>
<string name="junk_packet_maximum_size">Tamanho máximo de pacote-lixo</string>
<string name="init_packet_junk_size">Tamanho de pacote-lixo inicial</string>
<string name="response_packet_junk_size">Tamanho de resposta de pacote-lixo</string>
<string name="unsure_how">se não tiver certeza em como continuar</string>
<string name="see_the">Veja o</string>
<string name="getting_started_guide">guia de início rápido</string>
<string name="error_file_format">Formato de configuração inválido</string>
<string name="restart_at_boot">Ativar na inicialização</string>
<string name="vpn_denied_dialog_title">Permissão negada</string>
<string name="vpn_settings">Configurações do sistema VPN</string>
<string name="always_on_message">A permissão de conexão VPN foi negada. Por favor, verifique</string>
<string name="always_on_message2">para ter certeza que VPN Sempre-ligada é desligada para todas as outras aplicações e tente novamente</string>
<string name="background_location_message">Permitir que toda a permissão de localização do tempo e/ou localização precisa é necessária para este recurso. Por favor, veja</string>
<string name="app_settings">configurações da app</string>
<string name="root_accepted">Shell root aceito</string>
<string name="set_custom_ping_ip">Definir ip ping personalizado</string>
<string name="default_ping_ip">(opcional, padrão para pares)</string>
<string name="set_custom_ping_internal">Intervalo de Ping (seg)</string>
<string name="optional_default">"opcional, padrão: "</string>
<string name="show_amnezia_properties">Mostrar propriedades de Amnezia</string>
<string name="never">nunca</string>
<string name="sec">seg</string>
<string name="handshake">handshake</string>
<string name="appearance">Aparência</string>
<string name="notifications">Notificações</string>
<string name="automatic">Automático</string>
<string name="light">Claro</string>
<string name="dark">Escuro</string>
<string name="dynamic">Dinâmico</string>
<string name="language">Idioma</string>
<string name="display_theme">Tema</string>
<string name="trusted_wifi_names">Nomes de Wi-Fi confiáveis</string>
<string name="add_wifi_name">Adicionar nome Wi-Fi</string>
<string name="mobile_tunnel">Túnel com dados móveis</string>
<string name="skip">Pular</string>
<string name="use_wildcards">Usar nomes coringas</string>
<string name="learn_more">Saber mais</string>
<string name="wildcards_active">Wildcards ativos</string>
<string name="wifi_name_via_shell">Nome do Wi-Fi por shell</string>
<string name="use_root_shell_for_wifi">Obter o nome do Wi-Fi através do shell root</string>
<string name="kernel_not_supported">Kernel não suportado</string>
<string name="start_auto">Iniciar túnel automático</string>
<string name="stop_auto">Pausar túnel automático</string>
<string name="tunnel_running">Túnel em execução</string>
<string name="monitoring_state_changes">Monitorar estado de alterações</string>
<string name="donate">Contribua com projeto</string>
<string name="local_logging">Registo local</string>
<string name="enable_local_logging">Ativar registo local</string>
<string name="add_from_clipboard">Adicionar da área de transferência</string>
<string name="stop_on_no_internet">Interromper quando não há internet</string>
<string name="stop_on_internet_loss">Interrompa o túnel quando a internet não estiver disponível</string>
<string name="ethernet_tunnel">Túnel ethernet</string>
<string name="set_ethernet_tunnel">Definir como túnel ethernet</string>
<string name="native_kill_switch">Interruptor de desligamento padrão</string>
<string name="vpn_kill_switch">Interruptor de desligamento VPN</string>
<string name="kill_switch_options">Opções do interruptor de desligamento</string>
<string name="allow_lan_traffic">Permitir tráfego LAN</string>
<string name="bypass_lan_for_kill_switch">Ignorar LAN no interruptor de desligamento</string>
<string name="stop">pausar</string>
<string name="splt_tunneling">Tunelamento dividido</string>
<string name="tunnel_specific_settings">Configurações específicas no túnel</string>
<string name="show_scripts">Mostrar scripts</string>
<string name="quick_actions">Ações rápidas</string>
<string name="advanced_settings">Configurações avançadas</string>
<string name="hide_amnezia_properties">Ocultar propriedades Amnezia</string>
<string name="hide_scripts">Ocultar scripts</string>
<string name="enable_amnezia_compatibility">Ativar compatibilidade Amnezia</string>
<string name="remove_amnezia_compatibility">Remover compatibilidade Amnezia</string>
<string name="exclude_lan">Excluir LAN</string>
<string name="include_lan">Incluir LAN</string>
</resources>
-4
View File
@@ -281,8 +281,4 @@
</string> </string>
<string name="release_notes">Release notes</string> <string name="release_notes">Release notes</string>
<string name="shizuku_not_detected">Shizuku not detected</string> <string name="shizuku_not_detected">Shizuku not detected</string>
<string name="sort">Sort</string>
<string name="drag_handle">Drag Handle</string>
<string name="move_up">Move Up</string>
<string name="move_down">Move Down</string>
</resources> </resources>
+1 -1
View File
@@ -9,5 +9,5 @@ repositories {
dependencies { dependencies {
implementation("org.semver4j:semver4j:5.7.0") implementation("org.semver4j:semver4j:5.7.0")
implementation("org.ajoberstar.grgit:grgit-core:5.3.2") implementation("org.ajoberstar.grgit:grgit-core:5.3.0")
} }
+3 -4
View File
@@ -1,7 +1,7 @@
object Constants { object Constants {
const val VERSION_NAME = "3.9.4" const val VERSION_NAME = "3.9.3"
const val JVM_TARGET = "17" const val JVM_TARGET = "17"
const val VERSION_CODE = 39400 const val VERSION_CODE = 39300
const val TARGET_SDK = 35 const val TARGET_SDK = 35
const val MIN_SDK = 26 const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel" const val APP_ID = "com.zaneschepke.wireguardautotunnel"
@@ -13,6 +13,5 @@ object Constants {
const val PRERELEASE = "prerelease" const val PRERELEASE = "prerelease"
val allowedLicenses = listOf("MIT", "Apache-2.0", "BSD-3-Clause") val allowedLicenses = listOf("MIT", "Apache-2.0", "BSD-3-Clause")
val allowedLicenseUrls = listOf("https://github.com/journeyapps/zxing-android-embedded/blob/master/COPYING", val allowedLicenseUrls = listOf("https://github.com/journeyapps/zxing-android-embedded/blob/master/COPYING", "https://github.com/RikkaApps/Shizuku-API/blob/master/LICENSE")
"https://github.com/RikkaApps/Shizuku-API/blob/master/LICENSE")
} }
-1
View File
@@ -1 +0,0 @@
WG Tunnel
@@ -1,6 +0,0 @@
What's new:
- Tunnel sorting
- Shizuku support for Wi-Fi SSIDs
- Android TV hover visibility improvements
- Auto-tunnel default detection method bug fix
- Other UI changes and improvements
@@ -0,0 +1 @@
Um cliente de VPN alternativo para WireGuard com recursos adicionais
+1
View File
@@ -0,0 +1 @@
WG Tunnel
+1 -1
View File
@@ -1 +1 @@
WG Tunnel WG Tunel
-1
View File
@@ -1 +0,0 @@
WG Tunnel
+5 -7
View File
@@ -1,7 +1,7 @@
[versions] [versions]
accompanist = "0.37.3" accompanist = "0.37.3"
activityCompose = "1.10.1" activityCompose = "1.10.1"
amneziawgAndroid = "1.5.0" amneziawgAndroid = "1.4.0"
androidx-junit = "1.2.1" androidx-junit = "1.2.1"
shizuku = "13.1.5" shizuku = "13.1.5"
appcompat = "1.7.1" appcompat = "1.7.1"
@@ -24,18 +24,17 @@ roomVersion = "2.7.1"
semver4j = "3.1.0" semver4j = "3.1.0"
slf4jAndroid = "1.7.36" slf4jAndroid = "1.7.36"
timber = "5.0.1" timber = "5.0.1"
tunnel = "1.4.0" tunnel = "1.3.0"
androidGradlePlugin = "8.10.1" androidGradlePlugin = "8.10.1"
kotlin = "2.2.0" kotlin = "2.1.21"
ksp = "2.2.0-2.0.2" ksp = "2.1.21-2.0.2"
composeBom = "2025.06.00" composeBom = "2025.06.00"
compose = "1.8.2" compose = "1.8.2"
icons = "1.7.8" icons = "1.7.8"
workRuntimeKtxVersion = "2.10.1" workRuntimeKtxVersion = "2.10.1"
zxingAndroidEmbedded = "4.3.0" zxingAndroidEmbedded = "4.3.0"
coreSplashscreen = "1.0.1" coreSplashscreen = "1.0.1"
gradlePlugins-grgit = "5.3.2" gradlePlugins-grgit = "5.3.0"
reorderable = "2.5.1"
#plugins #plugins
material = "1.12.0" material = "1.12.0"
@@ -111,7 +110,6 @@ timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxingAndroidEmbedded" } zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxingAndroidEmbedded" }
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" } desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "biometricKtx" } androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "biometricKtx" }
reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
# tunnel # tunnel
tunnel = { module = "com.zaneschepke:wireguard-android", version.ref = "tunnel" } tunnel = { module = "com.zaneschepke:wireguard-android", version.ref = "tunnel" }
@@ -1,32 +0,0 @@
package com.zaneschepke.networkmonitor
import android.net.Network
import android.net.NetworkCapabilities
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
// keep track of the currently active network(s)
class ActiveWifiStateManager {
private val _stateFlow =
MutableStateFlow(linkedMapOf<String, Pair<Network?, NetworkCapabilities?>>())
@Synchronized
fun put(key: String, value: Pair<Network?, NetworkCapabilities?>) {
_stateFlow.update { currentMap ->
linkedMapOf(*currentMap.toList().toTypedArray()).apply { put(key, value) }
}
}
@Synchronized
fun remove(key: String) {
_stateFlow.update { currentMap ->
linkedMapOf(*currentMap.toList().toTypedArray()).apply { remove(key) }
}
}
fun isEmpty(): Boolean = _stateFlow.value.isEmpty()
fun getLatestValue(): Pair<Network?, NetworkCapabilities?>? {
return _stateFlow.value.entries.lastOrNull()?.value
}
}
@@ -1,11 +1,9 @@
package com.zaneschepke.networkmonitor package com.zaneschepke.networkmonitor
import android.Manifest
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.PackageManager
import android.location.LocationManager import android.location.LocationManager
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.Network import android.net.Network
@@ -13,17 +11,13 @@ import android.net.NetworkCapabilities
import android.net.NetworkRequest import android.net.NetworkRequest
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
import android.os.Build import android.os.Build
import androidx.core.content.ContextCompat
import com.wireguard.android.util.RootShell import com.wireguard.android.util.RootShell
import com.zaneschepke.networkmonitor.shizuku.ShizukuShell import com.zaneschepke.networkmonitor.shizuku.ShizukuShell
import com.zaneschepke.networkmonitor.util.WIFI_SSID_SHELL_COMMAND
import com.zaneschepke.networkmonitor.util.getCurrentSecurityType
import com.zaneschepke.networkmonitor.util.getCurrentWifiName
import com.zaneschepke.networkmonitor.util.getWifiSsid
import com.zaneschepke.networkmonitor.util.isLocationServicesEnabled
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber import timber.log.Timber
class AndroidNetworkMonitor( class AndroidNetworkMonitor(
@@ -51,31 +45,76 @@ class AndroidNetworkMonitor(
companion object { companion object {
fun fromValue(value: Int): WifiDetectionMethod = fun fromValue(value: Int): WifiDetectionMethod =
entries.find { it.value == value } ?: DEFAULT WifiDetectionMethod.entries.find { it.value == value } ?: DEFAULT
} }
} }
private val packageName = appContext.packageName private val packageName = appContext.packageName
private val connectivityManager = private val connectivityManager =
appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private val wifiManager = appContext.getSystemService(Context.WIFI_SERVICE) as WifiManager? private val wifiManager = appContext.getSystemService(Context.WIFI_SERVICE) as WifiManager?
private val locationManager = private val locationManager =
appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager? appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
// Track active Wi-Fi networks, their capabilities, and last active network ID private val wifiMutex = Mutex()
private val activeWifiNetworks = ActiveWifiStateManager()
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
private var currentSsid: String? = null
private var securityType: WifiSecurityType? = null
private var wifiConnected = false
// Track active Wi-Fi networks and last active network ID
private val activeWifiNetworks = mutableSetOf<String>()
data class WifiState(
val connected: Boolean = false,
val ssid: String? = null,
val securityType: WifiSecurityType? = null,
)
data class TransportState(val connected: Boolean = false)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private val wifiFlow: Flow<TransportEvent> = private val wifiFlow: Flow<WifiState> =
configurationListener.detectionMethod.flatMapLatest { detectionMethod configurationListener.detectionMethod.flatMapLatest { detectionMethod
-> // cancels previous flow -> // cancels previous flow
Timber.d("Updated detectionMethod=$detectionMethod, recreating wifiFlow")
createWifiNetworkCallbackFlow(detectionMethod) // Create a new flow for each new method createWifiNetworkCallbackFlow(detectionMethod) // Create a new flow for each new method
} }
private fun createWifiNetworkCallbackFlow( private fun createWifiNetworkCallbackFlow(
detectionMethod: WifiDetectionMethod detectionMethod: WifiDetectionMethod
): Flow<TransportEvent> = callbackFlow { ): Flow<WifiState> = callbackFlow {
@Suppress("DEPRECATION")
suspend fun getWifiSsid(): String {
return withContext(ioDispatcher) {
if (wifiManager == null) return@withContext ANDROID_UNKNOWN_SSID
try {
wifiManager.connectionInfo?.ssid?.trim('"')?.takeIf { it.isNotEmpty() }
?: ANDROID_UNKNOWN_SSID
} catch (e: Exception) {
Timber.e(e)
ANDROID_UNKNOWN_SSID
}
}
}
suspend fun handleUnknownWifi() {
wifiMutex.withLock {
val newSsid = getWifiSsid()
val securityType = wifiManager?.getCurrentSecurityType()
// Only update if new SSID is valid; preserve existing valid SSID otherwise
if (newSsid != WifiManager.UNKNOWN_SSID) {
currentSsid = newSsid
trySend(WifiState(wifiConnected, currentSsid, securityType))
} else if (currentSsid == null || currentSsid == WifiManager.UNKNOWN_SSID) {
currentSsid = newSsid
trySend(WifiState(wifiConnected, currentSsid, securityType))
}
}
}
val locationPermissionReceiver = val locationPermissionReceiver =
object : BroadcastReceiver() { object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
@@ -86,15 +125,7 @@ class AndroidNetworkMonitor(
Timber.d( Timber.d(
"Received update: Precise and all-the-time location permissions are enabled" "Received update: Precise and all-the-time location permissions are enabled"
) )
activeWifiNetworks.getLatestValue()?.let { details -> applicationScope.launch { handleUnknownWifi() }
trySend(
TransportEvent.LocationPermissionGranted(
details.first,
details.second,
detectionMethod,
)
)
}
} }
} }
} }
@@ -104,39 +135,23 @@ class AndroidNetworkMonitor(
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
if (intent.action == LOCATION_SERVICES_FILTER) { if (intent.action == LOCATION_SERVICES_FILTER) {
val isGpsEnabled = val isGpsEnabled =
locationManager?.isProviderEnabled(LocationManager.GPS_PROVIDER) locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
?: false
val isNetworkEnabled = val isNetworkEnabled =
locationManager?.isProviderEnabled(LocationManager.NETWORK_PROVIDER) locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
?: false
val isLocationServicesEnabled = isGpsEnabled || isNetworkEnabled val isLocationServicesEnabled = isGpsEnabled || isNetworkEnabled
Timber.d( Timber.d(
"Location Services state changed. Enabled: $isLocationServicesEnabled, GPS: $isGpsEnabled, Network: $isNetworkEnabled" "Location Services state changed. Enabled: $isLocationServicesEnabled, GPS: $isGpsEnabled, Network: $isNetworkEnabled"
) )
activeWifiNetworks.getLatestValue()?.let { details -> if (isLocationServicesEnabled)
trySend( applicationScope.launch { handleUnknownWifi() }
TransportEvent.LocationServicesChanged(
isLocationServicesEnabled,
details.first,
details.second,
detectionMethod,
)
)
}
} }
} }
} }
val permissionReceiverFlags = // Use RECEIVER_NOT_EXPORTED for Android 14+ compatibility
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val flags =
Context.RECEIVER_NOT_EXPORTED // Internal broadcast
} else {
0
}
val servicesReceiverFlags =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
Context.RECEIVER_EXPORTED // System broadcast Context.RECEIVER_EXPORTED
} else { } else {
0 0
} }
@@ -144,42 +159,64 @@ class AndroidNetworkMonitor(
appContext.registerReceiver( appContext.registerReceiver(
locationPermissionReceiver, locationPermissionReceiver,
IntentFilter("$packageName.$LOCATION_GRANTED"), IntentFilter("$packageName.$LOCATION_GRANTED"),
permissionReceiverFlags, flags,
) )
appContext.registerReceiver( appContext.registerReceiver(
locationServicesReceiver, locationServicesReceiver,
IntentFilter(LOCATION_SERVICES_FILTER), IntentFilter(LOCATION_SERVICES_FILTER),
servicesReceiverFlags, flags,
) )
fun handleOnWifiLost(network: Network) { suspend fun handleOnWifiLost(network: Network) {
Timber.d("Wi-Fi onLost: network=$network") wifiMutex.withLock {
activeWifiNetworks.remove(network.toString()) Timber.d("Wi-Fi onLost: network=$network")
if (activeWifiNetworks.isEmpty()) { activeWifiNetworks.remove(network.toString())
Timber.d("All Wi-Fi networks disconnected, clearing currentSsid and wifiConnected") if (activeWifiNetworks.isEmpty()) {
trySend(TransportEvent.Lost(network)) Timber.d(
} else { "All Wi-Fi networks disconnected, clearing currentSsid and wifiConnected"
Timber.d("Wi-Fi onLost, but still connected to other networks, ignoring") )
// This can happen when switching between APs of the same SSID currentSsid = null
wifiConnected = false
trySend(WifiState(connected = false, ssid = null, securityType = null))
} else {
Timber.d("Wi-Fi onLost, but still connected to other networks, ignoring")
}
} }
} }
fun handleOnWifiAvailable(network: Network) { suspend fun handleOnWifiAvailable(
Timber.d("Wi-Fi onAvailable: network=$network")
activeWifiNetworks.put(network.toString(), Pair(network, null))
trySend(TransportEvent.Available(network, detectionMethod))
}
fun handleOnWifiCapabilitiesChanged(
network: Network, network: Network,
networkCapabilities: NetworkCapabilities, networkCapabilities: NetworkCapabilities?,
) { ) {
Timber.d("Wi-Fi onCapabilitiesChanged: network=$network") wifiMutex.withLock {
activeWifiNetworks.put(network.toString(), Pair(network, networkCapabilities)) Timber.d("Wi-Fi onAvailable: network=$network")
trySend( activeWifiNetworks.add(network.toString())
TransportEvent.CapabilitiesChanged(network, networkCapabilities, detectionMethod) currentSsid =
) try {
when (detectionMethod) {
WifiDetectionMethod.DEFAULT ->
networkCapabilities?.getWifiSsid() ?: getWifiSsid()
WifiDetectionMethod.LEGACY -> getWifiSsid()
WifiDetectionMethod.ROOT ->
configurationListener.rootShell.getCurrentWifiName()
WifiDetectionMethod.SHIZUKU ->
ShizukuShell(applicationScope)
.singleResponseCommand(WIFI_SSID_SHELL_COMMAND)
}
.trim()
.replace(Regex("[\n\r]"), "")
} catch (e: Exception) {
Timber.e(e)
ANDROID_UNKNOWN_SSID
}
.also { Timber.d("Current SSID via ${detectionMethod.name}: $it") }
securityType = wifiManager?.getCurrentSecurityType()
wifiConnected = true
trySend(
WifiState(connected = true, ssid = currentSsid, securityType = securityType)
)
}
} }
val callback = val callback =
@@ -188,11 +225,11 @@ class AndroidNetworkMonitor(
Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> Build.VERSION.SDK_INT < Build.VERSION_CODES.S ->
object : ConnectivityManager.NetworkCallback() { object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
handleOnWifiAvailable(network) applicationScope.launch { handleOnWifiAvailable(network, null) }
} }
override fun onLost(network: Network) { override fun onLost(network: Network) {
handleOnWifiLost(network) applicationScope.launch { handleOnWifiLost(network) }
} }
} }
else -> else ->
@@ -200,7 +237,7 @@ class AndroidNetworkMonitor(
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
if (detectionMethod != WifiDetectionMethod.DEFAULT) if (detectionMethod != WifiDetectionMethod.DEFAULT)
handleOnWifiAvailable(network) applicationScope.launch { handleOnWifiAvailable(network, null) }
} }
override fun onCapabilitiesChanged( override fun onCapabilitiesChanged(
@@ -208,11 +245,13 @@ class AndroidNetworkMonitor(
networkCapabilities: NetworkCapabilities, networkCapabilities: NetworkCapabilities,
) { ) {
if (detectionMethod == WifiDetectionMethod.DEFAULT) if (detectionMethod == WifiDetectionMethod.DEFAULT)
handleOnWifiCapabilitiesChanged(network, networkCapabilities) applicationScope.launch {
handleOnWifiAvailable(network, networkCapabilities)
}
} }
override fun onLost(network: Network) { override fun onLost(network: Network) {
handleOnWifiLost(network) applicationScope.launch { handleOnWifiLost(network) }
} }
} }
} }
@@ -223,31 +262,34 @@ class AndroidNetworkMonitor(
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI) .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.build() .build()
connectivityManager?.registerNetworkCallback(request, callback) connectivityManager.registerNetworkCallback(request, callback)
trySend(WifiState())
trySend(TransportEvent.Unknown)
awaitClose { awaitClose {
runCatching { try {
appContext.unregisterReceiver(locationPermissionReceiver) connectivityManager.unregisterNetworkCallback(callback)
appContext.unregisterReceiver(locationServicesReceiver) } catch (e: IllegalArgumentException) {
connectivityManager?.unregisterNetworkCallback(callback) Timber.e(
} e,
.onFailure { Timber.e(it, "Error unregistering network callback") } "Flow failed to unregister NetworkCallback, was already unregistered or not registered correctly.",
)
}
appContext.unregisterReceiver(locationPermissionReceiver)
appContext.unregisterReceiver(locationServicesReceiver)
} }
} }
private val cellularFlow: Flow<TransportEvent> = callbackFlow { private val cellularFlow: Flow<TransportState> = callbackFlow {
val callback = val callback =
object : ConnectivityManager.NetworkCallback() { object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
Timber.d("Cellular onAvailable: network=$network") Timber.d("Cellular onAvailable: network=$network")
trySend(TransportEvent.Available(network)) trySend(TransportState(connected = true))
} }
override fun onLost(network: Network) { override fun onLost(network: Network) {
Timber.d("Cellular onLost: network=$network") Timber.d("Cellular onLost: network=$network")
trySend(TransportEvent.Lost(network)) trySend(TransportState(connected = false))
} }
} }
@@ -257,26 +299,23 @@ class AndroidNetworkMonitor(
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.build() .build()
connectivityManager?.registerNetworkCallback(request, callback) connectivityManager.registerNetworkCallback(request, callback)
trySend(TransportEvent.Unknown) trySend(TransportState())
awaitClose { awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
runCatching { connectivityManager?.unregisterNetworkCallback(callback) }
.onFailure { Timber.e(it, "Error unregistering cellular network callback") }
}
} }
private val ethernetFlow: Flow<TransportEvent> = callbackFlow { private val ethernetFlow: Flow<TransportState> = callbackFlow {
val callback = val callback =
object : ConnectivityManager.NetworkCallback() { object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
Timber.d("Ethernet onAvailable: network=$network") Timber.d("Ethernet onAvailable: network=$network")
trySend(TransportEvent.Available(network)) trySend(TransportState(connected = true))
} }
override fun onLost(network: Network) { override fun onLost(network: Network) {
Timber.d("Ethernet onLost: network=$network") Timber.d("Ethernet onLost: network=$network")
trySend(TransportEvent.Lost(network)) trySend(TransportState(connected = false))
} }
} }
@@ -286,118 +325,35 @@ class AndroidNetworkMonitor(
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
.build() .build()
connectivityManager?.registerNetworkCallback(request, callback) connectivityManager.registerNetworkCallback(request, callback)
trySend(TransportEvent.Unknown) trySend(TransportState())
awaitClose { awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
runCatching { connectivityManager?.unregisterNetworkCallback(callback) }
.onFailure { Timber.e(it, "Error unregistering ethernet network callback") }
}
} }
suspend fun getSsidByDetectionMethod( override val networkStatusFlow =
detectionMethod: WifiDetectionMethod?, combine(wifiFlow, cellularFlow, ethernetFlow) { wifi, cellular, ethernet ->
networkCapabilities: NetworkCapabilities?, val hasAnyConnection = wifi.connected || cellular.connected || ethernet.connected
): String { if (hasAnyConnection) {
val method = detectionMethod ?: WifiDetectionMethod.DEFAULT NetworkStatus.Connected(
return try { wifiSsid = wifi.ssid,
when (method) { securityType = wifi.securityType,
WifiDetectionMethod.DEFAULT -> wifiConnected = wifi.connected,
networkCapabilities?.getWifiSsid() cellularConnected = cellular.connected,
?: wifiManager?.getWifiSsid() ethernetConnected = ethernet.connected,
?: ANDROID_UNKNOWN_SSID )
WifiDetectionMethod.LEGACY -> } else {
wifiManager?.getWifiSsid() ?: ANDROID_UNKNOWN_SSID NetworkStatus.Disconnected
WifiDetectionMethod.ROOT ->
withTimeoutOrNull(2000) { // 2-second timeout
configurationListener.rootShell.getCurrentWifiName()
} ?: ANDROID_UNKNOWN_SSID
WifiDetectionMethod.SHIZUKU ->
withTimeoutOrNull(2000) { // 2-second timeout
ShizukuShell(applicationScope)
.singleResponseCommand(WIFI_SSID_SHELL_COMMAND)
} ?: ANDROID_UNKNOWN_SSID
} }
.trim() .also { Timber.d("NetworkStatus: $it") }
.replace(Regex("[\n\r]"), "")
} catch (e: Exception) {
Timber.e(e, "Failed to get SSID with method: ${method.name}")
ANDROID_UNKNOWN_SSID
}
.also { Timber.d("Current SSID via ${method.name}: $it") }
}
override val connectivityStateFlow =
combine(
wifiFlow.scan(
WifiState(
locationPermissionsGranted =
ContextCompat.checkSelfPermission(
appContext,
Manifest.permission.ACCESS_FINE_LOCATION,
) == PackageManager.PERMISSION_GRANTED,
locationServicesEnabled =
locationManager?.isLocationServicesEnabled() ?: false,
)
) { previous, event ->
when (event) {
is TransportEvent.Available ->
previous.copy(
connected = true,
ssid =
getSsidByDetectionMethod(
event.wifiDetectionMethod ?: WifiDetectionMethod.DEFAULT,
null,
),
securityType = wifiManager?.getCurrentSecurityType(),
)
is TransportEvent.CapabilitiesChanged ->
previous.copy(
connected = true,
ssid =
getSsidByDetectionMethod(
event.wifiDetectionMethod ?: WifiDetectionMethod.DEFAULT,
null,
),
securityType = wifiManager?.getCurrentSecurityType(),
)
is TransportEvent.LocationPermissionGranted ->
previous.copy(
locationPermissionsGranted = true,
ssid =
getSsidByDetectionMethod(
event.wifiDetectionMethod,
event.networkCapabilities,
),
securityType = wifiManager?.getCurrentSecurityType(),
)
is TransportEvent.LocationServicesChanged ->
previous.copy(
locationServicesEnabled = event.enabled,
ssid =
getSsidByDetectionMethod(
event.wifiDetectionMethod,
event.networkCapabilities,
),
securityType = wifiManager?.getCurrentSecurityType(),
)
is TransportEvent.Lost ->
previous.copy(connected = false, securityType = null, ssid = null)
TransportEvent.Unknown -> previous
}
},
cellularFlow,
ethernetFlow,
) { wifi, cellular, ethernet ->
val cellularConnected = cellular is TransportEvent.Available
val ethernetConnected = ethernet is TransportEvent.Available
ConnectivityState(
wifi,
cellularConnected = cellularConnected,
ethernetConnected = ethernetConnected,
)
.also { Timber.d("Connectivity Status: $it") }
} }
.distinctUntilChanged() .distinctUntilChanged()
.shareIn(applicationScope, SharingStarted.WhileSubscribed(5000), replay = 1) .shareIn(applicationScope, SharingStarted.WhileSubscribed(5000), replay = 1)
override fun sendLocationPermissionsGrantedBroadcast() {
val action = "$packageName.$LOCATION_GRANTED"
val intent = Intent(action)
Timber.d("Sending broadcast: $action")
appContext.sendBroadcast(intent)
}
} }
@@ -1,19 +0,0 @@
package com.zaneschepke.networkmonitor
import com.zaneschepke.networkmonitor.util.WifiSecurityType
data class ConnectivityState(
val wifiState: WifiState,
val ethernetConnected: Boolean = false,
val cellularConnected: Boolean = false,
) {
fun hasConnectivity(): Boolean = wifiState.connected || ethernetConnected || cellularConnected
}
data class WifiState(
val connected: Boolean = false,
val ssid: String? = null,
val securityType: WifiSecurityType? = null,
val locationPermissionsGranted: Boolean,
val locationServicesEnabled: Boolean,
)
@@ -1,15 +1,11 @@
package com.zaneschepke.networkmonitor.util package com.zaneschepke.networkmonitor
import android.location.LocationManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.net.wifi.WifiInfo import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
import android.os.Build import android.os.Build
import com.wireguard.android.util.RootShell import com.wireguard.android.util.RootShell
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor.Companion.ANDROID_UNKNOWN_SSID import com.zaneschepke.networkmonitor.AndroidNetworkMonitor.Companion.ANDROID_UNKNOWN_SSID
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
const val WIFI_SSID_SHELL_COMMAND = const val WIFI_SSID_SHELL_COMMAND =
"dumpsys wifi | grep 'Supplicant state: COMPLETED' | grep -o 'SSID: \"[^\"]*\"' | cut -d '\"' -f2" "dumpsys wifi | grep 'Supplicant state: COMPLETED' | grep -o 'SSID: \"[^\"]*\"' | cut -d '\"' -f2"
@@ -23,25 +19,12 @@ fun RootShell.getCurrentWifiName(): String {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
fun WifiManager.getCurrentSecurityType(): WifiSecurityType? { fun WifiManager.getCurrentSecurityType(): WifiSecurityType? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
WifiSecurityType.Companion.from(connectionInfo.currentSecurityType) WifiSecurityType.from(connectionInfo.currentSecurityType)
} else { } else {
null null
} }
} }
@Suppress("DEPRECATION")
suspend fun WifiManager?.getWifiSsid(): String {
return withContext(Dispatchers.IO) {
try {
this@getWifiSsid?.connectionInfo?.ssid?.trim('"')?.takeIf { it.isNotEmpty() }
?: ANDROID_UNKNOWN_SSID
} catch (e: Exception) {
Timber.e(e)
ANDROID_UNKNOWN_SSID
}
}
}
fun NetworkCapabilities.getWifiSsid(): String { fun NetworkCapabilities.getWifiSsid(): String {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val info: WifiInfo val info: WifiInfo
@@ -52,14 +35,3 @@ fun NetworkCapabilities.getWifiSsid(): String {
} }
return ANDROID_UNKNOWN_SSID return ANDROID_UNKNOWN_SSID
} }
fun LocationManager.isLocationServicesEnabled(): Boolean {
return try {
val isGpsEnabled = isProviderEnabled(LocationManager.GPS_PROVIDER)
val isNetworkEnabled = isProviderEnabled(LocationManager.NETWORK_PROVIDER)
isGpsEnabled || isNetworkEnabled
} catch (e: Exception) {
Timber.e(e, "Error checking location services")
false
}
}
@@ -3,5 +3,7 @@ package com.zaneschepke.networkmonitor
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface NetworkMonitor { interface NetworkMonitor {
val connectivityStateFlow: Flow<ConnectivityState> val networkStatusFlow: Flow<NetworkStatus>
fun sendLocationPermissionsGrantedBroadcast()
} }
@@ -0,0 +1,21 @@
package com.zaneschepke.networkmonitor
sealed class NetworkStatus {
data object Disconnected : NetworkStatus() {
override val wifiConnected = false
override val ethernetConnected = false
override val cellularConnected = false
}
data class Connected(
val wifiSsid: String? = null,
val securityType: WifiSecurityType? = null,
override val wifiConnected: Boolean = false,
override val ethernetConnected: Boolean = false,
override val cellularConnected: Boolean = false,
) : NetworkStatus()
abstract val wifiConnected: Boolean
abstract val ethernetConnected: Boolean
abstract val cellularConnected: Boolean
}
@@ -1,34 +0,0 @@
package com.zaneschepke.networkmonitor
import android.net.Network
import android.net.NetworkCapabilities
sealed class TransportEvent {
data class Available(
val network: Network,
val wifiDetectionMethod: AndroidNetworkMonitor.WifiDetectionMethod? = null,
) : TransportEvent()
data class Lost(val network: Network) : TransportEvent()
data class CapabilitiesChanged(
val network: Network,
val networkCapabilities: NetworkCapabilities,
val wifiDetectionMethod: AndroidNetworkMonitor.WifiDetectionMethod? = null,
) : TransportEvent()
data class LocationPermissionGranted(
val network: Network?,
val networkCapabilities: NetworkCapabilities?,
val wifiDetectionMethod: AndroidNetworkMonitor.WifiDetectionMethod?,
) : TransportEvent()
data class LocationServicesChanged(
val enabled: Boolean,
val network: Network?,
val networkCapabilities: NetworkCapabilities?,
val wifiDetectionMethod: AndroidNetworkMonitor.WifiDetectionMethod?,
) : TransportEvent()
data object Unknown : TransportEvent()
}
@@ -1,4 +1,4 @@
package com.zaneschepke.networkmonitor.util package com.zaneschepke.networkmonitor
import android.net.wifi.WifiInfo import android.net.wifi.WifiInfo