Compare commits

...

112 Commits

Author SHA1 Message Date
Zane Schepke 5946d7c10d feat: add lock, logs, and ping
Fixes bug where control tile tunnel did not match with tunnel being controlled Closes #132
Fixes tunnel config edit screen error message #131

Revert to official lib to fix slow speeds issue Closes #137

Adds local app lock feature Closes #88

Adds restart vpn on ping fail with 1 minute interval and 60 minute cooldown Closes #6

Adds ability to easily make a copy of a tunnel.

Fixes bug on AndroidTV where tunnels were not being deleted properly.

Fixes bug where auto tunneling could be turned on before VPN permission was given.
2024-03-18 22:52:00 -04:00
Zane Schepke 4fc8ffbcbb feat: add logs screen 2024-03-13 02:52:57 -04:00
Zane Schepke c0cff297b2 remove firebase 2024-03-11 12:39:00 -04:00
Zane Schepke ee8db0a859 fix: ui bug and graphene notification
Fixes a bug where save button was hidden on config screen

Adds a disclaimer notification for when GrapheneOS auto enabled Always-on VPN on first app tunnel start

Closes #121 #120
2024-02-20 16:17:34 -05:00
Zane Schepke c8205c4c59 fix: ui tunnel display bug
Fixes a bug where turning on auto tunneling hides the first tunnel in the app.

Closes #116
2024-02-19 08:45:52 -05:00
Zane Schepke 3247e94358 Merge pull request #105 from zaneschepke/dependabot/github_actions/actions/upload-artifact-4.3.1
build(deps): bump actions/upload-artifact from 4.3.0 to 4.3.1
2024-02-19 06:59:32 -05:00
Zane Schepke 2690ce29e1 feat: migrate to forked lib
Migrated app to a forked version of wireguard-android to enable development work on features that require changes to the core lib, like #107 #104 #87 #52 #6

Improved first launch flow by change vpn permission to only launch on first tunnel start

Changed to proper database seeding strategy

Updated README to account for GitHub packages auth requirement

Migrated from deprecated UI components and libs

Bump versions
2024-02-18 23:28:06 -05:00
Zane Schepke 500b85f687 update gradle, vpn permission 2024-02-09 22:34:10 -05:00
dependabot[bot] 84b2b75271 build(deps): bump actions/upload-artifact from 4.3.0 to 4.3.1
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.3.0 to 4.3.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.3.0...v4.3.1)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-06 14:02:45 +00:00
Zane Schepke 0197198f7b Merge pull request #100 from zaneschepke/dependabot/github_actions/peter-evans/repository-dispatch-3
build(deps): bump peter-evans/repository-dispatch from 2 to 3
2024-01-27 12:59:38 -05:00
Zane Schepke 0b271778c9 Merge pull request #99 from zaneschepke/dependabot/github_actions/actions/upload-artifact-4.3.0
build(deps): bump actions/upload-artifact from 4.2.0 to 4.3.0
2024-01-27 12:59:13 -05:00
Zane Schepke 6427b2f832 fix: icon issues
Added support for auto start on reboot for Always-On VPN kernel mode

Added support for adaptive theme icons

Fixed notification icon size, tile icon, AndroidTV icons

Clean up pipelines

Bump versions

Closes #96
Closes #98
Closes #97
Closes #79
2024-01-27 12:44:38 -05:00
dependabot[bot] 097097f620 build(deps): bump peter-evans/repository-dispatch from 2 to 3
Bumps [peter-evans/repository-dispatch](https://github.com/peter-evans/repository-dispatch) from 2 to 3.
- [Release notes](https://github.com/peter-evans/repository-dispatch/releases)
- [Commits](https://github.com/peter-evans/repository-dispatch/compare/v2...v3)

---
updated-dependencies:
- dependency-name: peter-evans/repository-dispatch
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-25 13:27:12 +00:00
dependabot[bot] 20dfaed8de build(deps): bump actions/upload-artifact from 4.2.0 to 4.3.0
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.2.0 to 4.3.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.2.0...v4.3.0)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-24 13:47:56 +00:00
Zane Schepke 07aa37fc2a docs: remove FireTV support 2024-01-21 11:05:33 -05:00
Zane Schepke 091cd2e028 fix: ci versionCode 2024-01-20 21:16:09 -05:00
Zane Schepke 7efa6d0bf4 ci: test pre-release 2024-01-20 20:45:44 -05:00
Zane Schepke e31fb01410 ci: add pre-release pipeline
Change release pipeline to deploy to production google play
2024-01-20 20:17:56 -05:00
Zane Schepke 76674323e7 ci: dispatch update for fdroid repo 2024-01-20 18:47:30 -05:00
Zane Schepke f1fc9ca6f7 chore: add icon 2024-01-20 18:15:36 -05:00
Zane Schepke cb301e74eb Merge branch 'main' of https://github.com/zaneschepke/wgtunnel 2024-01-20 13:33:21 -05:00
Zane Schepke 8141fe19be chore: move donations to funding
Add liberapay
2024-01-20 13:33:07 -05:00
Zane Schepke 0fdb3d0b31 Merge pull request #94 from zaneschepke/dependabot/github_actions/actions/upload-artifact-4.2.0
build(deps): bump actions/upload-artifact from 4.0.0 to 4.2.0
2024-01-19 21:21:15 -05:00
Zane Schepke d9f3a21cc3 fix: create config not saving
Fixes bug where creating a config from scratch was failing to save

Closes #93
Closes #96
Closes #89
2024-01-19 20:54:18 -05:00
dependabot[bot] fec84bc6ac build(deps): bump actions/upload-artifact from 4.0.0 to 4.2.0
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.0.0 to 4.2.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.0.0...v4.2.0)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-19 13:58:13 +00:00
Zane Schepke d6ee36edc0 docs: add link to README 2024-01-14 12:06:29 -05:00
Zane Schepke e3fcf712d5 Merge branch 'main' of https://github.com/zaneschepke/wgtunnel 2024-01-08 19:07:10 -05:00
Zane Schepke 5a15776bb3 fix: tunnel disable frozen
Fixes a bug where after toggling a tunnel so many times it would eventually get stuck in the on position. This was also impacting auto-tunneling reliability.

Fixes a bug where clicking the email button on the support page would not populate the "to" email field.

Fixes a bug where you could not save a tunnel without having configured DNS.

Added a dialog to prompt user if they are deleting a tunnel.

Added battery optimization disable request when first launching auto-tunneling.

Format to kotlinlang standards.

Fix ci google play deploy.

Closes #63
2024-01-08 19:06:40 -05:00
Zane Schepke 3339448424 fix: tunnel disable frozen
Fixes a bug where after toggling a tunnel so many times it would eventually get stuck in the on position. This was also impacting auto-tunneling reliability.

Fixes a bug where clicking the email button on the support page would not populate the "to" email field.

Fixes a bug where you could not save a tunnel without having configured DNS.

Added a dialog to prompt user if they are deleting a tunnel.

Added battery optimization disable request when first launching auto-tunneling.

Format to kotlinlang standards.
2024-01-08 18:42:30 -05:00
Zane Schepke 7ec294b789 docs: update full_description.txt 2024-01-04 22:03:02 -05:00
Zane Schepke e59c72788d Merge pull request #82 from zaneschepke/dependabot/github_actions/actions/download-artifact-4
build(deps): bump actions/download-artifact from 1 to 4
2024-01-01 12:21:34 -05:00
Zane Schepke 62435d549c Merge pull request #84 from zaneschepke/dependabot/github_actions/actions/upload-artifact-4.0.0
build(deps): bump actions/upload-artifact from 3.1.2 to 4.0.0
2024-01-01 12:21:01 -05:00
Zane Schepke 96800037d1 Merge pull request #81 from zaneschepke/dependabot/github_actions/actions/checkout-4
build(deps): bump actions/checkout from 3 to 4
2024-01-01 12:20:19 -05:00
Zane Schepke f5b3bb1cb7 Merge pull request #80 from zaneschepke/dependabot/github_actions/actions/setup-java-4
build(deps): bump actions/setup-java from 3 to 4
2024-01-01 12:18:48 -05:00
dependabot[bot] 654b4a4719 build(deps): bump actions/upload-artifact from 3.1.2 to 4.0.0
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3.1.2 to 4.0.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3.1.2...v4.0.0)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-01 17:03:45 +00:00
dependabot[bot] a19b5ce22a build(deps): bump actions/download-artifact from 1 to 4
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 1 to 4.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v1...v4)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-01 17:03:43 +00:00
dependabot[bot] 86f592255c build(deps): bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-01 17:03:40 +00:00
dependabot[bot] 4a5dd76b5b build(deps): bump actions/setup-java from 3 to 4
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 3 to 4.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-01 17:03:39 +00:00
Zane Schepke 1139d17f13 chore: add dependabot 2024-01-01 12:03:18 -05:00
Zane Schepke 36855319a2 docs: update README 2024-01-01 11:53:04 -05:00
Zane Schepke 61e3751321 fix: start foreground
Fixes issue where auto tunnel should be starting service foreground
2023-12-31 22:44:44 -05:00
Zane Schepke dd16bd977f fix pipeline sdk target 2023-12-31 20:35:57 -05:00
Zane Schepke aeb4a13389 feat: androidtv navigation, auto-tunneling pause
Improved AndroidTV navigation to be less clunky and more streamlined

Added auto-tunneling pause feature to UI to allow of quick auto tunneling pauses.
App shortcuts and quick tile also override auto tunneling by engaging pause for temporary override of VPN purposes.

Fixed bug where auto start on reboot was not working on older devices and AndroidTV.

Fixed bug where location services is prefenting some flavors of Android from using auto-tunneling.

Fixed bug where location permissions were not being detected correctly on AndroidTV versions.

Fixed bug where quick tile could become out of sync.

Improved notifications to show proper state of auto-tunneling and vpn.

Removed excessive vibration from notifications.

Improved error handling.

Closes #75
Closes #73
Closes #61
Closes #53
Closes #30
2023-12-31 17:59:30 -05:00
Zane Schepke f0ec661223 fix: android 14 foreground permissions
Closes #71
2023-12-21 12:22:55 -05:00
Zane Schepke ffa7a207fb feat: add basic kernel support
Added basic kernel support to allow users to switch between userspace and kernel wireguard

Improved location disclosure flow to only show once once per app install

Fix airplane mode bug

Improve database migration testing

Fix auto-tunneling permission bug.

Lint

Closes #67
Closes #43
2023-12-20 23:17:15 -05:00
Zane Schepke 515e91d191 fix: airplane mode bug 2023-12-09 09:45:54 -05:00
Zane Schepke 16e65aec9f feat: mobile data only auto-tunneling
Added support for configuring auto-tunneling to only tunnel on mobile data with no location permissions necessary.

Improved UI on support screen and updated support resource links.

Changed UI on setting screen to hide trusted wifi networks configuration when not in use.

Changed verbiage on settings screen to make auto-tunneling configuration more intuitive.

Fixed UI bug where analytics expansion could show on deactivated tunnels.

Closes #55
Closes #56
2023-12-05 01:19:00 -05:00
Zane Schepke ff87bee3b4 docs: add support.md 2023-11-29 18:13:29 -05:00
Zane Schepke 9739d35eda chore: update descriptions 2023-11-23 23:48:23 -05:00
Zane Schepke 11ad494fbb feat: add statistics to main screen
Removed Details screen and moved tunnel statistics to be shown on expansion of a tunnel config row

Fixes bug where not scanning a QR code could cause app to crash when navigating back from camera view

Remove vibrations from notifications

Improve navigation of settings screen on AndroidTV
2023-11-23 21:04:26 -05:00
Zane Schepke 90b006acc5 change: build pipeline to fastlane deploy 2023-11-10 18:32:35 -05:00
Zane Schepke eb7b39c379 fix: build warnings and deprecations 2023-11-08 23:43:46 -05:00
Zane Schepke 0a17593310 add: properties to template 2023-11-08 22:26:36 -05:00
Zane Schepke c0e58125dd add: build and release CI
Add build and release pipeline for app

Fixes bug where location permission screen was not appearing on < Android 9

Bump versions
2023-11-08 22:11:52 -05:00
Zane Schepke 3791261f91 add: build and release CI
Add build and release pipeline for app

Fixes bug where location permission screen was not appearing on < Android 9

Bump versions
2023-11-08 21:58:17 -05:00
Zane Schepke d1e61be3ae Update issue templates 2023-11-04 11:45:00 -04:00
Zane Schepke afd4fb127f Update issue templates 2023-11-04 11:41:25 -04:00
Zane Schepke e0cce8fba4 fix: converter backwards compatibility
Fixes database converter to allow for backwards compatabilty.
2023-10-27 00:11:31 -04:00
Zane Schepke b70ecbdfff fix: fdroid build and ssid comma
Fixes bug where commas in SSID names were splitting into multiple SSIDs due to database type converters.
Closes #48

Fixes bug where F-Droid pipeline was failing to build due to lack of proguard rule. Closes #47

Fixes bug where crashes could happen if config QR code or file has improperly configured data.

Bump versions.
2023-10-26 22:05:20 -04:00
Zane Schepke 513d08998b fix: config save bug
Fixes a bug where config changes were saving on the wrong thread, causing a failure to save changes.

Fixes a bug where the quick tile could cause a crash by initializing the tile state before it was ready.
2023-10-23 16:13:37 -04:00
Zane Schepke 79583e0e61 docs: update README 2023-10-21 15:17:09 -04:00
Zane Schepke 75790ec6d5 bump: app version to 3.1.6 2023-10-21 14:13:47 -04:00
Zane Schepke a1941b7229 feat: shortcut intents and battery saver
Added the ability to turn on and off tunnels via intents to the shortcut activity.

Added a setting to enable or disable shortcut/intent control of tunnels.

Added an experimental battery saver setting to auto-tunneling to fix the battery drain issue caused by wakelock.

Fixes a bug where sometimes the config screen could crash if there are issues parsing the tunnel config data.

Database migration
2023-10-21 14:03:36 -04:00
Zane Schepke 37bae82700 feat: zip file import export
Added support for importing zip files containing multiple config files.
Closes #33

Added support for exporting all config files to downloads folder as a zip with biometric or security pin approval.

Added support for editing or viewing private key with biometric or security pin approval.

Fixed a bug where VPN status indicator functionality was unintentionally disabled.

Other various enhancements and refactors.
2023-10-16 22:43:28 -04:00
Zane Schepke 77cd328a71 fix: config screen edit bug
Fixes a bug where config edit will break configs that have commas because a comma and space are required in tunnel configs

 #42
2023-10-14 01:32:46 -04:00
Zane Schepke 5a1430706b fix: config edit whitespace bug
Closes #42
2023-10-13 23:42:39 -04:00
Zane Schepke 321730536d fix: file import handling
Fixes bug causing crashes when importing a file that is not a .conf file.

Fixes file import on AndroidTV and FireTV
Closes #39

Fixes usability issue on AndroidTV where textboxes immediately open the keyboard on hover.
Allows users two keypress access to turning on tunnels from app load.
Closes #36

Improves service efficiencies by making coroutines lifecycle aware.
2023-10-12 20:20:53 -04:00
Zane Schepke 2912238f27 Merge pull request #41 from licaon-kter/patch-1
Gradle 8.3 was released
2023-10-11 09:31:02 -04:00
Licaon_Kter bc7daacd13 Gradle 8.3 was released 2023-10-11 11:42:58 +00:00
Zane Schepke c8f2cfd758 feat: ui update with full config edit
feat: Change config screen UI to accommodate editing of all fields.
Change config screen to allow the addition of new peers.
Closes #31

Change main screen to denote primary tunnel with star.
Change main screen tunnel options to allow changing tunnel to primary.

feat: Change settings screen UI to improve usability.
Closes #32

fix: Change application shortcuts to static shortcuts for the primary tunnel.
Remove dynamic shortcuts.
Closes #38

fix: Database changes from the previous version causing crashes.
#40 #37

fix: bug on detail screen where tunnel name was not showing

feat: Change button color when hovering on AndroidTV to improve visibility
#36

fix: Change file importing method to fix bug on FireTV
Closes #39

docs: update README with new screenshots
2023-10-10 13:19:20 -04:00
Zane Schepke ca3f3fd439 fix: file selection AndroidTV
Closes #35 Closes #34
2023-09-28 09:52:35 -04:00
Zane Schepke 235170508b docs: Update README.md 2023-09-26 17:06:12 -04:00
Zane Schepke 11aea3f1c4 fix: trusted SSIDs bug
Fixes bugs where trusted SSIDs were getting whitespace added every time they were saved to the DB.

Fixes file explorer bug on AndroidTV where the app crashes if they do not have a file explorer installed on the device.
2023-09-25 06:49:07 -04:00
Zane Schepke 7fbc51af4c feat: tunnel on ethernet
Adds settings feature to enable auto tunneling on ethernet connections.

Add alphabetical sorting for split tunneling packages

Fix bug where search was not searching by displayed label but by package name

Other refactoring and improvements.

