mirror of
https://github.com/amnezia-vpn/amneziawg-android.git
synced 2026-07-03 14:05:13 +02:00
Compare commits
474 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9eaed5e745 | |||
| 68350bb4df | |||
| 12be972fcd | |||
| d200437813 | |||
| 3ffe7a5e68 | |||
| 08ff9f5ece | |||
| 4bee579e48 | |||
| a906c478c9 | |||
| 306d0648c6 | |||
| e99ccf9013 | |||
| 59935a12b9 | |||
| a9ec828506 | |||
| eebeece856 | |||
| 746ab00794 | |||
| b877593d55 | |||
| dcd596907a | |||
| 44c2afbfba | |||
| bd1679b7e0 | |||
| ff7d7e0edd | |||
| 2f088938c6 | |||
| 53adb0e9a6 | |||
| 6789c11a7b | |||
| c56065fcfe | |||
| 52a2ae36f6 | |||
| abcb51d2a6 | |||
| 8b9a40b3d7 | |||
| 4b36df504c | |||
| 35fe5bd5f0 | |||
| 79ae85c728 | |||
| 49ac61304e | |||
| d79cdb0d41 | |||
| a3726b07bf | |||
| 80c35a2053 | |||
| 601b58b670 | |||
| 9cf049775f | |||
| 92122e60c6 | |||
| f20d0f0659 | |||
| 9346a63753 | |||
| bab70ab51e | |||
| 2fc0bb1a03 | |||
| dd0ff8fe60 | |||
| 45a179580d | |||
| 0bcee7f9cc | |||
| af10b117b4 | |||
| 7aa7825209 | |||
| 8b7617294e | |||
| 9985b9b08e | |||
| 840d65881e | |||
| c18f6818e8 | |||
| dcd91cad1b | |||
| 898bb679d2 | |||
| 348d430cd3 | |||
| e3d98633fb | |||
| b451920408 | |||
| 1fa15e76e3 | |||
| 8a58270e03 | |||
| e08d873cc3 | |||
| 3748a1da88 | |||
| 9597d719ac | |||
| 041ce2134f | |||
| 4c17fc51e7 | |||
| 6cf7439053 | |||
| 814ab4937d | |||
| 3d088411e2 | |||
| 39e0c861e2 | |||
| d60efcd7c7 | |||
| 4dff537d21 | |||
| 761c2ade4a | |||
| 115a87af32 | |||
| c7ecaeb145 | |||
| 8e2d63db75 | |||
| 3208bac987 | |||
| fc1f2132fe | |||
| 09125e1e31 | |||
| 6b1164ad8d | |||
| c68a5c776b | |||
| f3ac787f5a | |||
| 751be2c469 | |||
| cfb7f1b016 | |||
| 33e07628db | |||
| b1b7d1d90c | |||
| 6cb9548f72 | |||
| 8207ae1e8d | |||
| 48e0e427eb | |||
| 03e95d2dd3 | |||
| 58b14cc650 | |||
| 3fa8e09545 | |||
| 9d76b354f0 | |||
| 1856a50c56 | |||
| ba0f3ece04 | |||
| e8ad75d924 | |||
| b858284b1e | |||
| 3c6e06f8d5 | |||
| 0d33a00e72 | |||
| 830d0992a7 | |||
| e4192ea172 | |||
| 4bbb1a0fcd | |||
| 77b5937fbb | |||
| 3144d36056 | |||
| 521bbb4b4c | |||
| 0a55e10b94 | |||
| d6e5fd9301 | |||
| 2c625f56fd | |||
| 7db0fa915e | |||
| e424765a61 | |||
| 1ca4dbf1a2 | |||
| 065893e31d | |||
| ef70aa88e1 | |||
| 6a11a199ca | |||
| 7b5ceac9f7 | |||
| 9fe0019ec1 | |||
| 84654a0302 | |||
| cd43444d1f | |||
| b7028896c7 | |||
| c1e86acb3c | |||
| 7d31bd2be9 | |||
| 2e573a66a4 | |||
| 8d128cf2e9 | |||
| d5ffa08480 | |||
| 43ce69bef4 | |||
| 44a0f53e58 | |||
| d74b988f75 | |||
| 10e910186e | |||
| 017f420d42 | |||
| bc186fe6ad | |||
| 09b40cdec7 | |||
| d2721f2d7d | |||
| 3095e19e13 | |||
| c547d033c3 | |||
| 4d4764eefb | |||
| d44a83faaa | |||
| 2337fe37be | |||
| c5b71cb484 | |||
| 480c95d4d6 | |||
| 8a45e965eb | |||
| c9717693d0 | |||
| 0fa3fe3d43 | |||
| ed090f7ecb | |||
| f916f96761 | |||
| 79e766c4e1 | |||
| 183273dcf5 | |||
| b3bb7c694b | |||
| 456a74db05 | |||
| fde724a658 | |||
| 574ee5d0bb | |||
| 03a838ba2d | |||
| b00aacbc41 | |||
| 532c33a13b | |||
| 6a7396bc1d | |||
| 870b2bf36d | |||
| 07b69be7bf | |||
| b41640837c | |||
| bee6ebe3b4 | |||
| 5989298d3f | |||
| 0235f19543 | |||
| cd6c2f68ca | |||
| 75252cf9d5 | |||
| 1da714852f | |||
| fe6b788f6b | |||
| fb3fec299f | |||
| d2f435b265 | |||
| 3a163acd6d | |||
| df4bf9b688 | |||
| 0f67a2f194 | |||
| b75946af46 | |||
| b9b188693c | |||
| 9fe008d407 | |||
| e905c355f9 | |||
| 12821fb70d | |||
| 2aaa316280 | |||
| 918d9b8b1f | |||
| 536a6f3f83 | |||
| b9fd3d37f2 | |||
| e0b87c3ff2 | |||
| 48a9fd46a6 | |||
| 8669c01eaa | |||
| 37949ba1ec | |||
| b2bbaf050c | |||
| 4d6837ea53 | |||
| c8ac970d11 | |||
| a3a429bc41 | |||
| 0726b1b4d9 | |||
| defc4f45ff | |||
| a984127e28 | |||
| 85dd303c88 | |||
| 2958144fd0 | |||
| ade8f18a95 | |||
| 46e2e29ead | |||
| 63a395125a | |||
| 6f973afa36 | |||
| 1ad0ef3f61 | |||
| 90bf46e8d3 | |||
| c1c285db86 | |||
| f1b541a1eb | |||
| 585257c995 | |||
| 93f80cdc50 | |||
| a832193010 | |||
| 383659fb8a | |||
| 4725e55090 | |||
| 07c85ee6f5 | |||
| c02dd9e040 | |||
| 7dae94976d | |||
| 38c360cb74 | |||
| edba640641 | |||
| cf25ae4448 | |||
| e86182af56 | |||
| 902a1ce46e | |||
| 55849cad65 | |||
| edb76af820 | |||
| 04d0b819f6 | |||
| 85aa5fbd46 | |||
| 1054e54c89 | |||
| 2fe5b92035 | |||
| 3a5a161c03 | |||
| 8451321a79 | |||
| 90050a0008 | |||
| 87c9efce4a | |||
| 240e049e46 | |||
| 0899b49bb3 | |||
| fc0660ca8d | |||
| b2ed5dbbc8 | |||
| 94c864503e | |||
| c387c6aebf | |||
| 55c9d39c13 | |||
| 6a3b143876 | |||
| 5f29abfa0d | |||
| ea3364ac12 | |||
| 2b31eac1af | |||
| 093139bc91 | |||
| 6c8a4a6a28 | |||
| f5d2fd6190 | |||
| 4d77bd8f25 | |||
| 1068adbee3 | |||
| 86fc518585 | |||
| 78377a5c67 | |||
| 56f2dcc073 | |||
| 48739b4141 | |||
| 704c344213 | |||
| ee263ba68e | |||
| 692b71af23 | |||
| 7e029f1db0 | |||
| d4875afe31 | |||
| de0e431d00 | |||
| 6b304391b5 | |||
| 8f85e4c88f | |||
| a3b9c3b884 | |||
| 021e16959f | |||
| 37adab76e1 | |||
| 5d342ee1ab | |||
| 697d131397 | |||
| 89d9e30025 | |||
| dfde86df76 | |||
| d55fb25a40 | |||
| 492fcce053 | |||
| d61f17dbd3 | |||
| 8996979dc2 | |||
| 31ddb242a6 | |||
| b7b5d96b3b | |||
| 2e55e5fd05 | |||
| 40ebf8006e | |||
| 8e8643122e | |||
| c00a0b12e4 | |||
| 0e21520fd1 | |||
| 0db233e5c7 | |||
| b9948085a4 | |||
| d62526fde6 | |||
| 134f9c014e | |||
| 056cf472d9 | |||
| 0c161cc0c2 | |||
| 7894894610 | |||
| 7d48bef70a | |||
| 6bc3e257f8 | |||
| adc613d801 | |||
| fd573f6c1c | |||
| 1235e966d2 | |||
| e6a2f049ea | |||
| d9e9dd04af | |||
| 3c2fa15dc2 | |||
| e8c9c20570 | |||
| 0db2578ca0 | |||
| e71d6157f1 | |||
| 7ca2a0df18 | |||
| 7a4af834c2 | |||
| 314a0d124d | |||
| 6f1e86e8a7 | |||
| 840c7ea560 | |||
| 1839730663 | |||
| afd75cc4cf | |||
| 453a1aaa65 | |||
| 4905185e61 | |||
| 0990430513 | |||
| 7df13a044f | |||
| 6135a1f60a | |||
| 5e94adc73a | |||
| 44fc0228a9 | |||
| 4d2bfe3ef3 | |||
| 7f37ff032f | |||
| 30d508464f | |||
| 75b0fed00d | |||
| 927cc1fcf5 | |||
| b3090e277a | |||
| 0b45151a3d | |||
| eb5bfa9b5d | |||
| 4267e94dcd | |||
| fac9e7612f | |||
| 01e8e535f4 | |||
| e5e2e7571f | |||
| c889a8c8de | |||
| cbf2ea7b48 | |||
| 8078347880 | |||
| dd8a802bec | |||
| 6f6602ddd1 | |||
| 33e69db436 | |||
| 13cbec28d6 | |||
| 826083adf6 | |||
| a62bd28e1f | |||
| da1188c6ee | |||
| e22cefbfe3 | |||
| 02ea696070 | |||
| d25702d99d | |||
| c554413327 | |||
| f8c5f238ea | |||
| 66b46c8618 | |||
| 04689d37b7 | |||
| e041cacf4b | |||
| f54fc92b14 | |||
| 6e7b6bcc35 | |||
| e70d5be535 | |||
| 2ce51c8c2e | |||
| c621ec0c50 | |||
| 2ed2a1431d | |||
| 3a425394ca | |||
| d98ba463ad | |||
| c3d97acb31 | |||
| 8dbd464fa4 | |||
| 63a5bb1bbf | |||
| 8c03878808 | |||
| d29e50e50b | |||
| 687bf8b208 | |||
| 10d0807395 | |||
| 0a89c87190 | |||
| a7df92a64c | |||
| 4d3043c041 | |||
| 8261a18472 | |||
| a9f04c0bf4 | |||
| 84334a6bc9 | |||
| 1e5596f977 | |||
| b67fa3a38c | |||
| 8b0123042f | |||
| 16890a659e | |||
| d40ac7f89d | |||
| 2694f48b87 | |||
| 0f91aeb2d3 | |||
| bc0111f895 | |||
| f8a3e9b332 | |||
| db9397fd3e | |||
| 20717ff128 | |||
| a532a88585 | |||
| 0b077bd523 | |||
| e008efcf97 | |||
| 7cf676f9bd | |||
| b83538d08d | |||
| 3c31c340d8 | |||
| 59620456ee | |||
| e42bd29382 | |||
| 18dbc21f96 | |||
| 68d871c47c | |||
| a45a219e5f | |||
| 49788240aa | |||
| 52166500fd | |||
| 3c8fef2655 | |||
| 21af2f2f62 | |||
| 6d01296e8b | |||
| 749efcde21 | |||
| 3af2420da9 | |||
| d0d24f4554 | |||
| 96b44c1771 | |||
| 4a1d07b364 | |||
| 7fbe5349a2 | |||
| d8bad72fd6 | |||
| 927b32c99f | |||
| 27b691bef6 | |||
| d2b9de740d | |||
| eb45b4b1bd | |||
| c545b5e65f | |||
| 992b6486a1 | |||
| 421b1f889b | |||
| 1ee1368e18 | |||
| 1a6a8789c1 | |||
| b5c155db1b | |||
| 6c5b46eadd | |||
| 5abbab2635 | |||
| e5766094f4 | |||
| 96d77988d3 | |||
| e9e4fd4e8b | |||
| b5d9fbf1f4 | |||
| 81dc89f85b | |||
| 76305045db | |||
| c4ba48d7a9 | |||
| d5dcdf13bf | |||
| 7feb3dccbf | |||
| 2251d74fce | |||
| 2c89d3fa7f | |||
| 49cc634678 | |||
| c93e81c632 | |||
| 050e202291 | |||
| efe04d6602 | |||
| 823b8324c7 | |||
| ae2f88a9ee | |||
| 32287c60c4 | |||
| 69c6fa0a24 | |||
| c3e63df7b5 | |||
| fb4f1e30d5 | |||
| 6d0fde218b | |||
| 6d02b0a26d | |||
| 495bf9c954 | |||
| cb1bc95e6b | |||
| 6fdf0266cf | |||
| ee26198e2c | |||
| fe424197da | |||
| 053ca232aa | |||
| 2e8d566bd4 | |||
| dcb0e9b3e8 | |||
| 3497882ea6 | |||
| dc2463a0ab | |||
| b514717076 | |||
| 23932952d8 | |||
| 266ee7626c | |||
| c1ba1f409c | |||
| 0f669e8ca3 | |||
| 9de711a4f5 | |||
| e1965f121c | |||
| b2c9b3500c | |||
| f60d26c4bf | |||
| b43044fee9 | |||
| 2bc66e4574 | |||
| a641a093ad | |||
| 704369d431 | |||
| 5aa8191cd3 | |||
| bb43804d58 | |||
| 7a8d14c85c | |||
| d1e85633fb | |||
| a264f7ab36 | |||
| 4e134772d7 | |||
| 3eb6c91c9e | |||
| e7fd53b809 | |||
| 164ec1e31d | |||
| 373a5f18d6 | |||
| 36058ead7d | |||
| 33fd5b4634 | |||
| 91647978e6 | |||
| d0b64f4bd5 | |||
| c23d58bc27 | |||
| 49a9475c4a | |||
| 364032fe84 | |||
| 5658584803 | |||
| d580200989 | |||
| c2cdde73d1 | |||
| bc74d4d7f8 | |||
| 535c611f2d | |||
| 6efbf65405 | |||
| abb121224d | |||
| 8ad4657d6f | |||
| 6d3f1e00a5 | |||
| c38f6c471d | |||
| 85462de254 | |||
| 61d4f17f5d | |||
| 09cf73cd3c | |||
| ffa3cefa67 | |||
| 8ec2cc8582 | |||
| 75dfa0643b | |||
| d5cde43158 | |||
| 6493a9a1f2 | |||
| f35e059194 |
+2
-2
@@ -6,7 +6,7 @@
|
||||
/captures/
|
||||
/local.properties
|
||||
.DS_Store
|
||||
.externalNativeBuild/
|
||||
.cxx/
|
||||
Thumbs.db
|
||||
build/
|
||||
*.apk
|
||||
@@ -15,4 +15,4 @@ build/
|
||||
*.iml
|
||||
*.jks
|
||||
keystore.properties
|
||||
package-info.java
|
||||
gradlew.bat
|
||||
|
||||
+3
-9
@@ -1,9 +1,3 @@
|
||||
[submodule "app/tools/libmnl"]
|
||||
path = app/tools/libmnl
|
||||
url = https://git.netfilter.org/libmnl/
|
||||
[submodule "app/tools/wireguard"]
|
||||
path = app/tools/wireguard
|
||||
url = https://git.zx2c4.com/WireGuard
|
||||
[submodule "app/tools/wireguard-go"]
|
||||
path = app/tools/wireguard-go
|
||||
url = https://git.zx2c4.com/wireguard-go
|
||||
[submodule "tunnel/tools/wireguard-tools"]
|
||||
path = tunnel/tools/wireguard-tools
|
||||
url = https://git.zx2c4.com/wireguard-tools
|
||||
|
||||
Generated
+32
-93
@@ -29,16 +29,10 @@
|
||||
<emptyLine />
|
||||
</value>
|
||||
</option>
|
||||
<option name="RIGHT_MARGIN" value="100" />
|
||||
<AndroidXmlCodeStyleSettings>
|
||||
<option name="USE_CUSTOM_SETTINGS" value="true" />
|
||||
</AndroidXmlCodeStyleSettings>
|
||||
<JavaCodeStyleSettings>
|
||||
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
|
||||
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
|
||||
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
|
||||
<value />
|
||||
</option>
|
||||
<option name="GENERATE_FINAL_LOCALS" value="true" />
|
||||
<option name="GENERATE_FINAL_PARAMETERS" value="true" />
|
||||
<option name="INSERT_INNER_CLASS_IMPORTS" value="true" />
|
||||
<option name="IMPORT_LAYOUT_TABLE">
|
||||
<value>
|
||||
<package name="android" withSubpackages="true" static="false" />
|
||||
@@ -62,35 +56,25 @@
|
||||
</value>
|
||||
</option>
|
||||
</JavaCodeStyleSettings>
|
||||
<Objective-C-extensions>
|
||||
<file>
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
|
||||
</file>
|
||||
<class>
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
|
||||
</class>
|
||||
<extensions>
|
||||
<pair source="cpp" header="h" fileNamingConvention="NONE" />
|
||||
<pair source="c" header="h" fileNamingConvention="NONE" />
|
||||
</extensions>
|
||||
</Objective-C-extensions>
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
<value>
|
||||
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="PACKAGES_IMPORT_LAYOUT">
|
||||
<value>
|
||||
<package name="" alias="false" withSubpackages="true" />
|
||||
<package name="java" alias="false" withSubpackages="true" />
|
||||
<package name="javax" alias="false" withSubpackages="true" />
|
||||
<package name="kotlin" alias="false" withSubpackages="true" />
|
||||
<package name="" alias="true" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="10" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="10" />
|
||||
</JetCodeStyleSettings>
|
||||
<XML>
|
||||
<option name="XML_KEEP_LINE_BREAKS" value="false" />
|
||||
<option name="XML_ALIGN_ATTRIBUTES" value="false" />
|
||||
<option name="XML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
|
||||
</XML>
|
||||
<codeStyleSettings language="JAVA">
|
||||
@@ -382,7 +366,6 @@
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
@@ -393,6 +376,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@@ -403,6 +387,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@@ -414,6 +399,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@@ -424,6 +410,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@@ -434,6 +421,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@@ -444,6 +432,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@@ -454,6 +443,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@@ -464,64 +454,12 @@
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:layout_width</NAME>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:layout_height</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:layout_.*</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:width</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:height</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
@@ -529,6 +467,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
<component name="CopyrightManager">
|
||||
<copyright>
|
||||
<option name="notice" value="Copyright © &#36;today.year Firstname Lastname <email@example.org> SPDX-License-Identifier: Apache-2.0
|
||||
<option name="notice" value="Copyright © &#36;today.year WireGuard LLC. All Rights Reserved. SPDX-License-Identifier: Apache-2.0" />
|
||||
<option name="myName" value="Default" />
|
||||
</copyright>
|
||||
</component>
|
||||
|
||||
Generated
+2
-2
@@ -1,3 +1,3 @@
|
||||
<component name="CopyrightManager">
|
||||
<settings default="GPL-2.0-or-later" />
|
||||
</component>
|
||||
<settings default="Default" />
|
||||
</component>
|
||||
|
||||
Generated
+14
-14
@@ -9,7 +9,6 @@
|
||||
</option>
|
||||
<option name="nonThreadSafeTypes" value="" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="AndroidLintGoogleAppIndexingWarning" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="AndroidLintIconExpectedSize" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AndroidLintNegativeMargin" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AndroidLintTypographyQuotes" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
@@ -40,7 +39,9 @@
|
||||
<inspection_tool class="AssignmentToLambdaParameter" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AssignmentToSuperclassField" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AssignmentUsedAsCondition" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="METHOD_MATCHER_CONFIG" value="java.util.Formatter,format,java.io.Writer,append,com.google.common.base.Preconditions,checkNotNull,org.hibernate.Session,close,java.io.PrintWriter,printf,java.util.HashMap,put,java.util.Map,put" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="BadExceptionCaught" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="exceptionsString" value="" />
|
||||
<option name="exceptions">
|
||||
@@ -59,6 +60,9 @@
|
||||
<inspection_tool class="CallToStringConcatCanBeReplacedByOperator" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="CannotResolve" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="CastConflictsWithInstanceof" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="CatchMayIgnoreException" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="m_ignoreCatchBlocksWithComments" value="false" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ChainedEquality" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ClassInitializer" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ClassNameDiffersFromFileName" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
@@ -103,9 +107,6 @@
|
||||
<inspection_tool class="DoubleLiteralMayBeFloatLiteral" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="DuplicateAlternationBranch" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="DuplicateBooleanBranch" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="DuplicateCondition" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoreSideEffectConditions" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="DuplicateDeclarations" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="DynamicRegexReplaceableByCompiledPattern" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ElementOnlyUsedFromTestCode" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
@@ -196,7 +197,6 @@
|
||||
<inspection_tool class="ImplicitSubclassInspection" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="IncompatibleTypes" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="InitializerIssues" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="InnerClassMayBeStatic" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="InnerClassReferencedViaSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="InnerClassVariableHidesOuterClassVariable" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="m_ignoreInvisibleFields" value="true" />
|
||||
@@ -215,7 +215,6 @@
|
||||
<inspection_tool class="InstanceofThis" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="InstantiationOfUtilityClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="IntLiteralMayBeLongLiteral" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="IntegerDivisionInFloatingPointContext" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="IntegerTypeRequired" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="InterfaceMayBeAnnotatedFunctional" enabled="true" level="INFORMATION" enabled_by_default="true" />
|
||||
<inspection_tool class="InterfaceNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
|
||||
@@ -294,6 +293,7 @@
|
||||
<option name="ignoreForLoopDeclarations" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="MultipleTopLevelClassesInFile" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="MultipleVariablesInDeclaration" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="NamedResource" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="NativeMethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="NegatedConditional" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
@@ -347,7 +347,6 @@
|
||||
<inspection_tool class="OverriddenMethodCallDuringObjectConstruction" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="PackageInfoWithoutPackage" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="PointerTypeRequired" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="PointlessNullCheck" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ProblematicVarargsMethodOverride" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ProtectedField" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ProtectedInnerClass" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
@@ -385,7 +384,6 @@
|
||||
<option name="ignorePrivateMethods" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ReturnOfDateField" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="ReturnOfInnerClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ScalarTypeRequired" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="SerializableClassInSecureContext" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="SetReplaceableByEnumSet" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
@@ -395,7 +393,13 @@
|
||||
<inspection_tool class="SizeReplaceableByIsEmpty" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="StaticCallOnSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="StaticFieldReferenceOnSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="StaticImport" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="StaticImport" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="allowedClasses">
|
||||
<set>
|
||||
<option value="org.junit.Assert" />
|
||||
</set>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="StaticInheritance" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="StaticMethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
|
||||
<option name="m_regex" value="[a-z][A-Za-z\d]*" />
|
||||
@@ -433,7 +437,6 @@
|
||||
<inspection_tool class="TemplateArgumentsIssues" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="TestMethodWithoutAssertion" enabled="false" level="WARNING" enabled_by_default="false">
|
||||
<option name="assertionMethods" value="org.junit.Assert,assert.*|fail.*,junit.framework.Assert,assert.*|fail.*,org.junit.jupiter.api.Assertions,assert.*|fail.*,org.mockito.Mockito,verify.*,org.mockito.InOrder,verify,org.junit.rules.ExpectedException,expect.*,org.hamcrest.MatcherAssert,assertThat" />
|
||||
<option name="assertKeywordIsAssertion" value="false" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="TestNGMethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="ThisEscapedInConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
@@ -441,7 +444,6 @@
|
||||
<option name="ignoreRethrownExceptions" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ThrowsRuntimeException" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ToArrayCallWithZeroLengthArrayArgument" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="TooBroadScope" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="m_allowConstructorAsInitializer" value="false" />
|
||||
<option name="m_onlyLookAtBlocks" value="true" />
|
||||
@@ -467,10 +469,8 @@
|
||||
<inspection_tool class="UnnecessaryBlockStatement" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoreSwitchBranches" value="false" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="UnnecessaryCallToStringValueOf" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnnecessaryConstantArrayCreationExpression" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnnecessaryConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnnecessaryDefault" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnnecessaryExplicitNumericCast" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnnecessaryFullyQualifiedName" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="m_ignoreJavadoc" value="false" />
|
||||
|
||||
@@ -7,9 +7,30 @@ This is an Android GUI for [WireGuard](https://www.wireguard.com/). It [opportun
|
||||
## Building
|
||||
|
||||
```
|
||||
$ git clone https://git.zx2c4.com/wireguard-android
|
||||
$ git clone --recurse-submodules https://git.zx2c4.com/wireguard-android
|
||||
$ cd wireguard-android
|
||||
$ git submodule init
|
||||
$ git submodule update
|
||||
$ ./gradlew assembleRelease
|
||||
```
|
||||
|
||||
macOS users may need [flock(1)](https://github.com/discoteq/flock).
|
||||
|
||||
## Embedding
|
||||
|
||||
The tunnel library is [on JCenter](https://bintray.com/wireguard/wireguard-android/wireguard-android/_latestVersion), alongside [extensive class library documentation](https://javadoc.io/doc/com.wireguard.android/tunnel).
|
||||
|
||||
```
|
||||
implementation 'com.wireguard.android:tunnel:$wireguardTunnelVersion'
|
||||
```
|
||||
|
||||
The library makes use of Java 8 features, so be sure to support those in your gradle configuration:
|
||||
|
||||
```
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
```
|
||||
|
||||
## Translating
|
||||
|
||||
Please help us translate the app into several languages on [our translation platform](https://crowdin.com/project/WireGuard).
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply from: 'nonnull.gradle'
|
||||
|
||||
// Create a variable called keystorePropertiesFile, and initialize it to your
|
||||
// keystore.properties file, in the rootProject folder.
|
||||
final def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
|
||||
android {
|
||||
buildToolsVersion '28.0.1'
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
compileSdkVersion 27
|
||||
dataBinding.enabled true
|
||||
defaultConfig {
|
||||
applicationId 'com.wireguard.android'
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 27
|
||||
versionCode 438
|
||||
versionName '0.0.20180729'
|
||||
buildConfigField 'int', 'MIN_SDK_VERSION', "$minSdkVersion.apiLevel"
|
||||
}
|
||||
// If the keystore file exists
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
// Initialize a new Properties() object called keystoreProperties.
|
||||
final def keystoreProperties = new Properties()
|
||||
|
||||
// Load your keystore.properties file into the keystoreProperties object.
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
storeFile file(keystoreProperties['storeFile'])
|
||||
storePassword keystoreProperties['storePassword']
|
||||
}
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
if (keystorePropertiesFile.exists()) signingConfig signingConfigs.release
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments "-DANDROID_PACKAGE_NAME=${android.defaultConfig.applicationId}"
|
||||
}
|
||||
}
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
versionNameSuffix "-debug"
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments "-DANDROID_PACKAGE_NAME=${android.defaultConfig.applicationId}${applicationIdSuffix}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path 'tools/CMakeLists.txt'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ext {
|
||||
databindingVersion = '3.1.3'
|
||||
supportLibsVersion = '27.1.1'
|
||||
streamsupportVersion = '1.6.0'
|
||||
jsr305Version = '3.0.2'
|
||||
zxingEmbeddedVersion = '3.6.0'
|
||||
acraVersion = '5.2.0-rc2'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "com.android.databinding:library:$databindingVersion"
|
||||
implementation "com.android.support:appcompat-v7:$supportLibsVersion"
|
||||
implementation "com.android.support:cardview-v7:$supportLibsVersion"
|
||||
implementation "com.android.support:design:$supportLibsVersion"
|
||||
implementation "com.android.support:preference-v14:$supportLibsVersion"
|
||||
implementation "com.android.support:support-annotations:$supportLibsVersion"
|
||||
implementation "net.sourceforge.streamsupport:android-retrofuture:$streamsupportVersion"
|
||||
implementation "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion"
|
||||
implementation "com.google.code.findbugs:jsr305:$jsr305Version"
|
||||
implementation "com.journeyapps:zxing-android-embedded:$zxingEmbeddedVersion"
|
||||
implementation "ch.acra:acra-http:$acraVersion"
|
||||
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.compilerArgs << '-Xlint:unchecked'
|
||||
options.deprecation = true
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Eric Kuck <eric@bluelinelabs.com>.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
task generateNonNullJavaFiles(dependsOn: "assembleDebug", type: Copy) {
|
||||
group = "Copying"
|
||||
description = "Generate package-info.java classes"
|
||||
|
||||
def basePackage = "com" + File.separatorChar + "wireguard"
|
||||
def mainSrcPhrase = "src" + File.separatorChar + "main" + File.separatorChar +
|
||||
"java" + File.separatorChar
|
||||
def mainTestSrcPhrase = "src" + File.separatorChar + "test" + File.separatorChar +
|
||||
"java" + File.separatorChar
|
||||
def mainAndroidTestSrcPhrase = "src" + File.separatorChar + "androidTest" + File.separatorChar +
|
||||
"java" + File.separatorChar
|
||||
|
||||
def sourceDir = file( "${projectDir}" + File.separatorChar + "src" + File.separatorChar +
|
||||
"main" + File.separatorChar + "java" + File.separatorChar +
|
||||
basePackage )
|
||||
def testSourceDir = file( "${projectDir}" + File.separatorChar + "src" + File.separatorChar +
|
||||
"test" + File.separatorChar + "java" + File.separatorChar +
|
||||
basePackage)
|
||||
def androidTestSourceDir = file( "${projectDir}" + File.separatorChar + "src" + File
|
||||
.separatorChar +
|
||||
"androidTest" + File.separatorChar + "java" + File.separatorChar +
|
||||
basePackage )
|
||||
|
||||
generateInfoFiles(sourceDir, mainSrcPhrase);
|
||||
sourceDir.eachDirRecurse { dir ->
|
||||
generateInfoFiles(dir, mainSrcPhrase)
|
||||
}
|
||||
if (file(testSourceDir).exists()) {
|
||||
generateInfoFiles(testSourceDir, mainTestSrcPhrase);
|
||||
testSourceDir.eachDirRecurse { dir ->
|
||||
generateInfoFiles(dir, mainTestSrcPhrase)
|
||||
}
|
||||
}
|
||||
if (file(androidTestSourceDir).exists()) {
|
||||
generateInfoFiles(androidTestSourceDir, mainAndroidTestSrcPhrase);
|
||||
androidTestSourceDir.eachDirRecurse { dir ->
|
||||
generateInfoFiles(dir, mainAndroidTestSrcPhrase)
|
||||
}
|
||||
}
|
||||
println "[SUCCESS] NonNull generator: package-info.java files checked"
|
||||
}
|
||||
|
||||
private void generateInfoFiles(File dir, String mainSrcPhrase) {
|
||||
def infoFileContentHeader = getFileContentHeader();
|
||||
def infoFileContentFooter = getFileContentFooter();
|
||||
def infoFilePath = dir.getAbsolutePath() + File.separatorChar + "package-info.java"
|
||||
|
||||
//file(infoFilePath).delete(); //do not use in production code
|
||||
if (!file(infoFilePath).exists()) {
|
||||
def infoFileContentPackage = getFileContentPackage(dir.getAbsolutePath(), mainSrcPhrase);
|
||||
new File(infoFilePath).write(infoFileContentHeader +
|
||||
infoFileContentPackage + infoFileContentFooter)
|
||||
println "[dir] " + infoFilePath + " created";
|
||||
}
|
||||
}
|
||||
|
||||
def getFileContentPackage(String path, String mainSrcPhrase) {
|
||||
def mainSrcPhraseIndex = path.indexOf(mainSrcPhrase)
|
||||
def output = path.substring(mainSrcPhraseIndex)
|
||||
|
||||
// Win hotfix
|
||||
if (System.properties['os.name'].toLowerCase().contains('windows')) {
|
||||
output = output.replace("\\", "/")
|
||||
mainSrcPhrase = mainSrcPhrase.replace("\\", "/")
|
||||
}
|
||||
|
||||
return "package " + output.replaceAll(mainSrcPhrase, "").replaceAll(
|
||||
"/", ".") + ";\n"
|
||||
}
|
||||
|
||||
def getFileContentHeader() {
|
||||
return "/**\n" +
|
||||
" * Make all method parameters @NonNull by default.\n" +
|
||||
" */\n" +
|
||||
"@NonNullForAll\n"
|
||||
}
|
||||
|
||||
def getFileContentFooter() {
|
||||
return "\n" +
|
||||
"import com.wireguard.util.NonNullForAll;\n"
|
||||
}
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.Signature;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AppCompatDelegate;
|
||||
|
||||
import com.wireguard.android.backend.Backend;
|
||||
import com.wireguard.android.backend.GoBackend;
|
||||
import com.wireguard.android.backend.WgQuickBackend;
|
||||
import com.wireguard.android.configStore.FileConfigStore;
|
||||
import com.wireguard.android.model.TunnelManager;
|
||||
import com.wireguard.android.util.AsyncWorker;
|
||||
import com.wireguard.android.util.RootShell;
|
||||
import com.wireguard.android.util.ToolsInstaller;
|
||||
|
||||
import org.acra.ACRA;
|
||||
import org.acra.annotation.AcraCore;
|
||||
import org.acra.annotation.AcraHttpSender;
|
||||
import org.acra.data.StringFormat;
|
||||
import org.acra.sender.HttpSender;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import java9.util.concurrent.CompletableFuture;
|
||||
|
||||
|
||||
@AcraCore(reportFormat = StringFormat.JSON,
|
||||
buildConfigClass = BuildConfig.class,
|
||||
logcatArguments = {"-b", "all", "-d", "-v", "threadtime", "*:V"},
|
||||
excludeMatchingSharedPreferencesKeys = {"last_used_tunnel", "enabled_configs"})
|
||||
@AcraHttpSender(uri = "https://crashreport.zx2c4.com/android/report",
|
||||
basicAuthLogin = "6RCovLxEVCTXGiW5",
|
||||
basicAuthPassword = "O7I3sVa5ULVdiC51",
|
||||
httpMethod = HttpSender.Method.POST,
|
||||
compress = true)
|
||||
public class Application extends android.app.Application {
|
||||
@SuppressWarnings("NullableProblems") private static WeakReference<Application> weakSelf;
|
||||
@SuppressWarnings("NullableProblems") private AsyncWorker asyncWorker;
|
||||
@SuppressWarnings("NullableProblems") private RootShell rootShell;
|
||||
@SuppressWarnings("NullableProblems") private SharedPreferences sharedPreferences;
|
||||
@SuppressWarnings("NullableProblems") private ToolsInstaller toolsInstaller;
|
||||
@SuppressWarnings("NullableProblems") private TunnelManager tunnelManager;
|
||||
@Nullable private Backend backend;
|
||||
private final CompletableFuture<Backend> futureBackend = new CompletableFuture<>();
|
||||
|
||||
public Application() {
|
||||
weakSelf = new WeakReference<>(this);
|
||||
}
|
||||
|
||||
/* The ACRA password can be trivially reverse engineered and is open source anyway,
|
||||
* so there's no point in trying to protect it. However, we do want to at least
|
||||
* prevent innocent self-builders from uploading stuff to our crash reporter. So, we
|
||||
* check the DN of the certs that signed the apk, without even bothering to try
|
||||
* validating that they're authentic. It's a good enough heuristic.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Nullable
|
||||
private static String getInstallSource(final Context context) {
|
||||
if (BuildConfig.DEBUG)
|
||||
return null;
|
||||
try {
|
||||
final CertificateFactory cf = CertificateFactory.getInstance("X509");
|
||||
for (final Signature sig : context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES).signatures) {
|
||||
try {
|
||||
for (final String category : ((X509Certificate) cf.generateCertificate(new ByteArrayInputStream(sig.toByteArray()))).getSubjectDN().getName().split(", *")) {
|
||||
final String[] parts = category.split("=", 2);
|
||||
if (!"O".equals(parts[0]))
|
||||
continue;
|
||||
switch (parts[1]) {
|
||||
case "Google Inc.":
|
||||
return "Play Store";
|
||||
case "fdroid.org":
|
||||
return "F-Droid";
|
||||
}
|
||||
}
|
||||
} catch (final Exception ignored) { }
|
||||
}
|
||||
} catch (final Exception ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(final Context context) {
|
||||
super.attachBaseContext(context);
|
||||
|
||||
if (BuildConfig.MIN_SDK_VERSION > Build.VERSION.SDK_INT) {
|
||||
final Intent intent = new Intent(Intent.ACTION_MAIN);
|
||||
intent.addCategory(Intent.CATEGORY_HOME);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
final String installSource = getInstallSource(context);
|
||||
if (installSource != null) {
|
||||
ACRA.init(this);
|
||||
ACRA.getErrorReporter().putCustomData("installSource", installSource);
|
||||
}
|
||||
}
|
||||
|
||||
public static Application get() {
|
||||
return weakSelf.get();
|
||||
}
|
||||
|
||||
public static AsyncWorker getAsyncWorker() {
|
||||
return get().asyncWorker;
|
||||
}
|
||||
|
||||
public static Backend getBackend() {
|
||||
final Application app = get();
|
||||
synchronized (app.futureBackend) {
|
||||
if (app.backend == null) {
|
||||
Backend backend = null;
|
||||
if (new File("/sys/module/wireguard").exists()) {
|
||||
try {
|
||||
app.rootShell.start();
|
||||
backend = new WgQuickBackend(app.getApplicationContext());
|
||||
} catch (final Exception ignored) {
|
||||
}
|
||||
}
|
||||
if (backend == null)
|
||||
backend = new GoBackend(app.getApplicationContext());
|
||||
app.backend = backend;
|
||||
}
|
||||
return app.backend;
|
||||
}
|
||||
}
|
||||
|
||||
public static CompletableFuture<Backend> getBackendAsync() {
|
||||
return get().futureBackend;
|
||||
}
|
||||
|
||||
public static RootShell getRootShell() {
|
||||
return get().rootShell;
|
||||
}
|
||||
|
||||
public static SharedPreferences getSharedPreferences() {
|
||||
return get().sharedPreferences;
|
||||
}
|
||||
|
||||
public static ToolsInstaller getToolsInstaller() {
|
||||
return get().toolsInstaller;
|
||||
}
|
||||
|
||||
public static TunnelManager getTunnelManager() {
|
||||
return get().tunnelManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
asyncWorker = new AsyncWorker(AsyncTask.SERIAL_EXECUTOR, new Handler(Looper.getMainLooper()));
|
||||
rootShell = new RootShell(getApplicationContext());
|
||||
toolsInstaller = new ToolsInstaller(getApplicationContext());
|
||||
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
sharedPreferences.getBoolean("dark_theme", false) ?
|
||||
AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
|
||||
|
||||
tunnelManager = new TunnelManager(new FileConfigStore(getApplicationContext()));
|
||||
tunnelManager.onCreate();
|
||||
|
||||
asyncWorker.supplyAsync(Application::getBackend).thenAccept(backend -> {
|
||||
futureBackend.complete(backend);
|
||||
if (ACRA.isInitialised()) {
|
||||
ACRA.getErrorReporter().putCustomData("backend", backend.getClass().getSimpleName());
|
||||
asyncWorker.supplyAsync(backend::getVersion).thenAccept(version ->
|
||||
ACRA.getErrorReporter().putCustomData("backendVersion", version));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.backend.WgQuickBackend;
|
||||
import com.wireguard.android.model.TunnelManager;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
|
||||
public class BootShutdownReceiver extends BroadcastReceiver {
|
||||
private static final String TAG = "WireGuard/" + BootShutdownReceiver.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
Application.getBackendAsync().thenAccept(backend -> {
|
||||
if (!(backend instanceof WgQuickBackend))
|
||||
return;
|
||||
final String action = intent.getAction();
|
||||
if (action == null)
|
||||
return;
|
||||
final TunnelManager tunnelManager = Application.getTunnelManager();
|
||||
if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
|
||||
Log.i(TAG, "Broadcast receiver restoring state (boot)");
|
||||
tunnelManager.restoreState(false).whenComplete(ExceptionLoggers.D);
|
||||
} else if (Intent.ACTION_SHUTDOWN.equals(action)) {
|
||||
Log.i(TAG, "Broadcast receiver saving state (shutdown)");
|
||||
tunnelManager.saveState();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Intent;
|
||||
import android.databinding.Observable;
|
||||
import android.databinding.Observable.OnPropertyChangedCallback;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.service.quicksettings.Tile;
|
||||
import android.service.quicksettings.TileService;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.wireguard.android.activity.MainActivity;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.Tunnel.State;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
import com.wireguard.android.widget.SlashDrawable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Service that maintains the application's custom Quick Settings tile. This service is bound by the
|
||||
* system framework as necessary to update the appearance of the tile in the system UI, and to
|
||||
* forward click events to the application.
|
||||
*/
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
public class QuickTileService extends TileService {
|
||||
private static final String TAG = "WireGuard/" + QuickTileService.class.getSimpleName();
|
||||
|
||||
private final OnStateChangedCallback onStateChangedCallback = new OnStateChangedCallback();
|
||||
private final OnTunnelChangedCallback onTunnelChangedCallback = new OnTunnelChangedCallback();
|
||||
@Nullable private Tunnel tunnel;
|
||||
@Nullable private Icon iconOn;
|
||||
@Nullable private Icon iconOff;
|
||||
|
||||
/* This works around an annoying unsolved frameworks bug some people are hitting. */
|
||||
@Override
|
||||
@Nullable
|
||||
public IBinder onBind(final Intent intent) {
|
||||
IBinder ret = null;
|
||||
try {
|
||||
ret = super.onBind(intent);
|
||||
} catch (final Exception e) {
|
||||
Log.d(TAG, "Failed to bind to TileService", e);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* TODO: for Android P: Build.VERSION_CODES.P */
|
||||
private static final int BUILD_VERSION_CODES_P = 28;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void onCreate() {
|
||||
if (Build.VERSION.SDK_INT >= BUILD_VERSION_CODES_P) {
|
||||
iconOff = iconOn = Icon.createWithResource(this, R.drawable.ic_tile);
|
||||
return;
|
||||
}
|
||||
final SlashDrawable icon = new SlashDrawable(getResources().getDrawable(R.drawable.ic_tile));
|
||||
icon.setAnimationEnabled(false); /* Unfortunately we can't have animations, since Icons are marshaled. */
|
||||
icon.setSlashed(false);
|
||||
Bitmap b = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
|
||||
Canvas c = new Canvas(b);
|
||||
icon.setBounds(0, 0, c.getWidth(), c.getHeight());
|
||||
icon.draw(c);
|
||||
iconOn = Icon.createWithBitmap(b);
|
||||
icon.setSlashed(true);
|
||||
b = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
|
||||
c = new Canvas(b);
|
||||
icon.setBounds(0, 0, c.getWidth(), c.getHeight());
|
||||
icon.draw(c);
|
||||
iconOff = Icon.createWithBitmap(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
if (tunnel != null) {
|
||||
final Tile tile = getQsTile();
|
||||
if (tile != null) {
|
||||
tile.setIcon(tile.getIcon() == iconOn ? iconOff : iconOn);
|
||||
tile.updateTile();
|
||||
}
|
||||
tunnel.setState(State.TOGGLE).whenComplete(this::onToggleFinished);
|
||||
} else {
|
||||
final Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivityAndCollapse(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartListening() {
|
||||
Application.getTunnelManager().addOnPropertyChangedCallback(onTunnelChangedCallback);
|
||||
if (tunnel != null)
|
||||
tunnel.addOnPropertyChangedCallback(onStateChangedCallback);
|
||||
updateTile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopListening() {
|
||||
if (tunnel != null)
|
||||
tunnel.removeOnPropertyChangedCallback(onStateChangedCallback);
|
||||
Application.getTunnelManager().removeOnPropertyChangedCallback(onTunnelChangedCallback);
|
||||
}
|
||||
|
||||
private void onToggleFinished(@SuppressWarnings("unused") final State state,
|
||||
@Nullable final Throwable throwable) {
|
||||
if (throwable == null)
|
||||
return;
|
||||
final String error = ExceptionLoggers.unwrapMessage(throwable);
|
||||
final String message = getString(R.string.toggle_error, error);
|
||||
Log.e(TAG, message, throwable);
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private void updateTile() {
|
||||
// Update the tunnel.
|
||||
final Tunnel newTunnel = Application.getTunnelManager().getLastUsedTunnel();
|
||||
if (newTunnel != tunnel) {
|
||||
if (tunnel != null)
|
||||
tunnel.removeOnPropertyChangedCallback(onStateChangedCallback);
|
||||
tunnel = newTunnel;
|
||||
if (tunnel != null)
|
||||
tunnel.addOnPropertyChangedCallback(onStateChangedCallback);
|
||||
}
|
||||
// Update the tile contents.
|
||||
final String label;
|
||||
final int state;
|
||||
final Tile tile = getQsTile();
|
||||
if (tunnel != null) {
|
||||
label = tunnel.getName();
|
||||
state = tunnel.getState() == Tunnel.State.UP ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
|
||||
} else {
|
||||
label = getString(R.string.app_name);
|
||||
state = Tile.STATE_INACTIVE;
|
||||
}
|
||||
if (tile == null)
|
||||
return;
|
||||
tile.setLabel(label);
|
||||
if (tile.getState() != state) {
|
||||
tile.setIcon(state == Tile.STATE_ACTIVE ? iconOn : iconOff);
|
||||
tile.setState(state);
|
||||
}
|
||||
tile.updateTile();
|
||||
}
|
||||
|
||||
private final class OnStateChangedCallback extends OnPropertyChangedCallback {
|
||||
@Override
|
||||
public void onPropertyChanged(final Observable sender, final int propertyId) {
|
||||
if (!Objects.equals(sender, tunnel)) {
|
||||
sender.removeOnPropertyChangedCallback(this);
|
||||
return;
|
||||
}
|
||||
if (propertyId != 0 && propertyId != BR.state)
|
||||
return;
|
||||
updateTile();
|
||||
}
|
||||
}
|
||||
|
||||
private final class OnTunnelChangedCallback extends OnPropertyChangedCallback {
|
||||
@Override
|
||||
public void onPropertyChanged(final Observable sender, final int propertyId) {
|
||||
if (propertyId != 0 && propertyId != BR.lastUsedTunnel)
|
||||
return;
|
||||
updateTile();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.activity;
|
||||
|
||||
import android.databinding.CallbackRegistry;
|
||||
import android.databinding.CallbackRegistry.NotifierCallback;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Base class for activities that need to remember the currently-selected tunnel.
|
||||
*/
|
||||
|
||||
public abstract class BaseActivity extends ThemeChangeAwareActivity {
|
||||
private static final String KEY_SELECTED_TUNNEL = "selected_tunnel";
|
||||
|
||||
private final SelectionChangeRegistry selectionChangeRegistry = new SelectionChangeRegistry();
|
||||
@Nullable private Tunnel selectedTunnel;
|
||||
|
||||
public void addOnSelectedTunnelChangedListener(final OnSelectedTunnelChangedListener listener) {
|
||||
selectionChangeRegistry.add(listener);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Tunnel getSelectedTunnel() {
|
||||
return selectedTunnel;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
// Restore the saved tunnel if there is one; otherwise grab it from the arguments.
|
||||
final String savedTunnelName;
|
||||
if (savedInstanceState != null)
|
||||
savedTunnelName = savedInstanceState.getString(KEY_SELECTED_TUNNEL);
|
||||
else if (getIntent() != null)
|
||||
savedTunnelName = getIntent().getStringExtra(KEY_SELECTED_TUNNEL);
|
||||
else
|
||||
savedTunnelName = null;
|
||||
|
||||
if (savedTunnelName != null)
|
||||
Application.getTunnelManager().getTunnels()
|
||||
.thenAccept(tunnels -> setSelectedTunnel(tunnels.get(savedTunnelName)));
|
||||
|
||||
// The selected tunnel must be set before the superclass method recreates fragments.
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(final Bundle outState) {
|
||||
if (selectedTunnel != null)
|
||||
outState.putString(KEY_SELECTED_TUNNEL, selectedTunnel.getName());
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
protected abstract void onSelectedTunnelChanged(@Nullable Tunnel oldTunnel, @Nullable Tunnel newTunnel);
|
||||
|
||||
public void removeOnSelectedTunnelChangedListener(
|
||||
final OnSelectedTunnelChangedListener listener) {
|
||||
selectionChangeRegistry.remove(listener);
|
||||
}
|
||||
|
||||
public void setSelectedTunnel(@Nullable final Tunnel tunnel) {
|
||||
final Tunnel oldTunnel = selectedTunnel;
|
||||
if (Objects.equals(oldTunnel, tunnel))
|
||||
return;
|
||||
selectedTunnel = tunnel;
|
||||
onSelectedTunnelChanged(oldTunnel, tunnel);
|
||||
selectionChangeRegistry.notifyCallbacks(oldTunnel, 0, tunnel);
|
||||
}
|
||||
|
||||
public interface OnSelectedTunnelChangedListener {
|
||||
void onSelectedTunnelChanged(@Nullable Tunnel oldTunnel, @Nullable Tunnel newTunnel);
|
||||
}
|
||||
|
||||
private static final class SelectionChangeNotifier
|
||||
extends NotifierCallback<OnSelectedTunnelChangedListener, Tunnel, Tunnel> {
|
||||
@Override
|
||||
public void onNotifyCallback(final OnSelectedTunnelChangedListener listener,
|
||||
final Tunnel oldTunnel, final int ignored,
|
||||
final Tunnel newTunnel) {
|
||||
listener.onSelectedTunnelChanged(oldTunnel, newTunnel);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SelectionChangeRegistry
|
||||
extends CallbackRegistry<OnSelectedTunnelChangedListener, Tunnel, Tunnel> {
|
||||
private SelectionChangeRegistry() {
|
||||
super(new SelectionChangeNotifier());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.activity;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.fragment.TunnelDetailFragment;
|
||||
import com.wireguard.android.fragment.TunnelEditorFragment;
|
||||
import com.wireguard.android.fragment.TunnelListFragment;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
|
||||
/**
|
||||
* CRUD interface for WireGuard tunnels. This activity serves as the main entry point to the
|
||||
* WireGuard application, and contains several fragments for listing, viewing details of, and
|
||||
* editing the configuration and interface state of WireGuard tunnels.
|
||||
*/
|
||||
|
||||
public class MainActivity extends BaseActivity
|
||||
implements FragmentManager.OnBackStackChangedListener {
|
||||
@Nullable private ActionBar actionBar;
|
||||
private boolean isTwoPaneLayout;
|
||||
@Nullable private TunnelListFragment listFragment;
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
final int backStackEntries = getSupportFragmentManager().getBackStackEntryCount();
|
||||
// If the action menu is visible and expanded, collapse it instead of navigating back.
|
||||
if (isTwoPaneLayout || backStackEntries == 0) {
|
||||
if (listFragment != null && listFragment.collapseActionMenu())
|
||||
return;
|
||||
}
|
||||
// If the two-pane layout does not have an editor open, going back should exit the app.
|
||||
if (isTwoPaneLayout && backStackEntries <= 1) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
// Deselect the current tunnel on navigating back from the detail pane to the one-pane list.
|
||||
if (!isTwoPaneLayout && backStackEntries == 1) {
|
||||
setSelectedTunnel(null);
|
||||
return;
|
||||
}
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
@Override public void onBackStackChanged() {
|
||||
if (actionBar == null)
|
||||
return;
|
||||
// Do not show the home menu when the two-pane layout is at the detail view (see above).
|
||||
final int backStackEntries = getSupportFragmentManager().getBackStackEntryCount();
|
||||
final int minBackStackEntries = isTwoPaneLayout ? 2 : 1;
|
||||
actionBar.setDisplayHomeAsUpEnabled(backStackEntries >= minBackStackEntries);
|
||||
}
|
||||
|
||||
// We use onTouchListener here to avoid the UI click sound, hence
|
||||
// calling View#performClick defeats the purpose of it.
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
protected void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.main_activity);
|
||||
actionBar = getSupportActionBar();
|
||||
isTwoPaneLayout = findViewById(R.id.master_detail_wrapper) instanceof LinearLayout;
|
||||
listFragment = (TunnelListFragment) getSupportFragmentManager().findFragmentByTag("LIST");
|
||||
getSupportFragmentManager().addOnBackStackChangedListener(this);
|
||||
onBackStackChanged();
|
||||
final View actionBarView = findViewById(R.id.action_bar);
|
||||
if (actionBarView != null)
|
||||
actionBarView.setOnTouchListener((v, e) -> listFragment != null && listFragment.collapseActionMenu());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.main_activity, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
// The back arrow in the action bar should act the same as the back button.
|
||||
onBackPressed();
|
||||
return true;
|
||||
case R.id.menu_action_edit:
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.detail_container, new TunnelEditorFragment())
|
||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
return true;
|
||||
case R.id.menu_action_save:
|
||||
// This menu item is handled by the editor fragment.
|
||||
return false;
|
||||
case R.id.menu_settings:
|
||||
startActivity(new Intent(this, SettingsActivity.class));
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel,
|
||||
@Nullable final Tunnel newTunnel) {
|
||||
final FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
final int backStackEntries = fragmentManager.getBackStackEntryCount();
|
||||
if (newTunnel == null) {
|
||||
// Clear everything off the back stack (all editors and detail fragments).
|
||||
fragmentManager.popBackStackImmediate(0, FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
||||
return;
|
||||
}
|
||||
if (backStackEntries == 2) {
|
||||
// Pop the editor off the back stack to reveal the detail fragment. Use the immediate
|
||||
// method to avoid the editor picking up the new tunnel while it is still visible.
|
||||
fragmentManager.popBackStackImmediate();
|
||||
} else if (backStackEntries == 0) {
|
||||
// Create and show a new detail fragment.
|
||||
fragmentManager.beginTransaction()
|
||||
.add(R.id.detail_container, new TunnelDetailFragment())
|
||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.activity;
|
||||
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.preference.Preference;
|
||||
import android.support.v7.preference.PreferenceFragmentCompat;
|
||||
import android.support.v7.preference.PreferenceScreen;
|
||||
import android.util.SparseArray;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.backend.WgQuickBackend;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interface for changing application-global persistent settings.
|
||||
*/
|
||||
|
||||
public class SettingsActivity extends ThemeChangeAwareActivity {
|
||||
private final SparseArray<PermissionRequestCallback> permissionRequestCallbacks = new SparseArray<>();
|
||||
private int permissionRequestCounter;
|
||||
|
||||
public void ensurePermissions(final String[] permissions, final PermissionRequestCallback cb) {
|
||||
final List<String> needPermissions = new ArrayList<>(permissions.length);
|
||||
for (final String permission : permissions) {
|
||||
if (ContextCompat.checkSelfPermission(this, permission)
|
||||
!= PackageManager.PERMISSION_GRANTED)
|
||||
needPermissions.add(permission);
|
||||
}
|
||||
if (needPermissions.isEmpty()) {
|
||||
final int[] granted = new int[permissions.length];
|
||||
Arrays.fill(granted, PackageManager.PERMISSION_GRANTED);
|
||||
cb.done(permissions, granted);
|
||||
return;
|
||||
}
|
||||
final int idx = permissionRequestCounter++;
|
||||
permissionRequestCallbacks.put(idx, cb);
|
||||
ActivityCompat.requestPermissions(this,
|
||||
needPermissions.toArray(new String[needPermissions.size()]), idx);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (getSupportFragmentManager().findFragmentById(android.R.id.content) == null) {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.add(android.R.id.content, new SettingsFragment())
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(final int requestCode,
|
||||
final String[] permissions,
|
||||
final int[] grantResults) {
|
||||
final PermissionRequestCallback f = permissionRequestCallbacks.get(requestCode);
|
||||
if (f != null) {
|
||||
permissionRequestCallbacks.remove(requestCode);
|
||||
f.done(permissions, grantResults);
|
||||
}
|
||||
}
|
||||
|
||||
public interface PermissionRequestCallback {
|
||||
void done(String[] permissions, int[] grantResults);
|
||||
}
|
||||
|
||||
public static class SettingsFragment extends PreferenceFragmentCompat {
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String key) {
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
final Preference wgQuickOnlyPrefs[] = {
|
||||
getPreferenceManager().findPreference("tools_installer"),
|
||||
getPreferenceManager().findPreference("restore_on_boot")
|
||||
};
|
||||
for (final Preference pref : wgQuickOnlyPrefs)
|
||||
pref.setVisible(false);
|
||||
final PreferenceScreen screen = getPreferenceScreen();
|
||||
Application.getBackendAsync().thenAccept(backend -> {
|
||||
for (final Preference pref : wgQuickOnlyPrefs) {
|
||||
if (backend instanceof WgQuickBackend)
|
||||
pref.setVisible(true);
|
||||
else
|
||||
screen.removePreference(pref);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.activity;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.app.AppCompatDelegate;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
public abstract class ThemeChangeAwareActivity extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final String TAG = "WireGuard/" + ThemeChangeAwareActivity.class.getSimpleName();
|
||||
|
||||
@Nullable private static Resources lastResources;
|
||||
private static boolean lastDarkMode;
|
||||
private static synchronized void invalidateDrawableCache(final Resources resources, final boolean darkMode) {
|
||||
if (resources == lastResources && darkMode == lastDarkMode)
|
||||
return;
|
||||
|
||||
try {
|
||||
Field f;
|
||||
Object o = resources;
|
||||
try {
|
||||
f = o.getClass().getDeclaredField("mResourcesImpl");
|
||||
f.setAccessible(true);
|
||||
o = f.get(o);
|
||||
} catch (final Exception ignored) { }
|
||||
f = o.getClass().getDeclaredField("mDrawableCache");
|
||||
f.setAccessible(true);
|
||||
o = f.get(o);
|
||||
try {
|
||||
o.getClass().getMethod("onConfigurationChange", int.class).invoke(o, -1);
|
||||
} catch (final Exception ignored) {
|
||||
o.getClass().getMethod("clear").invoke(o);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
Log.e(TAG, "Failed to flush drawable cache", e);
|
||||
}
|
||||
|
||||
lastResources = resources;
|
||||
lastDarkMode = darkMode;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Application.getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
Application.getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) {
|
||||
if ("dark_theme".equals(key)) {
|
||||
final boolean darkMode = sharedPreferences.getBoolean(key, false);
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
sharedPreferences.getBoolean(key, false) ?
|
||||
AppCompatDelegate.MODE_NIGHT_YES :
|
||||
AppCompatDelegate.MODE_NIGHT_NO);
|
||||
invalidateDrawableCache(getResources(), darkMode);
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.activity;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.wireguard.android.fragment.TunnelEditorFragment;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
|
||||
/**
|
||||
* Standalone activity for creating tunnels.
|
||||
*/
|
||||
|
||||
public class TunnelCreatorActivity extends BaseActivity {
|
||||
@Override
|
||||
@SuppressWarnings("UnnecessaryFullyQualifiedName")
|
||||
protected void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (getSupportFragmentManager().findFragmentById(android.R.id.content) == null) {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.add(android.R.id.content, new TunnelEditorFragment())
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.backend;
|
||||
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.Tunnel.State;
|
||||
import com.wireguard.android.model.Tunnel.Statistics;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Interface for implementations of the WireGuard secure network tunnel.
|
||||
*/
|
||||
|
||||
public interface Backend {
|
||||
/**
|
||||
* Update the volatile configuration of a running tunnel and return the resulting configuration.
|
||||
* If the tunnel is not up, return the configuration that would result (if known), or else
|
||||
* simply return the given configuration.
|
||||
*
|
||||
* @param tunnel The tunnel to apply the configuration to.
|
||||
* @param config The new configuration for this tunnel.
|
||||
* @return The updated configuration of the tunnel.
|
||||
*/
|
||||
Config applyConfig(Tunnel tunnel, Config config) throws Exception;
|
||||
|
||||
/**
|
||||
* Enumerate the names of currently-running tunnels.
|
||||
*
|
||||
* @return The set of running tunnel names.
|
||||
*/
|
||||
Set<String> enumerate();
|
||||
|
||||
/**
|
||||
* Get the actual state of a tunnel.
|
||||
*
|
||||
* @param tunnel The tunnel to examine the state of.
|
||||
* @return The state of the tunnel.
|
||||
*/
|
||||
State getState(Tunnel tunnel) throws Exception;
|
||||
|
||||
/**
|
||||
* Get statistics about traffic and errors on this tunnel. If the tunnel is not running, the
|
||||
* statistics object will be filled with zero values.
|
||||
*
|
||||
* @param tunnel The tunnel to retrieve statistics for.
|
||||
* @return The statistics for the tunnel.
|
||||
*/
|
||||
Statistics getStatistics(Tunnel tunnel) throws Exception;
|
||||
|
||||
/**
|
||||
* Set the state of a tunnel.
|
||||
*
|
||||
* @param tunnel The tunnel to control the state of.
|
||||
* @param state The new state for this tunnel. Must be {@code UP}, {@code DOWN}, or
|
||||
* {@code TOGGLE}.
|
||||
* @return The updated state of the tunnel.
|
||||
*/
|
||||
State setState(Tunnel tunnel, State state) throws Exception;
|
||||
|
||||
/**
|
||||
* Determine version of underlying backend.
|
||||
*
|
||||
* @return The version of the backend.
|
||||
* @throws Exception
|
||||
*/
|
||||
String getVersion() throws Exception;
|
||||
|
||||
/**
|
||||
* Determine type name of underlying backend.
|
||||
*
|
||||
* @return Type name
|
||||
*/
|
||||
String getTypeName();
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.backend;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.util.ArraySet;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.activity.MainActivity;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.Tunnel.State;
|
||||
import com.wireguard.android.model.Tunnel.Statistics;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
import com.wireguard.android.util.SharedLibraryLoader;
|
||||
import com.wireguard.config.Config;
|
||||
import com.wireguard.config.InetNetwork;
|
||||
import com.wireguard.config.Interface;
|
||||
import com.wireguard.config.Peer;
|
||||
import com.wireguard.crypto.KeyEncoding;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Collections;
|
||||
import java.util.Formatter;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import java9.util.concurrent.CompletableFuture;
|
||||
|
||||
public final class GoBackend implements Backend {
|
||||
private static final String TAG = "WireGuard/" + GoBackend.class.getSimpleName();
|
||||
private static CompletableFuture<VpnService> vpnService = new CompletableFuture<>();
|
||||
|
||||
private final Context context;
|
||||
@Nullable private Tunnel currentTunnel;
|
||||
private int currentTunnelHandle = -1;
|
||||
|
||||
public GoBackend(final Context context) {
|
||||
SharedLibraryLoader.loadSharedLibrary(context, "wg-go");
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
private static native int wgGetSocketV4(int handle);
|
||||
|
||||
private static native int wgGetSocketV6(int handle);
|
||||
|
||||
private static native void wgTurnOff(int handle);
|
||||
|
||||
private static native int wgTurnOn(String ifName, int tunFd, String settings);
|
||||
|
||||
private static native String wgVersion();
|
||||
|
||||
@Override
|
||||
public String getVersion() { return wgVersion(); }
|
||||
|
||||
@Override
|
||||
public String getTypeName() { return "Go userspace"; }
|
||||
|
||||
@Override
|
||||
public Config applyConfig(final Tunnel tunnel, final Config config) throws Exception {
|
||||
if (tunnel.getState() == State.UP) {
|
||||
// Restart the tunnel to apply the new config.
|
||||
setStateInternal(tunnel, tunnel.getConfig(), State.DOWN);
|
||||
try {
|
||||
setStateInternal(tunnel, config, State.UP);
|
||||
} catch (final Exception e) {
|
||||
// The new configuration didn't work, so try to go back to the old one.
|
||||
setStateInternal(tunnel, tunnel.getConfig(), State.UP);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> enumerate() {
|
||||
if (currentTunnel != null) {
|
||||
final Set<String> runningTunnels = new ArraySet<>();
|
||||
runningTunnels.add(currentTunnel.getName());
|
||||
return runningTunnels;
|
||||
}
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public State getState(final Tunnel tunnel) {
|
||||
return currentTunnel == tunnel ? State.UP : State.DOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Statistics getStatistics(final Tunnel tunnel) {
|
||||
return new Statistics();
|
||||
}
|
||||
|
||||
@Override
|
||||
public State setState(final Tunnel tunnel, State state) throws Exception {
|
||||
final State originalState = getState(tunnel);
|
||||
if (state == State.TOGGLE)
|
||||
state = originalState == State.UP ? State.DOWN : State.UP;
|
||||
if (state == originalState)
|
||||
return originalState;
|
||||
if (state == State.UP && currentTunnel != null)
|
||||
throw new IllegalStateException("Only one userspace tunnel can run at a time");
|
||||
Log.d(TAG, "Changing tunnel " + tunnel.getName() + " to state " + state);
|
||||
setStateInternal(tunnel, tunnel.getConfig(), state);
|
||||
return getState(tunnel);
|
||||
}
|
||||
|
||||
private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state)
|
||||
throws Exception {
|
||||
|
||||
if (state == State.UP) {
|
||||
Log.i(TAG, "Bringing tunnel up");
|
||||
|
||||
Objects.requireNonNull(config, "Trying to bring up a tunnel with no config");
|
||||
|
||||
if (VpnService.prepare(context) != null)
|
||||
throw new Exception("VPN service not authorized by user");
|
||||
|
||||
final VpnService service;
|
||||
if (!vpnService.isDone())
|
||||
startVpnService();
|
||||
|
||||
try {
|
||||
service = vpnService.get(2, TimeUnit.SECONDS);
|
||||
} catch (final TimeoutException e) {
|
||||
throw new Exception("Unable to start Android VPN service", e);
|
||||
}
|
||||
|
||||
if (currentTunnelHandle != -1) {
|
||||
Log.w(TAG, "Tunnel already up");
|
||||
return;
|
||||
}
|
||||
|
||||
// Build config
|
||||
final Interface iface = config.getInterface();
|
||||
final String goConfig;
|
||||
try (final Formatter fmt = new Formatter(new StringBuilder())) {
|
||||
fmt.format("replace_peers=true\n");
|
||||
if (iface.getPrivateKey() != null)
|
||||
fmt.format("private_key=%s\n", KeyEncoding.keyToHex(KeyEncoding.keyFromBase64(iface.getPrivateKey())));
|
||||
if (iface.getListenPort() != 0)
|
||||
fmt.format("listen_port=%d\n", config.getInterface().getListenPort());
|
||||
for (final Peer peer : config.getPeers()) {
|
||||
if (peer.getPublicKey() != null)
|
||||
fmt.format("public_key=%s\n", KeyEncoding.keyToHex(KeyEncoding.keyFromBase64(peer.getPublicKey())));
|
||||
if (peer.getPreSharedKey() != null)
|
||||
fmt.format("preshared_key=%s\n", KeyEncoding.keyToHex(KeyEncoding.keyFromBase64(peer.getPreSharedKey())));
|
||||
if (peer.getEndpoint() != null)
|
||||
fmt.format("endpoint=%s\n", peer.getResolvedEndpointString());
|
||||
if (peer.getPersistentKeepalive() != 0)
|
||||
fmt.format("persistent_keepalive_interval=%d\n", peer.getPersistentKeepalive());
|
||||
for (final InetNetwork addr : peer.getAllowedIPs()) {
|
||||
fmt.format("allowed_ip=%s\n", addr.toString());
|
||||
}
|
||||
}
|
||||
goConfig = fmt.toString();
|
||||
}
|
||||
|
||||
// Create the vpn tunnel with android API
|
||||
final VpnService.Builder builder = service.getBuilder();
|
||||
builder.setSession(tunnel.getName());
|
||||
|
||||
final Intent configureIntent = new Intent(context, MainActivity.class);
|
||||
configureIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
builder.setConfigureIntent(PendingIntent.getActivity(context, 0, configureIntent, 0));
|
||||
|
||||
for (final String excludedApplication : config.getInterface().getExcludedApplications())
|
||||
builder.addDisallowedApplication(excludedApplication);
|
||||
|
||||
for (final InetNetwork addr : config.getInterface().getAddresses())
|
||||
builder.addAddress(addr.getAddress(), addr.getMask());
|
||||
|
||||
for (final InetAddress addr : config.getInterface().getDnses())
|
||||
builder.addDnsServer(addr.getHostAddress());
|
||||
|
||||
for (final Peer peer : config.getPeers()) {
|
||||
for (final InetNetwork addr : peer.getAllowedIPs())
|
||||
builder.addRoute(addr.getAddress(), addr.getMask());
|
||||
}
|
||||
|
||||
int mtu = config.getInterface().getMtu();
|
||||
if (mtu == 0)
|
||||
mtu = 1280;
|
||||
builder.setMtu(mtu);
|
||||
|
||||
builder.setBlocking(true);
|
||||
try (final ParcelFileDescriptor tun = builder.establish()) {
|
||||
if (tun == null)
|
||||
throw new Exception("Unable to create tun device");
|
||||
Log.d(TAG, "Go backend v" + wgVersion());
|
||||
currentTunnelHandle = wgTurnOn(tunnel.getName(), tun.detachFd(), goConfig);
|
||||
}
|
||||
if (currentTunnelHandle < 0)
|
||||
throw new Exception("Unable to turn tunnel on (wgTurnOn return " + currentTunnelHandle + ')');
|
||||
|
||||
currentTunnel = tunnel;
|
||||
|
||||
service.protect(wgGetSocketV4(currentTunnelHandle));
|
||||
service.protect(wgGetSocketV6(currentTunnelHandle));
|
||||
} else {
|
||||
Log.i(TAG, "Bringing tunnel down");
|
||||
|
||||
if (currentTunnelHandle == -1) {
|
||||
Log.w(TAG, "Tunnel already down");
|
||||
return;
|
||||
}
|
||||
|
||||
wgTurnOff(currentTunnelHandle);
|
||||
currentTunnel = null;
|
||||
currentTunnelHandle = -1;
|
||||
}
|
||||
}
|
||||
|
||||
private void startVpnService() {
|
||||
Log.d(TAG, "Requesting to start VpnService");
|
||||
context.startService(new Intent(context, VpnService.class));
|
||||
}
|
||||
|
||||
public static class VpnService extends android.net.VpnService {
|
||||
public Builder getBuilder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
vpnService.complete(this);
|
||||
super.onCreate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
|
||||
for (final Tunnel tunnel : tunnels) {
|
||||
if (tunnel != null && tunnel.getState() != State.DOWN)
|
||||
tunnel.setState(State.DOWN);
|
||||
}
|
||||
});
|
||||
|
||||
vpnService = vpnService.newIncompleteFuture();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(@Nullable final Intent intent, final int flags, final int startId) {
|
||||
vpnService.complete(this);
|
||||
if (intent == null || intent.getComponent() == null || !intent.getComponent().getPackageName().equals(getPackageName())) {
|
||||
Log.d(TAG, "Service started by Always-on VPN feature");
|
||||
Application.getTunnelManager().restoreState(true).whenComplete(ExceptionLoggers.D);
|
||||
}
|
||||
return super.onStartCommand(intent, flags, startId);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.backend;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.Tunnel.State;
|
||||
import com.wireguard.android.model.Tunnel.Statistics;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import java9.util.stream.Collectors;
|
||||
import java9.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* WireGuard backend that uses {@code wg-quick} to implement tunnel configuration.
|
||||
*/
|
||||
|
||||
public final class WgQuickBackend implements Backend {
|
||||
private static final String TAG = "WireGuard/" + WgQuickBackend.class.getSimpleName();
|
||||
|
||||
private final File localTemporaryDir;
|
||||
|
||||
public WgQuickBackend(final Context context) {
|
||||
localTemporaryDir = new File(context.getCacheDir(), "tmp");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() throws Exception {
|
||||
final List<String> output = new ArrayList<>();
|
||||
if (Application.getRootShell()
|
||||
.run(output, "cat /sys/module/wireguard/version") != 0 || output.isEmpty())
|
||||
throw new Exception("Unable to determine kernel module version");
|
||||
return output.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName() { return "Kernel module"; }
|
||||
|
||||
@Override
|
||||
public Config applyConfig(final Tunnel tunnel, final Config config) throws Exception {
|
||||
if (tunnel.getState() == State.UP) {
|
||||
// Restart the tunnel to apply the new config.
|
||||
setStateInternal(tunnel, tunnel.getConfig(), State.DOWN);
|
||||
try {
|
||||
setStateInternal(tunnel, config, State.UP);
|
||||
} catch (final Exception e) {
|
||||
// The new configuration didn't work, so try to go back to the old one.
|
||||
setStateInternal(tunnel, tunnel.getConfig(), State.UP);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> enumerate() {
|
||||
final List<String> output = new ArrayList<>();
|
||||
// Don't throw an exception here or nothing will show up in the UI.
|
||||
try {
|
||||
Application.getToolsInstaller().ensureToolsAvailable();
|
||||
if (Application.getRootShell().run(output, "wg show interfaces") != 0 || output.isEmpty())
|
||||
return Collections.emptySet();
|
||||
} catch (final Exception e) {
|
||||
Log.w(TAG, "Unable to enumerate running tunnels", e);
|
||||
return Collections.emptySet();
|
||||
}
|
||||
// wg puts all interface names on the same line. Split them into separate elements.
|
||||
return Stream.of(output.get(0).split(" ")).collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public State getState(final Tunnel tunnel) {
|
||||
return enumerate().contains(tunnel.getName()) ? State.UP : State.DOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Statistics getStatistics(final Tunnel tunnel) {
|
||||
return new Statistics();
|
||||
}
|
||||
|
||||
@Override
|
||||
public State setState(final Tunnel tunnel, State state) throws Exception {
|
||||
final State originalState = getState(tunnel);
|
||||
if (state == State.TOGGLE)
|
||||
state = originalState == State.UP ? State.DOWN : State.UP;
|
||||
if (state == originalState)
|
||||
return originalState;
|
||||
Log.d(TAG, "Changing tunnel " + tunnel.getName() + " to state " + state);
|
||||
Application.getToolsInstaller().ensureToolsAvailable();
|
||||
setStateInternal(tunnel, tunnel.getConfig(), state);
|
||||
return getState(tunnel);
|
||||
}
|
||||
|
||||
private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state) throws Exception {
|
||||
Objects.requireNonNull(config, "Trying to set state with a null config");
|
||||
|
||||
final File tempFile = new File(localTemporaryDir, tunnel.getName() + ".conf");
|
||||
try (final FileOutputStream stream = new FileOutputStream(tempFile, false)) {
|
||||
stream.write(config.toString().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
String command = String.format("wg-quick %s '%s'",
|
||||
state.toString().toLowerCase(), tempFile.getAbsolutePath());
|
||||
if (state == State.UP)
|
||||
command = "cat /sys/module/wireguard/version && " + command;
|
||||
final int result = Application.getRootShell().run(null, command);
|
||||
// noinspection ResultOfMethodCallIgnored
|
||||
tempFile.delete();
|
||||
if (result != 0)
|
||||
throw new Exception("Unable to configure tunnel (wg-quick returned " + result + ')');
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.configStore;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Set;
|
||||
|
||||
import java9.util.stream.Collectors;
|
||||
import java9.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Configuration store that uses a {@code wg-quick}-style file for each configured tunnel.
|
||||
*/
|
||||
|
||||
public final class FileConfigStore implements ConfigStore {
|
||||
private static final String TAG = "WireGuard/" + FileConfigStore.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
|
||||
public FileConfigStore(final Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Config create(final String name, final Config config) throws IOException {
|
||||
Log.d(TAG, "Creating configuration for tunnel " + name);
|
||||
final File file = fileFor(name);
|
||||
if (!file.createNewFile())
|
||||
throw new IOException("Configuration file " + file.getName() + " already exists");
|
||||
try (final FileOutputStream stream = new FileOutputStream(file, false)) {
|
||||
stream.write(config.toString().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(final String name) throws IOException {
|
||||
Log.d(TAG, "Deleting configuration for tunnel " + name);
|
||||
final File file = fileFor(name);
|
||||
if (!file.delete())
|
||||
throw new IOException("Cannot delete configuration file " + file.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> enumerate() {
|
||||
return Stream.of(context.fileList())
|
||||
.filter(name -> name.endsWith(".conf"))
|
||||
.map(name -> name.substring(0, name.length() - ".conf".length()))
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
private File fileFor(final String name) {
|
||||
return new File(context.getFilesDir(), name + ".conf");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Config load(final String name) throws IOException {
|
||||
try (final FileInputStream stream = new FileInputStream(fileFor(name))) {
|
||||
return Config.from(stream);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rename(final String name, final String replacement) throws IOException {
|
||||
Log.d(TAG, "Renaming configuration for tunnel " + name + " to " + replacement);
|
||||
final File file = fileFor(name);
|
||||
final File replacementFile = fileFor(replacement);
|
||||
if (!replacementFile.createNewFile())
|
||||
throw new IOException("Configuration for " + replacement + " already exists");
|
||||
if (!file.renameTo(replacementFile)) {
|
||||
if (!replacementFile.delete())
|
||||
Log.w(TAG, "Couldn't delete marker file for new name " + replacement);
|
||||
throw new IOException("Cannot rename configuration file " + file.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Config save(final String name, final Config config) throws IOException {
|
||||
Log.d(TAG, "Saving configuration for tunnel " + name);
|
||||
final File file = fileFor(name);
|
||||
if (!file.isFile())
|
||||
throw new FileNotFoundException("Configuration file " + file.getName() + " not found");
|
||||
try (final FileOutputStream stream = new FileOutputStream(file, false)) {
|
||||
stream.write(config.toString().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
return config;
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.databinding;
|
||||
|
||||
import android.databinding.BindingAdapter;
|
||||
import android.databinding.ObservableList;
|
||||
import android.databinding.adapters.ListenerUtil;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.InputFilter;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler;
|
||||
import com.wireguard.android.util.ObservableKeyedList;
|
||||
import com.wireguard.android.widget.ToggleSwitch;
|
||||
import com.wireguard.android.widget.ToggleSwitch.OnBeforeCheckedChangeListener;
|
||||
import com.wireguard.util.Keyed;
|
||||
|
||||
/**
|
||||
* Static methods for use by generated code in the Android data binding library.
|
||||
*/
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class BindingAdapters {
|
||||
private BindingAdapters() {
|
||||
// Prevent instantiation.
|
||||
}
|
||||
|
||||
@BindingAdapter("checked")
|
||||
public static void setChecked(final ToggleSwitch view, final boolean checked) {
|
||||
view.setCheckedInternal(checked);
|
||||
}
|
||||
|
||||
@BindingAdapter("filter")
|
||||
public static void setFilter(final TextView view, final InputFilter filter) {
|
||||
view.setFilters(new InputFilter[]{filter});
|
||||
}
|
||||
|
||||
@BindingAdapter({"items", "layout"})
|
||||
public static <E> void setItems(final LinearLayout view,
|
||||
final ObservableList<E> oldList, final int oldLayoutId,
|
||||
final ObservableList<E> newList, final int newLayoutId) {
|
||||
if (oldList == newList && oldLayoutId == newLayoutId)
|
||||
return;
|
||||
ItemChangeListener<E> listener = ListenerUtil.getListener(view, R.id.item_change_listener);
|
||||
// If the layout changes, any existing listener must be replaced.
|
||||
if (listener != null && oldList != null && oldLayoutId != newLayoutId) {
|
||||
listener.setList(null);
|
||||
listener = null;
|
||||
// Stop tracking the old listener.
|
||||
ListenerUtil.trackListener(view, null, R.id.item_change_listener);
|
||||
}
|
||||
// Avoid adding a listener when there is no new list or layout.
|
||||
if (newList == null || newLayoutId == 0)
|
||||
return;
|
||||
if (listener == null) {
|
||||
listener = new ItemChangeListener<>(view, newLayoutId);
|
||||
ListenerUtil.trackListener(view, listener, R.id.item_change_listener);
|
||||
}
|
||||
// Either the list changed, or this is an entirely new listener because the layout changed.
|
||||
listener.setList(newList);
|
||||
}
|
||||
|
||||
@BindingAdapter(requireAll = false, value = {"items", "layout", "configurationHandler"})
|
||||
public static <K, E extends Keyed<? extends K>>
|
||||
void setItems(final RecyclerView view,
|
||||
final ObservableKeyedList<K, E> oldList, final int oldLayoutId, final RowConfigurationHandler oldRowConfigurationHandler,
|
||||
final ObservableKeyedList<K, E> newList, final int newLayoutId, final RowConfigurationHandler newRowConfigurationHandler) {
|
||||
if (view.getLayoutManager() == null)
|
||||
view.setLayoutManager(new LinearLayoutManager(view.getContext(), RecyclerView.VERTICAL, false));
|
||||
|
||||
if (oldList == newList && oldLayoutId == newLayoutId)
|
||||
return;
|
||||
// The ListAdapter interface is not generic, so this cannot be checked.
|
||||
@SuppressWarnings("unchecked") ObservableKeyedRecyclerViewAdapter<K, E> adapter =
|
||||
(ObservableKeyedRecyclerViewAdapter<K, E>) view.getAdapter();
|
||||
// If the layout changes, any existing adapter must be replaced.
|
||||
if (adapter != null && oldList != null && oldLayoutId != newLayoutId) {
|
||||
adapter.setList(null);
|
||||
adapter = null;
|
||||
}
|
||||
// Avoid setting an adapter when there is no new list or layout.
|
||||
if (newList == null || newLayoutId == 0)
|
||||
return;
|
||||
if (adapter == null) {
|
||||
adapter = new ObservableKeyedRecyclerViewAdapter<>(view.getContext(), newLayoutId, newList);
|
||||
view.setAdapter(adapter);
|
||||
}
|
||||
|
||||
adapter.setRowConfigurationHandler(newRowConfigurationHandler);
|
||||
// Either the list changed, or this is an entirely new listener because the layout changed.
|
||||
adapter.setList(newList);
|
||||
}
|
||||
|
||||
@BindingAdapter("onBeforeCheckedChanged")
|
||||
public static void setOnBeforeCheckedChanged(final ToggleSwitch view,
|
||||
final OnBeforeCheckedChangeListener listener) {
|
||||
view.setOnBeforeCheckedChangeListener(listener);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.databinding;
|
||||
|
||||
import android.databinding.DataBindingUtil;
|
||||
import android.databinding.ObservableList;
|
||||
import android.databinding.ViewDataBinding;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.wireguard.android.BR;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Helper class for binding an ObservableList to the children of a ViewGroup.
|
||||
*/
|
||||
|
||||
class ItemChangeListener<T> {
|
||||
private final OnListChangedCallback<T> callback = new OnListChangedCallback<>(this);
|
||||
private final ViewGroup container;
|
||||
private final int layoutId;
|
||||
private final LayoutInflater layoutInflater;
|
||||
@Nullable private ObservableList<T> list;
|
||||
|
||||
ItemChangeListener(final ViewGroup container, final int layoutId) {
|
||||
this.container = container;
|
||||
this.layoutId = layoutId;
|
||||
layoutInflater = LayoutInflater.from(container.getContext());
|
||||
}
|
||||
|
||||
private View getView(final int position, @Nullable final View convertView) {
|
||||
ViewDataBinding binding = convertView != null ? DataBindingUtil.getBinding(convertView) : null;
|
||||
if (binding == null) {
|
||||
binding = DataBindingUtil.inflate(layoutInflater, layoutId, container, false);
|
||||
}
|
||||
|
||||
Objects.requireNonNull(list, "Trying to get a view while list is still null");
|
||||
|
||||
binding.setVariable(BR.collection, list);
|
||||
binding.setVariable(BR.item, list.get(position));
|
||||
binding.executePendingBindings();
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
void setList(@Nullable final ObservableList<T> newList) {
|
||||
if (list != null)
|
||||
list.removeOnListChangedCallback(callback);
|
||||
list = newList;
|
||||
if (list != null) {
|
||||
list.addOnListChangedCallback(callback);
|
||||
callback.onChanged(list);
|
||||
} else {
|
||||
container.removeAllViews();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class OnListChangedCallback<T>
|
||||
extends ObservableList.OnListChangedCallback<ObservableList<T>> {
|
||||
|
||||
private final WeakReference<ItemChangeListener<T>> weakListener;
|
||||
|
||||
private OnListChangedCallback(final ItemChangeListener<T> listener) {
|
||||
weakListener = new WeakReference<>(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(final ObservableList<T> sender) {
|
||||
final ItemChangeListener<T> listener = weakListener.get();
|
||||
if (listener != null) {
|
||||
// TODO: recycle views
|
||||
listener.container.removeAllViews();
|
||||
for (int i = 0; i < sender.size(); ++i)
|
||||
listener.container.addView(listener.getView(i, null));
|
||||
} else {
|
||||
sender.removeOnListChangedCallback(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeChanged(final ObservableList<T> sender, final int positionStart,
|
||||
final int itemCount) {
|
||||
final ItemChangeListener<T> listener = weakListener.get();
|
||||
if (listener != null) {
|
||||
for (int i = positionStart; i < positionStart + itemCount; ++i) {
|
||||
final View child = listener.container.getChildAt(i);
|
||||
listener.container.removeViewAt(i);
|
||||
listener.container.addView(listener.getView(i, child));
|
||||
}
|
||||
} else {
|
||||
sender.removeOnListChangedCallback(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeInserted(final ObservableList<T> sender, final int positionStart,
|
||||
final int itemCount) {
|
||||
final ItemChangeListener<T> listener = weakListener.get();
|
||||
if (listener != null) {
|
||||
for (int i = positionStart; i < positionStart + itemCount; ++i)
|
||||
listener.container.addView(listener.getView(i, null));
|
||||
} else {
|
||||
sender.removeOnListChangedCallback(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeMoved(final ObservableList<T> sender, final int fromPosition,
|
||||
final int toPosition, final int itemCount) {
|
||||
final ItemChangeListener<T> listener = weakListener.get();
|
||||
if (listener != null) {
|
||||
final View[] views = new View[itemCount];
|
||||
for (int i = 0; i < itemCount; ++i)
|
||||
views[i] = listener.container.getChildAt(fromPosition + i);
|
||||
listener.container.removeViews(fromPosition, itemCount);
|
||||
for (int i = 0; i < itemCount; ++i)
|
||||
listener.container.addView(views[i], toPosition + i);
|
||||
} else {
|
||||
sender.removeOnListChangedCallback(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeRemoved(final ObservableList<T> sender, final int positionStart,
|
||||
final int itemCount) {
|
||||
final ItemChangeListener<T> listener = weakListener.get();
|
||||
if (listener != null) {
|
||||
listener.container.removeViews(positionStart, itemCount);
|
||||
} else {
|
||||
sender.removeOnListChangedCallback(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-160
@@ -1,160 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Eric Kuck <eric@bluelinelabs.com>.
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.databinding;
|
||||
|
||||
import android.content.Context;
|
||||
import android.databinding.DataBindingUtil;
|
||||
import android.databinding.ObservableList;
|
||||
import android.databinding.ViewDataBinding;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.RecyclerView.Adapter;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.wireguard.android.BR;
|
||||
import com.wireguard.android.util.ObservableKeyedList;
|
||||
import com.wireguard.util.Keyed;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* A generic {@code RecyclerView.Adapter} backed by a {@code ObservableKeyedList}.
|
||||
*/
|
||||
|
||||
public class ObservableKeyedRecyclerViewAdapter<K, E extends Keyed<? extends K>> extends Adapter<ObservableKeyedRecyclerViewAdapter.ViewHolder> {
|
||||
|
||||
private final OnListChangedCallback<E> callback = new OnListChangedCallback<>(this);
|
||||
private final int layoutId;
|
||||
private final LayoutInflater layoutInflater;
|
||||
@Nullable private ObservableKeyedList<K, E> list;
|
||||
@Nullable private RowConfigurationHandler rowConfigurationHandler;
|
||||
|
||||
ObservableKeyedRecyclerViewAdapter(final Context context, final int layoutId,
|
||||
final ObservableKeyedList<K, E> list) {
|
||||
this.layoutId = layoutId;
|
||||
layoutInflater = LayoutInflater.from(context);
|
||||
setList(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return list != null ? list.size() : 0;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private E getItem(final int position) {
|
||||
if (list == null || position < 0 || position >= list.size())
|
||||
return null;
|
||||
return list.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(final int position) {
|
||||
final K key = getKey(position);
|
||||
return key != null ? key.hashCode() : -1;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private K getKey(final int position) {
|
||||
final E item = getItem(position);
|
||||
return item != null ? item.getKey() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
|
||||
return new ViewHolder(DataBindingUtil.inflate(layoutInflater, layoutId, parent, false));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public void onBindViewHolder(final ViewHolder holder, final int position) {
|
||||
holder.binding.setVariable(BR.collection, list);
|
||||
holder.binding.setVariable(BR.key, getKey(position));
|
||||
holder.binding.setVariable(BR.item, getItem(position));
|
||||
holder.binding.executePendingBindings();
|
||||
|
||||
if (rowConfigurationHandler != null) {
|
||||
E item = getItem(position);
|
||||
if (item != null) {
|
||||
rowConfigurationHandler.onConfigureRow(holder.binding, item, position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setList(@Nullable final ObservableKeyedList<K, E> newList) {
|
||||
if (list != null)
|
||||
list.removeOnListChangedCallback(callback);
|
||||
list = newList;
|
||||
if (list != null) {
|
||||
list.addOnListChangedCallback(callback);
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
void setRowConfigurationHandler(final RowConfigurationHandler rowConfigurationHandler) {
|
||||
this.rowConfigurationHandler = rowConfigurationHandler;
|
||||
}
|
||||
|
||||
private static final class OnListChangedCallback<E extends Keyed<?>>
|
||||
extends ObservableList.OnListChangedCallback<ObservableList<E>> {
|
||||
|
||||
private final WeakReference<ObservableKeyedRecyclerViewAdapter<?, E>> weakAdapter;
|
||||
|
||||
private OnListChangedCallback(final ObservableKeyedRecyclerViewAdapter<?, E> adapter) {
|
||||
weakAdapter = new WeakReference<>(adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(final ObservableList<E> sender) {
|
||||
final ObservableKeyedRecyclerViewAdapter adapter = weakAdapter.get();
|
||||
if (adapter != null)
|
||||
adapter.notifyDataSetChanged();
|
||||
else
|
||||
sender.removeOnListChangedCallback(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeChanged(final ObservableList<E> sender, final int positionStart,
|
||||
final int itemCount) {
|
||||
onChanged(sender);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeInserted(final ObservableList<E> sender, final int positionStart,
|
||||
final int itemCount) {
|
||||
onChanged(sender);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeMoved(final ObservableList<E> sender, final int fromPosition,
|
||||
final int toPosition, final int itemCount) {
|
||||
onChanged(sender);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeRemoved(final ObservableList<E> sender, final int positionStart,
|
||||
final int itemCount) {
|
||||
onChanged(sender);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
final ViewDataBinding binding;
|
||||
|
||||
public ViewHolder(final ViewDataBinding binding) {
|
||||
super(binding.getRoot());
|
||||
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
public interface RowConfigurationHandler<B extends ViewDataBinding, T> {
|
||||
void onConfigureRow(B binding, T item, int position);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Eric Kuck <eric@bluelinelabs.com>.
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.databinding.AppListDialogFragmentBinding;
|
||||
import com.wireguard.android.model.ApplicationData;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
import com.wireguard.android.util.ObservableKeyedArrayList;
|
||||
import com.wireguard.android.util.ObservableKeyedList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class AppListDialogFragment extends DialogFragment {
|
||||
|
||||
private static final String KEY_EXCLUDED_APPS = "excludedApps";
|
||||
|
||||
private List<String> currentlyExcludedApps;
|
||||
private final ObservableKeyedList<String, ApplicationData> appData = new ObservableKeyedArrayList<>();
|
||||
|
||||
public static <T extends Fragment & AppExclusionListener> AppListDialogFragment newInstance(final String[] excludedApps, final T target) {
|
||||
final Bundle extras = new Bundle();
|
||||
extras.putStringArray(KEY_EXCLUDED_APPS, excludedApps);
|
||||
final AppListDialogFragment fragment = new AppListDialogFragment();
|
||||
fragment.setTargetFragment(target, 0);
|
||||
fragment.setArguments(extras);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
currentlyExcludedApps = Arrays.asList(getArguments().getStringArray(KEY_EXCLUDED_APPS));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(final Bundle savedInstanceState) {
|
||||
final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity());
|
||||
alertDialogBuilder.setTitle(R.string.excluded_applications);
|
||||
|
||||
final AppListDialogFragmentBinding binding = AppListDialogFragmentBinding.inflate(getActivity().getLayoutInflater(), null, false);
|
||||
binding.executePendingBindings();
|
||||
alertDialogBuilder.setView(binding.getRoot());
|
||||
|
||||
alertDialogBuilder.setPositiveButton(R.string.set_exclusions, (dialog, which) -> setExclusionsAndDismiss());
|
||||
alertDialogBuilder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
|
||||
alertDialogBuilder.setNeutralButton(R.string.deselect_all, (dialog, which) -> { });
|
||||
|
||||
binding.setFragment(this);
|
||||
binding.setAppData(appData);
|
||||
|
||||
loadData();
|
||||
|
||||
final AlertDialog dialog = alertDialogBuilder.create();
|
||||
dialog.setOnShowListener(d -> dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener(view -> {
|
||||
for (final ApplicationData app : appData)
|
||||
app.setExcludedFromTunnel(false);
|
||||
}));
|
||||
return dialog;
|
||||
}
|
||||
|
||||
private void loadData() {
|
||||
final Activity activity = getActivity();
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final PackageManager pm = activity.getPackageManager();
|
||||
Application.getAsyncWorker().supplyAsync(() -> {
|
||||
final Intent launcherIntent = new Intent(Intent.ACTION_MAIN, null);
|
||||
launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||
final List<ResolveInfo> resolveInfos = pm.queryIntentActivities(launcherIntent, 0);
|
||||
|
||||
final List<ApplicationData> appData = new ArrayList<>();
|
||||
for (ResolveInfo resolveInfo : resolveInfos) {
|
||||
String packageName = resolveInfo.activityInfo.packageName;
|
||||
appData.add(new ApplicationData(resolveInfo.loadIcon(pm), resolveInfo.loadLabel(pm).toString(), packageName, currentlyExcludedApps.contains(packageName)));
|
||||
}
|
||||
|
||||
Collections.sort(appData, (lhs, rhs) -> lhs.getName().toLowerCase().compareTo(rhs.getName().toLowerCase()));
|
||||
return appData;
|
||||
}).whenComplete(((data, throwable) -> {
|
||||
if (data != null) {
|
||||
appData.clear();
|
||||
appData.addAll(data);
|
||||
} else {
|
||||
final String error = throwable != null ? ExceptionLoggers.unwrapMessage(throwable) : "Unknown";
|
||||
final String message = activity.getString(R.string.error_fetching_apps, error);
|
||||
Toast.makeText(activity, message, Toast.LENGTH_LONG).show();
|
||||
dismissAllowingStateLoss();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
void setExclusionsAndDismiss() {
|
||||
final List<String> excludedApps = new ArrayList<>();
|
||||
for (final ApplicationData data : appData) {
|
||||
if (data.isExcludedFromTunnel()) {
|
||||
excludedApps.add(data.getPackageName());
|
||||
}
|
||||
}
|
||||
|
||||
((AppExclusionListener) getTargetFragment()).onExcludedAppsSelected(excludedApps);
|
||||
dismiss();
|
||||
}
|
||||
|
||||
public interface AppExclusionListener {
|
||||
void onExcludedAppsSelected(List<String> excludedApps);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.fragment;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.databinding.DataBindingUtil;
|
||||
import android.databinding.ViewDataBinding;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.activity.BaseActivity;
|
||||
import com.wireguard.android.activity.BaseActivity.OnSelectedTunnelChangedListener;
|
||||
import com.wireguard.android.backend.GoBackend;
|
||||
import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
|
||||
import com.wireguard.android.databinding.TunnelListItemBinding;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.Tunnel.State;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
import com.wireguard.android.widget.MonkeyedSnackbar;
|
||||
|
||||
/**
|
||||
* Base class for fragments that need to know the currently-selected tunnel. Only does anything when
|
||||
* attached to a {@code BaseActivity}.
|
||||
*/
|
||||
|
||||
public abstract class BaseFragment extends Fragment implements OnSelectedTunnelChangedListener {
|
||||
private static final String TAG = "WireGuard/" + BaseFragment.class.getSimpleName();
|
||||
private static final int REQUEST_CODE_VPN_PERMISSION = 23491;
|
||||
|
||||
@Nullable private BaseActivity activity;
|
||||
@Nullable private Tunnel pendingTunnel;
|
||||
@Nullable private Boolean pendingTunnelUp;
|
||||
|
||||
@Nullable
|
||||
protected Tunnel getSelectedTunnel() {
|
||||
return activity != null ? activity.getSelectedTunnel() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(final Context context) {
|
||||
super.onAttach(context);
|
||||
if (context instanceof BaseActivity) {
|
||||
activity = (BaseActivity) context;
|
||||
activity.addOnSelectedTunnelChangedListener(this);
|
||||
} else {
|
||||
activity = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
if (activity != null)
|
||||
activity.removeOnSelectedTunnelChangedListener(this);
|
||||
activity = null;
|
||||
super.onDetach();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode == REQUEST_CODE_VPN_PERMISSION) {
|
||||
if (pendingTunnel != null && pendingTunnelUp != null)
|
||||
setTunnelStateWithPermissionsResult(pendingTunnel, pendingTunnelUp);
|
||||
pendingTunnel = null;
|
||||
pendingTunnelUp = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected void setSelectedTunnel(@Nullable final Tunnel tunnel) {
|
||||
if (activity != null)
|
||||
activity.setSelectedTunnel(tunnel);
|
||||
}
|
||||
|
||||
public void setTunnelState(final View view, final boolean checked) {
|
||||
final ViewDataBinding binding = DataBindingUtil.findBinding(view);
|
||||
final Tunnel tunnel;
|
||||
if (binding instanceof TunnelDetailFragmentBinding)
|
||||
tunnel = ((TunnelDetailFragmentBinding) binding).getTunnel();
|
||||
else if (binding instanceof TunnelListItemBinding)
|
||||
tunnel = ((TunnelListItemBinding) binding).getItem();
|
||||
else
|
||||
return;
|
||||
if (tunnel == null)
|
||||
return;
|
||||
|
||||
Application.getBackendAsync().thenAccept(backend -> {
|
||||
if (backend instanceof GoBackend) {
|
||||
final Intent intent = GoBackend.VpnService.prepare(view.getContext());
|
||||
if (intent != null) {
|
||||
pendingTunnel = tunnel;
|
||||
pendingTunnelUp = checked;
|
||||
startActivityForResult(intent, REQUEST_CODE_VPN_PERMISSION);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setTunnelStateWithPermissionsResult(tunnel, checked);
|
||||
});
|
||||
}
|
||||
|
||||
private void setTunnelStateWithPermissionsResult(final Tunnel tunnel, final boolean checked) {
|
||||
tunnel.setState(State.of(checked)).whenComplete((state, throwable) -> {
|
||||
if (throwable == null)
|
||||
return;
|
||||
final String error = ExceptionLoggers.unwrapMessage(throwable);
|
||||
final int messageResId = checked ? R.string.error_up : R.string.error_down;
|
||||
final String message = getContext().getString(messageResId, error);
|
||||
final View view = getView();
|
||||
if (view != null)
|
||||
MonkeyedSnackbar.make(view, message, Snackbar.LENGTH_LONG).show();
|
||||
else
|
||||
Toast.makeText(getContext(), message, Toast.LENGTH_LONG).show();
|
||||
Log.e(TAG, message, throwable);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Eric Kuck <eric@bluelinelabs.com>.
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.view.WindowManager;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.databinding.ConfigNamingDialogFragmentBinding;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
public class ConfigNamingDialogFragment extends DialogFragment {
|
||||
|
||||
private static final String KEY_CONFIG_TEXT = "config_text";
|
||||
|
||||
@Nullable private Config config;
|
||||
@Nullable private ConfigNamingDialogFragmentBinding binding;
|
||||
@Nullable private InputMethodManager imm;
|
||||
|
||||
public static ConfigNamingDialogFragment newInstance(final String configText) {
|
||||
final Bundle extras = new Bundle();
|
||||
extras.putString(KEY_CONFIG_TEXT, configText);
|
||||
final ConfigNamingDialogFragment fragment = new ConfigNamingDialogFragment();
|
||||
fragment.setArguments(extras);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
try {
|
||||
config = Config.from(getArguments().getString(KEY_CONFIG_TEXT));
|
||||
} catch (final IOException exception) {
|
||||
throw new RuntimeException("Invalid config passed to " + getClass().getSimpleName(), exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
final AlertDialog dialog = (AlertDialog) getDialog();
|
||||
if (dialog != null) {
|
||||
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> createTunnelAndDismiss());
|
||||
|
||||
setKeyboardVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(final Bundle savedInstanceState) {
|
||||
final Activity activity = getActivity();
|
||||
|
||||
imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
|
||||
final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity);
|
||||
alertDialogBuilder.setTitle(R.string.import_from_qrcode);
|
||||
|
||||
binding = ConfigNamingDialogFragmentBinding.inflate(getActivity().getLayoutInflater(), null, false);
|
||||
binding.executePendingBindings();
|
||||
alertDialogBuilder.setView(binding.getRoot());
|
||||
|
||||
alertDialogBuilder.setPositiveButton(R.string.create_tunnel, null);
|
||||
alertDialogBuilder.setNegativeButton(R.string.cancel, (dialog, which) -> dismiss());
|
||||
|
||||
return alertDialogBuilder.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dismiss() {
|
||||
setKeyboardVisible(false);
|
||||
super.dismiss();
|
||||
}
|
||||
|
||||
private void createTunnelAndDismiss() {
|
||||
if (binding != null) {
|
||||
final String name = binding.tunnelNameText.getText().toString();
|
||||
|
||||
Application.getTunnelManager().create(name, config).whenComplete((tunnel, throwable) -> {
|
||||
if (tunnel != null) {
|
||||
dismiss();
|
||||
} else {
|
||||
binding.tunnelNameTextLayout.setError(throwable.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void setKeyboardVisible(final boolean visible) {
|
||||
Objects.requireNonNull(imm);
|
||||
|
||||
if (visible) {
|
||||
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
|
||||
} else if (binding != null) {
|
||||
imm.hideSoftInputFromWindow(binding.tunnelNameText.getWindowToken(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
/**
|
||||
* Fragment that shows details about a specific tunnel.
|
||||
*/
|
||||
|
||||
public class TunnelDetailFragment extends BaseFragment {
|
||||
@Nullable private TunnelDetailFragmentBinding binding;
|
||||
|
||||
private void onConfigLoaded(final String name, final Config config) {
|
||||
if (binding != null) {
|
||||
binding.setConfig(new Config.Observable(config, name));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.tunnel_detail, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreateView(inflater, container, savedInstanceState);
|
||||
binding = TunnelDetailFragmentBinding.inflate(inflater, container, false);
|
||||
binding.executePendingBindings();
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
binding = null;
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
|
||||
if (binding == null)
|
||||
return;
|
||||
binding.setTunnel(newTunnel);
|
||||
if (newTunnel == null)
|
||||
binding.setConfig(null);
|
||||
else
|
||||
newTunnel.getConfigAsync().thenAccept(a -> onConfigLoaded(newTunnel.getName(), a));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
|
||||
if (binding == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
binding.setFragment(this);
|
||||
onSelectedTunnelChanged(null, getSelectedTunnel());
|
||||
super.onViewStateRestored(savedInstanceState);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,314 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.databinding.Observable;
|
||||
import android.databinding.ObservableList;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.BR;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.databinding.TunnelEditorFragmentBinding;
|
||||
import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.TunnelManager;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
import com.wireguard.android.widget.MonkeyedSnackbar;
|
||||
import com.wireguard.config.Attribute;
|
||||
import com.wireguard.config.Config;
|
||||
import com.wireguard.config.Peer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Fragment for editing a WireGuard configuration.
|
||||
*/
|
||||
|
||||
public class TunnelEditorFragment extends BaseFragment implements AppExclusionListener {
|
||||
private static final String KEY_LOCAL_CONFIG = "local_config";
|
||||
private static final String KEY_ORIGINAL_NAME = "original_name";
|
||||
private static final String TAG = "WireGuard/" + TunnelEditorFragment.class.getSimpleName();
|
||||
|
||||
@Nullable private TunnelEditorFragmentBinding binding;
|
||||
@Nullable private Tunnel tunnel;
|
||||
|
||||
private void onConfigLoaded(final String name, final Config config) {
|
||||
if (binding != null) {
|
||||
binding.setConfig(new Config.Observable(config, name));
|
||||
}
|
||||
}
|
||||
|
||||
private void onConfigSaved(final Tunnel savedTunnel,
|
||||
@Nullable final Throwable throwable) {
|
||||
final String message;
|
||||
if (throwable == null) {
|
||||
message = getString(R.string.config_save_success, savedTunnel.getName());
|
||||
Log.d(TAG, message);
|
||||
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
|
||||
onFinished();
|
||||
} else {
|
||||
final String error = ExceptionLoggers.unwrapMessage(throwable);
|
||||
message = getString(R.string.config_save_error, savedTunnel.getName(), error);
|
||||
Log.e(TAG, message, throwable);
|
||||
if (binding != null) {
|
||||
MonkeyedSnackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.config_editor, menu);
|
||||
}
|
||||
|
||||
private final ObservableList.OnListChangedCallback<? extends ObservableList<Peer.Observable>> breakObjectListOrientedLayeringHandler = new ObservableList.OnListChangedCallback<ObservableList<Peer.Observable>>() {
|
||||
@Override
|
||||
public void onChanged(final ObservableList<Peer.Observable> sender) { }
|
||||
@Override
|
||||
public void onItemRangeChanged(final ObservableList<Peer.Observable> sender, final int positionStart, final int itemCount) { }
|
||||
@Override
|
||||
public void onItemRangeMoved(final ObservableList<Peer.Observable> sender, final int fromPosition, final int toPosition, final int itemCount) { }
|
||||
|
||||
@Override
|
||||
public void onItemRangeInserted(final ObservableList<Peer.Observable> sender, final int positionStart, final int itemCount) {
|
||||
if (binding != null)
|
||||
breakObjectOrientedLayeringHandler.onPropertyChanged(binding.getConfig(), BR.peers);
|
||||
}
|
||||
@Override
|
||||
public void onItemRangeRemoved(final ObservableList<Peer.Observable> sender, final int positionStart, final int itemCount) {
|
||||
if (binding != null)
|
||||
breakObjectOrientedLayeringHandler.onPropertyChanged(binding.getConfig(), BR.peers);
|
||||
}
|
||||
};
|
||||
|
||||
private final Collection<Object> breakObjectOrientedLayeringHandlerReceivers = new ArrayList<>();
|
||||
private final Observable.OnPropertyChangedCallback breakObjectOrientedLayeringHandler = new Observable.OnPropertyChangedCallback() {
|
||||
@Override
|
||||
public void onPropertyChanged(final Observable sender, final int propertyId) {
|
||||
if (binding == null)
|
||||
return;
|
||||
final Config.Observable config = binding.getConfig();
|
||||
if (config == null)
|
||||
return;
|
||||
if (propertyId == BR.config) {
|
||||
config.addOnPropertyChangedCallback(breakObjectOrientedLayeringHandler);
|
||||
breakObjectOrientedLayeringHandlerReceivers.add(config);
|
||||
config.getInterfaceSection().addOnPropertyChangedCallback(breakObjectOrientedLayeringHandler);
|
||||
breakObjectOrientedLayeringHandlerReceivers.add(config.getInterfaceSection());
|
||||
config.getPeers().addOnListChangedCallback(breakObjectListOrientedLayeringHandler);
|
||||
breakObjectOrientedLayeringHandlerReceivers.add(config.getPeers());
|
||||
} else if (propertyId == BR.dnses || propertyId == BR.peers)
|
||||
;
|
||||
else
|
||||
return;
|
||||
final int numSiblings = config.getPeers().size() - 1;
|
||||
for (final Peer.Observable peer : config.getPeers()) {
|
||||
peer.setInterfaceDNSRoutes(config.getInterfaceSection().getDnses());
|
||||
peer.setNumSiblings(numSiblings);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreateView(inflater, container, savedInstanceState);
|
||||
binding = TunnelEditorFragmentBinding.inflate(inflater, container, false);
|
||||
binding.addOnPropertyChangedCallback(breakObjectOrientedLayeringHandler);
|
||||
breakObjectOrientedLayeringHandlerReceivers.add(binding);
|
||||
binding.executePendingBindings();
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
binding = null;
|
||||
for (final Object o : breakObjectOrientedLayeringHandlerReceivers) {
|
||||
if (o instanceof Observable)
|
||||
((Observable)o).removeOnPropertyChangedCallback(breakObjectOrientedLayeringHandler);
|
||||
else if (o instanceof ObservableList)
|
||||
((ObservableList)o).removeOnListChangedCallback(breakObjectListOrientedLayeringHandler);
|
||||
}
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void onFinished() {
|
||||
// Hide the keyboard; it rarely goes away on its own.
|
||||
final Activity activity = getActivity();
|
||||
if (activity == null) return;
|
||||
final View focusedView = activity.getCurrentFocus();
|
||||
if (focusedView != null) {
|
||||
final Object service = activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
final InputMethodManager inputManager = (InputMethodManager) service;
|
||||
if (inputManager != null)
|
||||
inputManager.hideSoftInputFromWindow(focusedView.getWindowToken(),
|
||||
InputMethodManager.HIDE_NOT_ALWAYS);
|
||||
}
|
||||
// Tell the activity to finish itself or go back to the detail view.
|
||||
getActivity().runOnUiThread(() -> {
|
||||
// TODO(smaeul): Remove this hack when fixing the Config ViewModel
|
||||
// The selected tunnel has to actually change, but we have to remember this one.
|
||||
final Tunnel savedTunnel = tunnel;
|
||||
if (savedTunnel == getSelectedTunnel())
|
||||
setSelectedTunnel(null);
|
||||
setSelectedTunnel(savedTunnel);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_action_save:
|
||||
final Config newConfig = new Config();
|
||||
try {
|
||||
binding.getConfig().commitData(newConfig);
|
||||
} catch (final Exception e) {
|
||||
final String error = ExceptionLoggers.unwrapMessage(e);
|
||||
final String tunnelName = tunnel == null ? binding.getConfig().getName() : tunnel.getName();
|
||||
final String message = getString(R.string.config_save_error, tunnelName, error);
|
||||
Log.e(TAG, message, e);
|
||||
MonkeyedSnackbar.make(binding.mainContainer, error, Snackbar.LENGTH_LONG).show();
|
||||
return false;
|
||||
}
|
||||
if (tunnel == null) {
|
||||
Log.d(TAG, "Attempting to create new tunnel " + binding.getConfig().getName());
|
||||
final TunnelManager manager = Application.getTunnelManager();
|
||||
manager.create(binding.getConfig().getName(), newConfig)
|
||||
.whenComplete(this::onTunnelCreated);
|
||||
} else if (!tunnel.getName().equals(binding.getConfig().getName())) {
|
||||
Log.d(TAG, "Attempting to rename tunnel to " + binding.getConfig().getName());
|
||||
tunnel.setName(binding.getConfig().getName())
|
||||
.whenComplete((a, b) -> onTunnelRenamed(tunnel, newConfig, b));
|
||||
} else {
|
||||
Log.d(TAG, "Attempting to save config of " + tunnel.getName());
|
||||
tunnel.setConfig(newConfig)
|
||||
.whenComplete((a, b) -> onConfigSaved(tunnel, b));
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(final Bundle outState) {
|
||||
outState.putParcelable(KEY_LOCAL_CONFIG, binding.getConfig());
|
||||
outState.putString(KEY_ORIGINAL_NAME, tunnel == null ? null : tunnel.getName());
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
|
||||
tunnel = newTunnel;
|
||||
if (binding == null)
|
||||
return;
|
||||
binding.setConfig(new Config.Observable(null, null));
|
||||
if (tunnel != null)
|
||||
tunnel.getConfigAsync().thenAccept(a -> onConfigLoaded(tunnel.getName(), a));
|
||||
}
|
||||
|
||||
private void onTunnelCreated(final Tunnel newTunnel, @Nullable final Throwable throwable) {
|
||||
final String message;
|
||||
if (throwable == null) {
|
||||
tunnel = newTunnel;
|
||||
message = getString(R.string.tunnel_create_success, tunnel.getName());
|
||||
Log.d(TAG, message);
|
||||
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
|
||||
onFinished();
|
||||
} else {
|
||||
final String error = ExceptionLoggers.unwrapMessage(throwable);
|
||||
message = getString(R.string.tunnel_create_error, error);
|
||||
Log.e(TAG, message, throwable);
|
||||
if (binding != null) {
|
||||
MonkeyedSnackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onTunnelRenamed(final Tunnel renamedTunnel, final Config newConfig,
|
||||
@Nullable final Throwable throwable) {
|
||||
final String message;
|
||||
if (throwable == null) {
|
||||
message = getString(R.string.tunnel_rename_success, renamedTunnel.getName());
|
||||
Log.d(TAG, message);
|
||||
// Now save the rest of configuration changes.
|
||||
Log.d(TAG, "Attempting to save config of renamed tunnel " + tunnel.getName());
|
||||
renamedTunnel.setConfig(newConfig).whenComplete((a, b) -> onConfigSaved(renamedTunnel, b));
|
||||
} else {
|
||||
final String error = ExceptionLoggers.unwrapMessage(throwable);
|
||||
message = getString(R.string.tunnel_rename_error, error);
|
||||
Log.e(TAG, message, throwable);
|
||||
if (binding != null) {
|
||||
MonkeyedSnackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
|
||||
if (binding == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
binding.setFragment(this);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
onSelectedTunnelChanged(null, getSelectedTunnel());
|
||||
} else {
|
||||
tunnel = getSelectedTunnel();
|
||||
final Config.Observable config = savedInstanceState.getParcelable(KEY_LOCAL_CONFIG);
|
||||
final String originalName = savedInstanceState.getString(KEY_ORIGINAL_NAME);
|
||||
if (tunnel != null && !tunnel.getName().equals(originalName))
|
||||
onSelectedTunnelChanged(null, tunnel);
|
||||
else
|
||||
binding.setConfig(config);
|
||||
}
|
||||
|
||||
super.onViewStateRestored(savedInstanceState);
|
||||
}
|
||||
|
||||
public void onRequestSetExcludedApplications(@SuppressWarnings("unused") final View view) {
|
||||
final FragmentManager fragmentManager = getFragmentManager();
|
||||
if (fragmentManager != null && binding != null) {
|
||||
final String[] excludedApps = Attribute.stringToList(binding.getConfig().getInterfaceSection().getExcludedApplications());
|
||||
final AppListDialogFragment fragment = AppListDialogFragment.newInstance(excludedApps, this);
|
||||
fragment.show(fragmentManager, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExcludedAppsSelected(final List<String> excludedApps) {
|
||||
Objects.requireNonNull(binding, "Tried to set excluded apps while no view was loaded");
|
||||
binding.getConfig().getInterfaceSection().setExcludedApplications(Attribute.iterableToString(excludedApps));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,490 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.fragment;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.view.ActionMode;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.google.zxing.integration.android.IntentIntegrator;
|
||||
import com.google.zxing.integration.android.IntentResult;
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.activity.TunnelCreatorActivity;
|
||||
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter;
|
||||
import com.wireguard.android.databinding.TunnelListFragmentBinding;
|
||||
import com.wireguard.android.databinding.TunnelListItemBinding;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
import com.wireguard.android.util.ObservableSortedKeyedList;
|
||||
import com.wireguard.android.widget.MonkeyedSnackbar;
|
||||
import com.wireguard.android.widget.fab.FloatingActionsMenuRecyclerViewScrollListener;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import java9.util.concurrent.CompletableFuture;
|
||||
import java9.util.stream.StreamSupport;
|
||||
|
||||
/**
|
||||
* Fragment containing a list of known WireGuard tunnels. It allows creating and deleting tunnels.
|
||||
*/
|
||||
|
||||
public class TunnelListFragment extends BaseFragment {
|
||||
private static final int REQUEST_IMPORT = 1;
|
||||
private static final String TAG = "WireGuard/" + TunnelListFragment.class.getSimpleName();
|
||||
|
||||
private final ActionModeListener actionModeListener = new ActionModeListener();
|
||||
@Nullable private ActionMode actionMode;
|
||||
@Nullable private TunnelListFragmentBinding binding;
|
||||
|
||||
public boolean collapseActionMenu() {
|
||||
if (binding != null && binding.createMenu.isExpanded()) {
|
||||
binding.createMenu.collapse();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void importTunnel(@NonNull final String configText) {
|
||||
try {
|
||||
// Ensure the config text is parseable before proceeding…
|
||||
Config.from(configText);
|
||||
|
||||
// Config text is valid, now create the tunnel…
|
||||
final FragmentManager fragmentManager = getFragmentManager();
|
||||
if (fragmentManager != null)
|
||||
ConfigNamingDialogFragment.newInstance(configText).show(fragmentManager, null);
|
||||
} catch (final Exception exception) {
|
||||
onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(exception));
|
||||
}
|
||||
}
|
||||
|
||||
private void importTunnel(@Nullable final Uri uri) {
|
||||
final Activity activity = getActivity();
|
||||
if (activity == null || uri == null)
|
||||
return;
|
||||
final ContentResolver contentResolver = activity.getContentResolver();
|
||||
|
||||
final Collection<CompletableFuture<Tunnel>> futureTunnels = new ArrayList<>();
|
||||
final List<Throwable> throwables = new ArrayList<>();
|
||||
Application.getAsyncWorker().supplyAsync(() -> {
|
||||
final String[] columns = {OpenableColumns.DISPLAY_NAME};
|
||||
String name = null;
|
||||
try (Cursor cursor = contentResolver.query(uri, columns,
|
||||
null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst() && !cursor.isNull(0))
|
||||
name = cursor.getString(0);
|
||||
}
|
||||
if (name == null)
|
||||
name = Uri.decode(uri.getLastPathSegment());
|
||||
int idx = name.lastIndexOf('/');
|
||||
if (idx >= 0) {
|
||||
if (idx >= name.length() - 1)
|
||||
throw new IllegalArgumentException("Illegal file name: " + name);
|
||||
name = name.substring(idx + 1);
|
||||
}
|
||||
boolean isZip = name.toLowerCase().endsWith(".zip");
|
||||
if (name.toLowerCase().endsWith(".conf"))
|
||||
name = name.substring(0, name.length() - ".conf".length());
|
||||
else if (!isZip)
|
||||
throw new IllegalArgumentException("File must be .conf or .zip");
|
||||
|
||||
if (isZip) {
|
||||
try (ZipInputStream zip = new ZipInputStream(contentResolver.openInputStream(uri))) {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(zip, StandardCharsets.UTF_8));
|
||||
ZipEntry entry;
|
||||
while ((entry = zip.getNextEntry()) != null) {
|
||||
if (entry.isDirectory())
|
||||
continue;
|
||||
name = entry.getName();
|
||||
idx = name.lastIndexOf('/');
|
||||
if (idx >= 0) {
|
||||
if (idx >= name.length() - 1)
|
||||
continue;
|
||||
name = name.substring(name.lastIndexOf('/') + 1);
|
||||
}
|
||||
if (name.toLowerCase().endsWith(".conf"))
|
||||
name = name.substring(0, name.length() - ".conf".length());
|
||||
else
|
||||
continue;
|
||||
Config config = null;
|
||||
try {
|
||||
config = Config.from(reader);
|
||||
} catch (Exception e) {
|
||||
throwables.add(e);
|
||||
}
|
||||
if (config != null)
|
||||
futureTunnels.add(Application.getTunnelManager().create(name, config).toCompletableFuture());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
futureTunnels.add(Application.getTunnelManager().create(name,
|
||||
Config.from(contentResolver.openInputStream(uri))).toCompletableFuture());
|
||||
}
|
||||
|
||||
if (futureTunnels.isEmpty()) {
|
||||
if (throwables.size() == 1)
|
||||
throw throwables.get(0);
|
||||
else if (throwables.isEmpty())
|
||||
throw new IllegalArgumentException("No configurations found");
|
||||
}
|
||||
|
||||
return CompletableFuture.allOf(futureTunnels.toArray(new CompletableFuture[futureTunnels.size()]));
|
||||
}).whenComplete((future, exception) -> {
|
||||
if (exception != null) {
|
||||
onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(exception));
|
||||
} else {
|
||||
future.whenComplete((ignored1, ignored2) -> {
|
||||
final List<Tunnel> tunnels = new ArrayList<>(futureTunnels.size());
|
||||
for (final CompletableFuture<Tunnel> futureTunnel : futureTunnels) {
|
||||
Tunnel tunnel = null;
|
||||
try {
|
||||
tunnel = futureTunnel.getNow(null);
|
||||
} catch (final Exception e) {
|
||||
throwables.add(e);
|
||||
}
|
||||
if (tunnel != null)
|
||||
tunnels.add(tunnel);
|
||||
}
|
||||
onTunnelImportFinished(tunnels, throwables);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
|
||||
switch (requestCode) {
|
||||
case REQUEST_IMPORT:
|
||||
if (resultCode == Activity.RESULT_OK && data != null)
|
||||
importTunnel(data.getData());
|
||||
return;
|
||||
case IntentIntegrator.REQUEST_CODE:
|
||||
final IntentResult result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
|
||||
if (result != null && result.getContents() != null) {
|
||||
importTunnel(result.getContents());
|
||||
}
|
||||
return;
|
||||
default:
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreateView(inflater, container, savedInstanceState);
|
||||
binding = TunnelListFragmentBinding.inflate(inflater, container, false);
|
||||
|
||||
binding.tunnelList.setOnTouchListener((view, motionEvent) -> {
|
||||
if (binding != null) {
|
||||
binding.createMenu.collapse();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
binding.tunnelList.setOnScrollListener(new FloatingActionsMenuRecyclerViewScrollListener(binding.createMenu));
|
||||
binding.executePendingBindings();
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
binding = null;
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
public void onRequestCreateConfig(@SuppressWarnings("unused") final View view) {
|
||||
startActivity(new Intent(getActivity(), TunnelCreatorActivity.class));
|
||||
if (binding != null)
|
||||
binding.createMenu.collapse();
|
||||
}
|
||||
|
||||
public void onRequestImportConfig(@SuppressWarnings("unused") final View view) {
|
||||
final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("*/*");
|
||||
startActivityForResult(intent, REQUEST_IMPORT);
|
||||
if (binding != null)
|
||||
binding.createMenu.collapse();
|
||||
}
|
||||
|
||||
public void onRequestScanQRCode(@SuppressWarnings("unused") final View view) {
|
||||
final IntentIntegrator intentIntegrator = IntentIntegrator.forSupportFragment(this);
|
||||
intentIntegrator.setOrientationLocked(false);
|
||||
intentIntegrator.setBeepEnabled(false);
|
||||
intentIntegrator.setPrompt(getString(R.string.qrcode_hint));
|
||||
intentIntegrator.initiateScan(Collections.singletonList(IntentIntegrator.QR_CODE));
|
||||
|
||||
if (binding != null)
|
||||
binding.createMenu.collapse();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
if (binding != null) {
|
||||
binding.createMenu.collapse();
|
||||
}
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
|
||||
if (binding == null)
|
||||
return;
|
||||
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
|
||||
if (newTunnel != null)
|
||||
binding.tunnelList.findViewHolderForAdapterPosition(tunnels.indexOf(newTunnel)).itemView.setActivated(true);
|
||||
if (oldTunnel != null)
|
||||
binding.tunnelList.findViewHolderForAdapterPosition(tunnels.indexOf(oldTunnel)).itemView.setActivated(false);
|
||||
|
||||
});
|
||||
|
||||
/* Alternative 1: results in sluggish change:
|
||||
|
||||
if (binding.tunnelList.getAdapter() == null)
|
||||
return;
|
||||
|
||||
|
||||
* Alternative 2: results in overly quick change:
|
||||
|
||||
binding.tunnelList.getAdapter().notifyDataSetChanged();
|
||||
|
||||
* Hence, we go with the above.
|
||||
*/
|
||||
}
|
||||
|
||||
private void onTunnelDeletionFinished(final Integer count, @Nullable final Throwable throwable) {
|
||||
final String message;
|
||||
if (throwable == null) {
|
||||
message = getResources().getQuantityString(R.plurals.delete_success, count, count);
|
||||
} else {
|
||||
final String error = ExceptionLoggers.unwrapMessage(throwable);
|
||||
message = getResources().getQuantityString(R.plurals.delete_error, count, count, error);
|
||||
Log.e(TAG, message, throwable);
|
||||
}
|
||||
if (binding != null) {
|
||||
MonkeyedSnackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void onTunnelImportFinished(final List<Tunnel> tunnels, final Collection<Throwable> throwables) {
|
||||
String message = null;
|
||||
|
||||
for (final Throwable throwable : throwables) {
|
||||
final String error = ExceptionLoggers.unwrapMessage(throwable);
|
||||
message = getString(R.string.import_error, error);
|
||||
Log.e(TAG, message, throwable);
|
||||
}
|
||||
|
||||
if (tunnels.size() == 1 && throwables.isEmpty())
|
||||
message = getString(R.string.import_success, tunnels.get(0).getName());
|
||||
else if (tunnels.isEmpty() && throwables.size() == 1)
|
||||
/* Use the exception message from above. */ ;
|
||||
else if (throwables.isEmpty())
|
||||
message = getResources().getQuantityString(R.plurals.import_total_success,
|
||||
tunnels.size(), tunnels.size());
|
||||
else if (!throwables.isEmpty())
|
||||
message = getResources().getQuantityString(R.plurals.import_partial_success,
|
||||
tunnels.size() + throwables.size(),
|
||||
tunnels.size(), tunnels.size() + throwables.size());
|
||||
|
||||
if (binding != null)
|
||||
MonkeyedSnackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
outState.putIntegerArrayList("CHECKED_ITEMS", actionModeListener.getCheckedItems());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(@Nullable final Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
final Collection<Integer> checkedItems = savedInstanceState.getIntegerArrayList("CHECKED_ITEMS");
|
||||
if (checkedItems != null) {
|
||||
for (final Integer i : checkedItems)
|
||||
actionModeListener.setItemChecked(i, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
|
||||
super.onViewStateRestored(savedInstanceState);
|
||||
|
||||
if (binding == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
binding.setFragment(this);
|
||||
Application.getTunnelManager().getTunnels().thenAccept(binding::setTunnels);
|
||||
binding.setRowConfigurationHandler((ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TunnelListItemBinding, Tunnel>) (binding, tunnel, position) -> {
|
||||
binding.setFragment(this);
|
||||
binding.getRoot().setOnClickListener(clicked -> {
|
||||
if (actionMode == null) {
|
||||
setSelectedTunnel(tunnel);
|
||||
} else {
|
||||
actionModeListener.toggleItemChecked(position);
|
||||
}
|
||||
});
|
||||
binding.getRoot().setOnLongClickListener(clicked -> {
|
||||
actionModeListener.toggleItemChecked(position);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (actionMode != null)
|
||||
binding.getRoot().setActivated(actionModeListener.checkedItems.contains(position));
|
||||
else
|
||||
binding.getRoot().setActivated(getSelectedTunnel() == tunnel);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private final class ActionModeListener implements ActionMode.Callback {
|
||||
private final Collection<Integer> checkedItems = new HashSet<>();
|
||||
|
||||
@Nullable private Resources resources;
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_action_delete:
|
||||
final Iterable<Integer> copyCheckedItems = new HashSet<>(checkedItems);
|
||||
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
|
||||
final Collection<Tunnel> tunnelsToDelete = new ArrayList<>();
|
||||
for (final Integer position : copyCheckedItems)
|
||||
tunnelsToDelete.add(tunnels.get(position));
|
||||
|
||||
final CompletableFuture[] futures = StreamSupport.stream(tunnelsToDelete)
|
||||
.map(Tunnel::delete)
|
||||
.toArray(CompletableFuture[]::new);
|
||||
CompletableFuture.allOf(futures)
|
||||
.thenApply(x -> futures.length)
|
||||
.whenComplete(TunnelListFragment.this::onTunnelDeletionFinished);
|
||||
|
||||
});
|
||||
checkedItems.clear();
|
||||
mode.finish();
|
||||
return true;
|
||||
case R.id.menu_action_select_all:
|
||||
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
|
||||
for (int i = 0; i < tunnels.size(); ++i) {
|
||||
setItemChecked(i, true);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(final ActionMode mode, final Menu menu) {
|
||||
actionMode = mode;
|
||||
if (getActivity() != null) {
|
||||
resources = getActivity().getResources();
|
||||
}
|
||||
mode.getMenuInflater().inflate(R.menu.tunnel_list_action_mode, menu);
|
||||
binding.tunnelList.getAdapter().notifyDataSetChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(final ActionMode mode) {
|
||||
actionMode = null;
|
||||
resources = null;
|
||||
|
||||
checkedItems.clear();
|
||||
binding.tunnelList.getAdapter().notifyDataSetChanged();
|
||||
}
|
||||
|
||||
void toggleItemChecked(final int position) {
|
||||
setItemChecked(position, !checkedItems.contains(position));
|
||||
}
|
||||
|
||||
public ArrayList<Integer> getCheckedItems() {
|
||||
return new ArrayList<>(checkedItems);
|
||||
}
|
||||
|
||||
void setItemChecked(final int position, final boolean checked) {
|
||||
if (checked) {
|
||||
checkedItems.add(position);
|
||||
} else {
|
||||
checkedItems.remove(position);
|
||||
}
|
||||
|
||||
final RecyclerView.Adapter adapter = binding == null ? null : binding.tunnelList.getAdapter();
|
||||
|
||||
if (actionMode == null && !checkedItems.isEmpty() && getActivity() != null) {
|
||||
((AppCompatActivity) getActivity()).startSupportActionMode(this);
|
||||
} else if (actionMode != null && checkedItems.isEmpty()) {
|
||||
actionMode.finish();
|
||||
}
|
||||
|
||||
if (adapter != null)
|
||||
adapter.notifyItemChanged(position);
|
||||
|
||||
updateTitle(actionMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(final ActionMode mode, final Menu menu) {
|
||||
updateTitle(mode);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void updateTitle(@Nullable final ActionMode mode) {
|
||||
if (mode == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int count = checkedItems.size();
|
||||
if (count == 0) {
|
||||
mode.setTitle("");
|
||||
} else {
|
||||
mode.setTitle(resources.getQuantityString(R.plurals.delete_title, count, count));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Eric Kuck <eric@bluelinelabs.com>.
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.model;
|
||||
|
||||
import android.databinding.BaseObservable;
|
||||
import android.databinding.Bindable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import com.wireguard.android.BR;
|
||||
import com.wireguard.util.Keyed;
|
||||
|
||||
public class ApplicationData extends BaseObservable implements Keyed<String> {
|
||||
|
||||
private final Drawable icon;
|
||||
private final String name;
|
||||
private final String packageName;
|
||||
private boolean excludedFromTunnel;
|
||||
|
||||
public ApplicationData(final Drawable icon, final String name, final String packageName, final boolean excludedFromTunnel) {
|
||||
this.icon = icon;
|
||||
this.name = name;
|
||||
this.packageName = packageName;
|
||||
this.excludedFromTunnel = excludedFromTunnel;
|
||||
}
|
||||
|
||||
public Drawable getIcon() {
|
||||
return icon;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public boolean isExcludedFromTunnel() {
|
||||
return excludedFromTunnel;
|
||||
}
|
||||
|
||||
public void setExcludedFromTunnel(final boolean excludedFromTunnel) {
|
||||
this.excludedFromTunnel = excludedFromTunnel;
|
||||
notifyPropertyChanged(BR.excludedFromTunnel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.model;
|
||||
|
||||
import android.databinding.BaseObservable;
|
||||
import android.databinding.Bindable;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.wireguard.android.BR;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
import com.wireguard.config.Config;
|
||||
import com.wireguard.util.Keyed;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import java9.util.concurrent.CompletableFuture;
|
||||
import java9.util.concurrent.CompletionStage;
|
||||
|
||||
/**
|
||||
* Encapsulates the volatile and nonvolatile state of a WireGuard tunnel.
|
||||
*/
|
||||
|
||||
public class Tunnel extends BaseObservable implements Keyed<String> {
|
||||
public static final int NAME_MAX_LENGTH = 15;
|
||||
private static final Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z0-9_=+.-]{1,15}");
|
||||
|
||||
private final TunnelManager manager;
|
||||
@Nullable private Config config;
|
||||
private String name;
|
||||
private State state;
|
||||
@Nullable private Statistics statistics;
|
||||
|
||||
Tunnel(final TunnelManager manager, final String name,
|
||||
@Nullable final Config config, final State state) {
|
||||
this.manager = manager;
|
||||
this.name = name;
|
||||
this.config = config;
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public static boolean isNameInvalid(final CharSequence name) {
|
||||
return !NAME_PATTERN.matcher(name).matches();
|
||||
}
|
||||
|
||||
public CompletionStage<Void> delete() {
|
||||
return manager.delete(this);
|
||||
}
|
||||
|
||||
@Bindable @Nullable
|
||||
public Config getConfig() {
|
||||
if (config == null)
|
||||
manager.getTunnelConfig(this).whenComplete(ExceptionLoggers.E);
|
||||
return config;
|
||||
}
|
||||
|
||||
public CompletionStage<Config> getConfigAsync() {
|
||||
if (config == null)
|
||||
return manager.getTunnelConfig(this);
|
||||
return CompletableFuture.completedFuture(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public State getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public CompletionStage<State> getStateAsync() {
|
||||
return TunnelManager.getTunnelState(this);
|
||||
}
|
||||
|
||||
@Bindable @Nullable
|
||||
public Statistics getStatistics() {
|
||||
// FIXME: Check age of statistics.
|
||||
if (statistics == null)
|
||||
TunnelManager.getTunnelStatistics(this).whenComplete(ExceptionLoggers.E);
|
||||
return statistics;
|
||||
}
|
||||
|
||||
public CompletionStage<Statistics> getStatisticsAsync() {
|
||||
// FIXME: Check age of statistics.
|
||||
if (statistics == null)
|
||||
return TunnelManager.getTunnelStatistics(this);
|
||||
return CompletableFuture.completedFuture(statistics);
|
||||
}
|
||||
|
||||
Config onConfigChanged(final Config config) {
|
||||
this.config = config;
|
||||
notifyPropertyChanged(BR.config);
|
||||
return config;
|
||||
}
|
||||
|
||||
public String onNameChanged(final String name) {
|
||||
this.name = name;
|
||||
notifyPropertyChanged(BR.name);
|
||||
return name;
|
||||
}
|
||||
|
||||
State onStateChanged(final State state) {
|
||||
if (state != State.UP)
|
||||
onStatisticsChanged(null);
|
||||
this.state = state;
|
||||
notifyPropertyChanged(BR.state);
|
||||
return state;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Statistics onStatisticsChanged(@Nullable final Statistics statistics) {
|
||||
this.statistics = statistics;
|
||||
notifyPropertyChanged(BR.statistics);
|
||||
return statistics;
|
||||
}
|
||||
|
||||
public CompletionStage<Config> setConfig(final Config config) {
|
||||
if (!config.equals(this.config))
|
||||
return manager.setTunnelConfig(this, config);
|
||||
return CompletableFuture.completedFuture(this.config);
|
||||
}
|
||||
|
||||
public CompletionStage<String> setName(final String name) {
|
||||
if (!name.equals(this.name))
|
||||
return manager.setTunnelName(this, name);
|
||||
return CompletableFuture.completedFuture(this.name);
|
||||
}
|
||||
|
||||
public CompletionStage<State> setState(final State state) {
|
||||
if (state != this.state)
|
||||
return manager.setTunnelState(this, state);
|
||||
return CompletableFuture.completedFuture(this.state);
|
||||
}
|
||||
|
||||
public enum State {
|
||||
DOWN,
|
||||
TOGGLE,
|
||||
UP;
|
||||
|
||||
public static State of(final boolean running) {
|
||||
return running ? UP : DOWN;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Statistics extends BaseObservable { }
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.model;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.databinding.BaseObservable;
|
||||
import android.databinding.Bindable;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.BR;
|
||||
import com.wireguard.android.configStore.ConfigStore;
|
||||
import com.wireguard.android.model.Tunnel.State;
|
||||
import com.wireguard.android.model.Tunnel.Statistics;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
import com.wireguard.android.util.ObservableSortedKeyedArrayList;
|
||||
import com.wireguard.android.util.ObservableSortedKeyedList;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.Set;
|
||||
|
||||
import java9.util.Comparators;
|
||||
import java9.util.concurrent.CompletableFuture;
|
||||
import java9.util.concurrent.CompletionStage;
|
||||
import java9.util.stream.Collectors;
|
||||
import java9.util.stream.StreamSupport;
|
||||
|
||||
/**
|
||||
* Maintains and mediates changes to the set of available WireGuard tunnels,
|
||||
*/
|
||||
|
||||
public final class TunnelManager extends BaseObservable {
|
||||
private static final Comparator<String> COMPARATOR = Comparators.<String>thenComparing(
|
||||
String.CASE_INSENSITIVE_ORDER, Comparators.naturalOrder());
|
||||
private static final String KEY_LAST_USED_TUNNEL = "last_used_tunnel";
|
||||
private static final String KEY_RESTORE_ON_BOOT = "restore_on_boot";
|
||||
private static final String KEY_RUNNING_TUNNELS = "enabled_configs";
|
||||
|
||||
private final ConfigStore configStore;
|
||||
private final CompletableFuture<ObservableSortedKeyedList<String, Tunnel>> completableTunnels = new CompletableFuture<>();
|
||||
private final ObservableSortedKeyedList<String, Tunnel> tunnels = new ObservableSortedKeyedArrayList<>(COMPARATOR);
|
||||
@Nullable private Tunnel lastUsedTunnel;
|
||||
private boolean haveLoaded;
|
||||
private final ArrayList<CompletableFuture<Void>> delayedLoadRestoreTunnels = new ArrayList<>();
|
||||
|
||||
public TunnelManager(final ConfigStore configStore) {
|
||||
this.configStore = configStore;
|
||||
}
|
||||
|
||||
private Tunnel addToList(final String name, @Nullable final Config config, final State state) {
|
||||
final Tunnel tunnel = new Tunnel(this, name, config, state);
|
||||
tunnels.add(tunnel);
|
||||
return tunnel;
|
||||
}
|
||||
|
||||
public CompletionStage<Tunnel> create(final String name, @Nullable final Config config) {
|
||||
if (Tunnel.isNameInvalid(name))
|
||||
return CompletableFuture.failedFuture(new IllegalArgumentException("Invalid name"));
|
||||
if (tunnels.containsKey(name)) {
|
||||
final String message = "Tunnel " + name + " already exists";
|
||||
return CompletableFuture.failedFuture(new IllegalArgumentException(message));
|
||||
}
|
||||
return Application.getAsyncWorker().supplyAsync(() -> configStore.create(name, config))
|
||||
.thenApply(savedConfig -> addToList(name, savedConfig, State.DOWN));
|
||||
}
|
||||
|
||||
CompletionStage<Void> delete(final Tunnel tunnel) {
|
||||
final State originalState = tunnel.getState();
|
||||
final boolean wasLastUsed = tunnel == lastUsedTunnel;
|
||||
// Make sure nothing touches the tunnel.
|
||||
if (wasLastUsed)
|
||||
setLastUsedTunnel(null);
|
||||
tunnels.remove(tunnel);
|
||||
return Application.getAsyncWorker().runAsync(() -> {
|
||||
if (originalState == State.UP)
|
||||
Application.getBackend().setState(tunnel, State.DOWN);
|
||||
try {
|
||||
configStore.delete(tunnel.getName());
|
||||
} catch (final Exception e) {
|
||||
if (originalState == State.UP)
|
||||
Application.getBackend().setState(tunnel, State.UP);
|
||||
// Re-throw the exception to fail the completion.
|
||||
throw e;
|
||||
}
|
||||
}).whenComplete((x, e) -> {
|
||||
if (e == null)
|
||||
return;
|
||||
// Failure, put the tunnel back.
|
||||
tunnels.add(tunnel);
|
||||
if (wasLastUsed)
|
||||
setLastUsedTunnel(tunnel);
|
||||
});
|
||||
}
|
||||
|
||||
@Bindable @Nullable
|
||||
public Tunnel getLastUsedTunnel() {
|
||||
return lastUsedTunnel;
|
||||
}
|
||||
|
||||
CompletionStage<Config> getTunnelConfig(final Tunnel tunnel) {
|
||||
return Application.getAsyncWorker().supplyAsync(() -> configStore.load(tunnel.getName()))
|
||||
.thenApply(tunnel::onConfigChanged);
|
||||
}
|
||||
|
||||
static CompletionStage<State> getTunnelState(final Tunnel tunnel) {
|
||||
return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getState(tunnel))
|
||||
.thenApply(tunnel::onStateChanged);
|
||||
}
|
||||
|
||||
static CompletionStage<Statistics> getTunnelStatistics(final Tunnel tunnel) {
|
||||
return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getStatistics(tunnel))
|
||||
.thenApply(tunnel::onStatisticsChanged);
|
||||
}
|
||||
|
||||
public CompletableFuture<ObservableSortedKeyedList<String, Tunnel>> getTunnels() {
|
||||
return completableTunnels;
|
||||
}
|
||||
|
||||
public void onCreate() {
|
||||
Application.getAsyncWorker().supplyAsync(configStore::enumerate)
|
||||
.thenAcceptBoth(Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().enumerate()), this::onTunnelsLoaded)
|
||||
.whenComplete(ExceptionLoggers.E);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void onTunnelsLoaded(final Iterable<String> present, final Collection<String> running) {
|
||||
for (final String name : present)
|
||||
addToList(name, null, running.contains(name) ? State.UP : State.DOWN);
|
||||
final String lastUsedName = Application.getSharedPreferences().getString(KEY_LAST_USED_TUNNEL, null);
|
||||
if (lastUsedName != null)
|
||||
setLastUsedTunnel(tunnels.get(lastUsedName));
|
||||
final CompletableFuture<Void>[] toComplete;
|
||||
synchronized (delayedLoadRestoreTunnels) {
|
||||
haveLoaded = true;
|
||||
toComplete = delayedLoadRestoreTunnels.toArray(new CompletableFuture[delayedLoadRestoreTunnels.size()]);
|
||||
delayedLoadRestoreTunnels.clear();
|
||||
}
|
||||
restoreState(true).whenComplete((v, t) -> {
|
||||
for (final CompletableFuture<Void> f : toComplete) {
|
||||
if (t == null)
|
||||
f.complete(v);
|
||||
else
|
||||
f.completeExceptionally(t);
|
||||
}
|
||||
});
|
||||
|
||||
completableTunnels.complete(tunnels);
|
||||
}
|
||||
|
||||
public void refreshTunnelStates() {
|
||||
Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().enumerate())
|
||||
.thenAccept(running -> {
|
||||
for (final Tunnel tunnel : tunnels)
|
||||
tunnel.onStateChanged(running.contains(tunnel.getName()) ? State.UP : State.DOWN);
|
||||
})
|
||||
.whenComplete(ExceptionLoggers.E);
|
||||
}
|
||||
|
||||
public CompletionStage<Void> restoreState(final boolean force) {
|
||||
if (!force && !Application.getSharedPreferences().getBoolean(KEY_RESTORE_ON_BOOT, false))
|
||||
return CompletableFuture.completedFuture(null);
|
||||
synchronized (delayedLoadRestoreTunnels) {
|
||||
if (!haveLoaded) {
|
||||
final CompletableFuture<Void> f = new CompletableFuture<>();
|
||||
delayedLoadRestoreTunnels.add(f);
|
||||
return f;
|
||||
}
|
||||
}
|
||||
final Set<String> previouslyRunning = Application.getSharedPreferences().getStringSet(KEY_RUNNING_TUNNELS, null);
|
||||
if (previouslyRunning == null)
|
||||
return CompletableFuture.completedFuture(null);
|
||||
return CompletableFuture.allOf(StreamSupport.stream(tunnels)
|
||||
.filter(tunnel -> previouslyRunning.contains(tunnel.getName()))
|
||||
.map(tunnel -> setTunnelState(tunnel, State.UP))
|
||||
.toArray(CompletableFuture[]::new));
|
||||
}
|
||||
|
||||
public void saveState() {
|
||||
final Set<String> runningTunnels = StreamSupport.stream(tunnels)
|
||||
.filter(tunnel -> tunnel.getState() == State.UP)
|
||||
.map(Tunnel::getName)
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
Application.getSharedPreferences().edit().putStringSet(KEY_RUNNING_TUNNELS, runningTunnels).apply();
|
||||
}
|
||||
|
||||
private void setLastUsedTunnel(@Nullable final Tunnel tunnel) {
|
||||
if (tunnel == lastUsedTunnel)
|
||||
return;
|
||||
lastUsedTunnel = tunnel;
|
||||
notifyPropertyChanged(BR.lastUsedTunnel);
|
||||
if (tunnel != null)
|
||||
Application.getSharedPreferences().edit().putString(KEY_LAST_USED_TUNNEL, tunnel.getName()).apply();
|
||||
else
|
||||
Application.getSharedPreferences().edit().remove(KEY_LAST_USED_TUNNEL).apply();
|
||||
}
|
||||
|
||||
CompletionStage<Config> setTunnelConfig(final Tunnel tunnel, final Config config) {
|
||||
return Application.getAsyncWorker().supplyAsync(() -> {
|
||||
final Config appliedConfig = Application.getBackend().applyConfig(tunnel, config);
|
||||
return configStore.save(tunnel.getName(), appliedConfig);
|
||||
}).thenApply(tunnel::onConfigChanged);
|
||||
}
|
||||
|
||||
CompletionStage<String> setTunnelName(final Tunnel tunnel, final String name) {
|
||||
if (Tunnel.isNameInvalid(name))
|
||||
return CompletableFuture.failedFuture(new IllegalArgumentException("Invalid name"));
|
||||
if (tunnels.containsKey(name)) {
|
||||
final String message = "Tunnel " + name + " already exists";
|
||||
return CompletableFuture.failedFuture(new IllegalArgumentException(message));
|
||||
}
|
||||
final State originalState = tunnel.getState();
|
||||
final boolean wasLastUsed = tunnel == lastUsedTunnel;
|
||||
// Make sure nothing touches the tunnel.
|
||||
if (wasLastUsed)
|
||||
setLastUsedTunnel(null);
|
||||
tunnels.remove(tunnel);
|
||||
return Application.getAsyncWorker().supplyAsync(() -> {
|
||||
if (originalState == State.UP)
|
||||
Application.getBackend().setState(tunnel, State.DOWN);
|
||||
configStore.rename(tunnel.getName(), name);
|
||||
final String newName = tunnel.onNameChanged(name);
|
||||
if (originalState == State.UP)
|
||||
Application.getBackend().setState(tunnel, State.UP);
|
||||
return newName;
|
||||
}).whenComplete((newName, e) -> {
|
||||
// On failure, we don't know what state the tunnel might be in. Fix that.
|
||||
if (e != null)
|
||||
getTunnelState(tunnel);
|
||||
// Add the tunnel back to the manager, under whatever name it thinks it has.
|
||||
tunnels.add(tunnel);
|
||||
if (wasLastUsed)
|
||||
setLastUsedTunnel(tunnel);
|
||||
});
|
||||
}
|
||||
|
||||
CompletionStage<State> setTunnelState(final Tunnel tunnel, final State state) {
|
||||
// Ensure the configuration is loaded before trying to use it.
|
||||
return tunnel.getConfigAsync().thenCompose(x ->
|
||||
Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().setState(tunnel, state))
|
||||
).whenComplete((newState, e) -> {
|
||||
// Ensure onStateChanged is always called (failure or not), and with the correct state.
|
||||
tunnel.onStateChanged(e == null ? newState : tunnel.getState());
|
||||
if (e == null && newState == State.UP)
|
||||
setLastUsedTunnel(tunnel);
|
||||
saveState();
|
||||
});
|
||||
}
|
||||
|
||||
public static final class IntentReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(final Context context, @Nullable final Intent intent) {
|
||||
final TunnelManager manager = Application.getTunnelManager();
|
||||
if (intent == null)
|
||||
return;
|
||||
final String action = intent.getAction();
|
||||
if (action == null)
|
||||
return;
|
||||
|
||||
if ("com.wireguard.android.action.REFRESH_TUNNEL_STATES".equals(action)) {
|
||||
manager.refreshTunnelStates();
|
||||
return;
|
||||
}
|
||||
|
||||
/* We disable the below, for now, as the security model of allowing this
|
||||
* might take a bit more consideration.
|
||||
*/
|
||||
if (true)
|
||||
return;
|
||||
|
||||
final State state;
|
||||
if ("com.wireguard.android.action.SET_TUNNEL_UP".equals(action))
|
||||
state = State.UP;
|
||||
else if ("com.wireguard.android.action.SET_TUNNEL_DOWN".equals(action))
|
||||
state = State.DOWN;
|
||||
else
|
||||
return;
|
||||
|
||||
final String tunnelName = intent.getStringExtra("tunnel");
|
||||
if (tunnelName == null)
|
||||
return;
|
||||
manager.getTunnels().thenAccept(tunnels -> {
|
||||
final Tunnel tunnel = tunnels.get(tunnelName);
|
||||
if (tunnel == null)
|
||||
return;
|
||||
manager.setTunnelState(tunnel, state);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.preference;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Environment;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v7.preference.Preference;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
import com.wireguard.android.util.FragmentUtils;
|
||||
import com.wireguard.android.widget.MonkeyedSnackbar;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
/**
|
||||
* Preference implementing a button that asynchronously exports logs.
|
||||
*/
|
||||
|
||||
public class LogExporterPreference extends Preference {
|
||||
private static final String TAG = "WireGuard/" + LogExporterPreference.class.getSimpleName();
|
||||
|
||||
@Nullable private String exportedFilePath;
|
||||
|
||||
public LogExporterPreference(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
private void exportLog() {
|
||||
Application.getAsyncWorker().supplyAsync(() -> {
|
||||
final File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||
final File file = new File(path, "wireguard-log.txt");
|
||||
if (!path.isDirectory() && !path.mkdirs())
|
||||
throw new IOException("Cannot create output directory");
|
||||
|
||||
/* We would like to simply run `builder.redirectOutput(file);`, but this is API 26.
|
||||
* Instead we have to do this dance, since logcat appends.
|
||||
*/
|
||||
new FileOutputStream(file).close();
|
||||
|
||||
try {
|
||||
final Process process = Runtime.getRuntime().exec(new String[]{
|
||||
"logcat", "-b", "all", "-d", "-v", "threadtime", "-f", file.getAbsolutePath(), "*:V"});
|
||||
if (process.waitFor() != 0) {
|
||||
try (final BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
|
||||
final StringBuilder errors = new StringBuilder();
|
||||
errors.append("Unable to run logcat: ");
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null)
|
||||
errors.append(line);
|
||||
throw new Exception(errors.toString());
|
||||
}
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
// noinspection ResultOfMethodCallIgnored
|
||||
file.delete();
|
||||
throw e;
|
||||
}
|
||||
return file.getAbsolutePath();
|
||||
}).whenComplete(this::exportLogComplete);
|
||||
}
|
||||
|
||||
private void exportLogComplete(final String filePath, @Nullable final Throwable throwable) {
|
||||
if (throwable != null) {
|
||||
final String error = ExceptionLoggers.unwrapMessage(throwable);
|
||||
final String message = getContext().getString(R.string.log_export_error, error);
|
||||
Log.e(TAG, message, throwable);
|
||||
MonkeyedSnackbar.make(
|
||||
FragmentUtils.getPrefActivity(this).findViewById(android.R.id.content),
|
||||
message, Snackbar.LENGTH_LONG).show();
|
||||
setEnabled(true);
|
||||
} else {
|
||||
exportedFilePath = filePath;
|
||||
notifyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getSummary() {
|
||||
return exportedFilePath == null ?
|
||||
getContext().getString(R.string.log_export_summary) :
|
||||
getContext().getString(R.string.log_export_success, exportedFilePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getTitle() {
|
||||
return getContext().getString(R.string.log_exporter_title);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onClick() {
|
||||
FragmentUtils.getPrefActivity(this).ensurePermissions(
|
||||
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||
(permissions, granted) -> {
|
||||
if (granted.length > 0 && granted[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
setEnabled(false);
|
||||
exportLog();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.preference.Preference;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.util.ToolsInstaller;
|
||||
|
||||
/**
|
||||
* Preference implementing a button that asynchronously runs {@code ToolsInstaller} and displays the
|
||||
* result as the preference summary.
|
||||
*/
|
||||
|
||||
public class ToolsInstallerPreference extends Preference {
|
||||
private State state = State.INITIAL;
|
||||
|
||||
public ToolsInstallerPreference(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getSummary() {
|
||||
return getContext().getString(state.messageResourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getTitle() {
|
||||
return getContext().getString(R.string.tools_installer_title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttached() {
|
||||
super.onAttached();
|
||||
Application.getAsyncWorker().supplyAsync(Application.getToolsInstaller()::areInstalled).whenComplete(this::onCheckResult);
|
||||
}
|
||||
|
||||
private void onCheckResult(final int state, @Nullable final Throwable throwable) {
|
||||
if (throwable != null || state == ToolsInstaller.ERROR)
|
||||
setState(State.INITIAL);
|
||||
else if ((state & ToolsInstaller.YES) == ToolsInstaller.YES)
|
||||
setState(State.ALREADY);
|
||||
else if ((state & (ToolsInstaller.MAGISK | ToolsInstaller.NO)) == (ToolsInstaller.MAGISK | ToolsInstaller.NO))
|
||||
setState(State.INITIAL_MAGISK);
|
||||
else if ((state & (ToolsInstaller.SYSTEM | ToolsInstaller.NO)) == (ToolsInstaller.SYSTEM | ToolsInstaller.NO))
|
||||
setState(State.INITIAL_SYSTEM);
|
||||
else
|
||||
setState(State.INITIAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onClick() {
|
||||
setState(State.WORKING);
|
||||
Application.getAsyncWorker().supplyAsync(Application.getToolsInstaller()::install).whenComplete(this::onInstallResult);
|
||||
}
|
||||
|
||||
private void onInstallResult(final Integer result, @Nullable final Throwable throwable) {
|
||||
if (throwable != null)
|
||||
setState(State.FAILURE);
|
||||
else if ((result & (ToolsInstaller.YES | ToolsInstaller.MAGISK)) == (ToolsInstaller.YES | ToolsInstaller.MAGISK))
|
||||
setState(State.SUCCESS_MAGISK);
|
||||
else if ((result & (ToolsInstaller.YES | ToolsInstaller.SYSTEM)) == (ToolsInstaller.YES | ToolsInstaller.SYSTEM))
|
||||
setState(State.SUCCESS_SYSTEM);
|
||||
else
|
||||
setState(State.FAILURE);
|
||||
}
|
||||
|
||||
private void setState(final State state) {
|
||||
if (this.state == state)
|
||||
return;
|
||||
this.state = state;
|
||||
if (isEnabled() != state.shouldEnableView)
|
||||
setEnabled(state.shouldEnableView);
|
||||
notifyChanged();
|
||||
}
|
||||
|
||||
private enum State {
|
||||
INITIAL(R.string.tools_installer_initial, true),
|
||||
ALREADY(R.string.tools_installer_already, false),
|
||||
FAILURE(R.string.tools_installer_failure, true),
|
||||
WORKING(R.string.tools_installer_working, false),
|
||||
INITIAL_SYSTEM(R.string.tools_installer_initial_system, true),
|
||||
SUCCESS_SYSTEM(R.string.tools_installer_success_system, false),
|
||||
INITIAL_MAGISK(R.string.tools_installer_initial_magisk, true),
|
||||
SUCCESS_MAGISK(R.string.tools_installer_success_magisk, false);
|
||||
|
||||
private final int messageResourceId;
|
||||
private final boolean shouldEnableView;
|
||||
|
||||
State(final int messageResourceId, final boolean shouldEnableView) {
|
||||
this.messageResourceId = messageResourceId;
|
||||
this.shouldEnableView = shouldEnableView;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.preference;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.preference.Preference;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.BuildConfig;
|
||||
import com.wireguard.android.R;
|
||||
|
||||
public class VersionPreference extends Preference {
|
||||
@Nullable private String versionSummary;
|
||||
|
||||
public VersionPreference(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
Application.getBackendAsync().thenAccept(backend -> {
|
||||
versionSummary = getContext().getString(R.string.version_summary_checking, backend.getTypeName().toLowerCase());
|
||||
Application.getAsyncWorker().supplyAsync(backend::getVersion).whenComplete((version, exception) -> {
|
||||
versionSummary = exception == null
|
||||
? getContext().getString(R.string.version_summary, backend.getTypeName(), version)
|
||||
: getContext().getString(R.string.version_summary_unknown, backend.getTypeName().toLowerCase());
|
||||
notifyChanged();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override @Nullable
|
||||
public CharSequence getSummary() {
|
||||
return versionSummary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getTitle() {
|
||||
return getContext().getString(R.string.version_title, BuildConfig.VERSION_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onClick() {
|
||||
final Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse("https://www.wireguard.com/"));
|
||||
try {
|
||||
getContext().startActivity(intent);
|
||||
} catch (final ActivityNotFoundException ignored) { }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.preference;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Environment;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v7.preference.Preference;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
import com.wireguard.android.util.FragmentUtils;
|
||||
import com.wireguard.android.widget.MonkeyedSnackbar;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import java9.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Preference implementing a button that asynchronously exports config zips.
|
||||
*/
|
||||
|
||||
public class ZipExporterPreference extends Preference {
|
||||
private static final String TAG = "WireGuard/" + ZipExporterPreference.class.getSimpleName();
|
||||
|
||||
@Nullable private String exportedFilePath;
|
||||
|
||||
public ZipExporterPreference(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
private void exportZip() {
|
||||
Application.getTunnelManager().getTunnels().thenAccept(this::exportZip);
|
||||
}
|
||||
|
||||
private void exportZip(final List<Tunnel> tunnels) {
|
||||
final List<CompletableFuture<Config>> futureConfigs = new ArrayList<>(tunnels.size());
|
||||
for (final Tunnel tunnel : tunnels)
|
||||
futureConfigs.add(tunnel.getConfigAsync().toCompletableFuture());
|
||||
if (futureConfigs.isEmpty()) {
|
||||
exportZipComplete(null, new IllegalArgumentException("No tunnels exist"));
|
||||
return;
|
||||
}
|
||||
CompletableFuture.allOf(futureConfigs.toArray(new CompletableFuture[futureConfigs.size()]))
|
||||
.whenComplete((ignored1, exception) -> Application.getAsyncWorker().supplyAsync(() -> {
|
||||
if (exception != null)
|
||||
throw exception;
|
||||
final File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||
final File file = new File(path, "wireguard-export.zip");
|
||||
if (!path.isDirectory() && !path.mkdirs())
|
||||
throw new IOException("Cannot create output directory");
|
||||
try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(file))) {
|
||||
for (int i = 0; i < futureConfigs.size(); ++i) {
|
||||
zip.putNextEntry(new ZipEntry(tunnels.get(i).getName() + ".conf"));
|
||||
zip.write(futureConfigs.get(i).getNow(null).
|
||||
toString().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
zip.closeEntry();
|
||||
} catch (final Exception e) {
|
||||
// noinspection ResultOfMethodCallIgnored
|
||||
file.delete();
|
||||
throw e;
|
||||
}
|
||||
return file.getAbsolutePath();
|
||||
}).whenComplete(this::exportZipComplete));
|
||||
}
|
||||
|
||||
private void exportZipComplete(@Nullable final String filePath, @Nullable final Throwable throwable) {
|
||||
if (throwable != null) {
|
||||
final String error = ExceptionLoggers.unwrapMessage(throwable);
|
||||
final String message = getContext().getString(R.string.zip_export_error, error);
|
||||
Log.e(TAG, message, throwable);
|
||||
MonkeyedSnackbar.make(
|
||||
FragmentUtils.getPrefActivity(this).findViewById(android.R.id.content),
|
||||
message, Snackbar.LENGTH_LONG).show();
|
||||
setEnabled(true);
|
||||
} else {
|
||||
exportedFilePath = filePath;
|
||||
notifyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getSummary() {
|
||||
return exportedFilePath == null ?
|
||||
getContext().getString(R.string.zip_export_summary) :
|
||||
getContext().getString(R.string.zip_export_success, exportedFilePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getTitle() {
|
||||
return getContext().getString(R.string.zip_exporter_title);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onClick() {
|
||||
FragmentUtils.getPrefActivity(this).ensurePermissions(
|
||||
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||
(permissions, granted) -> {
|
||||
if (granted.length > 0 && granted[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
setEnabled(false);
|
||||
exportZip();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.os.Handler;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import java9.util.concurrent.CompletableFuture;
|
||||
import java9.util.concurrent.CompletionStage;
|
||||
|
||||
/**
|
||||
* Helper class for running asynchronous tasks and ensuring they are completed on the main thread.
|
||||
*/
|
||||
|
||||
public class AsyncWorker {
|
||||
private final Executor executor;
|
||||
private final Handler handler;
|
||||
|
||||
public AsyncWorker(final Executor executor, final Handler handler) {
|
||||
this.executor = executor;
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
public CompletionStage<Void> runAsync(final AsyncRunnable<?> runnable) {
|
||||
final CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
runnable.run();
|
||||
handler.post(() -> future.complete(null));
|
||||
} catch (final Throwable t) {
|
||||
handler.post(() -> future.completeExceptionally(t));
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
public <T> CompletionStage<T> supplyAsync(final AsyncSupplier<T, ?> supplier) {
|
||||
final CompletableFuture<T> future = new CompletableFuture<>();
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
final T result = supplier.get();
|
||||
handler.post(() -> future.complete(result));
|
||||
} catch (final Throwable t) {
|
||||
handler.post(() -> future.completeExceptionally(t));
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface AsyncRunnable<E extends Throwable> {
|
||||
void run() throws E;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface AsyncSupplier<T, E extends Throwable> {
|
||||
T get() throws E;
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.wireguard.android.widget.MonkeyedSnackbar;
|
||||
|
||||
/**
|
||||
* Standalone utilities for interacting with the system clipboard.
|
||||
*/
|
||||
|
||||
public final class ClipboardUtils {
|
||||
private ClipboardUtils() {
|
||||
// Prevent instantiation
|
||||
}
|
||||
|
||||
public static void copyTextView(final View view) {
|
||||
if (!(view instanceof TextView))
|
||||
return;
|
||||
final CharSequence text = ((TextView) view).getText();
|
||||
if (text == null || text.length() == 0)
|
||||
return;
|
||||
final Object service = view.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
if (!(service instanceof ClipboardManager))
|
||||
return;
|
||||
final CharSequence description = view.getContentDescription();
|
||||
((ClipboardManager) service).setPrimaryClip(ClipData.newPlainText(description, text));
|
||||
MonkeyedSnackbar.make(view, description + " copied to clipboard", Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import java9.util.concurrent.CompletionException;
|
||||
import java9.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* Helpers for logging exceptions from asynchronous tasks. These can be passed to
|
||||
* {@code CompletionStage.whenComplete()} at the end of an asynchronous future chain.
|
||||
*/
|
||||
|
||||
public enum ExceptionLoggers implements BiConsumer<Object, Throwable> {
|
||||
D(Log.DEBUG),
|
||||
E(Log.ERROR);
|
||||
|
||||
private static final String TAG = "WireGuard/" + ExceptionLoggers.class.getSimpleName();
|
||||
|
||||
private final int priority;
|
||||
|
||||
ExceptionLoggers(final int priority) {
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
public static Throwable unwrap(final Throwable throwable) {
|
||||
if (throwable instanceof CompletionException && throwable.getCause() != null)
|
||||
return throwable.getCause();
|
||||
return throwable;
|
||||
}
|
||||
|
||||
public static String unwrapMessage(Throwable throwable) {
|
||||
throwable = unwrap(throwable);
|
||||
final String message = throwable.getMessage();
|
||||
if (message != null)
|
||||
return message;
|
||||
return throwable.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(final Object result, @Nullable final Throwable throwable) {
|
||||
if (throwable != null)
|
||||
Log.println(Log.ERROR, TAG, Log.getStackTraceString(throwable));
|
||||
else if (priority <= Log.DEBUG)
|
||||
Log.println(priority, TAG, "Future completed successfully");
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Harsh Shandilya <msfjarvis@gmail.com>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v7.preference.Preference;
|
||||
import android.view.ContextThemeWrapper;
|
||||
|
||||
import com.wireguard.android.activity.SettingsActivity;
|
||||
|
||||
public class FragmentUtils {
|
||||
|
||||
public static SettingsActivity getPrefActivity(final Preference preference) {
|
||||
final Context context = preference.getContext();
|
||||
if (context instanceof ContextThemeWrapper) {
|
||||
if (((ContextThemeWrapper) context).getBaseContext() instanceof SettingsActivity) {
|
||||
return ((SettingsActivity) ((ContextThemeWrapper) context).getBaseContext());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.databinding.ObservableArrayList;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.wireguard.util.Keyed;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* ArrayList that allows looking up elements by some key property. As the key property must always
|
||||
* be retrievable, this list cannot hold {@code null} elements. Because this class places no
|
||||
* restrictions on the order or duplication of keys, lookup by key, as well as all list modification
|
||||
* operations, require O(n) time.
|
||||
*/
|
||||
|
||||
public class ObservableKeyedArrayList<K, E extends Keyed<? extends K>>
|
||||
extends ObservableArrayList<E> implements ObservableKeyedList<K, E> {
|
||||
@Override
|
||||
public boolean add(@Nullable final E e) {
|
||||
if (e == null)
|
||||
throw new NullPointerException("Trying to add a null element");
|
||||
return super.add(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(final int index, @Nullable final E e) {
|
||||
if (e == null)
|
||||
throw new NullPointerException("Trying to add a null element");
|
||||
super.add(index, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(final Collection<? extends E> c) {
|
||||
if (c.contains(null))
|
||||
throw new NullPointerException("Trying to add a collection with null element(s)");
|
||||
return super.addAll(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(final int index, final Collection<? extends E> c) {
|
||||
if (c.contains(null))
|
||||
throw new NullPointerException("Trying to add a collection with null element(s)");
|
||||
return super.addAll(index, c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsAllKeys(final Collection<K> keys) {
|
||||
for (final K key : keys)
|
||||
if (!containsKey(key))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(final K key) {
|
||||
return indexOfKey(key) >= 0;
|
||||
}
|
||||
|
||||
@Override @Nullable
|
||||
public E get(final K key) {
|
||||
final int index = indexOfKey(key);
|
||||
return index >= 0 ? get(index) : null;
|
||||
}
|
||||
|
||||
@Override @Nullable
|
||||
public E getLast(final K key) {
|
||||
final int index = lastIndexOfKey(key);
|
||||
return index >= 0 ? get(index) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int indexOfKey(final K key) {
|
||||
final ListIterator<E> iterator = listIterator();
|
||||
while (iterator.hasNext()) {
|
||||
final int index = iterator.nextIndex();
|
||||
if (Objects.equals(iterator.next().getKey(), key))
|
||||
return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int lastIndexOfKey(final K key) {
|
||||
final ListIterator<E> iterator = listIterator(size());
|
||||
while (iterator.hasPrevious()) {
|
||||
final int index = iterator.previousIndex();
|
||||
if (Objects.equals(iterator.previous().getKey(), key))
|
||||
return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public E set(final int index, @Nullable final E e) {
|
||||
if (e == null)
|
||||
throw new NullPointerException("Trying to set a null key");
|
||||
return super.set(index, e);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.databinding.ObservableList;
|
||||
|
||||
import com.wireguard.util.Keyed;
|
||||
import com.wireguard.util.KeyedList;
|
||||
|
||||
/**
|
||||
* A list that is both keyed and observable.
|
||||
*/
|
||||
|
||||
public interface ObservableKeyedList<K, E extends Keyed<? extends K>>
|
||||
extends KeyedList<K, E>, ObservableList<E> {
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.wireguard.util.Keyed;
|
||||
import com.wireguard.util.SortedKeyedList;
|
||||
|
||||
import java.util.AbstractList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Set;
|
||||
import java.util.Spliterator;
|
||||
|
||||
/**
|
||||
* KeyedArrayList that enforces uniqueness and sorted order across the set of keys. This class uses
|
||||
* binary search to improve lookup and replacement times to O(log(n)). However, due to the
|
||||
* array-based nature of this class, insertion and removal of elements with anything but the largest
|
||||
* key still require O(n) time.
|
||||
*/
|
||||
|
||||
public class ObservableSortedKeyedArrayList<K, E extends Keyed<? extends K>>
|
||||
extends ObservableKeyedArrayList<K, E> implements ObservableSortedKeyedList<K, E> {
|
||||
@Nullable
|
||||
private final Comparator<? super K> comparator;
|
||||
private final transient KeyList<K, E> keyList = new KeyList<>(this);
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public ObservableSortedKeyedArrayList() {
|
||||
comparator = null;
|
||||
}
|
||||
|
||||
public ObservableSortedKeyedArrayList(final Comparator<? super K> comparator) {
|
||||
this.comparator = comparator;
|
||||
}
|
||||
|
||||
public ObservableSortedKeyedArrayList(final Collection<? extends E> c) {
|
||||
this();
|
||||
addAll(c);
|
||||
}
|
||||
|
||||
public ObservableSortedKeyedArrayList(final SortedKeyedList<K, E> other) {
|
||||
this(other.comparator());
|
||||
addAll(other);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean add(final E e) {
|
||||
final int insertionPoint = getInsertionPoint(e);
|
||||
if (insertionPoint < 0) {
|
||||
// Skipping insertion is non-destructive if the new and existing objects are the same.
|
||||
if (e == get(-insertionPoint - 1))
|
||||
return false;
|
||||
throw new IllegalArgumentException("Element with same key already exists in list");
|
||||
}
|
||||
super.add(insertionPoint, e);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(final int index, final E e) {
|
||||
final int insertionPoint = getInsertionPoint(e);
|
||||
if (insertionPoint < 0)
|
||||
throw new IllegalArgumentException("Element with same key already exists in list");
|
||||
if (insertionPoint != index)
|
||||
throw new IndexOutOfBoundsException("Wrong index given for element");
|
||||
super.add(index, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(final Collection<? extends E> c) {
|
||||
boolean didChange = false;
|
||||
for (final E e : c)
|
||||
if (add(e))
|
||||
didChange = true;
|
||||
return didChange;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(int index, final Collection<? extends E> c) {
|
||||
for (final E e : c)
|
||||
add(index++, e);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Comparator<? super K> comparator() {
|
||||
return comparator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public K firstKey() {
|
||||
if (isEmpty())
|
||||
// The parameter in the exception is only to shut
|
||||
// lint up, we never care for the exception message.
|
||||
throw new NoSuchElementException("Empty set");
|
||||
return get(0).getKey();
|
||||
}
|
||||
|
||||
private int getInsertionPoint(final E e) {
|
||||
if (comparator != null) {
|
||||
return -Collections.binarySearch(keyList, e.getKey(), comparator) - 1;
|
||||
} else {
|
||||
@SuppressWarnings("unchecked") final List<Comparable<? super K>> list =
|
||||
(List<Comparable<? super K>>) keyList;
|
||||
return -Collections.binarySearch(list, e.getKey()) - 1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int indexOfKey(final K key) {
|
||||
final int index;
|
||||
if (comparator != null) {
|
||||
index = Collections.binarySearch(keyList, key, comparator);
|
||||
} else {
|
||||
@SuppressWarnings("unchecked") final List<Comparable<? super K>> list =
|
||||
(List<Comparable<? super K>>) keyList;
|
||||
index = Collections.binarySearch(list, key);
|
||||
}
|
||||
return index >= 0 ? index : -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<K> keySet() {
|
||||
return keyList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int lastIndexOfKey(final K key) {
|
||||
// There can never be more than one element with the same key in the list.
|
||||
return indexOfKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public K lastKey() {
|
||||
if (isEmpty())
|
||||
// The parameter in the exception is only to shut
|
||||
// lint up, we never care for the exception message.
|
||||
throw new NoSuchElementException("Empty set");
|
||||
return get(size() - 1).getKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public E set(final int index, final E e) {
|
||||
final int order;
|
||||
if (comparator != null) {
|
||||
order = comparator.compare(e.getKey(), get(index).getKey());
|
||||
} else {
|
||||
@SuppressWarnings("unchecked") final Comparable<? super K> key =
|
||||
(Comparable<? super K>) e.getKey();
|
||||
order = key.compareTo(get(index).getKey());
|
||||
}
|
||||
if (order != 0) {
|
||||
// Allow replacement if the new key would be inserted adjacent to the replaced element.
|
||||
final int insertionPoint = getInsertionPoint(e);
|
||||
if (insertionPoint < index || insertionPoint > index + 1)
|
||||
throw new IndexOutOfBoundsException("Wrong index given for element");
|
||||
}
|
||||
return super.set(index, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<E> values() {
|
||||
return this;
|
||||
}
|
||||
|
||||
private static final class KeyList<K, E extends Keyed<? extends K>>
|
||||
extends AbstractList<K> implements Set<K> {
|
||||
private final ObservableSortedKeyedArrayList<K, E> list;
|
||||
|
||||
private KeyList(final ObservableSortedKeyedArrayList<K, E> list) {
|
||||
this.list = list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public K get(final int index) {
|
||||
return list.get(index).getKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return list.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("EmptyMethod")
|
||||
public Spliterator<K> spliterator() {
|
||||
return super.spliterator();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import com.wireguard.util.Keyed;
|
||||
import com.wireguard.util.SortedKeyedList;
|
||||
|
||||
/**
|
||||
* A list that is both sorted/keyed and observable.
|
||||
*/
|
||||
|
||||
public interface ObservableSortedKeyedList<K, E extends Keyed<? extends K>>
|
||||
extends ObservableKeyedList<K, E>, SortedKeyedList<K, E> {
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
public final class SharedLibraryLoader {
|
||||
private static final String TAG = "WireGuard/" + SharedLibraryLoader.class.getSimpleName();
|
||||
|
||||
private SharedLibraryLoader() { }
|
||||
|
||||
public static void loadSharedLibrary(final Context context, final String libName) {
|
||||
Throwable noAbiException;
|
||||
try {
|
||||
System.loadLibrary(libName);
|
||||
return;
|
||||
} catch (final UnsatisfiedLinkError e) {
|
||||
Log.d(TAG, "Failed to load library normally, so attempting to extract from apk", e);
|
||||
noAbiException = e;
|
||||
}
|
||||
|
||||
final ZipFile zipFile;
|
||||
try {
|
||||
zipFile = new ZipFile(new File(context.getApplicationInfo().sourceDir), ZipFile.OPEN_READ);
|
||||
} catch (final IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
final String mappedLibName = System.mapLibraryName(libName);
|
||||
final byte[] buffer = new byte[1024 * 32];
|
||||
for (final String abi : Build.SUPPORTED_ABIS) {
|
||||
final String libZipPath = "lib" + File.separatorChar + abi + File.separatorChar + mappedLibName;
|
||||
final ZipEntry zipEntry = zipFile.getEntry(libZipPath);
|
||||
if (zipEntry == null)
|
||||
continue;
|
||||
File f = null;
|
||||
try {
|
||||
f = File.createTempFile("lib", ".so", context.getCacheDir());
|
||||
Log.d(TAG, "Extracting apk:/" + libZipPath + " to " + f.getAbsolutePath() + " and loading");
|
||||
try (final FileOutputStream out = new FileOutputStream(f);
|
||||
final InputStream in = zipFile.getInputStream(zipEntry)) {
|
||||
int len;
|
||||
while ((len = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, len);
|
||||
}
|
||||
}
|
||||
System.load(f.getAbsolutePath());
|
||||
return;
|
||||
} catch (final Exception e) {
|
||||
Log.d(TAG, "Failed to load library apk:/" + libZipPath, e);
|
||||
noAbiException = e;
|
||||
} finally {
|
||||
if (f != null)
|
||||
// noinspection ResultOfMethodCallIgnored
|
||||
f.delete();
|
||||
}
|
||||
}
|
||||
if (noAbiException instanceof RuntimeException)
|
||||
throw (RuntimeException) noAbiException;
|
||||
throw new RuntimeException(noAbiException);
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.system.OsConstants;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.BuildConfig;
|
||||
import com.wireguard.android.util.RootShell.NoRootException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Helper to install WireGuard tools to the system partition.
|
||||
*/
|
||||
|
||||
public final class ToolsInstaller {
|
||||
public static final int ERROR = 0x0;
|
||||
public static final int YES = 0x1;
|
||||
public static final int NO = 0x2;
|
||||
public static final int MAGISK = 0x4;
|
||||
public static final int SYSTEM = 0x8;
|
||||
|
||||
private static final String[][] EXECUTABLES = {
|
||||
{"libwg.so", "wg"},
|
||||
{"libwg-quick.so", "wg-quick"},
|
||||
};
|
||||
private static final File[] INSTALL_DIRS = {
|
||||
new File("/system/xbin"),
|
||||
new File("/system/bin"),
|
||||
};
|
||||
@Nullable private static final File INSTALL_DIR = getInstallDir();
|
||||
private static final String TAG = "WireGuard/" + ToolsInstaller.class.getSimpleName();
|
||||
|
||||
private final File localBinaryDir;
|
||||
private final Object lock = new Object();
|
||||
private final File nativeLibraryDir;
|
||||
@Nullable private Boolean areToolsAvailable;
|
||||
@Nullable private Boolean installAsMagiskModule;
|
||||
|
||||
public ToolsInstaller(final Context context) {
|
||||
localBinaryDir = new File(context.getCacheDir(), "bin");
|
||||
nativeLibraryDir = new File(context.getApplicationInfo().nativeLibraryDir);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static File getInstallDir() {
|
||||
final String path = System.getenv("PATH");
|
||||
if (path == null)
|
||||
return INSTALL_DIRS[0];
|
||||
final List<String> paths = Arrays.asList(path.split(":"));
|
||||
for (final File dir : INSTALL_DIRS) {
|
||||
if (paths.contains(dir.getPath()) && dir.isDirectory())
|
||||
return dir;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int areInstalled() throws NoRootException {
|
||||
if (INSTALL_DIR == null)
|
||||
return ERROR;
|
||||
final StringBuilder script = new StringBuilder();
|
||||
for (final String[] names : EXECUTABLES) {
|
||||
script.append(String.format("cmp -s '%s' '%s' && ",
|
||||
new File(nativeLibraryDir, names[0]),
|
||||
new File(INSTALL_DIR, names[1])));
|
||||
}
|
||||
script.append("exit ").append(OsConstants.EALREADY).append(';');
|
||||
try {
|
||||
final int ret = Application.getRootShell().run(null, script.toString());
|
||||
if (ret == OsConstants.EALREADY)
|
||||
return willInstallAsMagiskModule() ? YES | MAGISK : YES | SYSTEM;
|
||||
else
|
||||
return willInstallAsMagiskModule() ? NO | MAGISK : NO | SYSTEM;
|
||||
} catch (final IOException ignored) {
|
||||
return ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
public void ensureToolsAvailable() throws FileNotFoundException, NoRootException {
|
||||
synchronized (lock) {
|
||||
if (areToolsAvailable == null) {
|
||||
final int ret = symlink();
|
||||
if (ret == OsConstants.EALREADY) {
|
||||
Log.d(TAG, "Tools were already symlinked into our private binary dir");
|
||||
areToolsAvailable = true;
|
||||
} else if (ret == OsConstants.EXIT_SUCCESS) {
|
||||
Log.d(TAG, "Tools are now symlinked into our private binary dir");
|
||||
areToolsAvailable = true;
|
||||
} else {
|
||||
Log.e(TAG, "For some reason, wg and wg-quick are not available at all");
|
||||
areToolsAvailable = false;
|
||||
}
|
||||
}
|
||||
if (!areToolsAvailable)
|
||||
throw new FileNotFoundException("Required tools unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean willInstallAsMagiskModule() {
|
||||
synchronized (lock) {
|
||||
if (installAsMagiskModule == null) {
|
||||
try {
|
||||
installAsMagiskModule = Application.getRootShell().run(null, "[ -d /sbin/.core/mirror -a -d /sbin/.core/img -a ! -f /cache/.disable_magisk ]") == OsConstants.EXIT_SUCCESS;
|
||||
} catch (final Exception ignored) {
|
||||
installAsMagiskModule = false;
|
||||
}
|
||||
}
|
||||
return installAsMagiskModule;
|
||||
}
|
||||
}
|
||||
|
||||
private int installSystem() throws NoRootException {
|
||||
if (INSTALL_DIR == null)
|
||||
return OsConstants.ENOENT;
|
||||
final StringBuilder script = new StringBuilder("set -ex; ");
|
||||
script.append("trap 'mount -o ro,remount /system' EXIT; mount -o rw,remount /system; ");
|
||||
for (final String[] names : EXECUTABLES) {
|
||||
final File destination = new File(INSTALL_DIR, names[1]);
|
||||
script.append(String.format("cp '%s' '%s'; chmod 755 '%s'; restorecon '%s' || true; ",
|
||||
new File(nativeLibraryDir, names[0]), destination, destination, destination));
|
||||
}
|
||||
try {
|
||||
return Application.getRootShell().run(null, script.toString()) == 0 ? YES | SYSTEM : ERROR;
|
||||
} catch (final IOException ignored) {
|
||||
return ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
private int installMagisk() throws NoRootException {
|
||||
final StringBuilder script = new StringBuilder("set -ex; ");
|
||||
|
||||
script.append("trap 'rm -rf /sbin/.core/img/wireguard' INT TERM EXIT; ");
|
||||
script.append(String.format("rm -rf /sbin/.core/img/wireguard/; mkdir -p /sbin/.core/img/wireguard%s; ", INSTALL_DIR));
|
||||
script.append(String.format("printf 'name=WireGuard Command Line Tools\nversion=%s\nversionCode=%s\nauthor=zx2c4\ndescription=Command line tools for WireGuard\nminMagisk=1500\n' > /sbin/.core/img/wireguard/module.prop; ", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
|
||||
script.append("touch /sbin/.core/img/wireguard/auto_mount; ");
|
||||
for (final String[] names : EXECUTABLES) {
|
||||
final File destination = new File("/sbin/.core/img/wireguard" + INSTALL_DIR, names[1]);
|
||||
script.append(String.format("cp '%s' '%s'; chmod 755 '%s'; restorecon '%s' || true; ",
|
||||
new File(nativeLibraryDir, names[0]), destination, destination, destination));
|
||||
}
|
||||
script.append("trap - INT TERM EXIT;");
|
||||
|
||||
try {
|
||||
return Application.getRootShell().run(null, script.toString()) == 0 ? YES | MAGISK : ERROR;
|
||||
} catch (final IOException ignored) {
|
||||
return ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
public int install() throws NoRootException {
|
||||
return willInstallAsMagiskModule() ? installMagisk() : installSystem();
|
||||
}
|
||||
|
||||
public int symlink() throws NoRootException {
|
||||
final StringBuilder script = new StringBuilder("set -x; ");
|
||||
for (final String[] names : EXECUTABLES) {
|
||||
script.append(String.format("test '%s' -ef '%s' && ",
|
||||
new File(nativeLibraryDir, names[0]),
|
||||
new File(localBinaryDir, names[1])));
|
||||
}
|
||||
script.append("exit ").append(OsConstants.EALREADY).append("; set -e; ");
|
||||
|
||||
for (final String[] names : EXECUTABLES) {
|
||||
script.append(String.format("ln -fns '%s' '%s'; ",
|
||||
new File(nativeLibraryDir, names[0]),
|
||||
new File(localBinaryDir, names[1])));
|
||||
}
|
||||
script.append("exit ").append(OsConstants.EXIT_SUCCESS).append(';');
|
||||
|
||||
try {
|
||||
return Application.getRootShell().run(null, script.toString());
|
||||
} catch (final IOException ignored) {
|
||||
return OsConstants.EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.InputFilter;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
|
||||
import com.wireguard.crypto.KeyEncoding;
|
||||
|
||||
/**
|
||||
* InputFilter for entering WireGuard private/public keys encoded with base64.
|
||||
*/
|
||||
|
||||
public class KeyInputFilter implements InputFilter {
|
||||
private static boolean isAllowed(final char c) {
|
||||
return Character.isLetterOrDigit(c) || c == '+' || c == '/';
|
||||
}
|
||||
|
||||
public static InputFilter newInstance() {
|
||||
return new KeyInputFilter();
|
||||
}
|
||||
|
||||
@Override @Nullable
|
||||
public CharSequence filter(final CharSequence source,
|
||||
final int sStart, final int sEnd,
|
||||
final Spanned dest,
|
||||
final int dStart, final int dEnd) {
|
||||
SpannableStringBuilder replacement = null;
|
||||
int rIndex = 0;
|
||||
final int dLength = dest.length();
|
||||
for (int sIndex = sStart; sIndex < sEnd; ++sIndex) {
|
||||
final char c = source.charAt(sIndex);
|
||||
final int dIndex = dStart + (sIndex - sStart);
|
||||
// Restrict characters to the base64 character set.
|
||||
// Ensure adding this character does not push the length over the limit.
|
||||
if (((dIndex + 1 < KeyEncoding.KEY_LENGTH_BASE64 && isAllowed(c)) ||
|
||||
(dIndex + 1 == KeyEncoding.KEY_LENGTH_BASE64 && c == '=')) &&
|
||||
dLength + (sIndex - sStart) < KeyEncoding.KEY_LENGTH_BASE64) {
|
||||
++rIndex;
|
||||
} else {
|
||||
if (replacement == null)
|
||||
replacement = new SpannableStringBuilder(source, sStart, sEnd);
|
||||
replacement.delete(rIndex, rIndex + 1);
|
||||
}
|
||||
}
|
||||
return replacement;
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package com.wireguard.android.widget;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.design.widget.BaseTransientBottomBar;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.accessibility.AccessibilityManager;
|
||||
|
||||
import com.wireguard.android.BootShutdownReceiver;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
public class MonkeyedSnackbar {
|
||||
private static final String TAG = "WireGuard/" + BootShutdownReceiver.class.getSimpleName();
|
||||
|
||||
public static Snackbar make(@NonNull final View view, @NonNull final CharSequence text,
|
||||
@BaseTransientBottomBar.Duration final int duration) {
|
||||
final Snackbar snackbar = Snackbar.make(view, text, duration);
|
||||
|
||||
try {
|
||||
final Field accessibilityManager = Snackbar.class.getSuperclass().getDeclaredField("mAccessibilityManager");
|
||||
accessibilityManager.setAccessible(true);
|
||||
final Field isEnabled = AccessibilityManager.class.getDeclaredField("mIsEnabled");
|
||||
isEnabled.setAccessible(true);
|
||||
isEnabled.setBoolean(accessibilityManager.get(snackbar), false);
|
||||
} catch (final Exception e) {
|
||||
Log.e(TAG, "Unable to force-enable snackbar animations", e);
|
||||
}
|
||||
|
||||
return snackbar;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.InputFilter;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
|
||||
/**
|
||||
* InputFilter for entering WireGuard configuration names (Linux interface names).
|
||||
*/
|
||||
|
||||
public class NameInputFilter implements InputFilter {
|
||||
private static boolean isAllowed(final char c) {
|
||||
return Character.isLetterOrDigit(c) || "_=+.-".indexOf(c) >= 0;
|
||||
}
|
||||
|
||||
public static InputFilter newInstance() {
|
||||
return new NameInputFilter();
|
||||
}
|
||||
|
||||
@Override @Nullable
|
||||
public CharSequence filter(final CharSequence source,
|
||||
final int sStart, final int sEnd,
|
||||
final Spanned dest,
|
||||
final int dStart, final int dEnd) {
|
||||
SpannableStringBuilder replacement = null;
|
||||
int rIndex = 0;
|
||||
final int dLength = dest.length();
|
||||
for (int sIndex = sStart; sIndex < sEnd; ++sIndex) {
|
||||
final char c = source.charAt(sIndex);
|
||||
final int dIndex = dStart + (sIndex - sStart);
|
||||
// Restrict characters to those valid in interfaces.
|
||||
// Ensure adding this character does not push the length over the limit.
|
||||
if ((dIndex < Tunnel.NAME_MAX_LENGTH && isAllowed(c)) &&
|
||||
dLength + (sIndex - sStart) < Tunnel.NAME_MAX_LENGTH) {
|
||||
++rIndex;
|
||||
} else {
|
||||
if (replacement == null)
|
||||
replacement = new SpannableStringBuilder(source, sStart, sEnd);
|
||||
replacement.delete(rIndex, rIndex + 1);
|
||||
}
|
||||
}
|
||||
return replacement;
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 The Android Open Source Project
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Path.Direction;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.PorterDuff.Mode;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Region;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.IntRange;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.FloatProperty;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
public class SlashDrawable extends Drawable {
|
||||
|
||||
private static final float CORNER_RADIUS = Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? 0f : 1f;
|
||||
private static final long QS_ANIM_LENGTH = 350;
|
||||
|
||||
private final Path mPath = new Path();
|
||||
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
|
||||
// These values are derived in un-rotated (vertical) orientation
|
||||
private static final float SLASH_WIDTH = 1.8384776f;
|
||||
private static final float SLASH_HEIGHT = 28f;
|
||||
private static final float CENTER_X = 10.65f;
|
||||
private static final float CENTER_Y = 11.869239f;
|
||||
private static final float SCALE = 24f;
|
||||
|
||||
// Bottom is derived during animation
|
||||
private static final float LEFT = (CENTER_X - (SLASH_WIDTH / 2)) / SCALE;
|
||||
private static final float TOP = (CENTER_Y - (SLASH_HEIGHT / 2)) / SCALE;
|
||||
private static final float RIGHT = (CENTER_X + (SLASH_WIDTH / 2)) / SCALE;
|
||||
// Draw the slash washington-monument style; rotate to no-u-turn style
|
||||
private static final float DEFAULT_ROTATION = -45f;
|
||||
|
||||
private final Drawable mDrawable;
|
||||
private final RectF mSlashRect = new RectF(0, 0, 0, 0);
|
||||
private float mRotation;
|
||||
private boolean mSlashed;
|
||||
private boolean mAnimationEnabled = true;
|
||||
|
||||
public SlashDrawable(final Drawable d) {
|
||||
mDrawable = d;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicHeight() {
|
||||
return mDrawable.getIntrinsicHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth() {
|
||||
return mDrawable.getIntrinsicWidth();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBoundsChange(final Rect bounds) {
|
||||
super.onBoundsChange(bounds);
|
||||
mDrawable.setBounds(bounds);
|
||||
}
|
||||
|
||||
public void setRotation(final float rotation) {
|
||||
if (mRotation == rotation)
|
||||
return;
|
||||
mRotation = rotation;
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
public void setAnimationEnabled(final boolean enabled) {
|
||||
mAnimationEnabled = enabled;
|
||||
}
|
||||
|
||||
// Animate this value on change
|
||||
private float mCurrentSlashLength;
|
||||
private static final FloatProperty mSlashLengthProp = new FloatProperty<SlashDrawable>("slashLength") {
|
||||
@Override
|
||||
public void setValue(final SlashDrawable object, final float value) {
|
||||
object.mCurrentSlashLength = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Float get(final SlashDrawable object) {
|
||||
return object.mCurrentSlashLength;
|
||||
}
|
||||
};
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setSlashed(final boolean slashed) {
|
||||
if (mSlashed == slashed) return;
|
||||
|
||||
mSlashed = slashed;
|
||||
|
||||
final float end = mSlashed ? SLASH_HEIGHT / SCALE : 0f;
|
||||
final float start = mSlashed ? 0f : SLASH_HEIGHT / SCALE;
|
||||
|
||||
if (mAnimationEnabled) {
|
||||
final ObjectAnimator anim = ObjectAnimator.ofFloat(this, mSlashLengthProp, start, end);
|
||||
anim.addUpdateListener((ValueAnimator valueAnimator) -> invalidateSelf());
|
||||
anim.setDuration(QS_ANIM_LENGTH);
|
||||
anim.start();
|
||||
} else {
|
||||
mCurrentSlashLength = end;
|
||||
invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void draw(final Canvas canvas) {
|
||||
canvas.save();
|
||||
final Matrix m = new Matrix();
|
||||
final int width = getBounds().width();
|
||||
final int height = getBounds().height();
|
||||
final float radiusX = scale(CORNER_RADIUS, width);
|
||||
final float radiusY = scale(CORNER_RADIUS, height);
|
||||
updateRect(
|
||||
scale(LEFT, width),
|
||||
scale(TOP, height),
|
||||
scale(RIGHT, width),
|
||||
scale(TOP + mCurrentSlashLength, height)
|
||||
);
|
||||
|
||||
mPath.reset();
|
||||
// Draw the slash vertically
|
||||
mPath.addRoundRect(mSlashRect, radiusX, radiusY, Direction.CW);
|
||||
// Rotate -45 + desired rotation
|
||||
m.setRotate(mRotation + DEFAULT_ROTATION, width / 2, height / 2);
|
||||
mPath.transform(m);
|
||||
canvas.drawPath(mPath, mPaint);
|
||||
|
||||
// Rotate back to vertical
|
||||
m.setRotate(-mRotation - DEFAULT_ROTATION, width / 2, height / 2);
|
||||
mPath.transform(m);
|
||||
|
||||
// Draw another rect right next to the first, for clipping
|
||||
m.setTranslate(mSlashRect.width(), 0);
|
||||
mPath.transform(m);
|
||||
mPath.addRoundRect(mSlashRect, 1.0f * width, 1.0f * height, Direction.CW);
|
||||
m.setRotate(mRotation + DEFAULT_ROTATION, width / 2, height / 2);
|
||||
mPath.transform(m);
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
|
||||
canvas.clipPath(mPath, Region.Op.DIFFERENCE);
|
||||
else
|
||||
canvas.clipOutPath(mPath);
|
||||
|
||||
mDrawable.draw(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
private float scale(final float frac, final int width) {
|
||||
return frac * width;
|
||||
}
|
||||
|
||||
private void updateRect(final float left, final float top, final float right, final float bottom) {
|
||||
mSlashRect.left = left;
|
||||
mSlashRect.top = top;
|
||||
mSlashRect.right = right;
|
||||
mSlashRect.bottom = bottom;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTint(@ColorInt final int tintColor) {
|
||||
super.setTint(tintColor);
|
||||
mDrawable.setTint(tintColor);
|
||||
mPaint.setColor(tintColor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTintList(@Nullable final ColorStateList tint) {
|
||||
super.setTintList(tint);
|
||||
setDrawableTintList(tint);
|
||||
mPaint.setColor(tint == null ? 0 : tint.getDefaultColor());
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
private void setDrawableTintList(@Nullable final ColorStateList tint) {
|
||||
mDrawable.setTintList(tint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTintMode(final Mode tintMode) {
|
||||
super.setTintMode(tintMode);
|
||||
mDrawable.setTintMode(tintMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(@IntRange(from = 0, to = 255) final int alpha) {
|
||||
mDrawable.setAlpha(alpha);
|
||||
mPaint.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable final ColorFilter colorFilter) {
|
||||
mDrawable.setColorFilter(colorFilter);
|
||||
mPaint.setColorFilter(colorFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.OPAQUE;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 The Android Open Source Project
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.Switch;
|
||||
|
||||
public class ToggleSwitch extends Switch {
|
||||
private boolean isRestoringState;
|
||||
@Nullable private OnBeforeCheckedChangeListener listener;
|
||||
|
||||
public ToggleSwitch(final Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"SameParameterValue", "WeakerAccess"})
|
||||
public ToggleSwitch(final Context context, @Nullable final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(final Parcelable state) {
|
||||
isRestoringState = true;
|
||||
super.onRestoreInstanceState(state);
|
||||
isRestoringState = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChecked(final boolean checked) {
|
||||
if (checked == isChecked())
|
||||
return;
|
||||
if (isRestoringState || listener == null) {
|
||||
super.setChecked(checked);
|
||||
return;
|
||||
}
|
||||
setEnabled(false);
|
||||
listener.onBeforeCheckedChanged(this, checked);
|
||||
}
|
||||
|
||||
public void setCheckedInternal(final boolean checked) {
|
||||
super.setChecked(checked);
|
||||
setEnabled(true);
|
||||
}
|
||||
|
||||
public void setOnBeforeCheckedChangeListener(final OnBeforeCheckedChangeListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public interface OnBeforeCheckedChangeListener {
|
||||
void onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Harsh Shandilya <msfjarvis@gmail.com>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget.fab;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.TimeInterpolator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.support.design.widget.CoordinatorLayout;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.view.animation.FastOutSlowInInterpolator;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
public class FloatingActionButtonBehavior extends CoordinatorLayout.Behavior<FloatingActionsMenu> {
|
||||
|
||||
public FloatingActionButtonBehavior(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean layoutDependsOn(final CoordinatorLayout parent, final FloatingActionsMenu child,
|
||||
final View dependency) {
|
||||
return dependency instanceof Snackbar.SnackbarLayout;
|
||||
}
|
||||
|
||||
private static final long ANIMATION_DURATION = 250;
|
||||
private static final TimeInterpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator();
|
||||
|
||||
private static void animateChange(final FloatingActionsMenu child, final float destination, final float fullSpan) {
|
||||
final float origin = child.getBehaviorYTranslation();
|
||||
if (Math.abs(destination - origin) < fullSpan / 2) {
|
||||
child.setBehaviorYTranslation(destination);
|
||||
return;
|
||||
}
|
||||
final ValueAnimator animator = new ValueAnimator();
|
||||
animator.setFloatValues(origin, destination);
|
||||
animator.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
|
||||
animator.setDuration((long)(ANIMATION_DURATION * (Math.abs(destination - origin) / fullSpan)));
|
||||
animator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(final Animator a) {
|
||||
child.setBehaviorYTranslation(destination);
|
||||
}
|
||||
});
|
||||
animator.addUpdateListener(a -> child.setBehaviorYTranslation((float)a.getAnimatedValue()));
|
||||
animator.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDependentViewChanged(final CoordinatorLayout parent, final FloatingActionsMenu child,
|
||||
final View dependency) {
|
||||
animateChange(child, Math.min(0, dependency.getTranslationY() - dependency.getMeasuredHeight()), dependency.getMeasuredHeight());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDependentViewRemoved(final CoordinatorLayout parent, final FloatingActionsMenu child,
|
||||
final View dependency) {
|
||||
animateChange(child, 0, dependency.getMeasuredHeight());
|
||||
}
|
||||
}
|
||||
@@ -1,628 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2014 Jerzy Chalupski
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget.fab;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.TimeInterpolator;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Keep;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.v4.content.res.ResourcesCompat;
|
||||
import android.support.v7.widget.AppCompatTextView;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.TouchDelegate;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.OvershootInterpolator;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.wireguard.android.R;
|
||||
|
||||
public class FloatingActionsMenu extends ViewGroup {
|
||||
public static final int EXPAND_UP = 0;
|
||||
public static final int EXPAND_DOWN = 1;
|
||||
public static final int EXPAND_LEFT = 2;
|
||||
public static final int EXPAND_RIGHT = 3;
|
||||
|
||||
public static final int LABELS_ON_LEFT_SIDE = 0;
|
||||
public static final int LABELS_ON_RIGHT_SIDE = 1;
|
||||
|
||||
private static final int ANIMATION_DURATION = 300;
|
||||
private static final float COLLAPSED_PLUS_ROTATION = 0f;
|
||||
private static final float EXPANDED_PLUS_ROTATION = 90f + 45f;
|
||||
private static final TimeInterpolator EXPAND_INTERPOLATOR = new OvershootInterpolator();
|
||||
private static final TimeInterpolator COLLAPSE_INTERPOLATOR = new DecelerateInterpolator(3f);
|
||||
private static final TimeInterpolator ALPHA_EXPAND_INTERPOLATOR = new DecelerateInterpolator();
|
||||
private int mExpandDirection;
|
||||
private int mButtonSpacing;
|
||||
private int mLabelsMargin;
|
||||
private int mLabelsVerticalOffset;
|
||||
private boolean mExpanded;
|
||||
private final AnimatorSet mExpandAnimation = new AnimatorSet().setDuration(ANIMATION_DURATION);
|
||||
private final AnimatorSet mCollapseAnimation = new AnimatorSet().setDuration(ANIMATION_DURATION);
|
||||
@Nullable private FloatingActionButton mAddButton;
|
||||
@Nullable private RotatingDrawable mRotatingDrawable;
|
||||
private int mMaxButtonWidth;
|
||||
private int mMaxButtonHeight;
|
||||
private int mLabelsStyle;
|
||||
private int mLabelsPosition;
|
||||
private int mButtonsCount;
|
||||
@Nullable private TouchDelegateGroup mTouchDelegateGroup;
|
||||
@Nullable private OnFloatingActionsMenuUpdateListener mListener;
|
||||
private final Rect touchArea = new Rect(0, 0, 0, 0);
|
||||
private float scrollYTranslation;
|
||||
private float behaviorYTranslation;
|
||||
|
||||
public FloatingActionsMenu(final Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public FloatingActionsMenu(final Context context, @Nullable final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
public FloatingActionsMenu(final Context context, @Nullable final AttributeSet attrs, final int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
private void init(final Context context, @Nullable final AttributeSet attributeSet) {
|
||||
mButtonSpacing = (int) (getResources().getDimension(R.dimen.fab_actions_spacing));
|
||||
mLabelsMargin = getResources().getDimensionPixelSize(R.dimen.fab_labels_margin);
|
||||
mLabelsVerticalOffset = getResources().getDimensionPixelSize(R.dimen.fab_shadow_offset);
|
||||
|
||||
mTouchDelegateGroup = new TouchDelegateGroup(this);
|
||||
setTouchDelegate(mTouchDelegateGroup);
|
||||
|
||||
final TypedArray attr = context.obtainStyledAttributes(attributeSet, R.styleable.FloatingActionsMenu, 0, 0);
|
||||
mExpandDirection = attr.getInt(R.styleable.FloatingActionsMenu_fab_expandDirection, EXPAND_UP);
|
||||
mLabelsStyle = attr.getResourceId(R.styleable.FloatingActionsMenu_fab_labelStyle, 0);
|
||||
mLabelsPosition = attr.getInt(R.styleable.FloatingActionsMenu_fab_labelsPosition, LABELS_ON_LEFT_SIDE);
|
||||
attr.recycle();
|
||||
|
||||
if (mLabelsStyle != 0 && expandsHorizontally()) {
|
||||
throw new IllegalStateException("Action labels in horizontal expand orientation is not supported.");
|
||||
}
|
||||
|
||||
createAddButton(context);
|
||||
}
|
||||
|
||||
public float getScrollYTranslation() {
|
||||
return scrollYTranslation;
|
||||
}
|
||||
|
||||
public void setScrollYTranslation(final float scrollYTranslation) {
|
||||
this.scrollYTranslation = scrollYTranslation;
|
||||
setTranslationY(behaviorYTranslation + scrollYTranslation);
|
||||
}
|
||||
|
||||
public float getBehaviorYTranslation() {
|
||||
return behaviorYTranslation;
|
||||
}
|
||||
|
||||
public void setBehaviorYTranslation(final float behaviorYTranslation) {
|
||||
this.behaviorYTranslation = behaviorYTranslation;
|
||||
setTranslationY(behaviorYTranslation + scrollYTranslation);
|
||||
}
|
||||
|
||||
public void setOnFloatingActionsMenuUpdateListener(final OnFloatingActionsMenuUpdateListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
private boolean expandsHorizontally() {
|
||||
return mExpandDirection == EXPAND_LEFT || mExpandDirection == EXPAND_RIGHT;
|
||||
}
|
||||
|
||||
private void createAddButton(final Context context) {
|
||||
final RotatingDrawable rotatingDrawable = new RotatingDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_action_add_white, context.getTheme()));
|
||||
mRotatingDrawable = rotatingDrawable;
|
||||
|
||||
final TimeInterpolator interpolator = new OvershootInterpolator();
|
||||
|
||||
final ObjectAnimator collapseAnimator = ObjectAnimator.ofFloat(rotatingDrawable, "rotation", EXPANDED_PLUS_ROTATION, COLLAPSED_PLUS_ROTATION);
|
||||
final ObjectAnimator expandAnimator = ObjectAnimator.ofFloat(rotatingDrawable, "rotation", COLLAPSED_PLUS_ROTATION, EXPANDED_PLUS_ROTATION);
|
||||
|
||||
collapseAnimator.setInterpolator(interpolator);
|
||||
expandAnimator.setInterpolator(interpolator);
|
||||
|
||||
mExpandAnimation.play(expandAnimator);
|
||||
mCollapseAnimation.play(collapseAnimator);
|
||||
|
||||
mAddButton = new FloatingActionButton(context);
|
||||
mAddButton.setImageDrawable(rotatingDrawable);
|
||||
mAddButton.setId(R.id.fab_expand_menu_button);
|
||||
mAddButton.setOnClickListener(v -> toggle());
|
||||
|
||||
addView(mAddButton, super.generateDefaultLayoutParams());
|
||||
mButtonsCount++;
|
||||
}
|
||||
|
||||
public void addButton(final LabeledFloatingActionButton button) {
|
||||
addView(button, mButtonsCount - 1);
|
||||
mButtonsCount++;
|
||||
|
||||
if (mLabelsStyle != 0) {
|
||||
createLabels();
|
||||
}
|
||||
}
|
||||
|
||||
public void removeButton(final LabeledFloatingActionButton button) {
|
||||
removeView(button.getLabelView());
|
||||
removeView(button);
|
||||
button.setTag(R.id.fab_label, null);
|
||||
mButtonsCount--;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
|
||||
measureChildren(widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
mMaxButtonWidth = 0;
|
||||
mMaxButtonHeight = 0;
|
||||
int maxLabelWidth = 0;
|
||||
|
||||
for (int i = 0; i < mButtonsCount; i++) {
|
||||
final View child = getChildAt(i);
|
||||
|
||||
if (child.getVisibility() == GONE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (mExpandDirection) {
|
||||
case EXPAND_UP:
|
||||
case EXPAND_DOWN:
|
||||
mMaxButtonWidth = Math.max(mMaxButtonWidth, child.getMeasuredWidth());
|
||||
height += child.getMeasuredHeight();
|
||||
break;
|
||||
case EXPAND_LEFT:
|
||||
case EXPAND_RIGHT:
|
||||
width += child.getMeasuredWidth();
|
||||
mMaxButtonHeight = Math.max(mMaxButtonHeight, child.getMeasuredHeight());
|
||||
break;
|
||||
}
|
||||
|
||||
if (!expandsHorizontally()) {
|
||||
final TextView label = (TextView) child.getTag(R.id.fab_label);
|
||||
if (label != null) {
|
||||
maxLabelWidth = Math.max(maxLabelWidth, label.getMeasuredWidth());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (expandsHorizontally()) {
|
||||
height = mMaxButtonHeight;
|
||||
} else {
|
||||
width = mMaxButtonWidth + (maxLabelWidth > 0 ? maxLabelWidth + mLabelsMargin : 0);
|
||||
}
|
||||
|
||||
switch (mExpandDirection) {
|
||||
case EXPAND_UP:
|
||||
case EXPAND_DOWN:
|
||||
height += mButtonSpacing * (mButtonsCount - 1);
|
||||
height = adjustForOvershoot(height);
|
||||
break;
|
||||
case EXPAND_LEFT:
|
||||
case EXPAND_RIGHT:
|
||||
width += mButtonSpacing * (mButtonsCount - 1);
|
||||
width = adjustForOvershoot(width);
|
||||
break;
|
||||
}
|
||||
|
||||
setMeasuredDimension(width, height);
|
||||
}
|
||||
|
||||
private static int adjustForOvershoot(final int dimension) {
|
||||
return dimension * 12 / 10;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(final boolean changed, final int l, final int t, final int r, final int b) {
|
||||
switch (mExpandDirection) {
|
||||
case EXPAND_UP:
|
||||
case EXPAND_DOWN:
|
||||
final boolean expandUp = mExpandDirection == EXPAND_UP;
|
||||
|
||||
if (changed) {
|
||||
mTouchDelegateGroup.clearTouchDelegates();
|
||||
}
|
||||
|
||||
final int addButtonY = expandUp ? b - t - mAddButton.getMeasuredHeight() : 0;
|
||||
// Ensure mAddButton is centered on the line where the buttons should be
|
||||
final int buttonsHorizontalCenter = (mLabelsPosition == LABELS_ON_LEFT_SIDE
|
||||
? r - l - mMaxButtonWidth / 2
|
||||
: mMaxButtonWidth / 2);
|
||||
final int addButtonLeft = buttonsHorizontalCenter - mAddButton.getMeasuredWidth() / 2;
|
||||
mAddButton.layout(addButtonLeft, addButtonY, addButtonLeft + mAddButton.getMeasuredWidth(), addButtonY + mAddButton.getMeasuredHeight());
|
||||
|
||||
final int labelsOffset = mMaxButtonWidth / 2 + mLabelsMargin;
|
||||
final int labelsXNearButton = mLabelsPosition == LABELS_ON_LEFT_SIDE
|
||||
? buttonsHorizontalCenter - labelsOffset
|
||||
: buttonsHorizontalCenter + labelsOffset;
|
||||
|
||||
int nextY = expandUp ?
|
||||
addButtonY - mButtonSpacing :
|
||||
addButtonY + mAddButton.getMeasuredHeight() + mButtonSpacing;
|
||||
|
||||
for (int i = mButtonsCount - 1; i >= 0; i--) {
|
||||
final View child = getChildAt(i);
|
||||
|
||||
if (child == mAddButton || child.getVisibility() == GONE) continue;
|
||||
|
||||
final int childX = buttonsHorizontalCenter - child.getMeasuredWidth() / 2;
|
||||
final int childY = expandUp ? nextY - child.getMeasuredHeight() : nextY;
|
||||
child.layout(childX, childY, childX + child.getMeasuredWidth(), childY + child.getMeasuredHeight());
|
||||
|
||||
final float collapsedTranslation = addButtonY - childY;
|
||||
final float expandedTranslation = 0f;
|
||||
|
||||
child.setTranslationY(mExpanded ? expandedTranslation : collapsedTranslation);
|
||||
child.setAlpha(mExpanded ? 1f : 0f);
|
||||
|
||||
final LayoutParams params = (LayoutParams) child.getLayoutParams();
|
||||
params.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation);
|
||||
params.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation);
|
||||
params.setAnimationsTarget(child);
|
||||
|
||||
final View label = (View) child.getTag(R.id.fab_label);
|
||||
if (label != null) {
|
||||
final int labelXAwayFromButton = mLabelsPosition == LABELS_ON_LEFT_SIDE
|
||||
? labelsXNearButton - label.getMeasuredWidth()
|
||||
: labelsXNearButton + label.getMeasuredWidth();
|
||||
|
||||
final int labelLeft = mLabelsPosition == LABELS_ON_LEFT_SIDE
|
||||
? labelXAwayFromButton
|
||||
: labelsXNearButton;
|
||||
|
||||
final int labelRight = mLabelsPosition == LABELS_ON_LEFT_SIDE
|
||||
? labelsXNearButton
|
||||
: labelXAwayFromButton;
|
||||
|
||||
final int labelTop = childY - mLabelsVerticalOffset + (child.getMeasuredHeight() - label.getMeasuredHeight()) / 2;
|
||||
|
||||
label.layout(labelLeft, labelTop, labelRight, labelTop + label.getMeasuredHeight());
|
||||
|
||||
touchArea.set(Math.min(childX, labelLeft),
|
||||
childY - mButtonSpacing / 2,
|
||||
Math.max(childX + child.getMeasuredWidth(), labelRight),
|
||||
childY + child.getMeasuredHeight() + mButtonSpacing / 2);
|
||||
mTouchDelegateGroup.addTouchDelegate(new TouchDelegate(new Rect(touchArea), child));
|
||||
|
||||
label.setTranslationY(mExpanded ? expandedTranslation : collapsedTranslation);
|
||||
label.setAlpha(mExpanded ? 1f : 0f);
|
||||
|
||||
final LayoutParams labelParams = (LayoutParams) label.getLayoutParams();
|
||||
labelParams.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation);
|
||||
labelParams.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation);
|
||||
labelParams.setAnimationsTarget(label);
|
||||
}
|
||||
|
||||
nextY = expandUp ?
|
||||
childY - mButtonSpacing :
|
||||
childY + child.getMeasuredHeight() + mButtonSpacing;
|
||||
}
|
||||
break;
|
||||
|
||||
case EXPAND_LEFT:
|
||||
case EXPAND_RIGHT:
|
||||
final boolean expandLeft = mExpandDirection == EXPAND_LEFT;
|
||||
|
||||
final int addButtonX = expandLeft ? r - l - mAddButton.getMeasuredWidth() : 0;
|
||||
// Ensure mAddButton is centered on the line where the buttons should be
|
||||
final int addButtonTop = b - t - mMaxButtonHeight + (mMaxButtonHeight - mAddButton.getMeasuredHeight()) / 2;
|
||||
mAddButton.layout(addButtonX, addButtonTop, addButtonX + mAddButton.getMeasuredWidth(), addButtonTop + mAddButton.getMeasuredHeight());
|
||||
|
||||
int nextX = expandLeft ?
|
||||
addButtonX - mButtonSpacing :
|
||||
addButtonX + mAddButton.getMeasuredWidth() + mButtonSpacing;
|
||||
|
||||
for (int i = mButtonsCount - 1; i >= 0; i--) {
|
||||
final View child = getChildAt(i);
|
||||
|
||||
if (child == mAddButton || child.getVisibility() == GONE) continue;
|
||||
|
||||
final int childX = expandLeft ? nextX - child.getMeasuredWidth() : nextX;
|
||||
final int childY = addButtonTop + (mAddButton.getMeasuredHeight() - child.getMeasuredHeight()) / 2;
|
||||
child.layout(childX, childY, childX + child.getMeasuredWidth(), childY + child.getMeasuredHeight());
|
||||
|
||||
final float collapsedTranslation = addButtonX - childX;
|
||||
final float expandedTranslation = 0f;
|
||||
|
||||
child.setTranslationX(mExpanded ? expandedTranslation : collapsedTranslation);
|
||||
child.setAlpha(mExpanded ? 1f : 0f);
|
||||
|
||||
final LayoutParams params = (LayoutParams) child.getLayoutParams();
|
||||
params.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation);
|
||||
params.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation);
|
||||
params.setAnimationsTarget(child);
|
||||
|
||||
nextX = expandLeft ?
|
||||
childX - mButtonSpacing :
|
||||
childX + child.getMeasuredWidth() + mButtonSpacing;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
|
||||
return new LayoutParams(super.generateDefaultLayoutParams());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewGroup.LayoutParams generateLayoutParams(final AttributeSet attrs) {
|
||||
return new LayoutParams(super.generateLayoutParams(attrs));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ViewGroup.LayoutParams generateLayoutParams(final ViewGroup.LayoutParams p) {
|
||||
return new LayoutParams(super.generateLayoutParams(p));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
bringChildToFront(mAddButton);
|
||||
mButtonsCount = getChildCount();
|
||||
|
||||
if (mLabelsStyle != 0) {
|
||||
createLabels();
|
||||
}
|
||||
}
|
||||
|
||||
private void createLabels() {
|
||||
final Context context = new ContextThemeWrapper(getContext(), mLabelsStyle);
|
||||
|
||||
for (int i = 0; i < mButtonsCount; i++) {
|
||||
final FloatingActionButton button = (FloatingActionButton) getChildAt(i);
|
||||
|
||||
if (button instanceof LabeledFloatingActionButton) {
|
||||
final String title = ((LabeledFloatingActionButton) button).getTitle();
|
||||
|
||||
final AppCompatTextView label = new AppCompatTextView(context);
|
||||
label.setTextAppearance(context, mLabelsStyle);
|
||||
label.setText(title);
|
||||
addView(label);
|
||||
|
||||
button.setTag(R.id.fab_label, label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void collapse() {
|
||||
collapse(false);
|
||||
}
|
||||
|
||||
public void collapseImmediately() {
|
||||
collapse(true);
|
||||
}
|
||||
|
||||
private void collapse(final boolean immediately) {
|
||||
if (mExpanded) {
|
||||
mExpanded = false;
|
||||
mTouchDelegateGroup.setEnabled(false);
|
||||
mCollapseAnimation.setDuration(immediately ? 0 : ANIMATION_DURATION);
|
||||
mCollapseAnimation.start();
|
||||
mExpandAnimation.cancel();
|
||||
|
||||
if (mListener != null) {
|
||||
mListener.onMenuCollapsed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void toggle() {
|
||||
if (mExpanded) {
|
||||
collapse();
|
||||
} else {
|
||||
expand();
|
||||
}
|
||||
}
|
||||
|
||||
public void expand() {
|
||||
if (!mExpanded) {
|
||||
mExpanded = true;
|
||||
mTouchDelegateGroup.setEnabled(true);
|
||||
mCollapseAnimation.cancel();
|
||||
mExpandAnimation.start();
|
||||
|
||||
if (mListener != null) {
|
||||
mListener.onMenuExpanded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isExpanded() {
|
||||
return mExpanded;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(final boolean enabled) {
|
||||
super.setEnabled(enabled);
|
||||
|
||||
mAddButton.setEnabled(enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Parcelable onSaveInstanceState() {
|
||||
final Parcelable superState = super.onSaveInstanceState();
|
||||
final SavedState savedState = new SavedState(superState);
|
||||
savedState.mExpanded = mExpanded;
|
||||
|
||||
return savedState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(final Parcelable state) {
|
||||
if (state instanceof SavedState) {
|
||||
final SavedState savedState = (SavedState) state;
|
||||
mExpanded = savedState.mExpanded;
|
||||
mTouchDelegateGroup.setEnabled(mExpanded);
|
||||
|
||||
if (mRotatingDrawable != null) {
|
||||
mRotatingDrawable.setRotation(mExpanded ? EXPANDED_PLUS_ROTATION : COLLAPSED_PLUS_ROTATION);
|
||||
}
|
||||
|
||||
super.onRestoreInstanceState(savedState.getSuperState());
|
||||
} else {
|
||||
super.onRestoreInstanceState(state);
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnFloatingActionsMenuUpdateListener {
|
||||
void onMenuExpanded();
|
||||
|
||||
void onMenuCollapsed();
|
||||
}
|
||||
|
||||
private static class RotatingDrawable extends LayerDrawable {
|
||||
private float mRotation;
|
||||
|
||||
RotatingDrawable(final Drawable drawable) {
|
||||
super(new Drawable[]{drawable});
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
public float getRotation() {
|
||||
return mRotation;
|
||||
}
|
||||
|
||||
@Keep
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
public void setRotation(final float rotation) {
|
||||
mRotation = rotation;
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(final Canvas canvas) {
|
||||
canvas.save();
|
||||
canvas.rotate(mRotation, getBounds().centerX(), getBounds().centerY());
|
||||
super.draw(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
public static class SavedState extends BaseSavedState {
|
||||
public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
|
||||
|
||||
@Override
|
||||
public SavedState createFromParcel(final Parcel in) {
|
||||
return new SavedState(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SavedState[] newArray(final int size) {
|
||||
return new SavedState[size];
|
||||
}
|
||||
};
|
||||
public boolean mExpanded;
|
||||
|
||||
public SavedState(final Parcelable parcel) {
|
||||
super(parcel);
|
||||
}
|
||||
|
||||
private SavedState(final Parcel in) {
|
||||
super(in);
|
||||
mExpanded = in.readInt() == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel out, final int flags) {
|
||||
super.writeToParcel(out, flags);
|
||||
out.writeInt(mExpanded ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
private class LayoutParams extends ViewGroup.LayoutParams {
|
||||
|
||||
private final ObjectAnimator mExpandDir = new ObjectAnimator();
|
||||
private final ObjectAnimator mExpandAlpha = new ObjectAnimator();
|
||||
private final ObjectAnimator mCollapseDir = new ObjectAnimator();
|
||||
private final ObjectAnimator mCollapseAlpha = new ObjectAnimator();
|
||||
private boolean animationsSetToPlay;
|
||||
|
||||
LayoutParams(final ViewGroup.LayoutParams source) {
|
||||
super(source);
|
||||
|
||||
mExpandDir.setInterpolator(EXPAND_INTERPOLATOR);
|
||||
mExpandAlpha.setInterpolator(ALPHA_EXPAND_INTERPOLATOR);
|
||||
mCollapseDir.setInterpolator(COLLAPSE_INTERPOLATOR);
|
||||
mCollapseAlpha.setInterpolator(COLLAPSE_INTERPOLATOR);
|
||||
|
||||
mCollapseAlpha.setProperty(View.ALPHA);
|
||||
mCollapseAlpha.setFloatValues(1f, 0f);
|
||||
|
||||
mExpandAlpha.setProperty(View.ALPHA);
|
||||
mExpandAlpha.setFloatValues(0f, 1f);
|
||||
|
||||
switch (mExpandDirection) {
|
||||
case EXPAND_UP:
|
||||
case EXPAND_DOWN:
|
||||
mCollapseDir.setProperty(View.TRANSLATION_Y);
|
||||
mExpandDir.setProperty(View.TRANSLATION_Y);
|
||||
break;
|
||||
case EXPAND_LEFT:
|
||||
case EXPAND_RIGHT:
|
||||
mCollapseDir.setProperty(View.TRANSLATION_X);
|
||||
mExpandDir.setProperty(View.TRANSLATION_X);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void setAnimationsTarget(final View view) {
|
||||
mCollapseAlpha.setTarget(view);
|
||||
mCollapseDir.setTarget(view);
|
||||
mExpandAlpha.setTarget(view);
|
||||
mExpandDir.setTarget(view);
|
||||
|
||||
// Now that the animations have targets, set them to be played
|
||||
if (!animationsSetToPlay) {
|
||||
addLayerTypeListener(mExpandDir, view);
|
||||
addLayerTypeListener(mCollapseDir, view);
|
||||
|
||||
mCollapseAnimation.play(mCollapseAlpha);
|
||||
mCollapseAnimation.play(mCollapseDir);
|
||||
mExpandAnimation.play(mExpandAlpha);
|
||||
mExpandAnimation.play(mExpandDir);
|
||||
animationsSetToPlay = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void addLayerTypeListener(final Animator animator, final View view) {
|
||||
animator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(final Animator animation) {
|
||||
view.setLayerType(LAYER_TYPE_NONE, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationStart(final Animator animation) {
|
||||
view.setLayerType(LAYER_TYPE_HARDWARE, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget.fab;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
|
||||
public class FloatingActionsMenuRecyclerViewScrollListener extends RecyclerView.OnScrollListener {
|
||||
private static final float SCALE_FACTOR = 1.5f;
|
||||
private final FloatingActionsMenu menu;
|
||||
|
||||
public FloatingActionsMenuRecyclerViewScrollListener(final FloatingActionsMenu menu) {
|
||||
this.menu = menu;
|
||||
}
|
||||
|
||||
private static float bound(final float min, final float proposal, final float max) {
|
||||
return Math.min(max, Math.max(min, proposal));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
menu.setScrollYTranslation(bound(0, menu.getScrollYTranslation() + dy * SCALE_FACTOR, menu.getMeasuredHeight() - menu.getTranslationY()));
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2014 Jerzy Chalupski
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget.fab;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.wireguard.android.R;
|
||||
|
||||
public class LabeledFloatingActionButton extends FloatingActionButton {
|
||||
|
||||
@Nullable private final String title;
|
||||
|
||||
public LabeledFloatingActionButton(final Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public LabeledFloatingActionButton(final Context context, @Nullable final AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public LabeledFloatingActionButton(final Context context, @Nullable final AttributeSet attrs, final int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
|
||||
final TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.LabeledFloatingActionButton, 0, 0);
|
||||
title = attr.getString(R.styleable.LabeledFloatingActionButton_fab_title);
|
||||
attr.recycle();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
TextView getLabelView() {
|
||||
return (TextView) getTag(R.id.fab_label);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisibility(final int visibility) {
|
||||
final TextView label = getLabelView();
|
||||
if (label != null) {
|
||||
label.setVisibility(visibility);
|
||||
}
|
||||
|
||||
super.setVisibility(visibility);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2014 Jerzy Chalupski
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget.fab;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.TouchDelegate;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
public class TouchDelegateGroup extends TouchDelegate {
|
||||
private static final Rect USELESS_HACKY_RECT = new Rect();
|
||||
private final Collection<TouchDelegate> mTouchDelegates = new ArrayList<>();
|
||||
@Nullable private TouchDelegate mCurrentTouchDelegate;
|
||||
private boolean mEnabled;
|
||||
|
||||
public TouchDelegateGroup(final View uselessHackyView) {
|
||||
super(USELESS_HACKY_RECT, uselessHackyView);
|
||||
}
|
||||
|
||||
public void addTouchDelegate(final TouchDelegate touchDelegate) {
|
||||
mTouchDelegates.add(touchDelegate);
|
||||
}
|
||||
|
||||
public void removeTouchDelegate(final TouchDelegate touchDelegate) {
|
||||
mTouchDelegates.remove(touchDelegate);
|
||||
if (mCurrentTouchDelegate == touchDelegate) {
|
||||
mCurrentTouchDelegate = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void clearTouchDelegates() {
|
||||
mTouchDelegates.clear();
|
||||
mCurrentTouchDelegate = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(final MotionEvent event) {
|
||||
if (!mEnabled)
|
||||
return false;
|
||||
|
||||
TouchDelegate delegate = null;
|
||||
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
for (final TouchDelegate touchDelegate : mTouchDelegates) {
|
||||
if (touchDelegate.onTouchEvent(event)) {
|
||||
mCurrentTouchDelegate = touchDelegate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
delegate = mCurrentTouchDelegate;
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
case MotionEvent.ACTION_UP:
|
||||
delegate = mCurrentTouchDelegate;
|
||||
mCurrentTouchDelegate = null;
|
||||
break;
|
||||
}
|
||||
|
||||
return delegate != null && delegate.onTouchEvent(event);
|
||||
}
|
||||
|
||||
public void setEnabled(final boolean enabled) {
|
||||
mEnabled = enabled;
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* The set of valid attributes for an interface or peer in a WireGuard configuration file.
|
||||
*/
|
||||
|
||||
public enum Attribute {
|
||||
ADDRESS("Address"),
|
||||
ALLOWED_IPS("AllowedIPs"),
|
||||
DNS("DNS"),
|
||||
EXCLUDED_APPLICATIONS("ExcludedApplications"),
|
||||
ENDPOINT("Endpoint"),
|
||||
LISTEN_PORT("ListenPort"),
|
||||
MTU("MTU"),
|
||||
PERSISTENT_KEEPALIVE("PersistentKeepalive"),
|
||||
PRESHARED_KEY("PresharedKey"),
|
||||
PRIVATE_KEY("PrivateKey"),
|
||||
PUBLIC_KEY("PublicKey");
|
||||
|
||||
private static final String[] EMPTY_LIST = new String[0];
|
||||
private static final Map<String, Attribute> KEY_MAP;
|
||||
private static final Pattern LIST_SEPARATOR_PATTERN = Pattern.compile("\\s*,\\s*");
|
||||
private static final Pattern SEPARATOR_PATTERN = Pattern.compile("\\s|=");
|
||||
|
||||
static {
|
||||
KEY_MAP = new HashMap<>(Attribute.values().length);
|
||||
for (final Attribute key : Attribute.values()) {
|
||||
KEY_MAP.put(key.token.toLowerCase(), key);
|
||||
}
|
||||
}
|
||||
|
||||
private final Pattern pattern;
|
||||
private final String token;
|
||||
|
||||
Attribute(final String token) {
|
||||
pattern = Pattern.compile(token + "\\s*=\\s*(\\S.*)");
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public static <T> String iterableToString(final Iterable<T> iterable) {
|
||||
return TextUtils.join(", ", iterable);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Attribute match(final CharSequence line) {
|
||||
return KEY_MAP.get(SEPARATOR_PATTERN.split(line)[0].toLowerCase());
|
||||
}
|
||||
|
||||
public static String[] stringToList(@Nullable final String string) {
|
||||
if (TextUtils.isEmpty(string))
|
||||
return EMPTY_LIST;
|
||||
return LIST_SEPARATOR_PATTERN.split(string.trim());
|
||||
}
|
||||
|
||||
public String composeWith(@Nullable final Object value) {
|
||||
return String.format("%s = %s%n", token, value);
|
||||
}
|
||||
|
||||
public String composeWith(final int value) {
|
||||
return String.format("%s = %d%n", token, value);
|
||||
}
|
||||
|
||||
public <T> String composeWith(final Iterable<T> value) {
|
||||
return String.format("%s = %s%n", token, iterableToString(value));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String parse(final CharSequence line) {
|
||||
final Matcher matcher = pattern.matcher(line);
|
||||
return matcher.matches() ? matcher.group(1) : null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String[] parseList(final CharSequence line) {
|
||||
final Matcher matcher = pattern.matcher(line);
|
||||
return matcher.matches() ? stringToList(matcher.group(1)) : null;
|
||||
}
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import android.databinding.BaseObservable;
|
||||
import android.databinding.Bindable;
|
||||
import android.databinding.ObservableArrayList;
|
||||
import android.databinding.ObservableList;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.android.databinding.library.baseAdapters.BR;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.StringReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a wg-quick configuration file, its name, and its connection state.
|
||||
*/
|
||||
|
||||
public class Config {
|
||||
private final Interface interfaceSection = new Interface();
|
||||
private List<Peer> peers = new ArrayList<>();
|
||||
|
||||
public static Config from(final String string) throws IOException {
|
||||
return from(new BufferedReader(new StringReader(string)));
|
||||
}
|
||||
|
||||
public static Config from(final InputStream stream) throws IOException {
|
||||
return from(new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
public static Config from(final BufferedReader reader) throws IOException {
|
||||
final Config config = new Config();
|
||||
Peer currentPeer = null;
|
||||
String line;
|
||||
boolean inInterfaceSection = false;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
final int commentIndex = line.indexOf('#');
|
||||
if (commentIndex != -1)
|
||||
line = line.substring(0, commentIndex);
|
||||
line = line.trim();
|
||||
if (line.isEmpty())
|
||||
continue;
|
||||
if ("[Interface]".toLowerCase().equals(line.toLowerCase())) {
|
||||
currentPeer = null;
|
||||
inInterfaceSection = true;
|
||||
} else if ("[Peer]".toLowerCase().equals(line.toLowerCase())) {
|
||||
currentPeer = new Peer();
|
||||
config.peers.add(currentPeer);
|
||||
inInterfaceSection = false;
|
||||
} else if (inInterfaceSection) {
|
||||
config.interfaceSection.parse(line);
|
||||
} else if (currentPeer != null) {
|
||||
currentPeer.parse(line);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid configuration line: " + line);
|
||||
}
|
||||
}
|
||||
if (!inInterfaceSection && currentPeer == null) {
|
||||
throw new IllegalArgumentException("Could not find any config information");
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
public Interface getInterface() {
|
||||
return interfaceSection;
|
||||
}
|
||||
|
||||
public List<Peer> getPeers() {
|
||||
return peers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder().append(interfaceSection);
|
||||
for (final Peer peer : peers)
|
||||
sb.append('\n').append(peer);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static class Observable extends BaseObservable implements Parcelable {
|
||||
public static final Creator<Observable> CREATOR = new Creator<Observable>() {
|
||||
@Override
|
||||
public Observable createFromParcel(final Parcel in) {
|
||||
return new Observable(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observable[] newArray(final int size) {
|
||||
return new Observable[size];
|
||||
}
|
||||
};
|
||||
@Nullable private String name;
|
||||
private final Interface.Observable observableInterface;
|
||||
private final ObservableList<Peer.Observable> observablePeers;
|
||||
|
||||
public Observable(@Nullable final Config parent, @Nullable final String name) {
|
||||
this.name = name;
|
||||
|
||||
observableInterface = new Interface.Observable(parent == null ? null : parent.interfaceSection);
|
||||
observablePeers = new ObservableArrayList<>();
|
||||
if (parent != null) {
|
||||
for (final Peer peer : parent.getPeers())
|
||||
observablePeers.add(new Peer.Observable(peer));
|
||||
}
|
||||
}
|
||||
|
||||
private Observable(final Parcel in) {
|
||||
name = in.readString();
|
||||
observableInterface = in.readParcelable(Interface.Observable.class.getClassLoader());
|
||||
observablePeers = new ObservableArrayList<>();
|
||||
in.readTypedList(observablePeers, Peer.Observable.CREATOR);
|
||||
}
|
||||
|
||||
public void commitData(final Config parent) {
|
||||
observableInterface.commitData(parent.interfaceSection);
|
||||
final List<Peer> newPeers = new ArrayList<>(observablePeers.size());
|
||||
for (final Peer.Observable observablePeer : observablePeers) {
|
||||
final Peer peer = new Peer();
|
||||
observablePeer.commitData(peer);
|
||||
newPeers.add(peer);
|
||||
}
|
||||
parent.peers = newPeers;
|
||||
notifyChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public Interface.Observable getInterfaceSection() {
|
||||
return observableInterface;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public String getName() {
|
||||
return name == null ? "" : name;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public ObservableList<Peer.Observable> getPeers() {
|
||||
return observablePeers;
|
||||
}
|
||||
|
||||
public void setName(final String name) {
|
||||
this.name = name;
|
||||
notifyPropertyChanged(BR.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeString(name);
|
||||
dest.writeParcelable(observableInterface, flags);
|
||||
dest.writeTypedList(observablePeers);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.InetAddress;
|
||||
|
||||
public final class InetAddresses {
|
||||
private static final Method PARSER_METHOD;
|
||||
|
||||
static {
|
||||
try {
|
||||
// This method is only present on Android.
|
||||
PARSER_METHOD = InetAddress.class.getMethod("parseNumericAddress", String.class);
|
||||
} catch (final NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private InetAddresses() {
|
||||
// Prevent instantiation.
|
||||
}
|
||||
|
||||
public static InetAddress parse(@Nullable final String address) {
|
||||
if (address == null || address.isEmpty())
|
||||
throw new IllegalArgumentException("Empty address");
|
||||
try {
|
||||
return (InetAddress) PARSER_METHOD.invoke(null, address);
|
||||
} catch (final IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getCause() == null ? e : e.getCause());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
|
||||
public class InetNetwork {
|
||||
private final InetAddress address;
|
||||
private final int mask;
|
||||
|
||||
public InetNetwork(final String input) {
|
||||
final int slash = input.lastIndexOf('/');
|
||||
final int rawMask;
|
||||
final String rawAddress;
|
||||
if (slash >= 0) {
|
||||
rawMask = Integer.parseInt(input.substring(slash + 1), 10);
|
||||
rawAddress = input.substring(0, slash);
|
||||
} else {
|
||||
rawMask = -1;
|
||||
rawAddress = input;
|
||||
}
|
||||
address = InetAddresses.parse(rawAddress);
|
||||
final int maxMask = (address instanceof Inet4Address) ? 32 : 128;
|
||||
mask = rawMask >= 0 && rawMask <= maxMask ? rawMask : maxMask;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
if (!(obj instanceof InetNetwork))
|
||||
return false;
|
||||
final InetNetwork other = (InetNetwork) obj;
|
||||
return Objects.equals(address, other.address) && mask == other.mask;
|
||||
}
|
||||
|
||||
public InetAddress getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public int getMask() {
|
||||
return mask;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return address.hashCode() ^ mask;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return address.getHostAddress() + '/' + mask;
|
||||
}
|
||||
}
|
||||
@@ -1,396 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import android.databinding.BaseObservable;
|
||||
import android.databinding.Bindable;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.wireguard.android.BR;
|
||||
import com.wireguard.crypto.Keypair;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents the configuration for a WireGuard interface (an [Interface] block).
|
||||
*/
|
||||
|
||||
public class Interface {
|
||||
private final List<InetNetwork> addressList;
|
||||
private final List<InetAddress> dnsList;
|
||||
private final List<String> excludedApplications;
|
||||
@Nullable private Keypair keypair;
|
||||
private int listenPort;
|
||||
private int mtu;
|
||||
|
||||
public Interface() {
|
||||
addressList = new ArrayList<>();
|
||||
dnsList = new ArrayList<>();
|
||||
excludedApplications = new ArrayList<>();
|
||||
}
|
||||
|
||||
private void addAddresses(@Nullable final String[] addresses) {
|
||||
if (addresses != null && addresses.length > 0) {
|
||||
for (final String addr : addresses) {
|
||||
if (addr.isEmpty())
|
||||
throw new IllegalArgumentException("Address is empty");
|
||||
addressList.add(new InetNetwork(addr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addDnses(@Nullable final String[] dnses) {
|
||||
if (dnses != null && dnses.length > 0) {
|
||||
for (final String dns : dnses) {
|
||||
dnsList.add(InetAddresses.parse(dns));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addExcludedApplications(@Nullable final String[] applications) {
|
||||
if (applications != null && applications.length > 0) {
|
||||
excludedApplications.addAll(Arrays.asList(applications));
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getAddressString() {
|
||||
if (addressList.isEmpty())
|
||||
return null;
|
||||
return Attribute.iterableToString(addressList);
|
||||
}
|
||||
|
||||
public InetNetwork[] getAddresses() {
|
||||
return addressList.toArray(new InetNetwork[addressList.size()]);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getDnsString() {
|
||||
if (dnsList.isEmpty())
|
||||
return null;
|
||||
return Attribute.iterableToString(getDnsStrings());
|
||||
}
|
||||
|
||||
private List<String> getDnsStrings() {
|
||||
final List<String> strings = new ArrayList<>();
|
||||
for (final InetAddress addr : dnsList)
|
||||
strings.add(addr.getHostAddress());
|
||||
return strings;
|
||||
}
|
||||
|
||||
public InetAddress[] getDnses() {
|
||||
return dnsList.toArray(new InetAddress[dnsList.size()]);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getExcludedApplicationsString() {
|
||||
if (excludedApplications.isEmpty())
|
||||
return null;
|
||||
return Attribute.iterableToString(excludedApplications);
|
||||
}
|
||||
|
||||
public String[] getExcludedApplications() {
|
||||
return excludedApplications.toArray(new String[excludedApplications.size()]);
|
||||
}
|
||||
|
||||
public int getListenPort() {
|
||||
return listenPort;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getListenPortString() {
|
||||
if (listenPort == 0)
|
||||
return null;
|
||||
return Integer.valueOf(listenPort).toString();
|
||||
}
|
||||
|
||||
public int getMtu() {
|
||||
return mtu;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getMtuString() {
|
||||
if (mtu == 0)
|
||||
return null;
|
||||
return Integer.toString(mtu);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPrivateKey() {
|
||||
if (keypair == null)
|
||||
return null;
|
||||
return keypair.getPrivateKey();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPublicKey() {
|
||||
if (keypair == null)
|
||||
return null;
|
||||
return keypair.getPublicKey();
|
||||
}
|
||||
|
||||
public void parse(final String line) {
|
||||
final Attribute key = Attribute.match(line);
|
||||
if (key == null)
|
||||
throw new IllegalArgumentException(String.format("Unable to parse line: \"%s\"", line));
|
||||
switch (key) {
|
||||
case ADDRESS:
|
||||
addAddresses(key.parseList(line));
|
||||
break;
|
||||
case DNS:
|
||||
addDnses(key.parseList(line));
|
||||
break;
|
||||
case EXCLUDED_APPLICATIONS:
|
||||
addExcludedApplications(key.parseList(line));
|
||||
break;
|
||||
case LISTEN_PORT:
|
||||
setListenPortString(key.parse(line));
|
||||
break;
|
||||
case MTU:
|
||||
setMtuString(key.parse(line));
|
||||
break;
|
||||
case PRIVATE_KEY:
|
||||
setPrivateKey(key.parse(line));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(line);
|
||||
}
|
||||
}
|
||||
|
||||
private void setAddressString(@Nullable final String addressString) {
|
||||
addressList.clear();
|
||||
addAddresses(Attribute.stringToList(addressString));
|
||||
}
|
||||
|
||||
private void setDnsString(@Nullable final String dnsString) {
|
||||
dnsList.clear();
|
||||
addDnses(Attribute.stringToList(dnsString));
|
||||
}
|
||||
|
||||
private void setExcludedApplicationsString(@Nullable final String applicationsString) {
|
||||
excludedApplications.clear();
|
||||
addExcludedApplications(Attribute.stringToList(applicationsString));
|
||||
}
|
||||
|
||||
private void setListenPort(final int listenPort) {
|
||||
this.listenPort = listenPort;
|
||||
}
|
||||
|
||||
private void setListenPortString(@Nullable final String port) {
|
||||
if (port != null && !port.isEmpty())
|
||||
setListenPort(Integer.parseInt(port, 10));
|
||||
else
|
||||
setListenPort(0);
|
||||
}
|
||||
|
||||
private void setMtu(final int mtu) {
|
||||
this.mtu = mtu;
|
||||
}
|
||||
|
||||
private void setMtuString(@Nullable final String mtu) {
|
||||
if (mtu != null && !mtu.isEmpty())
|
||||
setMtu(Integer.parseInt(mtu, 10));
|
||||
else
|
||||
setMtu(0);
|
||||
}
|
||||
|
||||
private void setPrivateKey(@Nullable String privateKey) {
|
||||
if (privateKey != null && privateKey.isEmpty())
|
||||
privateKey = null;
|
||||
keypair = privateKey == null ? null : new Keypair(privateKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder().append("[Interface]\n");
|
||||
if (!addressList.isEmpty())
|
||||
sb.append(Attribute.ADDRESS.composeWith(addressList));
|
||||
if (!dnsList.isEmpty())
|
||||
sb.append(Attribute.DNS.composeWith(getDnsStrings()));
|
||||
if (!excludedApplications.isEmpty())
|
||||
sb.append(Attribute.EXCLUDED_APPLICATIONS.composeWith(excludedApplications));
|
||||
if (listenPort != 0)
|
||||
sb.append(Attribute.LISTEN_PORT.composeWith(listenPort));
|
||||
if (mtu != 0)
|
||||
sb.append(Attribute.MTU.composeWith(mtu));
|
||||
if (keypair != null)
|
||||
sb.append(Attribute.PRIVATE_KEY.composeWith(keypair.getPrivateKey()));
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static class Observable extends BaseObservable implements Parcelable {
|
||||
public static final Creator<Observable> CREATOR = new Creator<Observable>() {
|
||||
@Override
|
||||
public Observable createFromParcel(final Parcel in) {
|
||||
return new Observable(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observable[] newArray(final int size) {
|
||||
return new Observable[size];
|
||||
}
|
||||
};
|
||||
@Nullable private String addresses;
|
||||
@Nullable private String dnses;
|
||||
@Nullable private String excludedApplications;
|
||||
@Nullable private String listenPort;
|
||||
@Nullable private String mtu;
|
||||
@Nullable private String privateKey;
|
||||
@Nullable private String publicKey;
|
||||
|
||||
public Observable(@Nullable final Interface parent) {
|
||||
if (parent != null)
|
||||
loadData(parent);
|
||||
}
|
||||
|
||||
private Observable(final Parcel in) {
|
||||
addresses = in.readString();
|
||||
dnses = in.readString();
|
||||
publicKey = in.readString();
|
||||
privateKey = in.readString();
|
||||
listenPort = in.readString();
|
||||
mtu = in.readString();
|
||||
excludedApplications = in.readString();
|
||||
}
|
||||
|
||||
public void commitData(final Interface parent) {
|
||||
parent.setAddressString(addresses);
|
||||
parent.setDnsString(dnses);
|
||||
parent.setExcludedApplicationsString(excludedApplications);
|
||||
parent.setPrivateKey(privateKey);
|
||||
parent.setListenPortString(listenPort);
|
||||
parent.setMtuString(mtu);
|
||||
loadData(parent);
|
||||
notifyChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void generateKeypair() {
|
||||
final Keypair keypair = new Keypair();
|
||||
privateKey = keypair.getPrivateKey();
|
||||
publicKey = keypair.getPublicKey();
|
||||
notifyPropertyChanged(BR.privateKey);
|
||||
notifyPropertyChanged(BR.publicKey);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Bindable
|
||||
public String getAddresses() {
|
||||
return addresses;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Bindable
|
||||
public String getDnses() {
|
||||
return dnses;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Bindable
|
||||
public String getExcludedApplications() {
|
||||
return excludedApplications;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public int getExcludedApplicationsCount() {
|
||||
return Attribute.stringToList(excludedApplications).length;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Bindable
|
||||
public String getListenPort() {
|
||||
return listenPort;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Bindable
|
||||
public String getMtu() {
|
||||
return mtu;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Bindable
|
||||
public String getPrivateKey() {
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Bindable
|
||||
public String getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
private void loadData(final Interface parent) {
|
||||
addresses = parent.getAddressString();
|
||||
dnses = parent.getDnsString();
|
||||
excludedApplications = parent.getExcludedApplicationsString();
|
||||
publicKey = parent.getPublicKey();
|
||||
privateKey = parent.getPrivateKey();
|
||||
listenPort = parent.getListenPortString();
|
||||
mtu = parent.getMtuString();
|
||||
}
|
||||
|
||||
public void setAddresses(final String addresses) {
|
||||
this.addresses = addresses;
|
||||
notifyPropertyChanged(BR.addresses);
|
||||
}
|
||||
|
||||
public void setDnses(final String dnses) {
|
||||
this.dnses = dnses;
|
||||
notifyPropertyChanged(BR.dnses);
|
||||
}
|
||||
|
||||
public void setExcludedApplications(final String excludedApplications) {
|
||||
this.excludedApplications = excludedApplications;
|
||||
notifyPropertyChanged(BR.excludedApplications);
|
||||
notifyPropertyChanged(BR.excludedApplicationsCount);
|
||||
}
|
||||
|
||||
public void setListenPort(final String listenPort) {
|
||||
this.listenPort = listenPort;
|
||||
notifyPropertyChanged(BR.listenPort);
|
||||
}
|
||||
|
||||
public void setMtu(final String mtu) {
|
||||
this.mtu = mtu;
|
||||
notifyPropertyChanged(BR.mtu);
|
||||
}
|
||||
|
||||
public void setPrivateKey(final String privateKey) {
|
||||
this.privateKey = privateKey;
|
||||
|
||||
try {
|
||||
publicKey = new Keypair(privateKey).getPublicKey();
|
||||
} catch (final IllegalArgumentException ignored) {
|
||||
publicKey = "";
|
||||
}
|
||||
|
||||
notifyPropertyChanged(BR.privateKey);
|
||||
notifyPropertyChanged(BR.publicKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeString(addresses);
|
||||
dest.writeString(dnses);
|
||||
dest.writeString(publicKey);
|
||||
dest.writeString(privateKey);
|
||||
dest.writeString(listenPort);
|
||||
dest.writeString(mtu);
|
||||
dest.writeString(excludedApplications);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,384 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import android.databinding.BaseObservable;
|
||||
import android.databinding.Bindable;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.android.databinding.library.baseAdapters.BR;
|
||||
import com.wireguard.crypto.KeyEncoding;
|
||||
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import java9.lang.Iterables;
|
||||
|
||||
/**
|
||||
* Represents the configuration for a WireGuard peer (a [Peer] block).
|
||||
*/
|
||||
|
||||
public class Peer {
|
||||
private final List<InetNetwork> allowedIPsList;
|
||||
@Nullable private InetSocketAddress endpoint;
|
||||
private int persistentKeepalive;
|
||||
@Nullable private String preSharedKey;
|
||||
@Nullable private String publicKey;
|
||||
|
||||
public Peer() {
|
||||
allowedIPsList = new ArrayList<>();
|
||||
}
|
||||
|
||||
private void addAllowedIPs(@Nullable final String[] allowedIPs) {
|
||||
if (allowedIPs != null && allowedIPs.length > 0) {
|
||||
for (final String allowedIP : allowedIPs) {
|
||||
allowedIPsList.add(new InetNetwork(allowedIP));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public InetNetwork[] getAllowedIPs() {
|
||||
return allowedIPsList.toArray(new InetNetwork[allowedIPsList.size()]);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getAllowedIPsString() {
|
||||
if (allowedIPsList.isEmpty())
|
||||
return null;
|
||||
return Attribute.iterableToString(allowedIPsList);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public InetSocketAddress getEndpoint() {
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getEndpointString() {
|
||||
if (endpoint == null)
|
||||
return null;
|
||||
return String.format("%s:%d", endpoint.getHostString(), endpoint.getPort());
|
||||
}
|
||||
|
||||
public int getPersistentKeepalive() {
|
||||
return persistentKeepalive;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getPersistentKeepaliveString() {
|
||||
if (persistentKeepalive == 0)
|
||||
return null;
|
||||
return Integer.valueOf(persistentKeepalive).toString();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPreSharedKey() {
|
||||
return preSharedKey;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public String getResolvedEndpointString() throws UnknownHostException {
|
||||
if (endpoint == null)
|
||||
throw new UnknownHostException("{empty}");
|
||||
if (endpoint.isUnresolved())
|
||||
endpoint = new InetSocketAddress(endpoint.getHostString(), endpoint.getPort());
|
||||
if (endpoint.isUnresolved())
|
||||
throw new UnknownHostException(endpoint.getHostString());
|
||||
if (endpoint.getAddress() instanceof Inet6Address)
|
||||
return String.format("[%s]:%d",
|
||||
endpoint.getAddress().getHostAddress(),
|
||||
endpoint.getPort());
|
||||
return String.format("%s:%d",
|
||||
endpoint.getAddress().getHostAddress(),
|
||||
endpoint.getPort());
|
||||
}
|
||||
|
||||
public void parse(final String line) {
|
||||
final Attribute key = Attribute.match(line);
|
||||
if (key == null)
|
||||
throw new IllegalArgumentException(String.format("Unable to parse line: \"%s\"", line));
|
||||
switch (key) {
|
||||
case ALLOWED_IPS:
|
||||
addAllowedIPs(key.parseList(line));
|
||||
break;
|
||||
case ENDPOINT:
|
||||
setEndpointString(key.parse(line));
|
||||
break;
|
||||
case PERSISTENT_KEEPALIVE:
|
||||
setPersistentKeepaliveString(key.parse(line));
|
||||
break;
|
||||
case PRESHARED_KEY:
|
||||
setPreSharedKey(key.parse(line));
|
||||
break;
|
||||
case PUBLIC_KEY:
|
||||
setPublicKey(key.parse(line));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(line);
|
||||
}
|
||||
}
|
||||
|
||||
private void setAllowedIPsString(@Nullable final String allowedIPsString) {
|
||||
allowedIPsList.clear();
|
||||
addAllowedIPs(Attribute.stringToList(allowedIPsString));
|
||||
}
|
||||
|
||||
private void setEndpoint(@Nullable final InetSocketAddress endpoint) {
|
||||
this.endpoint = endpoint;
|
||||
}
|
||||
|
||||
private void setEndpointString(@Nullable final String endpoint) {
|
||||
if (endpoint != null && !endpoint.isEmpty()) {
|
||||
final InetSocketAddress constructedEndpoint;
|
||||
if (endpoint.indexOf('/') != -1 || endpoint.indexOf('?') != -1 || endpoint.indexOf('#') != -1)
|
||||
throw new IllegalArgumentException("Forbidden characters in endpoint");
|
||||
final URI uri;
|
||||
try {
|
||||
uri = new URI("wg://" + endpoint);
|
||||
} catch (final URISyntaxException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
constructedEndpoint = InetSocketAddress.createUnresolved(uri.getHost(), uri.getPort());
|
||||
setEndpoint(constructedEndpoint);
|
||||
} else
|
||||
setEndpoint(null);
|
||||
}
|
||||
|
||||
private void setPersistentKeepalive(final int persistentKeepalive) {
|
||||
this.persistentKeepalive = persistentKeepalive;
|
||||
}
|
||||
|
||||
private void setPersistentKeepaliveString(@Nullable final String persistentKeepalive) {
|
||||
if (persistentKeepalive != null && !persistentKeepalive.isEmpty())
|
||||
setPersistentKeepalive(Integer.parseInt(persistentKeepalive, 10));
|
||||
else
|
||||
setPersistentKeepalive(0);
|
||||
}
|
||||
|
||||
private void setPreSharedKey(@Nullable String preSharedKey) {
|
||||
if (preSharedKey != null && preSharedKey.isEmpty())
|
||||
preSharedKey = null;
|
||||
if (preSharedKey != null)
|
||||
KeyEncoding.keyFromBase64(preSharedKey);
|
||||
this.preSharedKey = preSharedKey;
|
||||
}
|
||||
|
||||
private void setPublicKey(@Nullable String publicKey) {
|
||||
if (publicKey != null && publicKey.isEmpty())
|
||||
publicKey = null;
|
||||
if (publicKey != null)
|
||||
KeyEncoding.keyFromBase64(publicKey);
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder().append("[Peer]\n");
|
||||
if (!allowedIPsList.isEmpty())
|
||||
sb.append(Attribute.ALLOWED_IPS.composeWith(allowedIPsList));
|
||||
if (endpoint != null)
|
||||
sb.append(Attribute.ENDPOINT.composeWith(getEndpointString()));
|
||||
if (persistentKeepalive != 0)
|
||||
sb.append(Attribute.PERSISTENT_KEEPALIVE.composeWith(persistentKeepalive));
|
||||
if (preSharedKey != null)
|
||||
sb.append(Attribute.PRESHARED_KEY.composeWith(preSharedKey));
|
||||
if (publicKey != null)
|
||||
sb.append(Attribute.PUBLIC_KEY.composeWith(publicKey));
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static class Observable extends BaseObservable implements Parcelable {
|
||||
public static final Creator<Observable> CREATOR = new Creator<Observable>() {
|
||||
@Override
|
||||
public Observable createFromParcel(final Parcel in) {
|
||||
return new Observable(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observable[] newArray(final int size) {
|
||||
return new Observable[size];
|
||||
}
|
||||
};
|
||||
@Nullable private String allowedIPs;
|
||||
@Nullable private String endpoint;
|
||||
@Nullable private String persistentKeepalive;
|
||||
@Nullable private String preSharedKey;
|
||||
@Nullable private String publicKey;
|
||||
private final List<String> interfaceDNSRoutes = new ArrayList<>();
|
||||
private int numSiblings;
|
||||
|
||||
public Observable(final Peer parent) {
|
||||
loadData(parent);
|
||||
}
|
||||
|
||||
private Observable(final Parcel in) {
|
||||
allowedIPs = in.readString();
|
||||
endpoint = in.readString();
|
||||
persistentKeepalive = in.readString();
|
||||
preSharedKey = in.readString();
|
||||
publicKey = in.readString();
|
||||
numSiblings = in.readInt();
|
||||
in.readStringList(interfaceDNSRoutes);
|
||||
}
|
||||
|
||||
public static Observable newInstance() {
|
||||
return new Observable(new Peer());
|
||||
}
|
||||
|
||||
public void commitData(final Peer parent) {
|
||||
parent.setAllowedIPsString(allowedIPs);
|
||||
parent.setEndpointString(endpoint);
|
||||
parent.setPersistentKeepaliveString(persistentKeepalive);
|
||||
parent.setPreSharedKey(preSharedKey);
|
||||
parent.setPublicKey(publicKey);
|
||||
if (parent.getPublicKey() == null)
|
||||
throw new IllegalArgumentException("Peer public key may not be empty");
|
||||
loadData(parent);
|
||||
notifyChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static final String DEFAULT_ROUTE_V4 = "0.0.0.0/0";
|
||||
private static final List<String> DEFAULT_ROUTE_MOD_RFC1918_V4 = Arrays.asList("0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12", "172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7", "176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16", "192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10", "193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4");
|
||||
|
||||
public void toggleExcludePrivateIPs() {
|
||||
final Collection<String> ips = new HashSet<>(Arrays.asList(Attribute.stringToList(allowedIPs)));
|
||||
final boolean hasDefaultRoute = ips.contains(DEFAULT_ROUTE_V4);
|
||||
final boolean hasDefaultRouteModRFC1918 = ips.containsAll(DEFAULT_ROUTE_MOD_RFC1918_V4);
|
||||
if ((!hasDefaultRoute && !hasDefaultRouteModRFC1918) || numSiblings > 0)
|
||||
return;
|
||||
Iterables.removeIf(ips, ip -> !ip.contains(":"));
|
||||
if (hasDefaultRoute) {
|
||||
ips.addAll(DEFAULT_ROUTE_MOD_RFC1918_V4);
|
||||
ips.addAll(interfaceDNSRoutes);
|
||||
} else if (hasDefaultRouteModRFC1918)
|
||||
ips.add(DEFAULT_ROUTE_V4);
|
||||
setAllowedIPs(Attribute.iterableToString(ips));
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public boolean getCanToggleExcludePrivateIPs() {
|
||||
final Collection<String> ips = Arrays.asList(Attribute.stringToList(allowedIPs));
|
||||
return numSiblings == 0 && (ips.contains(DEFAULT_ROUTE_V4) || ips.containsAll(DEFAULT_ROUTE_MOD_RFC1918_V4));
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public boolean getIsExcludePrivateIPsOn() {
|
||||
return numSiblings == 0 && Arrays.asList(Attribute.stringToList(allowedIPs)).containsAll(DEFAULT_ROUTE_MOD_RFC1918_V4);
|
||||
}
|
||||
|
||||
@Bindable @Nullable
|
||||
public String getAllowedIPs() {
|
||||
return allowedIPs;
|
||||
}
|
||||
|
||||
@Bindable @Nullable
|
||||
public String getEndpoint() {
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
@Bindable @Nullable
|
||||
public String getPersistentKeepalive() {
|
||||
return persistentKeepalive;
|
||||
}
|
||||
|
||||
@Bindable @Nullable
|
||||
public String getPreSharedKey() {
|
||||
return preSharedKey;
|
||||
}
|
||||
|
||||
@Bindable @Nullable
|
||||
public String getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
private void loadData(final Peer parent) {
|
||||
allowedIPs = parent.getAllowedIPsString();
|
||||
endpoint = parent.getEndpointString();
|
||||
persistentKeepalive = parent.getPersistentKeepaliveString();
|
||||
preSharedKey = parent.getPreSharedKey();
|
||||
publicKey = parent.getPublicKey();
|
||||
}
|
||||
|
||||
public void setAllowedIPs(final String allowedIPs) {
|
||||
this.allowedIPs = allowedIPs;
|
||||
notifyPropertyChanged(BR.allowedIPs);
|
||||
notifyPropertyChanged(BR.canToggleExcludePrivateIPs);
|
||||
notifyPropertyChanged(BR.isExcludePrivateIPsOn);
|
||||
}
|
||||
|
||||
public void setEndpoint(final String endpoint) {
|
||||
this.endpoint = endpoint;
|
||||
notifyPropertyChanged(BR.endpoint);
|
||||
}
|
||||
|
||||
public void setPersistentKeepalive(final String persistentKeepalive) {
|
||||
this.persistentKeepalive = persistentKeepalive;
|
||||
notifyPropertyChanged(BR.persistentKeepalive);
|
||||
}
|
||||
|
||||
public void setPreSharedKey(final String preSharedKey) {
|
||||
this.preSharedKey = preSharedKey;
|
||||
notifyPropertyChanged(BR.preSharedKey);
|
||||
}
|
||||
|
||||
public void setPublicKey(final String publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
notifyPropertyChanged(BR.publicKey);
|
||||
}
|
||||
|
||||
public void setInterfaceDNSRoutes(@Nullable final String dnsServers) {
|
||||
final Collection<String> ips = new HashSet<>(Arrays.asList(Attribute.stringToList(allowedIPs)));
|
||||
final boolean modifyAllowedIPs = ips.containsAll(DEFAULT_ROUTE_MOD_RFC1918_V4);
|
||||
|
||||
ips.removeAll(interfaceDNSRoutes);
|
||||
interfaceDNSRoutes.clear();
|
||||
for (final String dnsServer : Attribute.stringToList(dnsServers)) {
|
||||
if (!dnsServer.contains(":"))
|
||||
interfaceDNSRoutes.add(dnsServer + "/32");
|
||||
}
|
||||
ips.addAll(interfaceDNSRoutes);
|
||||
if (modifyAllowedIPs)
|
||||
setAllowedIPs(Attribute.iterableToString(ips));
|
||||
}
|
||||
|
||||
public void setNumSiblings(final int num) {
|
||||
numSiblings = num;
|
||||
notifyPropertyChanged(BR.canToggleExcludePrivateIPs);
|
||||
notifyPropertyChanged(BR.isExcludePrivateIPsOn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeString(allowedIPs);
|
||||
dest.writeString(endpoint);
|
||||
dest.writeString(persistentKeepalive);
|
||||
dest.writeString(preSharedKey);
|
||||
dest.writeString(publicKey);
|
||||
dest.writeInt(numSiblings);
|
||||
dest.writeStringList(interfaceDNSRoutes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2015-2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.crypto;
|
||||
|
||||
/**
|
||||
* This is a specialized constant-time base64 and hex implementation that resists side-channel attacks.
|
||||
*/
|
||||
|
||||
@SuppressWarnings("MagicNumber")
|
||||
public final class KeyEncoding {
|
||||
public static final int KEY_LENGTH = 32;
|
||||
public static final int KEY_LENGTH_BASE64 = 44;
|
||||
public static final int KEY_LENGTH_HEX = 64;
|
||||
private static final String KEY_LENGTH_BASE64_EXCEPTION_MESSAGE =
|
||||
"WireGuard base64 keys must be 44 characters encoding 32 bytes";
|
||||
private static final String KEY_LENGTH_EXCEPTION_MESSAGE =
|
||||
"WireGuard keys must be 32 bytes";
|
||||
private static final String KEY_LENGTH_HEX_EXCEPTION_MESSAGE =
|
||||
"WireGuard hex keys must be 64 characters encoding 32 bytes";
|
||||
|
||||
private KeyEncoding() {
|
||||
// Prevent instantiation.
|
||||
}
|
||||
|
||||
private static int decodeBase64(final char[] src, final int srcOffset) {
|
||||
int val = 0;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
final char c = src[i + srcOffset];
|
||||
val |= (-1
|
||||
+ ((((('A' - 1) - c) & (c - ('Z' + 1))) >>> 8) & (c - 64))
|
||||
+ ((((('a' - 1) - c) & (c - ('z' + 1))) >>> 8) & (c - 70))
|
||||
+ ((((('0' - 1) - c) & (c - ('9' + 1))) >>> 8) & (c + 5))
|
||||
+ ((((('+' - 1) - c) & (c - ('+' + 1))) >>> 8) & 63)
|
||||
+ ((((('/' - 1) - c) & (c - ('/' + 1))) >>> 8) & 64)
|
||||
) << (18 - 6 * i);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
private static void encodeBase64(final byte[] src, final int srcOffset,
|
||||
final char[] dest, final int destOffset) {
|
||||
final byte[] input = {
|
||||
(byte) ((src[srcOffset] >>> 2) & 63),
|
||||
(byte) ((src[srcOffset] << 4 | ((src[1 + srcOffset] & 0xff) >>> 4)) & 63),
|
||||
(byte) ((src[1 + srcOffset] << 2 | ((src[2 + srcOffset] & 0xff) >>> 6)) & 63),
|
||||
(byte) ((src[2 + srcOffset]) & 63),
|
||||
};
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
dest[i + destOffset] = (char) (input[i] + 'A'
|
||||
+ (((25 - input[i]) >>> 8) & 6)
|
||||
- (((51 - input[i]) >>> 8) & 75)
|
||||
- (((61 - input[i]) >>> 8) & 15)
|
||||
+ (((62 - input[i]) >>> 8) & 3));
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] keyFromBase64(final String str) {
|
||||
final char[] input = str.toCharArray();
|
||||
final byte[] key = new byte[KEY_LENGTH];
|
||||
if (input.length != KEY_LENGTH_BASE64 || input[KEY_LENGTH_BASE64 - 1] != '=')
|
||||
throw new IllegalArgumentException(KEY_LENGTH_BASE64_EXCEPTION_MESSAGE);
|
||||
int i;
|
||||
int ret = 0;
|
||||
for (i = 0; i < KEY_LENGTH / 3; ++i) {
|
||||
final int val = decodeBase64(input, i * 4);
|
||||
ret |= val >>> 31;
|
||||
key[i * 3] = (byte) ((val >>> 16) & 0xff);
|
||||
key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);
|
||||
key[i * 3 + 2] = (byte) (val & 0xff);
|
||||
}
|
||||
final char[] endSegment = {
|
||||
input[i * 4],
|
||||
input[i * 4 + 1],
|
||||
input[i * 4 + 2],
|
||||
'A',
|
||||
};
|
||||
final int val = decodeBase64(endSegment, 0);
|
||||
ret |= (val >>> 31) | (val & 0xff);
|
||||
key[i * 3] = (byte) ((val >>> 16) & 0xff);
|
||||
key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);
|
||||
|
||||
if (ret != 0)
|
||||
throw new IllegalArgumentException(KEY_LENGTH_BASE64_EXCEPTION_MESSAGE);
|
||||
return key;
|
||||
}
|
||||
|
||||
public static byte[] keyFromHex(final String str) {
|
||||
final char[] input = str.toCharArray();
|
||||
final byte[] key = new byte[KEY_LENGTH];
|
||||
if (input.length != KEY_LENGTH_HEX)
|
||||
throw new IllegalArgumentException(KEY_LENGTH_HEX_EXCEPTION_MESSAGE);
|
||||
int ret = 0;
|
||||
|
||||
for (int i = 0; i < KEY_LENGTH_HEX; i += 2) {
|
||||
int c;
|
||||
int cNum;
|
||||
int cNum0;
|
||||
int cAlpha;
|
||||
int cAlpha0;
|
||||
int cVal;
|
||||
final int cAcc;
|
||||
|
||||
c = input[i];
|
||||
cNum = c ^ 48;
|
||||
cNum0 = ((cNum - 10) >>> 8) & 0xff;
|
||||
cAlpha = (c & ~32) - 55;
|
||||
cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff;
|
||||
ret |= ((cNum0 | cAlpha0) - 1) >>> 8;
|
||||
cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha);
|
||||
cAcc = cVal * 16;
|
||||
|
||||
c = input[i + 1];
|
||||
cNum = c ^ 48;
|
||||
cNum0 = ((cNum - 10) >>> 8) & 0xff;
|
||||
cAlpha = (c & ~32) - 55;
|
||||
cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff;
|
||||
ret |= ((cNum0 | cAlpha0) - 1) >>> 8;
|
||||
cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha);
|
||||
key[i / 2] = (byte) (cAcc | cVal);
|
||||
}
|
||||
if (ret != 0)
|
||||
throw new IllegalArgumentException(KEY_LENGTH_HEX_EXCEPTION_MESSAGE);
|
||||
return key;
|
||||
}
|
||||
|
||||
public static String keyToBase64(final byte[] key) {
|
||||
final char[] output = new char[KEY_LENGTH_BASE64];
|
||||
if (key.length != KEY_LENGTH)
|
||||
throw new IllegalArgumentException(KEY_LENGTH_EXCEPTION_MESSAGE);
|
||||
int i;
|
||||
for (i = 0; i < KEY_LENGTH / 3; ++i)
|
||||
encodeBase64(key, i * 3, output, i * 4);
|
||||
final byte[] endSegment = {
|
||||
key[i * 3],
|
||||
key[i * 3 + 1],
|
||||
0,
|
||||
};
|
||||
encodeBase64(endSegment, 0, output, i * 4);
|
||||
output[KEY_LENGTH_BASE64 - 1] = '=';
|
||||
return new String(output);
|
||||
}
|
||||
|
||||
public static String keyToHex(final byte[] key) {
|
||||
final char[] output = new char[KEY_LENGTH_HEX];
|
||||
if (key.length != KEY_LENGTH)
|
||||
throw new IllegalArgumentException(KEY_LENGTH_EXCEPTION_MESSAGE);
|
||||
for (int i = 0; i < KEY_LENGTH; ++i) {
|
||||
output[i * 2] = (char) (87 + (key[i] >> 4 & 0xf)
|
||||
+ ((((key[i] >> 4 & 0xf) - 10) >> 8) & ~38));
|
||||
output[i * 2 + 1] = (char) (87 + (key[i] & 0xf)
|
||||
+ ((((key[i] & 0xf) - 10) >> 8) & ~38));
|
||||
}
|
||||
return new String(output);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.crypto;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
/**
|
||||
* Represents a Curve25519 keypair as used by WireGuard.
|
||||
*/
|
||||
|
||||
public class Keypair {
|
||||
private final byte[] privateKey;
|
||||
private final byte[] publicKey;
|
||||
|
||||
public Keypair() {
|
||||
this(generatePrivateKey());
|
||||
}
|
||||
|
||||
private Keypair(final byte[] privateKey) {
|
||||
this.privateKey = privateKey;
|
||||
publicKey = generatePublicKey(privateKey);
|
||||
}
|
||||
|
||||
public Keypair(final String privateKey) {
|
||||
this(KeyEncoding.keyFromBase64(privateKey));
|
||||
}
|
||||
|
||||
@SuppressWarnings("MagicNumber")
|
||||
private static byte[] generatePrivateKey() {
|
||||
final SecureRandom secureRandom = new SecureRandom();
|
||||
final byte[] privateKey = new byte[KeyEncoding.KEY_LENGTH];
|
||||
secureRandom.nextBytes(privateKey);
|
||||
privateKey[0] &= 248;
|
||||
privateKey[31] &= 127;
|
||||
privateKey[31] |= 64;
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
private static byte[] generatePublicKey(final byte[] privateKey) {
|
||||
final byte[] publicKey = new byte[KeyEncoding.KEY_LENGTH];
|
||||
Curve25519.eval(publicKey, 0, privateKey, null);
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public String getPrivateKey() {
|
||||
return KeyEncoding.keyToBase64(privateKey);
|
||||
}
|
||||
|
||||
public String getPublicKey() {
|
||||
return KeyEncoding.keyToBase64(publicKey);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.util;
|
||||
|
||||
/**
|
||||
* Interface for objects that have a identifying key of the given type.
|
||||
*/
|
||||
|
||||
public interface Keyed<K> {
|
||||
K getKey();
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.util;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A list containing elements that can be looked up by key. A {@code KeyedList} cannot contain
|
||||
* {@code null} elements.
|
||||
*/
|
||||
|
||||
public interface KeyedList<K, E extends Keyed<? extends K>> extends List<E> {
|
||||
boolean containsAllKeys(Collection<K> keys);
|
||||
|
||||
boolean containsKey(K key);
|
||||
|
||||
@Nullable
|
||||
E get(K key);
|
||||
|
||||
@Nullable
|
||||
E getLast(K key);
|
||||
|
||||
int indexOfKey(K key);
|
||||
|
||||
int lastIndexOfKey(K key);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.util;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A keyed list where all elements are sorted by the comparator returned by {@code comparator()}
|
||||
* applied to their keys.
|
||||
*/
|
||||
|
||||
public interface SortedKeyedList<K, E extends Keyed<? extends K>> extends KeyedList<K, E> {
|
||||
Comparator<? super K> comparator();
|
||||
|
||||
@Nullable
|
||||
K firstKey();
|
||||
|
||||
Set<K> keySet();
|
||||
|
||||
@Nullable
|
||||
K lastKey();
|
||||
|
||||
Collection<E> values();
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="4dp" />
|
||||
<padding
|
||||
android:bottom="4dp"
|
||||
android:left="8dp"
|
||||
android:right="8dp"
|
||||
android:top="4dp" />
|
||||
<solid android:color="@color/fab_label_background_color" />
|
||||
</shape>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="?android:attr/colorForeground"
|
||||
android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z" />
|
||||
</vector>
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- TODO(msf): somehow make an additional activated color entry here (or elsewhere?) that
|
||||
is in use only in multiselect mode, and one that's in use only in ordinary
|
||||
select mode. In otherwords, we don't want to use the same background color
|
||||
for both multiselect and normal select (on two column machines).
|
||||
-->
|
||||
<item android:drawable="@color/list_item_activated" android:state_activated="true" /> <!-- TODO(msf): get color from theme with ?android/attr: -->
|
||||
<item android:drawable="@android:color/transparent" /> <!-- TODO(msf): get color from theme with ?android/attr: -->
|
||||
</selector>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@color/list_item_ripple"> <!-- TODO(msf): get color from theme with ?android/attr: -->
|
||||
<item android:drawable="@drawable/list_item_background" />
|
||||
<item android:id="@android:id/mask" android:drawable="@android:color/white" /> <!-- TODO(msf): get color from theme with ?android/attr: -->
|
||||
</ripple>
|
||||
@@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/master_detail_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:baselineAligned="false"
|
||||
android:divider="?android:attr/dividerHorizontal"
|
||||
android:orientation="horizontal"
|
||||
android:showDividers="middle"
|
||||
tools:context=".activity.MainActivity">
|
||||
|
||||
<fragment
|
||||
android:name="com.wireguard.android.fragment.TunnelListFragment"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="2"
|
||||
android:tag="LIST" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/detail_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="3" />
|
||||
</LinearLayout>
|
||||
@@ -1,43 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="android.view.View" />
|
||||
|
||||
<import type="com.wireguard.android.model.ApplicationData" />
|
||||
|
||||
<variable
|
||||
name="fragment"
|
||||
type="com.wireguard.android.fragment.AppListDialogFragment" />
|
||||
|
||||
<variable
|
||||
name="appData"
|
||||
type="com.wireguard.android.util.ObservableKeyedList<String, ApplicationData>" />
|
||||
</data>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:minHeight="200dp" >
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true"
|
||||
android:visibility="@{appData.isEmpty() ? View.VISIBLE : View.GONE}"/>
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/app_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:items="@{appData}"
|
||||
app:layout="@{@layout/app_list_item}" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
</layout>
|
||||
@@ -1,138 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="com.wireguard.android.model.Tunnel.State" />
|
||||
|
||||
<import type="com.wireguard.android.util.ClipboardUtils" />
|
||||
|
||||
<variable
|
||||
name="fragment"
|
||||
type="com.wireguard.android.fragment.TunnelDetailFragment" />
|
||||
|
||||
<variable
|
||||
name="tunnel"
|
||||
type="com.wireguard.android.model.Tunnel" />
|
||||
|
||||
<variable
|
||||
name="config"
|
||||
type="com.wireguard.config.Config.Observable" />
|
||||
</data>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/colorBackground">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<android.support.v7.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="?android:attr/colorBackground"
|
||||
app:cardCornerRadius="4dp"
|
||||
app:cardElevation="2dp"
|
||||
app:contentPadding="8dp">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interface_title"
|
||||
style="?android:attr/textAppearanceMedium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:text="@string/interface_title" />
|
||||
|
||||
<com.wireguard.android.widget.ToggleSwitch
|
||||
android:id="@+id/tunnel_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@+id/interface_title"
|
||||
android:layout_alignParentEnd="true"
|
||||
app:checked="@{tunnel.state == State.UP}"
|
||||
app:onBeforeCheckedChanged="@{fragment::setTunnelState}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interface_name_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/interface_title"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/interface_name_text"
|
||||
android:text="@string/name" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interface_name_text"
|
||||
style="?android:attr/textAppearanceMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/interface_name_label"
|
||||
android:text="@{tunnel.name}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/public_key_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/interface_name_text"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/public_key_text"
|
||||
android:text="@string/public_key" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/public_key_text"
|
||||
style="?android:attr/textAppearanceMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/public_key_label"
|
||||
android:contentDescription="@string/public_key_description"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:text="@{config.interfaceSection.publicKey}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/addresses_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/public_key_text"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/addresses_text"
|
||||
android:text="@string/addresses" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/addresses_text"
|
||||
style="?android:attr/textAppearanceMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/addresses_label"
|
||||
android:contentDescription="@string/addresses"
|
||||
android:text="@{config.interfaceSection.addresses}" />
|
||||
</RelativeLayout>
|
||||
</android.support.v7.widget.CardView>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:divider="@null"
|
||||
android:orientation="vertical"
|
||||
app:items="@{config.peers}"
|
||||
app:layout="@{@layout/tunnel_detail_peer}"
|
||||
tools:ignore="UselessLeaf" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</layout>
|
||||
@@ -1,94 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="com.wireguard.android.util.ClipboardUtils" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="com.wireguard.config.Peer.Observable" />
|
||||
</data>
|
||||
|
||||
<android.support.v7.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:background="?android:attr/colorBackground"
|
||||
app:cardCornerRadius="4dp"
|
||||
app:cardElevation="2dp"
|
||||
app:contentPadding="8dp">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/peer_title"
|
||||
style="?android:attr/textAppearanceMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:text="@string/peer" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/public_key_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/peer_title"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/public_key_text"
|
||||
android:text="@string/public_key" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/public_key_text"
|
||||
style="?android:attr/textAppearanceMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/public_key_label"
|
||||
android:contentDescription="@string/public_key_description"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:text="@{item.publicKey}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/allowed_ips_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/public_key_text"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/allowed_ips_text"
|
||||
android:text="@string/allowed_ips" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/allowed_ips_text"
|
||||
style="?android:attr/textAppearanceMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/allowed_ips_label"
|
||||
android:text="@{item.allowedIPs}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/endpoint_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/allowed_ips_text"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/endpoint_text"
|
||||
android:text="@string/endpoint" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/endpoint_text"
|
||||
style="?android:attr/textAppearanceMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/endpoint_label"
|
||||
android:text="@{item.endpoint}" />
|
||||
</RelativeLayout>
|
||||
</android.support.v7.widget.CardView>
|
||||
</layout>
|
||||
@@ -1,252 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="com.wireguard.android.util.ClipboardUtils" />
|
||||
|
||||
<import type="com.wireguard.android.widget.KeyInputFilter" />
|
||||
|
||||
<import type="com.wireguard.android.widget.NameInputFilter" />
|
||||
|
||||
<import type="com.wireguard.config.Peer" />
|
||||
|
||||
<variable
|
||||
name="fragment"
|
||||
type="com.wireguard.android.fragment.TunnelEditorFragment" />
|
||||
|
||||
<variable
|
||||
name="config"
|
||||
type="com.wireguard.config.Config.Observable" />
|
||||
</data>
|
||||
|
||||
<android.support.design.widget.CoordinatorLayout
|
||||
android:id="@+id/main_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/colorBackground">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<android.support.v7.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="?android:attr/colorBackground"
|
||||
app:cardCornerRadius="4dp"
|
||||
app:cardElevation="2dp"
|
||||
app:contentPadding="8dp">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interface_title"
|
||||
style="?android:attr/textAppearanceMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:text="@string/interface_title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interface_name_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/interface_title"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/interface_name_text"
|
||||
android:text="@string/name" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/interface_name_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/interface_name_label"
|
||||
android:inputType="textNoSuggestions|textVisiblePassword"
|
||||
android:text="@={config.name}"
|
||||
app:filter="@{NameInputFilter.newInstance()}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/private_key_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/interface_name_text"
|
||||
android:labelFor="@+id/private_key_text"
|
||||
android:text="@string/private_key" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/private_key_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/private_key_label"
|
||||
android:layout_toStartOf="@+id/generate_private_key_button"
|
||||
android:contentDescription="@string/public_key_description"
|
||||
android:inputType="textNoSuggestions|textVisiblePassword"
|
||||
android:text="@={config.interfaceSection.privateKey}"
|
||||
app:filter="@{KeyInputFilter.newInstance()}" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.AppCompat.Button.Borderless.Colored"
|
||||
android:id="@+id/generate_private_key_button"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBottom="@id/private_key_text"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_below="@+id/private_key_label"
|
||||
android:onClick="@{() -> config.interfaceSection.generateKeypair()}"
|
||||
android:text="@string/generate" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/public_key_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/private_key_text"
|
||||
android:labelFor="@+id/public_key_text"
|
||||
android:text="@string/public_key" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/public_key_text"
|
||||
style="?android:attr/editTextStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/public_key_label"
|
||||
android:contentDescription="@string/public_key_description"
|
||||
android:ellipsize="end"
|
||||
android:focusable="false"
|
||||
android:hint="@string/hint_generated"
|
||||
android:maxLines="1"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:text="@{config.interfaceSection.publicKey}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/addresses_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/public_key_text"
|
||||
android:layout_toStartOf="@+id/listen_port_label"
|
||||
android:labelFor="@+id/addresses_text"
|
||||
android:text="@string/addresses" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/addresses_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/addresses_label"
|
||||
android:layout_toStartOf="@+id/listen_port_text"
|
||||
android:inputType="textNoSuggestions|textVisiblePassword"
|
||||
android:text="@={config.interfaceSection.addresses}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/listen_port_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@+id/addresses_label"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignStart="@+id/generate_private_key_button"
|
||||
android:labelFor="@+id/listen_port_text"
|
||||
android:text="@string/listen_port" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/listen_port_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@+id/addresses_text"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignStart="@+id/generate_private_key_button"
|
||||
android:hint="@string/hint_random"
|
||||
android:inputType="number"
|
||||
android:text="@={config.interfaceSection.listenPort}"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dns_servers_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/addresses_text"
|
||||
android:layout_toStartOf="@+id/mtu_label"
|
||||
android:labelFor="@+id/dns_servers_text"
|
||||
android:text="@string/dns_servers" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/dns_servers_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/dns_servers_label"
|
||||
android:layout_toStartOf="@+id/mtu_text"
|
||||
android:inputType="textNoSuggestions|textVisiblePassword"
|
||||
android:text="@={config.interfaceSection.dnses}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mtu_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@+id/dns_servers_label"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignStart="@+id/generate_private_key_button"
|
||||
android:labelFor="@+id/mtu_text"
|
||||
android:text="@string/mtu" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/mtu_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@+id/dns_servers_text"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignStart="@+id/generate_private_key_button"
|
||||
android:hint="@string/hint_automatic"
|
||||
android:inputType="number"
|
||||
android:text="@={config.interfaceSection.mtu}"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.AppCompat.Button.Borderless.Colored"
|
||||
android:id="@+id/set_excluded_applications"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="-8dp"
|
||||
android:layout_below="@+id/dns_servers_text"
|
||||
android:onClick="@{fragment::onRequestSetExcludedApplications}"
|
||||
android:text="@{@plurals/set_excluded_applications(config.interfaceSection.excludedApplicationsCount, config.interfaceSection.excludedApplicationsCount)}" />
|
||||
</RelativeLayout>
|
||||
</android.support.v7.widget.CardView>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:divider="@null"
|
||||
android:orientation="vertical"
|
||||
app:items="@{config.peers}"
|
||||
app:layout="@{@layout/tunnel_editor_peer}"
|
||||
tools:ignore="UselessLeaf" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.AppCompat.Button.Colored"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:onClick="@{() -> config.peers.add(Peer.Observable.newInstance())}"
|
||||
android:text="@string/add_peer" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</android.support.design.widget.CoordinatorLayout>
|
||||
</layout>
|
||||
@@ -1,161 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="android.view.View"/>
|
||||
<import type="com.wireguard.android.widget.KeyInputFilter" />
|
||||
|
||||
<variable
|
||||
name="collection"
|
||||
type="android.databinding.ObservableList<com.wireguard.config.Peer.Observable>" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="com.wireguard.config.Peer.Observable" />
|
||||
</data>
|
||||
|
||||
<android.support.v7.widget.CardView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:background="?android:attr/colorBackground"
|
||||
app:cardCornerRadius="4dp"
|
||||
app:cardElevation="2dp"
|
||||
app:contentPadding="8dp">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/peer_title"
|
||||
style="?android:attr/textAppearanceMedium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_toStartOf="@+id/peer_action_delete"
|
||||
android:text="@string/peer" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/peer_action_delete"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="@null"
|
||||
android:contentDescription="@string/delete"
|
||||
android:onClick="@{() -> collection.remove(item)}"
|
||||
android:src="@drawable/ic_action_delete" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/public_key_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/peer_title"
|
||||
android:labelFor="@+id/public_key_text"
|
||||
android:text="@string/public_key" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/public_key_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/public_key_label"
|
||||
android:inputType="textNoSuggestions|textVisiblePassword"
|
||||
android:text="@={item.publicKey}"
|
||||
app:filter="@{KeyInputFilter.newInstance()}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pre_shared_key_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/public_key_text"
|
||||
android:labelFor="@+id/pre_shared_key_text"
|
||||
android:text="@string/pre_shared_key" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/pre_shared_key_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/pre_shared_key_label"
|
||||
android:hint="@string/hint_optional"
|
||||
android:inputType="textNoSuggestions|textVisiblePassword"
|
||||
android:text="@={item.preSharedKey}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/allowed_ips_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/pre_shared_key_text"
|
||||
android:layout_toStartOf="@+id/exclude_private_ips"
|
||||
android:labelFor="@+id/allowed_ips_text"
|
||||
android:text="@string/allowed_ips" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/exclude_private_ips"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@+id/allowed_ips_label"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:checked="@{item.isExcludePrivateIPsOn}"
|
||||
android:onClick="@{() -> item.toggleExcludePrivateIPs()}"
|
||||
android:text="@string/exclude_private_ips"
|
||||
android:visibility="@{item.canToggleExcludePrivateIPs ? View.VISIBLE : View.GONE}" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/allowed_ips_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/allowed_ips_label"
|
||||
android:inputType="textNoSuggestions|textVisiblePassword"
|
||||
android:text="@={item.allowedIPs}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/endpoint_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/allowed_ips_text"
|
||||
android:layout_toStartOf="@+id/persistent_keepalive_label"
|
||||
android:labelFor="@+id/endpoint_text"
|
||||
android:text="@string/endpoint" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/endpoint_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/endpoint_label"
|
||||
android:layout_toStartOf="@+id/persistent_keepalive_text"
|
||||
android:inputType="textNoSuggestions|textVisiblePassword"
|
||||
android:text="@={item.endpoint}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/persistent_keepalive_label"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@+id/endpoint_label"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:labelFor="@+id/persistent_keepalive_text"
|
||||
android:text="@string/persistent_keepalive" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/persistent_keepalive_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@+id/endpoint_text"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignStart="@+id/persistent_keepalive_label"
|
||||
android:hint="@string/hint_optional"
|
||||
android:inputType="number"
|
||||
android:text="@={item.persistentKeepalive}"
|
||||
android:textAlignment="center" />
|
||||
</RelativeLayout>
|
||||
</android.support.v7.widget.CardView>
|
||||
</layout>
|
||||
@@ -1,104 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="com.wireguard.android.model.Tunnel" />
|
||||
|
||||
<variable
|
||||
name="fragment"
|
||||
type="com.wireguard.android.fragment.TunnelListFragment" />
|
||||
|
||||
<variable
|
||||
name="rowConfigurationHandler"
|
||||
type="com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler" />
|
||||
|
||||
<variable
|
||||
name="tunnels"
|
||||
type="com.wireguard.android.util.ObservableKeyedList<String, Tunnel>" />
|
||||
</data>
|
||||
|
||||
<android.support.design.widget.CoordinatorLayout
|
||||
android:id="@+id/main_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/colorBackground"
|
||||
android:clipChildren="false">
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/tunnel_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="@{@dimen/design_fab_size_normal * 1.1f}"
|
||||
android:clipToPadding="false"
|
||||
android:choiceMode="multipleChoiceModal"
|
||||
android:visibility="@{tunnels.size() > 0 ? android.view.View.VISIBLE : android.view.View.GONE}"
|
||||
app:items="@{tunnels}"
|
||||
app:layout="@{@layout/tunnel_list_item}"
|
||||
app:configurationHandler="@{rowConfigurationHandler}" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="@{tunnels.size() == 0 ? android.view.View.VISIBLE : android.view.View.GONE}"
|
||||
android:layout_gravity="center">
|
||||
<android.support.v7.widget.AppCompatImageView
|
||||
android:id="@+id/logo_placeholder"
|
||||
android:layout_width="140dp"
|
||||
android:layout_height="140dp"
|
||||
android:layout_gravity="center"
|
||||
android:tint="@color/transparent_background_placeholder"
|
||||
android:layout_marginTop="-70dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:src="@mipmap/ic_launcher" />
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:textSize="20sp"
|
||||
android:text="@string/tunnel_list_placeholder" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.wireguard.android.widget.fab.FloatingActionsMenu
|
||||
android:id="@+id/create_menu"
|
||||
android:clipChildren="false"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="@dimen/fab_margin"
|
||||
app:fab_labelStyle="@style/fab_label"
|
||||
app:fab_labelsPosition="@integer/label_position"
|
||||
app:layout_behavior="com.wireguard.android.widget.fab.FloatingActionButtonBehavior" >
|
||||
|
||||
<com.wireguard.android.widget.fab.LabeledFloatingActionButton
|
||||
android:id="@+id/create_from_file"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:onClick="@{fragment::onRequestImportConfig}"
|
||||
app:srcCompat="@drawable/ic_action_open_white"
|
||||
app:fabSize="mini"
|
||||
app:fab_title="@string/create_from_file" />
|
||||
|
||||
<com.wireguard.android.widget.fab.LabeledFloatingActionButton
|
||||
android:id="@+id/scan_qr_code"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:onClick="@{fragment::onRequestScanQRCode}"
|
||||
app:srcCompat="@drawable/ic_action_scan_qr_code_white"
|
||||
app:fabSize="mini"
|
||||
app:fab_title="@string/create_from_qrcode" />
|
||||
|
||||
<com.wireguard.android.widget.fab.LabeledFloatingActionButton
|
||||
android:id="@+id/create_empty"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:onClick="@{fragment::onRequestCreateConfig}"
|
||||
app:fabSize="mini"
|
||||
app:srcCompat="@drawable/ic_action_edit_white"
|
||||
app:fab_title="@string/create_empty" />
|
||||
</com.wireguard.android.widget.fab.FloatingActionsMenu>
|
||||
|
||||
</android.support.design.widget.CoordinatorLayout>
|
||||
</layout>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<integer name="label_position">1</integer>
|
||||
</resources>
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- TODO(msf): remove these 4 hard-coded colors and replace with theme colors -->
|
||||
<color name="list_item_activated">#64cfd8dc</color>
|
||||
<color name="list_item_ripple">#a0808e95</color>
|
||||
<color name="fab_label_text_color">#000000</color>
|
||||
<color name="fab_label_background_color">#bbbbbb</color>
|
||||
|
||||
<color name="accent">#5e97f6</color>
|
||||
|
||||
<!-- TODO(msf): replace with ?android:attr/colorBackground with some alpha -->
|
||||
<color name="transparent_background_placeholder">#aa2e2e2e</color>
|
||||
</resources>
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- TODO(msf): remove these 4 hard-coded colors and replace with theme colors -->
|
||||
<color name="list_item_activated">#cfd8dc</color> <!-- Blue Grey 200 -->
|
||||
<color name="list_item_ripple">#808e95</color> <!-- Blue Grey 200 dark -->
|
||||
<color name="fab_label_text_color">#ffffff</color>
|
||||
<color name="fab_label_background_color">#444444</color>
|
||||
|
||||
<color name="accent">#2196F3</color>
|
||||
|
||||
<!-- TODO(msf): replace with ?android:attr/colorBackground with some alpha -->
|
||||
<color name="transparent_background_placeholder">#aafafafa</color>
|
||||
</resources>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="fab_margin">16dp</dimen>
|
||||
</resources>
|
||||
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item name="fab_expand_menu_button" type="id"/>
|
||||
<item name="fab_label" type="id"/>
|
||||
|
||||
<dimen name="fab_shadow_offset">3dp</dimen>
|
||||
<dimen name="fab_shadow_radius">9dp</dimen>
|
||||
|
||||
<dimen name="fab_stroke_width">1dp</dimen>
|
||||
|
||||
<dimen name="fab_actions_spacing">24dp</dimen>
|
||||
<dimen name="fab_labels_margin">8dp</dimen>
|
||||
|
||||
<declare-styleable name="LabeledFloatingActionButton">
|
||||
<attr name="fab_title" format="string"/>
|
||||
</declare-styleable>
|
||||
<declare-styleable name="FloatingActionsMenu">
|
||||
<attr name="fab_labelStyle" format="reference"/>
|
||||
<attr name="fab_labelsPosition" format="enum">
|
||||
<enum name="left" value="0"/>
|
||||
<enum name="right" value="1"/>
|
||||
</attr>
|
||||
<attr name="fab_expandDirection" format="enum">
|
||||
<enum name="up" value="0"/>
|
||||
<enum name="down" value="1"/>
|
||||
<enum name="left" value="2"/>
|
||||
<enum name="right" value="3"/>
|
||||
</attr>
|
||||
</declare-styleable>
|
||||
<integer name="label_position">0</integer>
|
||||
</resources>
|
||||
@@ -1,108 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="delete_error">
|
||||
<item quantity="one">Unable to delete %d tunnel: %s</item>
|
||||
<item quantity="other">Unable to delete %d tunnels: %s</item>
|
||||
</plurals>
|
||||
<plurals name="delete_success">
|
||||
<item quantity="one">Successfully deleted %d tunnel</item>
|
||||
<item quantity="other">Successfully deleted %d tunnels</item>
|
||||
</plurals>
|
||||
<plurals name="delete_title">
|
||||
<item quantity="one">%d tunnel selected</item>
|
||||
<item quantity="other">%d tunnels selected</item>
|
||||
</plurals>
|
||||
<plurals name="import_partial_success">
|
||||
<item quantity="one">Imported %d of %d tunnels</item>
|
||||
<item quantity="other">Imported %d of %d tunnels</item>
|
||||
</plurals>
|
||||
<plurals name="import_total_success">
|
||||
<item quantity="one">Imported %d tunnel</item>
|
||||
<item quantity="other">Imported %d tunnels</item>
|
||||
</plurals>
|
||||
<string name="add_peer">Add peer</string>
|
||||
<string name="addresses">Addresses</string>
|
||||
<string name="allowed_ips">Allowed IPs</string>
|
||||
<string name="app_name">WireGuard</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="config_save_error">Unable to save configuration for “%s”: %s</string>
|
||||
<string name="config_save_success">Successfully saved configuration for “%s”</string>
|
||||
<string name="create_activity_title">Create WireGuard Tunnel</string>
|
||||
<string name="create_empty">Create from scratch</string>
|
||||
<string name="create_from_file">Create from file or archive</string>
|
||||
<string name="create_from_qrcode">Create from QR code</string>
|
||||
<string name="create_tunnel">Create Tunnel</string>
|
||||
<string name="dark_theme_title">Use dark theme</string>
|
||||
<string name="dark_theme_summary_on">Currently using dark night theme</string>
|
||||
<string name="dark_theme_summary_off">Currently using light day theme</string>
|
||||
<string name="delete">Delete</string>
|
||||
<string name="deselect_all">Deselect All</string>
|
||||
<string name="select_all">Select all</string>
|
||||
<string name="dns_servers">DNS servers</string>
|
||||
<string name="edit">Edit</string>
|
||||
<string name="endpoint">Endpoint</string>
|
||||
<string name="error_fetching_apps">Error fetching apps list: %s</string>
|
||||
<string name="error_down">Error bringing down tunnel: %s</string>
|
||||
<string name="error_root">Please obtain root access and try again</string>
|
||||
<string name="error_up">Error bringing up tunnel: %s</string>
|
||||
<string name="excluded_applications">Excluded Applications</string>
|
||||
<string name="exclude_private_ips">Exclude private IPs</string>
|
||||
<string name="generate">Generate</string>
|
||||
<string name="hint_automatic">(auto)</string>
|
||||
<string name="hint_generated">(generated)</string>
|
||||
<string name="hint_optional">(optional)</string>
|
||||
<string name="hint_random">(random)</string>
|
||||
<string name="import_from_qrcode">Import Tunnel from QR Code</string>
|
||||
<string name="import_error">Unable to import tunnel: %s</string>
|
||||
<string name="import_success">Imported “%s”</string>
|
||||
<string name="interface_title">Interface</string>
|
||||
<string name="listen_port">Listen port</string>
|
||||
<string name="log_exporter_title">Export log file</string>
|
||||
<string name="log_export_error">Unable to export log: %s</string>
|
||||
<string name="log_export_success">Saved to %s</string>
|
||||
<string name="log_export_summary">Log file will be saved to downloads folder</string>
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="name">Name</string>
|
||||
<string name="peer">Peer</string>
|
||||
<string name="permission_label">control WireGuard tunnels</string>
|
||||
<string name="permission_description">Allows an app to control WireGuard tunnels. Apps with this permission may enable and disable WireGuard tunnels at will, potentially misdirecting Internet traffic.</string>
|
||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
||||
<string name="pre_shared_key">Pre-shared key</string>
|
||||
<string name="private_key">Private key</string>
|
||||
<string name="public_key">Public key</string>
|
||||
<string name="public_key_description">Public key</string>
|
||||
<string name="qrcode_hint">Tip: generate with `qrencode -t ansiutf8 < tunnel.conf`.</string>
|
||||
<string name="restore_on_boot_summary">Bring up previously-enabled tunnels on boot</string>
|
||||
<string name="restore_on_boot_title">Restore on boot</string>
|
||||
<string name="save">Save</string>
|
||||
<plurals name="set_excluded_applications">
|
||||
<item quantity="one">%d Excluded Application</item>
|
||||
<item quantity="other">%d Excluded Applications</item>
|
||||
</plurals>
|
||||
<string name="set_exclusions">Set Exclusions</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="toggle_error">Error toggling WireGuard tunnel: %s</string>
|
||||
<string name="tools_installer_already">wg and wg-quick are already installed</string>
|
||||
<string name="tools_installer_failure">Unable to install command-line tools (no root?)</string>
|
||||
<string name="tools_installer_initial">Install optional tools for scripting</string>
|
||||
<string name="tools_installer_working">Installing wg and wg-quick</string>
|
||||
<string name="tools_installer_initial_system">Install optional tools for scripting into the system partition</string>
|
||||
<string name="tools_installer_initial_magisk">Install optional tools for scripting as Magisk module</string>
|
||||
<string name="tools_installer_success_system">wg and wg-quick installed into the system partition</string>
|
||||
<string name="tools_installer_success_magisk">wg and wg-quick installed as a Magisk module (reboot required)</string>
|
||||
<string name="tools_installer_title">Install command line tools</string>
|
||||
<string name="tunnel_create_error">Unable to create tunnel: %s</string>
|
||||
<string name="tunnel_create_success">Successfully created tunnel “%s”</string>
|
||||
<string name="tunnel_list_placeholder">Add a tunnel using the blue button</string>
|
||||
<string name="tunnel_name">Tunnel Name</string>
|
||||
<string name="tunnel_rename_error">Unable to rename tunnel: %s</string>
|
||||
<string name="tunnel_rename_success">Successfully renamed tunnel to “%s”</string>
|
||||
<string name="version_title">WireGuard for Android v%s"</string>
|
||||
<string name="version_summary">%s backend v%s</string>
|
||||
<string name="version_summary_checking">Checking %s backend version</string>
|
||||
<string name="version_summary_unknown">Unknown %s version</string>
|
||||
<string name="zip_exporter_title">Export tunnels to zip file</string>
|
||||
<string name="zip_export_error">Unable to export tunnels: %s</string>
|
||||
<string name="zip_export_success">Saved to %s</string>
|
||||
<string name="zip_export_summary">Zip file will be saved to downloads folder</string>
|
||||
</resources>
|
||||
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.DarkActionBar">
|
||||
<item name="colorAccent">@color/accent</item>
|
||||
</style>
|
||||
|
||||
<style name="SettingsTheme" parent="AppTheme">
|
||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
|
||||
</style>
|
||||
|
||||
<style name="fab_label" parent="TextAppearance.AppCompat.Inverse">
|
||||
<item name="android:background">@drawable/fab_label_background</item>
|
||||
<item name="android:textColor">@color/fab_label_text_color</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<com.wireguard.android.preference.VersionPreference android:icon="@mipmap/ic_launcher" />
|
||||
<CheckBoxPreference
|
||||
android:defaultValue="false"
|
||||
android:key="restore_on_boot"
|
||||
android:summary="@string/restore_on_boot_summary"
|
||||
android:title="@string/restore_on_boot_title" />
|
||||
<com.wireguard.android.preference.ToolsInstallerPreference android:key="tools_installer" />
|
||||
<com.wireguard.android.preference.ZipExporterPreference />
|
||||
<com.wireguard.android.preference.LogExporterPreference />
|
||||
<CheckBoxPreference
|
||||
android:defaultValue="false"
|
||||
android:key="dark_theme"
|
||||
android:summaryOn="@string/dark_theme_summary_on"
|
||||
android:summaryOff="@string/dark_theme_summary_off"
|
||||
android:title="@string/dark_theme_title" />
|
||||
</PreferenceScreen>
|
||||
Submodule app/tools/libmnl deleted from 0930a63252
@@ -1,2 +0,0 @@
|
||||
src/
|
||||
.gobuildversion
|
||||
@@ -1,58 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
|
||||
FILES := $(filter-out %/main.go,$(wildcard ../wireguard-go/*/*.go) $(wildcard ../wireguard-go/*.go))
|
||||
|
||||
NDK_GO_ARCH_MAP_x86 := 386
|
||||
NDK_GO_ARCH_MAP_x86_64 := amd64
|
||||
NDK_GO_ARCH_MAP_arm := arm
|
||||
NDK_GO_ARCH_MAP_arm64 := arm64
|
||||
NDK_GO_ARCH_MAP_mips := mipsx
|
||||
NDK_GO_ARCH_MAP_mips64 := mips64x
|
||||
|
||||
CLANG_FLAGS := --target=$(ANDROID_LLVM_TRIPLE) --gcc-toolchain=$(ANDROID_TOOLCHAIN_ROOT) --sysroot=$(ANDROID_SYSROOT)
|
||||
export CGO_CFLAGS := $(CLANG_FLAGS) $(CFLAGS)
|
||||
export CGO_LDFLAGS := $(CLANG_FLAGS) $(LDFLAGS)
|
||||
export CC := $(ANDROID_C_COMPILER)
|
||||
export GOARCH := $(NDK_GO_ARCH_MAP_$(ANDROID_ARCH_NAME))
|
||||
export GOOS := android
|
||||
export CGO_ENABLED := 1
|
||||
|
||||
default: $(DESTDIR)/libwg-go.so
|
||||
|
||||
GOBUILDARCH := $(NDK_GO_ARCH_MAP_$(shell uname -m))
|
||||
GOBUILDOS := $(shell uname -s | tr '[:upper:]' '[:lower:]')
|
||||
GOBUILDVERSION := 1.10.2
|
||||
GOBUILDTARBALL := https://dl.google.com/go/go$(GOBUILDVERSION).$(GOBUILDOS)-$(GOBUILDARCH).tar.gz
|
||||
GOBUILDVERSION_NEEDED := go version go$(GOBUILDVERSION) $(GOBUILDOS)/$(GOBUILDARCH)
|
||||
|
||||
GOBUILDVERSION_CURRENT := $(shell go version 2>/dev/null)
|
||||
ifeq ($(GOBUILDVERSION_NEEDED),$(GOBUILDVERSION_CURRENT))
|
||||
$(info Using system Go)
|
||||
else
|
||||
export GOROOT := $(CURDIR)/goroot
|
||||
export PATH := $(GOROOT)/bin:$(PATH)
|
||||
GOBUILDVERSION_CURRENT := $(shell $(GOROOT)/bin/go version 2>/dev/null)
|
||||
ifneq ($(GOBUILDVERSION_NEEDED),$(GOBUILDVERSION_CURRENT))
|
||||
$(shell rm -f $(GOROOT)/bin/go)
|
||||
endif
|
||||
$(GOROOT)/bin/go:
|
||||
rm -rf "$(GOROOT)"
|
||||
mkdir -p "$(GOROOT)"
|
||||
curl "$(GOBUILDTARBALL)" | tar -C "$(GOROOT)" --strip-components=1 -xzf - || { rm -rf "$(GOROOT)"; exit 1; }
|
||||
patch -p1 -f -N -r- -d "$(GOROOT)" < goruntime-boottime-over-monotonic.diff || { rm -rf "$(GOROOT)"; exit 1; }
|
||||
$(DESTDIR)/libwg-go.so: $(GOROOT)/bin/go
|
||||
endif
|
||||
|
||||
$(shell test "$$(cat .gobuildversion 2>/dev/null)" = "$(GOBUILDVERSION_CURRENT)" || rm -f "$(DESTDIR)/libwg-go.so")
|
||||
|
||||
$(DESTDIR)/libwg-go.so: $(FILES) src/git.zx2c4.com/wireguard-go/api-android.go src/git.zx2c4.com/wireguard-go/tun/api-android.go src/git.zx2c4.com/wireguard-go/jni.c
|
||||
@goroot="$$(go env GOROOT)" || exit 1; patch -p1 -f -R -s --dry-run -d "$$goroot" < goruntime-boottime-over-monotonic.diff >/dev/null 2>&1 || { echo "ERROR: Your system's Go is not patched to use the suspend-aware monotonic clock. Please try again after having run: \`patch -p1 -d \"$$goroot\" < \"$(CURDIR)/goruntime-boottime-over-monotonic.diff\"\`."; exit 1; }
|
||||
find . -name '*.go' -type l -delete
|
||||
find . -type d -empty -delete
|
||||
mkdir -p $(subst ../wireguard-go/,./src/git.zx2c4.com/wireguard-go/,$(dir $(FILES)))
|
||||
$(foreach FILE,$(FILES),ln -sfrt $(subst ../wireguard-go/,./src/git.zx2c4.com/wireguard-go/,$(dir $(FILE))) $(FILE);)
|
||||
GOPATH=$(PWD) go get -v -d git.zx2c4.com/wireguard-go
|
||||
GOPATH=$(PWD) go build -ldflags="-X main.socketDirectory=/data/data/$(ANDROID_PACKAGE_NAME)/cache/wireguard" -v -o $(DESTDIR)/libwg-go.so -buildmode c-shared git.zx2c4.com/wireguard-go
|
||||
go version > .gobuildversion
|
||||
@@ -1,103 +0,0 @@
|
||||
diff -ru b/src/runtime/sys_linux_386.s a/src/runtime/sys_linux_386.s
|
||||
--- b/src/runtime/sys_linux_386.s 2018-04-30 22:26:39.000000000 +0200
|
||||
+++ a/src/runtime/sys_linux_386.s 2018-05-09 17:09:42.319531743 +0200
|
||||
@@ -287,13 +287,13 @@
|
||||
|
||||
LEAL 8(SP), BX // &ts (struct timespec)
|
||||
MOVL BX, 4(SP)
|
||||
- MOVL $1, 0(SP) // CLOCK_MONOTONIC
|
||||
+ MOVL $7, 0(SP) // CLOCK_BOOTTIME
|
||||
CALL AX
|
||||
JMP finish
|
||||
|
||||
fallback:
|
||||
MOVL $SYS_clock_gettime, AX
|
||||
- MOVL $1, BX // CLOCK_MONOTONIC
|
||||
+ MOVL $7, BX // CLOCK_BOOTTIME
|
||||
LEAL 8(SP), CX
|
||||
INVOKE_SYSCALL
|
||||
|
||||
diff -ru b/src/runtime/sys_linux_amd64.s a/src/runtime/sys_linux_amd64.s
|
||||
--- b/src/runtime/sys_linux_amd64.s 2018-04-30 22:26:39.000000000 +0200
|
||||
+++ a/src/runtime/sys_linux_amd64.s 2018-05-09 17:09:42.319531743 +0200
|
||||
@@ -258,7 +258,7 @@
|
||||
MOVQ runtime·__vdso_clock_gettime_sym(SB), AX
|
||||
CMPQ AX, $0
|
||||
JEQ fallback
|
||||
- MOVL $1, DI // CLOCK_MONOTONIC
|
||||
+ MOVL $7, DI // CLOCK_BOOTTIME
|
||||
LEAQ 0(SP), SI
|
||||
CALL AX
|
||||
MOVQ 0(SP), AX // sec
|
||||
diff -ru b/src/runtime/sys_linux_arm64.s a/src/runtime/sys_linux_arm64.s
|
||||
--- b/src/runtime/sys_linux_arm64.s 2018-04-30 22:26:39.000000000 +0200
|
||||
+++ a/src/runtime/sys_linux_arm64.s 2018-05-09 17:12:47.844591628 +0200
|
||||
@@ -201,7 +201,7 @@
|
||||
RET
|
||||
|
||||
TEXT runtime·nanotime(SB),NOSPLIT,$24-8
|
||||
- MOVW $1, R0 // CLOCK_MONOTONIC
|
||||
+ MOVW $7, R0 // CLOCK_BOOTTIME
|
||||
MOVD RSP, R1
|
||||
MOVD $SYS_clock_gettime, R8
|
||||
SVC
|
||||
diff -ru b/src/runtime/sys_linux_arm.s a/src/runtime/sys_linux_arm.s
|
||||
--- b/src/runtime/sys_linux_arm.s 2018-04-30 22:26:39.000000000 +0200
|
||||
+++ a/src/runtime/sys_linux_arm.s 2018-05-09 17:13:18.983281049 +0200
|
||||
@@ -235,7 +235,7 @@
|
||||
|
||||
// int64 nanotime(void)
|
||||
TEXT runtime·nanotime(SB),NOSPLIT,$32
|
||||
- MOVW $1, R0 // CLOCK_MONOTONIC
|
||||
+ MOVW $7, R0 // CLOCK_BOOTTIME
|
||||
MOVW $8(R13), R1 // timespec
|
||||
MOVW $SYS_clock_gettime, R7
|
||||
SWI $0
|
||||
diff -ru b/src/runtime/sys_linux_mips64x.s a/src/runtime/sys_linux_mips64x.s
|
||||
--- b/src/runtime/sys_linux_mips64x.s 2018-04-30 22:26:39.000000000 +0200
|
||||
+++ a/src/runtime/sys_linux_mips64x.s 2018-05-09 17:09:42.321531721 +0200
|
||||
@@ -197,7 +197,7 @@
|
||||
RET
|
||||
|
||||
TEXT runtime·nanotime(SB),NOSPLIT,$16
|
||||
- MOVW $1, R4 // CLOCK_MONOTONIC
|
||||
+ MOVW $7, R4 // CLOCK_BOOTTIME
|
||||
MOVV $0(R29), R5
|
||||
MOVV $SYS_clock_gettime, R2
|
||||
SYSCALL
|
||||
diff -ru b/src/runtime/sys_linux_mipsx.s a/src/runtime/sys_linux_mipsx.s
|
||||
--- b/src/runtime/sys_linux_mipsx.s 2018-04-30 22:26:39.000000000 +0200
|
||||
+++ a/src/runtime/sys_linux_mipsx.s 2018-05-09 17:09:42.321531721 +0200
|
||||
@@ -202,7 +202,7 @@
|
||||
RET
|
||||
|
||||
TEXT runtime·nanotime(SB),NOSPLIT,$8-8
|
||||
- MOVW $1, R4 // CLOCK_MONOTONIC
|
||||
+ MOVW $7, R4 // CLOCK_BOOTTIME
|
||||
MOVW $4(R29), R5
|
||||
MOVW $SYS_clock_gettime, R2
|
||||
SYSCALL
|
||||
diff -ru b/src/runtime/sys_linux_ppc64x.s a/src/runtime/sys_linux_ppc64x.s
|
||||
--- b/src/runtime/sys_linux_ppc64x.s 2018-04-30 22:26:39.000000000 +0200
|
||||
+++ a/src/runtime/sys_linux_ppc64x.s 2018-05-09 17:09:42.322531710 +0200
|
||||
@@ -176,7 +176,7 @@
|
||||
RET
|
||||
|
||||
TEXT runtime·nanotime(SB),NOSPLIT,$16
|
||||
- MOVW $1, R3 // CLOCK_MONOTONIC
|
||||
+ MOVW $7, R3 // CLOCK_BOOTTIME
|
||||
MOVD $0(R1), R4
|
||||
SYSCALL $SYS_clock_gettime
|
||||
MOVD 0(R1), R3 // sec
|
||||
diff -ru b/src/runtime/sys_linux_s390x.s a/src/runtime/sys_linux_s390x.s
|
||||
--- b/src/runtime/sys_linux_s390x.s 2018-04-30 22:26:39.000000000 +0200
|
||||
+++ a/src/runtime/sys_linux_s390x.s 2018-05-09 17:09:42.322531710 +0200
|
||||
@@ -188,7 +188,7 @@
|
||||
RET
|
||||
|
||||
TEXT runtime·nanotime(SB),NOSPLIT,$16
|
||||
- MOVW $1, R2 // CLOCK_MONOTONIC
|
||||
+ MOVW $7, R2 // CLOCK_BOOTTIME
|
||||
MOVD $tp-16(SP), R3
|
||||
MOVW $SYS_clock_gettime, R1
|
||||
SYSCALL
|
||||
@@ -1,31 +0,0 @@
|
||||
/* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Copyright (C) 2017-2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package tun
|
||||
|
||||
import (
|
||||
"git.zx2c4.com/wireguard-go/rwcancel"
|
||||
"os"
|
||||
)
|
||||
|
||||
func CreateTUNFromFD(tun_fd int) (TUNDevice, string, error) {
|
||||
tun := &nativeTun{
|
||||
fd: os.NewFile(uintptr(tun_fd), "/dev/tun"),
|
||||
events: make(chan TUNEvent, 5),
|
||||
errors: make(chan error, 5),
|
||||
nopi: true,
|
||||
}
|
||||
var err error
|
||||
tun.fdCancel, err = rwcancel.NewRWCancel(tun_fd)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
name, err := tun.Name()
|
||||
if err != nil {
|
||||
tun.fdCancel.Cancel()
|
||||
return nil, "", err
|
||||
}
|
||||
return tun, name, nil
|
||||
}
|
||||
Submodule app/tools/wireguard deleted from 8f49f7b30f
Submodule app/tools/wireguard-go deleted from 3ad3e83c7a
+45
-5
@@ -1,20 +1,60 @@
|
||||
allprojects {
|
||||
buildscript {
|
||||
ext {
|
||||
activityVersion = '1.2.0-alpha07'
|
||||
agpVersion = '4.0.1'
|
||||
annotationsVersion = '1.1.0'
|
||||
appcompatVersion = '1.2.0'
|
||||
bintrayPluginVersion = '1.8.5'
|
||||
biometricVersion = '1.1.0-alpha02'
|
||||
collectionVersion = '1.1.0'
|
||||
constraintLayoutVersion = '2.0.1'
|
||||
coordinatorLayoutVersion = '1.1.0'
|
||||
coreKtxVersion = '1.3.1'
|
||||
coroutinesVersion = '1.3.9'
|
||||
datastoreVersion = '1.0.0-alpha01'
|
||||
desugarVersion = '1.0.10'
|
||||
fragmentVersion = '1.3.0-alpha07'
|
||||
jsr305Version = '3.0.2'
|
||||
junitVersion = '4.13'
|
||||
kotlinVersion = '1.4.10'
|
||||
lifecycleRuntimeKtxVersion = '2.3.0-alpha07'
|
||||
materialComponentsVersion = '22973ff10305397bfc72fed41684175fee8aeefb'
|
||||
mavenPluginVersion = '2.1'
|
||||
preferenceVersion = '1.1.1'
|
||||
zxingEmbeddedVersion = '3.6.0'
|
||||
|
||||
groupName = 'com.wireguard.android'
|
||||
}
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:$agpVersion"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
classpath "com.github.dcendents:android-maven-gradle-plugin:$mavenPluginVersion"
|
||||
classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:$bintrayPluginVersion"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
}
|
||||
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.1.3'
|
||||
}
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
tasks {
|
||||
wrapper {
|
||||
gradleVersion = "6.6.1"
|
||||
distributionType = Wrapper.DistributionType.ALL
|
||||
distributionSha256Sum = "11657af6356b7587bfb37287b5992e94a9686d5c8a0a1b60b87b9928a2decde5"
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "version.gradle"
|
||||
|
||||
@@ -9,9 +9,25 @@
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
android.enableJetifier=true
|
||||
android.useAndroidX=true
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
# R8 Full mode
|
||||
android.enableR8.fullMode=true
|
||||
|
||||
# https://jakewharton.com/increased-accuracy-of-aapt2-keep-rules/
|
||||
android.useMinimalKeepRules=true
|
||||
|
||||
# Enable rudimentary R class namespacing where each library only contains
|
||||
# references to the resources it declares instead of declarations plus all
|
||||
# transitive dependency references.
|
||||
android.namespacedRClass=true
|
||||
|
||||
# Suppress warnings for some features that aren't yet stabilized
|
||||
android.suppressUnsupportedOptionWarnings=android.enableR8.fullMode,android.useMinimalKeepRules,android.namespacedRClass,android.suppressUnsupportedOptionWarnings
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+2
-1
@@ -1,5 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-bin.zip
|
||||
distributionSha256Sum=11657af6356b7587bfb37287b5992e94a9686d5c8a0a1b60b87b9928a2decde5
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
@@ -28,7 +44,7 @@ APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
@@ -66,6 +82,7 @@ esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
@@ -109,10 +126,11 @@ if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
@@ -138,19 +156,19 @@ if $cygwin ; then
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
@@ -159,14 +177,9 @@ save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
||||
+2
-1
@@ -1 +1,2 @@
|
||||
include ':app'
|
||||
include ':tunnel'
|
||||
include ':ui'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user