mirror of
https://github.com/wgtunnel/android.git
synced 2026-06-02 08:33:40 +02:00
Compare commits
552 Commits
3.1.7
...
testing123
| Author | SHA1 | Date | |
|---|---|---|---|
| 85316bec3f | |||
| 1935653309 | |||
| e3e24b4a06 | |||
| 7af53dcc18 | |||
| 2eb0ab0f19 | |||
| 07857a53c2 | |||
| 25fd31e252 | |||
| 0c90b33813 | |||
| e6671fd3b4 | |||
| 735e38e989 | |||
| 90698c2b17 | |||
| 245b8ee3e7 | |||
| 343554407a | |||
| b493d83730 | |||
| 53cd717340 | |||
| 76574e3dd2 | |||
| 282a752389 | |||
| 5aa9145361 | |||
| 586726c848 | |||
| af759a3909 | |||
| b467d66554 | |||
| c833e15c8f | |||
| eec1bbd2f6 | |||
| 969e9dfe03 | |||
| aeb590db8c | |||
| 312062aa36 | |||
| 287732dfb8 | |||
| dca72a70e8 | |||
| 1c6543554f | |||
| 8c01f5bea4 | |||
| dd9f329721 | |||
| f30c48a90a | |||
| 4707d3eb95 | |||
| cedc2db326 | |||
| 256e3f7951 | |||
| 9e797b24d6 | |||
| f2b9eb526e | |||
| abb29607d3 | |||
| f6d7cbc032 | |||
| 9304d79775 | |||
| 4d18decbf7 | |||
| 76186c092f | |||
| c90a7bbaf5 | |||
| d3d70ab2e7 | |||
| 9b2d4a3fb5 | |||
| d7741c37c5 | |||
| 6046e4131f | |||
| 4b2d2d20db | |||
| a09501aaf5 | |||
| d46a0653f1 | |||
| 49ee2431c2 | |||
| dfcc022257 | |||
| bc08069a64 | |||
| fb97adca4f | |||
| 41540db9b7 | |||
| a1c663233d | |||
| c520fa5ed2 | |||
| 120bde2939 | |||
| 58fcc358ce | |||
| 72722a0be5 | |||
| 29aba65690 | |||
| 5d9a534e1c | |||
| f5dafa6bf7 | |||
| 4d64d058de | |||
| 7e9687aeb9 | |||
| a6e559ecec | |||
| c6bacf8e15 | |||
| fdfc348e76 | |||
| 77b83ea569 | |||
| 5ded556647 | |||
| b62e592ee9 | |||
| 869e1ebf0d | |||
| 352eae0b28 | |||
| 7cb91ecd94 | |||
| 3291bb0718 | |||
| ce3f0b85c1 | |||
| f9768fc9f0 | |||
| 64db37648a | |||
| cc5a2a972b | |||
| 6294c7372a | |||
| d562f36652 | |||
| e77966d70a | |||
| dcf213b63c | |||
| ca10586604 | |||
| 53480b0233 | |||
| 84de3a3991 | |||
| 820ff8a9ad | |||
| 1c0b54a8e4 | |||
| 75364f323c | |||
| b87aa75bf0 | |||
| c59e7d7637 | |||
| 28ef1a7683 | |||
| a5aadb42ed | |||
| 9e0e17787d | |||
| b4c5b51644 | |||
| 76191c46f3 | |||
| ecf5036f56 | |||
| 1c0d968cfb | |||
| bda1a2080a | |||
| 14a71e3118 | |||
| 57391290c5 | |||
| cd623c0c0c | |||
| 212c6cf088 | |||
| ca47127bff | |||
| e63733286c | |||
| 36c76565f7 | |||
| 47f8de8c57 | |||
| 5740012101 | |||
| 6f5bb24cfa | |||
| 5f791ffda1 | |||
| ec244eeda3 | |||
| ff2a2cc082 | |||
| a873546e9e | |||
| 757669ddbe | |||
| c71c4e5b29 | |||
| 7f0fea3766 | |||
| 53c19762ef | |||
| c98fa04f73 | |||
| aba0f7d4d3 | |||
| fa517b2124 | |||
| d7e2648393 | |||
| 53ff3bb1e5 | |||
| 97ede3d5b4 | |||
| dcd15f7bd8 | |||
| 6031d85edd | |||
| a71f8f86b1 | |||
| 007c9f4c5d | |||
| e32a99db77 | |||
| 6670a62e2f | |||
| 34b20bd7f7 | |||
| 01e15099ca | |||
| 8f2fd93e77 | |||
| 94197c9943 | |||
| 39fc9014d8 | |||
| 8860b45230 | |||
| e220b26d88 | |||
| 2302b473b4 | |||
| b01473073f | |||
| 3ae9a24ca4 | |||
| dc3f7fa736 | |||
| 93d6f8aa45 | |||
| 3ea4aea5cf | |||
| 68b41c8925 | |||
| 06de1f24c2 | |||
| a39feeeea6 | |||
| c1619ff012 | |||
| 2534b86005 | |||
| 15c550737c | |||
| e1e7e27bb5 | |||
| 8021c133a5 | |||
| 6009445a15 | |||
| f80af9dd5e | |||
| 3f912ed532 | |||
| 13bb300f20 | |||
| 1375a468fd | |||
| f52efd92ff | |||
| ae665d3e61 | |||
| 1857977d9b | |||
| 4784b1f65c | |||
| f3debcfe45 | |||
| a5229c62b1 | |||
| 3e6a2981cb | |||
| 00254874f0 | |||
| 1b9560b601 | |||
| ff97c65d4f | |||
| 02dea1e6b0 | |||
| 548362cdde | |||
| 932d27edd7 | |||
| d89870e6de | |||
| 0dcee673e1 | |||
| 7b7c8f6e8c | |||
| 7cbe3311c9 | |||
| a5898d4ad1 | |||
| 48c01aa0e3 | |||
| b1dc6c5d59 | |||
| 2a7397edba | |||
| 53c09267df | |||
| f772dc0f8a | |||
| ba064b267f | |||
| 2c04b9d69c | |||
| 48adaae0a0 | |||
| 8e49a4d343 | |||
| 67f53915cc | |||
| 156c5478ae | |||
| 85319ba874 | |||
| ab858ab59e | |||
| 4196a543b2 | |||
| 02a8db0f9a | |||
| eb7c6ca7ba | |||
| 8ffe145ade | |||
| 4bdc43deb3 | |||
| 83c0ff497b | |||
| c8ac40d370 | |||
| 4dc91b5fae | |||
| 7a3627bf6a | |||
| e86bba6a0e | |||
| ac846bfdc9 | |||
| 11e385d350 | |||
| b2e266fc9f | |||
| 7cbbf00e52 | |||
| 6a0b2b678f | |||
| 1bfea142ad | |||
| 6fdf80e84d | |||
| b7ec9e2696 | |||
| 6502f553ac | |||
| 946ac71fb1 | |||
| dca8b50083 | |||
| 1ab611aa7f | |||
| 40a2732ec9 | |||
| ffcb4f6ed8 | |||
| 7b6bdec133 | |||
| 6ece224592 | |||
| 66051e4a0a | |||
| c8298297e2 | |||
| 83bec24ca9 | |||
| 44353df400 | |||
| 0da6e3e82c | |||
| dbaf0312f8 | |||
| 7cf7817cea | |||
| 133bd6caf7 | |||
| 25b986ef2f | |||
| fe45a2fad9 | |||
| a3eb53f90f | |||
| 246916ab0d | |||
| 7f89dcaab0 | |||
| 1d2b305047 | |||
| 0201523262 | |||
| fcc1c1e7a1 | |||
| cd82b83789 | |||
| 9f1af0140f | |||
| 14cd49983a | |||
| 40dd14ca16 | |||
| 048bcd4ad7 | |||
| 94bd23f2a4 | |||
| 7285af5c2d | |||
| 11efad703c | |||
| b81f43c7cf | |||
| 68865843dc | |||
| 62daf138dd | |||
| 1120a8fc81 | |||
| ab10be7266 | |||
| 88f9d43e51 | |||
| 2877757396 | |||
| c657c62640 | |||
| d84d9df57d | |||
| 6762d4733e | |||
| add18d5cef | |||
| c5b42b55c3 | |||
| 670d9d680c | |||
| bbfc0e2fab | |||
| 708b4c7646 | |||
| efba604c31 | |||
| 1441488053 | |||
| bb3b05d224 | |||
| cda747deee | |||
| c3a2e05eb2 | |||
| a992009c71 | |||
| 57676bf4bb | |||
| 921e33cb70 | |||
| 70649383e0 | |||
| 64a7680b81 | |||
| a81d3a8843 | |||
| bad2f55121 | |||
| fecc55fe9c | |||
| 4d2e9629f9 | |||
| 347c79741f | |||
| 9bb30069fe | |||
| 9a2d77c8bf | |||
| f79f922838 | |||
| a9d5994070 | |||
| f967b38af1 | |||
| a060f00490 | |||
| b15cdbce7c | |||
| 63d46c1817 | |||
| f9ef308f8a | |||
| b2938c3ac8 | |||
| 17b575b7f2 | |||
| 777a948244 | |||
| 72bf0a1979 | |||
| c7a45845d6 | |||
| c2725bf066 | |||
| 2993f1db22 | |||
| e9ae48f96c | |||
| 5cc2ae0d01 | |||
| 7fdec509e9 | |||
| cab2945930 | |||
| 6a90cd02b9 | |||
| 7d810c7c3d | |||
| dad34b9e24 | |||
| fb33b8996f | |||
| ec3a5dcd65 | |||
| 7b443add3a | |||
| d70ef658e2 | |||
| c01b045022 | |||
| d3ea75869a | |||
| 0784c96011 | |||
| 553279ea76 | |||
| 89f6dec357 | |||
| ab7499a616 | |||
| 105c753c66 | |||
| d9f0de2dd4 | |||
| 82280091ad | |||
| b97b7cf989 | |||
| f83e40f6cc | |||
| 1fab9dfdf2 | |||
| a670931b06 | |||
| 2c0c88baf2 | |||
| 05e7cb7c04 | |||
| bc811f74ef | |||
| 09b669f54b | |||
| f08d73cbb7 | |||
| 7ca23d3ef5 | |||
| 7fdd95ea51 | |||
| e11f0f794a | |||
| fea31437cd | |||
| 1306fdc8b2 | |||
| d2e5d6d3bc | |||
| 960af02beb | |||
| 3f1ff22488 | |||
| d6b032845b | |||
| ffad6b331f | |||
| 1fb953e2fe | |||
| 893a03d3b1 | |||
| 30b577a03d | |||
| 47f619ff07 | |||
| 791b532d85 | |||
| 57d096ebb1 | |||
| cfb4f85ded | |||
| d330fa4c28 | |||
| a362327fa3 | |||
| 9c72390398 | |||
| d1d59aaf5c | |||
| 03fdb82c24 | |||
| 4355e2e9ce | |||
| d7b4fbecb9 | |||
| 7fee0b3768 | |||
| bd76ee5392 | |||
| 911ed140e0 | |||
| a9d7648425 | |||
| 230f505806 | |||
| e58dad00ea | |||
| 8ddf5cf537 | |||
| d86fa782cc | |||
| fe7b9072d6 | |||
| 9895ed1061 | |||
| 34066ea182 | |||
| b99b116fdb | |||
| 9d42e7299b | |||
| b782cdeb35 | |||
| e40515c138 | |||
| d62df80682 | |||
| 71284b5a9c | |||
| 0a730b7a1a | |||
| 753d7eb22a | |||
| 6e961e0994 | |||
| 6bea44269f | |||
| b2a2b9fcf4 | |||
| 543a61efe0 | |||
| 688fad770c | |||
| e87dd8d3ce | |||
| 30851a7d7b | |||
| 3f4673b2a7 | |||
| 528a1f84e4 | |||
| 1af474c449 | |||
| 7e3405f3fd | |||
| ffeb089aa7 | |||
| 3838c32ddf | |||
| 0c1cb40add | |||
| bfb8d59827 | |||
| 19961ca343 | |||
| 6c007a8ca8 | |||
| 8e6a9bb309 | |||
| 594834a908 | |||
| a5e9aa83b8 | |||
| 5a77661fb3 | |||
| ee5d3ea6a9 | |||
| f6da0fe31b | |||
| 80a02382e1 | |||
| b9a8400453 | |||
| 3a17d2855b | |||
| 086b48c79d | |||
| 1f561fbf38 | |||
| 45e63e9910 | |||
| 66e89c83e2 | |||
| 470fa0191b | |||
| 7c15943a81 | |||
| 05adf7539f | |||
| a9a49e3421 | |||
| 4dd8241fa1 | |||
| 431b2f9061 | |||
| f822292584 | |||
| 4ffc5d4069 | |||
| 680fbed28c | |||
| 737524831b | |||
| b8c36ac192 | |||
| 547686069f | |||
| ac18ac8274 | |||
| cb983da990 | |||
| cfe64dcb61 | |||
| 2db521d510 | |||
| ff6c763b7b | |||
| ebf7521fa1 | |||
| 7a2d96fcd7 | |||
| c6c8047982 | |||
| 9cfb7250de | |||
| 79b5b039b0 | |||
| 29616f8325 | |||
| 8bbe81d294 | |||
| 571fb1b12c | |||
| 02f6f97aa1 | |||
| 1d74d0984e | |||
| 6448386f76 | |||
| d09e85ba45 | |||
| a9bc1cc7f0 | |||
| 54d9653f04 | |||
| efc66821a6 | |||
| 3af7adc45b | |||
| 5754f2183c | |||
| f7f7f1bd9d | |||
| 57bb3f5e74 | |||
| 49196e7c7b | |||
| 894b63e668 | |||
| e16d44ff20 | |||
| b8b3f3001b | |||
| d142ecea6e | |||
| b793984ede | |||
| ae2532afe5 | |||
| 2720a3b35e | |||
| 2350364543 | |||
| f4172cb1fc | |||
| 90c482ae4f | |||
| 1eb8ad62e0 | |||
| d44baa84a8 | |||
| cb1b8ee7d6 | |||
| 4153351fc4 | |||
| 48e6f341cb | |||
| d531adede5 | |||
| 2df1bb07ab | |||
| a5e60c3fbe | |||
| e4af481402 | |||
| 77b3fc8360 | |||
| 4fd908f271 | |||
| 632da245ab | |||
| 04f22cb92d | |||
| 31194d8b88 | |||
| 421bf418d1 | |||
| e84d7e354c | |||
| 681b066d99 | |||
| ad53fca928 | |||
| f7e4b7e8ef | |||
| b04e8e7f60 | |||
| cbee5cfd1b | |||
| 440fe6ceda | |||
| 27def018bd | |||
| 16979dbb2b | |||
| 5d8190628d | |||
| 6d597a235d | |||
| 61fd2f01b9 | |||
| 914a641977 | |||
| c97a3afbc4 | |||
| fadc2d1562 | |||
| 106ab76b82 | |||
| 48b3f60b37 | |||
| e7f5cee6dd | |||
| 5e01fd6c85 | |||
| dcbd72c6f6 | |||
| f27aa1452a | |||
| 82c3521c58 | |||
| c6535ffe7e | |||
| be5fe94ce6 | |||
| 1e905c1cfb | |||
| a549c111aa | |||
| 5ab344044e | |||
| 7030f68548 | |||
| ecd51b6fe5 | |||
| 3e4f8e0791 | |||
| e940a0dbc5 | |||
| 8e233475df | |||
| 24789826ad | |||
| 787e0d1a71 | |||
| 914ef53ef0 | |||
| a569974beb | |||
| 87bc89b6f1 | |||
| c343220e96 | |||
| 5447ec73f7 | |||
| a2b8eb5b0b | |||
| e37777e662 | |||
| ee3fcabcf1 | |||
| 2a8895ffbc | |||
| b1fdb5b9b2 | |||
| 1d644748e5 | |||
| 61989b596f | |||
| 5946d7c10d | |||
| 4fc8ffbcbb | |||
| c0cff297b2 | |||
| 7ca0db3a40 | |||
| ee8db0a859 | |||
| c8205c4c59 | |||
| 3247e94358 | |||
| 2690ce29e1 | |||
| 500b85f687 | |||
| 84b2b75271 | |||
| 0197198f7b | |||
| 0b271778c9 | |||
| 6427b2f832 | |||
| 097097f620 | |||
| 20dfaed8de | |||
| 07aa37fc2a | |||
| 091cd2e028 | |||
| 7efa6d0bf4 | |||
| e31fb01410 | |||
| 76674323e7 | |||
| f1fc9ca6f7 | |||
| cb301e74eb | |||
| 8141fe19be | |||
| 0fdb3d0b31 | |||
| d9f3a21cc3 | |||
| fec84bc6ac | |||
| d6ee36edc0 | |||
| e3fcf712d5 | |||
| 5a15776bb3 | |||
| 3339448424 | |||
| 7ec294b789 | |||
| e59c72788d | |||
| 62435d549c | |||
| 96800037d1 | |||
| f5b3bb1cb7 | |||
| 654b4a4719 | |||
| a19b5ce22a | |||
| 86f592255c | |||
| 4a5dd76b5b | |||
| 1139d17f13 | |||
| 36855319a2 | |||
| 61e3751321 | |||
| dd16bd977f | |||
| aeb4a13389 | |||
| f0ec661223 | |||
| ffa7a207fb | |||
| 515e91d191 | |||
| 16e65aec9f | |||
| ff87bee3b4 | |||
| 9739d35eda | |||
| 11ad494fbb | |||
| 90b006acc5 | |||
| eb7b39c379 | |||
| 0a17593310 | |||
| c0e58125dd | |||
| 3791261f91 | |||
| d1e61be3ae | |||
| afd4fb127f | |||
| e0cce8fba4 | |||
| b70ecbdfff |
@@ -0,0 +1,22 @@
|
||||
# Contributor Code of Conduct
|
||||
|
||||
## Pledge
|
||||
|
||||
We as individuals involved in this project, pledge to participate in this
|
||||
community in a respectful, constructive, and civil manner as we work towards a common goal
|
||||
of delivering free, open source, and value adding software for all.
|
||||
|
||||
## Standard
|
||||
|
||||
The standard for this community is the Golden Rule.
|
||||
|
||||
> “Do unto others as you would have them do unto you.”
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies to all spaces related to WG Tunnel.
|
||||
|
||||
## Incidents or Concerns
|
||||
|
||||
For any incidents or concerns, reach out to Zane at
|
||||
<support@zaneschepke.com>.
|
||||
@@ -0,0 +1,3 @@
|
||||
ko_fi: zaneschepke
|
||||
liberapay: zaneschepke
|
||||
github: zaneschepke
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG] - Problem with app"
|
||||
labels: bug
|
||||
assignees: zaneschepke
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
|
||||
- Device: [e.g. Pixel 4a]
|
||||
- Android Version: [e.g. Android 13]
|
||||
- App Version [e.g. 3.3.3]
|
||||
- Backend: [e.g. Kernel, Userspace]
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots (Only if necessary)**
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEATURE] - New feature request"
|
||||
labels: enhancement
|
||||
assignees: zaneschepke
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Support
|
||||
|
||||
If you are experiencing issues with the app, the following resources are available to help you.
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
See the app docs site <a href="https://zaneschepke.com/wgtunnel-docs/overview.html">here</a> (work in progress).
|
||||
</li>
|
||||
<li>
|
||||
Chat with me and our community on Discord <a href="https://discord.gg/rbRRNh6H7V">here</a>, or open an issue on GitHub <a href="https://github.com/zaneschepke/wgtunnel/issues/new/choose">here</a>.
|
||||
</li>
|
||||
<li>
|
||||
Send me an email <a href="mailto:zanecschepke@gmail.com">here</a>.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
Thank you for using WG Tunnel.
|
||||
@@ -0,0 +1,10 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
- package-ecosystem: gradle
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
@@ -0,0 +1,128 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_type:
|
||||
type: choice
|
||||
description: "Build type"
|
||||
required: true
|
||||
default: debug
|
||||
options:
|
||||
- debug
|
||||
- prerelease
|
||||
- nightly
|
||||
- release
|
||||
flavor:
|
||||
type: choice
|
||||
description: "Product flavor"
|
||||
required: true
|
||||
default: fdroid
|
||||
options:
|
||||
- fdroid
|
||||
- standalone
|
||||
secrets:
|
||||
SIGNING_KEY_ALIAS:
|
||||
required: false
|
||||
SIGNING_KEY_PASSWORD:
|
||||
required: false
|
||||
SIGNING_STORE_PASSWORD:
|
||||
required: false
|
||||
SERVICE_ACCOUNT_JSON:
|
||||
required: false
|
||||
KEYSTORE:
|
||||
required: false
|
||||
workflow_call:
|
||||
inputs:
|
||||
build_type:
|
||||
type: string
|
||||
description: "Build type"
|
||||
required: true
|
||||
default: debug
|
||||
flavor:
|
||||
type: string
|
||||
description: "Product flavor"
|
||||
required: false
|
||||
default: fdroid
|
||||
secrets:
|
||||
SIGNING_KEY_ALIAS:
|
||||
required: false
|
||||
SIGNING_KEY_PASSWORD:
|
||||
required: false
|
||||
SIGNING_STORE_PASSWORD:
|
||||
required: false
|
||||
SERVICE_ACCOUNT_JSON:
|
||||
required: false
|
||||
KEYSTORE:
|
||||
required: false
|
||||
|
||||
env:
|
||||
UPLOAD_DIR_ANDROID: android_artifacts
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
KEY_STORE_FILE: 'android_keystore.jks'
|
||||
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
|
||||
outputs:
|
||||
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
cache: gradle
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
- name: Decode Keystore
|
||||
id: decode_keystore
|
||||
uses: timheuer/base64-to-file@v1.2
|
||||
with:
|
||||
fileName: ${{ env.KEY_STORE_FILE }}
|
||||
fileDir: ${{ env.KEY_STORE_LOCATION }}
|
||||
encodedString: ${{ secrets.KEYSTORE }}
|
||||
- name: Create keystore path env var
|
||||
if: ${{ inputs.build_type != 'debug' }}
|
||||
run: |
|
||||
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
|
||||
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
|
||||
- name: Create service_account.json
|
||||
if: ${{ inputs.build_type != 'debug' }}
|
||||
id: createServiceAccount
|
||||
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
||||
- name: Build APK
|
||||
run: |
|
||||
flavor=${{ inputs.flavor }}
|
||||
build_type=${{ inputs.build_type }}
|
||||
case $build_type in
|
||||
"release")
|
||||
./gradlew :app:assemble${flavor^}Release --info
|
||||
;;
|
||||
"prerelease")
|
||||
./gradlew :app:assemble${flavor^}Prerelease --info
|
||||
;;
|
||||
"nightly")
|
||||
./gradlew :app:assemble${flavor^}Nightly --info
|
||||
;;
|
||||
"debug")
|
||||
./gradlew :app:assemble${flavor^}Debug --stacktrace
|
||||
;;
|
||||
esac
|
||||
- name: Get release apk path
|
||||
id: apk-path
|
||||
run: echo "path=$(find . -regex '^.*/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: android_artifacts_${{ inputs.flavor }}
|
||||
path: app/build/outputs/apk/${{ inputs.flavor }}/release/wgtunnel-${{ inputs.flavor }}-release-*.apk
|
||||
retention-days: 1
|
||||
if-no-files-found: warn
|
||||
@@ -0,0 +1,23 @@
|
||||
name: on-pr
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
format_check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
cache: gradle
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Run ktfmt
|
||||
run: ./gradlew ktfmtCheck
|
||||
@@ -0,0 +1,277 @@
|
||||
name: publish
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "4 3 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
track:
|
||||
type: choice
|
||||
description: "Google Play release track"
|
||||
options:
|
||||
- none
|
||||
- internal
|
||||
- alpha
|
||||
- beta
|
||||
- production
|
||||
default: none
|
||||
required: true
|
||||
release_type:
|
||||
type: choice
|
||||
description: "GitHub release type"
|
||||
options:
|
||||
- none
|
||||
- prerelease
|
||||
- nightly
|
||||
- release
|
||||
default: release
|
||||
required: true
|
||||
tag_name:
|
||||
description: "Tag name for release"
|
||||
required: false
|
||||
default: nightly
|
||||
flavor:
|
||||
type: choice
|
||||
description: "Product flavor"
|
||||
required: true
|
||||
default: standalone
|
||||
options:
|
||||
- fdroid
|
||||
- standalone
|
||||
workflow_call:
|
||||
inputs:
|
||||
flavor:
|
||||
type: string
|
||||
description: "Product flavor"
|
||||
required: false
|
||||
default: standalone
|
||||
|
||||
env:
|
||||
UPLOAD_DIR_ANDROID: android_artifacts
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
check_commits:
|
||||
name: Check for New Commits
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
has_new_commits: ${{ steps.check.outputs.new_commits }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Check for new commits
|
||||
id: check
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
NEW_COMMITS=$(git rev-list --count --after="$(date -Iseconds -d '23 hours ago')" ${{ github.sha }})
|
||||
echo "new_commits=$NEW_COMMITS" >> $GITHUB_OUTPUT
|
||||
|
||||
build-fdroid:
|
||||
if: ${{ inputs.release_type == 'release' || inputs.flavor == 'fdroid' }}
|
||||
uses: ./.github/workflows/build.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
build_type: ${{ inputs.release_type == '' && 'nightly' || inputs.release_type }}
|
||||
flavor: fdroid
|
||||
|
||||
build-standalone:
|
||||
if: ${{ inputs.release_type == 'release' || inputs.release_type == 'nightly' || inputs.release_type == 'prerelease' || inputs.flavor == 'standalone' }}
|
||||
uses: ./.github/workflows/build.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
build_type: ${{ inputs.release_type == '' && 'nightly' || inputs.release_type }}
|
||||
flavor: standalone
|
||||
|
||||
publish:
|
||||
needs:
|
||||
- check_commits
|
||||
- build-standalone
|
||||
if: ${{ needs.check_commits.outputs.has_new_commits > 0 && inputs.release_type != 'none' }}
|
||||
name: publish-github
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: main
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt update && sudo apt install -y gh apksigner
|
||||
- name: Set TAG_NAME
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "TAG_NAME=${{ github.event.inputs.tag_name }}" >> $GITHUB_ENV
|
||||
elif [ "${{ github.event_name }}" = "schedule" ]; then
|
||||
echo "TAG_NAME=nightly" >> $GITHUB_ENV
|
||||
echo "RELEASE_TYPE=nightly" >> $GITHUB_ENV
|
||||
fi
|
||||
- name: Set latest tag
|
||||
uses: rickstaa/action-create-tag@v1
|
||||
id: tag_creation
|
||||
with:
|
||||
tag: "latest"
|
||||
message: "Automated tag for HEAD commit"
|
||||
force_push_tag: true
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag_exists_error: false
|
||||
- name: Get latest release
|
||||
id: latest_release
|
||||
uses: kaliber5/action-get-release@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
latest: true
|
||||
- name: Generate Changelog
|
||||
id: changelog
|
||||
uses: requarks/changelog-action@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
toTag: ${{ github.event_name == 'schedule' && 'nightly' || steps.latest_release.outputs.tag_name }}
|
||||
fromTag: "latest"
|
||||
writeToFile: false
|
||||
- name: Make download dir
|
||||
run: mkdir ${{ github.workspace }}/temp
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: android_artifacts_*
|
||||
path: ${{ github.workspace }}/temp
|
||||
merge-multiple: true
|
||||
- name: Set version release notes
|
||||
if: ${{ inputs.release_type == 'release' }}
|
||||
run: |
|
||||
VERSION_NAME=$(grep "const val VERSION_NAME" buildSrc/src/main/kotlin/Constants.kt | awk -F'"' '{print $2}')
|
||||
RELEASE_NOTES="$(cat ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${VERSION_NAME}.txt || echo "No changelog found for ${VERSION_NAME}")"
|
||||
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
|
||||
echo "$RELEASE_NOTES" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: On nightly release notes
|
||||
if: ${{ contains(env.TAG_NAME, 'nightly') }}
|
||||
run: |
|
||||
echo "RELEASE_NOTES=Nightly build for the latest development version of the app." >> $GITHUB_ENV
|
||||
|
||||
- name: On prerelease release notes
|
||||
if: ${{ inputs.release_type == 'prerelease' }}
|
||||
run: |
|
||||
echo "RELEASE_NOTES=Testing version of app for specific feature." >> $GITHUB_ENV
|
||||
|
||||
- name: Delete previous release
|
||||
if: ${{ contains(env.TAG_NAME, 'nightly') || inputs.release_type == 'prerelease' }}
|
||||
uses: ClementTsang/delete-tag-and-release@v0.4.0
|
||||
with:
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
delete_release: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get checksums
|
||||
id: checksum
|
||||
run: |
|
||||
checksums=""
|
||||
for file_path in $(find ${{ github.workspace }}/temp -type f -iname "*.apk"); do
|
||||
checksum=$(apksigner verify -print-certs $file_path | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")
|
||||
checksums="$checksums\n$file_path: $checksum"
|
||||
done
|
||||
echo "checksum<<EOF" >> $GITHUB_OUTPUT
|
||||
echo -e "$checksums" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
body: |
|
||||
${{ env.RELEASE_NOTES }}
|
||||
|
||||
SHA-256 fingerprints for the 4096-bit signing certificate:
|
||||
```sh
|
||||
${{ steps.checksum.outputs.checksum }}
|
||||
```
|
||||
|
||||
To verify fingerprint:
|
||||
```sh
|
||||
apksigner verify --print-certs [path to APK file] | grep SHA-256
|
||||
```
|
||||
|
||||
### Changelog
|
||||
${{ steps.changelog.outputs.changes }}
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
name: ${{ env.TAG_NAME }}
|
||||
draft: false
|
||||
prerelease: ${{ inputs.release_type == 'prerelease' || inputs.release_type == '' || inputs.release_type == 'nightly' }}
|
||||
make_latest: ${{ inputs.release_type == 'release' }}
|
||||
files: |
|
||||
${{ github.workspace }}/temp/*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
publish-fdroid-public:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-fdroid
|
||||
if: inputs.release_type == 'release'
|
||||
steps:
|
||||
- name: Dispatch update for fdroid repo
|
||||
uses: peter-evans/repository-dispatch@v3
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: wgtunnel/fdroid
|
||||
event-type: fdroid-update
|
||||
|
||||
publish-play:
|
||||
if: ${{ inputs.track != 'none' && inputs.track != '' }}
|
||||
name: Publish to Google Play
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
KEY_STORE_FILE: 'android_keystore.jks'
|
||||
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
cache: gradle
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
||||
# Here we need to decode keystore.jks from base64 string and place it
|
||||
# in the folder specified in the release signing configuration
|
||||
- name: Decode Keystore
|
||||
id: decode_keystore
|
||||
uses: timheuer/base64-to-file@v1.2
|
||||
with:
|
||||
fileName: ${{ env.KEY_STORE_FILE }}
|
||||
fileDir: ${{ env.KEY_STORE_LOCATION }}
|
||||
encodedString: ${{ secrets.KEYSTORE }}
|
||||
|
||||
# create keystore path for gradle to read
|
||||
- name: Create keystore path env var
|
||||
run: |
|
||||
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
|
||||
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
|
||||
|
||||
- name: Create service_account.json
|
||||
id: createServiceAccount
|
||||
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
||||
|
||||
- name: Deploy with fastlane
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.2' # Not needed with a .ruby-version file
|
||||
bundler-cache: true
|
||||
|
||||
- name: Distribute app to Prod track 🚀
|
||||
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane ${{ inputs.track }})
|
||||
@@ -69,3 +69,6 @@ lint/tmp/
|
||||
# App Specific cases
|
||||
app/release/output.json
|
||||
.idea/codeStyles/
|
||||
# where we keep our signing secrets locally
|
||||
/.kotlin/
|
||||
/app/keystore/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 WG Auto Tunnel
|
||||
Copyright © 2023-2025 Zane Schepke
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -4,71 +4,121 @@ WG Tunnel
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://discord.gg/rbRRNh6H7V)
|
||||
An alternative Android client app for [WireGuard](https://www.wireguard.com/)
|
||||
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://github.com/zaneschepke/wgtunnel/issues/new?assignees=zaneschepke&labels=bug&projects=&template=bug_report.md&title=%5BBUG%5D+-+Problem+with+app">Report a Bug</a>
|
||||
·
|
||||
<a href="https://github.com/zaneschepke/wgtunnel/issues/new?assignees=zaneschepke&labels=enhancement&projects=&template=feature_request.md&title=%5BFEATURE%5D+-+New+feature+request">Request a Feature</a>
|
||||
·
|
||||
<a href="https://github.com/zaneschepke/wgtunnel/discussions">Ask a Question</a>
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<br/>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
|
||||
[](https://www.amazon.com/gp/product/B0CFGGL7WK)
|
||||
[](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
|
||||
|
||||
[](https://github.com/zaneschepke/fdroid)
|
||||
[](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.zaneschepke.wireguardautotunnel%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fzaneschepke%2Fwgtunnel%22%2C%22author%22%3A%22zaneschepke%22%2C%22name%22%3A%22WG%20Tunnel%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Atrue%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22WG%20Tunnel%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22Zane%20Schepke%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22overrideSource%22%3Anull%7D)
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://ko-fi.com/N4N8NMJN2)
|
||||
[<img src="https://img.shields.io/badge/Telegram-26A5E4.svg?style=for-the-badge&logo=Telegram&logoColor=white">](https://t.me/wgtunnel)
|
||||
[<img src="https://img.shields.io/badge/Matrix-000000.svg?style=for-the-badge&logo=Matrix&logoColor=white">](https://matrix.to/#/#wg-tunnel-space:matrix.org)
|
||||
</div>
|
||||
|
||||
<details open="open">
|
||||
<summary>Table of Contents</summary>
|
||||
|
||||
- [About](#about)
|
||||
- [Acknowledgements](#acknowledgements)
|
||||
- [Screenshots](#screenshots)
|
||||
- [Features](#features)
|
||||
- [Building](#building)
|
||||
- [Translation](#translation)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
</details>
|
||||
|
||||
<div style="text-align: left;">
|
||||
|
||||
## About
|
||||
Inspired by the official [wireguard-android](https://github.com/WireGuard/wireguard-android) app, WG Tunnel was created to address features and support missing from the official app. This app combines support for both [WireGuard](https://www.wireguard.com/)
|
||||
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/), with its primary feature of auto-tunneling (on-demand tunneling).
|
||||
|
||||
</div>
|
||||
|
||||
<div style="text-align: left;">
|
||||
|
||||
<div align="left">
|
||||
## Acknowledgements
|
||||
|
||||
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) with added features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android) library and [Jetpack Compose](https://developer.android.com/jetpack/compose), this application was inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app.
|
||||
Thank you to the following:
|
||||
|
||||
</div>
|
||||
- All of the users that have helped contribute to the project with ideas, translations, feedback, bug reports, testing, and donations.
|
||||
- [WireGuard](https://www.wireguard.com/) - Jason A. Donenfeld (https://github.com/WireGuard/wireguard-android)
|
||||
|
||||
<div align="center">
|
||||
- [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) - Amnezia Team (https://github.com/amnezia-vpn/amneziawg-android)
|
||||
|
||||
## Screenshots
|
||||
|
||||
<p float="center">
|
||||
<img label="Main" style="padding-right:25px" src="asset/main_screen.png" width="200" />
|
||||
<img label="Config" style="padding-left:25px" src="asset/config_screen.png" width="200" />
|
||||
<img label="Settings" style="padding-left:25px" src="asset/settings_screen.png" width="200" />
|
||||
<img label="Support" style="padding-left:25px" src="asset/support_screen.png" width="200" />
|
||||
</p>
|
||||
</div>
|
||||
<div style="display: flex; flex-wrap: wrap; justify-content: left; gap: 10px;">
|
||||
<img label="Main" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
|
||||
<img label="Settings" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
|
||||
<img label="Auto" src="fastlane/metadata/android/en-US/images/phoneScreenshots/auto_screen.png" width="200" />
|
||||
<img label="Config" src="fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png" width="200" />
|
||||
</div>
|
||||
|
||||
<div align="left">
|
||||
|
||||
## Inspiration
|
||||
|
||||
The inspiration for this app came from the inconvenience of constantly having to turn VPN off and on while on different networks. With there being no free solution to this problem, this app was created to meet that need.
|
||||
<div style="text-align: left;">
|
||||
|
||||
## Features
|
||||
|
||||
* Add tunnels via .conf file
|
||||
* Auto connect to VPN based on Wi-Fi SSID
|
||||
* Add tunnels via .conf file, zip, manual entry, clipboard, or QR code
|
||||
* Auto-tunnel based on Wi-Fi SSID, ethernet, or mobile data
|
||||
* Split tunneling by application with search
|
||||
* Always-on VPN for Android support
|
||||
* Quick tile support for vpn toggling
|
||||
* Dynamic shortcuts support for automation integration
|
||||
* Configurable Trusted Network list
|
||||
* Optional auto connect on mobile data
|
||||
* Automatic service restart after reboot
|
||||
* Service will stay running in background after app has been closed
|
||||
|
||||
* Support for kernel and userspace modes
|
||||
* Amnezia support for userspace mode for DPI/censorship protection
|
||||
* Pre/Post Up/Down scripts support for all modes on a rooted device
|
||||
* Always-On VPN support
|
||||
* Export tunnels to zip
|
||||
* Quick tile support for tunnel toggling, auto-tunneling
|
||||
* Shortcuts support for tunnel toggling, auto-tunneling
|
||||
* Intent automation support for all tunnels
|
||||
* In app VPN kill switch with LAN bypass
|
||||
* Automatic auto-tunneling service and/or tunnel restart after reboot or app update
|
||||
* Battery preservation measures
|
||||
* Restart tunnel on ping failure
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
$ git clone https://github.com/zaneschepke/wgtunnel
|
||||
$ cd wgtunnel
|
||||
$ ./gradlew assembleRelease
|
||||
|
||||
```sh
|
||||
git clone https://github.com/zaneschepke/wgtunnel
|
||||
cd wgtunnel
|
||||
```
|
||||
|
||||
</span>
|
||||
```sh
|
||||
./gradlew assembleDebug
|
||||
```
|
||||
|
||||
## Translation
|
||||
|
||||
This app is using [Weblate](https://weblate.org) to assist with translations.
|
||||
|
||||
Help translate WG Tunnel into your language
|
||||
at [Hosted Weblate](https://hosted.weblate.org/engage/wg-tunnel/).\
|
||||
[](https://hosted.weblate.org/engage/wg-tunnel/)
|
||||
|
||||
## Contributing
|
||||
|
||||
Any contributions in the form of feedback, issues, code, or translations are welcome and much
|
||||
appreciated!
|
||||
|
||||
Please read
|
||||
the [code of conduct](https://github.com/zaneschepke/wgtunnel?tab=coc-ov-file#contributor-code-of-conduct)
|
||||
before contributing.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report security issues to `support@zaneschepke.com`
|
||||
+2
-1
@@ -1,2 +1,3 @@
|
||||
/build
|
||||
/release
|
||||
/release
|
||||
/src/main/assets/licenses.json
|
||||
|
||||
+134
-60
@@ -2,90 +2,153 @@ plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.hilt.android)
|
||||
id("org.jetbrains.kotlin.plugin.serialization")
|
||||
alias(libs.plugins.kotlinxSerialization)
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
alias(libs.plugins.grgit)
|
||||
alias(libs.plugins.licensee)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.zaneschepke.wireguardautotunnel"
|
||||
compileSdk = 34
|
||||
namespace = Constants.APP_ID
|
||||
compileSdk = Constants.TARGET_SDK
|
||||
|
||||
androidResources { generateLocaleConfig = true }
|
||||
|
||||
dependenciesInfo {
|
||||
includeInApk = false
|
||||
includeInBundle = false
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.zaneschepke.wireguardautotunnel"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 31700
|
||||
versionName = "3.1.7"
|
||||
applicationId = Constants.APP_ID
|
||||
minSdk = Constants.MIN_SDK
|
||||
targetSdk = Constants.TARGET_SDK
|
||||
versionCode = computeVersionCode()
|
||||
versionName = computeVersionName()
|
||||
|
||||
multiDexEnabled = true
|
||||
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
|
||||
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
sourceSets { getByName("debug").assets.srcDirs(files("$projectDir/schemas")) }
|
||||
|
||||
resourceConfigurations.addAll(listOf("en"))
|
||||
buildConfigField(
|
||||
"String[]",
|
||||
"LANGUAGES",
|
||||
"new String[]{ ${languageList().joinToString(separator = ", ") { "\"$it\"" }} }",
|
||||
)
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
vectorDrawables { useSupportLibrary = true }
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create(Constants.RELEASE) {
|
||||
storeFile = file(System.getenv("KEY_STORE_PATH") ?: "keystore/android_keystore.jks")
|
||||
storePassword =
|
||||
LocalProperties.get("SIGNING_STORE_PASSWORD")
|
||||
?: System.getenv("SIGNING_STORE_PASSWORD")
|
||||
keyAlias =
|
||||
LocalProperties.get("SIGNING_KEY_ALIAS") ?: System.getenv("SIGNING_KEY_ALIAS")
|
||||
keyPassword =
|
||||
LocalProperties.get("SIGNING_KEY_PASSWORD") ?: System.getenv("SIGNING_KEY_PASSWORD")
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
packaging.jniLibs.keepDebugSymbols.addAll(
|
||||
listOf("libwg-go.so", "libwg-quick.so", "libwg.so")
|
||||
)
|
||||
|
||||
release {
|
||||
isDebuggable = false
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
signingConfig = signingConfigs.getByName(Constants.RELEASE)
|
||||
resValue("string", "provider", "\"${Constants.APP_NAME}.provider\"")
|
||||
}
|
||||
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
resValue("string", "app_name", "WG Tunnel - Debug")
|
||||
isDebuggable = true
|
||||
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.debug\"")
|
||||
}
|
||||
|
||||
create(Constants.PRERELEASE) {
|
||||
initWith(buildTypes.getByName(Constants.RELEASE))
|
||||
applicationIdSuffix = ".prerelease"
|
||||
resValue("string", "app_name", "WG Tunnel - Pre")
|
||||
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.pre\"")
|
||||
}
|
||||
|
||||
create(Constants.NIGHTLY) {
|
||||
initWith(buildTypes.getByName(Constants.RELEASE))
|
||||
applicationIdSuffix = ".nightly"
|
||||
resValue("string", "app_name", "WG Tunnel - Nightly")
|
||||
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.nightly\"")
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions.add("type")
|
||||
productFlavors {
|
||||
create("fdroid") {
|
||||
dimension = "type"
|
||||
buildConfigField("String", "FLAVOR", "\"fdroid\"")
|
||||
}
|
||||
create("general") {
|
||||
create("google") {
|
||||
dimension = "type"
|
||||
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle))
|
||||
{
|
||||
apply(plugin = "com.google.gms.google-services")
|
||||
apply(plugin = "com.google.firebase.crashlytics")
|
||||
}
|
||||
buildConfigField("String", "FLAVOR", "\"google\"")
|
||||
}
|
||||
create("standalone") {
|
||||
dimension = "type"
|
||||
buildConfigField("String", "FLAVOR", "\"standalone\"")
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
kotlinOptions { jvmTarget = Constants.JVM_TARGET }
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
|
||||
|
||||
licensee {
|
||||
Constants.allowedLicenses.forEach { allow(it) }
|
||||
allowUrl(Constants.XZING_LICENSE_URL)
|
||||
allowUrl("https://rafaellins.mit-license.org/2021/")
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
|
||||
}
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
|
||||
applicationVariants.all {
|
||||
val variant = this
|
||||
variant.outputs
|
||||
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
|
||||
.forEach { output ->
|
||||
val outputFileName =
|
||||
if (variant.flavorName == "fdroid" && variant.buildType.name == "release") {
|
||||
"${Constants.APP_NAME}-fdroid-release-${variant.versionName}.apk"
|
||||
} else {
|
||||
"${Constants.APP_NAME}-${variant.flavorName}-v${variant.versionName}.apk"
|
||||
}
|
||||
output.outputFileName = outputFileName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val generalImplementation by configurations
|
||||
dependencies {
|
||||
implementation(project(":logcatter"))
|
||||
implementation(project(":networkmonitor"))
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
// optional - helpers for implementing LifecycleOwner in a Service
|
||||
implementation(libs.androidx.lifecycle.service)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
@@ -94,66 +157,77 @@ dependencies {
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.material)
|
||||
implementation(libs.androidx.storage)
|
||||
|
||||
//test
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.compose.ui.test)
|
||||
androidTestImplementation(libs.androidx.room.testing)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
debugImplementation(libs.androidx.compose.manifest)
|
||||
|
||||
//wg
|
||||
implementation(libs.tunnel)
|
||||
implementation(libs.amneziawg.android)
|
||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||
|
||||
//logging
|
||||
implementation(libs.timber)
|
||||
|
||||
// compose navigation
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
|
||||
// hilt
|
||||
implementation(libs.hilt.android)
|
||||
ksp(libs.hilt.android.compiler)
|
||||
ksp(libs.androidx.hilt.compiler)
|
||||
|
||||
//accompanist
|
||||
implementation(libs.accompanist.systemuicontroller)
|
||||
implementation(libs.accompanist.permissions)
|
||||
implementation(libs.accompanist.flowlayout)
|
||||
implementation(libs.accompanist.navigation.animation)
|
||||
implementation(libs.accompanist.drawablepainter)
|
||||
|
||||
//room
|
||||
implementation(libs.androidx.room.runtime)
|
||||
ksp(libs.androidx.room.compiler)
|
||||
implementation(libs.androidx.room.ktx)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
|
||||
//lifecycle
|
||||
implementation(libs.lifecycle.runtime.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.process)
|
||||
|
||||
|
||||
//icons
|
||||
implementation(libs.material.icons.extended)
|
||||
//serialization
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
//firebase crashlytics
|
||||
generalImplementation(platform(libs.firebase.bom))
|
||||
generalImplementation(libs.google.firebase.crashlytics.ktx)
|
||||
generalImplementation(libs.google.firebase.analytics.ktx)
|
||||
|
||||
//barcode scanning
|
||||
implementation(libs.zxing.android.embedded)
|
||||
implementation(libs.zxing.core)
|
||||
implementation(libs.material.icons.extended)
|
||||
|
||||
//bio
|
||||
implementation(libs.androidx.biometric.ktx)
|
||||
implementation(libs.pin.lock.compose)
|
||||
|
||||
//shortcuts
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.core.google.shortcuts)
|
||||
}
|
||||
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
|
||||
implementation(libs.androidx.work.runtime)
|
||||
implementation(libs.androidx.hilt.work)
|
||||
|
||||
implementation(libs.qrcode.kotlin)
|
||||
implementation(libs.semver4j)
|
||||
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.okhttp)
|
||||
implementation(libs.ktor.client.cio)
|
||||
implementation(libs.ktor.client.content.negotiation)
|
||||
implementation(libs.ktor.serialization.kotlinx.json)
|
||||
implementation(libs.slf4j.android)
|
||||
}
|
||||
|
||||
tasks.register<Copy>("copyLicenseeJsonToAssets") {
|
||||
dependsOn("licensee")
|
||||
val outputAssets = layout.projectDirectory.dir("src/main/assets")
|
||||
from(layout.buildDirectory.file("reports/licensee/androidFdroidRelease/artifacts.json")) {
|
||||
rename("artifacts.json", "licenses.json")
|
||||
}
|
||||
into(outputAssets)
|
||||
}
|
||||
|
||||
tasks.named("preBuild") { dependsOn("copyLicenseeJsonToAssets") }
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "328300975830",
|
||||
"project_id": "wireguard-auto-tunnel",
|
||||
"storage_bucket": "wireguard-auto-tunnel.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:328300975830:android:82cd774598ccb7234b1b77",
|
||||
"android_client_info": {
|
||||
"package_name": "com.zaneschepke.wireguardautotunnel"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "328300975830-m72lc3hr69ddhdqh9ngr27rvc8o0jb2d.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyBsSMY0LlckizXDnuYBy7nXWGSdl8zZedI"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "328300975830-m72lc3hr69ddhdqh9ngr27rvc8o0jb2d.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
Vendored
-21
@@ -1,21 +0,0 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,225 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 10,
|
||||
"identityHash": "c8621055524f90b4d1972f6171f59e80",
|
||||
"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_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelEnabled",
|
||||
"columnName": "is_kernel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRestoreOnBootEnabled",
|
||||
"columnName": "is_restore_on_boot_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMultiTunnelEnabled",
|
||||
"columnName": "is_multi_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelPaused",
|
||||
"columnName": "is_auto_tunnel_paused",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "wgQuick",
|
||||
"columnName": "wg_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelNetworks",
|
||||
"columnName": "tunnel_networks",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMobileDataTunnel",
|
||||
"columnName": "is_mobile_data_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPrimaryTunnel",
|
||||
"columnName": "is_primary_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "amQuick",
|
||||
"columnName": "am_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActive",
|
||||
"columnName": "is_Active",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingInterval",
|
||||
"columnName": "ping_interval",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false,
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingCooldown",
|
||||
"columnName": "ping_cooldown",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false,
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingIp",
|
||||
"columnName": "ping_ip",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false,
|
||||
"defaultValue": "null"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c8621055524f90b4d1972f6171f59e80')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 11,
|
||||
"identityHash": "4c9418386f72dfac5d28ab96c1e5ea0b",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelEnabled",
|
||||
"columnName": "is_kernel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRestoreOnBootEnabled",
|
||||
"columnName": "is_restore_on_boot_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMultiTunnelEnabled",
|
||||
"columnName": "is_multi_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAmneziaEnabled",
|
||||
"columnName": "is_amnezia_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isWildcardsEnabled",
|
||||
"columnName": "is_wildcards_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isWifiNameByShellEnabled",
|
||||
"columnName": "is_wifi_by_shell_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "wgQuick",
|
||||
"columnName": "wg_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelNetworks",
|
||||
"columnName": "tunnel_networks",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMobileDataTunnel",
|
||||
"columnName": "is_mobile_data_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPrimaryTunnel",
|
||||
"columnName": "is_primary_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "amQuick",
|
||||
"columnName": "am_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActive",
|
||||
"columnName": "is_Active",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingInterval",
|
||||
"columnName": "ping_interval",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false,
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingCooldown",
|
||||
"columnName": "ping_cooldown",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false,
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingIp",
|
||||
"columnName": "ping_ip",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false,
|
||||
"defaultValue": "null"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4c9418386f72dfac5d28ab96c1e5ea0b')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 12,
|
||||
"identityHash": "acf79ac5defacda5be6c3f976e777de3",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelEnabled",
|
||||
"columnName": "is_kernel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRestoreOnBootEnabled",
|
||||
"columnName": "is_restore_on_boot_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMultiTunnelEnabled",
|
||||
"columnName": "is_multi_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAmneziaEnabled",
|
||||
"columnName": "is_amnezia_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isWildcardsEnabled",
|
||||
"columnName": "is_wildcards_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isWifiNameByShellEnabled",
|
||||
"columnName": "is_wifi_by_shell_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isStopOnNoInternetEnabled",
|
||||
"columnName": "is_stop_on_no_internet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "wgQuick",
|
||||
"columnName": "wg_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelNetworks",
|
||||
"columnName": "tunnel_networks",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMobileDataTunnel",
|
||||
"columnName": "is_mobile_data_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPrimaryTunnel",
|
||||
"columnName": "is_primary_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "amQuick",
|
||||
"columnName": "am_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActive",
|
||||
"columnName": "is_Active",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingInterval",
|
||||
"columnName": "ping_interval",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false,
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingCooldown",
|
||||
"columnName": "ping_cooldown",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false,
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingIp",
|
||||
"columnName": "ping_ip",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false,
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isEthernetTunnel",
|
||||
"columnName": "is_ethernet_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'acf79ac5defacda5be6c3f976e777de3')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 13,
|
||||
"identityHash": "ff209157b98a641c424f5086818ec585",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelEnabled",
|
||||
"columnName": "is_kernel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRestoreOnBootEnabled",
|
||||
"columnName": "is_restore_on_boot_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMultiTunnelEnabled",
|
||||
"columnName": "is_multi_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAmneziaEnabled",
|
||||
"columnName": "is_amnezia_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isWildcardsEnabled",
|
||||
"columnName": "is_wildcards_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isWifiNameByShellEnabled",
|
||||
"columnName": "is_wifi_by_shell_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isStopOnNoInternetEnabled",
|
||||
"columnName": "is_stop_on_no_internet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isVpnKillSwitchEnabled",
|
||||
"columnName": "is_vpn_kill_switch_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelKillSwitchEnabled",
|
||||
"columnName": "is_kernel_kill_switch_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isLanOnKillSwitchEnabled",
|
||||
"columnName": "is_lan_on_kill_switch_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "wgQuick",
|
||||
"columnName": "wg_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelNetworks",
|
||||
"columnName": "tunnel_networks",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMobileDataTunnel",
|
||||
"columnName": "is_mobile_data_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPrimaryTunnel",
|
||||
"columnName": "is_primary_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "amQuick",
|
||||
"columnName": "am_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActive",
|
||||
"columnName": "is_Active",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingInterval",
|
||||
"columnName": "ping_interval",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false,
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingCooldown",
|
||||
"columnName": "ping_cooldown",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false,
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingIp",
|
||||
"columnName": "ping_ip",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false,
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isEthernetTunnel",
|
||||
"columnName": "is_ethernet_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ff209157b98a641c424f5086818ec585')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 14,
|
||||
"identityHash": "f2b260c389fb2e53216de40e4b1047f3",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelEnabled",
|
||||
"columnName": "is_kernel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRestoreOnBootEnabled",
|
||||
"columnName": "is_restore_on_boot_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMultiTunnelEnabled",
|
||||
"columnName": "is_multi_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAmneziaEnabled",
|
||||
"columnName": "is_amnezia_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isWildcardsEnabled",
|
||||
"columnName": "is_wildcards_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isWifiNameByShellEnabled",
|
||||
"columnName": "is_wifi_by_shell_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isStopOnNoInternetEnabled",
|
||||
"columnName": "is_stop_on_no_internet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isVpnKillSwitchEnabled",
|
||||
"columnName": "is_vpn_kill_switch_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelKillSwitchEnabled",
|
||||
"columnName": "is_kernel_kill_switch_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isLanOnKillSwitchEnabled",
|
||||
"columnName": "is_lan_on_kill_switch_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "debounceDelaySeconds",
|
||||
"columnName": "debounce_delay_seconds",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "3"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "wgQuick",
|
||||
"columnName": "wg_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelNetworks",
|
||||
"columnName": "tunnel_networks",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMobileDataTunnel",
|
||||
"columnName": "is_mobile_data_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPrimaryTunnel",
|
||||
"columnName": "is_primary_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "amQuick",
|
||||
"columnName": "am_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActive",
|
||||
"columnName": "is_Active",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingInterval",
|
||||
"columnName": "ping_interval",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false,
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingCooldown",
|
||||
"columnName": "ping_cooldown",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false,
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingIp",
|
||||
"columnName": "ping_ip",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false,
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isEthernetTunnel",
|
||||
"columnName": "is_ethernet_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f2b260c389fb2e53216de40e4b1047f3')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 15,
|
||||
"identityHash": "4827f3b1ab5a4e5aa35937a0925d50e4",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_disable_kill_switch_on_trusted_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelEnabled",
|
||||
"columnName": "is_kernel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRestoreOnBootEnabled",
|
||||
"columnName": "is_restore_on_boot_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMultiTunnelEnabled",
|
||||
"columnName": "is_multi_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAmneziaEnabled",
|
||||
"columnName": "is_amnezia_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isWildcardsEnabled",
|
||||
"columnName": "is_wildcards_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isWifiNameByShellEnabled",
|
||||
"columnName": "is_wifi_by_shell_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isStopOnNoInternetEnabled",
|
||||
"columnName": "is_stop_on_no_internet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isVpnKillSwitchEnabled",
|
||||
"columnName": "is_vpn_kill_switch_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelKillSwitchEnabled",
|
||||
"columnName": "is_kernel_kill_switch_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isLanOnKillSwitchEnabled",
|
||||
"columnName": "is_lan_on_kill_switch_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "debounceDelaySeconds",
|
||||
"columnName": "debounce_delay_seconds",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "3"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isDisableKillSwitchOnTrustedEnabled",
|
||||
"columnName": "is_disable_kill_switch_on_trusted_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "wgQuick",
|
||||
"columnName": "wg_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelNetworks",
|
||||
"columnName": "tunnel_networks",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMobileDataTunnel",
|
||||
"columnName": "is_mobile_data_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPrimaryTunnel",
|
||||
"columnName": "is_primary_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "amQuick",
|
||||
"columnName": "am_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActive",
|
||||
"columnName": "is_Active",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingInterval",
|
||||
"columnName": "ping_interval",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false,
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingCooldown",
|
||||
"columnName": "ping_cooldown",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false,
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingIp",
|
||||
"columnName": "ping_ip",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false,
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isEthernetTunnel",
|
||||
"columnName": "is_ethernet_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4827f3b1ab5a4e5aa35937a0925d50e4')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 16,
|
||||
"identityHash": "ae51793c4d09ea3194ecd26f0606f35c",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_disable_kill_switch_on_trusted_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelEnabled",
|
||||
"columnName": "is_kernel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRestoreOnBootEnabled",
|
||||
"columnName": "is_restore_on_boot_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMultiTunnelEnabled",
|
||||
"columnName": "is_multi_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAmneziaEnabled",
|
||||
"columnName": "is_amnezia_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isWildcardsEnabled",
|
||||
"columnName": "is_wildcards_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isWifiNameByShellEnabled",
|
||||
"columnName": "is_wifi_by_shell_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isStopOnNoInternetEnabled",
|
||||
"columnName": "is_stop_on_no_internet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isVpnKillSwitchEnabled",
|
||||
"columnName": "is_vpn_kill_switch_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelKillSwitchEnabled",
|
||||
"columnName": "is_kernel_kill_switch_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isLanOnKillSwitchEnabled",
|
||||
"columnName": "is_lan_on_kill_switch_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "debounceDelaySeconds",
|
||||
"columnName": "debounce_delay_seconds",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "3"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isDisableKillSwitchOnTrustedEnabled",
|
||||
"columnName": "is_disable_kill_switch_on_trusted_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `is_ipv4_preferred` INTEGER NOT NULL DEFAULT true)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "wgQuick",
|
||||
"columnName": "wg_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelNetworks",
|
||||
"columnName": "tunnel_networks",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMobileDataTunnel",
|
||||
"columnName": "is_mobile_data_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPrimaryTunnel",
|
||||
"columnName": "is_primary_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "amQuick",
|
||||
"columnName": "am_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActive",
|
||||
"columnName": "is_Active",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingInterval",
|
||||
"columnName": "ping_interval",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false,
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingCooldown",
|
||||
"columnName": "ping_cooldown",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false,
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingIp",
|
||||
"columnName": "ping_ip",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false,
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isEthernetTunnel",
|
||||
"columnName": "is_ethernet_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isIpv4Preferred",
|
||||
"columnName": "is_ipv4_preferred",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "true"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ae51793c4d09ea3194ecd26f0606f35c')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 3,
|
||||
"identityHash": "6b30daba29bb95f8ddc4d26206329d4f",
|
||||
"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, `default_tunnel` TEXT, `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_battery_saver_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultTunnel",
|
||||
"columnName": "default_tunnel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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": "isBatterySaverEnabled",
|
||||
"columnName": "is_battery_saver_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6b30daba29bb95f8ddc4d26206329d4f')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 4,
|
||||
"identityHash": "aee55639422df8dadfe74c3bad204477",
|
||||
"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, `default_tunnel` TEXT, `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_battery_saver_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)",
|
||||
"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": "defaultTunnel",
|
||||
"columnName": "default_tunnel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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": "isBatterySaverEnabled",
|
||||
"columnName": "is_battery_saver_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"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'aee55639422df8dadfe74c3bad204477')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 5,
|
||||
"identityHash": "bc15003a44746e18b9c260ec49737089",
|
||||
"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, `default_tunnel` TEXT, `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_battery_saver_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_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultTunnel",
|
||||
"columnName": "default_tunnel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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": "isBatterySaverEnabled",
|
||||
"columnName": "is_battery_saver_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": "isAutoTunnelPaused",
|
||||
"columnName": "is_auto_tunnel_paused",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bc15003a44746e18b9c260ec49737089')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 6,
|
||||
"identityHash": "625820076477aca948536f7bccccc7ca",
|
||||
"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, `default_tunnel` TEXT, `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_battery_saver_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_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultTunnel",
|
||||
"columnName": "default_tunnel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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": "isBatterySaverEnabled",
|
||||
"columnName": "is_battery_saver_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": "isAutoTunnelPaused",
|
||||
"columnName": "is_auto_tunnel_paused",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '625820076477aca948536f7bccccc7ca')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 7,
|
||||
"identityHash": "e65e4e7cf01f50fb03196d47b54288b1",
|
||||
"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_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelEnabled",
|
||||
"columnName": "is_kernel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRestoreOnBootEnabled",
|
||||
"columnName": "is_restore_on_boot_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMultiTunnelEnabled",
|
||||
"columnName": "is_multi_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelPaused",
|
||||
"columnName": "is_auto_tunnel_paused",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false)",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e65e4e7cf01f50fb03196d47b54288b1')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 8,
|
||||
"identityHash": "b4d4a7c489f6b2f0d3aa4fa6f37b4935",
|
||||
"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_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelEnabled",
|
||||
"columnName": "is_kernel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRestoreOnBootEnabled",
|
||||
"columnName": "is_restore_on_boot_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMultiTunnelEnabled",
|
||||
"columnName": "is_multi_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelPaused",
|
||||
"columnName": "is_auto_tunnel_paused",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '')",
|
||||
"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": "''"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b4d4a7c489f6b2f0d3aa4fa6f37b4935')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 9,
|
||||
"identityHash": "e2c91dbf1885a9da592d3f54f1e08302",
|
||||
"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_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelEnabled",
|
||||
"columnName": "is_kernel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRestoreOnBootEnabled",
|
||||
"columnName": "is_restore_on_boot_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMultiTunnelEnabled",
|
||||
"columnName": "is_multi_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelPaused",
|
||||
"columnName": "is_auto_tunnel_paused",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false)",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e2c91dbf1885a9da592d3f54f1e08302')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
SIGNING_STORE_PASSWORD=
|
||||
SIGNING_KEY_ALIAS=
|
||||
SIGNING_KEY_PASSWORD=
|
||||
KEY_STORE_PATH=/
|
||||
+1
-1
@@ -19,4 +19,4 @@ class ExampleInstrumentedTest {
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.zaneschepke.wireguardautotunnel", appContext.packageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import androidx.room.testing.MigrationTestHelper
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.data.Queries
|
||||
import java.io.IOException
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MigrationTest {
|
||||
private val dbName = "migration-test"
|
||||
|
||||
@get:Rule
|
||||
val helper: MigrationTestHelper =
|
||||
MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), AppDatabase::class.java)
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun migrate6To7() {
|
||||
helper.createDatabase(dbName, 6).apply {
|
||||
// Database has schema version 1. Insert some data using SQL queries.
|
||||
// You can't use DAO classes because they expect the latest schema.
|
||||
execSQL(Queries.createDefaultSettings())
|
||||
execSQL(Queries.createTunnelConfig())
|
||||
// Prepare for the next version.
|
||||
close()
|
||||
}
|
||||
|
||||
// Re-open the database with version 2 and provide
|
||||
// MIGRATION_1_2 as the migration process.
|
||||
helper.runMigrationsAndValidate(dbName, 7, true)
|
||||
// MigrationTestHelper automatically verifies the schema changes,
|
||||
// but you need to validate that the data was migrated properly.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Permissions specific to full -->
|
||||
<!--updater-->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
</manifest>
|
||||
@@ -1,130 +1,205 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
|
||||
android:maxSdkVersion="30"
|
||||
tools:ignore="LeanbackUsesWifi" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING"/>
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!--foreground service exempt android 14-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
|
||||
<!--foreground service permissions-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<!--start service on boot permission-->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<!--android tv support-->
|
||||
<uses-feature android:name="android.software.leanback"
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<permission
|
||||
android:name="${applicationId}.permission.CONTROL_TUNNELS"
|
||||
android:label="@string/app_permission_title"
|
||||
android:description="@string/app_permission_description"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:protectionLevel="dangerous" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
<uses-feature android:name="android.hardware.touchscreen"
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.location.gps"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.screen.portrait"
|
||||
android:required="false" />
|
||||
android:name="android.hardware.screen.portrait"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.gamepad"
|
||||
android:required="false"/>
|
||||
|
||||
<uses-feature android:name="android.hardware.wifi"
|
||||
android:required="false"/>
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent>
|
||||
</queries>
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:name=".WireGuardAutoTunnel"
|
||||
android:allowBackup="false"
|
||||
android:banner="@mipmap/ic_banner"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:banner="@mipmap/ic_banner"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.WireguardAutoTunnel"
|
||||
tools:targetApi="31">
|
||||
android:theme="@style/Theme.App.Start"
|
||||
tools:targetApi="tiramisu">
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.WireguardAutoTunnel">
|
||||
android:windowSoftInputMode="adjustNothing"
|
||||
android:theme="@style/Theme.WireguardAutoTunnel"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<action android:name="android.intent.action.SHOW_APP_INFO" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES"/>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.CaptureActivityPortrait"
|
||||
android:screenOrientation="fullSensor"
|
||||
android:stateNotNeeded="true"
|
||||
android:theme="@style/zxing_CaptureTheme"
|
||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||
android:name="com.journeyapps.barcodescanner.CaptureActivity"
|
||||
android:screenOrientation="portrait"
|
||||
tools:replace="screenOrientation" />
|
||||
|
||||
<activity
|
||||
android:finishOnTaskLaunch="true"
|
||||
android:name=".core.shortcut.ShortcutsActivity"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:name=".service.shortcut.ShortcutsActivity"/>
|
||||
<service
|
||||
android:name=".service.foreground.ForegroundService"
|
||||
android:enabled="true"
|
||||
android:foregroundServiceType="remoteMessaging"
|
||||
android:exported="false">
|
||||
</service>
|
||||
android:exported="false"
|
||||
android:noHistory="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:finishOnTaskLaunch="true"
|
||||
android:launchMode="singleInstance"
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="@string/provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:multiprocess="true"
|
||||
tools:node="remove">
|
||||
</provider>
|
||||
|
||||
<service
|
||||
android:name=".core.service.tile.TunnelControlTile"
|
||||
android:exported="true"
|
||||
android:name=".service.tile.TunnelControlTile"
|
||||
android:icon="@drawable/shield"
|
||||
android:label="WG Tunnel"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:label="@string/tunnel_control"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<meta-data android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||
android:value="true" />
|
||||
<meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
android:value="true" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".service.foreground.WireGuardTunnelService"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:enabled="true"
|
||||
android:persistent="true"
|
||||
android:foregroundServiceType="remoteMessaging"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
||||
android:name=".core.service.tile.AutoTunnelControlTile"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:label="@string/auto_tunnel"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
android:value="true" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".service.foreground.WireGuardConnectivityWatcherService"
|
||||
android:name=".core.service.autotunnel.AutoTunnelService"
|
||||
android:enabled="true"
|
||||
android:stopWithTask="false"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
android:persistent="true"
|
||||
android:foregroundServiceType="location"
|
||||
android:permission=""
|
||||
android:stopWithTask="false"
|
||||
tools:node="merge" />
|
||||
|
||||
<service
|
||||
android:name=".core.service.TunnelForegroundService"
|
||||
android:exported="false"
|
||||
android:persistent="true"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<receiver
|
||||
android:name=".core.broadcast.RestartReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
</service>
|
||||
<receiver android:enabled="true" android:name=".receiver.BootReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:exported="false" android:name=".receiver.NotificationActionReceiver"/>
|
||||
<receiver
|
||||
android:name=".core.broadcast.KernelReceiver"
|
||||
android:exported="false"
|
||||
android:permission="${applicationId}.permission.CONTROL_TUNNELS">
|
||||
<intent-filter>
|
||||
<action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<!--custom security solution for easier user integration-->
|
||||
<receiver
|
||||
android:name=".core.broadcast.RemoteControlReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true" tools:ignore="ExportedReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="com.zaneschepke.wireguardautotunnel.START_TUNNEL" />
|
||||
<action android:name="com.zaneschepke.wireguardautotunnel.STOP_TUNNEL" />
|
||||
<action android:name="com.zaneschepke.wireguardautotunnel.START_AUTO_TUNNEL" />
|
||||
<action android:name="com.zaneschepke.wireguardautotunnel.STOP_AUTO_TUNNEL" />
|
||||
<action android:name="com.zaneschepke.wireguardautotunnel.START_KILL_SWITCH" />
|
||||
<action android:name="com.zaneschepke.wireguardautotunnel.STOP_KILL_SWITCH" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".core.broadcast.NotificationActionReceiver"
|
||||
android:exported="false"
|
||||
android:permission="${applicationId}.permission.CONTROL_TUNNELS">
|
||||
</receiver>
|
||||
</application>
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 21 KiB |
@@ -1,19 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
object Constants {
|
||||
const val MANUAL_TUNNEL_CONFIG_ID = "0"
|
||||
const val WATCHER_SERVICE_WAKE_LOCK_TIMEOUT = 10*60*1000L /*10 minute*/
|
||||
const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L
|
||||
const val VPN_STATISTIC_CHECK_INTERVAL = 10000L
|
||||
const val TOGGLE_TUNNEL_DELAY = 500L
|
||||
const val FADE_IN_ANIMATION_DURATION = 1000
|
||||
const val SLIDE_IN_ANIMATION_DURATION = 500
|
||||
const val SLIDE_IN_TRANSITION_OFFSET = 1000
|
||||
const val CONF_FILE_EXTENSION = ".conf"
|
||||
const val ZIP_FILE_EXTENSION = ".zip"
|
||||
const val URI_CONTENT_SCHEME = "content"
|
||||
const val URI_PACKAGE_SCHEME = "package"
|
||||
const val ALLOWED_FILE_TYPES = "*/*"
|
||||
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
|
||||
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
fun BroadcastReceiver.goAsync(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
) {
|
||||
val pendingResult = goAsync()
|
||||
@OptIn(DelicateCoroutinesApi::class) // Must run globally; there's no teardown callback.
|
||||
GlobalScope.launch(context) {
|
||||
try {
|
||||
block()
|
||||
} finally {
|
||||
pendingResult.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.toRoute
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.BottomNavbar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.DynamicTopAppBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.components.currentNavBackStackEntryAsNavBarState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.AutoTunnelScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.AutoTunnelAdvancedScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.LocationDisclosureScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.TunnelAutoTunnelScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.ConfigScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.scanner.ScannerScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.SplitTunnelScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.TunnelOptionsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.pin.PinLockScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.advanced.SettingsAdvancedScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.AppearanceScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.KillSwitchScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.LogsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.LicenseScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlin.system.exitProcess
|
||||
import org.amnezia.awg.backend.GoBackend.VpnService
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
@Inject lateinit var appStateRepository: AppStateRepository
|
||||
|
||||
@Inject lateinit var tunnelManager: TunnelManager
|
||||
|
||||
@Inject lateinit var networkMonitor: NetworkMonitor
|
||||
|
||||
private var lastLocationPermissionState: Boolean? = null
|
||||
|
||||
@SuppressLint("BatteryLife")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.Companion.auto(Color.TRANSPARENT, Color.TRANSPARENT),
|
||||
navigationBarStyle = SystemBarStyle.Companion.auto(Color.TRANSPARENT, Color.TRANSPARENT),
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.isNavigationBarContrastEnforced = false
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val viewModel by viewModels<AppViewModel>()
|
||||
|
||||
installSplashScreen().apply {
|
||||
setKeepOnScreenCondition { !viewModel.appViewState.value.isAppReady }
|
||||
}
|
||||
|
||||
setContent {
|
||||
val isTv = isRunningOnTv()
|
||||
val appUiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val appViewState by viewModel.appViewState.collectAsStateWithLifecycle()
|
||||
val tunnelError by viewModel.tunnelManager.errorEvents.collectAsStateWithLifecycle(null)
|
||||
|
||||
val navController = rememberNavController()
|
||||
val backStackEntry by navController.currentBackStackEntryAsState()
|
||||
val navBarState by
|
||||
currentNavBackStackEntryAsNavBarState(
|
||||
navController,
|
||||
backStackEntry,
|
||||
viewModel,
|
||||
appUiState,
|
||||
appViewState,
|
||||
)
|
||||
val snackbar = remember { SnackbarHostState() }
|
||||
var showVpnPermissionDialog by remember { mutableStateOf(false) }
|
||||
var vpnPermissionDenied by remember { mutableStateOf(false) }
|
||||
|
||||
val vpnActivity =
|
||||
rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = {
|
||||
if (it.resultCode != RESULT_OK) {
|
||||
showVpnPermissionDialog = true
|
||||
vpnPermissionDenied = true
|
||||
} else {
|
||||
vpnPermissionDenied = false
|
||||
showVpnPermissionDialog = false
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
LaunchedEffect(appUiState.tunnels) {
|
||||
if (!appViewState.isAppReady) {
|
||||
viewModel.handleEvent(AppEvent.AppReadyCheck(appUiState.tunnels))
|
||||
}
|
||||
}
|
||||
|
||||
val batteryActivity =
|
||||
rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { _: ActivityResult ->
|
||||
viewModel.handleEvent(AppEvent.SetBatteryOptimizeDisableShown)
|
||||
}
|
||||
|
||||
LaunchedEffect(tunnelError) {
|
||||
if (tunnelError == null) return@LaunchedEffect
|
||||
val message = tunnelError!!.second.toStringRes()
|
||||
val context = this@MainActivity
|
||||
snackbar.showSnackbar(
|
||||
context.getString(R.string.tunnel_error_template, context.getString(message))
|
||||
)
|
||||
}
|
||||
|
||||
with(appViewState) {
|
||||
LaunchedEffect(isConfigChanged) {
|
||||
if (isConfigChanged) {
|
||||
Intent(this@MainActivity, MainActivity::class.java).also {
|
||||
startActivity(it)
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(errorMessage) {
|
||||
errorMessage?.let {
|
||||
snackbar.showSnackbar(it.asString(this@MainActivity))
|
||||
viewModel.handleEvent(AppEvent.MessageShown)
|
||||
}
|
||||
}
|
||||
LaunchedEffect(popBackStack) {
|
||||
if (popBackStack) {
|
||||
navController.popBackStack()
|
||||
viewModel.handleEvent(AppEvent.PopBackStack(false))
|
||||
}
|
||||
}
|
||||
LaunchedEffect(requestVpnPermission) {
|
||||
if (requestVpnPermission) {
|
||||
if (!vpnPermissionDenied) {
|
||||
vpnActivity.launch(VpnService.prepare(this@MainActivity))
|
||||
} else {
|
||||
showVpnPermissionDialog = true
|
||||
}
|
||||
viewModel.handleEvent(AppEvent.VpnPermissionRequested)
|
||||
}
|
||||
}
|
||||
LaunchedEffect(requestBatteryPermission) {
|
||||
if (requestBatteryPermission) {
|
||||
batteryActivity.launch(
|
||||
Intent().apply {
|
||||
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||
data = "package:${this@MainActivity.packageName}".toUri()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalIsAndroidTV provides isTv) {
|
||||
CompositionLocalProvider(LocalNavController provides navController) {
|
||||
WireguardAutoTunnelTheme(theme = appUiState.appState.theme) {
|
||||
VpnDeniedDialog(
|
||||
showVpnPermissionDialog,
|
||||
onDismiss = {
|
||||
showVpnPermissionDialog = false
|
||||
vpnPermissionDenied = false
|
||||
},
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
modifier =
|
||||
Modifier.pointerInput(Unit) {
|
||||
detectTapGestures {
|
||||
viewModel.handleEvent(AppEvent.ClearSelectedTunnels)
|
||||
}
|
||||
},
|
||||
snackbarHost = {
|
||||
SnackbarHost(snackbar) { snackbarData: SnackbarData ->
|
||||
CustomSnackBar(
|
||||
snackbarData.visuals.message,
|
||||
isRtl = false,
|
||||
containerColor =
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
topBar = { DynamicTopAppBar(navBarState) },
|
||||
bottomBar = {
|
||||
AnimatedVisibility(
|
||||
visible = navBarState.showBottom,
|
||||
enter = slideInVertically(initialOffsetY = { it }),
|
||||
exit = slideOutVertically(targetOffsetY = { it }),
|
||||
) {
|
||||
BottomNavbar(appUiState = appUiState)
|
||||
}
|
||||
},
|
||||
) { padding ->
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
.imePadding()
|
||||
) {
|
||||
NavHost(
|
||||
navController,
|
||||
startDestination =
|
||||
(if (appUiState.appState.isPinLockEnabled) Route.Lock
|
||||
else Route.Main),
|
||||
) {
|
||||
composable<Route.Main> {
|
||||
MainScreen(appUiState, appViewState, viewModel)
|
||||
}
|
||||
composable<Route.Settings> {
|
||||
SettingsScreen(appUiState, viewModel)
|
||||
}
|
||||
composable<Route.SettingsAdvanced> {
|
||||
SettingsAdvancedScreen(appUiState, viewModel)
|
||||
}
|
||||
composable<Route.LocationDisclosure> {
|
||||
LocationDisclosureScreen(appUiState, viewModel)
|
||||
}
|
||||
composable<Route.AutoTunnel> {
|
||||
AutoTunnelScreen(appUiState, viewModel)
|
||||
}
|
||||
composable<Route.Appearance> { AppearanceScreen() }
|
||||
composable<Route.Language> {
|
||||
LanguageScreen(appUiState, viewModel)
|
||||
}
|
||||
composable<Route.Display> {
|
||||
DisplayScreen(appUiState, viewModel)
|
||||
}
|
||||
composable<Route.Support> {
|
||||
SupportScreen(appViewModel = viewModel)
|
||||
}
|
||||
composable<Route.License> { LicenseScreen() }
|
||||
composable<Route.AutoTunnelAdvanced> {
|
||||
AutoTunnelAdvancedScreen(appUiState, viewModel)
|
||||
}
|
||||
composable<Route.Logs> { LogsScreen(appViewState, viewModel) }
|
||||
composable<Route.Config> { backStack ->
|
||||
val args = backStack.toRoute<Route.Config>()
|
||||
val config =
|
||||
appUiState.tunnels.firstOrNull { it.id == args.id }
|
||||
ConfigScreen(config, viewModel)
|
||||
}
|
||||
composable<Route.TunnelOptions> { backStack ->
|
||||
val args = backStack.toRoute<Route.TunnelOptions>()
|
||||
appUiState.tunnels
|
||||
.firstOrNull { it.id == args.id }
|
||||
?.let { config ->
|
||||
TunnelOptionsScreen(config, viewModel)
|
||||
}
|
||||
}
|
||||
composable<Route.Lock> { PinLockScreen(viewModel) }
|
||||
composable<Route.Scanner> { ScannerScreen(viewModel) }
|
||||
composable<Route.KillSwitch> {
|
||||
KillSwitchScreen(appUiState, viewModel)
|
||||
}
|
||||
composable<Route.SplitTunnel> { SplitTunnelScreen(viewModel) }
|
||||
composable<Route.TunnelAutoTunnel> { backStack ->
|
||||
val args = backStack.toRoute<Route.TunnelOptions>()
|
||||
appUiState.tunnels
|
||||
.firstOrNull { it.id == args.id }
|
||||
?.let {
|
||||
TunnelAutoTunnelScreen(
|
||||
it,
|
||||
appUiState.appSettings,
|
||||
viewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,134 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.StrictMode
|
||||
import android.os.StrictMode.ThreadPolicy
|
||||
import androidx.hilt.work.HiltWorkerFactory
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||
import androidx.work.Configuration
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.zaneschepke.logcatter.LogReader
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
@HiltAndroidApp
|
||||
class WireGuardAutoTunnel : Application() {
|
||||
class WireGuardAutoTunnel : Application(), Configuration.Provider {
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepo : SettingsDoa
|
||||
@Inject lateinit var workerFactory: HiltWorkerFactory
|
||||
|
||||
override val workManagerConfiguration: Configuration
|
||||
get() = Configuration.Builder().setWorkerFactory(workerFactory).build()
|
||||
|
||||
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
|
||||
|
||||
@Inject lateinit var logReader: LogReader
|
||||
|
||||
@Inject lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
|
||||
|
||||
@Inject @MainDispatcher lateinit var mainDispatcher: CoroutineDispatcher
|
||||
|
||||
@Inject lateinit var tunnelManager: TunnelManager
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
if(BuildConfig.DEBUG) {
|
||||
instance = this
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver())
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
StrictMode.setThreadPolicy(
|
||||
ThreadPolicy.Builder()
|
||||
.detectDiskReads()
|
||||
.detectDiskWrites()
|
||||
.detectNetwork()
|
||||
.penaltyLog()
|
||||
.build()
|
||||
)
|
||||
} else {
|
||||
Timber.plant(ReleaseTree())
|
||||
}
|
||||
initSettings()
|
||||
}
|
||||
|
||||
private fun initSettings() {
|
||||
with(ProcessLifecycleOwner.get()) {
|
||||
lifecycleScope.launch {
|
||||
if(settingsRepo.getAll().isEmpty()) {
|
||||
settingsRepo.save(Settings())
|
||||
GoBackend.setAlwaysOnCallback {
|
||||
applicationScope.launch {
|
||||
val settings = appDataRepository.settings.get()
|
||||
if (settings.isAlwaysOnVpnEnabled) {
|
||||
val tunnel = appDataRepository.getPrimaryOrFirstTunnel()
|
||||
tunnel?.let { tunnelManager.startTunnel(it) }
|
||||
} else {
|
||||
Timber.w("Always-on VPN is not enabled in app settings")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ServiceWorker.start(this)
|
||||
|
||||
applicationScope.launch {
|
||||
appDataRepository.appState.getLocale()?.let {
|
||||
withContext(mainDispatcher) { LocaleUtil.changeLocale(it) }
|
||||
}
|
||||
appDataRepository.appState.isLocalLogsEnabled().let { enabled ->
|
||||
if (enabled) logReader.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
applicationScope.launch {
|
||||
tunnelManager.setBackendState(BackendState.INACTIVE, emptyList())
|
||||
}
|
||||
super.onTerminate()
|
||||
}
|
||||
|
||||
class AppLifecycleObserver : DefaultLifecycleObserver {
|
||||
|
||||
companion object {
|
||||
fun isRunningOnAndroidTv(context : Context) : Boolean {
|
||||
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
Timber.d("Application entered foreground")
|
||||
foreground = true
|
||||
}
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
Timber.d("Application entered background")
|
||||
foreground = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var foreground = false
|
||||
|
||||
fun isForeground(): Boolean {
|
||||
return foreground
|
||||
}
|
||||
|
||||
@Volatile private var lastActiveTunnels: List<Int> = emptyList()
|
||||
|
||||
@Synchronized
|
||||
fun getLastActiveTunnels(): List<Int> {
|
||||
return lastActiveTunnels
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setLastActiveTunnels(newTunnels: List<Int>) {
|
||||
lastActiveTunnels = newTunnels
|
||||
}
|
||||
|
||||
lateinit var instance: WireGuardAutoTunnel
|
||||
private set
|
||||
}
|
||||
}
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.broadcast
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AndroidEntryPoint
|
||||
class KernelReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
|
||||
|
||||
@Inject lateinit var tunnelRepository: TunnelRepository
|
||||
|
||||
@Inject lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject lateinit var tunnelManager: TunnelManager
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val action = intent.action ?: return
|
||||
applicationScope.launch {
|
||||
if (action == REFRESH_TUNNELS_ACTION) {
|
||||
tunnelManager.runningTunnelNames().forEach { name ->
|
||||
val tunnel = tunnelRepository.findByTunnelName(name)
|
||||
tunnel?.let { tunnelRepository.save(it.copy(isActive = true)) }
|
||||
}
|
||||
serviceManager.updateTunnelTile()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REFRESH_TUNNELS_ACTION = "com.wireguard.android.action.REFRESH_TUNNEL_STATES"
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.broadcast
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AndroidEntryPoint
|
||||
class NotificationActionReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject lateinit var tunnelManager: TunnelManager
|
||||
|
||||
@Inject lateinit var tunnelRepository: TunnelRepository
|
||||
|
||||
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
applicationScope.launch {
|
||||
when (intent.action) {
|
||||
NotificationAction.AUTO_TUNNEL_OFF.name -> serviceManager.stopAutoTunnel()
|
||||
NotificationAction.TUNNEL_OFF.name -> {
|
||||
val tunnelId = intent.getIntExtra(NotificationManager.EXTRA_ID, 0)
|
||||
if (tunnelId == STOP_ALL_TUNNELS_ID) return@launch tunnelManager.stopTunnel()
|
||||
val tunnel = tunnelRepository.getById(tunnelId)
|
||||
tunnelManager.stopTunnel(tunnel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val STOP_ALL_TUNNELS_ID = 0
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.broadcast
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
class RemoteControlReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
|
||||
|
||||
@Inject lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject lateinit var tunnelManager: TunnelManager
|
||||
|
||||
enum class Action(private val suffix: String) {
|
||||
START_TUNNEL("START_TUNNEL"),
|
||||
STOP_TUNNEL("STOP_TUNNEL"),
|
||||
START_AUTO_TUNNEL("START_AUTO_TUNNEL"),
|
||||
STOP_AUTO_TUNNEL("STOP_AUTO_TUNNEL");
|
||||
|
||||
fun getFullAction(): String {
|
||||
return "${Constants.BASE_PACKAGE}.$suffix"
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromAction(action: String): Action? {
|
||||
for (a in entries) {
|
||||
if (a.getFullAction() == action) {
|
||||
return a
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Timber.i("onReceive")
|
||||
val action = intent.action ?: return
|
||||
val appAction = Action.fromAction(action) ?: return Timber.w("Unknown action $action")
|
||||
applicationScope.launch {
|
||||
if (!appDataRepository.appState.isRemoteControlEnabled())
|
||||
return@launch Timber.w("Remote control disabled")
|
||||
val key =
|
||||
appDataRepository.appState.getRemoteKey()
|
||||
?: return@launch Timber.w("Remote control key missing")
|
||||
if (key != intent.getStringExtra(EXTRA_KEY)?.trim())
|
||||
return@launch Timber.w("Invalid remote control key")
|
||||
when (appAction) {
|
||||
Action.START_TUNNEL -> {
|
||||
val tunnelName =
|
||||
intent.getStringExtra(EXTRA_TUN_NAME) ?: return@launch startDefaultTunnel()
|
||||
val tunnel =
|
||||
appDataRepository.tunnels.findByTunnelName(tunnelName)
|
||||
?: return@launch startDefaultTunnel()
|
||||
tunnelManager.startTunnel(tunnel)
|
||||
}
|
||||
Action.STOP_TUNNEL -> {
|
||||
val tunnelName =
|
||||
intent.getStringExtra(EXTRA_TUN_NAME)
|
||||
?: return@launch tunnelManager.stopTunnel()
|
||||
val tunnel =
|
||||
appDataRepository.tunnels.findByTunnelName(tunnelName)
|
||||
?: return@launch tunnelManager.stopTunnel()
|
||||
tunnelManager.stopTunnel(tunnel)
|
||||
}
|
||||
Action.START_AUTO_TUNNEL -> serviceManager.startAutoTunnel()
|
||||
Action.STOP_AUTO_TUNNEL -> serviceManager.stopAutoTunnel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startDefaultTunnel() {
|
||||
appDataRepository.getPrimaryOrFirstTunnel()?.let { tunnel ->
|
||||
tunnelManager.startTunnel(tunnel)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_TUN_NAME = "tunnelName"
|
||||
const val EXTRA_KEY = "key"
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.broadcast
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.logcatter.LogReader
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
class RestartReceiver : BroadcastReceiver() {
|
||||
@Inject lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
|
||||
|
||||
@Inject lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject lateinit var tunnelManager: TunnelManager
|
||||
|
||||
@Inject lateinit var logReader: LogReader
|
||||
|
||||
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Timber.d("RestartReceiver triggered with action: ${intent.action}")
|
||||
serviceManager.updateTunnelTile()
|
||||
serviceManager.updateAutoTunnelTile()
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
val settings = appDataRepository.settings.get()
|
||||
if (settings.isRestoreOnBootEnabled) {
|
||||
if (
|
||||
settings.isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null
|
||||
) {
|
||||
Timber.d("Starting auto-tunnel on boot/update")
|
||||
serviceManager.startAutoTunnel()
|
||||
} else {
|
||||
Timber.d("Restoring previous tunnel state")
|
||||
tunnelManager.restorePreviousState()
|
||||
}
|
||||
} else {
|
||||
Timber.d("Restore on boot disabled, skipping")
|
||||
}
|
||||
if (intent.action == Intent.ACTION_MY_PACKAGE_REPLACED) logReader.deleteAndClearLogs()
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
|
||||
interface NotificationManager {
|
||||
val context: Context
|
||||
|
||||
fun createNotification(
|
||||
channel: NotificationChannels,
|
||||
title: String = "",
|
||||
actions: Collection<NotificationCompat.Action> = emptyList(),
|
||||
description: String = "",
|
||||
showTimestamp: Boolean = false,
|
||||
importance: Int = NotificationManager.IMPORTANCE_HIGH,
|
||||
onGoing: Boolean = true,
|
||||
onlyAlertOnce: Boolean = true,
|
||||
): Notification
|
||||
|
||||
fun createNotification(
|
||||
channel: NotificationChannels,
|
||||
title: StringValue,
|
||||
actions: Collection<NotificationCompat.Action> = emptyList(),
|
||||
description: StringValue,
|
||||
showTimestamp: Boolean = false,
|
||||
importance: Int = NotificationManager.IMPORTANCE_HIGH,
|
||||
onGoing: Boolean = true,
|
||||
onlyAlertOnce: Boolean = true,
|
||||
): Notification
|
||||
|
||||
fun createNotificationAction(
|
||||
notificationAction: NotificationAction,
|
||||
extraId: Int? = null,
|
||||
): NotificationCompat.Action
|
||||
|
||||
fun remove(notificationId: Int)
|
||||
|
||||
fun show(notificationId: Int, notification: Notification)
|
||||
|
||||
companion object {
|
||||
const val AUTO_TUNNEL_NOTIFICATION_ID = 122
|
||||
const val VPN_NOTIFICATION_ID = 100
|
||||
const val EXTRA_ID = "id"
|
||||
}
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Color
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.broadcast.NotificationActionReceiver
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager.Companion.EXTRA_ID
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class WireGuardNotification @Inject constructor(@ApplicationContext override val context: Context) :
|
||||
com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager {
|
||||
|
||||
enum class NotificationChannels {
|
||||
VPN,
|
||||
AUTO_TUNNEL,
|
||||
}
|
||||
|
||||
private val notificationManager = NotificationManagerCompat.from(context)
|
||||
|
||||
override fun createNotification(
|
||||
channel: NotificationChannels,
|
||||
title: String,
|
||||
actions: Collection<NotificationCompat.Action>,
|
||||
description: String,
|
||||
showTimestamp: Boolean,
|
||||
importance: Int,
|
||||
onGoing: Boolean,
|
||||
onlyAlertOnce: Boolean,
|
||||
): Notification {
|
||||
notificationManager.createNotificationChannel(channel.asChannel())
|
||||
return channel
|
||||
.asBuilder()
|
||||
.apply {
|
||||
actions.forEach { addAction(it) }
|
||||
setContentTitle(title)
|
||||
setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
Intent(context, MainActivity::class.java),
|
||||
PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
)
|
||||
setContentText(description)
|
||||
setOnlyAlertOnce(onlyAlertOnce)
|
||||
setOngoing(onGoing)
|
||||
setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
setShowWhen(showTimestamp)
|
||||
setSmallIcon(R.drawable.ic_notification)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun createNotification(
|
||||
channel: NotificationChannels,
|
||||
title: StringValue,
|
||||
actions: Collection<NotificationCompat.Action>,
|
||||
description: StringValue,
|
||||
showTimestamp: Boolean,
|
||||
importance: Int,
|
||||
onGoing: Boolean,
|
||||
onlyAlertOnce: Boolean,
|
||||
): Notification {
|
||||
return createNotification(
|
||||
channel,
|
||||
title.asString(context),
|
||||
actions,
|
||||
description.asString(context),
|
||||
showTimestamp,
|
||||
importance,
|
||||
onGoing,
|
||||
onlyAlertOnce,
|
||||
)
|
||||
}
|
||||
|
||||
override fun createNotificationAction(
|
||||
notificationAction: NotificationAction,
|
||||
extraId: Int?,
|
||||
): NotificationCompat.Action {
|
||||
val pendingIntent =
|
||||
PendingIntent.getBroadcast(
|
||||
context,
|
||||
0,
|
||||
Intent(context, NotificationActionReceiver::class.java).apply {
|
||||
action = notificationAction.name
|
||||
if (extraId != null) putExtra(EXTRA_ID, extraId)
|
||||
},
|
||||
PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
return NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_notification,
|
||||
notificationAction.title(context).uppercase(),
|
||||
pendingIntent,
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun remove(notificationId: Int) {
|
||||
notificationManager.cancel(notificationId)
|
||||
}
|
||||
|
||||
override fun show(notificationId: Int, notification: Notification) {
|
||||
with(notificationManager) {
|
||||
if (
|
||||
ActivityCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.POST_NOTIFICATIONS,
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
return
|
||||
}
|
||||
notify(notificationId, notification)
|
||||
}
|
||||
}
|
||||
|
||||
private fun NotificationChannels.asBuilder(): NotificationCompat.Builder {
|
||||
return when (this) {
|
||||
NotificationChannels.AUTO_TUNNEL -> {
|
||||
NotificationCompat.Builder(
|
||||
context,
|
||||
context.getString(R.string.auto_tunnel_channel_id),
|
||||
)
|
||||
}
|
||||
NotificationChannels.VPN -> {
|
||||
NotificationCompat.Builder(context, context.getString(R.string.vpn_channel_id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun NotificationChannels.asChannel(): NotificationChannel {
|
||||
return when (this) {
|
||||
NotificationChannels.VPN -> {
|
||||
NotificationChannel(
|
||||
context.getString(R.string.vpn_channel_id),
|
||||
context.getString(R.string.vpn_channel_name),
|
||||
NotificationManager.IMPORTANCE_HIGH,
|
||||
)
|
||||
.apply {
|
||||
description = context.getString(R.string.vpn_channel_description)
|
||||
enableLights(true)
|
||||
lightColor = Color.WHITE
|
||||
enableVibration(false)
|
||||
vibrationPattern = longArrayOf(100, 200, 300)
|
||||
}
|
||||
}
|
||||
NotificationChannels.AUTO_TUNNEL -> {
|
||||
NotificationChannel(
|
||||
context.getString(R.string.auto_tunnel_channel_id),
|
||||
context.getString(R.string.auto_tunnel_channel_name),
|
||||
NotificationManager.IMPORTANCE_HIGH,
|
||||
)
|
||||
.apply {
|
||||
description = context.getString(R.string.auto_tunnel_channel_description)
|
||||
enableLights(true)
|
||||
lightColor = Color.WHITE
|
||||
enableVibration(false)
|
||||
vibrationPattern = longArrayOf(100, 200, 300)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.net.VpnService
|
||||
import android.os.IBinder
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
|
||||
import jakarta.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
class ServiceManager
|
||||
@Inject
|
||||
constructor(
|
||||
private val context: Context,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||
@MainDispatcher private val mainDispatcher: CoroutineDispatcher,
|
||||
private val appDataRepository: AppDataRepository,
|
||||
) {
|
||||
|
||||
private val autoTunnelMutex = Mutex()
|
||||
|
||||
private val _tunnelService = MutableStateFlow<TunnelForegroundService?>(null)
|
||||
private val _autoTunnelService = MutableStateFlow<AutoTunnelService?>(null)
|
||||
val autoTunnelService = _autoTunnelService.asStateFlow()
|
||||
|
||||
private val tunnelServiceConnection =
|
||||
object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||
val binder = service as? TunnelForegroundService.LocalBinder
|
||||
_tunnelService.value = binder?.service
|
||||
Timber.d("TunnelForegroundService connected")
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
_tunnelService.value = null
|
||||
Timber.d("TunnelForegroundService disconnected")
|
||||
}
|
||||
}
|
||||
|
||||
private val autoTunnelServiceConnection =
|
||||
object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||
val binder = service as? AutoTunnelService.LocalBinder
|
||||
_autoTunnelService.value = binder?.service
|
||||
Timber.d("AutoTunnelService connected")
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
_autoTunnelService.value = null
|
||||
Timber.d("AutoTunnelService disconnected")
|
||||
}
|
||||
}
|
||||
|
||||
fun hasVpnPermission(): Boolean {
|
||||
return VpnService.prepare(context) == null
|
||||
}
|
||||
|
||||
suspend fun startAutoTunnel() {
|
||||
autoTunnelMutex.withLock {
|
||||
val settings = appDataRepository.settings.get()
|
||||
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true))
|
||||
if (_autoTunnelService.value != null) return
|
||||
withContext(ioDispatcher) {
|
||||
val intent = Intent(context, AutoTunnelService::class.java)
|
||||
context.startForegroundService(intent)
|
||||
context.bindService(intent, autoTunnelServiceConnection, Context.BIND_AUTO_CREATE)
|
||||
withContext(mainDispatcher) { updateAutoTunnelTile() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stopAutoTunnel() {
|
||||
autoTunnelMutex.withLock {
|
||||
val settings = appDataRepository.settings.get()
|
||||
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false))
|
||||
if (_autoTunnelService.value == null) return
|
||||
_autoTunnelService.value?.let { service ->
|
||||
service.stop()
|
||||
try {
|
||||
context.unbindService(autoTunnelServiceConnection)
|
||||
} finally {
|
||||
_tunnelService.value = null
|
||||
}
|
||||
}
|
||||
withContext(mainDispatcher) { updateAutoTunnelTile() }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startTunnelForegroundService() {
|
||||
if (_tunnelService.value != null) return
|
||||
withContext(ioDispatcher) {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
val intent = Intent(context, TunnelForegroundService::class.java)
|
||||
context.startForegroundService(intent)
|
||||
context.bindService(intent, tunnelServiceConnection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stopTunnelForegroundService() {
|
||||
_tunnelService.value?.let { service ->
|
||||
service.stop()
|
||||
try {
|
||||
context.unbindService(tunnelServiceConnection)
|
||||
} finally {
|
||||
_tunnelService.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleAutoTunnel() {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
if (_autoTunnelService.value != null) stopAutoTunnel() else startAutoTunnel()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateAutoTunnelTile() {
|
||||
context.requestAutoTunnelTileServiceUpdate()
|
||||
}
|
||||
|
||||
fun updateTunnelTile() {
|
||||
context.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
|
||||
fun handleTunnelServiceDestroy() {
|
||||
_tunnelService.update { null }
|
||||
}
|
||||
|
||||
fun handleAutoTunnelServiceDestroy() {
|
||||
_autoTunnelService.update { null }
|
||||
}
|
||||
}
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Intent
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkStatus
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
class TunnelForegroundService : LifecycleService() {
|
||||
|
||||
@Inject lateinit var notificationManager: NotificationManager
|
||||
|
||||
@Inject lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject lateinit var networkMonitor: NetworkMonitor
|
||||
|
||||
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
|
||||
|
||||
@Inject lateinit var tunnelRepo: TunnelRepository
|
||||
|
||||
@Inject lateinit var tunnelManager: TunnelManager
|
||||
|
||||
private val isNetworkConnected = MutableStateFlow(true)
|
||||
|
||||
private val tunnelJobs = ConcurrentHashMap<TunnelConf, Job>()
|
||||
private val pingJobs = ConcurrentHashMap<TunnelConf, Job>()
|
||||
|
||||
private val jobsMutex = Mutex()
|
||||
|
||||
class LocalBinder(val service: TunnelForegroundService) : Binder()
|
||||
|
||||
private val binder = LocalBinder(this)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
ServiceCompat.startForeground(
|
||||
this@TunnelForegroundService,
|
||||
NotificationManager.VPN_NOTIFICATION_ID,
|
||||
onCreateNotification(),
|
||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
super.onBind(intent)
|
||||
return binder
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
ServiceCompat.startForeground(
|
||||
this@TunnelForegroundService,
|
||||
NotificationManager.VPN_NOTIFICATION_ID,
|
||||
onCreateNotification(),
|
||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||
)
|
||||
start()
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
fun start() =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
tunnelManager.activeTunnels.distinctByKeys().collect { activeTunnels ->
|
||||
// No active tunnels and no jobs: nothing to do
|
||||
if (activeTunnels.isEmpty() && tunnelJobs.isEmpty()) return@collect
|
||||
|
||||
// Synchronize jobs with active tunnels
|
||||
synchronizeJobs(activeTunnels)
|
||||
updateServiceNotification()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun synchronizeJobs(activeTunnels: Map<TunnelConf, TunnelState>) {
|
||||
jobsMutex.withLock {
|
||||
// Stop jobs for tunnels that are no longer active
|
||||
stopInactiveJobs(activeTunnels)
|
||||
// Start jobs for new tunnels
|
||||
startNewJobs(activeTunnels)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopInactiveJobs(activeTunnels: Map<TunnelConf, TunnelState>) {
|
||||
// If no active tunnels, clear all jobs
|
||||
if (activeTunnels.isEmpty()) {
|
||||
clearAllJobs()
|
||||
return
|
||||
}
|
||||
// Stop jobs for tunnels not in activeTunnels
|
||||
val tunnelsToStop = tunnelJobs.keys - activeTunnels.keys
|
||||
tunnelsToStop.forEach { tun -> stopTunnelJobs(tun) }
|
||||
}
|
||||
|
||||
private fun clearAllJobs() {
|
||||
tunnelJobs.forEach { (tun, job) ->
|
||||
Timber.d("Stopping tunnel job for ${tun.tunName}")
|
||||
job.cancel()
|
||||
}
|
||||
tunnelJobs.clear()
|
||||
|
||||
pingJobs.forEach { (tun, job) ->
|
||||
if (isPingBounce(tun)) {
|
||||
Timber.d("Preserving ping job for ${tun.tunName} due to PING bounce")
|
||||
return@forEach
|
||||
}
|
||||
Timber.d("Stopping ping job for ${tun.tunName}")
|
||||
job.cancel()
|
||||
}
|
||||
pingJobs.entries.removeIf { (tun, _) -> !isPingBounce(tun) }
|
||||
}
|
||||
|
||||
private fun stopTunnelJobs(tun: TunnelConf) {
|
||||
tunnelJobs.remove(tun)?.cancel()
|
||||
Timber.d("Stopped tunnel job for ${tun.tunName}")
|
||||
if (isPingBounce(tun))
|
||||
return Timber.d("Preserving ${tun.tunName} ping job due to ping bounce")
|
||||
pingJobs.remove(tun)?.cancel()
|
||||
Timber.d("Stopped ping job for ${tun.tunName}")
|
||||
}
|
||||
|
||||
private fun startNewJobs(activeTunnels: Map<TunnelConf, TunnelState>) {
|
||||
val tunnelsToStart = activeTunnels.keys - tunnelJobs.keys
|
||||
tunnelsToStart.forEach { tun ->
|
||||
tunnelJobs[tun] = startTunnelJobs(tun)
|
||||
Timber.d("Started tunnel job for ${tun.tunName}")
|
||||
|
||||
if (pingJobs[tun]?.isActive == true) {
|
||||
Timber.d("Reusing active ping job for ${tun.tunName}")
|
||||
} else {
|
||||
pingJobs[tun]?.cancel() // Cancel any stale job
|
||||
if (tun.isPingEnabled) {
|
||||
if (tun.isStaticallyConfigured()) {
|
||||
Timber.d("Skipping ping for statically configured tunnel")
|
||||
} else {
|
||||
pingJobs[tun] = startPingJob(tun)
|
||||
Timber.d("Started ping job for ${tun.tunName}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPingBounce(tun: TunnelConf): Boolean =
|
||||
tunnelManager.bouncingTunnelIds[tun.id] == TunnelStatus.StopReason.PING
|
||||
|
||||
// TODO Would be cool to have this include kill switch
|
||||
// TODO also we need to include errors
|
||||
private fun updateServiceNotification() {
|
||||
val notification =
|
||||
when (tunnelJobs.size) {
|
||||
0 -> onCreateNotification()
|
||||
1 -> createTunnelNotification(tunnelJobs.keys.first())
|
||||
else -> createTunnelsNotification()
|
||||
}
|
||||
ServiceCompat.startForeground(
|
||||
this@TunnelForegroundService,
|
||||
NotificationManager.VPN_NOTIFICATION_ID,
|
||||
notification,
|
||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||
)
|
||||
}
|
||||
|
||||
// use same scope so we can cancel all of these
|
||||
private fun startTunnelJobs(tunnelConf: TunnelConf) =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
// monitor if we have internet connectivity
|
||||
launch { startNetworkMonitorJob() }
|
||||
// job to trigger stats emit on interval
|
||||
launch { startTunnelStatsJob(tunnelConf) }
|
||||
// monitor changes to the tunnel config
|
||||
launch { startTunnelConfChangesJob(tunnelConf) }
|
||||
}
|
||||
|
||||
private suspend fun startTunnelConfChangesJob(tunnelConf: TunnelConf) {
|
||||
tunnelRepo.flow
|
||||
.flowOn(ioDispatcher)
|
||||
.map { storedTunnels -> storedTunnels.firstOrNull { it.id == tunnelConf.id } }
|
||||
.filterNotNull()
|
||||
// only emit when one of these 3 values change
|
||||
.distinctUntilChanged { old, new -> old == new }
|
||||
.collect { storedTunnel ->
|
||||
if (tunnelConf != storedTunnel) {
|
||||
Timber.d("Config changed for ${storedTunnel.tunName}, bouncing")
|
||||
// let this complete, even after cancel
|
||||
withContext(NonCancellable) {
|
||||
tunnelManager.bounceTunnel(
|
||||
storedTunnel,
|
||||
TunnelStatus.StopReason.CONFIG_CHANGED,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startNetworkMonitorJob() {
|
||||
networkMonitor.networkStatusFlow.flowOn(ioDispatcher).collectLatest { status ->
|
||||
val isAvailable = status !is NetworkStatus.Disconnected
|
||||
isNetworkConnected.value = isAvailable
|
||||
Timber.d("Network available: $status")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startTunnelStatsJob(tunnel: TunnelConf) = coroutineScope {
|
||||
while (isActive) {
|
||||
tunnelManager.updateTunnelStatistics(tunnel)
|
||||
delay(STATS_DELAY)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startPingJob(tunnel: TunnelConf) =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
// delay for initial duration
|
||||
delay(tunnel.pingInterval ?: Constants.PING_INTERVAL)
|
||||
while (isActive) {
|
||||
val shouldBounce = shouldBounceTunnel(tunnel)
|
||||
val delayMs =
|
||||
if (shouldBounce) {
|
||||
// let this complete, even after cancel
|
||||
withContext(NonCancellable) {
|
||||
tunnelManager.bounceTunnel(tunnel, TunnelStatus.StopReason.PING)
|
||||
}
|
||||
tunnel.pingCooldown ?: Constants.PING_COOLDOWN
|
||||
} else {
|
||||
tunnel.pingInterval ?: Constants.PING_INTERVAL
|
||||
}
|
||||
delay(delayMs)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun shouldBounceTunnel(tunnel: TunnelConf): Boolean {
|
||||
if (!isNetworkConnected.value) {
|
||||
Timber.d("Network disconnected, skipping ping for ${tunnel.tunName}")
|
||||
return false
|
||||
}
|
||||
return runCatching { !tunnel.isTunnelPingable(ioDispatcher) }
|
||||
.onFailure { e -> Timber.e(e, "Ping check failed for ${tunnel.tunName}") }
|
||||
.getOrDefault(true)
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
serviceManager.handleTunnelServiceDestroy()
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun createTunnelNotification(tunnelConf: TunnelConf): Notification {
|
||||
return notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.VPN,
|
||||
title = "${getString(R.string.tunnel_running)} - ${tunnelConf.tunName}",
|
||||
actions =
|
||||
listOf(
|
||||
notificationManager.createNotificationAction(
|
||||
NotificationAction.TUNNEL_OFF,
|
||||
tunnelConf.id,
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createTunnelsNotification(): Notification {
|
||||
return notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.VPN,
|
||||
title = "${getString(R.string.tunnel_running)} - ${getString(R.string.multiple)}",
|
||||
actions =
|
||||
listOf(
|
||||
notificationManager.createNotificationAction(NotificationAction.TUNNEL_OFF, 0)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun onCreateNotification(): Notification {
|
||||
return notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.VPN,
|
||||
title = getString(R.string.tunnel_starting),
|
||||
)
|
||||
}
|
||||
|
||||
// TODO add notification handling and optional log reading for restart on handshake failures
|
||||
companion object {
|
||||
const val STATS_DELAY = 1_000L
|
||||
// ipv6 disabled or block on network
|
||||
// Failed to send handshake initiation: write udp [::]"
|
||||
// Failed to send data packets: write udp [::]
|
||||
// Failed to send data packets: write udp 0.0.0.0:51820
|
||||
// Handshake did not complete after 5 seconds, retrying
|
||||
}
|
||||
}
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkStatus
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AutoTunnelService : LifecycleService() {
|
||||
|
||||
@Inject lateinit var networkMonitor: NetworkMonitor
|
||||
|
||||
@Inject lateinit var appDataRepository: Provider<AppDataRepository>
|
||||
|
||||
@Inject lateinit var notificationManager: NotificationManager
|
||||
|
||||
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
|
||||
|
||||
@Inject lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject lateinit var tunnelManager: TunnelManager
|
||||
|
||||
private val defaultState = AutoTunnelState()
|
||||
|
||||
private val autoTunnelStateFlow = MutableStateFlow(defaultState)
|
||||
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
|
||||
private var killSwitchJob: Job? = null
|
||||
|
||||
class LocalBinder(val service: AutoTunnelService) : Binder()
|
||||
|
||||
private val binder = LocalBinder(this)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
launchWatcherNotification()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
super.onBind(intent)
|
||||
return binder
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
Timber.d("onStartCommand executed with startId: $startId")
|
||||
start()
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
fun start() {
|
||||
kotlin
|
||||
.runCatching {
|
||||
launchWatcherNotification()
|
||||
initWakeLock()
|
||||
startAutoTunnelJob()
|
||||
startAutoTunnelStateJob()
|
||||
killSwitchJob = startKillSwitchJob()
|
||||
}
|
||||
.onFailure { Timber.e(it) }
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
wakeLock?.let { if (it.isHeld) it.release() }
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
serviceManager.handleAutoTunnelServiceDestroy()
|
||||
restoreVpnKillSwitch()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun restoreVpnKillSwitch() {
|
||||
with(autoTunnelStateFlow.value) {
|
||||
if (
|
||||
settings.isVpnKillSwitchEnabled &&
|
||||
tunnelManager.getBackendState() != BackendState.KILL_SWITCH_ACTIVE
|
||||
) {
|
||||
killSwitchJob?.cancel()
|
||||
val allowedIps =
|
||||
if (settings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS
|
||||
else emptyList()
|
||||
tunnelManager.setBackendState(BackendState.KILL_SWITCH_ACTIVE, allowedIps)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchWatcherNotification(
|
||||
description: String = getString(R.string.monitoring_state_changes)
|
||||
) {
|
||||
val notification =
|
||||
notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
|
||||
title = getString(R.string.auto_tunnel_title),
|
||||
description = description,
|
||||
actions =
|
||||
listOf(
|
||||
notificationManager.createNotificationAction(
|
||||
NotificationAction.AUTO_TUNNEL_OFF
|
||||
)
|
||||
),
|
||||
)
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
NotificationManager.AUTO_TUNNEL_NOTIFICATION_ID,
|
||||
notification,
|
||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||
)
|
||||
}
|
||||
|
||||
private fun initWakeLock() {
|
||||
wakeLock =
|
||||
(getSystemService(POWER_SERVICE) as PowerManager).run {
|
||||
val tag = this.javaClass.name
|
||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
||||
try {
|
||||
Timber.i("Initiating wakelock with 10 min timeout")
|
||||
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNetworkState(networkStatus: NetworkStatus): NetworkState {
|
||||
return with(autoTunnelStateFlow.value.networkState) {
|
||||
val wifiName =
|
||||
when (networkStatus) {
|
||||
is NetworkStatus.Connected -> {
|
||||
networkStatus.wifiSsid
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
copy(
|
||||
isWifiConnected = networkStatus.wifiConnected,
|
||||
isMobileDataConnected = networkStatus.cellularConnected,
|
||||
isEthernetConnected = networkStatus.ethernetConnected,
|
||||
wifiName = wifiName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun startAutoTunnelStateJob() =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
combine(
|
||||
combineSettings(),
|
||||
appDataRepository
|
||||
.get()
|
||||
.settings
|
||||
.flow
|
||||
.distinctUntilChanged { old, new ->
|
||||
old.isKernelEnabled == new.isKernelEnabled
|
||||
} // Only emit when isKernelEnabled changes
|
||||
.flatMapLatest {
|
||||
networkMonitor.networkStatusFlow.flowOn(ioDispatcher).map {
|
||||
buildNetworkState(it)
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged(),
|
||||
) { double, networkState ->
|
||||
AutoTunnelState(
|
||||
tunnelManager.activeTunnels.value,
|
||||
networkState,
|
||||
double.first,
|
||||
double.second,
|
||||
)
|
||||
}
|
||||
.collect { state ->
|
||||
autoTunnelStateFlow.update {
|
||||
it.copy(
|
||||
activeTunnels = state.activeTunnels,
|
||||
networkState = state.networkState,
|
||||
settings = state.settings,
|
||||
tunnels = state.tunnels,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun combineSettings(): Flow<Pair<AppSettings, Tunnels>> {
|
||||
return combine(
|
||||
appDataRepository.get().settings.flow,
|
||||
appDataRepository.get().tunnels.flow.map { tunnels ->
|
||||
// isActive is ignored for equality checks so user can manually toggle off
|
||||
// tunnel with auto-tunnel
|
||||
tunnels.map { it.copy(isActive = false) }
|
||||
},
|
||||
) { settings, tunnels ->
|
||||
Pair(settings, tunnels)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private fun startKillSwitchJob() =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
autoTunnelStateFlow.collect {
|
||||
if (it == defaultState) return@collect
|
||||
when (val event = it.asKillSwitchEvent()) {
|
||||
KillSwitchEvent.DoNothing -> Unit
|
||||
is KillSwitchEvent.Start -> {
|
||||
Timber.d("Starting kill switch")
|
||||
tunnelManager.setBackendState(
|
||||
BackendState.KILL_SWITCH_ACTIVE,
|
||||
event.allowedIps,
|
||||
)
|
||||
}
|
||||
KillSwitchEvent.Stop -> {
|
||||
Timber.d("Stopping kill switch")
|
||||
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptySet())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private fun startAutoTunnelJob() =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
Timber.i("Starting auto-tunnel network event watcher")
|
||||
val settings = appDataRepository.get().settings.get()
|
||||
Timber.d("Starting with debounce delay of: ${settings.debounceDelaySeconds} seconds")
|
||||
autoTunnelStateFlow.debounce(settings.debounceDelayMillis()).collect { watcherState ->
|
||||
if (watcherState == defaultState) return@collect
|
||||
Timber.d("New auto tunnel state emitted ${watcherState.networkState}")
|
||||
when (val event = watcherState.asAutoTunnelEvent()) {
|
||||
is AutoTunnelEvent.Start ->
|
||||
(event.tunnelConf ?: appDataRepository.get().getPrimaryOrFirstTunnel())
|
||||
?.let { tunnelManager.startTunnel(it) }
|
||||
// TODO improve this to target specific tunnels to better support multi-tunnel
|
||||
is AutoTunnelEvent.Stop -> tunnelManager.stopTunnel()
|
||||
AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: no condition met")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LifecycleRegistry
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AutoTunnelControlTile : TileService(), LifecycleOwner {
|
||||
@Inject lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject lateinit var serviceManager: ServiceManager
|
||||
|
||||
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
}
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
||||
Timber.d("Start listening called for auto tunnel tile")
|
||||
lifecycleScope.launch {
|
||||
serviceManager.autoTunnelService.collect {
|
||||
if (it != null) return@collect setActive()
|
||||
setInactive()
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
appDataRepository.tunnels.flow.collect {
|
||||
if (it.isEmpty()) {
|
||||
setUnavailable()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
unlockAndRun {
|
||||
lifecycleScope.launch {
|
||||
if (serviceManager.autoTunnelService.value != null) {
|
||||
serviceManager.stopAutoTunnel()
|
||||
setInactive()
|
||||
} else {
|
||||
serviceManager.startAutoTunnel()
|
||||
setActive()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setActive() {
|
||||
runCatching {
|
||||
qsTile.state = Tile.STATE_ACTIVE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setInactive() {
|
||||
runCatching {
|
||||
qsTile.state = Tile.STATE_INACTIVE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
/* This works around an annoying unsolved frameworks bug some people are hitting. */
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
var ret: IBinder? = null
|
||||
try {
|
||||
ret = super.onBind(intent)
|
||||
} catch (_: Throwable) {
|
||||
Timber.e("Failed to bind to TunnelControlTile")
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
private fun setUnavailable() {
|
||||
runCatching {
|
||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
override val lifecycle: Lifecycle
|
||||
get() = lifecycleRegistry
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LifecycleRegistry
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
class TunnelControlTile : TileService(), LifecycleOwner {
|
||||
@Inject lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject lateinit var tunnelManager: TunnelManager
|
||||
|
||||
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
|
||||
|
||||
private var isCollecting = false
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
}
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
||||
Timber.d("Start listening called for tunnel tile")
|
||||
if (isCollecting) return
|
||||
isCollecting = true
|
||||
lifecycleScope.launch { tunnelManager.activeTunnels.collect { updateTileState() } }
|
||||
}
|
||||
|
||||
private suspend fun updateTileState() {
|
||||
try {
|
||||
val tunnels = appDataRepository.tunnels.getAll()
|
||||
if (tunnels.isEmpty()) {
|
||||
setUnavailable()
|
||||
return
|
||||
}
|
||||
|
||||
val activeTunnels =
|
||||
tunnelManager.activeTunnels.value.filter { it.value.status.isUpOrStarting() }
|
||||
|
||||
when {
|
||||
activeTunnels.isNotEmpty() -> {
|
||||
val activeIds = activeTunnels.map { it.key.id }
|
||||
// TODO improvements would be needed to make this work well with toggling
|
||||
// multiple tunnels
|
||||
// this would be better managed elsewhere
|
||||
WireGuardAutoTunnel.setLastActiveTunnels(activeIds)
|
||||
updateTileForActiveTunnels(activeTunnels)
|
||||
}
|
||||
else -> updateTileForLastActiveTunnels()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
setUnavailable()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTileForActiveTunnels(activeTunnels: Map<TunnelConf, TunnelState>) {
|
||||
val tileName =
|
||||
when (activeTunnels.size) {
|
||||
1 -> activeTunnels.keys.first().tunName
|
||||
else -> getString(R.string.multiple)
|
||||
}
|
||||
updateTile(tileName, true)
|
||||
}
|
||||
|
||||
private suspend fun updateTileForLastActiveTunnels() {
|
||||
val lastActiveIds = WireGuardAutoTunnel.getLastActiveTunnels()
|
||||
when {
|
||||
lastActiveIds.isEmpty() -> {
|
||||
appDataRepository.getStartTunnelConfig()?.let { config ->
|
||||
updateTile(config.tunName, false)
|
||||
} ?: setUnavailable()
|
||||
}
|
||||
lastActiveIds.size > 1 -> updateTile(getString(R.string.multiple), false)
|
||||
else -> {
|
||||
val tunnelId = lastActiveIds.first()
|
||||
appDataRepository.tunnels.getById(tunnelId)?.let { tunnel ->
|
||||
updateTile(tunnel.tunName, false)
|
||||
} ?: setUnavailable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
unlockAndRun {
|
||||
lifecycleScope.launch {
|
||||
if (tunnelManager.activeTunnels.value.isNotEmpty())
|
||||
return@launch tunnelManager.stopTunnel()
|
||||
val lastActive = WireGuardAutoTunnel.getLastActiveTunnels()
|
||||
if (lastActive.isEmpty()) {
|
||||
appDataRepository.getStartTunnelConfig()?.let { tunnelManager.startTunnel(it) }
|
||||
} else {
|
||||
lastActive.forEach { id ->
|
||||
appDataRepository.tunnels.getById(id)?.let { tunnelManager.startTunnel(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setActive() {
|
||||
runCatching {
|
||||
qsTile.state = Tile.STATE_ACTIVE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setInactive() {
|
||||
runCatching {
|
||||
qsTile.state = Tile.STATE_INACTIVE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUnavailable() {
|
||||
runCatching {
|
||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||
setTileDescription("")
|
||||
qsTile.updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setTileDescription(description: String) {
|
||||
runCatching {
|
||||
if (qsTile == null) return@runCatching
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
qsTile.subtitle = description
|
||||
qsTile.stateDescription = description
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
qsTile.subtitle = description
|
||||
}
|
||||
qsTile.updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
/* This works around an annoying unsolved frameworks bug some people are hitting. */
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
var ret: IBinder? = null
|
||||
try {
|
||||
ret = super.onBind(intent)
|
||||
} catch (_: Throwable) {
|
||||
Timber.e("Failed to bind to TunnelControlTile")
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
private fun updateTile(name: String, active: Boolean) {
|
||||
runCatching {
|
||||
setTileDescription(name)
|
||||
if (active) return setActive()
|
||||
setInactive()
|
||||
}
|
||||
.onFailure { Timber.e(it) }
|
||||
}
|
||||
|
||||
override val lifecycle: Lifecycle
|
||||
get() = lifecycleRegistry
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.shortcut
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class DynamicShortcutManager(
|
||||
private val context: Context,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : ShortcutManager {
|
||||
override suspend fun addShortcuts() {
|
||||
withContext(ioDispatcher) {
|
||||
ShortcutManagerCompat.setDynamicShortcuts(context, createShortcuts())
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun removeShortcuts() {
|
||||
withContext(ioDispatcher) {
|
||||
ShortcutManagerCompat.removeDynamicShortcuts(context, createShortcuts().map { it.id })
|
||||
}
|
||||
}
|
||||
|
||||
private fun createShortcuts(): List<ShortcutInfoCompat> {
|
||||
return listOf(
|
||||
buildShortcut(
|
||||
context.getString(R.string.vpn_off),
|
||||
context.getString(R.string.vpn_off),
|
||||
context.getString(R.string.vpn_off),
|
||||
intent =
|
||||
Intent(context, ShortcutsActivity::class.java).apply {
|
||||
putExtra("className", "WireGuardTunnelService")
|
||||
action = ShortcutsActivity.Action.STOP.name
|
||||
},
|
||||
shortcutIcon = R.drawable.vpn_off,
|
||||
),
|
||||
buildShortcut(
|
||||
context.getString(R.string.vpn_on),
|
||||
context.getString(R.string.vpn_on),
|
||||
context.getString(R.string.vpn_on),
|
||||
intent =
|
||||
Intent(context, ShortcutsActivity::class.java).apply {
|
||||
putExtra("className", "WireGuardTunnelService")
|
||||
action = ShortcutsActivity.Action.START.name
|
||||
},
|
||||
shortcutIcon = R.drawable.vpn_on,
|
||||
),
|
||||
buildShortcut(
|
||||
context.getString(R.string.start_auto),
|
||||
context.getString(R.string.start_auto),
|
||||
context.getString(R.string.start_auto),
|
||||
intent =
|
||||
Intent(context, ShortcutsActivity::class.java).apply {
|
||||
putExtra("className", "WireGuardConnectivityWatcherService")
|
||||
action = ShortcutsActivity.Action.START.name
|
||||
},
|
||||
shortcutIcon = R.drawable.auto_play,
|
||||
),
|
||||
buildShortcut(
|
||||
context.getString(R.string.stop_auto),
|
||||
context.getString(R.string.stop_auto),
|
||||
context.getString(R.string.stop_auto),
|
||||
intent =
|
||||
Intent(context, ShortcutsActivity::class.java).apply {
|
||||
putExtra("className", "WireGuardConnectivityWatcherService")
|
||||
action = ShortcutsActivity.Action.STOP.name
|
||||
},
|
||||
shortcutIcon = R.drawable.auto_pause,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildShortcut(
|
||||
id: String,
|
||||
shortLabel: String,
|
||||
longLabel: String,
|
||||
intent: Intent,
|
||||
shortcutIcon: Int,
|
||||
): ShortcutInfoCompat {
|
||||
return ShortcutInfoCompat.Builder(context, id)
|
||||
.setShortLabel(shortLabel)
|
||||
.setLongLabel(longLabel)
|
||||
.setIntent(intent)
|
||||
.setIcon(IconCompat.createWithResource(context, shortcutIcon))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.shortcut
|
||||
|
||||
interface ShortcutManager {
|
||||
suspend fun addShortcuts()
|
||||
|
||||
suspend fun removeShortcuts()
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.shortcut
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ShortcutsActivity : ComponentActivity() {
|
||||
@Inject lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject lateinit var tunnelManager: TunnelManager
|
||||
|
||||
@Inject @ApplicationScope lateinit var applicationScope: CoroutineScope
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
applicationScope.launch {
|
||||
val settings = appDataRepository.settings.get()
|
||||
if (settings.isShortcutsEnabled) {
|
||||
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
|
||||
LEGACY_TUNNEL_SERVICE_NAME,
|
||||
TunnelProvider::class.java.simpleName -> {
|
||||
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
|
||||
Timber.d("Tunnel name extra: $tunnelName")
|
||||
val tunnelConfig =
|
||||
tunnelName?.let {
|
||||
appDataRepository.tunnels.getAll().firstOrNull {
|
||||
it.tunName == tunnelName
|
||||
}
|
||||
} ?: appDataRepository.getStartTunnelConfig()
|
||||
Timber.d("Shortcut action on name: ${tunnelConfig?.tunName}")
|
||||
tunnelConfig?.let {
|
||||
when (intent.action) {
|
||||
Action.START.name -> tunnelManager.startTunnel(it)
|
||||
Action.STOP.name -> tunnelManager.stopTunnel()
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
AutoTunnelService::class.java.simpleName,
|
||||
LEGACY_AUTO_TUNNEL_SERVICE_NAME -> {
|
||||
when (intent.action) {
|
||||
Action.START.name -> serviceManager.startAutoTunnel()
|
||||
Action.STOP.name -> serviceManager.stopAutoTunnel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
enum class Action {
|
||||
START,
|
||||
STOP,
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LEGACY_TUNNEL_SERVICE_NAME = "WireGuardTunnelService"
|
||||
const val LEGACY_AUTO_TUNNEL_SERVICE_NAME = "WireGuardConnectivityWatcherService"
|
||||
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName"
|
||||
const val CLASS_NAME_EXTRA_KEY = "className"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.concurrent.thread
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import timber.log.Timber
|
||||
|
||||
abstract class BaseTunnel(
|
||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||
private val appDataRepository: AppDataRepository,
|
||||
private val serviceManager: ServiceManager,
|
||||
) : TunnelProvider {
|
||||
|
||||
private val _errorEvents =
|
||||
MutableSharedFlow<Pair<TunnelConf, BackendError>>(replay = 0, extraBufferCapacity = 1)
|
||||
override val errorEvents = _errorEvents.asSharedFlow()
|
||||
|
||||
private val activeTuns = MutableStateFlow<Map<TunnelConf, TunnelState>>(emptyMap())
|
||||
private val tunThreads = ConcurrentHashMap<Int, Thread>()
|
||||
override val activeTunnels = activeTuns.asStateFlow()
|
||||
|
||||
private val tunMutex = Mutex()
|
||||
private val tunStatusMutex = Mutex()
|
||||
private val bounceTunnelMutex = Mutex()
|
||||
|
||||
override val bouncingTunnelIds = ConcurrentHashMap<Int, TunnelStatus.StopReason>()
|
||||
|
||||
abstract suspend fun startBackend(tunnel: TunnelConf)
|
||||
|
||||
abstract fun stopBackend(tunnel: TunnelConf)
|
||||
|
||||
override fun hasVpnPermission(): Boolean {
|
||||
return serviceManager.hasVpnPermission()
|
||||
}
|
||||
|
||||
protected suspend fun updateTunnelStatus(
|
||||
tunnelConf: TunnelConf,
|
||||
status: TunnelStatus? = null,
|
||||
stats: TunnelStatistics? = null,
|
||||
) {
|
||||
tunStatusMutex.withLock {
|
||||
activeTuns.update { currentTuns ->
|
||||
val originalConf = currentTuns.getKeyById(tunnelConf.id) ?: tunnelConf
|
||||
val existingState = currentTuns.getValueById(tunnelConf.id) ?: TunnelState()
|
||||
val newState = status ?: existingState.status
|
||||
if (newState == TunnelStatus.Down) {
|
||||
Timber.d("Removing tunnel ${tunnelConf.id} from activeTunnels as state is DOWN")
|
||||
cleanUpTunThread(tunnelConf)
|
||||
currentTuns - originalConf
|
||||
} else if (existingState.status == newState && stats == null) {
|
||||
Timber.d("Skipping redundant state update for ${tunnelConf.id}: $newState")
|
||||
currentTuns
|
||||
} else {
|
||||
val updated =
|
||||
existingState.copy(
|
||||
status = newState,
|
||||
statistics = stats ?: existingState.statistics,
|
||||
)
|
||||
currentTuns + (originalConf to updated)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun stopActiveTunnels() {
|
||||
activeTunnels.value.forEach { (config, state) ->
|
||||
if (state.status.isUpOrStarting()) {
|
||||
stopTunnel(config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun configureTunnelCallbacks(tunnelConf: TunnelConf) {
|
||||
Timber.d("Configuring TunnelConf instance: ${tunnelConf.hashCode()}")
|
||||
tunnelConf.setStateChangeCallback { state ->
|
||||
applicationScope.launch {
|
||||
Timber.d(
|
||||
"State change callback triggered for tunnel ${tunnelConf.id}: ${tunnelConf.tunName} with state $state at ${System.currentTimeMillis()}"
|
||||
)
|
||||
when (state) {
|
||||
is Tunnel.State -> updateTunnelStatus(tunnelConf, state.asTunnelState())
|
||||
is org.amnezia.awg.backend.Tunnel.State ->
|
||||
updateTunnelStatus(tunnelConf, state.asTunnelState())
|
||||
}
|
||||
handleServiceStateOnChange()
|
||||
}
|
||||
serviceManager.updateTunnelTile()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateTunnelStatistics(tunnel: TunnelConf) {
|
||||
val stats = getStatistics(tunnel)
|
||||
updateTunnelStatus(tunnel, null, stats)
|
||||
}
|
||||
|
||||
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
||||
if (activeTuns.exists(tunnelConf.id) || tunThreads.containsKey(tunnelConf.id)) return
|
||||
if (this@BaseTunnel is UserspaceTunnel) stopActiveTunnels()
|
||||
tunMutex.withLock {
|
||||
tunThreads[tunnelConf.id] = thread {
|
||||
runBlocking {
|
||||
try {
|
||||
Timber.d("Starting tunnel ${tunnelConf.id}...")
|
||||
startTunnelInner(tunnelConf)
|
||||
Timber.d("Started complete for tunnel ${tunnelConf.name}...")
|
||||
} catch (e: InterruptedException) {
|
||||
Timber.w(
|
||||
"Tunnel start has been interrupted as ${tunnelConf.name} failed to start"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startTunnelInner(tunnelConf: TunnelConf) {
|
||||
configureTunnelCallbacks(tunnelConf)
|
||||
Timber.d("Starting backend for tunnel ${tunnelConf.id}...")
|
||||
try {
|
||||
startBackend(tunnelConf)
|
||||
updateTunnelStatus(tunnelConf, TunnelStatus.Up)
|
||||
Timber.d("Started for tun ${tunnelConf.id}...")
|
||||
saveTunnelActiveState(tunnelConf, true)
|
||||
serviceManager.startTunnelForegroundService()
|
||||
} catch (e: BackendError) {
|
||||
Timber.e(e, "Failed to start backend for ${tunnelConf.name}")
|
||||
_errorEvents.emit(tunnelConf to e)
|
||||
updateTunnelStatus(tunnelConf, TunnelStatus.Down)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveTunnelActiveState(tunnelConf: TunnelConf, active: Boolean) {
|
||||
val tunnelCopy = tunnelConf.copyWithCallback(isActive = active)
|
||||
appDataRepository.tunnels.save(tunnelCopy)
|
||||
}
|
||||
|
||||
override suspend fun stopTunnel(tunnelConf: TunnelConf?, reason: TunnelStatus.StopReason) {
|
||||
if (tunnelConf == null) return stopActiveTunnels()
|
||||
tunMutex.withLock {
|
||||
if (activeTuns.isStarting(tunnelConf.id))
|
||||
return handleStuckStartingTunnelShutdown(tunnelConf)
|
||||
updateTunnelStatus(tunnelConf, TunnelStatus.Stopping(reason))
|
||||
stopTunnelInner(tunnelConf)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun stopTunnelInner(tunnelConf: TunnelConf) {
|
||||
try {
|
||||
val tunnel = activeTuns.findTunnel(tunnelConf.id) ?: return
|
||||
stopBackend(tunnel)
|
||||
saveTunnelActiveState(tunnelConf, false)
|
||||
removeActiveTunnel(tunnel)
|
||||
} catch (e: BackendError) {
|
||||
Timber.e(e, "Failed to stop tunnel ${tunnelConf.id}")
|
||||
_errorEvents.emit(tunnelConf to e)
|
||||
updateTunnelStatus(tunnelConf, TunnelStatus.Down)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleServiceStateOnChange() {
|
||||
if (activeTuns.value.isEmpty() && bouncingTunnelIds.isEmpty())
|
||||
serviceManager.stopTunnelForegroundService()
|
||||
}
|
||||
|
||||
private suspend fun handleStuckStartingTunnelShutdown(tunnel: TunnelConf) {
|
||||
Timber.d("Stuck in starting state so shutting down tunnel thread for tunnel ${tunnel.name}")
|
||||
try {
|
||||
tunThreads[tunnel.id]?.let {
|
||||
if (it.state != Thread.State.TERMINATED) {
|
||||
it.interrupt()
|
||||
} else {
|
||||
Timber.d("Thread already terminated")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to stop tunnel thread for ${tunnel.name}")
|
||||
} finally {
|
||||
updateTunnelStatus(tunnel, TunnelStatus.Down)
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanUpTunThread(tunnel: TunnelConf) {
|
||||
Timber.d("Removing thread for ${tunnel.name}")
|
||||
tunThreads -= tunnel.id
|
||||
}
|
||||
|
||||
private fun removeActiveTunnel(tunnelConf: TunnelConf) {
|
||||
activeTuns.update { current -> current.toMutableMap().apply { remove(tunnelConf) } }
|
||||
}
|
||||
|
||||
override suspend fun bounceTunnel(tunnelConf: TunnelConf, reason: TunnelStatus.StopReason) {
|
||||
bounceTunnelMutex.withLock {
|
||||
Timber.i(
|
||||
"Bounce tunnel ${tunnelConf.name} for reason: $reason, current bouncing: ${bouncingTunnelIds.size}"
|
||||
)
|
||||
bouncingTunnelIds[tunnelConf.id] = reason
|
||||
try {
|
||||
stopTunnel(tunnelConf, reason)
|
||||
delay(BOUNCE_DELAY)
|
||||
startTunnel(tunnelConf)
|
||||
} finally {
|
||||
bouncingTunnelIds.remove(tunnelConf.id)
|
||||
handleServiceStateOnChange()
|
||||
Timber.d(
|
||||
"Cleared bounce state for ${tunnelConf.name}, remaining: ${bouncingTunnelIds.size}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun runningTunnelNames(): Set<String> =
|
||||
activeTuns.value.keys.map { it.tunName }.toSet()
|
||||
|
||||
companion object {
|
||||
const val BOUNCE_DELAY = 300L
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
fun Map<TunnelConf, TunnelState>.allDown(): Boolean {
|
||||
return this.all { it.value.status.isDown() }
|
||||
}
|
||||
|
||||
fun Map<TunnelConf, TunnelState>.hasActive(): Boolean {
|
||||
return this.any { it.value.status.isUp() }
|
||||
}
|
||||
|
||||
fun Map<TunnelConf, TunnelState>.getValueById(id: Int): TunnelState? {
|
||||
val key = this.keys.find { it.id == id }
|
||||
return key?.let { this@getValueById[it] }
|
||||
}
|
||||
|
||||
fun Map<TunnelConf, TunnelState>.getKeyById(id: Int): TunnelConf? {
|
||||
return this.keys.find { it.id == id }
|
||||
}
|
||||
|
||||
fun Map<TunnelConf, TunnelState>.isUp(tunnelConf: TunnelConf): Boolean {
|
||||
return this.getValueById(tunnelConf.id)?.status?.isUp() ?: false
|
||||
}
|
||||
|
||||
fun MutableStateFlow<Map<TunnelConf, TunnelState>>.exists(id: Int): Boolean {
|
||||
return this.value.any { it.key.id == id }
|
||||
}
|
||||
|
||||
fun MutableStateFlow<Map<TunnelConf, TunnelState>>.isUp(id: Int): Boolean {
|
||||
return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Up }
|
||||
}
|
||||
|
||||
fun MutableStateFlow<Map<TunnelConf, TunnelState>>.isStarting(id: Int): Boolean {
|
||||
return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Starting }
|
||||
}
|
||||
|
||||
fun MutableStateFlow<Map<TunnelConf, TunnelState>>.findTunnel(id: Int): TunnelConf? {
|
||||
return this.value.keys.find { it.id == id }
|
||||
}
|
||||
|
||||
private val URL_PATTERN =
|
||||
Regex("""^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}:[0-9]{1,5}$""")
|
||||
|
||||
fun String.isUrl(): Boolean {
|
||||
return URL_PATTERN.matches(this)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.wireguard.android.backend.Backend
|
||||
import com.wireguard.android.backend.BackendException
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import timber.log.Timber
|
||||
|
||||
class KernelTunnel
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||
serviceManager: ServiceManager,
|
||||
appDataRepository: AppDataRepository,
|
||||
private val backend: Backend,
|
||||
) : BaseTunnel(applicationScope, appDataRepository, serviceManager) {
|
||||
|
||||
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
|
||||
return try {
|
||||
WireGuardStatistics(backend.getStatistics(tunnelConf))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun startBackend(tunnel: TunnelConf) {
|
||||
try {
|
||||
updateTunnelStatus(tunnel, TunnelStatus.Starting)
|
||||
backend.setState(tunnel, Tunnel.State.UP, tunnel.toWgConfig())
|
||||
} catch (e: BackendException) {
|
||||
throw e.toBackendError()
|
||||
}
|
||||
}
|
||||
|
||||
override fun stopBackend(tunnel: TunnelConf) {
|
||||
Timber.i("Stopping tunnel ${tunnel.id} kernel")
|
||||
try {
|
||||
backend.setState(tunnel, Tunnel.State.DOWN, tunnel.toWgConfig())
|
||||
} catch (e: BackendException) {
|
||||
throw e.toBackendError()
|
||||
}
|
||||
}
|
||||
|
||||
override fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
||||
Timber.w("Not yet implemented for kernel")
|
||||
}
|
||||
|
||||
override fun getBackendState(): BackendState {
|
||||
return BackendState.INACTIVE
|
||||
}
|
||||
|
||||
override suspend fun runningTunnelNames(): Set<String> {
|
||||
return backend.runningTunnelNames
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.di.Kernel
|
||||
import com.zaneschepke.wireguardautotunnel.di.Userspace
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
|
||||
class TunnelManager
|
||||
@Inject
|
||||
constructor(
|
||||
@Kernel private val kernelTunnel: TunnelProvider,
|
||||
@Userspace private val userspaceTunnel: TunnelProvider,
|
||||
private val appDataRepository: AppDataRepository,
|
||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : TunnelProvider {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val tunnelProviderFlow =
|
||||
appDataRepository.settings.flow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { settings ->
|
||||
MutableStateFlow(if (settings.isKernelEnabled) kernelTunnel else userspaceTunnel)
|
||||
}
|
||||
.stateIn(
|
||||
scope = applicationScope.plus(ioDispatcher),
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = userspaceTunnel,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val activeTunnels =
|
||||
appDataRepository.settings.flow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { settings ->
|
||||
if (settings.isKernelEnabled) {
|
||||
kernelTunnel.activeTunnels
|
||||
} else {
|
||||
userspaceTunnel.activeTunnels
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
scope = applicationScope.plus(ioDispatcher),
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = emptyMap(),
|
||||
)
|
||||
|
||||
override val errorEvents: SharedFlow<Pair<TunnelConf, BackendError>>
|
||||
get() = tunnelProviderFlow.value.errorEvents
|
||||
|
||||
override val bouncingTunnelIds: ConcurrentHashMap<Int, TunnelStatus.StopReason> =
|
||||
tunnelProviderFlow.value.bouncingTunnelIds
|
||||
|
||||
override fun hasVpnPermission(): Boolean {
|
||||
return userspaceTunnel.hasVpnPermission()
|
||||
}
|
||||
|
||||
override suspend fun updateTunnelStatistics(tunnel: TunnelConf) {
|
||||
tunnelProviderFlow.value.updateTunnelStatistics(tunnel)
|
||||
}
|
||||
|
||||
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
||||
tunnelProviderFlow.value.startTunnel(tunnelConf)
|
||||
}
|
||||
|
||||
override suspend fun stopTunnel(tunnelConf: TunnelConf?, reason: TunnelStatus.StopReason) {
|
||||
tunnelProviderFlow.value.stopTunnel(tunnelConf, reason)
|
||||
}
|
||||
|
||||
override suspend fun bounceTunnel(tunnelConf: TunnelConf, reason: TunnelStatus.StopReason) {
|
||||
tunnelProviderFlow.value.bounceTunnel(tunnelConf, reason)
|
||||
}
|
||||
|
||||
override fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
||||
tunnelProviderFlow.value.setBackendState(backendState, allowedIps)
|
||||
}
|
||||
|
||||
override fun getBackendState(): BackendState {
|
||||
return tunnelProviderFlow.value.getBackendState()
|
||||
}
|
||||
|
||||
override suspend fun runningTunnelNames(): Set<String> {
|
||||
return tunnelProviderFlow.value.runningTunnelNames()
|
||||
}
|
||||
|
||||
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
|
||||
return tunnelProviderFlow.value.getStatistics(tunnelConf)
|
||||
}
|
||||
|
||||
fun restorePreviousState() =
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
val settings = appDataRepository.settings.get()
|
||||
if (settings.isRestoreOnBootEnabled) {
|
||||
val previouslyActiveTuns = appDataRepository.tunnels.getActive()
|
||||
val tunsToStart =
|
||||
previouslyActiveTuns.filterNot { tun ->
|
||||
activeTunnels.value.any { tun.id == it.key.id }
|
||||
}
|
||||
if (settings.isKernelEnabled) {
|
||||
return@launch tunsToStart.forEach { startTunnel(it) }
|
||||
} else {
|
||||
tunsToStart.firstOrNull()?.let { startTunnel(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface TunnelProvider {
|
||||
/** Starts the specified tunnel configuration. */
|
||||
suspend fun startTunnel(tunnelConf: TunnelConf)
|
||||
|
||||
/**
|
||||
* Stops the specified tunnel, or all tunnels if none is provided.
|
||||
*
|
||||
* @param tunnelConf The tunnel to stop, or null to stop all active tunnels.
|
||||
* @param reason The reason for stopping, defaults to USER for manual stops. Callers should
|
||||
* override with specific reasons (e.g., PING, CONFIG_CHANGED) when applicable.
|
||||
*/
|
||||
suspend fun stopTunnel(
|
||||
tunnelConf: TunnelConf? = null,
|
||||
reason: TunnelStatus.StopReason = TunnelStatus.StopReason.USER,
|
||||
)
|
||||
|
||||
/**
|
||||
* Bounces (stops and restarts) the specified tunnel.
|
||||
*
|
||||
* @param tunnelConf The tunnel to bounce.
|
||||
* @param reason The reason for bouncing, defaults to USER for manual actions. Callers should
|
||||
* override with specific reasons (e.g., PING, CONFIG_CHANGED) when applicable.
|
||||
*/
|
||||
suspend fun bounceTunnel(
|
||||
tunnelConf: TunnelConf,
|
||||
reason: TunnelStatus.StopReason = TunnelStatus.StopReason.USER,
|
||||
)
|
||||
|
||||
fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
|
||||
|
||||
fun getBackendState(): BackendState
|
||||
|
||||
suspend fun runningTunnelNames(): Set<String>
|
||||
|
||||
fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics?
|
||||
|
||||
val activeTunnels: StateFlow<Map<TunnelConf, TunnelState>>
|
||||
|
||||
val errorEvents: SharedFlow<Pair<TunnelConf, BackendError>>
|
||||
|
||||
val bouncingTunnelIds: ConcurrentHashMap<Int, TunnelStatus.StopReason>
|
||||
|
||||
fun hasVpnPermission(): Boolean
|
||||
|
||||
suspend fun updateTunnelStatistics(tunnel: TunnelConf)
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
|
||||
import javax.inject.Inject
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.amnezia.awg.backend.Backend
|
||||
import org.amnezia.awg.backend.BackendException
|
||||
import org.amnezia.awg.backend.Tunnel
|
||||
import org.amnezia.awg.config.Config
|
||||
import timber.log.Timber
|
||||
|
||||
class UserspaceTunnel
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||
val serviceManager: ServiceManager,
|
||||
val appDataRepository: AppDataRepository,
|
||||
private val backend: Backend,
|
||||
) : BaseTunnel(applicationScope, appDataRepository, serviceManager) {
|
||||
|
||||
private var previousBackendState: Pair<BackendState, Boolean>? = null
|
||||
|
||||
override suspend fun startBackend(tunnel: TunnelConf) {
|
||||
try {
|
||||
updateTunnelStatus(tunnel, TunnelStatus.Starting)
|
||||
val amConfig = tunnel.toAmConfig()
|
||||
handleVpnKillSwitchWithDomainEndpoints(amConfig)
|
||||
backend.setState(tunnel, Tunnel.State.UP, amConfig)
|
||||
} catch (e: BackendException) {
|
||||
Timber.e(e, "Failed to start up backend for tunnel ${tunnel.name}")
|
||||
throw e.toBackendError()
|
||||
}
|
||||
}
|
||||
|
||||
override fun stopBackend(tunnel: TunnelConf) {
|
||||
Timber.i("Stopping tunnel ${tunnel.name} userspace")
|
||||
try {
|
||||
backend.setState(tunnel, Tunnel.State.DOWN, tunnel.toAmConfig())
|
||||
} catch (e: BackendException) {
|
||||
Timber.e(e, "Failed to stop tunnel ${tunnel.id}")
|
||||
throw e.toBackendError()
|
||||
} finally {
|
||||
handlePreviouslyEnabledVpnKillSwitch()
|
||||
}
|
||||
}
|
||||
|
||||
// stop vpn kill switch if we need to resolve DNS for peer endpoints
|
||||
private suspend fun handleVpnKillSwitchWithDomainEndpoints(config: Config) {
|
||||
if (
|
||||
config.peers.any { it.endpoint.getOrNull()?.toString()?.isUrl() == true } &&
|
||||
backend.backendState.asBackendState() == BackendState.KILL_SWITCH_ACTIVE
|
||||
) {
|
||||
val bypassLan = appDataRepository.settings.get().isLanOnKillSwitchEnabled
|
||||
previousBackendState = Pair(BackendState.KILL_SWITCH_ACTIVE, bypassLan)
|
||||
setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
// restore vpn kill switch if needed
|
||||
private fun handlePreviouslyEnabledVpnKillSwitch() {
|
||||
// let auto tunnel handle this if it is active
|
||||
if (serviceManager.autoTunnelService.value == null) {
|
||||
previousBackendState?.let { (state, lanEnabled) ->
|
||||
Timber.d("Restoring kill switch configuration")
|
||||
val lan = if (lanEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS else emptyList()
|
||||
backend.setBackendState(state.asAmBackendState(), lan)
|
||||
}
|
||||
}
|
||||
previousBackendState = null
|
||||
}
|
||||
|
||||
override fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
||||
Timber.d("Setting backend state: $backendState with allowedIps: $allowedIps")
|
||||
try {
|
||||
backend.setBackendState(backendState.asAmBackendState(), allowedIps)
|
||||
} catch (e: BackendException) {
|
||||
throw e.toBackendError()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getBackendState(): BackendState {
|
||||
return backend.backendState.asBackendState()
|
||||
}
|
||||
|
||||
override suspend fun runningTunnelNames(): Set<String> {
|
||||
return backend.runningTunnelNames
|
||||
}
|
||||
|
||||
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
|
||||
return try {
|
||||
AmneziaStatistics(backend.getStatistics(tunnelConf))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to get stats for ${tunnelConf.tunName}")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.worker
|
||||
|
||||
import android.content.Context
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
@HiltWorker
|
||||
class ServiceWorker
|
||||
@AssistedInject
|
||||
constructor(
|
||||
@Assisted private val context: Context,
|
||||
@Assisted private val params: WorkerParameters,
|
||||
private val serviceManager: ServiceManager,
|
||||
private val appDataRepository: AppDataRepository,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
private val tunnelManager: TunnelManager,
|
||||
) : CoroutineWorker(context, params) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "service_worker"
|
||||
|
||||
fun stop(context: Context) {
|
||||
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
|
||||
}
|
||||
|
||||
fun start(context: Context) {
|
||||
val periodicWorkRequest =
|
||||
PeriodicWorkRequestBuilder<ServiceWorker>(
|
||||
repeatInterval = 15,
|
||||
repeatIntervalTimeUnit = TimeUnit.MINUTES,
|
||||
)
|
||||
.build()
|
||||
WorkManager.getInstance(context)
|
||||
.enqueueUniquePeriodicWork(
|
||||
TAG,
|
||||
ExistingPeriodicWorkPolicy.KEEP,
|
||||
periodicWorkRequest,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun doWork(): Result =
|
||||
withContext(ioDispatcher) {
|
||||
Timber.i("Service worker started")
|
||||
with(appDataRepository.settings.get()) {
|
||||
if (isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null)
|
||||
return@with serviceManager.startAutoTunnel()
|
||||
if (tunnelManager.activeTunnels.value.isEmpty())
|
||||
tunnelManager.restorePreviousState()
|
||||
}
|
||||
Result.success()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data
|
||||
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.DeleteColumn
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
|
||||
@Database(
|
||||
entities = [Settings::class, TunnelConfig::class],
|
||||
version = 16,
|
||||
autoMigrations =
|
||||
[
|
||||
AutoMigration(from = 1, to = 2),
|
||||
AutoMigration(from = 2, to = 3),
|
||||
AutoMigration(from = 3, to = 4),
|
||||
AutoMigration(from = 4, to = 5),
|
||||
AutoMigration(from = 5, to = 6),
|
||||
AutoMigration(from = 6, to = 7, spec = RemoveLegacySettingColumnsMigration::class),
|
||||
AutoMigration(7, 8),
|
||||
AutoMigration(8, 9),
|
||||
AutoMigration(9, 10),
|
||||
AutoMigration(from = 10, to = 11, spec = RemoveTunnelPauseMigration::class),
|
||||
AutoMigration(from = 11, to = 12),
|
||||
AutoMigration(from = 12, to = 13),
|
||||
AutoMigration(from = 13, to = 14),
|
||||
AutoMigration(from = 14, to = 15),
|
||||
AutoMigration(from = 15, to = 16),
|
||||
],
|
||||
exportSchema = true,
|
||||
)
|
||||
@TypeConverters(DatabaseListConverters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun settingDao(): SettingsDao
|
||||
|
||||
abstract fun tunnelConfigDoa(): TunnelConfigDao
|
||||
}
|
||||
|
||||
@DeleteColumn(tableName = "Settings", columnName = "default_tunnel")
|
||||
@DeleteColumn(tableName = "Settings", columnName = "is_battery_saver_enabled")
|
||||
class RemoveLegacySettingColumnsMigration : AutoMigrationSpec
|
||||
|
||||
@DeleteColumn(tableName = "Settings", columnName = "is_auto_tunnel_paused")
|
||||
class RemoveTunnelPauseMigration : AutoMigrationSpec
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
class DataStoreManager(
|
||||
private val context: Context,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
companion object {
|
||||
val locationDisclosureShown = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
||||
val batteryDisableShown = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
|
||||
val pinLockEnabled = booleanPreferencesKey("PIN_LOCK_ENABLED")
|
||||
val expandedTunnelIds = stringPreferencesKey("EXPANDED_TUNNEL_IDS")
|
||||
val isLocalLogsEnabled = booleanPreferencesKey("LOCAL_LOGS_ENABLED")
|
||||
val locale = stringPreferencesKey("LOCALE")
|
||||
val theme = stringPreferencesKey("THEME")
|
||||
val isRemoteControlEnabled = booleanPreferencesKey("IS_REMOTE_CONTROL_ENABLED")
|
||||
val remoteKey = stringPreferencesKey("REMOTE_KEY")
|
||||
}
|
||||
|
||||
// preferences
|
||||
private val preferencesKey = "preferences"
|
||||
private val Context.dataStore by preferencesDataStore(name = preferencesKey)
|
||||
|
||||
suspend fun init() {
|
||||
withContext(ioDispatcher) {
|
||||
try {
|
||||
context.dataStore.data.first()
|
||||
} catch (e: IOException) {
|
||||
Timber.Forest.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) {
|
||||
withContext(ioDispatcher) {
|
||||
try {
|
||||
context.dataStore.edit { it[key] = value }
|
||||
} catch (e: IOException) {
|
||||
Timber.Forest.e(e)
|
||||
} catch (e: Exception) {
|
||||
Timber.Forest.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> removeFromDataStore(key: Preferences.Key<T>) {
|
||||
withContext(ioDispatcher) {
|
||||
try {
|
||||
context.dataStore.edit { it.remove(key) }
|
||||
} catch (e: IOException) {
|
||||
Timber.Forest.e(e)
|
||||
} catch (e: Exception) {
|
||||
Timber.Forest.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
|
||||
|
||||
suspend fun <T> getFromStore(key: Preferences.Key<T>): T? {
|
||||
return withContext(ioDispatcher) {
|
||||
try {
|
||||
context.dataStore.data.map { it[key] }.first()
|
||||
} catch (e: IOException) {
|
||||
Timber.Forest.e(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> getFromStoreBlocking(key: Preferences.Key<T>) = runBlocking {
|
||||
context.dataStore.data.map { it[key] }.first()
|
||||
}
|
||||
|
||||
val preferencesFlow: Flow<Preferences?> = context.dataStore.data.flowOn(ioDispatcher)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data
|
||||
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import timber.log.Timber
|
||||
|
||||
class DatabaseCallback : RoomDatabase.Callback() {
|
||||
override fun onCreate(db: SupportSQLiteDatabase) =
|
||||
db.run {
|
||||
beginTransaction()
|
||||
try {
|
||||
execSQL(Queries.createDefaultSettings())
|
||||
Timber.i("Bootstrapping settings data")
|
||||
setTransactionSuccessful()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
} finally {
|
||||
endTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class DatabaseListConverters {
|
||||
@TypeConverter
|
||||
fun listToString(value: MutableList<String>): String {
|
||||
return Json.encodeToString(value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun stringToList(value: String): MutableList<String> {
|
||||
if (value.isBlank() || value.isEmpty()) return mutableListOf()
|
||||
return try {
|
||||
Json.decodeFromString<MutableList<String>>(value)
|
||||
} catch (e: Exception) {
|
||||
val list = value.split(",").toMutableList()
|
||||
val json = listToString(list)
|
||||
Json.decodeFromString<MutableList<String>>(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data
|
||||
|
||||
object Queries {
|
||||
fun createDefaultSettings(): String {
|
||||
return """
|
||||
INSERT INTO Settings (is_tunnel_enabled,
|
||||
is_tunnel_on_mobile_data_enabled,
|
||||
trusted_network_ssids,
|
||||
is_always_on_vpn_enabled,
|
||||
is_tunnel_on_ethernet_enabled,
|
||||
is_shortcuts_enabled,
|
||||
is_tunnel_on_wifi_enabled,
|
||||
is_kernel_enabled,
|
||||
is_restore_on_boot_enabled,
|
||||
is_multi_tunnel_enabled)
|
||||
VALUES
|
||||
('false',
|
||||
'false',
|
||||
'',
|
||||
'false',
|
||||
'false',
|
||||
'false',
|
||||
'false',
|
||||
'false',
|
||||
'false',
|
||||
'false')
|
||||
"""
|
||||
.trimIndent()
|
||||
}
|
||||
|
||||
fun createTunnelConfig(): String {
|
||||
return """
|
||||
INSERT INTO TunnelConfig (name, wg_quick) VALUES ('test', 'test')
|
||||
"""
|
||||
.trimIndent()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface SettingsDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: Settings)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<Settings>)
|
||||
|
||||
@Query("SELECT * FROM settings WHERE id=:id") suspend fun getById(id: Long): Settings?
|
||||
|
||||
@Query("SELECT * FROM settings") suspend fun getAll(): List<Settings>
|
||||
|
||||
@Query("SELECT * FROM settings LIMIT 1") fun getSettingsFlow(): Flow<Settings>
|
||||
|
||||
@Query("SELECT * FROM settings") fun getAllFlow(): Flow<MutableList<Settings>>
|
||||
|
||||
@Delete suspend fun delete(t: Settings)
|
||||
|
||||
@Query("SELECT COUNT('id') FROM settings") suspend fun count(): Long
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface TunnelConfigDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: TunnelConfig)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: TunnelConfigs)
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig WHERE id=:id") suspend fun getById(id: Long): TunnelConfig?
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig WHERE name=:name")
|
||||
suspend fun getByName(name: String): TunnelConfig?
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig WHERE is_Active=1") suspend fun getActive(): TunnelConfigs
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig") suspend fun getAll(): TunnelConfigs
|
||||
|
||||
@Delete suspend fun delete(t: TunnelConfig)
|
||||
|
||||
@Query("SELECT COUNT('id') FROM TunnelConfig") suspend fun count(): Long
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig WHERE tunnel_networks LIKE '%' || :name || '%'")
|
||||
suspend fun findByTunnelNetworkName(name: String): TunnelConfigs
|
||||
|
||||
@Query("UPDATE TunnelConfig SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1")
|
||||
suspend fun resetPrimaryTunnel()
|
||||
|
||||
@Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1")
|
||||
suspend fun resetMobileDataTunnel()
|
||||
|
||||
@Query("UPDATE TunnelConfig SET is_ethernet_tunnel = 0 WHERE is_ethernet_tunnel =1")
|
||||
suspend fun resetEthernetTunnel()
|
||||
|
||||
@Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1")
|
||||
suspend fun findByPrimary(): TunnelConfigs
|
||||
|
||||
@Query("SELECT * FROM TUNNELCONFIG WHERE is_mobile_data_tunnel=1")
|
||||
suspend fun findByMobileDataTunnel(): TunnelConfigs
|
||||
|
||||
@Query("SELECT * FROM tunnelconfig") fun getAllFlow(): Flow<MutableList<TunnelConfig>>
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Asset(
|
||||
val name: String,
|
||||
@SerialName("browser_download_url") val browserDownloadUrl: String,
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.model
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||
|
||||
data class GeneralState(
|
||||
val isLocationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
|
||||
val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
|
||||
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
|
||||
val expandedTunnelIds: List<Int> = emptyList(),
|
||||
val isLocalLogsEnabled: Boolean = IS_LOGS_ENABLED_DEFAULT,
|
||||
val isRemoteControlEnabled: Boolean = IS_REMOTE_CONTROL_ENABLED,
|
||||
val remoteKey: String? = null,
|
||||
val locale: String? = null,
|
||||
val theme: Theme = Theme.AUTOMATIC,
|
||||
) {
|
||||
|
||||
fun toAppState(): AppState =
|
||||
AppState(
|
||||
isLocationDisclosureShown,
|
||||
isBatteryOptimizationDisableShown,
|
||||
isPinLockEnabled,
|
||||
expandedTunnelIds,
|
||||
isLocalLogsEnabled,
|
||||
isRemoteControlEnabled,
|
||||
remoteKey,
|
||||
locale,
|
||||
theme,
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun from(appState: AppState): GeneralState {
|
||||
return with(appState) {
|
||||
GeneralState(
|
||||
isLocationDisclosureShown,
|
||||
isBatteryOptimizationDisableShown,
|
||||
isPinLockEnabled,
|
||||
expandedTunnelIds,
|
||||
isLocalLogsEnabled,
|
||||
isRemoteControlEnabled,
|
||||
remoteKey,
|
||||
locale,
|
||||
theme,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
|
||||
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
|
||||
const val PIN_LOCK_ENABLED_DEFAULT = false
|
||||
const val IS_LOGS_ENABLED_DEFAULT = false
|
||||
const val IS_REMOTE_CONTROL_ENABLED = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.model
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppUpdate
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GitHubRelease(
|
||||
@SerialName("tag_name") val tagName: String,
|
||||
val name: String?,
|
||||
val body: String?,
|
||||
val assets: List<Asset>,
|
||||
) {
|
||||
fun toAppUpdate(): AppUpdate {
|
||||
val apkAsset = assets.firstOrNull { it.name.endsWith(".apk") }
|
||||
return AppUpdate(
|
||||
version = tagName.removePrefix("v"),
|
||||
title = name ?: "Update $tagName",
|
||||
releaseNotes = body ?: "No release notes provided",
|
||||
apkUrl = apkAsset?.browserDownloadUrl,
|
||||
apkFileName = apkAsset?.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
||||
|
||||
@Entity
|
||||
data class Settings(
|
||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||
@ColumnInfo(name = "is_tunnel_enabled") val isAutoTunnelEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled")
|
||||
val isTunnelOnMobileDataEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "trusted_network_ssids")
|
||||
val trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
||||
@ColumnInfo(name = "is_always_on_vpn_enabled") val isAlwaysOnVpnEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled")
|
||||
val isTunnelOnEthernetEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_shortcuts_enabled", defaultValue = "false")
|
||||
val isShortcutsEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_tunnel_on_wifi_enabled", defaultValue = "false")
|
||||
val isTunnelOnWifiEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_kernel_enabled", defaultValue = "false")
|
||||
val isKernelEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_restore_on_boot_enabled", defaultValue = "false")
|
||||
val isRestoreOnBootEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_multi_tunnel_enabled", defaultValue = "false")
|
||||
val isMultiTunnelEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_ping_enabled", defaultValue = "false")
|
||||
val isPingEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_amnezia_enabled", defaultValue = "false")
|
||||
val isAmneziaEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_wildcards_enabled", defaultValue = "false")
|
||||
val isWildcardsEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_wifi_by_shell_enabled", defaultValue = "false")
|
||||
val isWifiNameByShellEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_stop_on_no_internet_enabled", defaultValue = "false")
|
||||
val isStopOnNoInternetEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_vpn_kill_switch_enabled", defaultValue = "false")
|
||||
val isVpnKillSwitchEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_kernel_kill_switch_enabled", defaultValue = "false")
|
||||
val isKernelKillSwitchEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_lan_on_kill_switch_enabled", defaultValue = "false")
|
||||
val isLanOnKillSwitchEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "debounce_delay_seconds", defaultValue = "3")
|
||||
val debounceDelaySeconds: Int = 3,
|
||||
@ColumnInfo(name = "is_disable_kill_switch_on_trusted_enabled", defaultValue = "false")
|
||||
val isDisableKillSwitchOnTrustedEnabled: Boolean = false,
|
||||
) {
|
||||
|
||||
fun toAppSettings(): AppSettings {
|
||||
return AppSettings(
|
||||
id,
|
||||
isAutoTunnelEnabled,
|
||||
isTunnelOnMobileDataEnabled,
|
||||
trustedNetworkSSIDs,
|
||||
isAlwaysOnVpnEnabled,
|
||||
isTunnelOnEthernetEnabled,
|
||||
isShortcutsEnabled,
|
||||
isTunnelOnWifiEnabled,
|
||||
isKernelEnabled,
|
||||
isRestoreOnBootEnabled,
|
||||
isMultiTunnelEnabled,
|
||||
isPingEnabled,
|
||||
isAmneziaEnabled,
|
||||
isWildcardsEnabled,
|
||||
isWifiNameByShellEnabled,
|
||||
isStopOnNoInternetEnabled,
|
||||
isVpnKillSwitchEnabled,
|
||||
isKernelKillSwitchEnabled,
|
||||
isLanOnKillSwitchEnabled,
|
||||
debounceDelaySeconds,
|
||||
isDisableKillSwitchOnTrustedEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(appSettings: AppSettings): Settings {
|
||||
return with(appSettings) {
|
||||
Settings(
|
||||
id,
|
||||
isAutoTunnelEnabled,
|
||||
isTunnelOnMobileDataEnabled,
|
||||
trustedNetworkSSIDs.toMutableList(),
|
||||
isAlwaysOnVpnEnabled,
|
||||
isTunnelOnEthernetEnabled,
|
||||
isShortcutsEnabled,
|
||||
isTunnelOnWifiEnabled,
|
||||
isKernelEnabled,
|
||||
isRestoreOnBootEnabled,
|
||||
isMultiTunnelEnabled,
|
||||
isPingEnabled,
|
||||
isAmneziaEnabled,
|
||||
isWildcardsEnabled,
|
||||
isWifiNameByShellEnabled,
|
||||
isStopOnNoInternetEnabled,
|
||||
isVpnKillSwitchEnabled,
|
||||
isKernelKillSwitchEnabled,
|
||||
isLanOnKillSwitchEnabled,
|
||||
debounceDelaySeconds,
|
||||
isDisableKillSwitchOnTrustedEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
|
||||
@Entity(indices = [Index(value = ["name"], unique = true)])
|
||||
data class TunnelConfig(
|
||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||
@ColumnInfo(name = "name") val name: String,
|
||||
@ColumnInfo(name = "wg_quick") val wgQuick: String,
|
||||
@ColumnInfo(name = "tunnel_networks", defaultValue = "")
|
||||
val tunnelNetworks: MutableList<String> = mutableListOf(),
|
||||
@ColumnInfo(name = "is_mobile_data_tunnel", defaultValue = "false")
|
||||
val isMobileDataTunnel: Boolean = false,
|
||||
@ColumnInfo(name = "is_primary_tunnel", defaultValue = "false")
|
||||
val isPrimaryTunnel: Boolean = false,
|
||||
@ColumnInfo(name = "am_quick", defaultValue = "") val amQuick: String = AM_QUICK_DEFAULT,
|
||||
@ColumnInfo(name = "is_Active", defaultValue = "false") val isActive: Boolean = false,
|
||||
@ColumnInfo(name = "is_ping_enabled", defaultValue = "false")
|
||||
val isPingEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "ping_interval", defaultValue = "null") val pingInterval: Long? = null,
|
||||
@ColumnInfo(name = "ping_cooldown", defaultValue = "null") val pingCooldown: Long? = null,
|
||||
@ColumnInfo(name = "ping_ip", defaultValue = "null") var pingIp: String? = null,
|
||||
@ColumnInfo(name = "is_ethernet_tunnel", defaultValue = "false")
|
||||
var isEthernetTunnel: Boolean = false,
|
||||
@ColumnInfo(name = "is_ipv4_preferred", defaultValue = "true")
|
||||
var isIpv4Preferred: Boolean = true,
|
||||
) {
|
||||
|
||||
fun toTunnel(): TunnelConf {
|
||||
return TunnelConf(
|
||||
id,
|
||||
name,
|
||||
wgQuick,
|
||||
tunnelNetworks,
|
||||
isMobileDataTunnel,
|
||||
isPrimaryTunnel,
|
||||
amQuick,
|
||||
isActive,
|
||||
isPingEnabled,
|
||||
pingInterval,
|
||||
pingCooldown,
|
||||
pingIp,
|
||||
isEthernetTunnel,
|
||||
isIpv4Preferred,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val AM_QUICK_DEFAULT = ""
|
||||
|
||||
fun from(tunnelConf: TunnelConf): TunnelConfig {
|
||||
return with(tunnelConf) {
|
||||
return TunnelConfig(
|
||||
id,
|
||||
tunName,
|
||||
wgQuick,
|
||||
tunnelNetworks.toMutableList(),
|
||||
isMobileDataTunnel,
|
||||
isPrimaryTunnel,
|
||||
amQuick,
|
||||
isActive,
|
||||
isPingEnabled,
|
||||
pingInterval,
|
||||
pingCooldown,
|
||||
pingIp,
|
||||
isEthernetTunnel,
|
||||
isIpv4Preferred,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.network
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.GitHubRelease
|
||||
|
||||
interface GitHubApi {
|
||||
suspend fun getLatestRelease(owner: String, repo: String): Result<GitHubRelease>
|
||||
|
||||
suspend fun getNightlyRelease(owner: String, repo: String): Result<GitHubRelease>
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.network
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import io.ktor.client.plugins.HttpTimeout
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
object KtorClient {
|
||||
fun create(): HttpClient {
|
||||
return HttpClient(OkHttp) {
|
||||
install(ContentNegotiation) {
|
||||
json(
|
||||
Json {
|
||||
ignoreUnknownKeys = true
|
||||
isLenient = true
|
||||
}
|
||||
)
|
||||
}
|
||||
install(HttpTimeout) {
|
||||
requestTimeoutMillis = 15000
|
||||
connectTimeoutMillis = 15000
|
||||
socketTimeoutMillis = 15000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.network
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.GitHubRelease
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.plugins.ClientRequestException
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.http.HttpStatusCode
|
||||
|
||||
class KtorGitHubApi(private val client: HttpClient) : GitHubApi {
|
||||
override suspend fun getLatestRelease(owner: String, repo: String): Result<GitHubRelease> {
|
||||
return try {
|
||||
val response: GitHubRelease =
|
||||
client.get("https://api.github.com/repos/$owner/$repo/releases/latest").body()
|
||||
Result.success(response)
|
||||
} catch (e: ClientRequestException) {
|
||||
when (e.response.status) {
|
||||
HttpStatusCode.Forbidden -> Result.failure(Exception("Rate limit exceeded"))
|
||||
HttpStatusCode.NotFound ->
|
||||
Result.failure(Exception("Repository or release not found"))
|
||||
else -> Result.failure(e)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getNightlyRelease(owner: String, repo: String): Result<GitHubRelease> {
|
||||
return try {
|
||||
// Fetch all releases
|
||||
val releases: List<GitHubRelease> =
|
||||
client.get("https://api.github.com/repos/$owner/$repo/releases").body()
|
||||
|
||||
// Find the first release with "nightly" in the tag_name (case-insensitive)
|
||||
val nightlyRelease =
|
||||
releases.firstOrNull { release ->
|
||||
release.tagName.contains("nightly", ignoreCase = true)
|
||||
}
|
||||
|
||||
if (nightlyRelease != null) {
|
||||
Result.success(nightlyRelease)
|
||||
} else {
|
||||
Result.failure(Exception("No release with 'nightly' tag found"))
|
||||
}
|
||||
} catch (e: ClientRequestException) {
|
||||
when (e.response.status) {
|
||||
HttpStatusCode.Forbidden -> Result.failure(Exception("Rate limit exceeded"))
|
||||
HttpStatusCode.NotFound ->
|
||||
Result.failure(Exception("Repository or release not found"))
|
||||
else -> Result.failure(e)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class AppDataRoomRepository
|
||||
@Inject
|
||||
constructor(
|
||||
override val settings: AppSettingRepository,
|
||||
override val tunnels: TunnelRepository,
|
||||
override val appState: AppStateRepository,
|
||||
) : AppDataRepository {
|
||||
|
||||
override suspend fun getPrimaryOrFirstTunnel(): TunnelConf? {
|
||||
return tunnels.findPrimary().firstOrNull() ?: tunnels.getAll().firstOrNull()
|
||||
}
|
||||
|
||||
override suspend fun getStartTunnelConfig(): TunnelConf? {
|
||||
tunnels.getActive().let {
|
||||
if (it.isNotEmpty()) return it.first()
|
||||
return getPrimaryOrFirstTunnel()
|
||||
}
|
||||
}
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import timber.log.Timber
|
||||
|
||||
class DataStoreAppStateRepository(private val dataStoreManager: DataStoreManager) :
|
||||
AppStateRepository {
|
||||
override suspend fun isLocationDisclosureShown(): Boolean {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.locationDisclosureShown)
|
||||
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT
|
||||
}
|
||||
|
||||
override suspend fun setLocationDisclosureShown(shown: Boolean) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.locationDisclosureShown, shown)
|
||||
}
|
||||
|
||||
override suspend fun isPinLockEnabled(): Boolean {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.pinLockEnabled)
|
||||
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT
|
||||
}
|
||||
|
||||
override suspend fun setPinLockEnabled(enabled: Boolean) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.pinLockEnabled, enabled)
|
||||
}
|
||||
|
||||
override suspend fun isBatteryOptimizationDisableShown(): Boolean {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.batteryDisableShown)
|
||||
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
|
||||
}
|
||||
|
||||
override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.batteryDisableShown, shown)
|
||||
}
|
||||
|
||||
override suspend fun setTunnelExpanded(id: Int) {
|
||||
val ids =
|
||||
dataStoreManager
|
||||
.getFromStore(DataStoreManager.expandedTunnelIds)
|
||||
?.split(",")
|
||||
?.mapNotNull { it.toIntOrNull() } ?: emptyList()
|
||||
|
||||
if (ids.contains(id)) return
|
||||
|
||||
val updatedList = ids.toMutableList().apply { add(id) }
|
||||
dataStoreManager.saveToDataStore(
|
||||
DataStoreManager.expandedTunnelIds,
|
||||
updatedList.joinToString(","),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun removeTunnelExpanded(id: Int) {
|
||||
val ids =
|
||||
dataStoreManager
|
||||
.getFromStore(DataStoreManager.expandedTunnelIds)
|
||||
?.split(",")
|
||||
?.mapNotNull { it.toIntOrNull() } ?: emptyList()
|
||||
|
||||
if (ids.isEmpty() || !ids.contains(id)) return
|
||||
|
||||
val updatedList = ids.toMutableList().apply { remove(id) }
|
||||
dataStoreManager.saveToDataStore(
|
||||
DataStoreManager.expandedTunnelIds,
|
||||
updatedList.joinToString(","),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun setTheme(theme: Theme) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.theme, theme.name)
|
||||
}
|
||||
|
||||
override suspend fun getTheme(): Theme {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.theme)?.let {
|
||||
try {
|
||||
Theme.valueOf(it)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
Theme.AUTOMATIC
|
||||
}
|
||||
} ?: Theme.AUTOMATIC
|
||||
}
|
||||
|
||||
override suspend fun isLocalLogsEnabled(): Boolean {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.isLocalLogsEnabled)
|
||||
?: GeneralState.IS_LOGS_ENABLED_DEFAULT
|
||||
}
|
||||
|
||||
override suspend fun setLocalLogsEnabled(enabled: Boolean) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.isLocalLogsEnabled, enabled)
|
||||
}
|
||||
|
||||
override suspend fun setLocale(localeTag: String) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.locale, localeTag)
|
||||
}
|
||||
|
||||
override suspend fun getLocale(): String? {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.locale)
|
||||
}
|
||||
|
||||
override suspend fun setIsRemoteControlEnabled(enabled: Boolean) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.isRemoteControlEnabled, enabled)
|
||||
}
|
||||
|
||||
override suspend fun isRemoteControlEnabled(): Boolean {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.isRemoteControlEnabled)
|
||||
?: GeneralState.IS_REMOTE_CONTROL_ENABLED
|
||||
}
|
||||
|
||||
override suspend fun setRemoteKey(key: String) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.remoteKey, key)
|
||||
}
|
||||
|
||||
override suspend fun getRemoteKey(): String? {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.remoteKey)
|
||||
}
|
||||
|
||||
override val flow: Flow<AppState> =
|
||||
dataStoreManager.preferencesFlow
|
||||
.map { prefs ->
|
||||
prefs?.let { pref ->
|
||||
try {
|
||||
GeneralState(
|
||||
isLocationDisclosureShown =
|
||||
pref[DataStoreManager.locationDisclosureShown]
|
||||
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT,
|
||||
isBatteryOptimizationDisableShown =
|
||||
pref[DataStoreManager.batteryDisableShown]
|
||||
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
|
||||
isPinLockEnabled =
|
||||
pref[DataStoreManager.pinLockEnabled]
|
||||
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT,
|
||||
expandedTunnelIds =
|
||||
pref[DataStoreManager.expandedTunnelIds]?.split(",")?.mapNotNull {
|
||||
it.toIntOrNull()
|
||||
} ?: emptyList(),
|
||||
isLocalLogsEnabled =
|
||||
pref[DataStoreManager.isLocalLogsEnabled]
|
||||
?: GeneralState.IS_LOGS_ENABLED_DEFAULT,
|
||||
isRemoteControlEnabled =
|
||||
pref[DataStoreManager.isRemoteControlEnabled]
|
||||
?: GeneralState.IS_REMOTE_CONTROL_ENABLED,
|
||||
remoteKey = pref[DataStoreManager.remoteKey],
|
||||
locale = pref[DataStoreManager.locale],
|
||||
theme = getTheme(),
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Timber.e(e)
|
||||
GeneralState()
|
||||
}
|
||||
} ?: GeneralState()
|
||||
}
|
||||
.map { it.toAppState() }
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import android.content.Context
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppUpdate
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.UpdateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.http.contentLength
|
||||
import io.ktor.utils.io.ByteReadChannel
|
||||
import io.ktor.utils.io.readAvailable
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
class GitHubUpdateRepository(
|
||||
private val gitHubApi: GitHubApi,
|
||||
private val httpClient: HttpClient,
|
||||
private val githubOwner: String,
|
||||
private val githubRepo: String,
|
||||
private val context: Context,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : UpdateRepository {
|
||||
override suspend fun checkForUpdate(currentVersion: String): Result<AppUpdate?> =
|
||||
withContext(ioDispatcher) {
|
||||
Timber.i("Checking for update")
|
||||
val release =
|
||||
if (BuildConfig.VERSION_NAME.contains("nightly")) {
|
||||
gitHubApi.getNightlyRelease(githubOwner, githubRepo)
|
||||
} else {
|
||||
gitHubApi.getLatestRelease(githubOwner, githubRepo)
|
||||
}
|
||||
release.map { release ->
|
||||
val apkAsset =
|
||||
release.assets.find { asset ->
|
||||
asset.name.startsWith("wgtunnel-full-v") && asset.name.endsWith(".apk")
|
||||
}
|
||||
val newVersion =
|
||||
apkAsset?.name?.removePrefix("wgtunnel-full-v")?.removeSuffix(".apk")
|
||||
?: return@map null
|
||||
|
||||
Timber.i("Latest version: $newVersion, current version: $currentVersion")
|
||||
if (NumberUtils.compareVersions(newVersion, currentVersion) > 0) {
|
||||
release.toAppUpdate()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun downloadApk(
|
||||
apkUrl: String,
|
||||
fileName: String,
|
||||
onProgress: (Float) -> Unit,
|
||||
): Result<File> =
|
||||
withContext(ioDispatcher) {
|
||||
try {
|
||||
// clean up old files
|
||||
context.getExternalFilesDir(null)?.listFiles()?.forEach { file ->
|
||||
if (file.extension == "apk") file.delete()
|
||||
}
|
||||
|
||||
val response: HttpResponse = httpClient.get(apkUrl)
|
||||
|
||||
val apkFile = File(context.getExternalFilesDir(null), fileName)
|
||||
|
||||
val channel: ByteReadChannel = response.bodyAsChannel()
|
||||
val totalBytes: Long = response.contentLength() ?: -1L
|
||||
var bytesCopied = 0L
|
||||
|
||||
apkFile.outputStream().use { output ->
|
||||
val buffer = ByteArray(8 * 1024)
|
||||
|
||||
while (!channel.isClosedForRead) {
|
||||
val bytesRead = channel.readAvailable(buffer)
|
||||
if (bytesRead <= 0) break
|
||||
output.write(buffer, 0, bytesRead)
|
||||
bytesCopied += bytesRead
|
||||
|
||||
if (totalBytes > 0) {
|
||||
val progress = bytesCopied.toFloat() / totalBytes
|
||||
onProgress(progress.coerceIn(0f, 1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Result.success(apkFile)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class RoomSettingsRepository(
|
||||
private val settingsDoa: SettingsDao,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : AppSettingRepository {
|
||||
|
||||
override suspend fun save(appSettings: AppSettings) {
|
||||
withContext(ioDispatcher) { settingsDoa.save(Settings.from(appSettings)) }
|
||||
}
|
||||
|
||||
override val flow =
|
||||
settingsDoa.getSettingsFlow().flowOn(ioDispatcher).map { it.toAppSettings() }
|
||||
|
||||
override suspend fun get(): AppSettings {
|
||||
return withContext(ioDispatcher) {
|
||||
(settingsDoa.getAll().firstOrNull() ?: Settings()).toAppSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class RoomTunnelRepository(
|
||||
private val tunnelConfigDao: TunnelConfigDao,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : TunnelRepository {
|
||||
|
||||
override val flow =
|
||||
tunnelConfigDao.getAllFlow().flowOn(ioDispatcher).map { it.map { it.toTunnel() } }
|
||||
|
||||
override suspend fun getAll(): Tunnels {
|
||||
return withContext(ioDispatcher) { tunnelConfigDao.getAll().map { it.toTunnel() } }
|
||||
}
|
||||
|
||||
override suspend fun save(tunnelConf: TunnelConf) {
|
||||
withContext(ioDispatcher) { tunnelConfigDao.save(TunnelConfig.from(tunnelConf)) }
|
||||
}
|
||||
|
||||
override suspend fun saveAll(tunnelConfList: List<TunnelConf>) {
|
||||
withContext(ioDispatcher) {
|
||||
tunnelConfigDao.saveAll(tunnelConfList.map(TunnelConfig::from))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updatePrimaryTunnel(tunnelConf: TunnelConf?) {
|
||||
withContext(ioDispatcher) {
|
||||
tunnelConfigDao.resetPrimaryTunnel()
|
||||
tunnelConf?.let { save(it.copy(isPrimaryTunnel = true)) }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateMobileDataTunnel(tunnelConf: TunnelConf?) {
|
||||
withContext(ioDispatcher) {
|
||||
tunnelConfigDao.resetMobileDataTunnel()
|
||||
tunnelConf?.let { save(it.copy(isMobileDataTunnel = true)) }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateEthernetTunnel(tunnelConf: TunnelConf?) {
|
||||
withContext(ioDispatcher) {
|
||||
tunnelConfigDao.resetEthernetTunnel()
|
||||
tunnelConf?.let { save(it.copy(isEthernetTunnel = true)) }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(tunnelConf: TunnelConf) {
|
||||
withContext(ioDispatcher) { tunnelConfigDao.delete(TunnelConfig.from(tunnelConf)) }
|
||||
}
|
||||
|
||||
override suspend fun getById(id: Int): TunnelConf? {
|
||||
return withContext(ioDispatcher) { tunnelConfigDao.getById(id.toLong())?.toTunnel() }
|
||||
}
|
||||
|
||||
override suspend fun getActive(): Tunnels {
|
||||
return withContext(ioDispatcher) { tunnelConfigDao.getActive().map { it.toTunnel() } }
|
||||
}
|
||||
|
||||
override suspend fun count(): Int {
|
||||
return withContext(ioDispatcher) { tunnelConfigDao.count().toInt() }
|
||||
}
|
||||
|
||||
override suspend fun findByTunnelName(name: String): TunnelConf? {
|
||||
return withContext(ioDispatcher) { tunnelConfigDao.getByName(name)?.toTunnel() }
|
||||
}
|
||||
|
||||
override suspend fun findByTunnelNetworksName(name: String): Tunnels {
|
||||
return withContext(ioDispatcher) {
|
||||
tunnelConfigDao.findByTunnelNetworkName(name).map { it.toTunnel() }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun findByMobileDataTunnel(): Tunnels {
|
||||
return withContext(ioDispatcher) {
|
||||
tunnelConfigDao.findByMobileDataTunnel().map { it.toTunnel() }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun findPrimary(): Tunnels {
|
||||
return withContext(ioDispatcher) { tunnelConfigDao.findByPrimary().map { it.toTunnel() } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.zaneschepke.wireguardautotunnel.di
|
||||
|
||||
import android.content.Context
|
||||
import com.zaneschepke.logcatter.LogReader
|
||||
import com.zaneschepke.logcatter.LogcatReader
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.DynamicShortcutManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class AppModule {
|
||||
@Singleton
|
||||
@ApplicationScope
|
||||
@Provides
|
||||
fun providesApplicationScope(
|
||||
@DefaultDispatcher defaultDispatcher: CoroutineDispatcher
|
||||
): CoroutineScope = CoroutineScope(SupervisorJob() + defaultDispatcher)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideLogCollect(@ApplicationContext context: Context): LogReader {
|
||||
return LogcatReader.init(storageDir = context.filesDir.absolutePath)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideNotificationService(@ApplicationContext context: Context): NotificationManager {
|
||||
return WireGuardNotification(context)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideShortcutManager(
|
||||
@ApplicationContext context: Context,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||
): ShortcutManager {
|
||||
return DynamicShortcutManager(context, ioDispatcher)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.zaneschepke.wireguardautotunnel.di
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class TunnelShell
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class AppShell
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Kernel
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Userspace
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.zaneschepke.wireguardautotunnel.di
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME) @Qualifier annotation class DefaultDispatcher
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME) @Qualifier annotation class IoDispatcher
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME) @Qualifier annotation class MainDispatcher
|
||||
|
||||
@Retention(AnnotationRetention.BINARY) @Qualifier annotation class MainImmediateDispatcher
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME) @Qualifier annotation class ApplicationScope
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME) @Qualifier annotation class ServiceScope
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.zaneschepke.wireguardautotunnel.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object CoroutinesDispatchersModule {
|
||||
@DefaultDispatcher
|
||||
@Provides
|
||||
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
|
||||
|
||||
@IoDispatcher @Provides fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
|
||||
|
||||
@MainDispatcher @Provides fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
|
||||
|
||||
@MainImmediateDispatcher
|
||||
@Provides
|
||||
fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.zaneschepke.wireguardautotunnel.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
|
||||
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi
|
||||
import com.zaneschepke.wireguardautotunnel.data.network.KtorClient
|
||||
import com.zaneschepke.wireguardautotunnel.data.network.KtorGitHubApi
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRoomRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.DataStoreAppStateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.GitHubUpdateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomTunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.UpdateRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import io.ktor.client.HttpClient
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class RepositoryModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
|
||||
return Room.databaseBuilder(
|
||||
context,
|
||||
AppDatabase::class.java,
|
||||
context.getString(R.string.db_name),
|
||||
)
|
||||
.fallbackToDestructiveMigration(true)
|
||||
.addCallback(DatabaseCallback())
|
||||
.build()
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao {
|
||||
return appDatabase.settingDao()
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideTunnelConfigDoa(appDatabase: AppDatabase): TunnelConfigDao {
|
||||
return appDatabase.tunnelConfigDoa()
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideTunnelConfigRepository(
|
||||
tunnelConfigDao: TunnelConfigDao,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||
): TunnelRepository {
|
||||
return RoomTunnelRepository(tunnelConfigDao, ioDispatcher)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideSettingsRepository(
|
||||
settingsDao: SettingsDao,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||
): AppSettingRepository {
|
||||
return RoomSettingsRepository(settingsDao, ioDispatcher)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providePreferencesDataStore(
|
||||
@ApplicationContext context: Context,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||
): DataStoreManager {
|
||||
return DataStoreManager(context, ioDispatcher)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGeneralStateRepository(dataStoreManager: DataStoreManager): AppStateRepository {
|
||||
return DataStoreAppStateRepository(dataStoreManager)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppDataRepository(
|
||||
settingsRepository: AppSettingRepository,
|
||||
tunnelRepository: TunnelRepository,
|
||||
appStateRepository: AppStateRepository,
|
||||
): AppDataRepository {
|
||||
return AppDataRoomRepository(settingsRepository, tunnelRepository, appStateRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideHttpClient(): HttpClient {
|
||||
return KtorClient.create()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGitHubApi(client: HttpClient): GitHubApi {
|
||||
return KtorGitHubApi(client)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideUpdateRepository(
|
||||
gitHubApi: GitHubApi,
|
||||
client: HttpClient,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||
@ApplicationContext context: Context,
|
||||
): UpdateRepository {
|
||||
return GitHubUpdateRepository(
|
||||
gitHubApi,
|
||||
client,
|
||||
"wgtunnel",
|
||||
"wgtunnel",
|
||||
context,
|
||||
ioDispatcher,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package com.zaneschepke.wireguardautotunnel.di
|
||||
|
||||
import android.content.Context
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.wireguard.android.util.ToolsInstaller
|
||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.KernelTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.UserspaceTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.amnezia.awg.backend.Backend
|
||||
import org.amnezia.awg.backend.GoBackend
|
||||
import org.amnezia.awg.backend.RootTunnelActionHandler
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class TunnelModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@TunnelShell
|
||||
fun provideTunnelRootShell(@ApplicationContext context: Context): RootShell {
|
||||
return RootShell(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@AppShell
|
||||
fun provideAppRootShell(@ApplicationContext context: Context): RootShell {
|
||||
return RootShell(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAmneziaBackend(@ApplicationContext context: Context): Backend {
|
||||
return GoBackend(context, RootTunnelActionHandler(org.amnezia.awg.util.RootShell(context)))
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideKernelBackend(
|
||||
@ApplicationContext context: Context,
|
||||
@TunnelShell shell: RootShell,
|
||||
): com.wireguard.android.backend.Backend {
|
||||
return WgQuickBackend(
|
||||
context,
|
||||
shell,
|
||||
ToolsInstaller(context, shell),
|
||||
com.wireguard.android.backend.RootTunnelActionHandler(shell),
|
||||
)
|
||||
.also { it.setMultipleTunnels(true) }
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Kernel
|
||||
fun provideKernelProvider(
|
||||
@ApplicationScope applicationScope: CoroutineScope,
|
||||
serviceManager: ServiceManager,
|
||||
appDataRepository: AppDataRepository,
|
||||
backend: com.wireguard.android.backend.Backend,
|
||||
): TunnelProvider {
|
||||
return KernelTunnel(applicationScope, serviceManager, appDataRepository, backend)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Userspace
|
||||
fun provideUserspaceProvider(
|
||||
@ApplicationScope applicationScope: CoroutineScope,
|
||||
serviceManager: ServiceManager,
|
||||
appDataRepository: AppDataRepository,
|
||||
backend: Backend,
|
||||
): TunnelProvider {
|
||||
return UserspaceTunnel(applicationScope, serviceManager, appDataRepository, backend)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTunnelManager(
|
||||
@Kernel kernelTunnel: TunnelProvider,
|
||||
@Userspace userspaceTunnel: TunnelProvider,
|
||||
appDataRepository: AppDataRepository,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||
@ApplicationScope applicationScope: CoroutineScope,
|
||||
): TunnelManager {
|
||||
return TunnelManager(
|
||||
kernelTunnel,
|
||||
userspaceTunnel,
|
||||
appDataRepository,
|
||||
applicationScope,
|
||||
ioDispatcher,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNetworkMonitor(
|
||||
@ApplicationContext context: Context,
|
||||
settingsRepository: AppSettingRepository,
|
||||
): NetworkMonitor {
|
||||
return AndroidNetworkMonitor(context) {
|
||||
runBlocking { settingsRepository.get().isWifiNameByShellEnabled }
|
||||
}
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideServiceManager(
|
||||
@ApplicationContext context: Context,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||
@MainDispatcher mainCoroutineDispatcher: CoroutineDispatcher,
|
||||
@ApplicationScope applicationScope: CoroutineScope,
|
||||
appDataRepository: AppDataRepository,
|
||||
): ServiceManager {
|
||||
return ServiceManager(
|
||||
context,
|
||||
ioDispatcher,
|
||||
applicationScope,
|
||||
mainCoroutineDispatcher,
|
||||
appDataRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.zaneschepke.wireguardautotunnel.di
|
||||
|
||||
import android.content.Context
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
class ViewModelModule {
|
||||
@ViewModelScoped
|
||||
@Provides
|
||||
fun provideFileUtils(
|
||||
@ApplicationContext context: Context,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||
): FileUtils {
|
||||
return FileUtils(context, ioDispatcher)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.entity
|
||||
|
||||
data class AppSettings(
|
||||
val id: Int = 0,
|
||||
val isAutoTunnelEnabled: Boolean = false,
|
||||
val isTunnelOnMobileDataEnabled: Boolean = false,
|
||||
val trustedNetworkSSIDs: List<String> = emptyList(),
|
||||
val isAlwaysOnVpnEnabled: Boolean = false,
|
||||
val isTunnelOnEthernetEnabled: Boolean = false,
|
||||
val isShortcutsEnabled: Boolean = false,
|
||||
val isTunnelOnWifiEnabled: Boolean = false,
|
||||
val isKernelEnabled: Boolean = false,
|
||||
val isRestoreOnBootEnabled: Boolean = false,
|
||||
val isMultiTunnelEnabled: Boolean = false,
|
||||
val isPingEnabled: Boolean = false,
|
||||
val isAmneziaEnabled: Boolean = false,
|
||||
val isWildcardsEnabled: Boolean = false,
|
||||
val isWifiNameByShellEnabled: Boolean = false,
|
||||
val isStopOnNoInternetEnabled: Boolean = false,
|
||||
val isVpnKillSwitchEnabled: Boolean = false,
|
||||
val isKernelKillSwitchEnabled: Boolean = false,
|
||||
val isLanOnKillSwitchEnabled: Boolean = false,
|
||||
val debounceDelaySeconds: Int = 3,
|
||||
val isDisableKillSwitchOnTrustedEnabled: Boolean = false,
|
||||
) {
|
||||
fun debounceDelayMillis(): Long {
|
||||
return debounceDelaySeconds * 1000L
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.entity
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||
|
||||
data class AppState(
|
||||
val isLocationDisclosureShown: Boolean,
|
||||
val isBatteryOptimizationDisableShown: Boolean,
|
||||
val isPinLockEnabled: Boolean,
|
||||
val expandedTunnelIds: List<Int>,
|
||||
val isLocalLogsEnabled: Boolean,
|
||||
val isRemoteControlEnabled: Boolean,
|
||||
val remoteKey: String?,
|
||||
val locale: String?,
|
||||
val theme: Theme,
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.entity
|
||||
|
||||
data class AppUpdate(
|
||||
val version: String,
|
||||
val title: String,
|
||||
val releaseNotes: String,
|
||||
val apkUrl: String?,
|
||||
val apkFileName: String?,
|
||||
)
|
||||
@@ -0,0 +1,219 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.entity
|
||||
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.config.Config
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.*
|
||||
import java.io.InputStream
|
||||
import java.net.InetAddress
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
data class TunnelConf(
|
||||
val id: Int = 0,
|
||||
val tunName: String,
|
||||
val wgQuick: String,
|
||||
val tunnelNetworks: List<String> = emptyList(),
|
||||
val isMobileDataTunnel: Boolean = false,
|
||||
val isPrimaryTunnel: Boolean = false,
|
||||
val amQuick: String,
|
||||
val isActive: Boolean = false,
|
||||
val isPingEnabled: Boolean = false,
|
||||
val pingInterval: Long? = null,
|
||||
val pingCooldown: Long? = null,
|
||||
val pingIp: String? = null,
|
||||
val isEthernetTunnel: Boolean = false,
|
||||
val isIpv4Preferred: Boolean = true,
|
||||
val useCache: Boolean = false,
|
||||
@Transient private var stateChangeCallback: ((Any) -> Unit)? = null,
|
||||
) : Tunnel, org.amnezia.awg.backend.Tunnel {
|
||||
|
||||
fun setStateChangeCallback(callback: (Any) -> Unit) {
|
||||
stateChangeCallback = callback
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is TunnelConf) return false
|
||||
return id == other.id &&
|
||||
tunName == other.tunName &&
|
||||
wgQuick == other.wgQuick &&
|
||||
amQuick == other.amQuick &&
|
||||
isPrimaryTunnel == other.isPrimaryTunnel &&
|
||||
isMobileDataTunnel == other.isMobileDataTunnel &&
|
||||
isEthernetTunnel == other.isEthernetTunnel &&
|
||||
isPingEnabled == other.isPingEnabled &&
|
||||
pingIp == other.pingIp &&
|
||||
pingCooldown == other.pingCooldown &&
|
||||
pingInterval == other.pingInterval &&
|
||||
tunnelNetworks == other.tunnelNetworks &&
|
||||
isIpv4Preferred == other.isIpv4Preferred
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id
|
||||
result = 31 * result + tunName.hashCode()
|
||||
result = 31 * result + wgQuick.hashCode()
|
||||
result = 31 * result + amQuick.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
fun isStaticallyConfigured(): Boolean {
|
||||
return toAmConfig().peers.all { it.endpoint.get().host.isValidIpv4orIpv6Address() }
|
||||
}
|
||||
|
||||
fun copyWithCallback(
|
||||
id: Int = this.id,
|
||||
tunName: String = this.tunName,
|
||||
wgQuick: String = this.wgQuick,
|
||||
tunnelNetworks: List<String> = this.tunnelNetworks,
|
||||
isMobileDataTunnel: Boolean = this.isMobileDataTunnel,
|
||||
isPrimaryTunnel: Boolean = this.isPrimaryTunnel,
|
||||
amQuick: String = this.amQuick,
|
||||
isActive: Boolean = this.isActive,
|
||||
isPingEnabled: Boolean = this.isPingEnabled,
|
||||
pingInterval: Long? = this.pingInterval,
|
||||
pingCooldown: Long? = this.pingCooldown,
|
||||
pingIp: String? = this.pingIp,
|
||||
isEthernetTunnel: Boolean = this.isEthernetTunnel,
|
||||
isIpv4Preferred: Boolean = this.isIpv4Preferred,
|
||||
): TunnelConf {
|
||||
return TunnelConf(
|
||||
id,
|
||||
tunName,
|
||||
wgQuick,
|
||||
tunnelNetworks,
|
||||
isMobileDataTunnel,
|
||||
isPrimaryTunnel,
|
||||
amQuick,
|
||||
isActive,
|
||||
isPingEnabled,
|
||||
pingInterval,
|
||||
pingCooldown,
|
||||
pingIp,
|
||||
isEthernetTunnel,
|
||||
isIpv4Preferred,
|
||||
)
|
||||
.apply { stateChangeCallback = this@TunnelConf.stateChangeCallback }
|
||||
}
|
||||
|
||||
fun toAmConfig(): org.amnezia.awg.config.Config {
|
||||
return configFromAmQuick(amQuick.ifBlank { wgQuick })
|
||||
}
|
||||
|
||||
fun toWgConfig(): Config {
|
||||
return configFromWgQuick(wgQuick)
|
||||
}
|
||||
|
||||
override fun getName(): String = tunName
|
||||
|
||||
override fun isIpv4ResolutionPreferred(): Boolean = isIpv4Preferred
|
||||
|
||||
override fun useCache(): Boolean = useCache
|
||||
|
||||
override fun onStateChange(newState: org.amnezia.awg.backend.Tunnel.State) {
|
||||
stateChangeCallback?.invoke(newState)
|
||||
}
|
||||
|
||||
override fun onStateChange(newState: Tunnel.State) {
|
||||
stateChangeCallback?.invoke(newState)
|
||||
}
|
||||
|
||||
fun generateUniqueName(tunnelNames: List<String>): String {
|
||||
var tunnelName = this.tunName
|
||||
var num = 1
|
||||
while (tunnelNames.any { it == tunnelName }) {
|
||||
tunnelName =
|
||||
if (!tunnelName.hasNumberInParentheses()) {
|
||||
"$name($num)"
|
||||
} else {
|
||||
val pair = tunnelName.extractNameAndNumber()
|
||||
"${pair?.first}($num)"
|
||||
}
|
||||
num++
|
||||
}
|
||||
return tunnelName
|
||||
}
|
||||
|
||||
suspend fun isTunnelPingable(context: CoroutineContext): Boolean {
|
||||
return withContext(context) {
|
||||
val config = toWgConfig()
|
||||
if (pingIp != null) {
|
||||
return@withContext InetAddress.getByName(pingIp)
|
||||
.isReachable(Constants.PING_TIMEOUT.toInt())
|
||||
.also { Timber.i("Ping reachable $pingIp: $it") }
|
||||
}
|
||||
config.peers
|
||||
.map { peer -> peer.isReachable() }
|
||||
.all { true }
|
||||
.also { Timber.i("Ping of all peers reachable: $it") }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun configFromWgQuick(wgQuick: String): Config {
|
||||
val inputStream: InputStream = wgQuick.byteInputStream()
|
||||
return inputStream.bufferedReader(StandardCharsets.UTF_8).use { Config.parse(it) }
|
||||
}
|
||||
|
||||
fun configFromAmQuick(amQuick: String): org.amnezia.awg.config.Config {
|
||||
val inputStream: InputStream = amQuick.byteInputStream()
|
||||
return inputStream.bufferedReader(StandardCharsets.UTF_8).use {
|
||||
org.amnezia.awg.config.Config.parse(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun tunnelConfigFromAmConfig(
|
||||
config: org.amnezia.awg.config.Config,
|
||||
name: String? = null,
|
||||
): TunnelConf {
|
||||
val amQuick = config.toAwgQuickString(true)
|
||||
val wgQuick = config.toWgQuickString()
|
||||
return TunnelConf(
|
||||
tunName = name ?: config.defaultName(),
|
||||
wgQuick = wgQuick,
|
||||
amQuick = amQuick,
|
||||
)
|
||||
}
|
||||
|
||||
private const val IPV6_ALL_NETWORKS = "::/0"
|
||||
private const val IPV4_ALL_NETWORKS = "0.0.0.0/0"
|
||||
val ALL_IPS = listOf(IPV4_ALL_NETWORKS, IPV6_ALL_NETWORKS)
|
||||
private val IPV4_PUBLIC_NETWORKS =
|
||||
listOf(
|
||||
"0.0.0.0/5",
|
||||
"8.0.0.0/7",
|
||||
"11.0.0.0/8",
|
||||
"12.0.0.0/6",
|
||||
"16.0.0.0/4",
|
||||
"32.0.0.0/3",
|
||||
"64.0.0.0/2",
|
||||
"128.0.0.0/3",
|
||||
"160.0.0.0/5",
|
||||
"168.0.0.0/6",
|
||||
"172.0.0.0/12",
|
||||
"172.32.0.0/11",
|
||||
"172.64.0.0/10",
|
||||
"172.128.0.0/9",
|
||||
"173.0.0.0/8",
|
||||
"174.0.0.0/7",
|
||||
"176.0.0.0/4",
|
||||
"192.0.0.0/9",
|
||||
"192.128.0.0/11",
|
||||
"192.160.0.0/13",
|
||||
"192.169.0.0/16",
|
||||
"192.170.0.0/15",
|
||||
"192.172.0.0/14",
|
||||
"192.176.0.0/12",
|
||||
"192.192.0.0/10",
|
||||
"193.0.0.0/8",
|
||||
"194.0.0.0/7",
|
||||
"196.0.0.0/6",
|
||||
"200.0.0.0/5",
|
||||
"208.0.0.0/4",
|
||||
)
|
||||
val LAN_BYPASS_ALLOWED_IPS = listOf(IPV6_ALL_NETWORKS) + IPV4_PUBLIC_NETWORKS
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.enums
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
|
||||
sealed class BackendError : Exception() {
|
||||
data object DNS : BackendError()
|
||||
|
||||
data object Unauthorized : BackendError()
|
||||
|
||||
data object Config : BackendError()
|
||||
|
||||
data object KernelModuleName : BackendError()
|
||||
|
||||
data object InvalidConfig : BackendError()
|
||||
|
||||
data object NotAuthorized : BackendError()
|
||||
|
||||
data object ServiceNotRunning : BackendError()
|
||||
|
||||
data object Unknown : BackendError()
|
||||
|
||||
fun toStringRes() =
|
||||
when (this) {
|
||||
Config -> R.string.config_error
|
||||
DNS -> R.string.dns_resolve_error
|
||||
InvalidConfig -> R.string.invalid_config_error
|
||||
KernelModuleName -> R.string.kernel_name_error
|
||||
NotAuthorized,
|
||||
Unauthorized -> R.string.auth_error
|
||||
ServiceNotRunning -> R.string.service_running_error
|
||||
Unknown -> R.string.unknown_error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.enums
|
||||
|
||||
enum class BackendState {
|
||||
KILL_SWITCH_ACTIVE,
|
||||
SERVICE_ACTIVE,
|
||||
INACTIVE,
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.enums
|
||||
|
||||
enum class ConfigType {
|
||||
AMNEZIA,
|
||||
WG,
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user