Closes #29 Closes #27 Closes #28
2023-09-24 07:40:17 -04:00
Zane Schepke 1714618f0c refactor: bump release to 3.0.0
Bump release to 3.0.0 for F-Droid tag referencing for auto update
2023-09-16 10:46:30 -04:00
Zane Schepke 7cb798a111 refactor: change to static version codes for F-Droid auto-update 2023-09-16 02:06:55 -04:00
Zane Schepke f8bc264f30 refactor: change qrcode scanner 2023-09-15 14:51:38 -04:00
Zane Schepke 2174c3f48c fix: f-droid pipelines issues 2023-09-15 13:36:27 -04:00
Zane Schepke 413b9a37df fix: f-droid build warnings 2023-09-15 13:03:35 -04:00
Zane Schepke 6c30c6bae6 refactor: change flavor to fdroid 2023-09-15 11:31:16 -04:00
Zane Schepke 3c5aff31aa refactor: objectbox to room
Migrated app from using ObjectBox library for db to Room as ObjectBox is not FLOSS compliant.

Migrated app to using version catalog for managing dependancies.

Added floss build flavor for F-Droid and general build flavor for all other builds. Floss build excludes google analytics and crashlytics.

Other performance improvements and refactors.
2023-09-15 07:24:19 -04:00
Zane Schepke 991d1224ab chore: add fastlane for F-Droid 2023-09-13 03:10:40 -04:00
Zane Schepke 69b07eec6f feat: support Android 8
Add support for Android 8.

Fix shortcuts bug where it was toggling auto-tunneling without setting enabled.

Fix shortcuts name not updating with config edits.

Bump versions.

Closes #25
2023-09-12 12:48:54 -04:00
Zane Schepke e81066f508 fix: FireTV file selection bug
Fixes bug where FireTV devices were unable to launch a proper file browser to select tunnel configs.
2023-09-11 11:15:44 -04:00
Zane Schepke 64bb9f3b82 fix: foreground service start crashes
Attempt to fix startForegrounService causing crashes on some devices by not meeting the 5 second notification rule. Add notification to onCreate of services.

Limit startForeground to only be called where it is truly necessary in the TileService to allow starting the VPN while app is not running.

Attempt to manually initialize mlkit for QR code scanning to remediate some crashes caused by config scanning.
2023-09-10 00:23:23 -04:00
Zane Schepke c1b560e822 chore: update README.md 2023-09-08 12:43:41 -04:00
Zane Schepke 14fe5821cc Merge branch 'main' of github.com:zaneschepke/wgtunnel 2023-09-04 05:52:27 -04:00
Zane Schepke 9d9b7bebca fix: foreground service crash on older devices
Fixes a bug where older device take a longer time to launch the foreground service and connect to the VPN. Combined with a delayed launch of foreground notification until VPN connection is confirmed, this would break foreground service,s 5 second notification rule.

Fixed by adding a new attempting connection notification to launch on vpn initial connection attempt.
2023-09-04 05:52:15 -04:00
Zane Schepke 12d1ccc084 chore: update README.md 2023-09-04 02:51:05 -04:00
Zane Schepke 20cc2c09b0 feat: dynamic shortcuts and package search
Add dynamic on and off shortcuts for each new tunnel added. Shortcuts will toggle auto tunneling to allow manual override.

Add package search for split tunneling.

Add tunnel toggling from tile even while app is closed and auto tunneling is disabled.

Refactor and code cleanup.

Closes #23 Closes #21
2023-09-04 02:42:49 -04:00
Zane Schepke eeccc71469 fix: tile auto tunnel toggle and wifi internet connection check
Change tile toggle behavior to also toggle auto tunnel service to allow user temporary override and re-enablement of auto tunnel from the tile.

Add tunnel name to tile.

Fix bug where wifi networks without internet access were impacting auto tunneling determinations.

Closes #22
2023-09-01 13:05:17 -04:00
Zane Schepke 0e64bbb4e1 feat: add quick setting tile
Add quick settings tile for easy tunnel toggling and auto-tunnel override.

Fix bug on AndroidTV D-pad tunnel control for multiple tunnels.

Closes #18 , Closes #20
2023-08-30 23:58:03 -04:00
Zane Schepke f513297ba0 fix: file selection on older devices
Fixes bug where file selection was causing app to crash on older devices.
2023-08-11 21:13:54 -04:00
Zane Schepke 135f8c0459 chore: add tv assets 2023-08-11 20:56:03 -04:00
Zane Schepke 7a811f4152 fix: bug causing crashes on older devices
Fixes an issue where watcher service had the potential to crash on older devices if the job was not initialized fast enough.

Optimize imports.

Bump versions.
2023-08-11 20:21:28 -04:00
Zane Schepke 2abf681d17 feat: support Android 9
Added support for Android 9 by updating permission checks and wifi SSID logic.

Fix bug where setting screen was cut off on AndroidTV by updating padding values.

Bump wireguard-android library version.

Closes #13, Closes #16
2023-08-04 16:43:36 -04:00
Zane Schepke 689c97f452 fix: auto tunneling failure with rapid network changes
Fixes issue where rapid network switching could cause unexpected VPN connections and disconnection. Fixed by changing from real time network VPN triggers to three second interval checks.

Attempts to optimize battery drain while VPN connected by switching VPN statistics gathering to 10 second intervals.

Closes #11, Closes #12, Closes #10
2023-08-01 17:39:11 -04:00
Zane Schepke 08d11a53b4 fix: AndroidTV D-pad control and banner
Fix Android TV D-pad access to elements on screen without reloading screen

Update AndroidTV banner
2023-07-31 23:38:51 -04:00
Zane Schepke 9952e97e1c Merge branch 'main' of github.com:zaneschepke/wgtunnel 2023-07-29 18:18:26 -04:00
Zane Schepke 4cdc974778 fix: AndroidTV D-pad support and precise location check
Allows app be fully navigated via D-pad on AndroidTV (with some quirks)

Adds precise location check to setting screen as WiFi-SSID is not readable without this permission.

Closes #2
2023-07-29 18:18:24 -04:00
Zane Schepke e31a4c03cd docs: update README.md 2023-07-27 17:21:01 -04:00
Zane Schepke 5b94f22359 docs: update README.md 2023-07-27 17:19:25 -04:00
Zane Schepke c673a8dc91 feat: support for Always-On VPN and Android TV
Added support for Android TV

Added support for Always-On VPN in settings

Fixes bug where handshake notification is not dismissed after successful handshake

Fixes bug that allowed you to use Auto tunneling without Location Services enabled. Now checks for Location Services before allowing access to feature.

Closes #2,  Closes #5, Closes #8
2023-07-23 00:51:19 -04:00
Zane Schepke f6612abe28 fix: service requirements for Android 14
Fixes requirement for foregroundServiceType which is causing crashes on Android 14
2023-07-20 14:23:43 -04:00
Zane Schepke 7ca5de1836 fix: screens not scrollable in landscape
Fixes not being able to scroll when in landscape mode if content is off screen.

Fixes FAB being in the way of controlling tunnels by making it disappear on scroll down.

Bump targetSdk to 34
2023-07-19 09:02:31 -04:00
Zane Schepke 509d22a98c build: disable crashlytics on debug builds 2023-07-18 17:07:36 -04:00
Zane Schepke 68b0902398 feat: add tunnel details screen and handshake monitoring
Adds details screen which display details of tunnel configuration as well as last handshake and rx/tx of peer.

Adds last handshake monitoring with statuses and thresholds.

Adds handshake/connection notifications based on last successful handshake.

Adds status LED next to tunnel on main screen.

Fixes bug where first click on QR code could result in nothing happening if QR code module is being downloaded. Now shows message to user.

Fixes bug where changes made after editing tunnel were not propagated to settings if that tunnel was configured as the default tunnel.

Fixes bug causing crash if wrong config file selected

Update README

Closes #7, Closes #6
2023-07-18 11:53:03 -04:00
Zane Schepke 0c45558293 docs: update README.md 2023-07-09 19:57:07 -04:00
Zane Schepke 89212fe191 fix: fix bug causing inconsistent behavior after making trusted ssid changes, fix bug causing vpn not to disconnect on mobile data
This fixes a bug causing inconsistent connects and disconnects to vpn on wifi capability changes by properly associated a child job to a parent job using a SupervisorJob instead of a GlobalScope job.

This fixes a bug causing VPN to stay connected on mobile data even when the setting is disabled by adding an additional check for this situation to disable the tunnel.
2023-07-07 23:11:15 -04:00
Zane Schepke a98a47f54d fix: prevent crash from bad file config upload and bug preventing vpn from starting via watcher service after reboot
Add catch for file configs that are malformed and display message to the user

Pass full tunnel config to watcher service after detecting reboot to allow watcher service to start tunnel properly.
2023-07-05 23:21:55 -04:00
Zane Schepke cb3a7d024b fix: discord link expired 2023-07-04 22:28:39 -04:00
Zane Schepke 259be21b04 docs: update README 2023-07-04 17:03:30 -04:00
224 changed files with 8551 additions and 2593 deletions
+85
View File
@@ -0,0 +1,85 @@
[{*.kt,*.kts}]
indent_style = space
insert_final_newline = true
max_line_length = 100
indent_size = 4
ij_continuation_indent_size = 4
ij_java_names_count_to_use_import_on_demand = 9999
ij_kotlin_align_in_columns_case_branch = false
ij_kotlin_align_multiline_binary_operation = false
ij_kotlin_align_multiline_extends_list = false
ij_kotlin_align_multiline_method_parentheses = false
ij_kotlin_align_multiline_parameters = true
ij_kotlin_align_multiline_parameters_in_calls = false
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_assignment_wrap = normal
ij_kotlin_blank_lines_after_class_header = 0
ij_kotlin_blank_lines_around_block_when_branches = 0
ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1
ij_kotlin_block_comment_at_first_column = true
ij_kotlin_call_parameters_new_line_after_left_paren = true
ij_kotlin_call_parameters_right_paren_on_new_line = false
ij_kotlin_call_parameters_wrap = on_every_item
ij_kotlin_catch_on_new_line = false
ij_kotlin_class_annotation_wrap = split_into_lines
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
ij_kotlin_continuation_indent_for_chained_calls = true
ij_kotlin_continuation_indent_for_expression_bodies = true
ij_kotlin_continuation_indent_in_argument_lists = true
ij_kotlin_continuation_indent_in_elvis = false
ij_kotlin_continuation_indent_in_if_conditions = false
ij_kotlin_continuation_indent_in_parameter_lists = false
ij_kotlin_continuation_indent_in_supertype_lists = false
ij_kotlin_else_on_new_line = false
ij_kotlin_enum_constants_wrap = off
ij_kotlin_extends_list_wrap = normal
ij_kotlin_field_annotation_wrap = split_into_lines
ij_kotlin_finally_on_new_line = false
ij_kotlin_if_rparen_on_new_line = false
ij_kotlin_import_nested_classes = false
ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
ij_kotlin_keep_blank_lines_before_right_brace = 2
ij_kotlin_keep_blank_lines_in_code = 2
ij_kotlin_keep_blank_lines_in_declarations = 2
ij_kotlin_keep_first_column_comment = true
ij_kotlin_keep_indents_on_empty_lines = false
ij_kotlin_keep_line_breaks = true
ij_kotlin_lbrace_on_next_line = false
ij_kotlin_line_comment_add_space = false
ij_kotlin_line_comment_at_first_column = true
ij_kotlin_method_annotation_wrap = split_into_lines
ij_kotlin_method_call_chain_wrap = normal
ij_kotlin_method_parameters_new_line_after_left_paren = true
ij_kotlin_method_parameters_right_paren_on_new_line = true
ij_kotlin_method_parameters_wrap = on_every_item
ij_kotlin_name_count_to_use_star_import = 9999
ij_kotlin_name_count_to_use_star_import_for_members = 9999
ij_kotlin_parameter_annotation_wrap = off
ij_kotlin_space_after_comma = true
ij_kotlin_space_after_extend_colon = true
ij_kotlin_space_after_type_colon = true
ij_kotlin_space_before_catch_parentheses = true
ij_kotlin_space_before_comma = false
ij_kotlin_space_before_extend_colon = true
ij_kotlin_space_before_for_parentheses = true
ij_kotlin_space_before_if_parentheses = true
ij_kotlin_space_before_lambda_arrow = true
ij_kotlin_space_before_type_colon = false
ij_kotlin_space_before_when_parentheses = true
ij_kotlin_space_before_while_parentheses = true
ij_kotlin_spaces_around_additive_operators = true
ij_kotlin_spaces_around_assignment_operators = true
ij_kotlin_spaces_around_equality_operators = true
ij_kotlin_spaces_around_function_type_arrow = true
ij_kotlin_spaces_around_logical_operators = true
ij_kotlin_spaces_around_multiplicative_operators = true
ij_kotlin_spaces_around_range = false
ij_kotlin_spaces_around_relational_operators = true
ij_kotlin_spaces_around_unary_operator = false
ij_kotlin_spaces_around_when_arrow = true
ij_kotlin_variable_annotation_wrap = off
ij_kotlin_while_on_new_line = false
ij_kotlin_wrap_elvis_expressions = 1
ij_kotlin_wrap_expression_body_functions = 1
ij_kotlin_wrap_first_method_in_call_chain = false
+2
View File
@@ -0,0 +1,2 @@
ko_fi: zaneschepke
liberapay: zaneschepke
+33
View File
@@ -0,0 +1,33 @@
---
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]
**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.
+20
View File
@@ -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.
+17
View File
@@ -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.
+10
View File
@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily
- package-ecosystem: gradle
directory: /
schedule:
interval: daily
+99
View File
@@ -0,0 +1,99 @@
# name of the workflow
name: Android CI Tag Deployment (Pre-release)
on:
workflow_dispatch:
push:
tags:
- '*.*.*-**'
jobs:
build:
name: Build Signed APK
runs-on: ubuntu-latest
env:
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
KEY_STORE_FILE: 'android_keystore.jks'
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
GH_USER: ${{ secrets.GH_USER }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
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
# Build and sign APK ("-x test" argument is used to skip tests)
# add fdroid flavor for apk upload
- name: Build Fdroid Release APK
run: ./gradlew :app:assembleFdroidRelease -x test
# get fdroid flavor release apk path
- name: Get apk path
id: apk-path
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT
- name: Get version code
run: |
version_code=$(grep "VERSION_CODE" buildSrc/src/main/kotlin/Constants.kt | awk '{print $5}' | tr -d '\n')
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
# Save the APK after the Build job is complete to publish it as a Github release in the next job
- name: Upload APK
uses: actions/upload-artifact@v4.3.1
with:
name: wgtunnel
path: ${{ steps.apk-path.outputs.path }}
- name: Download APK from build
uses: actions/download-artifact@v4
with:
name: wgtunnel
- name: Create Release with Fastlane changelog notes
id: create_release
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# fix hardcode changelog file name
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
draft: false
prerelease: true
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
- 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 Beta track 🚀
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane beta)
+105
View File
@@ -0,0 +1,105 @@
# name of the workflow
name: Android CI Tag Deployment (Release)
on:
workflow_dispatch:
push:
tags:
- '*.*.*'
- '!*.*.*-**'
jobs:
build:
name: Build Signed APK
runs-on: ubuntu-latest
env:
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
KEY_STORE_FILE: 'android_keystore.jks'
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
GH_USER: ${{ secrets.GH_USER }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
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
# Build and sign APK ("-x test" argument is used to skip tests)
# add fdroid flavor for apk upload
- name: Build Fdroid Release APK
run: ./gradlew :app:assembleFdroidRelease -x test
# get fdroid flavor release apk path
- name: Get apk path
id: apk-path
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT
- name: Get version code
run: |
version_code=$(grep "VERSION_CODE" buildSrc/src/main/kotlin/Constants.kt | awk '{print $5}' | tr -d '\n')
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
# Save the APK after the Build job is complete to publish it as a Github release in the next job
- name: Upload APK
uses: actions/upload-artifact@v4.3.1
with:
name: wgtunnel
path: ${{ steps.apk-path.outputs.path }}
- name: Download APK from build
uses: actions/download-artifact@v4
with:
name: wgtunnel
- name: Repository Dispatch for my F-Droid repo
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.PAT }}
repository: zaneschepke/fdroid
event-type: fdroid-update
- name: Create Release with Fastlane changelog notes
id: create_release
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
draft: false
prerelease: false
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
- 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 production)
+2
View File
@@ -69,3 +69,5 @@ lint/tmp/
# App Specific cases
app/release/output.json
.idea/codeStyles/
# where we keep our signing secrets locally
app/signing.properties
+3
View File
@@ -0,0 +1,3 @@
source "https://rubygems.org"
gem "fastlane"
+65 -23
View File
@@ -2,59 +2,101 @@
WG Tunnel
</h1>
<span align="center">
<div align="center">
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Discord Chat](https://img.shields.io/discord/1108285024631001111.svg)](https://discord.gg/Ad5fuEts)
[![Discord Chat](https://img.shields.io/discord/1108285024631001111.svg)](https://discord.gg/rbRRNh6H7V)
</span>
</div>
<span align="center">
<div align="center">
[![Google Play](https://img.shields.io/badge/Google_Play-414141?style=for-the-badge&logo=google-play&logoColor=white)](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
[![F-Droid](https://img.shields.io/static/v1?style=for-the-badge&message=F-Droid&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
</span>
<span align="left">
</div>
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.
</span>
<div align="left">
<span align="center">
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.
</div>
<div align="center">
## 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" />
<img label="Main" style="padding-right:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
<img label="Config" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png" width="200" />
<img label="Settings" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
<img label="Support" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/support_screen.png" width="200" />
</p>
<span align="left">
<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.
The original inspiration for this app came from the inconvenience of having to manually turn VPN off
and on while on different networks. This app was created to offer a free solution to this problem.
## Features
* Add tunnels via .conf file
* Auto connect to VPN based on Wi-Fi SSID
* Configurable Trusted Network list
* Optional auto connect on mobile data
* Add tunnels via .conf file, zip, manual entry, or QR code
* Auto connect to VPN based on Wi-Fi SSID, ethernet, or mobile data
* Split tunneling by application with search
* WireGuard support for kernel and userspace modes
* Always-On VPN support
* Export tunnels to zip
* Quick tile support for VPN toggling
* Static shortcuts support for primary tunnel for automation integration
* Intent automation support for all tunnels
* Automatic service restart after reboot
* Service will stay running in background after app has been closed
* Battery preservation measures
## Docs (WIP)
Basic documentation of the feature and behaviors of this app can be found [here](https://zaneschepke.com/wgtunnel-docs/overview.html).
The repository for these docs can be found [here](https://github.com/zaneschepke/wgtunnel-docs).
## Building
```
$ git clone https://github.com/zaneschepke/wgtunnel
$ cd wgtunnel
$ ./gradlew assembleRelease
```
</span>
Create a personal access token (classic) in GitHuv to be able to pull the wireguard-android github dependencies from GitHub packages
as documented [here](https://docs.github.com/en/packages/learn-github-packages/introduction-to-github-packages#authenticating-to-github-packages).
Alternatively, you can clone [wireguard-android](https://github.com/zaneschepke/wireguard-android) and run the following command to publish the dependency to your local maven repository (requires you have maven installed). This is the ideal approach
if you intent to make changes to this lib.
```
$ git clone https://github.com/zaneschepke/wireguard-android
$ cd wireguard-android
$ brew install maven
$ ./gradlew publishToMavenLocal
```
The [wireguard-android](https://github.com/zaneschepke/wireguard-android) dependency is a fork of the official [wireguard-android](https://github.com/WireGuard/wireguard-android) library.
Add the following lines to local.properties file:
```
GH_USER=<your github username>
GH_TOKEN=<the personal access token with read package permission you just created>
```
And then build the app:
```
$ ./gradlew assembleDebug
```
</span>
+164 -87
View File
@@ -1,132 +1,209 @@
val rExtra = rootProject.extra
import java.util.Properties
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
kotlin("kapt")
id("com.google.dagger.hilt.android")
id("io.objectbox")
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
id("org.jetbrains.kotlin.plugin.serialization")
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.hilt.android)
alias(libs.plugins.kotlinxSerialization)
alias(libs.plugins.ksp)
}
android {
namespace = "com.zaneschepke.wireguardautotunnel"
compileSdk = 33
val versionMajor = 2
val versionMinor = 0
val versionPatch = 0
val versionBuild = 0
namespace = Constants.APP_ID
compileSdk = Constants.TARGET_SDK
defaultConfig {
applicationId = "com.zaneschepke.wireguardautotunnel"
minSdk = 29
targetSdk = 33
versionCode = versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild
versionName = "${versionMajor}.${versionMinor}.${versionPatch}"
applicationId = Constants.APP_ID
minSdk = Constants.MIN_SDK
targetSdk = Constants.TARGET_SDK
versionCode = Constants.VERSION_CODE
versionName = Constants.VERSION_NAME
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
sourceSets {
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
}
resourceConfigurations.addAll(listOf("en"))
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
vectorDrawables { useSupportLibrary = true }
}
signingConfigs {
create(Constants.RELEASE) {
val properties =
Properties().apply {
// created local file for signing details
try {
load(file("signing.properties").reader())
} catch (_: Exception) {
load(file("signing_template.properties").reader())
}
}
// try to get secrets from env first for pipeline build, then properties file for local
// build
storeFile =
file(
System.getenv()
.getOrDefault(
Constants.KEY_STORE_PATH_VAR,
properties.getProperty(Constants.KEY_STORE_PATH_VAR),
),
)
storePassword =
System.getenv()
.getOrDefault(
Constants.STORE_PASS_VAR,
properties.getProperty(Constants.STORE_PASS_VAR),
)
keyAlias =
System.getenv()
.getOrDefault(
Constants.KEY_ALIAS_VAR,
properties.getProperty(Constants.KEY_ALIAS_VAR),
)
keyPassword =
System.getenv()
.getOrDefault(
Constants.KEY_PASS_VAR,
properties.getProperty(Constants.KEY_PASS_VAR),
)
}
}
buildTypes {
// don't strip
packaging.jniLibs.keepDebugSymbols.addAll(
listOf("libwg-go.so", "libwg-quick.so", "libwg.so"),
)
applicationVariants.all {
val variant = this
variant.outputs
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
.forEach { output ->
val outputFileName =
"${Constants.APP_NAME}-${variant.flavorName}-${variant.buildType.name}-${variant.versionName}.apk"
output.outputFileName = outputFileName
}
}
release {
isDebuggable = false
isMinifyEnabled = false
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
"proguard-rules.pro",
)
signingConfig = signingConfigs.getByName(Constants.RELEASE)
}
debug { isDebuggable = true }
}
flavorDimensions.add(Constants.TYPE)
productFlavors {
create("fdroid") {
dimension = Constants.TYPE
proguardFile("fdroid-rules.pro")
}
create("general") {
dimension = Constants.TYPE
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle)) {
//any plugins general specific
}
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
jvmTarget = "17"
}
kotlinOptions { jvmTarget = Constants.JVM_TARGET }
buildFeatures {
compose = true
buildConfig = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.7"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
composeOptions { kotlinCompilerExtensionVersion = Constants.COMPOSE_COMPILER_EXTENSION_VERSION }
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
}
val generalImplementation by configurations
dependencies {
implementation("androidx.core:core-ktx:1.10.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
implementation("androidx.activity:activity-compose:1.7.2")
implementation(platform("androidx.compose:compose-bom:2023.03.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3:1.1.1")
implementation("androidx.appcompat:appcompat:1.6.1")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
implementation(project(":logcatter"))
//wireguard tunnel
implementation("com.wireguard.android:tunnel:1.0.20230405")
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
// helpers for implementing LifecycleOwner in a Service
implementation(libs.androidx.lifecycle.service)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.appcompat)
// 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)
// get tunnel lib from github packages or mavenLocal
implementation(libs.tunnel)
coreLibraryDesugaring(libs.desugar.jdk.libs)
// logging
implementation(libs.timber)
//logging
implementation("com.jakewharton.timber:timber:5.0.1")
// compose navigation
implementation("androidx.navigation:navigation-compose:2.6.0")
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.hilt.navigation.compose)
// hilt
implementation("com.google.dagger:hilt-android:${rExtra.get("hiltVersion")}")
kapt("com.google.dagger:hilt-android-compiler:${rExtra.get("hiltVersion")}")
implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler)
//accompanist
implementation("com.google.accompanist:accompanist-systemuicontroller:${rExtra.get("accompanistVersion")}")
implementation("com.google.accompanist:accompanist-permissions:${rExtra.get("accompanistVersion")}")
implementation("com.google.accompanist:accompanist-flowlayout:${rExtra.get("accompanistVersion")}")
implementation("com.google.accompanist:accompanist-navigation-animation:${rExtra.get("accompanistVersion")}")
implementation("com.google.accompanist:accompanist-drawablepainter:${rExtra.get("accompanistVersion")}")
// accompanist
implementation(libs.accompanist.permissions)
implementation(libs.accompanist.flowlayout)
implementation(libs.accompanist.drawablepainter)
//db
implementation("io.objectbox:objectbox-kotlin:${rExtra.get("objectBoxVersion")}")
// storage
implementation(libs.androidx.room.runtime)
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.ktx)
implementation(libs.androidx.datastore.preferences)
//lifecycle
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1")
// lifecycle
implementation(libs.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.process)
//icons
implementation("androidx.compose.material:material-icons-extended:1.4.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
//firebase crashlytics
implementation(platform("com.google.firebase:firebase-bom:32.0.0"))
implementation("com.google.firebase:firebase-crashlytics-ktx")
implementation("com.google.firebase:firebase-analytics-ktx")
//barcode scanning
implementation("com.google.android.gms:play-services-code-scanner:16.0.0")
// icons
implementation(libs.material.icons.extended)
// serialization
implementation(libs.kotlinx.serialization.json)
// barcode scanning
implementation(libs.zxing.android.embedded)
implementation(libs.zxing.core)
// bio
implementation(libs.androidx.biometric.ktx)
implementation(libs.pin.lock.compose)
// shortcuts
implementation(libs.androidx.core)
implementation(libs.androidx.core.google.shortcuts)
}
kapt {
correctErrorTypes = true
}
+5
View File
@@ -0,0 +1,5 @@
-dontwarn com.google.errorprone.annotations.**
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
<fields>;
}
-39
View File
@@ -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"
}
-94
View File
@@ -1,94 +0,0 @@
{
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
"entities": [
{
"id": "1:2692736974585027589",
"lastPropertyId": "15:5057486545428188436",
"name": "TunnelConfig",
"properties": [
{
"id": "1:1985347930017457084",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "12:2409068226744965585",
"name": "name",
"indexId": "1:4811206443952699137",
"type": 9,
"flags": 34848
},
{
"id": "13:8987443291286312275",
"name": "wgQuick",
"type": 9
}
],
"relations": []
},
{
"id": "2:8887605597748372702",
"lastPropertyId": "8:4981008812459251156",
"name": "Settings",
"properties": [
{
"id": "1:7485739868216068651",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:5814013113141456749",
"name": "isAutoTunnelEnabled",
"type": 1
},
{
"id": "4:5645665441196906014",
"name": "trustedNetworkSSIDs",
"type": 30
},
{
"id": "5:4989886999117763881",
"name": "isTunnelOnMobileDataEnabled",
"type": 1
},
{
"id": "6:3370284381040192129",
"name": "defaultTunnel",
"type": 9
}
],
"relations": []
}
],
"lastEntityId": "2:8887605597748372702",
"lastIndexId": "1:4811206443952699137",
"lastRelationId": "0:0",
"lastSequenceId": "0:0",
"modelVersion": 5,
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [
1763475292291320186,
6483820955437198310,
8323071516033820771,
5904440563612311217,
1408037976996390989,
7737847485212546994,
8215616901775229364,
8021610768066328637,
6174306582797008721,
2175939938544485767,
7555225587864607050,
969146862000617878,
5057486545428188436,
2814640993034665120,
4981008812459251156
],
"retiredRelationUids": [],
"version": 1
}
-98
View File
@@ -1,98 +0,0 @@
{
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
"entities": [
{
"id": "1:2692736974585027589",
"lastPropertyId": "15:5057486545428188436",
"name": "TunnelConfig",
"properties": [
{
"id": "1:1985347930017457084",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "12:2409068226744965585",
"name": "name",
"indexId": "1:4811206443952699137",
"type": 9,
"flags": 34848
},
{
"id": "13:8987443291286312275",
"name": "wgQuick",
"type": 9
}
],
"relations": []
},
{
"id": "2:8887605597748372702",
"lastPropertyId": "8:4981008812459251156",
"name": "Settings",
"properties": [
{
"id": "1:7485739868216068651",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:5814013113141456749",
"name": "isAutoTunnelEnabled",
"type": 1
},
{
"id": "4:5645665441196906014",
"name": "trustedNetworkSSIDs",
"type": 30
},
{
"id": "5:4989886999117763881",
"name": "isTunnelOnMobileDataEnabled",
"type": 1
},
{
"id": "6:3370284381040192129",
"name": "defaultTunnel",
"type": 9
},
{
"id": "8:4981008812459251156",
"name": "showProminentDisclosure",
"type": 1
}
],
"relations": []
}
],
"lastEntityId": "2:8887605597748372702",
"lastIndexId": "1:4811206443952699137",
"lastRelationId": "0:0",
"lastSequenceId": "0:0",
"modelVersion": 5,
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [
1763475292291320186,
6483820955437198310,
8323071516033820771,
5904440563612311217,
1408037976996390989,
7737847485212546994,
8215616901775229364,
8021610768066328637,
6174306582797008721,
2175939938544485767,
7555225587864607050,
969146862000617878,
5057486545428188436,
2814640993034665120
],
"retiredRelationUids": [],
"version": 1
}
+6 -1
View File
@@ -18,4 +18,9 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#-renamesourcefileattribute SourceFile
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
<fields>;
}
@@ -0,0 +1,112 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "ba86153e6fb0b823197b987239b03e64",
"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)",
"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
}
],
"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, 'ba86153e6fb0b823197b987239b03e64')"
]
}
}
@@ -0,0 +1,126 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "65b1c9efff61712231fa64d1f19f3915",
"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)",
"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"
}
],
"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, '65b1c9efff61712231fa64d1f19f3915')"
]
}
}
@@ -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')"
]
}
}
+4
View File
@@ -0,0 +1,4 @@
SIGNING_STORE_PASSWORD=
SIGNING_KEY_ALIAS=
SIGNING_KEY_PASSWORD=
KEY_STORE_PATH=/
@@ -1,13 +1,11 @@
package com.zaneschepke.wireguardautotunnel
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
@@ -21,4 +19,4 @@ class ExampleInstrumentedTest {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.zaneschepke.wireguardautotunnel", appContext.packageName)
}
}
}
@@ -0,0 +1,44 @@
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 org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.io.IOException
@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 migrate4To5() {
helper.createDatabase(dbName, 4).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(
"INSERT INTO TunnelConfig (name, wg_quick)" + " VALUES ('hello', 'hello')",
)
// 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, 5, true)
// MigrationTestHelper automatically verifies the schema changes,
// but you need to validate that the data was migrated properly.
}
}
+105 -20
View File
@@ -1,38 +1,66 @@
<?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"
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
android:maxSdkVersion="30" />
<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.ACCESS_BACKGROUND_LOCATION"/>
<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" />
<!--foreground service exempt android 14-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<!--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"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!--android tv support-->
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<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" />
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
</intent>
</queries>
<application
android:allowBackup="true"
android:name=".WireGuardAutoTunnel"
android:allowBackup="true"
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:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.WireguardAutoTunnel"
tools:targetApi="31">
tools:targetApi="tiramisu">
<activity
android:name=".ui.MainActivity"
android:exported="true"
@@ -41,32 +69,89 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity
android:name=".ui.CaptureActivityPortrait"
android:screenOrientation="fullSensor"
android:stateNotNeeded="true"
android:theme="@style/zxing_CaptureTheme"
android:windowSoftInputMode="stateAlwaysHidden"
tools:ignore="DiscouragedApi" />
<activity
android:name=".service.shortcut.ShortcutsActivity"
android:enabled="true"
android:exported="true"
android:finishOnTaskLaunch="true"
android:theme="@android:style/Theme.NoDisplay" />
<service
android:name=".service.foreground.ForegroundService"
android:enabled="true"
android:exported="false">
android:exported="false"
android:foregroundServiceType="systemExempted"
tools:node="merge" />
<service
android:name=".service.tile.TunnelControlTile"
android:exported="true"
android:icon="@drawable/ic_launcher"
android:label="WG 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.WireGuardTunnelService"
android:enabled="true"
android:exported="false">
android:exported="false"
android:foregroundServiceType="systemExempted"
android:permission="android.permission.BIND_VPN_SERVICE"
android:persistent="true"
tools:node="merge">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
<meta-data
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
android:value="true" />
</service>
<service
android:name=".service.foreground.WireGuardConnectivityWatcherService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="systemExempted"
android:persistent="true"
android:stopWithTask="false"
tools:node="merge" />
<receiver
android:name=".receiver.BootReceiver"
android:enabled="true"
android:exported="false">
</service>
<receiver android:enabled="true" android:name=".BootReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="barcode_ui"/>
<receiver
android:name=".receiver.NotificationActionReceiver"
android:exported="false" />
</application>
</manifest>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 16 KiB

@@ -1,49 +0,0 @@
package com.zaneschepke.wireguardautotunnel
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class BootReceiver : BroadcastReceiver() {
@Inject
lateinit var settingsRepo : Repository<Settings>
@OptIn(DelicateCoroutinesApi::class)
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
GlobalScope.launch {
try {
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
val setting = settings[0]
if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
val defaultTunnel = TunnelConfig.from(setting.defaultTunnel!!)
ServiceTracker.actionOnService(
Action.START, context,
WireGuardConnectivityWatcherService::class.java,
mapOf(context.resources.getString(R.string.tunnel_extras_key) to
defaultTunnel.id.toString())
)
}
}
} finally {
cancel()
}
}
}
}
}
@@ -1,21 +1,37 @@
package com.zaneschepke.wireguardautotunnel
import android.app.Application
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.service.quicksettings.TileService
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
import javax.inject.Inject
import xyz.teamgravity.pin_lock_compose.PinManager
@HiltAndroidApp
class WireGuardAutoTunnel : Application() {
@Inject
lateinit var settingsRepo : Repository<Settings>
override fun onCreate() {
super.onCreate()
if(BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
settingsRepo.init()
instance = this
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree())
PinManager.initialize(this)
}
}
companion object {
lateinit var instance: WireGuardAutoTunnel
private set
fun isRunningOnAndroidTv(): Boolean {
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
}
fun requestTileServiceStateUpdate(context : Context) {
TileService.requestListeningState(
context,
ComponentName(instance, TunnelControlTile::class.java),
)
}
}
}
@@ -0,0 +1,37 @@
package com.zaneschepke.wireguardautotunnel.data
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
@Database(
entities = [Settings::class, TunnelConfig::class],
version = 6,
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,
),
],
exportSchema = true,
)
@TypeConverters(DatabaseListConverters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun settingDao(): SettingsDao
abstract fun tunnelConfigDoa(): TunnelConfigDao
}
@@ -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 {
// Notice non-ui thread is here
beginTransaction()
try {
execSQL(Queries.createDefaultSettings())
Timber.i("Bootstrapping settings data")
setTransactionSuccessful()
} catch (e : Exception) {
Timber.e(e)
} finally {
endTransaction()
}
}
}
@@ -0,0 +1,24 @@
package com.zaneschepke.wireguardautotunnel.data
import androidx.room.TypeConverter
import kotlinx.serialization.encodeToString
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.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,33 @@
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,
default_tunnel,
is_always_on_vpn_enabled,
is_tunnel_on_ethernet_enabled,
is_shortcuts_enabled,
is_battery_saver_enabled,
is_tunnel_on_wifi_enabled,
is_kernel_enabled,
is_restore_on_boot_enabled,
is_multi_tunnel_enabled)
VALUES
('false',
'false',
'sampleSSID1,sampleSSID2',
NULL,
'false',
'false',
'false',
'false',
'false',
'false',
'false',
'false')
""".trimIndent()
}
}
@@ -0,0 +1,28 @@
package com.zaneschepke.wireguardautotunnel.data
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,26 @@
package com.zaneschepke.wireguardautotunnel.data
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 kotlinx.coroutines.flow.Flow
@Dao
interface TunnelConfigDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: TunnelConfig)
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<TunnelConfig>)
@Query("SELECT * FROM TunnelConfig WHERE id=:id") suspend fun getById(id: Long): TunnelConfig?
@Query("SELECT * FROM TunnelConfig") suspend fun getAll(): List<TunnelConfig>
@Delete suspend fun delete(t: TunnelConfig)
@Query("SELECT COUNT('id') FROM TunnelConfig") suspend fun count(): Long
@Query("SELECT * FROM tunnelconfig") fun getAllFlow(): Flow<MutableList<TunnelConfig>>
}
@@ -0,0 +1,43 @@
package com.zaneschepke.wireguardautotunnel.data.datastore
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.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
class DataStoreManager(private val context: Context) {
companion object {
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
}
// preferences
private val preferencesKey = "preferences"
private val Context.dataStore by
preferencesDataStore(
name = preferencesKey,
)
suspend fun init() {
context.dataStore.data.first()
}
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) =
context.dataStore.edit { it[key] = value }
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
suspend fun <T> getFromStore(key: Preferences.Key<T>) =
context.dataStore.data.map{ it[key] }.first()
fun <T> getFromStoreBlocking(key: Preferences.Key<T>) = runBlocking {
context.dataStore.data.map{ it[key] }.first()
}
val preferencesFlow: Flow<Preferences?> = context.dataStore.data
}
@@ -0,0 +1,68 @@
package com.zaneschepke.wireguardautotunnel.data.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Settings(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "is_tunnel_enabled") var isAutoTunnelEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled")
var isTunnelOnMobileDataEnabled: Boolean = false,
@ColumnInfo(name = "trusted_network_ssids")
var trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
@ColumnInfo(name = "default_tunnel") var defaultTunnel: String? = null,
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled")
var isTunnelOnEthernetEnabled: Boolean = false,
@ColumnInfo(
name = "is_shortcuts_enabled",
defaultValue = "false",
)
var isShortcutsEnabled: Boolean = false,
@ColumnInfo(
name = "is_battery_saver_enabled",
defaultValue = "false",
)
var isBatterySaverEnabled: Boolean = false,
@ColumnInfo(
name = "is_tunnel_on_wifi_enabled",
defaultValue = "false",
)
var isTunnelOnWifiEnabled: Boolean = false,
@ColumnInfo(
name = "is_kernel_enabled",
defaultValue = "false",
)
var isKernelEnabled: Boolean = false,
@ColumnInfo(
name = "is_restore_on_boot_enabled",
defaultValue = "false",
)
var isRestoreOnBootEnabled: Boolean = false,
@ColumnInfo(
name = "is_multi_tunnel_enabled",
defaultValue = "false",
)
var isMultiTunnelEnabled: Boolean = false,
@ColumnInfo(
name = "is_auto_tunnel_paused",
defaultValue = "false",
)
var isAutoTunnelPaused: Boolean = false,
@ColumnInfo(
name = "is_ping_enabled",
defaultValue = "false",
)
var isPingEnabled: Boolean = false,
) {
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
return if (defaultTunnel != null) {
val defaultConfig = TunnelConfig.from(defaultTunnel!!)
(tunnelConfig.id == defaultConfig.id)
} else {
false
}
}
}
@@ -0,0 +1,34 @@
package com.zaneschepke.wireguardautotunnel.data.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import com.wireguard.config.Config
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.io.InputStream
@Entity(indices = [Index(value = ["name"], unique = true)])
@Serializable
data class TunnelConfig(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "name") var name: String,
@ColumnInfo(name = "wg_quick") var wgQuick: String
) {
override fun toString(): String {
return Json.encodeToString(serializer(), this)
}
companion object {
fun from(string: String): TunnelConfig {
return Json.decodeFromString<TunnelConfig>(string)
}
fun configFromQuick(wgQuick: String): Config {
val inputStream: InputStream = wgQuick.byteInputStream()
val reader = inputStream.bufferedReader(Charsets.UTF_8)
return Config.parse(reader)
}
}
}
@@ -0,0 +1,14 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import kotlinx.coroutines.flow.Flow
interface SettingsRepository {
suspend fun save(settings: Settings)
fun getSettingsFlow(): Flow<Settings>
suspend fun getSettings(): Settings
suspend fun getAll(): List<Settings>
}
@@ -0,0 +1,24 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import kotlinx.coroutines.flow.Flow
class SettingsRepositoryImpl(private val settingsDoa: SettingsDao) : SettingsRepository {
override suspend fun save(settings: Settings) {
settingsDoa.save(settings)
}
override fun getSettingsFlow(): Flow<Settings> {
return settingsDoa.getSettingsFlow()
}
override suspend fun getSettings(): Settings {
return settingsDoa.getAll().firstOrNull() ?: Settings()
}
override suspend fun getAll(): List<Settings> {
return settingsDoa.getAll()
}
}
@@ -0,0 +1,18 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
import kotlinx.coroutines.flow.Flow
interface TunnelConfigRepository {
fun getTunnelConfigsFlow(): Flow<TunnelConfigs>
suspend fun getAll(): TunnelConfigs
suspend fun save(tunnelConfig: TunnelConfig)
suspend fun delete(tunnelConfig: TunnelConfig)
suspend fun count(): Int
}
@@ -0,0 +1,29 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
import kotlinx.coroutines.flow.Flow
class TunnelConfigRepositoryImpl(private val tunnelConfigDao: TunnelConfigDao) :
TunnelConfigRepository {
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
return tunnelConfigDao.getAllFlow()
}
override suspend fun getAll(): TunnelConfigs {
return tunnelConfigDao.getAll()
}
override suspend fun save(tunnelConfig: TunnelConfig) {
tunnelConfigDao.save(tunnelConfig)
}
override suspend fun delete(tunnelConfig: TunnelConfig) {
tunnelConfigDao.delete(tunnelConfig)
}
override suspend fun count(): Int {
return tunnelConfigDao.count().toInt()
}
}
@@ -1,40 +0,0 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.MyObjectBox
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import io.objectbox.Box
import io.objectbox.BoxStore
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class BoxModule {
@Provides
@Singleton
fun provideBoxStore(@ApplicationContext context : Context) : BoxStore {
return MyObjectBox.builder()
.androidContext(context.applicationContext)
.build()
}
@Provides
@Singleton
fun provideBoxForSettings(store : BoxStore) : Box<Settings> {
return store.boxFor(Settings::class.java)
}
@Provides
@Singleton
fun provideBoxForTunnels(store : BoxStore) : Box<TunnelConfig> {
return store.boxFor(TunnelConfig::class.java)
}
}
@@ -0,0 +1,30 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import androidx.room.Room
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
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
@Module
@InstallIn(SingletonComponent::class)
class DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
context.getString(R.string.db_name),
)
.fallbackToDestructiveMigration()
.addCallback(DatabaseCallback())
.build()
}
}
@@ -0,0 +1,5 @@
package com.zaneschepke.wireguardautotunnel.module
import javax.inject.Qualifier
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Kernel
@@ -1,25 +1,51 @@
package com.zaneschepke.wireguardautotunnel.module
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.repository.SettingsBox
import com.zaneschepke.wireguardautotunnel.repository.TunnelBox
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import dagger.Binds
import android.content.Context
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepositoryImpl
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepositoryImpl
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
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
class RepositoryModule {
@Singleton
abstract fun provideSettingsRepository(settingsBox: SettingsBox) : Repository<Settings>
@Provides
fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao {
return appDatabase.settingDao()
}
@Binds
@Singleton
abstract fun provideTunnelRepository(tunnelBox: TunnelBox) : Repository<TunnelConfig>
}
@Provides
fun provideTunnelConfigDoa(appDatabase: AppDatabase): TunnelConfigDao {
return appDatabase.tunnelConfigDoa()
}
@Singleton
@Provides
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao): TunnelConfigRepository {
return TunnelConfigRepositoryImpl(tunnelConfigDao)
}
@Singleton
@Provides
fun provideSettingsRepository(settingsDao: SettingsDao): SettingsRepository {
return SettingsRepositoryImpl(settingsDao)
}
@Singleton
@Provides
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
return DataStoreManager(context)
}
}
@@ -1,41 +0,0 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.codescanner.GmsBarcodeScanner
import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions
import com.google.mlkit.vision.codescanner.GmsBarcodeScanning
import com.zaneschepke.wireguardautotunnel.service.barcode.CodeScanner
import com.zaneschepke.wireguardautotunnel.service.barcode.QRScanner
import dagger.Binds
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
@Module
@InstallIn(ViewModelComponent::class)
class ScannerModule {
@ViewModelScoped
@Provides
fun provideBarCodeOptions() : GmsBarcodeScannerOptions {
return GmsBarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build()
}
@ViewModelScoped
@Provides
fun provideBarCodeScanner(@ApplicationContext context: Context, options: GmsBarcodeScannerOptions) : GmsBarcodeScanner {
return GmsBarcodeScanning.getClient(context, options)
}
@ViewModelScoped
@Provides
fun provideQRScanner(gmsBarcodeScanner: GmsBarcodeScanner) : CodeScanner {
return QRScanner(gmsBarcodeScanner)
}
}
@@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.module
import com.zaneschepke.wireguardautotunnel.service.barcode.CodeScanner
import com.zaneschepke.wireguardautotunnel.service.barcode.QRScanner
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
@@ -12,21 +11,29 @@ import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ServiceComponent
import dagger.hilt.android.scopes.ServiceScoped
import dagger.hilt.android.scopes.ViewModelScoped
@Module
@InstallIn(ServiceComponent::class)
abstract class ServiceModule {
@Binds
@ServiceScoped
abstract fun provideNotificationService(
wireGuardNotification: WireGuardNotification
): NotificationService
@Binds
@ServiceScoped
abstract fun provideNotificationService(wireGuardNotification: WireGuardNotification) : NotificationService
abstract fun provideWifiService(wifiService: WifiService): NetworkService<WifiService>
@Binds
@ServiceScoped
abstract fun provideWifiService(wifiService: WifiService) : NetworkService<WifiService>
abstract fun provideMobileDataService(
mobileDataService: MobileDataService
): NetworkService<MobileDataService>
@Binds
@ServiceScoped
abstract fun provideMobileDataService(mobileDataService : MobileDataService) : NetworkService<MobileDataService>
}
abstract fun provideEthernetService(
ethernetService: EthernetService
): NetworkService<EthernetService>
}
@@ -3,6 +3,10 @@ package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.GoBackend
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
import dagger.Module
@@ -15,17 +19,33 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class TunnelModule {
@Provides
@Singleton
fun provideRootShell(@ApplicationContext context: Context): RootShell {
return RootShell(context)
}
@Provides
@Singleton
fun provideBackend(@ApplicationContext context : Context) : Backend {
@Userspace
fun provideUserspaceBackend(@ApplicationContext context: Context): Backend {
return GoBackend(context)
}
@Provides
@Singleton
fun provideVpnService(backend: Backend) : VpnService {
return WireGuardTunnel(backend)
@Kernel
fun provideKernelBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell))
}
}
@Provides
@Singleton
fun provideVpnService(
@Userspace userspaceBackend: Backend,
@Kernel kernelBackend: Backend,
settingsRepository: SettingsRepository
): VpnService {
return WireGuardTunnel(userspaceBackend, kernelBackend, settingsRepository)
}
}
@@ -0,0 +1,5 @@
package com.zaneschepke.wireguardautotunnel.module
import javax.inject.Qualifier
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Userspace
@@ -0,0 +1,32 @@
package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.util.goAsync
import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class BootReceiver : BroadcastReceiver() {
@Inject lateinit var settingsRepository: SettingsRepository
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
override fun onReceive(context: Context?, intent: Intent?) = goAsync {
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
val settings = settingsRepository.getSettings()
if (settings.isAutoTunnelEnabled) {
Timber.i("Starting watcher service from boot")
ServiceManager.startWatcherServiceForeground(context!!)
} else if(settings.isAlwaysOnVpnEnabled) {
Timber.i("Starting tunnel from boot")
ServiceManager.startVpnServicePrimaryTunnel(context!!, settings, tunnelConfigRepository.getAll().firstOrNull())
}
}
}
@@ -0,0 +1,31 @@
package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.goAsync
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import javax.inject.Inject
@AndroidEntryPoint
class NotificationActionReceiver : BroadcastReceiver() {
@Inject lateinit var settingsRepository: SettingsRepository
override fun onReceive(context: Context, intent: Intent?) = goAsync {
try {
val settings = settingsRepository.getSettings()
if (settings.defaultTunnel != null) {
ServiceManager.stopVpnService(context)
delay(Constants.TOGGLE_TUNNEL_DELAY)
ServiceManager.startVpnServiceForeground(context, settings.defaultTunnel.toString())
}
} finally {
cancel()
}
}
}
@@ -1,16 +0,0 @@
package com.zaneschepke.wireguardautotunnel.repository
import kotlinx.coroutines.flow.Flow
interface Repository<T> {
suspend fun save(t : T)
suspend fun saveAll(t : List<T>)
suspend fun getById(id : Long) : T?
suspend fun getAll() : List<T>?
suspend fun delete(t : T) : Boolean?
suspend fun count() : Long?
val itemFlow : Flow<MutableList<T>>
fun init()
}
@@ -1,63 +0,0 @@
package com.zaneschepke.wireguardautotunnel.repository
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import io.objectbox.Box
import io.objectbox.BoxStore
import io.objectbox.kotlin.awaitCallInTx
import io.objectbox.kotlin.toFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import javax.inject.Inject
class SettingsBox @Inject constructor(private val box : Box<Settings>, private val boxStore : BoxStore) : Repository<Settings> {
@OptIn(ExperimentalCoroutinesApi::class)
override val itemFlow = box.query().build().subscribe().toFlow()
override fun init() {
CoroutineScope(Dispatchers.IO).launch {
if(getAll().isNullOrEmpty()) {
save(Settings())
}
}
}
override suspend fun save(t : Settings) {
boxStore.awaitCallInTx {
box.put(t)
}
}
override suspend fun saveAll(t : List<Settings>) {
boxStore.awaitCallInTx {
box.put(t)
}
}
override suspend fun getById(id: Long): Settings? {
return boxStore.awaitCallInTx {
box[id]
}
}
override suspend fun getAll(): List<Settings>? {
return boxStore.awaitCallInTx {
box.all
}
}
override suspend fun delete(t : Settings): Boolean? {
return boxStore.awaitCallInTx {
box.remove(t)
}
}
override suspend fun count() : Long? {
return boxStore.awaitCallInTx {
box.count()
}
}
}
@@ -1,57 +0,0 @@
package com.zaneschepke.wireguardautotunnel.repository
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import io.objectbox.Box
import io.objectbox.BoxStore
import io.objectbox.kotlin.awaitCallInTx
import io.objectbox.kotlin.toFlow
import kotlinx.coroutines.ExperimentalCoroutinesApi
import timber.log.Timber
import javax.inject.Inject
class TunnelBox @Inject constructor(private val box : Box<TunnelConfig>,private val boxStore : BoxStore) : Repository<TunnelConfig> {
@OptIn(ExperimentalCoroutinesApi::class)
override val itemFlow = box.query().build().subscribe().toFlow()
override fun init() {
}
override suspend fun save(t : TunnelConfig) {
Timber.d("Saving tunnel config")
boxStore.awaitCallInTx {
box.put(t)
}
}
override suspend fun saveAll(t : List<TunnelConfig>) {
boxStore.awaitCallInTx {
box.put(t)
}
}
override suspend fun getById(id: Long): TunnelConfig? {
return boxStore.awaitCallInTx {
box[id]
}
}
override suspend fun getAll(): List<TunnelConfig>? {
return boxStore.awaitCallInTx {
box.all
}
}
override suspend fun delete(t : TunnelConfig): Boolean? {
return boxStore.awaitCallInTx {
box.remove(t)
}
}
override suspend fun count() : Long? {
return boxStore.awaitCallInTx {
box.count()
}
}
}
@@ -1,7 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.barcode
import kotlinx.coroutines.flow.Flow
interface CodeScanner {
fun scan() : Flow<String?>
}
@@ -1,22 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.barcode
import com.google.mlkit.vision.codescanner.GmsBarcodeScanner
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import timber.log.Timber
import javax.inject.Inject
class QRScanner @Inject constructor(private val gmsBarcodeScanner: GmsBarcodeScanner) : CodeScanner {
override fun scan(): Flow<String?> {
return callbackFlow {
gmsBarcodeScanner.startScan().addOnSuccessListener {
trySend(it.rawValue)
}.addOnFailureListener {
Timber.e(it.message)
}
awaitClose {
}
}
}
}
@@ -2,5 +2,6 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
enum class Action {
START,
START_FOREGROUND,
STOP
}
}
@@ -1,60 +1,63 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Service
import android.content.Intent
import android.os.Bundle
import android.os.IBinder
import androidx.lifecycle.LifecycleService
import timber.log.Timber
open class ForegroundService : Service() {
open class ForegroundService : LifecycleService() {
private var isServiceStarted = false
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
// We don't provide binding, so return null
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
Timber.d("onStartCommand executed with startId: $startId")
if (intent != null) {
val action = intent.action
Timber.d("using an intent with action $action")
when (action) {
Action.START.name -> startService(intent.extras)
Action.START.name,
Action.START_FOREGROUND.name -> startService(intent.extras)
Action.STOP.name -> stopService(intent.extras)
"android.net.VpnService" -> {
Timber.d("Always-on VPN starting service")
startService(intent.extras)
}
else -> Timber.d("This should never happen. No action in the received intent")
}
} else {
Timber.d(
"with a null intent. It has been probably restarted by the system."
"with a null intent. It has been probably restarted by the system.",
)
}
// by returning this we make sure the service is restarted if the system kills the service
return START_STICKY
}
override fun onDestroy() {
super.onDestroy()
Timber.d("The service has been destroyed")
}
protected open fun startService(extras : Bundle?) {
protected open fun startService(extras: Bundle?) {
if (isServiceStarted) return
Timber.d("Starting ${this.javaClass.simpleName}")
isServiceStarted = true
}
protected open fun stopService(extras : Bundle?) {
protected open fun stopService(extras: Bundle?) {
Timber.d("Stopping ${this.javaClass.simpleName}")
try {
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
} catch (e: Exception) {
Timber.d("Service stopped without being started: ${e.message}")
Timber.e(e)
}
isServiceStarted = false
}
}
}
@@ -0,0 +1,116 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Service
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import timber.log.Timber
object ServiceManager {
// private
// fun <T> Context.isServiceRunning(service: Class<T>) =
// (getSystemService(ACTIVITY_SERVICE) as ActivityManager)
// .runningAppProcesses.any {
// it.processName == service.name
// }
//
// fun <T : Service> getServiceState(
// context: Context,
// cls: Class<T>
// ): ServiceState {
// val isServiceRunning = context.isServiceRunning(cls)
// return if (isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
// }
private fun <T : Service> actionOnService(
action: Action,
context: Context,
cls: Class<T>,
extras: Map<String, String>? = null
) {
val intent =
Intent(context, cls).also {
it.action = action.name
extras?.forEach { (k, v) -> it.putExtra(k, v) }
}
intent.component?.javaClass
try {
when (action) {
Action.START_FOREGROUND -> {
context.startForegroundService(intent)
}
Action.START -> {
context.startService(intent)
}
Action.STOP -> context.startService(intent)
}
} catch (e: Exception) {
Timber.e(e.message)
}
}
fun startVpnService(context: Context, tunnelConfig: String) {
actionOnService(
Action.START,
context,
WireGuardTunnelService::class.java,
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig),
)
}
fun stopVpnService(context: Context) {
Timber.d("Stopping vpn service action")
actionOnService(
Action.STOP,
context,
WireGuardTunnelService::class.java,
)
}
fun startVpnServiceForeground(context: Context, tunnelConfig: String) {
actionOnService(
Action.START_FOREGROUND,
context,
WireGuardTunnelService::class.java,
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig),
)
}
fun startVpnServicePrimaryTunnel(context: Context, settings: Settings, fallbackTunnel: TunnelConfig? = null) {
if(settings.defaultTunnel != null) {
return startVpnServiceForeground(context, settings.defaultTunnel!!)
}
if(fallbackTunnel != null) {
startVpnServiceForeground(context, fallbackTunnel.toString())
}
}
fun startWatcherServiceForeground(
context: Context,
) {
actionOnService(
Action.START_FOREGROUND,
context,
WireGuardConnectivityWatcherService::class.java,
)
}
fun startWatcherService(context: Context) {
actionOnService(
Action.START,
context,
WireGuardConnectivityWatcherService::class.java,
)
}
fun stopWatcherService(context: Context) {
actionOnService(
Action.STOP,
context,
WireGuardConnectivityWatcherService::class.java,
)
}
}
@@ -3,4 +3,4 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
enum class ServiceState {
STARTED,
STOPPED,
}
}
@@ -1,48 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.ActivityManager
import android.app.Application
import android.app.Service
import android.content.Context
import android.content.Context.ACTIVITY_SERVICE
import android.content.Intent
import android.content.SharedPreferences
import com.zaneschepke.wireguardautotunnel.R
object ServiceTracker {
@Suppress("DEPRECATION")
private // Deprecated for third party Services.
fun <T> Context.isServiceRunning(service: Class<T>) =
(getSystemService(ACTIVITY_SERVICE) as ActivityManager)
.getRunningServices(Integer.MAX_VALUE)
.any { it.service.className == service.name }
fun <T : Service> getServiceState(context: Context, cls : Class<T>): ServiceState {
val isServiceRunning = context.isServiceRunning(cls)
return if(isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
}
fun <T : Service> actionOnService(action: Action, application: Application, cls : Class<T>, extras : Map<String,String>? = null) {
if (getServiceState(application, cls) == ServiceState.STOPPED && action == Action.STOP) return
val intent = Intent(application, cls).also {
it.action = action.name
extras?.forEach {(k, v) ->
it.putExtra(k, v)
}
}
intent.component?.javaClass
application.startService(intent)
}
fun <T : Service> actionOnService(action: Action, context: Context, cls : Class<T>, extras : Map<String,String>? = null) {
if (getServiceState(context, cls) == ServiceState.STOPPED && action == Action.STOP) return
val intent = Intent(context, cls).also {
it.action = action.name
extras?.forEach {(k, v) ->
it.putExtra(k, v)
}
}
intent.component?.javaClass
context.startService(intent)
}
}
@@ -1,78 +1,88 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.AlarmManager
import android.app.Application
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.PowerManager
import android.os.SystemClock
import androidx.core.app.ServiceCompat
import androidx.lifecycle.lifecycleScope
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import timber.log.Timber
import java.net.InetAddress
import javax.inject.Inject
@AndroidEntryPoint
class WireGuardConnectivityWatcherService : ForegroundService() {
private val foregroundId = 122
private val foregroundId = 122;
@Inject lateinit var wifiService: NetworkService<WifiService>
@Inject
lateinit var wifiService : NetworkService<WifiService>
@Inject lateinit var mobileDataService: NetworkService<MobileDataService>
@Inject
lateinit var mobileDataService : NetworkService<MobileDataService>
@Inject lateinit var ethernetService: NetworkService<EthernetService>
@Inject
lateinit var settingsRepo: Repository<Settings>
@Inject lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var notificationService : NotificationService
@Inject lateinit var notificationService: NotificationService
@Inject
lateinit var vpnService : VpnService
@Inject lateinit var vpnService: VpnService
private lateinit var watcherJob : Job;
private lateinit var setting : Settings
private lateinit var tunnelId: String
private val networkEventsFlow = MutableStateFlow(WatcherState())
private var connecting = false
private var disconnecting = false
private var isWifiConnected = false
private var isMobileDataConnected = false
data class WatcherState(
val isWifiConnected: Boolean = false,
val isVpnConnected: Boolean = false,
val isEthernetConnected: Boolean = false,
val isMobileDataConnected: Boolean = false,
val currentNetworkSSID: String = "",
val settings: Settings = Settings()
)
private lateinit var watcherJob: Job
private var wakeLock: PowerManager.WakeLock? = null
private val tag = this.javaClass.name;
private val tag = this.javaClass.name
override fun onCreate() {
super.onCreate()
lifecycleScope.launch(Dispatchers.Main) {
try {
if (settingsRepository.getSettings().isAutoTunnelPaused) {
launchWatcherPausedNotification()
} else launchWatcherNotification()
} catch (e: Exception) {
Timber.e("Failed to start watcher service, not enough permissions")
}
}
}
override fun startService(extras: Bundle?) {
super.startService(extras)
val tunnelId = extras?.getString(getString(R.string.tunnel_extras_key))
if (tunnelId != null) {
this.tunnelId = tunnelId
}
// we need this lock so our service gets not affected by Doze Mode
initWakeLock()
cancelWatcherJob()
launchWatcherNotification()
if(this::tunnelId.isInitialized) {
try {
// we need this lock so our service gets not affected by Doze Mode
lifecycleScope.launch { initWakeLock() }
cancelWatcherJob()
startWatcherJob()
} else {
stopService(extras)
} catch (e: Exception) {
Timber.e("Failed to launch watcher service, no permissions")
}
}
@@ -84,82 +94,211 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
}
}
cancelWatcherJob()
stopVPN()
stopSelf()
}
private fun launchWatcherNotification() {
val notification = notificationService.createNotification(
channelId = getString(R.string.watcher_channel_id),
channelName = getString(R.string.watcher_channel_name),
description = getString(R.string.watcher_notification_text))
super.startForeground(foregroundId, notification)
private fun launchWatcherNotification(
description: String = getString(R.string.watcher_notification_text_active)
) {
val notification =
notificationService.createNotification(
channelId = getString(R.string.watcher_channel_id),
channelName = getString(R.string.watcher_channel_name),
title = getString(R.string.auto_tunnel_title),
description = description,
)
ServiceCompat.startForeground(
this,
foregroundId,
notification,
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
}
//try to start task again if killed
override fun onTaskRemoved(rootIntent: Intent) {
Timber.d("Task Removed called")
val restartServiceIntent = Intent(applicationContext, this::class.java).also {
it.setPackage(packageName)
};
val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE);
applicationContext.getSystemService(Context.ALARM_SERVICE);
val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager;
alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent);
private fun launchWatcherPausedNotification() {
launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
}
private fun initWakeLock() {
wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
acquire()
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
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 cancelWatcherJob() {
if(this::watcherJob.isInitialized) {
if (this::watcherJob.isInitialized) {
watcherJob.cancel()
}
}
@OptIn(DelicateCoroutinesApi::class)
private fun startWatcherJob() {
watcherJob = GlobalScope.launch {
val settings = settingsRepo.getAll();
if(!settings.isNullOrEmpty()) {
setting = settings[0]
watcherJob =
lifecycleScope.launch(Dispatchers.IO) {
val setting = settingsRepository.getSettings()
launch {
Timber.i("Starting wifi watcher")
watchForWifiConnectivityChanges()
}
if (setting.isTunnelOnMobileDataEnabled) {
launch {
Timber.i("Starting mobile data watcher")
watchForMobileDataConnectivityChanges()
}
}
if (setting.isTunnelOnEthernetEnabled) {
launch {
Timber.i("Starting ethernet data watcher")
watchForEthernetConnectivityChanges()
}
}
launch {
Timber.i("Starting vpn state watcher")
watchForVpnConnectivityChanges()
}
launch {
Timber.i("Starting settings watcher")
watchForSettingsChanges()
}
if(setting.isPingEnabled) {
launch {
Timber.i("Starting ping watcher")
watchForPingFailure()
}
}
launch {
Timber.i("Starting management watcher")
manageVpn()
}
}
GlobalScope.launch {
watchForWifiConnectivityChanges()
}
if(setting.isTunnelOnMobileDataEnabled) {
GlobalScope.launch {
watchForMobileDataConnectivityChanges()
}
private suspend fun watchForMobileDataConnectivityChanges() {
mobileDataService.networkStatus.collect {
when (it) {
is NetworkStatus.Available -> {
Timber.i("Gained Mobile data connection")
networkEventsFlow.value =
networkEventsFlow.value.copy(
isMobileDataConnected = true,
)
}
is NetworkStatus.CapabilitiesChanged -> {
networkEventsFlow.value =
networkEventsFlow.value.copy(
isMobileDataConnected = true,
)
Timber.i("Mobile data capabilities changed")
}
is NetworkStatus.Unavailable -> {
networkEventsFlow.value =
networkEventsFlow.value.copy(
isMobileDataConnected = false,
)
Timber.i("Lost mobile data connection")
}
}
}
}
private suspend fun watchForMobileDataConnectivityChanges() {
mobileDataService.networkStatus.collect {
when(it) {
private suspend fun watchForPingFailure() {
try {
do {
if(vpnService.vpnState.value.status == Tunnel.State.UP) {
val config = vpnService.vpnState.value.config
config?.let {
val results = it.peers.map { peer ->
val host = if(peer.endpoint.isPresent &&
peer.endpoint.get().resolved.isPresent)
peer.endpoint.get().resolved.get().host
else Constants.BACKUP_PING_HOST
Timber.i("Checking reachability of: $host")
val reachable = InetAddress.getByName(host).isReachable(Constants.PING_TIMEOUT.toInt())
Timber.i("Result: reachable - $reachable")
reachable
}
if(results.contains(false)) {
Timber.i("Restarting VPN for ping failure")
ServiceManager.stopVpnService(this)
delay(Constants.VPN_RESTART_DELAY)
val tunnel = networkEventsFlow.value.settings.defaultTunnel
ServiceManager.startVpnServiceForeground(this, tunnel!!)
delay(Constants.PING_COOLDOWN)
}
}
}
delay(Constants.PING_INTERVAL)
} while (true)
} catch (e: Exception) {
Timber.e(e)
}
}
private suspend fun watchForSettingsChanges() {
settingsRepository.getSettingsFlow().collect {
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
when (it.isAutoTunnelPaused) {
true -> launchWatcherPausedNotification()
false -> launchWatcherNotification()
}
}
networkEventsFlow.value =
networkEventsFlow.value.copy(
settings = it,
)
}
}
private suspend fun watchForVpnConnectivityChanges() {
vpnService.vpnState.collect {
when (it.status) {
Tunnel.State.DOWN ->
networkEventsFlow.value =
networkEventsFlow.value.copy(
isVpnConnected = false,
)
Tunnel.State.UP ->
networkEventsFlow.value =
networkEventsFlow.value.copy(
isVpnConnected = true,
)
else -> {}
}
}
}
private suspend fun watchForEthernetConnectivityChanges() {
ethernetService.networkStatus.collect {
when (it) {
is NetworkStatus.Available -> {
Timber.d("Gained Mobile data connection")
isMobileDataConnected = true
Timber.i("Gained Ethernet connection")
networkEventsFlow.value =
networkEventsFlow.value.copy(
isEthernetConnected = true,
)
}
is NetworkStatus.CapabilitiesChanged -> {
isMobileDataConnected = true
Timber.d("Mobile data capabilities changed")
if(!isWifiConnected && setting.isTunnelOnMobileDataEnabled
&& vpnService.getState() == Tunnel.State.DOWN)
startVPN()
Timber.i("Ethernet capabilities changed")
networkEventsFlow.value =
networkEventsFlow.value.copy(
isEthernetConnected = true,
)
}
is NetworkStatus.Unavailable -> {
isMobileDataConnected = false
if(!isWifiConnected && vpnService.getState() == Tunnel.State.UP) stopVPN()
Timber.d("Lost mobile data connection")
networkEventsFlow.value =
networkEventsFlow.value.copy(
isEthernetConnected = false,
)
Timber.i("Lost Ethernet connection")
}
}
}
@@ -167,62 +306,99 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
private suspend fun watchForWifiConnectivityChanges() {
wifiService.networkStatus.collect {
when (it) {
is NetworkStatus.Available -> {
Timber.d("Gained Wi-Fi connection")
isWifiConnected = true
when (it) {
is NetworkStatus.Available -> {
Timber.i("Gained Wi-Fi connection")
networkEventsFlow.value =
networkEventsFlow.value.copy(
isWifiConnected = true,
)
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Wifi capabilities changed")
networkEventsFlow.value =
networkEventsFlow.value.copy(
isWifiConnected = true,
)
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
Timber.i("Detected SSID: $ssid")
networkEventsFlow.value =
networkEventsFlow.value.copy(
currentNetworkSSID = ssid,
)
}
is NetworkStatus.Unavailable -> {
networkEventsFlow.value =
networkEventsFlow.value.copy(
isWifiConnected = false,
)
Timber.i("Lost Wi-Fi connection")
}
}
}
}
private suspend fun manageVpn() {
networkEventsFlow.collectLatest {
val autoTunnel = "Auto-tunnel watcher"
if (!it.settings.isAutoTunnelPaused && it.settings.defaultTunnel != null) {
delay(Constants.TOGGLE_TUNNEL_DELAY)
when {
((it.isEthernetConnected &&
it.settings.isTunnelOnEthernetEnabled &&
!it.isVpnConnected)) -> {
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
Timber.i("$autoTunnel condition 1 met")
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.d("Wifi capabilities changed")
isWifiConnected = true
if (!connecting && !disconnecting) {
Timber.d("Not connect and not disconnecting")
val ssid = wifiService.getNetworkName(it.networkCapabilities);
Timber.d("SSID: $ssid")
if ((setting.trustedNetworkSSIDs?.contains(ssid) == false) && vpnService.getState() == Tunnel.State.DOWN) {
Timber.d("Starting VPN Tunnel for untrusted network: $ssid")
startVPN()
} else if (!disconnecting && vpnService.getState() == Tunnel.State.UP && setting.trustedNetworkSSIDs.contains(
ssid
)
) {
Timber.d("Stopping VPN Tunnel for trusted network with ssid: $ssid")
stopVPN()
}
}
(!it.isEthernetConnected &&
it.settings.isTunnelOnMobileDataEnabled &&
!it.isWifiConnected &&
it.isMobileDataConnected &&
!it.isVpnConnected) -> {
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
Timber.i("$autoTunnel condition 2 met")
}
is NetworkStatus.Unavailable -> {
isWifiConnected = false
Timber.d("Lost Wi-Fi connection")
if(setting.isTunnelOnMobileDataEnabled && vpnService.getState() == Tunnel.State.DOWN
&& isMobileDataConnected){
Timber.d("Wifi not available so starting vpn for mobile data")
startVPN()
}
(!it.isEthernetConnected &&
!it.settings.isTunnelOnMobileDataEnabled &&
!it.isWifiConnected &&
it.isVpnConnected) -> {
ServiceManager.stopVpnService(this)
Timber.i("$autoTunnel condition 3 met")
}
(!it.isEthernetConnected &&
it.isWifiConnected &&
!it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID) &&
it.settings.isTunnelOnWifiEnabled &&
(!it.isVpnConnected)) -> {
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
Timber.i("$autoTunnel condition 4 met")
}
(!it.isEthernetConnected &&
(it.isWifiConnected &&
it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) &&
(it.isVpnConnected)) -> {
ServiceManager.stopVpnService(this)
Timber.i("$autoTunnel condition 5 met")
}
(!it.isEthernetConnected &&
(it.isWifiConnected &&
!it.settings.isTunnelOnWifiEnabled &&
(it.isVpnConnected))) -> {
ServiceManager.stopVpnService(this)
Timber.i("$autoTunnel condition 6 met")
}
(!it.isEthernetConnected &&
!it.isWifiConnected &&
!it.isMobileDataConnected &&
(it.isVpnConnected)) -> {
ServiceManager.stopVpnService(this)
Timber.i("$autoTunnel condition 7 met")
}
else -> {
Timber.i("$autoTunnel no condition met")
}
}
}
}
private fun startVPN() {
if(!connecting) {
connecting = true
ServiceTracker.actionOnService(
Action.START,
this.applicationContext as Application,
WireGuardTunnelService::class.java,
mapOf(getString(R.string.tunnel_extras_key) to tunnelId))
connecting = false
}
}
private fun stopVPN() {
if(!disconnecting) {
disconnecting = true
ServiceTracker.actionOnService(
Action.STOP,
this.applicationContext as Application,
WireGuardTunnelService::class.java
)
disconnecting = false
}
}
}
}
@@ -1,80 +1,190 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.PendingIntent
import android.content.Intent
import android.os.Bundle
import com.wireguard.android.backend.Tunnel
import androidx.core.app.ServiceCompat
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class WireGuardTunnelService : ForegroundService() {
private val foregroundId = 123
private val foregroundId = 123;
@Inject lateinit var vpnService: VpnService
@Inject
lateinit var vpnService : VpnService
@Inject lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var notificationService : NotificationService
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
private lateinit var job : Job
@Inject lateinit var notificationService: NotificationService
@OptIn(DelicateCoroutinesApi::class)
override fun startService(extras : Bundle?) {
super.startService(extras)
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
cancelJob()
job = GlobalScope.launch {
if(tunnelConfigString != null) {
try {
val tunnelConfig = TunnelConfig.from(tunnelConfigString)
val state = vpnService.startTunnel(tunnelConfig)
if (state == Tunnel.State.UP) {
launchVpnConnectedNotification(tunnelConfig.name)
}
} catch (e : Exception) {
Timber.e("Problem starting tunnel: ${e.message}")
stopService(extras)
}
} else {
Timber.e("Tunnel config null")
private lateinit var job: Job
private var tunnelName: String = ""
private var didShowConnected = false
override fun onCreate() {
super.onCreate()
lifecycleScope.launch(Dispatchers.Main) {
if (tunnelConfigRepository.getAll().isNotEmpty()) {
launchVpnNotification()
}
}
}
override fun stopService(extras : Bundle?) {
override fun startService(extras: Bundle?) {
super.startService(extras)
cancelJob()
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
val tunnelConfig = tunnelConfigString?.let { TunnelConfig.from(it) }
tunnelName = tunnelConfig?.name ?: ""
job =
lifecycleScope.launch(Dispatchers.IO) {
launch {
if (tunnelConfig != null) {
try {
tunnelName = tunnelConfig.name
vpnService.startTunnel(tunnelConfig)
} catch (e: Exception) {
Timber.e("Problem starting tunnel: ${e.message}")
stopService(extras)
}
} else {
Timber.i("Tunnel config null, starting default tunnel or first")
val settings = settingsRepository.getSettings()
val tunnels = tunnelConfigRepository.getAll()
if (settings.isAlwaysOnVpnEnabled) {
val tunnel =
if (settings.defaultTunnel != null) {
TunnelConfig.from(settings.defaultTunnel!!)
} else if (tunnels.isNotEmpty()) {
tunnels.first()
} else {
null
}
if (tunnel != null) {
tunnelName = tunnel.name
vpnService.startTunnel(tunnel)
}
} else {
launchAlwaysOnDisabledNotification()
}
}
}
// TODO add failed to connect notification
launch {
vpnService.vpnState.collect { state ->
state.statistics
?.mapPeerStats()
?.map { it.value?.handshakeStatus() }
.let { statuses ->
when {
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
if (!didShowConnected) {
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
launchVpnNotification(
getString(R.string.tunnel_start_title),
"${getString(R.string.tunnel_start_text)} $tunnelName",
)
didShowConnected = true
}
}
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
true -> {}
else -> {}
}
}
}
}
}
}
private fun launchAlwaysOnDisabledNotification() {
launchVpnNotification(title = this.getString(R.string.vpn_connection_failed),
description = this.getString(R.string.always_on_disabled))
}
override fun stopService(extras: Bundle?) {
super.stopService(extras)
CoroutineScope(Dispatchers.IO).launch() {
lifecycleScope.launch(Dispatchers.IO) {
vpnService.stopTunnel()
didShowConnected = false
}
cancelJob()
stopSelf()
}
private fun launchVpnConnectedNotification(tunnelName : String) {
val notification = notificationService.createNotification(
channelId = getString(R.string.vpn_channel_id),
channelName = getString(R.string.vpn_channel_name),
title = getString(R.string.tunnel_start_title),
onGoing = false,
showTimestamp = true,
description = "${getString(R.string.tunnel_start_text)} $tunnelName"
private fun launchVpnNotification(
title: String = getString(R.string.vpn_starting),
description: String = getString(R.string.attempt_connection)
) {
val notification =
notificationService.createNotification(
channelId = getString(R.string.vpn_channel_id),
channelName = getString(R.string.vpn_channel_name),
title = title,
onGoing = false,
vibration = false,
showTimestamp = true,
description = description,
)
ServiceCompat.startForeground(
this,
foregroundId,
notification,
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
super.startForeground(foregroundId, notification)
}
private fun launchVpnConnectionFailedNotification(message: String) {
val notification =
notificationService.createNotification(
channelId = getString(R.string.vpn_channel_id),
channelName = getString(R.string.vpn_channel_name),
action =
PendingIntent.getBroadcast(
this,
0,
Intent(this, NotificationActionReceiver::class.java),
PendingIntent.FLAG_IMMUTABLE,
),
actionText = getString(R.string.restart),
title = getString(R.string.vpn_connection_failed),
onGoing = false,
vibration = true,
showTimestamp = true,
description = message,
)
ServiceCompat.startForeground(
this,
foregroundId,
notification,
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
}
private fun cancelJob() {
if(this::job.isInitialized) {
if (this::job.isInitialized) {
job.cancel()
}
}
}
}
@@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.service.network
import android.app.Service
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
@@ -10,15 +9,15 @@ import android.net.wifi.SupplicantState
import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
import android.os.Build
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Context, networkCapability : Int) : NetworkService<T> {
abstract class BaseNetworkService<T : BaseNetworkService<T>>(
val context: Context,
networkCapability: Int
) : NetworkService<T> {
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
@@ -26,62 +25,72 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
override val networkStatus = callbackFlow {
val networkStatusCallback = when (Build.VERSION.SDK_INT) {
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
object : ConnectivityManager.NetworkCallback(
FLAG_INCLUDE_LOCATION_INFO
) {
val networkStatusCallback =
when (Build.VERSION.SDK_INT) {
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
object :
ConnectivityManager.NetworkCallback(
FLAG_INCLUDE_LOCATION_INFO,
) {
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network))
}
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network))
override fun onLost(network: Network) {
trySend(NetworkStatus.Unavailable(network))
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
trySend(
NetworkStatus.CapabilitiesChanged(
network,
networkCapabilities,
),
)
}
}
}
else -> {
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network))
}
override fun onLost(network: Network) {
trySend(NetworkStatus.Unavailable(network))
}
override fun onLost(network: Network) {
trySend(NetworkStatus.Unavailable(network))
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
trySend(NetworkStatus.CapabilitiesChanged(network, networkCapabilities))
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
trySend(
NetworkStatus.CapabilitiesChanged(
network,
networkCapabilities,
),
)
}
}
}
}
else -> {
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network))
}
override fun onLost(network: Network) {
trySend(NetworkStatus.Unavailable(network))
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
trySend(NetworkStatus.CapabilitiesChanged(network, networkCapabilities))
}
}
}
}
val request = NetworkRequest.Builder()
.addTransportType(networkCapability)
.build()
val request =
NetworkRequest.Builder()
.addTransportType(networkCapability)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
awaitClose {
connectivityManager.unregisterNetworkCallback(networkStatusCallback)
}
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
}
override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
var ssid : String? = getWifiNameFromCapabilities(networkCapabilities)
if((Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R)) {
var ssid: String? = getWifiNameFromCapabilities(networkCapabilities)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
val info = wifiManager.connectionInfo
if (info.supplicantState === SupplicantState.COMPLETED) {
ssid = info.ssid
@@ -90,28 +99,33 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
return ssid?.trim('"')
}
companion object {
private fun getWifiNameFromCapabilities(networkCapabilities: NetworkCapabilities) : String? {
val info : WifiInfo
if(networkCapabilities.transportInfo is WifiInfo) {
info = networkCapabilities.transportInfo as WifiInfo
} else {
return null
private fun getWifiNameFromCapabilities(networkCapabilities: NetworkCapabilities): String? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val info: WifiInfo
if (networkCapabilities.transportInfo is WifiInfo) {
info = networkCapabilities.transportInfo as WifiInfo
return info.ssid
}
}
return info.ssid
return null
}
}
}
inline fun <Result> Flow<NetworkStatus>.map(
crossinline onUnavailable: suspend (network : Network) -> Result,
crossinline onAvailable: suspend (network : Network) -> Result,
crossinline onCapabilitiesChanged: suspend (network : Network, networkCapabilities : NetworkCapabilities) -> Result,
crossinline onUnavailable: suspend (network: Network) -> Result,
crossinline onAvailable: suspend (network: Network) -> Result,
crossinline onCapabilitiesChanged:
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result
): Flow<Result> = map { status ->
when (status) {
is NetworkStatus.Unavailable -> onUnavailable(status.network)
is NetworkStatus.Available -> onAvailable(status.network)
is NetworkStatus.CapabilitiesChanged -> onCapabilitiesChanged(status.network, status.networkCapabilities)
is NetworkStatus.CapabilitiesChanged ->
onCapabilitiesChanged(
status.network,
status.networkCapabilities,
)
}
}
}
@@ -0,0 +1,9 @@
package com.zaneschepke.wireguardautotunnel.service.network
import android.content.Context
import android.net.NetworkCapabilities
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class EthernetService @Inject constructor(@ApplicationContext context: Context) :
BaseNetworkService<EthernetService>(context, NetworkCapabilities.TRANSPORT_ETHERNET)
@@ -6,5 +6,4 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class MobileDataService @Inject constructor(@ApplicationContext context: Context) :
BaseNetworkService<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR) {
}
BaseNetworkService<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR)
@@ -4,7 +4,7 @@ import android.net.NetworkCapabilities
import kotlinx.coroutines.flow.Flow
interface NetworkService<T> {
fun getNetworkName(networkCapabilities: NetworkCapabilities) : String?
val networkStatus : Flow<NetworkStatus>
fun getNetworkName(networkCapabilities: NetworkCapabilities): String?
}
val networkStatus: Flow<NetworkStatus>
}
@@ -4,7 +4,10 @@ import android.net.Network
import android.net.NetworkCapabilities
sealed class NetworkStatus {
class Available(val network : Network) : NetworkStatus()
class Unavailable(val network : Network) : NetworkStatus()
class CapabilitiesChanged(val network : Network, val networkCapabilities : NetworkCapabilities) : NetworkStatus()
class Available(val network: Network) : NetworkStatus()
class Unavailable(val network: Network) : NetworkStatus()
class CapabilitiesChanged(val network: Network, val networkCapabilities: NetworkCapabilities) :
NetworkStatus()
}
@@ -6,5 +6,4 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class WifiService @Inject constructor(@ApplicationContext context: Context) :
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI) {
}
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI)
@@ -2,17 +2,21 @@ package com.zaneschepke.wireguardautotunnel.service.notification
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
interface NotificationService {
fun createNotification(
channelId: String,
channelName: String,
title: String = "",
action: PendingIntent? = null,
actionText: String? = null,
description: String,
showTimestamp : Boolean = false,
showTimestamp: Boolean = false,
importance: Int = NotificationManager.IMPORTANCE_HIGH,
vibration: Boolean = true,
vibration: Boolean = false,
onGoing: Boolean = true,
lights: Boolean = true
lights: Boolean = true,
onlyAlertOnce: Boolean = true,
): Notification
}
}
@@ -7,57 +7,95 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Color
import androidx.core.app.NotificationCompat
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.MainActivity
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class WireGuardNotification @Inject constructor(@ApplicationContext private val context: Context) : NotificationService {
class WireGuardNotification @Inject constructor(@ApplicationContext private val context: Context) :
NotificationService {
private val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
private val watcherBuilder: NotificationCompat.Builder =
NotificationCompat.Builder(
context,
context.getString(R.string.watcher_channel_id),
)
private val tunnelBuilder: NotificationCompat.Builder =
NotificationCompat.Builder(
context,
context.getString(R.string.vpn_channel_id),
)
override fun createNotification(
channelId: String,
channelName: String,
title: String,
action: PendingIntent?,
actionText: String?,
description: String,
showTimestamp: Boolean,
importance: Int,
vibration: Boolean,
onGoing: Boolean,
lights: Boolean
) : Notification {
val channel = NotificationChannel(
channelId,
channelName,
importance
).let {
it.description = title
it.enableLights(lights)
it.lightColor = Color.RED
it.enableVibration(vibration)
it.vibrationPattern = longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
it
}
lights: Boolean,
onlyAlertOnce: Boolean,
): Notification {
val channel =
NotificationChannel(
channelId,
channelName,
importance,
)
.let {
it.description = title
it.enableLights(lights)
it.lightColor = Color.RED
it.enableVibration(vibration)
it.vibrationPattern = longArrayOf(100, 200, 300)
it
}
notificationManager.createNotificationChannel(channel)
val pendingIntent: PendingIntent =
Intent(context, MainActivity::class.java).let { notificationIntent ->
PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE)
PendingIntent.getActivity(
context,
0,
notificationIntent,
PendingIntent.FLAG_IMMUTABLE,
)
}
val builder: Notification.Builder =
Notification.Builder(
context,
channelId
)
val builder =
when (channelId) {
context.getString(R.string.watcher_channel_id) -> watcherBuilder
context.getString(R.string.vpn_channel_id) -> tunnelBuilder
else -> {
NotificationCompat.Builder(
context,
channelId,
)
}
}
return builder
.setContentTitle(title)
.setContentText(description)
.setContentIntent(pendingIntent)
.setOngoing(onGoing)
.setShowWhen(showTimestamp)
.setSmallIcon(R.mipmap.ic_launcher_foreground)
.build()
return builder.let {
if (action != null && actionText != null) {
it.addAction(
NotificationCompat.Action.Builder(0, actionText, action).build(),
)
it.setAutoCancel(true)
}
it.setContentTitle(title)
.setContentText(description)
.setOnlyAlertOnce(onlyAlertOnce)
.setContentIntent(pendingIntent)
.setOngoing(onGoing)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setShowWhen(showTimestamp)
.setSmallIcon(R.drawable.ic_launcher)
.build()
}
}
}
}
@@ -0,0 +1,89 @@
package com.zaneschepke.wireguardautotunnel.service.shortcut
import android.os.Bundle
import android.view.View
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class ShortcutsActivity : ComponentActivity() {
@Inject lateinit var settingsRepository: SettingsRepository
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
private suspend fun toggleWatcherServicePause() {
val settings = settingsRepository.getSettings()
if (settings.isAutoTunnelEnabled) {
val pauseAutoTunnel = !settings.isAutoTunnelPaused
settingsRepository.save(
settings.copy(
isAutoTunnelPaused = pauseAutoTunnel,
),
)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(View(this))
if (
intent
.getStringExtra(CLASS_NAME_EXTRA_KEY)
.equals(WireGuardTunnelService::class.java.simpleName)
) {
lifecycleScope.launch(Dispatchers.Main) {
val settings = settingsRepository.getSettings()
if (settings.isShortcutsEnabled) {
try {
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
val tunnelConfig =
if (tunnelName != null) {
tunnelConfigRepository.getAll().firstOrNull {
it.name == tunnelName
}
} else {
if (settings.defaultTunnel == null) {
tunnelConfigRepository.getAll().first()
} else {
TunnelConfig.from(settings.defaultTunnel!!)
}
}
tunnelConfig ?: return@launch
toggleWatcherServicePause()
when (intent.action) {
Action.STOP.name ->
ServiceManager.stopVpnService(
this@ShortcutsActivity,
)
Action.START.name ->
ServiceManager.startVpnServiceForeground(
this@ShortcutsActivity,
tunnelConfig.toString(),
)
}
} catch (e: Exception) {
Timber.e(e.message)
finish()
}
}
}
}
finish()
}
companion object {
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName"
const val CLASS_NAME_EXTRA_KEY = "className"
}
}
@@ -0,0 +1,135 @@
package com.zaneschepke.wireguardautotunnel.service.tile
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class TunnelControlTile() : TileService() {
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
@Inject lateinit var settingsRepository: SettingsRepository
@Inject lateinit var vpnService: VpnService
private val scope = CoroutineScope(Dispatchers.IO)
private var tunnelName: String? = null
override fun onStartListening() {
super.onStartListening()
Timber.d("On start listening called")
scope.launch {
vpnService.vpnState.collect {
when (it.status) {
Tunnel.State.UP -> setActive()
Tunnel.State.DOWN -> setInactive()
else -> setInactive()
}
val tunnels = tunnelConfigRepository.getAll()
if (tunnels.isEmpty()) {
setUnavailable()
return@collect
}
tunnelName = it.name.run {
val settings = settingsRepository.getSettings()
if (settings.defaultTunnel != null) {
TunnelConfig.from(settings.defaultTunnel!!).name
} else tunnels.firstOrNull()?.name
}
setTileDescription(tunnelName ?: "")
}
}
}
override fun onDestroy() {
super.onDestroy()
scope.cancel()
}
override fun onTileRemoved() {
super.onTileRemoved()
scope.cancel()
}
override fun onClick() {
super.onClick()
unlockAndRun {
scope.launch {
try {
val defaultTunnel = settingsRepository.getSettings().defaultTunnel
val config = defaultTunnel ?: run {
val tunnelConfigs = tunnelConfigRepository.getAll()
return@run tunnelConfigs.find { it.name == tunnelName }
}
toggleWatcherServicePause()
if (vpnService.getState() == Tunnel.State.UP) {
ServiceManager.stopVpnService(this@TunnelControlTile)
} else {
ServiceManager.startVpnServiceForeground(
this@TunnelControlTile,
config.toString(),
)
}
} catch (e: Exception) {
Timber.e(e.message)
} finally {
cancel()
}
}
}
}
private fun toggleWatcherServicePause() {
scope.launch {
val settings = settingsRepository.getSettings()
if (settings.isAutoTunnelEnabled) {
val pauseAutoTunnel = !settings.isAutoTunnelPaused
settingsRepository.save(
settings.copy(
isAutoTunnelPaused = pauseAutoTunnel,
),
)
}
}
}
private fun setActive() {
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
}
private fun setInactive() {
qsTile.state = Tile.STATE_INACTIVE
qsTile.updateTile()
}
private fun setUnavailable() {
qsTile.state = Tile.STATE_UNAVAILABLE
qsTile.updateTile()
}
private fun setTileDescription(description: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
qsTile.subtitle = description
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
qsTile.stateDescription = description
}
qsTile.updateTile()
}
}
@@ -0,0 +1,16 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel
enum class HandshakeStatus {
HEALTHY,
STALE,
UNKNOWN,
NOT_STARTED;
companion object {
private const val WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC = 180
const val STATUS_CHANGE_TIME_BUFFER = 30
const val STALE_TIME_LIMIT_SEC =
WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC + STATUS_CHANGE_TIME_BUFFER
const val NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC = 30
}
}
@@ -1,13 +1,15 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import kotlinx.coroutines.flow.SharedFlow
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import kotlinx.coroutines.flow.StateFlow
interface VpnService : Tunnel {
suspend fun startTunnel(tunnelConfig : TunnelConfig) : Tunnel.State
suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State
suspend fun stopTunnel()
val state : SharedFlow<Tunnel.State>
val tunnelName : SharedFlow<String>
fun getState() : Tunnel.State
}
val vpnState: StateFlow<VpnState>
fun getState(): Tunnel.State
}
@@ -0,0 +1,13 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
data class VpnState(
val status: Tunnel.State = Tunnel.State.DOWN,
val name: String = "",
val config: Config? = null,
val statistics: Statistics? = null
)
@@ -2,64 +2,157 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel.State
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.module.Kernel
import com.zaneschepke.wireguardautotunnel.module.Userspace
import com.zaneschepke.wireguardautotunnel.util.Constants
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
class WireGuardTunnel
@Inject
constructor(
@Userspace private val userspaceBackend : Backend,
@Kernel private val kernelBackend: Backend,
private val settingsRepository: SettingsRepository
) : VpnService {
private val _vpnState = MutableStateFlow(VpnState())
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
class WireGuardTunnel @Inject constructor(private val backend : Backend) : VpnService {
private val scope = CoroutineScope(Dispatchers.IO)
private val _tunnelName = MutableStateFlow("")
override val tunnelName get() = _tunnelName.asStateFlow()
private val _state = MutableSharedFlow<Tunnel.State>(
replay = 1,
onBufferOverflow = BufferOverflow.SUSPEND,
extraBufferCapacity = 1)
override val state get() = _state.asSharedFlow()
private lateinit var statsJob: Job
override suspend fun startTunnel(tunnelConfig: TunnelConfig) : Tunnel.State{
return try {
if(getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) {
stopTunnel()
private var config: Config? = null
private var backend: Backend = userspaceBackend
private var backendIsUserspace = true
init {
scope.launch {
settingsRepository.getSettingsFlow().collect {
if (it.isKernelEnabled && backendIsUserspace) {
Timber.d("Setting kernel backend")
backend = kernelBackend
backendIsUserspace = false
} else if (!it.isKernelEnabled && !backendIsUserspace) {
Timber.d("Setting userspace backend")
backend = userspaceBackend
backendIsUserspace = true
}
}
_tunnelName.emit(tunnelConfig.name)
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
val state = backend.setState(
this, Tunnel.State.UP, config)
_state.emit(state)
state;
} catch (e : Exception) {
}
}
override suspend fun startTunnel(tunnelConfig: TunnelConfig): State {
return try {
stopTunnelOnConfigChange(tunnelConfig)
emitTunnelName(tunnelConfig.name)
config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
emitTunnelConfig(config)
val state =
backend.setState(
this,
State.UP,
config,
)
emitTunnelState(state)
state
} catch (e: BackendException) {
Timber.e("Failed to start tunnel with error: ${e.message}")
Tunnel.State.DOWN
State.DOWN
}
}
private fun emitTunnelState(state: State) {
_vpnState.tryEmit(
_vpnState.value.copy(
status = state,
),
)
}
private fun emitBackendStatistics(statistics: Statistics) {
_vpnState.tryEmit(
_vpnState.value.copy(
statistics = statistics,
),
)
}
private suspend fun emitTunnelName(name: String) {
_vpnState.emit(
_vpnState.value.copy(
name = name,
),
)
}
private suspend fun emitTunnelConfig(config : Config?) {
_vpnState.emit(
_vpnState.value.copy(
config = config,
),
)
}
private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) {
if (getState() == State.UP && _vpnState.value.name != tunnelConfig.name) {
stopTunnel()
}
}
override fun getName(): String {
return _tunnelName.value
return _vpnState.value.name
}
override suspend fun stopTunnel() {
override suspend fun stopTunnel() {
try {
if(getState() == Tunnel.State.UP) {
val state = backend.setState(this, Tunnel.State.DOWN, null)
_state.emit(state)
if (getState() == State.UP) {
val state = backend.setState(this, State.DOWN, null)
emitTunnelState(state)
}
} catch (e : BackendException) {
} catch (e: BackendException) {
Timber.e("Failed to stop tunnel with error: ${e.message}")
}
}
override fun getState(): Tunnel.State {
override fun getState(): State {
return backend.getState(this)
}
override fun onStateChange(state : Tunnel.State) {
_state.tryEmit(state)
override fun onStateChange(state: State) {
val tunnel = this
emitTunnelState(state)
WireGuardAutoTunnel.requestTileServiceStateUpdate(WireGuardAutoTunnel.instance)
if (state == State.UP) {
statsJob =
scope.launch {
while (true) {
val statistics = backend.getStatistics(tunnel)
emitBackendStatistics(statistics)
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
}
}
}
if (state == State.DOWN) {
if (this::statsJob.isInitialized) {
statsJob.cancel()
}
}
}
}
}
@@ -1,14 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel.model
import io.objectbox.annotation.Entity
import io.objectbox.annotation.Id
@Entity
data class Settings(
@Id
var id : Long = 0,
var isAutoTunnelEnabled : Boolean = false,
var isTunnelOnMobileDataEnabled : Boolean = false,
var trustedNetworkSSIDs : MutableList<String> = mutableListOf(),
var defaultTunnel : String? = null
)
@@ -1,89 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel.model
import com.wireguard.config.Config
import io.objectbox.annotation.ConflictStrategy
import io.objectbox.annotation.Entity
import io.objectbox.annotation.Id
import io.objectbox.annotation.Unique
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.io.InputStream
@Entity
@Serializable
data class TunnelConfig(
@Id
var id : Long = 0,
@Unique(onConflict = ConflictStrategy.REPLACE)
var name : String,
var wgQuick : String
) {
override fun toString(): String {
return Json.encodeToString(serializer(), this)
}
companion object {
private const val INCLUDED_APPLICATIONS = "IncludedApplications = "
private const val EXCLUDED_APPLICATIONS = "ExcludedApplications = "
private const val INTERFACE = "[Interface]"
private const val NEWLINE_CHAR = "\n"
private const val APP_CONFIG_SEPARATOR = ", "
private fun addApplicationsToConfig(appConfig : String, wgQuick : String) : String {
val configList = wgQuick.split(NEWLINE_CHAR).toMutableList()
val interfaceIndex = configList.indexOf(INTERFACE)
configList.add(interfaceIndex + 1, appConfig)
return configList.joinToString(NEWLINE_CHAR)
}
fun clearAllApplicationsFromConfig(wgQuick : String) : String {
val configList = wgQuick.split(NEWLINE_CHAR).toMutableList()
val itr = configList.iterator()
while (itr.hasNext()) {
val next = itr.next()
if(next.contains(INCLUDED_APPLICATIONS) || next.contains(EXCLUDED_APPLICATIONS)) {
itr.remove()
}
}
return configList.joinToString(NEWLINE_CHAR)
}
fun setExcludedApplicationsOnQuick(packages : List<String>, wgQuick: String) : String {
if(packages.isEmpty()) {
return wgQuick
}
val clearedWgQuick = clearAllApplicationsFromConfig(wgQuick)
val excludeConfig = buildExcludedApplicationsString(packages)
return addApplicationsToConfig(excludeConfig, clearedWgQuick)
}
fun setIncludedApplicationsOnQuick(packages : List<String>, wgQuick: String) : String {
if(packages.isEmpty()) {
return wgQuick
}
val clearedWgQuick = clearAllApplicationsFromConfig(wgQuick)
val includeConfig = buildIncludedApplicationsString(packages)
return addApplicationsToConfig(includeConfig, clearedWgQuick)
}
private fun buildExcludedApplicationsString(packages : List<String>) : String {
return EXCLUDED_APPLICATIONS + packages.joinToString(APP_CONFIG_SEPARATOR)
}
private fun buildIncludedApplicationsString(packages : List<String>) : String {
return INCLUDED_APPLICATIONS + packages.joinToString(APP_CONFIG_SEPARATOR)
}
fun from(string : String) : TunnelConfig {
return Json.decodeFromString<TunnelConfig>(string)
}
fun configFromQuick(wgQuick: String): Config {
val inputStream: InputStream = wgQuick.byteInputStream()
val reader = inputStream.bufferedReader(Charsets.UTF_8)
return Config.parse(reader)
}
}
}
@@ -0,0 +1,9 @@
package com.zaneschepke.wireguardautotunnel.ui
data class AppUiState(
val snackbarMessage: String = "",
val snackbarMessageConsumed: Boolean = true,
val vpnPermissionAccepted: Boolean = false,
val notificationPermissionAccepted: Boolean = false,
val requestPermissions: Boolean = false
)
@@ -0,0 +1,142 @@
package com.zaneschepke.wireguardautotunnel.ui
import android.app.Application
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.android.backend.GoBackend
import com.zaneschepke.logcatter.Logcatter
import com.zaneschepke.logcatter.model.LogMessage
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.time.Instant
import javax.inject.Inject
@HiltViewModel
class AppViewModel
@Inject
constructor(
private val application: Application,
) : ViewModel() {
val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)
private val _appUiState = MutableStateFlow(AppUiState(
vpnPermissionAccepted = vpnIntent == null
))
val appUiState = _appUiState.asStateFlow()
fun isRequiredPermissionGranted() : Boolean {
val allAccepted = (_appUiState.value.vpnPermissionAccepted && _appUiState.value.vpnPermissionAccepted)
if(!allAccepted) requestPermissions()
return allAccepted
}
private fun requestPermissions() {
_appUiState.value = _appUiState.value.copy(
requestPermissions = true
)
}
fun permissionsRequested() {
_appUiState.value = _appUiState.value.copy(
requestPermissions = false
)
}
fun openWebPage(url: String) {
try {
val webpage: Uri = Uri.parse(url)
val intent = Intent(Intent.ACTION_VIEW, webpage).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
application.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Timber.e(e)
showSnackbarMessage(application.getString(R.string.no_browser_detected))
}
}
fun onVpnPermissionAccepted() {
_appUiState.value = _appUiState.value.copy(
vpnPermissionAccepted = true
)
}
fun launchEmail() {
try {
val intent =
Intent(Intent.ACTION_SENDTO).apply {
type = Constants.EMAIL_MIME_TYPE
putExtra(Intent.EXTRA_EMAIL, arrayOf(application.getString(R.string.my_email)))
putExtra(Intent.EXTRA_SUBJECT, application.getString(R.string.email_subject))
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
application.startActivity(
Intent.createChooser(intent, application.getString(R.string.email_chooser)).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
})
} catch (e: ActivityNotFoundException) {
Timber.e(e)
showSnackbarMessage(application.getString(R.string.no_email_detected))
}
}
fun showSnackbarMessage(message : String) {
_appUiState.value = _appUiState.value.copy(
snackbarMessage = message,
snackbarMessageConsumed = false
)
}
fun snackbarMessageConsumed() {
_appUiState.value = _appUiState.value.copy(
snackbarMessage = "",
snackbarMessageConsumed = true
)
}
val logs = mutableStateListOf<LogMessage>()
fun readLogCatOutput() = viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) {
launch {
Logcatter.logs {
logs.add(it)
if (logs.size > Constants.LOG_BUFFER_SIZE) {
logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt())
}
}
}
}
fun clearLogs() {
logs.clear()
Logcatter.clear()
}
fun saveLogsToFile() {
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
val content = logs.joinToString(separator = "\n")
FileUtils.saveFileToDownloads(application.applicationContext, content, fileName)
Toast.makeText(application, application.getString(R.string.logs_saved), Toast.LENGTH_SHORT).show()
}
fun setNotificationPermissionAccepted(accepted: Boolean) {
_appUiState.value = _appUiState.value.copy(
notificationPermissionAccepted = accepted
)
}
}
@@ -0,0 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui
import com.journeyapps.barcodescanner.CaptureActivity
class CaptureActivityPortrait : CaptureActivity()
@@ -1,169 +1,249 @@
package com.zaneschepke.wireguardautotunnel.ui
import android.Manifest
import android.content.Intent
import android.net.Uri
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.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.slideInHorizontally
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarData
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.composable
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.wireguard.android.backend.GoBackend
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import xyz.teamgravity.pin_lock_compose.PinLock
import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class,
ExperimentalPermissionsApi::class
@Inject
lateinit var dataStoreManager: DataStoreManager
@Inject
lateinit var settingsRepository: SettingsRepository
@OptIn(
ExperimentalPermissionsApi::class,
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb()))
// load preferences into memory and init data
lifecycleScope.launch {
dataStoreManager.init()
WireGuardAutoTunnel.requestTileServiceStateUpdate(this@MainActivity)
val settings = settingsRepository.getSettings()
if (settings.isAutoTunnelEnabled) {
ServiceManager.startWatcherService(application.applicationContext)
}
}
setContent {
val navController = rememberAnimatedNavController()
WireguardAutoTunnelTheme {
TransparentSystemBars()
val appViewModel = hiltViewModel<AppViewModel>()
val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle()
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val snackbarHostState = remember { SnackbarHostState() }
val notificationPermissionState =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) else null
val notificationPermissionState =
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
val focusRequester = remember { FocusRequester() }
val snackbarHostState = remember { SnackbarHostState() }
fun requestNotificationPermission() {
if (!notificationPermissionState.status.isGranted && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
notificationPermissionState.launchPermissionRequest()
}
}
var vpnIntent by remember { mutableStateOf(GoBackend.VpnService.prepare(this)) }
val vpnActivityResultState = rememberLauncherForActivityResult(
val vpnActivityResultState =
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
onResult = {
val accepted = (it.resultCode == RESULT_OK)
if (accepted) {
vpnIntent = null
appViewModel.onVpnPermissionAccepted()
}
})
LaunchedEffect(vpnIntent) {
if (vpnIntent != null) {
vpnActivityResultState.launch(vpnIntent)
} else requestNotificationPermission()
}
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) },
bottomBar = if (vpnIntent == null && notificationPermissionState.status.isGranted) {
{ BottomNavBar(navController, Routes.navItems) }
} else {
{}
},
)
{ padding ->
if (vpnIntent != null) {
PermissionRequestFailedScreen(
padding = padding,
onRequestAgain = { vpnActivityResultState.launch(vpnIntent) },
message = getString(R.string.vpn_permission_required),
getString(R.string.retry)
)
return@Scaffold
}
if (!notificationPermissionState.status.isGranted) {
PermissionRequestFailedScreen(
padding = padding,
onRequestAgain = {
val intentSettings =
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intentSettings.data =
Uri.fromParts("package", this.packageName, null)
startActivity(intentSettings);
},
message = getString(R.string.notification_permission_required),
getString(R.string.open_settings)
)
return@Scaffold
}
AnimatedNavHost(navController, startDestination = Routes.Main.name) {
composable(Routes.Main.name, enterTransition = {
when (initialState.destination.route) {
Routes.Settings.name, Routes.Support.name ->
slideInHorizontally(
initialOffsetX = { -1000 },
animationSpec = tween(500)
)
else -> {
fadeIn(animationSpec = tween(1000))
}
}
}) {
MainScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController)
fun showSnackBarMessage(message: StringValue) {
lifecycleScope.launch(Dispatchers.Main) {
val result =
snackbarHostState.showSnackbar(
message = message.asString(this@MainActivity),
duration = SnackbarDuration.Short,
)
when (result) {
SnackbarResult.ActionPerformed,
SnackbarResult.Dismissed -> {
snackbarHostState.currentSnackbarData?.dismiss()
}
composable(Routes.Settings.name, enterTransition = {
when (initialState.destination.route) {
Routes.Main.name ->
slideInHorizontally(
initialOffsetX = { 1000 },
animationSpec = tween(500)
)
}
}
}
Routes.Support.name -> {
slideInHorizontally(
initialOffsetX = { -1000 },
animationSpec = tween(500)
)
}
LaunchedEffect(appUiState.requestPermissions) {
if (appUiState.requestPermissions) {
appViewModel.permissionsRequested()
if (notificationPermissionState != null && !notificationPermissionState.status.isGranted
) {
showSnackBarMessage(StringValue.StringResource(R.string.notification_permission_required))
return@LaunchedEffect notificationPermissionState.launchPermissionRequest()
}
if (!appUiState.vpnPermissionAccepted) {
return@LaunchedEffect vpnActivityResultState.launch(appViewModel.vpnIntent)
}
}
}
else -> {
fadeIn(animationSpec = tween(1000))
}
WireguardAutoTunnelTheme {
LaunchedEffect(Unit) {
appViewModel.setNotificationPermissionAccepted(
notificationPermissionState?.status?.isGranted ?: true,
)
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) appViewModel.readLogCatOutput()
}
LaunchedEffect(appUiState.snackbarMessageConsumed) {
if (!appUiState.snackbarMessageConsumed) {
showSnackBarMessage(StringValue.DynamicString(appUiState.snackbarMessage))
appViewModel.snackbarMessageConsumed()
}
}
Scaffold(
snackbarHost = {
SnackbarHost(snackbarHostState) { snackbarData: SnackbarData ->
CustomSnackBar(
snackbarData.visuals.message,
isRtl = false,
containerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation(
2.dp,
),
)
}
},
//TODO refactor
modifier = Modifier
.focusable()
.focusProperties { when(navBackStackEntry?.destination?.route) {
Screen.Lock.route -> Unit
else -> up = focusRequester }
},
bottomBar = {
BottomNavBar(
navController,
listOf(
Screen.Main.navItem,
Screen.Settings.navItem,
Screen.Support.navItem,
),
)
},
) { padding ->
NavHost(
navController,
startDestination =
(if (PinManager.pinExists()) Screen.Lock.route else Screen.Main.route),
modifier =
Modifier
.padding(padding)
.fillMaxSize(),
) {
composable(
Screen.Main.route,
) {
MainScreen(
focusRequester = focusRequester,
appViewModel = appViewModel,
navController = navController,
)
}
composable(
Screen.Settings.route,
) {
SettingsScreen(
appViewModel = appViewModel,
navController = navController,
focusRequester = focusRequester
)
}
composable(
Screen.Support.route,
) {
SupportScreen(
focusRequester = focusRequester,
appViewModel = appViewModel,
navController = navController,
)
}
composable(Screen.Support.Logs.route) {
LogsScreen(appViewModel)
}
composable("${Screen.Config.route}/{id}") {
val id = it.arguments?.getString("id")
if (!id.isNullOrBlank()) {
ConfigScreen(
navController = navController,
id = id,
appViewModel = appViewModel,
focusRequester = focusRequester,
)
}
}) { SettingsScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController) }
composable(Routes.Support.name, enterTransition = {
when (initialState.destination.route) {
Routes.Settings.name, Routes.Main.name ->
slideInHorizontally(
initialOffsetX = { 1000 },
animationSpec = tween(500)
)
else -> {
fadeIn(animationSpec = tween(1000))
}
}
}) { SupportScreen(padding = padding) }
composable("${Routes.Config.name}/{id}", enterTransition = {
fadeIn(animationSpec = tween(1000))
}) { ConfigScreen(padding = padding, navController = navController, id = it.arguments?.getString("id"))}
}
composable(Screen.Lock.route) {
PinLockScreen(navController = navController, appViewModel = appViewModel)
}
}
}
}
@@ -6,30 +6,35 @@ import androidx.compose.material.icons.rounded.QuestionMark
import androidx.compose.material.icons.rounded.Settings
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
enum class Routes {
Main,
Settings,
Support,
Config;
companion object {
val navItems = listOf(
sealed class Screen(val route: String) {
data object Main : Screen("main") {
val navItem =
BottomNavItem(
name = "Tunnels",
route = Main.name,
route = route,
icon = Icons.Rounded.Home,
),
)
}
data object Settings : Screen("settings") {
val navItem =
BottomNavItem(
name = "Settings",
route = Settings.name,
route = route,
icon = Icons.Rounded.Settings,
),
)
}
data object Support : Screen("support") {
val navItem =
BottomNavItem(
name = "Support",
route = Support.name,
route = route,
icon = Icons.Rounded.QuestionMark,
)
)
data object Logs : Screen("support/logs")
}
}
data object Config : Screen("config")
data object Lock : Screen("lock")
}
@@ -1,9 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui
data class ViewState(
val showSnackbarMessage : Boolean = false,
val snackbarMessage : String = "",
val snackbarActionText : String = "",
val onSnackbarActionClick : () -> Unit = {},
val isLoading : Boolean = false
)
@@ -3,29 +3,39 @@ package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun ClickableIconButton(onIconClick : () -> Unit, text : String, icon : ImageVector, enabled : Boolean) {
Button(onClick = {},
enabled = enabled
fun ClickableIconButton(
onClick: () -> Unit,
onIconClick: () -> Unit,
text: String,
icon: ImageVector,
enabled: Boolean
) {
TextButton(
onClick = onClick,
enabled = enabled,
) {
Text(text)
Text(text, Modifier.weight(1f, false))
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
Icon(
imageVector = icon,
contentDescription = "Delete",
modifier = Modifier.size(ButtonDefaults.IconSize).clickable {
if(enabled) {
onIconClick()
}
}
contentDescription = icon.name,
modifier =
Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable {
if (enabled) {
onIconClick()
}
},
)
}
}
}
@@ -1,35 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
@Composable
fun PermissionRequestFailedScreen(padding : PaddingValues, onRequestAgain : () -> Unit, message : String, buttonText : String ) {
val scope = rememberCoroutineScope()
Column(horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize()
.padding(padding)) {
Text(message, textAlign = TextAlign.Center, modifier = Modifier.padding(15.dp))
Button(onClick = {
scope.launch {
onRequestAgain()
}
}) {
Text(buttonText)
}
}
}
@@ -1,41 +1,88 @@
package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.wireguard.android.backend.Statistics
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.toThreeDecimalPlaceString
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RowListItem(text : String, onHold : () -> Unit, rowButton : @Composable() () -> Unit ) {
fun RowListItem(
icon: @Composable () -> Unit,
text: String,
onHold: () -> Unit,
onClick: () -> Unit,
rowButton: @Composable () -> Unit,
expanded: Boolean,
statistics: Statistics?
) {
Box(
modifier = Modifier
.combinedClickable(
onClick = {
},
onLongClick = {
onHold()
}
)
modifier =
Modifier.animateContentSize()
.clip(RoundedCornerShape(30.dp))
.combinedClickable(
onClick = { onClick() },
onLongClick = { onHold() },
),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(14.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text)
rowButton()
Column {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp, vertical = 5.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
icon()
Text(text)
}
rowButton()
}
if (expanded) {
statistics?.peers()?.forEach {
Row(
modifier =
Modifier.fillMaxWidth()
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
) {
//TODO change these to string resources
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
val peerTx = statistics.peer(it)!!.txBytes
val peerRx = statistics.peer(it)!!.rxBytes
val peerId = it.toBase64().subSequence(0, 3).toString() + "***"
val handshakeSec =
NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch)
val handshake =
if (handshakeSec == null) "never" else "$handshakeSec secs ago"
val peerTxMB = NumberUtils.bytesToMB(peerTx).toThreeDecimalPlaceString()
val peerRxMB = NumberUtils.bytesToMB(peerRx).toThreeDecimalPlaceString()
val fontSize = 9.sp
Text("peer: $peerId", fontSize = fontSize)
Text("handshake: $handshake", fontSize = fontSize)
Text("tx: $peerTxMB MB", fontSize = fontSize)
Text("rx: $peerRxMB MB", fontSize = fontSize)
}
}
}
}
}
}
}
@@ -0,0 +1,79 @@
package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Clear
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun SearchBar(onQuery: (queryString: String) -> Unit) {
// Immediately update and keep track of query from text field changes.
var query: String by rememberSaveable { mutableStateOf("") }
var showClearIcon by rememberSaveable { mutableStateOf(false) }
if (query.isEmpty()) {
showClearIcon = false
} else if (query.isNotEmpty()) {
showClearIcon = true
}
TextField(
value = query,
onValueChange = { onQueryChanged ->
// If user makes changes to text, immediately updated it.
query = onQueryChanged
onQuery(onQueryChanged)
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Search,
tint = MaterialTheme.colorScheme.onBackground,
contentDescription = stringResource(id = R.string.search_icon),
)
},
trailingIcon = {
if (showClearIcon) {
IconButton(onClick = { query = "" }) {
Icon(
imageVector = Icons.Rounded.Clear,
tint = MaterialTheme.colorScheme.onBackground,
contentDescription = stringResource(id = R.string.clear_icon),
)
}
}
},
maxLines = 1,
colors =
TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
),
placeholder = { Text(text = stringResource(R.string.hint_search_packages)) },
textStyle = MaterialTheme.typography.bodySmall,
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
modifier =
Modifier.fillMaxWidth()
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape),
)
}
@@ -0,0 +1,36 @@
package com.zaneschepke.wireguardautotunnel.ui.common.config
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
@Composable
fun ConfigurationTextBox(
value: String,
hint: String,
onValueChange: (String) -> Unit,
keyboardActions: KeyboardActions,
label: String,
modifier: Modifier
) {
OutlinedTextField(
modifier = modifier,
value = value,
singleLine = true,
onValueChange = { onValueChange(it) },
label = { Text(label) },
maxLines = 1,
placeholder = { Text(hint) },
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done,
),
keyboardActions = keyboardActions,
)
}
@@ -0,0 +1,36 @@
package com.zaneschepke.wireguardautotunnel.ui.common.config
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
@Composable
fun ConfigurationToggle(
label: String,
enabled: Boolean,
checked: Boolean,
padding: Dp,
onCheckChanged: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = Modifier.fillMaxWidth().padding(padding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(label)
Switch(
modifier = modifier,
enabled = enabled,
checked = checked,
onCheckedChange = { onCheckChanged() },
)
}
}
@@ -6,19 +6,33 @@ import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState
import com.zaneschepke.wireguardautotunnel.ui.Screen
@Composable
fun BottomNavBar(navController : NavController, bottomNavItems : List<BottomNavItem>) {
fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) {
val backStackEntry = navController.currentBackStackEntryAsState()
var showBottomBar by rememberSaveable { mutableStateOf(true) }
val navBackStackEntry by navController.currentBackStackEntryAsState()
//TODO find a better way to hide nav bar
showBottomBar = when (navBackStackEntry?.destination?.route) {
Screen.Lock.route -> false
else -> true
}
NavigationBar(
containerColor = MaterialTheme.colorScheme.background,
containerColor = if(!showBottomBar) Color.Transparent else MaterialTheme.colorScheme.background,
) {
bottomNavItems.forEach { item ->
if(showBottomBar) bottomNavItems.forEach { item ->
val selected = item.route == backStackEntry.value?.destination?.route
NavigationBarItem(
@@ -35,8 +49,8 @@ fun BottomNavBar(navController : NavController, bottomNavItems : List<BottomNavI
imageVector = item.icon,
contentDescription = "${item.name} Icon",
)
}
},
)
}
}
}
}
@@ -0,0 +1,83 @@
package com.zaneschepke.wireguardautotunnel.ui.common.prompt
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.BiometricPrompt
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
@Composable
fun AuthorizationPrompt(onSuccess: () -> Unit, onFailure: () -> Unit, onError: (String) -> Unit) {
val context = LocalContext.current
val biometricManager = BiometricManager.from(context)
val bio = biometricManager.canAuthenticate(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
val isBiometricAvailable = remember {
when (bio) {
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
onError("Biometrics not available")
false
}
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
onError("Biometrics not created")
false
}
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
onError("Biometric hardware not found")
false
}
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
onError("Biometric security update required")
false
}
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
onError("Biometrics not supported")
false
}
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
onError("Biometrics status unknown")
false
}
BiometricManager.BIOMETRIC_SUCCESS -> true
else -> false
}
}
if (isBiometricAvailable) {
val executor = remember { ContextCompat.getMainExecutor(context) }
val promptInfo =
BiometricPrompt.PromptInfo.Builder()
.setAllowedAuthenticators(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
.setTitle("Biometric Authentication")
.setSubtitle("Log in using your biometric credential")
.build()
val biometricPrompt =
BiometricPrompt(
context as FragmentActivity,
executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
onFailure()
}
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
super.onAuthenticationSucceeded(result)
onSuccess()
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
onFailure()
}
},
)
biometricPrompt.authenticate(promptInfo)
}
}
@@ -0,0 +1,63 @@
package com.zaneschepke.wireguardautotunnel.ui.common.prompt
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Snackbar
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
@Composable
fun CustomSnackBar(
message: String,
isRtl: Boolean = true,
containerColor: Color = MaterialTheme.colorScheme.surface
) {
Snackbar(
containerColor = containerColor,
modifier =
Modifier.fillMaxWidth(
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f,
)
.padding(bottom = 100.dp),
shape = RoundedCornerShape(16.dp),
) {
CompositionLocalProvider(
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr,
) {
Row(
modifier = Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Min),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
) {
val icon = Icons.Rounded.Info
Icon(
icon,
contentDescription = icon.name,
tint = Color.White,
modifier = Modifier.padding(end = 10.dp),
)
Text(message, color = Color.White, modifier = Modifier.padding(end = 5.dp))
}
}
}
}
@@ -0,0 +1,23 @@
package com.zaneschepke.wireguardautotunnel.ui.common.screen
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun LoadingScreen() {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier = Modifier.fillMaxSize().focusable().padding(),
) {
Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() }
}
}
@@ -0,0 +1,20 @@
package com.zaneschepke.wireguardautotunnel.ui.common.text
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun LogTypeLabel(color : Color, content: @Composable () -> Unit,) {
Box(
modifier = Modifier.size(20.dp).clip(RoundedCornerShape(2.dp)).background(color), contentAlignment = Alignment.Center) {
content()
}
}
@@ -0,0 +1,22 @@
package com.zaneschepke.wireguardautotunnel.ui.common.text
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun SectionTitle(title: String, padding: Dp) {
Text(
title,
textAlign = TextAlign.Center,
style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold),
modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp),
)
}
@@ -0,0 +1,30 @@
package com.zaneschepke.wireguardautotunnel.ui.models
import com.wireguard.config.Interface
data class InterfaceProxy(
var privateKey: String = "",
var publicKey: String = "",
var addresses: String = "",
var dnsServers: String = "",
var listenPort: String = "",
var mtu: String = ""
) {
companion object {
fun from(i: Interface): InterfaceProxy {
return InterfaceProxy(
publicKey = i.keyPair.publicKey.toBase64().trim(),
privateKey = i.keyPair.privateKey.toBase64().trim(),
addresses = i.addresses.joinToString(", ").trim(),
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
listenPort =
if (i.listenPort.isPresent) {
i.listenPort.get().toString().trim()
} else {
""
},
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
)
}
}
}

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