Merge branch 'master' into temp-hotspot-use-system
This commit is contained in:
@@ -3,7 +3,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
working_directory: ~/code
|
working_directory: ~/code
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/android:api-30
|
- image: cimg/android:2023.02.1
|
||||||
environment:
|
environment:
|
||||||
GRADLE_OPTS: -Dorg.gradle.workers.max=1 -Dorg.gradle.daemon=false -Dkotlin.compiler.execution.strategy="in-process"
|
GRADLE_OPTS: -Dorg.gradle.workers.max=1 -Dorg.gradle.daemon=false -Dkotlin.compiler.execution.strategy="in-process"
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
119
README.md
119
README.md
@@ -1,15 +1,19 @@
|
|||||||
# VPN Hotspot
|
# VPN Hotspot
|
||||||
|
|
||||||
[](https://circleci.com/gh/Mygod/VPNHotspot)
|
[](https://circleci.com/gh/Mygod/VPNHotspot)
|
||||||
[](https://android-arsenal.com/api?level=21)
|
[](https://android-arsenal.com/api?level=28)
|
||||||
[](https://github.com/Mygod/VPNHotspot/releases)
|
[](https://github.com/Mygod/VPNHotspot/releases)
|
||||||
[](https://github.com/Mygod/VPNHotspot/search?l=kotlin)
|
[](https://github.com/Mygod/VPNHotspot/search?l=kotlin)
|
||||||
[](https://www.codacy.com/app/Mygod/VPNHotspot?utm_source=github.com&utm_medium=referral&utm_content=Mygod/VPNHotspot&utm_campaign=Badge_Grade)
|
[](https://www.codacy.com/gh/Mygod/VPNHotspot/dashboard?utm_source=github.com&utm_medium=referral&utm_content=Mygod/VPNHotspot&utm_campaign=Badge_Grade)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
|
||||||
Connecting things to your VPN made simple. Share your VPN connection over hotspot or repeater. (**root required**)
|
Connecting things to your VPN made simple. Share your VPN connection over hotspot or repeater. (**root required**)
|
||||||
<a href="https://play.google.com/store/apps/details?id=be.mygod.vpnhotspot" target="_blank"><img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" height="60"></a>,
|
|
||||||
<a href="https://appdistribution.firebase.dev/i/FUCPGdzm" target="_blank">sign up for beta</a>
|
| Release channel | [GitHub](https://github.com/Mygod/VPNHotspot/releases) | [Google Play](https://play.google.com/store/apps/details?id=be.mygod.vpnhotspot) ([beta](https://play.google.com/apps/testing/be.mygod.vpnhotspot)) |
|
||||||
|
|---------------------------------------------------------|:------------------------------------------------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------:|
|
||||||
|
| Auto update | Email updates via watching releases | ✓ |
|
||||||
|
| In-app update channel | GitHub | Google Play |
|
||||||
|
| [Sponsor/Donation](https://github.com/sponsors/Mygod) | ✓ | Google Play In-App Purchases only |
|
||||||
|
|
||||||
This app is useful for:
|
This app is useful for:
|
||||||
|
|
||||||
@@ -69,7 +73,7 @@ Default settings are picked to suit general use cases and maximize compatibility
|
|||||||
I find turning this option off sometimes works better for dummy VPNs like ad-blockers and socksifiers than Simple mode, e.g. Shadowsocks.
|
I find turning this option off sometimes works better for dummy VPNs like ad-blockers and socksifiers than Simple mode, e.g. Shadowsocks.
|
||||||
But you should never use this for real VPNs like OpenVPN, etc.
|
But you should never use this for real VPNs like OpenVPN, etc.
|
||||||
- Simple: Source address/port from downstream packets will be remapped and that's about it.
|
- Simple: Source address/port from downstream packets will be remapped and that's about it.
|
||||||
- (since Android 9) Android Netd Service:
|
- Android Netd Service:
|
||||||
Let your system handle masquerade.
|
Let your system handle masquerade.
|
||||||
Android system will do a few extra things to make things like FTP and tethering traffic counter work.
|
Android system will do a few extra things to make things like FTP and tethering traffic counter work.
|
||||||
You should probably not use this if you are trying to hide your tethering activity from your carrier.
|
You should probably not use this if you are trying to hide your tethering activity from your carrier.
|
||||||
@@ -78,7 +82,7 @@ Default settings are picked to suit general use cases and maximize compatibility
|
|||||||
|
|
||||||
* Disable IPv6 tethering: Turning this option on will disable IPv6 for system tethering. Useful for stopping IPv6 leaks
|
* Disable IPv6 tethering: Turning this option on will disable IPv6 for system tethering. Useful for stopping IPv6 leaks
|
||||||
as this app currently doesn't handle IPv6 VPN tethering (see [#6](https://github.com/Mygod/VPNHotspot/issues/6)).
|
as this app currently doesn't handle IPv6 VPN tethering (see [#6](https://github.com/Mygod/VPNHotspot/issues/6)).
|
||||||
* (since Android 8.1) Tethering hardware acceleration:
|
* Tethering hardware acceleration:
|
||||||
This is a shortcut to the same setting in system Developer options.
|
This is a shortcut to the same setting in system Developer options.
|
||||||
Turning this option off is probably a must for making VPN tethering over system tethering work,
|
Turning this option off is probably a must for making VPN tethering over system tethering work,
|
||||||
but it might also decrease your battery life while tethering is enabled.
|
but it might also decrease your battery life while tethering is enabled.
|
||||||
@@ -146,58 +150,56 @@ _a.k.a. things that can go wrong if this app doesn't work._
|
|||||||
This is a list of stuff that might impact this app's functionality if unavailable.
|
This is a list of stuff that might impact this app's functionality if unavailable.
|
||||||
This is only meant to be an index.
|
This is only meant to be an index.
|
||||||
You can read more in the source code.
|
You can read more in the source code.
|
||||||
API restrictions are updated up to [commit `ebe7044`](https://android.googlesource.com/platform/prebuilts/runtime/+/ebe7044/appcompat/hiddenapi-flags.csv).
|
API restrictions are updated up to [SHA-256 checksum `233a277aa8ac475b6df61bffd95665d86aac6eb2ad187b90bf42a98f5f2a11a3`](https://dl.google.com/developers/android/tm/non-sdk/hiddenapi-flags.csv).
|
||||||
|
|
||||||
Greylisted/blacklisted APIs or internal constants: (some constants are hardcoded or implicitly used)
|
Greylisted/blacklisted APIs or internal constants: (some constants are hardcoded or implicitly used)
|
||||||
|
|
||||||
* (prior to API 30) `Landroid/net/ConnectivityManager;->getLastTetherError(Ljava/lang/String;)I,max-target-r`
|
* (prior to API 30) `Landroid/net/ConnectivityManager;->getLastTetherError(Ljava/lang/String;)I,max-target-r`
|
||||||
* (since API 30) `Landroid/net/ConnectivityModuleConnector;->IN_PROCESS_SUFFIX:Ljava/lang/String;`
|
* (since API 30) `Landroid/net/ConnectivityModuleConnector;->IN_PROCESS_SUFFIX:Ljava/lang/String;`
|
||||||
* (since API 30) `Landroid/net/TetheringManager$TetheringEventCallback;->onTetherableInterfaceRegexpsChanged(Landroid/net/TetheringManager$TetheringInterfaceRegexps;)V,blocked`
|
* (since API 30) `Landroid/net/TetheringManager$TetheringEventCallback;->onTetherableInterfaceRegexpsChanged(Landroid/net/TetheringManager$TetheringInterfaceRegexps;)V,blocked`
|
||||||
* (since API 30) `Landroid/net/TetheringManager;->TETHERING_WIGIG:I,blocked`
|
* (since API 31) `Landroid/net/TetheringManager$TetheringEventCallback;->onSupportedTetheringTypes(Ljava/util/Set;)V,blocked`
|
||||||
* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setUserConfiguration(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,blocked`
|
* (since API 31) `Landroid/net/wifi/SoftApCapability;->getCountryCode()Ljava/lang/String;,blocked`
|
||||||
|
* (since API 33) `Landroid/net/wifi/SoftApConfiguration$Builder;->setRandomizedMacAddress(Landroid/net/MacAddress;)Landroid/net/wifi/SoftApConfiguration$Builder;,blocked`
|
||||||
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->BAND_TYPES:[I,blocked`
|
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->BAND_TYPES:[I,blocked`
|
||||||
* (since API 31) `Landroid/net/wifi/SoftApInfo;->getApInstanceIdentifier()Ljava/lang/String;,blocked`
|
* (since API 31) `Landroid/net/wifi/SoftApInfo;->getApInstanceIdentifier()Ljava/lang/String;,blocked`
|
||||||
* (since API 31) `Landroid/net/wifi/WifiClient;->getApInstanceIdentifier()Ljava/lang/String;,blocked`
|
* (since API 31) `Landroid/net/wifi/WifiClient;->getApInstanceIdentifier()Ljava/lang/String;,blocked`
|
||||||
* (prior to API 30) `Landroid/net/wifi/WifiConfiguration$KeyMgmt;->FT_PSK:I,lo-prio,max-target-o`
|
* (prior to API 30) `Landroid/net/wifi/WifiConfiguration$KeyMgmt;->FT_PSK:I,lo-prio,max-target-o`
|
||||||
* (prior to API 30) `Landroid/net/wifi/WifiConfiguration$KeyMgmt;->WPA_PSK_SHA256:I,blocked`
|
* (prior to API 30) `Landroid/net/wifi/WifiConfiguration$KeyMgmt;->WPA_PSK_SHA256:I,blocked`
|
||||||
* (since API 23, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->AP_BAND_2GHZ:I,lo-prio,max-target-o`
|
* (prior to API 30) `Landroid/net/wifi/WifiConfiguration;->AP_BAND_2GHZ:I,lo-prio,max-target-o`
|
||||||
* (since API 23, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->AP_BAND_5GHZ:I,lo-prio,max-target-o`
|
* (prior to API 30) `Landroid/net/wifi/WifiConfiguration;->AP_BAND_5GHZ:I,lo-prio,max-target-o`
|
||||||
* (since API 28, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->AP_BAND_ANY:I,lo-prio,max-target-o`
|
* (prior to API 30) `Landroid/net/wifi/WifiConfiguration;->AP_BAND_ANY:I,lo-prio,max-target-o`
|
||||||
* (since API 23, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->apBand:I,unsupported`
|
* (prior to API 30) `Landroid/net/wifi/WifiConfiguration;->apBand:I,unsupported`
|
||||||
* (since API 23, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->apChannel:I,unsupported`
|
* (prior to API 30) `Landroid/net/wifi/WifiConfiguration;->apChannel:I,unsupported`
|
||||||
* (since API 28, prior to API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onNumClientsChanged(I)V,greylist-max-o`
|
* (prior to API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onNumClientsChanged(I)V,greylist-max-o`
|
||||||
* (since API 26) `Landroid/net/wifi/WifiManager;->cancelLocalOnlyHotspotRequest()V,unsupported`
|
* `Landroid/net/wifi/WifiManager;->cancelLocalOnlyHotspotRequest()V,unsupported`
|
||||||
* (prior to API 26) `Landroid/net/wifi/WifiManager;->setWifiApEnabled(Landroid/net/wifi/WifiConfiguration;Z)Z`
|
|
||||||
* `Landroid/net/wifi/p2p/WifiP2pConfig$Builder;->MAC_ANY_ADDRESS:Landroid/net/MacAddress;,blocked`
|
* `Landroid/net/wifi/p2p/WifiP2pConfig$Builder;->MAC_ANY_ADDRESS:Landroid/net/MacAddress;,blocked`
|
||||||
* (since API 29) `Landroid/net/wifi/p2p/WifiP2pConfig$Builder;->mNetworkName:Ljava/lang/String;,blocked`
|
* (since API 29) `Landroid/net/wifi/p2p/WifiP2pConfig$Builder;->mNetworkName:Ljava/lang/String;,blocked`
|
||||||
* `Landroid/net/wifi/p2p/WifiP2pManager;->startWps(Landroid/net/wifi/p2p/WifiP2pManager$Channel;Landroid/net/wifi/WpsInfo;Landroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,max-target-r`
|
* `Landroid/net/wifi/p2p/WifiP2pManager;->startWps(Landroid/net/wifi/p2p/WifiP2pManager$Channel;Landroid/net/wifi/WpsInfo;Landroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,unsupported`
|
||||||
* (since API 28, prior to API 30) `Landroid/provider/Settings$Global;->SOFT_AP_TIMEOUT_ENABLED:Ljava/lang/String;,lo-prio,max-target-o`
|
* (prior to API 30) `Landroid/provider/Settings$Global;->SOFT_AP_TIMEOUT_ENABLED:Ljava/lang/String;,lo-prio,max-target-o`
|
||||||
* (prior to API 30) `Lcom/android/internal/R$array;->config_tether_bluetooth_regexs:I,max-target-q`
|
* (prior to API 30) `Lcom/android/internal/R$array;->config_tether_bluetooth_regexs:I,max-target-q`
|
||||||
* (prior to API 30) `Lcom/android/internal/R$array;->config_tether_usb_regexs:I,max-target-q`
|
* (prior to API 30) `Lcom/android/internal/R$array;->config_tether_usb_regexs:I,max-target-q`
|
||||||
* (prior to API 30) `Lcom/android/internal/R$array;->config_tether_wifi_regexs:I,max-target-q`
|
* (prior to API 30) `Lcom/android/internal/R$array;->config_tether_wifi_regexs:I,max-target-q`
|
||||||
* (on API 29) `Lcom/android/internal/R$bool;->config_wifi_p2p_mac_randomization_supported:I,blacklist`
|
* (on API 29) `Lcom/android/internal/R$bool;->config_wifi_p2p_mac_randomization_supported:I,blacklist`
|
||||||
* (since API 28, prior to API 30) `Lcom/android/internal/R$integer;->config_wifi_framework_soft_ap_timeout_delay:I,greylist-max-o`
|
* (prior to API 30) `Lcom/android/internal/R$integer;->config_wifi_framework_soft_ap_timeout_delay:I,greylist-max-o`
|
||||||
* `Lcom/android/internal/R$string;->config_ethernet_iface_regex:I,lo-prio,max-target-o`
|
* `Lcom/android/internal/R$string;->config_ethernet_iface_regex:I,lo-prio,max-target-o`
|
||||||
* (since API 27) `Lcom/android/server/connectivity/tethering/OffloadHardwareInterface;->DEFAULT_TETHER_OFFLOAD_DISABLED:I`
|
|
||||||
* (since API 30) `Lcom/android/server/wifi/WifiContext;->ACTION_RESOURCES_APK:Ljava/lang/String;`
|
* (since API 30) `Lcom/android/server/wifi/WifiContext;->ACTION_RESOURCES_APK:Ljava/lang/String;`
|
||||||
* (since API 29) `Lcom/android/server/wifi/p2p/WifiP2pServiceImpl;->ANONYMIZED_DEVICE_ADDRESS:Ljava/lang/String;`
|
* (since API 29) `Lcom/android/server/wifi/p2p/WifiP2pServiceImpl;->ANONYMIZED_DEVICE_ADDRESS:Ljava/lang/String;`
|
||||||
* (since API 30) `Lcom/android/server/SystemServer;->TETHERING_CONNECTOR_CLASS:Ljava/lang/String;`
|
* (since API 30) `Lcom/android/server/SystemServer;->TETHERING_CONNECTOR_CLASS:Ljava/lang/String;`
|
||||||
* (since API 29) `Ldalvik/system/VMDebug;->allowHiddenApiReflectionFrom(Ljava/lang/Class;)V,unsupported`
|
* `Ljava/lang/invoke/MethodHandles$Lookup;-><init>(Ljava/lang/Class;I)V,unsupported`
|
||||||
* (since API 26) `Ljava/lang/invoke/MethodHandles$Lookup;-><init>(Ljava/lang/Class;I)V,unsupported`
|
* `Ljava/lang/invoke/MethodHandles$Lookup;->ALL_MODES:I,lo-prio,max-target-o`
|
||||||
* (since API 26) `Ljava/lang/invoke/MethodHandles$Lookup;->ALL_MODES:I,lo-prio,max-target-o`
|
|
||||||
* (prior to API 29) `Ljava/net/InetAddress;->parseNumericAddress(Ljava/lang/String;)Ljava/net/InetAddress;,core-platform-api,max-target-p`
|
* (prior to API 29) `Ljava/net/InetAddress;->parseNumericAddress(Ljava/lang/String;)Ljava/net/InetAddress;,core-platform-api,max-target-p`
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Hidden whitelisted APIs: (same catch as above, however, things in this list are less likely to be broken)</summary>
|
<summary>Hidden whitelisted APIs: (same catch as above, however, things in this list are less likely to be broken)</summary>
|
||||||
|
|
||||||
* (since API 24) `Landroid/bluetooth/BluetoothPan;->isTetheringOn()Z,sdk,system-api,test-api`
|
* `Landroid/bluetooth/BluetoothPan;->isTetheringOn()Z,sdk,system-api,test-api`
|
||||||
* (since API 24) `Landroid/bluetooth/BluetoothProfile;->PAN:I,sdk,system-api,test-api`
|
* `Landroid/bluetooth/BluetoothProfile;->PAN:I,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/content/Context;->TETHERING_SERVICE:Ljava/lang/String;,sdk,system-api,test-api`
|
* (since API 30) `Landroid/content/Context;->TETHERING_SERVICE:Ljava/lang/String;,sdk,system-api,test-api`
|
||||||
* (since API 24, prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;-><init>()V,sdk,system-api,test-api`
|
* (prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;-><init>()V,sdk,system-api,test-api`
|
||||||
* (since API 24, prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringFailed()V,sdk,system-api,test-api`
|
* (prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringFailed()V,sdk,system-api,test-api`
|
||||||
* (since API 24, prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringStarted()V,sdk,system-api,test-api`
|
* (prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringStarted()V,sdk,system-api,test-api`
|
||||||
* (since API 24, prior to API 30) `Landroid/net/ConnectivityManager;->startTethering(IZLandroid/net/ConnectivityManager$OnStartTetheringCallback;Landroid/os/Handler;)V,sdk,system-api,test-api`
|
* (prior to API 30) `Landroid/net/ConnectivityManager;->startTethering(IZLandroid/net/ConnectivityManager$OnStartTetheringCallback;Landroid/os/Handler;)V,sdk,system-api,test-api`
|
||||||
* (since API 24, prior to API 30) `Landroid/net/ConnectivityManager;->stopTethering(I)V,sdk,system-api,test-api`
|
* (prior to API 30) `Landroid/net/ConnectivityManager;->stopTethering(I)V,sdk,system-api,test-api`
|
||||||
* `Landroid/net/LinkProperties;->getAllInterfaceNames()Ljava/util/List;,sdk,system-api,test-api`
|
* `Landroid/net/LinkProperties;->getAllInterfaceNames()Ljava/util/List;,sdk,system-api,test-api`
|
||||||
* `Landroid/net/LinkProperties;->getAllRoutes()Ljava/util/List;,sdk,system-api,test-api`
|
* `Landroid/net/LinkProperties;->getAllRoutes()Ljava/util/List;,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/TetheringManager$StartTetheringCallback;->onTetheringFailed(I)V,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/TetheringManager$StartTetheringCallback;->onTetheringFailed(I)V,sdk,system-api,test-api`
|
||||||
@@ -214,14 +216,14 @@ Greylisted/blacklisted APIs or internal constants: (some constants are hardcoded
|
|||||||
* (since API 30) `Landroid/net/TetheringManager$TetheringRequest$Builder;->setExemptFromEntitlementCheck(Z)Landroid/net/TetheringManager$TetheringRequest$Builder;,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/TetheringManager$TetheringRequest$Builder;->setExemptFromEntitlementCheck(Z)Landroid/net/TetheringManager$TetheringRequest$Builder;,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/TetheringManager$TetheringRequest$Builder;->setShouldShowEntitlementUi(Z)Landroid/net/TetheringManager$TetheringRequest$Builder;,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/TetheringManager$TetheringRequest$Builder;->setShouldShowEntitlementUi(Z)Landroid/net/TetheringManager$TetheringRequest$Builder;,sdk,system-api,test-api`
|
||||||
* `Landroid/net/TetheringManager;->ACTION_TETHER_STATE_CHANGED:Ljava/lang/String;,sdk,system-api,test-api`
|
* `Landroid/net/TetheringManager;->ACTION_TETHER_STATE_CHANGED:Ljava/lang/String;,sdk,system-api,test-api`
|
||||||
* (since API 26) `Landroid/net/TetheringManager;->EXTRA_ACTIVE_LOCAL_ONLY:Ljava/lang/String;,sdk,system-api,test-api`
|
* `Landroid/net/TetheringManager;->EXTRA_ACTIVE_LOCAL_ONLY:Ljava/lang/String;,sdk,system-api,test-api`
|
||||||
* `Landroid/net/TetheringManager;->EXTRA_ACTIVE_TETHER:Ljava/lang/String;,sdk,system-api,test-api`
|
* `Landroid/net/TetheringManager;->EXTRA_ACTIVE_TETHER:Ljava/lang/String;,sdk,system-api,test-api`
|
||||||
* `Landroid/net/TetheringManager;->EXTRA_ERRORED_TETHER:Ljava/lang/String;,sdk,system-api,test-api`
|
* `Landroid/net/TetheringManager;->EXTRA_ERRORED_TETHER:Ljava/lang/String;,sdk,system-api,test-api`
|
||||||
* (since API 24) `Landroid/net/TetheringManager;->TETHERING_BLUETOOTH:I,sdk,system-api,test-api`
|
* `Landroid/net/TetheringManager;->TETHERING_BLUETOOTH:I,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/TetheringManager;->TETHERING_ETHERNET:I,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/TetheringManager;->TETHERING_ETHERNET:I,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/TetheringManager;->TETHERING_NCM:I,sdk,system-api,test-api`
|
* `Landroid/net/TetheringManager;->TETHERING_USB:I,sdk,system-api,test-api`
|
||||||
* (since API 24) `Landroid/net/TetheringManager;->TETHERING_USB:I,sdk,system-api,test-api`
|
* `Landroid/net/TetheringManager;->TETHERING_WIFI:I,sdk,system-api,test-api`
|
||||||
* (since API 24) `Landroid/net/TetheringManager;->TETHERING_WIFI:I,sdk,system-api,test-api`
|
* (since API 31) `Landroid/net/TetheringManager;->TETHERING_WIFI_P2P:I,sdk,system-api,test-api`
|
||||||
* `Landroid/net/TetheringManager;->TETHER_ERROR_*:I,sdk,system-api,test-api`
|
* `Landroid/net/TetheringManager;->TETHER_ERROR_*:I,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/TetheringManager;->TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION:I,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/TetheringManager;->TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION:I,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/TetheringManager;->TETHER_HARDWARE_OFFLOAD_FAILED:I,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/TetheringManager;->TETHER_HARDWARE_OFFLOAD_FAILED:I,sdk,system-api,test-api`
|
||||||
@@ -242,48 +244,63 @@ Greylisted/blacklisted APIs or internal constants: (some constants are hardcoded
|
|||||||
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;-><init>()V,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;-><init>()V,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;-><init>(Landroid/net/wifi/SoftApConfiguration;)V,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;-><init>(Landroid/net/wifi/SoftApConfiguration;)V,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->build()Landroid/net/wifi/SoftApConfiguration;,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->build()Landroid/net/wifi/SoftApConfiguration;,sdk,system-api,test-api`
|
||||||
|
* (since API 33) `Landroid/net/wifi/SoftApConfiguration$Builder;->setAllowedAcsChannels(I[I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setAllowedClientList(Ljava/util/List;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setAllowedClientList(Ljava/util/List;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setAutoShutdownEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setAutoShutdownEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
||||||
* (on API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBand(I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
* (on API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBand(I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBlockedClientList(Ljava/util/List;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBlockedClientList(Ljava/util/List;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
||||||
* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBridgedModeOpportunisticShutdownEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBridgedModeOpportunisticShutdownEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
||||||
|
* (since API 33) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBridgedModeOpportunisticShutdownTimeoutMillis(J)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBssid(Landroid/net/MacAddress;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBssid(Landroid/net/MacAddress;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
||||||
* (on API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setChannel(II)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
* (on API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setChannel(II)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
||||||
* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setChannels(Landroid/util/SparseIntArray;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setChannels(Landroid/util/SparseIntArray;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setClientControlByUserEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setClientControlByUserEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setHiddenSsid(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setHiddenSsid(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
||||||
* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setIeee80211axEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setIeee80211axEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
||||||
|
* (since API 33) `Landroid/net/wifi/SoftApConfiguration$Builder;->setIeee80211beEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
||||||
* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setMacRandomizationSetting(I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setMacRandomizationSetting(I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
||||||
|
* (since API 33) `Landroid/net/wifi/SoftApConfiguration$Builder;->setMaxChannelBandwidth(I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setMaxNumberOfClients(I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setMaxNumberOfClients(I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setPassphrase(Ljava/lang/String;I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setPassphrase(Ljava/lang/String;I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setShutdownTimeoutMillis(J)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setShutdownTimeoutMillis(J)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setSsid(Ljava/lang/String;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
* (since API 30, prior to API 33) `Landroid/net/wifi/SoftApConfiguration$Builder;->setSsid(Ljava/lang/String;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
||||||
|
* (since API 33) `Landroid/net/wifi/SoftApConfiguration$Builder;->setVendorElements(Ljava/util/List;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
||||||
|
* (since API 33) `Landroid/net/wifi/SoftApConfiguration$Builder;->setWifiSsid(Landroid/net/wifi/WifiSsid;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->BAND_2GHZ:I,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->BAND_2GHZ:I,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->BAND_5GHZ:I,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->BAND_5GHZ:I,sdk,system-api,test-api`
|
||||||
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->BAND_60GHZ:I,sdk,system-api,test-api`
|
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->BAND_60GHZ:I,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->BAND_6GHZ:I,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->BAND_6GHZ:I,sdk,system-api,test-api`
|
||||||
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->BAND_*:I,sdk,system-api,test-api`
|
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->BAND_*:I,sdk,system-api,test-api`
|
||||||
|
* (since API 33) `Landroid/net/wifi/SoftApConfiguration;->DEFAULT_TIMEOUT:J,sdk,system-api,test-api`
|
||||||
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->RANDOMIZATION_NONE:I,sdk,system-api,test-api`
|
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->RANDOMIZATION_NONE:I,sdk,system-api,test-api`
|
||||||
|
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->RANDOMIZATION_NON_PERSISTENT:I,sdk,system-api,test-api`
|
||||||
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->RANDOMIZATION_PERSISTENT:I,sdk,system-api,test-api`
|
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->RANDOMIZATION_PERSISTENT:I,sdk,system-api,test-api`
|
||||||
|
* (since API 33) `Landroid/net/wifi/SoftApConfiguration;->getAllowedAcsChannels(I)[I,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getAllowedClientList()Ljava/util/List;,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getAllowedClientList()Ljava/util/List;,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getBand()I,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getBand()I,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getBlockedClientList()Ljava/util/List;,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getBlockedClientList()Ljava/util/List;,sdk,system-api,test-api`
|
||||||
|
* (since API 33) `Landroid/net/wifi/SoftApConfiguration;->getBridgedModeOpportunisticShutdownTimeoutMillis()J,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getChannel()I,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getChannel()I,sdk,system-api,test-api`
|
||||||
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->getChannels()Landroid/util/SparseIntArray;,sdk,system-api,test-api`
|
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->getChannels()Landroid/util/SparseIntArray;,sdk,system-api,test-api`
|
||||||
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->getMacRandomizationSetting()I,sdk,system-api,test-api`
|
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->getMacRandomizationSetting()I,sdk,system-api,test-api`
|
||||||
|
* (since API 33) `Landroid/net/wifi/SoftApConfiguration;->getMaxChannelBandwidth()I,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getMaxNumberOfClients()I,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getMaxNumberOfClients()I,sdk,system-api,test-api`
|
||||||
|
* (since API 33) `Landroid/net/wifi/SoftApConfiguration;->getPersistentRandomizedMacAddress()Landroid/net/MacAddress;,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getShutdownTimeoutMillis()J,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getShutdownTimeoutMillis()J,sdk,system-api,test-api`
|
||||||
|
* (since API 33) `Landroid/net/wifi/SoftApConfiguration;->getVendorElements()Ljava/util/List;,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->isAutoShutdownEnabled()Z,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->isAutoShutdownEnabled()Z,sdk,system-api,test-api`
|
||||||
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->isBridgedModeOpportunisticShutdownEnabled()Z,sdk,system-api,test-api`
|
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->isBridgedModeOpportunisticShutdownEnabled()Z,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->isClientControlByUserEnabled()Z,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->isClientControlByUserEnabled()Z,sdk,system-api,test-api`
|
||||||
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->isIeee80211axEnabled()Z,sdk,system-api,test-api`
|
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->isIeee80211axEnabled()Z,sdk,system-api,test-api`
|
||||||
|
* (since API 33) `Landroid/net/wifi/SoftApConfiguration;->isIeee80211beEnabled()Z,sdk,system-api,test-api`
|
||||||
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->isUserConfiguration()Z,sdk,system-api,test-api`
|
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->isUserConfiguration()Z,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApInfo;->CHANNEL_WIDTH_*:I,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/SoftApInfo;->CHANNEL_WIDTH_*:I,sdk,system-api,test-api`
|
||||||
|
* (since API 33) `Landroid/net/wifi/SoftApInfo;->CHANNEL_WIDTH_AUTO:I,sdk,system-api,test-api`
|
||||||
* (on API 30) `Landroid/net/wifi/SoftApInfo;->CHANNEL_WIDTH_INVALID:I,sdk,system-api,test-api`
|
* (on API 30) `Landroid/net/wifi/SoftApInfo;->CHANNEL_WIDTH_INVALID:I,sdk,system-api,test-api`
|
||||||
* (since API 31) `Landroid/net/wifi/SoftApInfo;->getAutoShutdownTimeoutMillis()J,sdk,system-api,test-api`
|
* (since API 31) `Landroid/net/wifi/SoftApInfo;->getAutoShutdownTimeoutMillis()J,sdk,system-api,test-api`
|
||||||
|
* (since API 30) `Landroid/net/wifi/SoftApInfo;->getBandwidth()I,sdk,system-api,test-api`
|
||||||
* (since API 31) `Landroid/net/wifi/SoftApInfo;->getBssid()Landroid/net/MacAddress;,sdk,system-api,test-api`
|
* (since API 31) `Landroid/net/wifi/SoftApInfo;->getBssid()Landroid/net/MacAddress;,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApInfo;->getBandwidth()I,system-api,whitelist`
|
* (since API 30) `Landroid/net/wifi/SoftApInfo;->getFrequency()I,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/SoftApInfo;->getFrequency()I,system-api,whitelist`
|
|
||||||
* (since API 31) `Landroid/net/wifi/SoftApInfo;->getWifiStandard()I,sdk,system-api,test-api`
|
* (since API 31) `Landroid/net/wifi/SoftApInfo;->getWifiStandard()I,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/WifiClient;->getMacAddress()Landroid/net/MacAddress;,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/WifiClient;->getMacAddress()Landroid/net/MacAddress;,sdk,system-api,test-api`
|
||||||
* (prior to API 30) `Landroid/net/wifi/WifiConfiguration$KeyMgmt;->WPA2_PSK:I,sdk,system-api,test-api`
|
* (prior to API 30) `Landroid/net/wifi/WifiConfiguration$KeyMgmt;->WPA2_PSK:I,sdk,system-api,test-api`
|
||||||
@@ -292,18 +309,26 @@ Greylisted/blacklisted APIs or internal constants: (some constants are hardcoded
|
|||||||
* (since API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onConnectedClientsChanged(Ljava/util/List;)V,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onConnectedClientsChanged(Ljava/util/List;)V,sdk,system-api,test-api`
|
||||||
* (on API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onInfoChanged(Landroid/net/wifi/SoftApInfo;)V,sdk,system-api,test-api`
|
* (on API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onInfoChanged(Landroid/net/wifi/SoftApInfo;)V,sdk,system-api,test-api`
|
||||||
* (since API 31) `Landroid/net/wifi/WifiManager$SoftApCallback;->onInfoChanged(Ljava/util/List;)V,sdk,system-api,test-api`
|
* (since API 31) `Landroid/net/wifi/WifiManager$SoftApCallback;->onInfoChanged(Ljava/util/List;)V,sdk,system-api,test-api`
|
||||||
* (since API 28) `Landroid/net/wifi/WifiManager$SoftApCallback;->onStateChanged(II)V,sdk,system-api,test-api`
|
* `Landroid/net/wifi/WifiManager$SoftApCallback;->onStateChanged(II)V,sdk,system-api,test-api`
|
||||||
|
* `Landroid/net/wifi/WifiManager;->EXTRA_WIFI_AP_FAILURE_REASON:Ljava/lang/String;,sdk,system-api,test-api`
|
||||||
|
* `Landroid/net/wifi/WifiManager;->EXTRA_WIFI_AP_INTERFACE_NAME:Ljava/lang/String;,sdk,system-api,test-api`
|
||||||
|
* `Landroid/net/wifi/WifiManager;->EXTRA_WIFI_AP_STATE:Ljava/lang/String;,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/WifiManager;->SAP_CLIENT_BLOCK_REASON_CODE_*:I,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/WifiManager;->SAP_CLIENT_BLOCK_REASON_CODE_*:I,sdk,system-api,test-api`
|
||||||
* (since API 28) `Landroid/net/wifi/WifiManager;->SAP_START_FAILURE_*:I,sdk,system-api,test-api`
|
* `Landroid/net/wifi/WifiManager;->SAP_START_FAILURE_*:I,sdk,system-api,test-api`
|
||||||
* (since API 28) `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_FAILED:I,sdk,system-api,test-api`
|
* `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_CHANGED_ACTION:Ljava/lang/String;,sdk,system-api,test-api`
|
||||||
|
* `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_DISABLED:I,sdk,system-api,test-api`
|
||||||
|
* `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_DISABLING:I,sdk,system-api,test-api`
|
||||||
|
* `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_ENABLED:I,sdk,system-api,test-api`
|
||||||
|
* `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_ENABLING:I,sdk,system-api,test-api`
|
||||||
|
* `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_FAILED:I,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/WifiManager;->getSoftApConfiguration()Landroid/net/wifi/SoftApConfiguration;,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/WifiManager;->getSoftApConfiguration()Landroid/net/wifi/SoftApConfiguration;,sdk,system-api,test-api`
|
||||||
* (prior to API 30) `Landroid/net/wifi/WifiManager;->getWifiApConfiguration()Landroid/net/wifi/WifiConfiguration;,sdk,system-api,test-api`
|
* (prior to API 30) `Landroid/net/wifi/WifiManager;->getWifiApConfiguration()Landroid/net/wifi/WifiConfiguration;,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/WifiManager;->isApMacRandomizationSupported()Z,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/WifiManager;->isApMacRandomizationSupported()Z,sdk,system-api,test-api`
|
||||||
* (since API 28) `Landroid/net/wifi/WifiManager;->registerSoftApCallback(Ljava/util/concurrent/Executor;Landroid/net/wifi/WifiManager$SoftApCallback;)V,sdk,system-api,test-api`
|
* `Landroid/net/wifi/WifiManager;->registerSoftApCallback(Ljava/util/concurrent/Executor;Landroid/net/wifi/WifiManager$SoftApCallback;)V,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/WifiManager;->setSoftApConfiguration(Landroid/net/wifi/SoftApConfiguration;)Z,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/WifiManager;->setSoftApConfiguration(Landroid/net/wifi/SoftApConfiguration;)Z,sdk,system-api,test-api`
|
||||||
* (prior to API 30) `Landroid/net/wifi/WifiManager;->setWifiApConfiguration(Landroid/net/wifi/WifiConfiguration;)Z,sdk,system-api,test-api`
|
* (prior to API 30) `Landroid/net/wifi/WifiManager;->setWifiApConfiguration(Landroid/net/wifi/WifiConfiguration;)Z,sdk,system-api,test-api`
|
||||||
* (since API 30) `Landroid/net/wifi/WifiManager;->startLocalOnlyHotspot(Landroid/net/wifi/SoftApConfiguration;Ljava/util/concurrent/Executor;Landroid/net/wifi/WifiManager$LocalOnlyHotspotCallback;)V,sdk,system-api,test-api`
|
* (since API 30) `Landroid/net/wifi/WifiManager;->startLocalOnlyHotspot(Landroid/net/wifi/SoftApConfiguration;Ljava/util/concurrent/Executor;Landroid/net/wifi/WifiManager$LocalOnlyHotspotCallback;)V,sdk,system-api,test-api`
|
||||||
* (since API 28) `Landroid/net/wifi/WifiManager;->unregisterSoftApCallback(Landroid/net/wifi/WifiManager$SoftApCallback;)V,sdk,system-api,test-api`
|
* `Landroid/net/wifi/WifiManager;->unregisterSoftApCallback(Landroid/net/wifi/WifiManager$SoftApCallback;)V,sdk,system-api,test-api`
|
||||||
* `Landroid/net/wifi/p2p/WifiP2pGroupList;->getGroupList()Ljava/util/List;,sdk,system-api,test-api`
|
* `Landroid/net/wifi/p2p/WifiP2pGroupList;->getGroupList()Ljava/util/List;,sdk,system-api,test-api`
|
||||||
* `Landroid/net/wifi/p2p/WifiP2pManager$PersistentGroupInfoListener;->onPersistentGroupInfoAvailable(Landroid/net/wifi/p2p/WifiP2pGroupList;)V,sdk,system-api,test-api`
|
* `Landroid/net/wifi/p2p/WifiP2pManager$PersistentGroupInfoListener;->onPersistentGroupInfoAvailable(Landroid/net/wifi/p2p/WifiP2pGroupList;)V,sdk,system-api,test-api`
|
||||||
* `Landroid/net/wifi/p2p/WifiP2pManager;->deletePersistentGroup(Landroid/net/wifi/p2p/WifiP2pManager$Channel;ILandroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,sdk,system-api,test-api`
|
* `Landroid/net/wifi/p2p/WifiP2pManager;->deletePersistentGroup(Landroid/net/wifi/p2p/WifiP2pManager$Channel;ILandroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,sdk,system-api,test-api`
|
||||||
@@ -322,20 +347,20 @@ Nonexported system resources:
|
|||||||
* (since API 30) `@com.android.networkstack.tethering:array/config_tether_wifi_regexs`
|
* (since API 30) `@com.android.networkstack.tethering:array/config_tether_wifi_regexs`
|
||||||
* (since API 30) `@com.android.networkstack.tethering:array/config_tether_wigig_regexs`
|
* (since API 30) `@com.android.networkstack.tethering:array/config_tether_wigig_regexs`
|
||||||
* (since API 30) `@com.android.wifi.resources:bool/config_wifi_p2p_mac_randomization_supported`
|
* (since API 30) `@com.android.wifi.resources:bool/config_wifi_p2p_mac_randomization_supported`
|
||||||
|
* (since API 31) `@com.android.wifi.resources:integer/config_wifiFrameworkSoftApShutDownIdleInstanceInBridgedModeTimeoutMillisecond`
|
||||||
* (since API 30) `@com.android.wifi.resources:integer/config_wifiFrameworkSoftApShutDownTimeoutMilliseconds`
|
* (since API 30) `@com.android.wifi.resources:integer/config_wifiFrameworkSoftApShutDownTimeoutMilliseconds`
|
||||||
|
|
||||||
Other: Activity `com.android.settings/.Settings$TetherSettingsActivity` is assumed to be exported.
|
Other: Activity `com.android.settings/.Settings$TetherSettingsActivity` is assumed to be exported.
|
||||||
|
|
||||||
For `ip rule` priorities, `RULE_PRIORITY_SECURE_VPN` and `RULE_PRIORITY_TETHERING` is assumed to be 12000 and 18000 respectively;
|
For `ip rule` priorities, `RULE_PRIORITY_SECURE_VPN` and `RULE_PRIORITY_TETHERING` is assumed to be 12000 and 18000 respectively;
|
||||||
(prior to API 24) `RULE_PRIORITY_DEFAULT_NETWORK` is assumed to be 22000 (or at least > 18000).
|
|
||||||
DHCP server like `dnsmasq` is assumed to run and send DHCP packets as root.
|
DHCP server like `dnsmasq` is assumed to run and send DHCP packets as root.
|
||||||
|
|
||||||
Undocumented system binaries are all bundled and executable:
|
Undocumented system binaries are all bundled and executable:
|
||||||
|
|
||||||
* (since API 24) `iptables-save`, `ip6tables-save`;
|
* `iptables-save`, `ip6tables-save`;
|
||||||
* `echo`;
|
* `echo`;
|
||||||
* `/system/bin/ip` (`monitor neigh rule unreachable`);
|
* `/system/bin/ip` (`monitor neigh rule unreachable`);
|
||||||
* `ndc` (`ipfwd` since API 23, `nat` since API 28);
|
* `ndc` (`ipfwd nat`);
|
||||||
* `iptables`, `ip6tables` (with correct version corresponding to API level, `-nvx -L <chain>`);
|
* `iptables`, `ip6tables` (with correct version corresponding to API level, `-nvx -L <chain>`);
|
||||||
* `sh`;
|
* `sh`;
|
||||||
* `su`.
|
* `su`.
|
||||||
|
|||||||
@@ -1,31 +1,13 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("com.github.ben-manes.versions") version "0.39.0"
|
id("com.android.application") version "8.0.0-beta04" apply false
|
||||||
|
id("com.github.ben-manes.versions") version "0.46.0"
|
||||||
|
id("org.jetbrains.kotlin.android") version "1.8.10" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath(kotlin("gradle-plugin", "1.5.10"))
|
classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.4")
|
||||||
classpath("com.android.tools.build:gradle:7.0.0-beta03")
|
classpath("com.google.android.gms:oss-licenses-plugin:0.10.6")
|
||||||
classpath("com.google.firebase:firebase-crashlytics-gradle:2.6.1")
|
classpath("com.google.gms:google-services:4.3.15")
|
||||||
classpath("com.google.android.gms:oss-licenses-plugin:0.10.4")
|
|
||||||
classpath("com.google.gms:google-services:4.3.8")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
jcenter()
|
|
||||||
mavenCentral()
|
|
||||||
maven("https://jitpack.io")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<Delete>("clean") {
|
|
||||||
delete(rootProject.buildDir)
|
|
||||||
}
|
|
||||||
|
|||||||
153
detekt.yml
153
detekt.yml
@@ -1,4 +1,4 @@
|
|||||||
# https://github.com/detekt/detekt/blob/v1.14.2/detekt-core/src/main/resources/default-detekt-config.yml
|
# https://github.com/detekt/detekt/blob/v1.19.0/detekt-core/src/main/resources/default-detekt-config.yml
|
||||||
|
|
||||||
comments:
|
comments:
|
||||||
active: false
|
active: false
|
||||||
@@ -19,6 +19,16 @@ complexity:
|
|||||||
ignoreSingleWhenExpression: false
|
ignoreSingleWhenExpression: false
|
||||||
ignoreSimpleWhenEntries: false
|
ignoreSimpleWhenEntries: false
|
||||||
ignoreNestingFunctions: false
|
ignoreNestingFunctions: false
|
||||||
|
nestingFunctions:
|
||||||
|
- 'also'
|
||||||
|
- 'apply'
|
||||||
|
- 'forEach'
|
||||||
|
- 'isNotNull'
|
||||||
|
- 'ifNull'
|
||||||
|
- 'let'
|
||||||
|
- 'run'
|
||||||
|
- 'use'
|
||||||
|
- 'with'
|
||||||
LabeledExpression:
|
LabeledExpression:
|
||||||
active: false
|
active: false
|
||||||
LargeClass:
|
LargeClass:
|
||||||
@@ -33,9 +43,11 @@ complexity:
|
|||||||
constructorThreshold: 7
|
constructorThreshold: 7
|
||||||
ignoreDefaultParameters: true
|
ignoreDefaultParameters: true
|
||||||
ignoreDataClasses: true
|
ignoreDataClasses: true
|
||||||
ignoreAnnotated: []
|
ignoreAnnotatedParameter: []
|
||||||
MethodOverloading:
|
MethodOverloading:
|
||||||
active: false
|
active: false
|
||||||
|
NamedArguments:
|
||||||
|
active: false
|
||||||
NestedBlockDepth:
|
NestedBlockDepth:
|
||||||
active: true
|
active: true
|
||||||
threshold: 4
|
threshold: 4
|
||||||
@@ -64,8 +76,12 @@ coroutines:
|
|||||||
active: true
|
active: true
|
||||||
GlobalCoroutineUsage:
|
GlobalCoroutineUsage:
|
||||||
active: false
|
active: false
|
||||||
|
InjectDispatcher:
|
||||||
|
active: false
|
||||||
RedundantSuspendModifier:
|
RedundantSuspendModifier:
|
||||||
active: true
|
active: true
|
||||||
|
SleepInsteadOfDelay:
|
||||||
|
active: true
|
||||||
SuspendFunWithFlowReturnType:
|
SuspendFunWithFlowReturnType:
|
||||||
active: true
|
active: true
|
||||||
|
|
||||||
@@ -108,13 +124,19 @@ exceptions:
|
|||||||
active: true
|
active: true
|
||||||
ExceptionRaisedInUnexpectedLocation:
|
ExceptionRaisedInUnexpectedLocation:
|
||||||
active: true
|
active: true
|
||||||
methodNames: [toString, hashCode, equals, finalize]
|
methodNames:
|
||||||
|
- 'equals'
|
||||||
|
- 'finalize'
|
||||||
|
- 'hashCode'
|
||||||
|
- 'toString'
|
||||||
InstanceOfCheckForException:
|
InstanceOfCheckForException:
|
||||||
active: false
|
active: false
|
||||||
NotImplementedDeclaration:
|
NotImplementedDeclaration:
|
||||||
active: true
|
active: true
|
||||||
|
ObjectExtendsThrowable:
|
||||||
|
active: true
|
||||||
PrintStackTrace:
|
PrintStackTrace:
|
||||||
active: false
|
active: true
|
||||||
RethrowCaughtException:
|
RethrowCaughtException:
|
||||||
active: false
|
active: false
|
||||||
ReturnFromFinally:
|
ReturnFromFinally:
|
||||||
@@ -123,10 +145,10 @@ exceptions:
|
|||||||
SwallowedException:
|
SwallowedException:
|
||||||
active: true
|
active: true
|
||||||
ignoredExceptionTypes:
|
ignoredExceptionTypes:
|
||||||
- InterruptedException
|
- 'InterruptedException'
|
||||||
- NumberFormatException
|
- 'MalformedURLException'
|
||||||
- ParseException
|
- 'NumberFormatException'
|
||||||
- MalformedURLException
|
- 'ParseException'
|
||||||
allowedExceptionNameRegex: '_|(ignore|expected).*'
|
allowedExceptionNameRegex: '_|(ignore|expected).*'
|
||||||
ThrowingExceptionFromFinally:
|
ThrowingExceptionFromFinally:
|
||||||
active: false
|
active: false
|
||||||
@@ -136,9 +158,15 @@ exceptions:
|
|||||||
active: true
|
active: true
|
||||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||||
exceptions:
|
exceptions:
|
||||||
- IllegalArgumentException
|
- 'ArrayIndexOutOfBoundsException'
|
||||||
- IllegalStateException
|
- 'Exception'
|
||||||
- IOException
|
- 'IllegalArgumentException'
|
||||||
|
- 'IllegalMonitorStateException'
|
||||||
|
- 'IllegalStateException'
|
||||||
|
- 'IndexOutOfBoundsException'
|
||||||
|
- 'NullPointerException'
|
||||||
|
- 'RuntimeException'
|
||||||
|
- 'Throwable'
|
||||||
ThrowingNewInstanceOfSameException:
|
ThrowingNewInstanceOfSameException:
|
||||||
active: true
|
active: true
|
||||||
TooGenericExceptionCaught:
|
TooGenericExceptionCaught:
|
||||||
@@ -146,10 +174,10 @@ exceptions:
|
|||||||
TooGenericExceptionThrown:
|
TooGenericExceptionThrown:
|
||||||
active: true
|
active: true
|
||||||
exceptionNames:
|
exceptionNames:
|
||||||
- Error
|
- 'Error'
|
||||||
- Exception
|
- 'Exception'
|
||||||
- Throwable
|
- 'RuntimeException'
|
||||||
- RuntimeException
|
- 'Throwable'
|
||||||
|
|
||||||
formatting:
|
formatting:
|
||||||
active: true
|
active: true
|
||||||
@@ -179,11 +207,13 @@ formatting:
|
|||||||
ImportOrdering:
|
ImportOrdering:
|
||||||
active: true
|
active: true
|
||||||
autoCorrect: true
|
autoCorrect: true
|
||||||
layout: 'idea'
|
layout: '*,java.**,javax.**,kotlin.**,^'
|
||||||
Indentation:
|
Indentation:
|
||||||
active: false
|
active: false
|
||||||
MaximumLineLength:
|
MaximumLineLength:
|
||||||
active: false
|
active: true
|
||||||
|
maxLineLength: 120
|
||||||
|
ignoreBackTickedIdentifier: false
|
||||||
ModifierOrdering:
|
ModifierOrdering:
|
||||||
active: true
|
active: true
|
||||||
autoCorrect: true
|
autoCorrect: true
|
||||||
@@ -229,6 +259,9 @@ formatting:
|
|||||||
autoCorrect: true
|
autoCorrect: true
|
||||||
ParameterListWrapping:
|
ParameterListWrapping:
|
||||||
active: false
|
active: false
|
||||||
|
SpacingAroundAngleBrackets:
|
||||||
|
active: true
|
||||||
|
autoCorrect: true
|
||||||
SpacingAroundColon:
|
SpacingAroundColon:
|
||||||
active: true
|
active: true
|
||||||
autoCorrect: true
|
autoCorrect: true
|
||||||
@@ -256,6 +289,9 @@ formatting:
|
|||||||
SpacingAroundRangeOperator:
|
SpacingAroundRangeOperator:
|
||||||
active: true
|
active: true
|
||||||
autoCorrect: true
|
autoCorrect: true
|
||||||
|
SpacingAroundUnaryOperator:
|
||||||
|
active: true
|
||||||
|
autoCorrect: true
|
||||||
SpacingBetweenDeclarationsWithAnnotations:
|
SpacingBetweenDeclarationsWithAnnotations:
|
||||||
active: false
|
active: false
|
||||||
SpacingBetweenDeclarationsWithComments:
|
SpacingBetweenDeclarationsWithComments:
|
||||||
@@ -266,6 +302,8 @@ formatting:
|
|||||||
|
|
||||||
naming:
|
naming:
|
||||||
active: true
|
active: true
|
||||||
|
BooleanPropertyNaming:
|
||||||
|
active: false
|
||||||
ClassNaming:
|
ClassNaming:
|
||||||
active: true
|
active: true
|
||||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||||
@@ -297,7 +335,6 @@ naming:
|
|||||||
functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)'
|
functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)'
|
||||||
excludeClassPattern: '$^'
|
excludeClassPattern: '$^'
|
||||||
ignoreOverridden: true
|
ignoreOverridden: true
|
||||||
ignoreAnnotated: ['Composable']
|
|
||||||
FunctionParameterNaming:
|
FunctionParameterNaming:
|
||||||
active: true
|
active: true
|
||||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||||
@@ -307,11 +344,17 @@ naming:
|
|||||||
InvalidPackageDeclaration:
|
InvalidPackageDeclaration:
|
||||||
active: true
|
active: true
|
||||||
rootPackage: ''
|
rootPackage: ''
|
||||||
|
LambdaParameterNaming:
|
||||||
|
active: true
|
||||||
|
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||||
|
parameterPattern: '[a-z][A-Za-z0-9]*|_'
|
||||||
MatchingDeclarationName:
|
MatchingDeclarationName:
|
||||||
active: true
|
active: true
|
||||||
mustBeFirst: true
|
mustBeFirst: true
|
||||||
MemberNameEqualsClassName:
|
MemberNameEqualsClassName:
|
||||||
active: false
|
active: false
|
||||||
|
NoNameShadowing:
|
||||||
|
active: true
|
||||||
NonBooleanPropertyPrefixedWithIs:
|
NonBooleanPropertyPrefixedWithIs:
|
||||||
active: true
|
active: true
|
||||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||||
@@ -327,7 +370,7 @@ naming:
|
|||||||
packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
|
packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
|
||||||
TopLevelPropertyNaming:
|
TopLevelPropertyNaming:
|
||||||
active: true
|
active: true
|
||||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
excludes: ['buildSrc/**', '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||||
constantPattern: '[A-Z][_A-Z0-9]*'
|
constantPattern: '[A-Z][_A-Z0-9]*'
|
||||||
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
|
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
|
||||||
privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
|
privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
|
||||||
@@ -359,14 +402,26 @@ performance:
|
|||||||
|
|
||||||
potential-bugs:
|
potential-bugs:
|
||||||
active: true
|
active: true
|
||||||
|
AvoidReferentialEquality:
|
||||||
|
active: true
|
||||||
|
forbiddenTypePatterns:
|
||||||
|
- 'kotlin.String'
|
||||||
|
CastToNullableType:
|
||||||
|
active: false
|
||||||
Deprecation:
|
Deprecation:
|
||||||
active: true
|
active: true
|
||||||
|
DontDowncastCollectionTypes:
|
||||||
|
active: true
|
||||||
|
DoubleMutabilityForCollection:
|
||||||
|
active: true
|
||||||
DuplicateCaseInWhenExpression:
|
DuplicateCaseInWhenExpression:
|
||||||
active: true
|
active: true
|
||||||
EqualsAlwaysReturnsTrueOrFalse:
|
EqualsAlwaysReturnsTrueOrFalse:
|
||||||
active: true
|
active: true
|
||||||
EqualsWithHashCodeExist:
|
EqualsWithHashCodeExist:
|
||||||
active: true
|
active: true
|
||||||
|
ExitOutsideMain:
|
||||||
|
active: true
|
||||||
ExplicitGarbageCollectionCall:
|
ExplicitGarbageCollectionCall:
|
||||||
active: true
|
active: true
|
||||||
HasPlatformType:
|
HasPlatformType:
|
||||||
@@ -374,7 +429,11 @@ potential-bugs:
|
|||||||
IgnoredReturnValue:
|
IgnoredReturnValue:
|
||||||
active: true
|
active: true
|
||||||
restrictToAnnotatedMethods: true
|
restrictToAnnotatedMethods: true
|
||||||
returnValueAnnotations: ['*.CheckReturnValue', '*.CheckResult']
|
returnValueAnnotations:
|
||||||
|
- '*.CheckResult'
|
||||||
|
- '*.CheckReturnValue'
|
||||||
|
ignoreReturnValueAnnotations:
|
||||||
|
- '*.CanIgnoreReturnValue'
|
||||||
ImplicitDefaultLocale:
|
ImplicitDefaultLocale:
|
||||||
active: true
|
active: true
|
||||||
ImplicitUnitReturnType:
|
ImplicitUnitReturnType:
|
||||||
@@ -389,6 +448,9 @@ potential-bugs:
|
|||||||
active: false
|
active: false
|
||||||
MapGetWithNotNullAssertionOperator:
|
MapGetWithNotNullAssertionOperator:
|
||||||
active: true
|
active: true
|
||||||
|
MissingPackageDeclaration:
|
||||||
|
active: true
|
||||||
|
excludes: ['buildSrc/**', '**/*.kts']
|
||||||
MissingWhenCase:
|
MissingWhenCase:
|
||||||
active: false
|
active: false
|
||||||
NullableToStringCall:
|
NullableToStringCall:
|
||||||
@@ -398,15 +460,20 @@ potential-bugs:
|
|||||||
UnconditionalJumpStatementInLoop:
|
UnconditionalJumpStatementInLoop:
|
||||||
active: true
|
active: true
|
||||||
UnnecessaryNotNullOperator:
|
UnnecessaryNotNullOperator:
|
||||||
active: false
|
active: true
|
||||||
UnnecessarySafeCall:
|
UnnecessarySafeCall:
|
||||||
active: false
|
active: true
|
||||||
|
UnreachableCatchBlock:
|
||||||
|
active: true
|
||||||
UnreachableCode:
|
UnreachableCode:
|
||||||
active: true
|
active: true
|
||||||
UnsafeCallOnNullableType:
|
UnsafeCallOnNullableType:
|
||||||
active: true
|
active: true
|
||||||
|
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||||
UnsafeCast:
|
UnsafeCast:
|
||||||
active: false
|
active: true
|
||||||
|
UnusedUnaryOperator:
|
||||||
|
active: true
|
||||||
UselessPostfixExpression:
|
UselessPostfixExpression:
|
||||||
active: true
|
active: true
|
||||||
WrongEqualsTypeParameter:
|
WrongEqualsTypeParameter:
|
||||||
@@ -422,6 +489,8 @@ style:
|
|||||||
active: false
|
active: false
|
||||||
DataClassShouldBeImmutable:
|
DataClassShouldBeImmutable:
|
||||||
active: false
|
active: false
|
||||||
|
DestructuringDeclarationWithTooManyEntries:
|
||||||
|
active: false
|
||||||
EqualsNullCall:
|
EqualsNullCall:
|
||||||
active: true
|
active: true
|
||||||
EqualsOnSignatureLine:
|
EqualsOnSignatureLine:
|
||||||
@@ -435,18 +504,27 @@ style:
|
|||||||
includeLineWrapping: false
|
includeLineWrapping: false
|
||||||
ForbiddenComment:
|
ForbiddenComment:
|
||||||
active: true
|
active: true
|
||||||
values: ['TODO:', 'FIXME:', 'STOPSHIP:']
|
values:
|
||||||
|
- 'FIXME:'
|
||||||
|
- 'STOPSHIP:'
|
||||||
|
- 'TODO:'
|
||||||
allowedPatterns: ''
|
allowedPatterns: ''
|
||||||
|
customMessage: ''
|
||||||
ForbiddenImport:
|
ForbiddenImport:
|
||||||
active: true
|
active: true
|
||||||
imports: []
|
imports: []
|
||||||
forbiddenPatterns: ''
|
forbiddenPatterns: ''
|
||||||
ForbiddenMethodCall:
|
ForbiddenMethodCall:
|
||||||
active: true
|
active: true
|
||||||
methods: ['kotlin.io.println', 'kotlin.io.print']
|
methods:
|
||||||
|
- 'kotlin.io.print'
|
||||||
|
- 'kotlin.io.println'
|
||||||
ForbiddenPublicDataClass:
|
ForbiddenPublicDataClass:
|
||||||
active: true
|
active: true
|
||||||
ignorePackages: ['*.internal', '*.internal.*']
|
excludes: ['**']
|
||||||
|
ignorePackages:
|
||||||
|
- '*.internal'
|
||||||
|
- '*.internal.*'
|
||||||
ForbiddenVoid:
|
ForbiddenVoid:
|
||||||
active: true
|
active: true
|
||||||
ignoreOverridden: true
|
ignoreOverridden: true
|
||||||
@@ -454,12 +532,14 @@ style:
|
|||||||
FunctionOnlyReturningConstant:
|
FunctionOnlyReturningConstant:
|
||||||
active: true
|
active: true
|
||||||
ignoreOverridableFunction: true
|
ignoreOverridableFunction: true
|
||||||
excludedFunctions: 'describeContents'
|
ignoreActualFunction: true
|
||||||
excludeAnnotatedFunction: ['dagger.Provides']
|
excludedFunctions: ''
|
||||||
LibraryCodeMustSpecifyReturnType:
|
LibraryCodeMustSpecifyReturnType:
|
||||||
active: true
|
active: true
|
||||||
|
excludes: ['**']
|
||||||
LibraryEntitiesShouldNotBePublic:
|
LibraryEntitiesShouldNotBePublic:
|
||||||
active: true
|
active: true
|
||||||
|
excludes: ['**']
|
||||||
LoopWithTooManyJumpStatements:
|
LoopWithTooManyJumpStatements:
|
||||||
active: true
|
active: true
|
||||||
maxJumpCount: 1
|
maxJumpCount: 1
|
||||||
@@ -479,12 +559,16 @@ style:
|
|||||||
active: true
|
active: true
|
||||||
ModifierOrder:
|
ModifierOrder:
|
||||||
active: true
|
active: true
|
||||||
|
MultilineLambdaItParameter:
|
||||||
|
active: false
|
||||||
NestedClassesVisibility:
|
NestedClassesVisibility:
|
||||||
active: true
|
active: true
|
||||||
NewLineAtEndOfFile:
|
NewLineAtEndOfFile:
|
||||||
active: true
|
active: true
|
||||||
NoTabs:
|
NoTabs:
|
||||||
active: true
|
active: true
|
||||||
|
ObjectLiteralToLambda:
|
||||||
|
active: true
|
||||||
OptionalAbstractKeyword:
|
OptionalAbstractKeyword:
|
||||||
active: true
|
active: true
|
||||||
OptionalUnit:
|
OptionalUnit:
|
||||||
@@ -497,6 +581,8 @@ style:
|
|||||||
active: true
|
active: true
|
||||||
RedundantExplicitType:
|
RedundantExplicitType:
|
||||||
active: true
|
active: true
|
||||||
|
RedundantHigherOrderMapUsage:
|
||||||
|
active: true
|
||||||
RedundantVisibilityModifierRule:
|
RedundantVisibilityModifierRule:
|
||||||
active: true
|
active: true
|
||||||
ReturnCount:
|
ReturnCount:
|
||||||
@@ -510,6 +596,7 @@ style:
|
|||||||
ThrowsCount:
|
ThrowsCount:
|
||||||
active: true
|
active: true
|
||||||
max: 2
|
max: 2
|
||||||
|
excludeGuardClauses: false
|
||||||
TrailingWhitespace:
|
TrailingWhitespace:
|
||||||
active: true
|
active: true
|
||||||
UnderscoresInNumericLiterals:
|
UnderscoresInNumericLiterals:
|
||||||
@@ -520,6 +607,8 @@ style:
|
|||||||
active: true
|
active: true
|
||||||
UnnecessaryApply:
|
UnnecessaryApply:
|
||||||
active: true
|
active: true
|
||||||
|
UnnecessaryFilter:
|
||||||
|
active: true
|
||||||
UnnecessaryInheritance:
|
UnnecessaryInheritance:
|
||||||
active: true
|
active: true
|
||||||
UnnecessaryLet:
|
UnnecessaryLet:
|
||||||
@@ -535,6 +624,8 @@ style:
|
|||||||
UnusedPrivateMember:
|
UnusedPrivateMember:
|
||||||
active: true
|
active: true
|
||||||
allowedNames: '(_|ignored|expected|serialVersionUID)'
|
allowedNames: '(_|ignored|expected|serialVersionUID)'
|
||||||
|
UseAnyOrNoneInsteadOfFind:
|
||||||
|
active: true
|
||||||
UseArrayLiteralsInAnnotations:
|
UseArrayLiteralsInAnnotations:
|
||||||
active: true
|
active: true
|
||||||
UseCheckNotNull:
|
UseCheckNotNull:
|
||||||
@@ -545,8 +636,14 @@ style:
|
|||||||
active: false
|
active: false
|
||||||
UseEmptyCounterpart:
|
UseEmptyCounterpart:
|
||||||
active: true
|
active: true
|
||||||
|
UseIfEmptyOrIfBlank:
|
||||||
|
active: true
|
||||||
UseIfInsteadOfWhen:
|
UseIfInsteadOfWhen:
|
||||||
active: false
|
active: false
|
||||||
|
UseIsNullOrEmpty:
|
||||||
|
active: true
|
||||||
|
UseOrEmpty:
|
||||||
|
active: true
|
||||||
UseRequire:
|
UseRequire:
|
||||||
active: true
|
active: true
|
||||||
UseRequireNotNull:
|
UseRequireNotNull:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ android.databinding.incremental=true
|
|||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
android.enableR8.fullMode=true
|
android.enableR8.fullMode=true
|
||||||
android.enableResourceOptimizations=false
|
android.enableResourceOptimizations=false
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
kapt.incremental.apt=true
|
kapt.incremental.apt=true
|
||||||
org.gradle.jvmargs=-Xmx1536m
|
org.gradle.jvmargs=-Xmx1536m
|
||||||
|
|||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
285
gradlew
vendored
285
gradlew
vendored
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env sh
|
#!/bin/sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright 2015 the original author or authors.
|
# Copyright © 2015-2021 the original authors.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -17,67 +17,101 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
##
|
#
|
||||||
## Gradle start up script for UN*X
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
##
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
# Attempt to set APP_HOME
|
||||||
# Resolve links: $0 may be a link
|
|
||||||
PRG="$0"
|
|
||||||
# Need this for relative symlinks.
|
|
||||||
while [ -h "$PRG" ] ; do
|
|
||||||
ls=`ls -ld "$PRG"`
|
|
||||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
|
||||||
if expr "$link" : '/.*' > /dev/null; then
|
|
||||||
PRG="$link"
|
|
||||||
else
|
|
||||||
PRG=`dirname "$PRG"`"/$link"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
SAVED="`pwd`"
|
|
||||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
|
||||||
APP_HOME="`pwd -P`"
|
|
||||||
cd "$SAVED" >/dev/null
|
|
||||||
|
|
||||||
APP_NAME="Gradle"
|
# Resolve links: $0 may be a link
|
||||||
APP_BASE_NAME=`basename "$0"`
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD="maximum"
|
MAX_FD=maximum
|
||||||
|
|
||||||
warn () {
|
warn () {
|
||||||
echo "$*"
|
echo "$*"
|
||||||
}
|
} >&2
|
||||||
|
|
||||||
die () {
|
die () {
|
||||||
echo
|
echo
|
||||||
echo "$*"
|
echo "$*"
|
||||||
echo
|
echo
|
||||||
exit 1
|
exit 1
|
||||||
}
|
} >&2
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
# OS specific support (must be 'true' or 'false').
|
||||||
cygwin=false
|
cygwin=false
|
||||||
msys=false
|
msys=false
|
||||||
darwin=false
|
darwin=false
|
||||||
nonstop=false
|
nonstop=false
|
||||||
case "`uname`" in
|
case "$( uname )" in #(
|
||||||
CYGWIN* )
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
cygwin=true
|
Darwin* ) darwin=true ;; #(
|
||||||
;;
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
Darwin* )
|
NONSTOP* ) nonstop=true ;;
|
||||||
darwin=true
|
|
||||||
;;
|
|
||||||
MINGW* )
|
|
||||||
msys=true
|
|
||||||
;;
|
|
||||||
NONSTOP* )
|
|
||||||
nonstop=true
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
@@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|||||||
if [ -n "$JAVA_HOME" ] ; then
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
else
|
else
|
||||||
JAVACMD="$JAVA_HOME/bin/java"
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
fi
|
fi
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
@@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
|
|||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
JAVACMD="java"
|
JAVACMD=java
|
||||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
@@ -106,80 +140,105 @@ location of your Java installation."
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
MAX_FD_LIMIT=`ulimit -H -n`
|
case $MAX_FD in #(
|
||||||
if [ $? -eq 0 ] ; then
|
max*)
|
||||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
MAX_FD="$MAX_FD_LIMIT"
|
# shellcheck disable=SC3045
|
||||||
fi
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
ulimit -n $MAX_FD
|
warn "Could not query maximum file descriptor limit"
|
||||||
if [ $? -ne 0 ] ; then
|
esac
|
||||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
case $MAX_FD in #(
|
||||||
fi
|
'' | soft) :;; #(
|
||||||
else
|
*)
|
||||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
fi
|
# shellcheck disable=SC3045
|
||||||
fi
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
# For Darwin, add options to specify how the application appears in the dock
|
|
||||||
if $darwin; then
|
|
||||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 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
|
|
||||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
|
||||||
SEP=""
|
|
||||||
for dir in $ROOTDIRSRAW ; do
|
|
||||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
|
||||||
SEP="|"
|
|
||||||
done
|
|
||||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
|
||||||
# Add a user-defined pattern to the cygpath arguments
|
|
||||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
|
||||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
|
||||||
fi
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
||||||
i=0
|
|
||||||
for arg in "$@" ; do
|
|
||||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
|
||||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
|
||||||
|
|
||||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
|
||||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
|
||||||
else
|
|
||||||
eval `echo args$i`="\"$arg\""
|
|
||||||
fi
|
|
||||||
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" ;;
|
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Escape application args
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
save () {
|
# * args from the command line
|
||||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
# * the main class name
|
||||||
echo " "
|
# * -classpath
|
||||||
}
|
# * -D...appname settings
|
||||||
APP_ARGS=`save "$@"`
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command;
|
||||||
|
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||||
|
# shell script including quotes and variable substitutions, so put them in
|
||||||
|
# double quotes to make sure that they get re-expanded; and
|
||||||
|
# * put everything else in single quotes, so that it's not re-expanded.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
exec "$JAVACMD" "$@"
|
||||||
|
|||||||
15
gradlew.bat
vendored
15
gradlew.bat
vendored
@@ -14,7 +14,7 @@
|
|||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
|
|
||||||
@if "%DEBUG%" == "" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@rem Gradle startup script for Windows
|
@rem Gradle startup script for Windows
|
||||||
@@ -25,7 +25,8 @@
|
|||||||
if "%OS%"=="Windows_NT" setlocal
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%" == "" set DIRNAME=.
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
|||||||
|
|
||||||
set JAVA_EXE=java.exe
|
set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if "%ERRORLEVEL%" == "0" goto execute
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
@@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
:fail
|
:fail
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
rem the _cmd.exe /c_ return code!
|
rem the _cmd.exe /c_ return code!
|
||||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
exit /b 1
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
:mainEnd
|
:mainEnd
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|||||||
@@ -9,23 +9,24 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
val javaVersion = JavaVersion.VERSION_1_8
|
namespace = "be.mygod.vpnhotspot"
|
||||||
val targetSdk = 29
|
|
||||||
buildToolsVersion = "31.0.0-rc4"
|
val javaVersion = 11
|
||||||
|
buildToolsVersion = "33.0.2"
|
||||||
compileOptions {
|
compileOptions {
|
||||||
isCoreLibraryDesugaringEnabled = true
|
isCoreLibraryDesugaringEnabled = true
|
||||||
sourceCompatibility = javaVersion
|
sourceCompatibility(javaVersion)
|
||||||
targetCompatibility = javaVersion
|
targetCompatibility(javaVersion)
|
||||||
}
|
}
|
||||||
compileSdkPreview = "android-S"
|
kotlin.jvmToolchain(javaVersion)
|
||||||
kotlinOptions.jvmTarget = javaVersion.toString()
|
compileSdk = 33
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "be.mygod.vpnhotspot"
|
applicationId = "be.mygod.vpnhotspot"
|
||||||
minSdk = 21
|
minSdk = 28
|
||||||
if (targetSdk == 31) targetSdkPreview = "S" else this.targetSdk = targetSdk
|
targetSdk = 33
|
||||||
resourceConfigurations.addAll(arrayOf("it", "ru", "zh-rCN", "zh-rTW"))
|
resourceConfigurations.addAll(arrayOf("it", "pt-rBR", "ru", "zh-rCN", "zh-rTW"))
|
||||||
versionCode = 260
|
versionCode = 1000
|
||||||
versionName = "2.11.7"
|
versionName = "2.16.0"
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
javaCompileOptions.annotationProcessorOptions.arguments.apply {
|
javaCompileOptions.annotationProcessorOptions.arguments.apply {
|
||||||
put("room.expandProjection", "true")
|
put("room.expandProjection", "true")
|
||||||
@@ -33,9 +34,9 @@ android {
|
|||||||
put("room.schemaLocation", "$projectDir/schemas")
|
put("room.schemaLocation", "$projectDir/schemas")
|
||||||
}
|
}
|
||||||
buildConfigField("boolean", "DONATIONS", "true")
|
buildConfigField("boolean", "DONATIONS", "true")
|
||||||
buildConfigField("int", "TARGET_SDK", targetSdk.toString())
|
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
|
buildConfig = true
|
||||||
dataBinding = true
|
dataBinding = true
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
}
|
}
|
||||||
@@ -57,6 +58,7 @@ android {
|
|||||||
}
|
}
|
||||||
create("google") {
|
create("google") {
|
||||||
dimension = "freedom"
|
dimension = "freedom"
|
||||||
|
versionNameSuffix = "-g"
|
||||||
buildConfigField("boolean", "DONATIONS", "false")
|
buildConfigField("boolean", "DONATIONS", "false")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,37 +66,39 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
val lifecycleVersion = "2.3.1"
|
val lifecycleVersion = "2.6.0-rc01"
|
||||||
val roomVersion = "2.3.0"
|
val roomVersion = "2.5.0"
|
||||||
|
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.2")
|
||||||
kapt("androidx.room:room-compiler:$roomVersion")
|
kapt("androidx.room:room-compiler:$roomVersion")
|
||||||
implementation(kotlin("stdlib-jdk8"))
|
implementation(kotlin("stdlib-jdk8"))
|
||||||
implementation("androidx.appcompat:appcompat:1.3.0") // https://issuetracker.google.com/issues/151603528
|
implementation("androidx.browser:browser:1.5.0")
|
||||||
implementation("androidx.browser:browser:1.3.0")
|
implementation("androidx.core:core-ktx:1.9.0")
|
||||||
implementation("androidx.core:core-ktx:1.6.0-beta01")
|
implementation("androidx.fragment:fragment-ktx:1.5.5")
|
||||||
implementation("androidx.emoji:emoji:1.1.0")
|
|
||||||
implementation("androidx.fragment:fragment-ktx:1.3.4")
|
|
||||||
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
|
|
||||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
||||||
implementation("androidx.preference:preference:1.1.1")
|
implementation("androidx.preference:preference:1.2.0")
|
||||||
implementation("androidx.room:room-ktx:$roomVersion")
|
implementation("androidx.room:room-ktx:$roomVersion")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
implementation("com.android.billingclient:billing-ktx:4.0.0")
|
implementation("be.mygod.librootkotlinx:librootkotlinx:1.0.1")
|
||||||
|
implementation("com.android.billingclient:billing-ktx:5.1.0")
|
||||||
|
implementation("com.github.tiann:FreeReflection:3.1.0")
|
||||||
|
implementation("com.google.android.gms:play-services-base:18.2.0") // fix for GoogleApiActivity crash
|
||||||
implementation("com.google.android.gms:play-services-oss-licenses:17.0.0")
|
implementation("com.google.android.gms:play-services-oss-licenses:17.0.0")
|
||||||
implementation("com.google.android.material:material:1.4.0-beta01")
|
implementation("com.google.android.material:material:1.8.0")
|
||||||
implementation("com.google.firebase:firebase-analytics-ktx:19.0.0")
|
implementation("com.google.firebase:firebase-analytics-ktx:21.2.0")
|
||||||
implementation("com.google.firebase:firebase-crashlytics:18.0.0")
|
implementation("com.google.firebase:firebase-crashlytics:18.3.5")
|
||||||
implementation("com.google.zxing:core:3.4.1")
|
implementation("com.google.zxing:core:3.5.1")
|
||||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
implementation("com.jakewharton.timber:timber:5.0.1")
|
||||||
implementation("com.linkedin.dexmaker:dexmaker:2.28.1")
|
implementation("com.linkedin.dexmaker:dexmaker:2.28.3")
|
||||||
implementation("com.takisoft.preferencex:preferencex-simplemenu:1.1.0")
|
implementation("com.takisoft.preferencex:preferencex-simplemenu:1.1.0")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.4")
|
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
|
||||||
|
add("googleImplementation", "com.google.android.play:core:1.10.3")
|
||||||
|
add("googleImplementation", "com.google.android.play:core-ktx:1.8.1")
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
androidTestImplementation("androidx.room:room-testing:$roomVersion")
|
androidTestImplementation("androidx.room:room-testing:$roomVersion")
|
||||||
androidTestImplementation("androidx.test:runner:1.3.0")
|
androidTestImplementation("androidx.test:runner:1.5.2")
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0")
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||||
androidTestImplementation("androidx.test.ext:junit-ktx:1.1.2")
|
androidTestImplementation("androidx.test.ext:junit-ktx:1.1.5")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,5 +4,6 @@
|
|||||||
<ignore regexp="org.mockito.*" />
|
<ignore regexp="org.mockito.*" />
|
||||||
</issue>
|
</issue>
|
||||||
<issue id="MissingTranslation" severity="ignore" />
|
<issue id="MissingTranslation" severity="ignore" />
|
||||||
|
<issue id="NewApi" severity="warning" />
|
||||||
<issue id="UseAppTint" severity="informational" />
|
<issue id="UseAppTint" severity="informational" />
|
||||||
</lint>
|
</lint>
|
||||||
|
|||||||
7
mobile/proguard-rules.pro
vendored
7
mobile/proguard-rules.pro
vendored
@@ -20,10 +20,3 @@
|
|||||||
# If you keep the line number information, uncomment this to
|
# If you keep the line number information, uncomment this to
|
||||||
# hide the original source file name.
|
# hide the original source file name.
|
||||||
#-renamesourcefileattribute SourceFile
|
#-renamesourcefileattribute SourceFile
|
||||||
|
|
||||||
-if public class be.mygod.librootkotlinx.RootServer {
|
|
||||||
private void doInit(android.content.Context, java.lang.String);
|
|
||||||
}
|
|
||||||
-keep class be.mygod.librootkotlinx.RootServer {
|
|
||||||
public static void main(java.lang.String[]);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -34,10 +34,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"primaryKey": {
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
"columnNames": [
|
"columnNames": [
|
||||||
"mac"
|
"mac"
|
||||||
],
|
]
|
||||||
"autoGenerate": false
|
|
||||||
},
|
},
|
||||||
"indices": [],
|
"indices": [],
|
||||||
"foreignKeys": []
|
"foreignKeys": []
|
||||||
@@ -114,10 +114,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"primaryKey": {
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
"columnNames": [
|
"columnNames": [
|
||||||
"id"
|
"id"
|
||||||
],
|
]
|
||||||
"autoGenerate": true
|
|
||||||
},
|
},
|
||||||
"indices": [
|
"indices": [
|
||||||
{
|
{
|
||||||
@@ -126,7 +126,8 @@
|
|||||||
"columnNames": [
|
"columnNames": [
|
||||||
"previousId"
|
"previousId"
|
||||||
],
|
],
|
||||||
"createSql": "CREATE UNIQUE INDEX `index_TrafficRecord_previousId` ON `${TABLE_NAME}` (`previousId`)"
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TrafficRecord_previousId` ON `${TABLE_NAME}` (`previousId`)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"foreignKeys": [
|
"foreignKeys": [
|
||||||
@@ -144,9 +145,10 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"views": [],
|
||||||
"setupQueries": [
|
"setupQueries": [
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"92a6c0406ed7265dbd98eb3c24095651\")"
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '92a6c0406ed7265dbd98eb3c24095651')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package be.mygod.vpnhotspot.util
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
|
import be.mygod.vpnhotspot.BuildConfig
|
||||||
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.ensureActive
|
||||||
|
import kotlinx.coroutines.flow.cancellable
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import org.json.JSONArray
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.IOException
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.concurrent.CancellationException
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
object UpdateChecker {
|
||||||
|
private const val KEY_LAST_FETCHED = "update.lastFetched"
|
||||||
|
private const val KEY_VERSION = "update.version"
|
||||||
|
private const val KEY_PUBLISHED = "update.published"
|
||||||
|
private const val UPDATE_INTERVAL = 1000 * 60 * 60 * 6
|
||||||
|
|
||||||
|
private data class GitHubUpdate(override val message: String, val published: Long) : AppUpdate {
|
||||||
|
override val stalenessDays get() = max(0,
|
||||||
|
TimeUnit.DAYS.convert(System.currentTimeMillis() - published, TimeUnit.MILLISECONDS)).toInt()
|
||||||
|
|
||||||
|
override fun updateForResult(activity: Activity, requestCode: Int) {
|
||||||
|
app.customTabsIntent.launchUrl(activity, Uri.parse("https://github.com/Mygod/VPNHotspot/releases"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class SemVer(val major: Int, val minor: Int, val revision: Int) : Comparable<SemVer> {
|
||||||
|
override fun compareTo(other: SemVer): Int {
|
||||||
|
var result = major - other.major
|
||||||
|
if (result != 0) return result
|
||||||
|
result = minor - other.minor
|
||||||
|
if (result != 0) return result
|
||||||
|
return revision - other.revision
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val semverParser = "^v?(\\d+)\\.(\\d+)\\.(\\d+)(?:-|$)".toPattern()
|
||||||
|
private fun CharSequence.toSemVer() = semverParser.matcher(this).let { matcher ->
|
||||||
|
require(matcher.find()) { "Unrecognized version $this" }
|
||||||
|
SemVer(matcher.group(1)!!.toInt(), matcher.group(2)!!.toInt(), matcher.group(3)!!.toInt())
|
||||||
|
}
|
||||||
|
private val myVer = BuildConfig.VERSION_NAME.toSemVer()
|
||||||
|
|
||||||
|
private fun findUpdate(response: JSONArray): GitHubUpdate? {
|
||||||
|
var latest: String? = null
|
||||||
|
var latestVer = myVer
|
||||||
|
var earliest = Long.MAX_VALUE
|
||||||
|
for (i in 0 until response.length()) {
|
||||||
|
val obj = response.getJSONObject(i)
|
||||||
|
val name = obj.getString("name")
|
||||||
|
val semver = try {
|
||||||
|
name.toSemVer()
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
Timber.w(e)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (semver <= myVer) continue
|
||||||
|
if (semver > latestVer) {
|
||||||
|
latest = name
|
||||||
|
latestVer = semver
|
||||||
|
}
|
||||||
|
earliest = min(earliest, Instant.parse(obj.getString("published_at")).toEpochMilli())
|
||||||
|
}
|
||||||
|
return latest?.let { GitHubUpdate(it, earliest) }
|
||||||
|
}
|
||||||
|
fun check() = flow<AppUpdate?> {
|
||||||
|
emit(app.pref.getString(KEY_VERSION, null)?.let {
|
||||||
|
if (myVer >= it.toSemVer()) null else GitHubUpdate(it, app.pref.getLong(KEY_PUBLISHED, -1))
|
||||||
|
})
|
||||||
|
while (true) {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val lastFetched = app.pref.getLong(KEY_LAST_FETCHED, -1)
|
||||||
|
if (lastFetched in 0..now) delay(lastFetched + UPDATE_INTERVAL - now)
|
||||||
|
currentCoroutineContext().ensureActive()
|
||||||
|
var reset: Long? = null
|
||||||
|
app.pref.edit {
|
||||||
|
try {
|
||||||
|
val update = findUpdate(JSONArray(connectCancellable(
|
||||||
|
"https://api.github.com/repos/Mygod/VPNHotspot/releases?per_page=100") { conn ->
|
||||||
|
conn.setRequestProperty("Accept", "application/vnd.github.v3+json")
|
||||||
|
reset = conn.getHeaderField("X-RateLimit-Reset")?.toLongOrNull()
|
||||||
|
conn.inputStream.bufferedReader().readText()
|
||||||
|
}))
|
||||||
|
putString(KEY_VERSION, update?.let {
|
||||||
|
putLong(KEY_PUBLISHED, update.published)
|
||||||
|
it.message
|
||||||
|
})
|
||||||
|
emit(update)
|
||||||
|
} catch (_: CancellationException) {
|
||||||
|
return@flow
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.d(e)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.w(e)
|
||||||
|
} finally {
|
||||||
|
putLong(KEY_LAST_FETCHED, System.currentTimeMillis())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reset?.let { delay(System.currentTimeMillis() - it * 1000) }
|
||||||
|
}
|
||||||
|
}.cancellable()
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package be.mygod.vpnhotspot.util
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.net.Uri
|
||||||
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
|
import com.google.android.play.core.appupdate.AppUpdateManagerFactory
|
||||||
|
import com.google.android.play.core.install.InstallException
|
||||||
|
import com.google.android.play.core.install.model.InstallErrorCode
|
||||||
|
import com.google.android.play.core.install.model.InstallStatus
|
||||||
|
import com.google.android.play.core.ktx.AppUpdateResult
|
||||||
|
import com.google.android.play.core.ktx.requestUpdateFlow
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
object UpdateChecker {
|
||||||
|
private class UpdateAvailable(private val update: AppUpdateResult.Available) : AppUpdate {
|
||||||
|
override val stalenessDays get() = update.updateInfo.clientVersionStalenessDays() ?: 0
|
||||||
|
override fun updateForResult(activity: Activity, requestCode: Int) = try {
|
||||||
|
check(update.startFlexibleUpdate(activity, requestCode)) { "startFlexibleUpdate failed" }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.w(e)
|
||||||
|
app.customTabsIntent.launchUrl(activity,
|
||||||
|
Uri.parse("https://play.google.com/store/apps/details?id=be.mygod.vpnhotspot"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private class UpdateDownloading(private val update: AppUpdateResult.InProgress) : AppUpdate {
|
||||||
|
override val downloaded get() = false
|
||||||
|
override val message: String? get() {
|
||||||
|
if (update.installState.installStatus() != InstallStatus.FAILED) return null
|
||||||
|
val code = update.installState.installErrorCode()
|
||||||
|
for (f in InstallErrorCode::class.java.declaredFields) if (f.getInt(null) == code) return f.name
|
||||||
|
return "Unrecognized Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private class UpdateDownloaded(private val update: AppUpdateResult.Downloaded) : AppUpdate {
|
||||||
|
override val downloaded get() = true
|
||||||
|
override val stalenessDays get() = 0
|
||||||
|
override fun updateForResult(activity: Activity, requestCode: Int) {
|
||||||
|
GlobalScope.launch { update.completeUpdate() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val manager by lazy { AppUpdateManagerFactory.create(app) }
|
||||||
|
|
||||||
|
fun check() = manager.requestUpdateFlow().catch { e ->
|
||||||
|
when (e) {
|
||||||
|
is InstallException -> {
|
||||||
|
app.logEvent("InstallErrorCode") { param("errorCode", e.errorCode.toLong()) }
|
||||||
|
throw AppUpdate.IgnoredException(e)
|
||||||
|
}
|
||||||
|
is RuntimeException -> if (e.message == "Failed to bind to the service.") {
|
||||||
|
app.logEvent("UpdateBindFailure")
|
||||||
|
throw AppUpdate.IgnoredException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}.map { result ->
|
||||||
|
when (result) {
|
||||||
|
is AppUpdateResult.NotAvailable -> null
|
||||||
|
is AppUpdateResult.Available -> UpdateAvailable(result)
|
||||||
|
is AppUpdateResult.InProgress -> {
|
||||||
|
if (result.installState.installStatus() == InstallStatus.CANCELED) null else UpdateDownloading(result)
|
||||||
|
}
|
||||||
|
is AppUpdateResult.Downloaded -> UpdateDownloaded(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
package="be.mygod.vpnhotspot">
|
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.bluetooth"
|
android:name="android.hardware.bluetooth"
|
||||||
@@ -9,9 +8,6 @@
|
|||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.ethernet"
|
android:name="android.hardware.ethernet"
|
||||||
android:required="false"/>
|
android:required="false"/>
|
||||||
<uses-feature
|
|
||||||
android:name="android.software.leanback"
|
|
||||||
android:required="false"/>
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.touchscreen"
|
android:name="android.hardware.touchscreen"
|
||||||
android:required="false"/>
|
android:required="false"/>
|
||||||
@@ -24,11 +20,15 @@
|
|||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.wifi.direct"
|
android:name="android.hardware.wifi.direct"
|
||||||
android:required="false"/>
|
android:required="false"/>
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.software.leanback"
|
||||||
|
android:required="false"/>
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
|
||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
|
||||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
|
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
@@ -37,8 +37,11 @@
|
|||||||
tools:ignore="ProtectedPermissions" />
|
tools:ignore="ProtectedPermissions" />
|
||||||
<uses-permission android:name="android.permission.MANAGE_USB"
|
<uses-permission android:name="android.permission.MANAGE_USB"
|
||||||
tools:ignore="ProtectedPermissions"/>
|
tools:ignore="ProtectedPermissions"/>
|
||||||
|
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"
|
||||||
|
android:usesPermissionFlags="neverForLocation"/>
|
||||||
<uses-permission android:name="android.permission.OVERRIDE_WIFI_CONFIG"
|
<uses-permission android:name="android.permission.OVERRIDE_WIFI_CONFIG"
|
||||||
tools:ignore="ProtectedPermissions"/>
|
tools:ignore="ProtectedPermissions"/>
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
<uses-permission android:name="android.permission.READ_WIFI_CREDENTIAL"
|
<uses-permission android:name="android.permission.READ_WIFI_CREDENTIAL"
|
||||||
tools:ignore="ProtectedPermissions"/>
|
tools:ignore="ProtectedPermissions"/>
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
@@ -49,11 +52,22 @@
|
|||||||
tools:ignore="ProtectedPermissions"/>
|
tools:ignore="ProtectedPermissions"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_SETTINGS"
|
<uses-permission android:name="android.permission.WRITE_SETTINGS"
|
||||||
tools:ignore="ProtectedPermissions"/>
|
tools:ignore="ProtectedPermissions"/>
|
||||||
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"
|
||||||
<!-- Required since API 29 -->
|
android:maxSdkVersion="32"/>
|
||||||
<uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
<uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION"
|
||||||
<!-- Required since API 31 -->
|
android:maxSdkVersion="32"/>
|
||||||
<uses-permission-sdk-23 android:name="android.permission.BLUETOOTH_CONNECT"/>
|
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.net.ITetheringConnector" />
|
||||||
|
</intent>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.net.ITetheringConnector.InProcess" />
|
||||||
|
</intent>
|
||||||
|
<intent>
|
||||||
|
<action android:name="com.android.server.wifi.intent.action.SERVICE_WIFI_RESOURCES_APK" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
@@ -63,7 +77,8 @@
|
|||||||
android:banner="@mipmap/banner"
|
android:banner="@mipmap/banner"
|
||||||
android:hasFragileUserData="true"
|
android:hasFragileUserData="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:localeConfig="@xml/locales_config"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
tools:ignore="GoogleAppIndexingWarning">
|
tools:ignore="GoogleAppIndexingWarning">
|
||||||
@@ -71,7 +86,6 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/app_name"
|
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:windowSoftInputMode="stateAlwaysHidden">
|
android:windowSoftInputMode="stateAlwaysHidden">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -88,8 +102,7 @@
|
|||||||
<service
|
<service
|
||||||
android:name=".LocalOnlyHotspotService"
|
android:name=".LocalOnlyHotspotService"
|
||||||
android:directBootAware="true"
|
android:directBootAware="true"
|
||||||
android:foregroundServiceType="location|connectedDevice"
|
android:foregroundServiceType="location|connectedDevice"/>
|
||||||
tools:targetApi="26"/>
|
|
||||||
<service
|
<service
|
||||||
android:name=".RepeaterService"
|
android:name=".RepeaterService"
|
||||||
android:directBootAware="true"
|
android:directBootAware="true"
|
||||||
@@ -105,8 +118,7 @@
|
|||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:icon="@drawable/ic_action_settings_input_antenna"
|
android:icon="@drawable/ic_action_settings_input_antenna"
|
||||||
android:label="@string/title_repeater"
|
android:label="@string/title_repeater"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
tools:targetApi="24">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@@ -117,12 +129,10 @@
|
|||||||
<service
|
<service
|
||||||
android:name=".manage.LocalOnlyHotspotTileService"
|
android:name=".manage.LocalOnlyHotspotTileService"
|
||||||
android:directBootAware="true"
|
android:directBootAware="true"
|
||||||
android:enabled="@bool/api_ge_26"
|
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:icon="@drawable/ic_action_perm_scan_wifi"
|
android:icon="@drawable/ic_action_perm_scan_wifi"
|
||||||
android:label="@string/tethering_temp_hotspot"
|
android:label="@string/tethering_temp_hotspot"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
tools:targetApi="26">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@@ -136,11 +146,13 @@
|
|||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:icon="@drawable/ic_device_wifi_tethering"
|
android:icon="@drawable/ic_device_wifi_tethering"
|
||||||
android:label="@string/tethering_manage_wifi"
|
android:label="@string/tethering_manage_wifi"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
tools:targetApi="24">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||||
|
android:value="true" />
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name=".manage.TetheringTileService$Usb"
|
android:name=".manage.TetheringTileService$Usb"
|
||||||
@@ -148,11 +160,13 @@
|
|||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:icon="@drawable/ic_device_usb"
|
android:icon="@drawable/ic_device_usb"
|
||||||
android:label="@string/tethering_manage_usb"
|
android:label="@string/tethering_manage_usb"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
tools:targetApi="24">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||||
|
android:value="true" />
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name=".manage.TetheringTileService$Bluetooth"
|
android:name=".manage.TetheringTileService$Bluetooth"
|
||||||
@@ -160,11 +174,13 @@
|
|||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:icon="@drawable/ic_device_bluetooth"
|
android:icon="@drawable/ic_device_bluetooth"
|
||||||
android:label="@string/tethering_manage_bluetooth"
|
android:label="@string/tethering_manage_bluetooth"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
tools:targetApi="24">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||||
|
android:value="true" />
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name=".manage.TetheringTileService$Ethernet"
|
android:name=".manage.TetheringTileService$Ethernet"
|
||||||
@@ -178,46 +194,9 @@
|
|||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
<meta-data
|
||||||
<service
|
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||||
android:name=".manage.TetheringTileService$Ncm"
|
android:value="true" />
|
||||||
android:directBootAware="true"
|
|
||||||
android:enabled="@bool/api_ge_30"
|
|
||||||
android:exported="true"
|
|
||||||
android:icon="@drawable/ic_action_settings_ethernet"
|
|
||||||
android:label="@string/tethering_manage_ncm"
|
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
|
||||||
tools:targetApi="30">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
<service
|
|
||||||
android:name=".manage.TetheringTileService$WiGig"
|
|
||||||
android:directBootAware="true"
|
|
||||||
android:enabled="@bool/api_ge_30"
|
|
||||||
android:exported="true"
|
|
||||||
android:icon="@drawable/ic_image_flash_on"
|
|
||||||
android:label="@string/tethering_manage_wigig"
|
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
|
||||||
tools:targetApi="30">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
<!--suppress DeprecatedClassUsageInspection -->
|
|
||||||
<service
|
|
||||||
android:name=".manage.TetheringTileService$WifiLegacy"
|
|
||||||
android:directBootAware="true"
|
|
||||||
android:enabled="@bool/api_lt_25"
|
|
||||||
android:exported="true"
|
|
||||||
android:icon="@drawable/ic_device_wifi_tethering"
|
|
||||||
android:label="@string/tethering_manage_wifi_legacy"
|
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
|
||||||
tools:targetApi="24">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
@@ -228,6 +207,7 @@
|
|||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||||
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
|
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
|
||||||
|
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
package be.mygod.librootkotlinx
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Debug
|
|
||||||
import android.os.Process
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
object AppProcess {
|
|
||||||
/**
|
|
||||||
* Based on: https://android.googlesource.com/platform/bionic/+/aff9a34/linker/linker.cpp#3397
|
|
||||||
*/
|
|
||||||
@get:RequiresApi(28)
|
|
||||||
val genericLdConfigFilePath: String get() {
|
|
||||||
"/system/etc/ld.config.$currentInstructionSet.txt".let { if (File(it).isFile) return it }
|
|
||||||
if (Build.VERSION.SDK_INT >= 30) "/linkerconfig/ld.config.txt".let {
|
|
||||||
if (File(it).isFile) return it
|
|
||||||
Logger.me.w("Failed to find generated linker configuration from \"$it\"")
|
|
||||||
}
|
|
||||||
if (isVndkLite) {
|
|
||||||
"/system/etc/ld.config.vndk_lite.txt".let { if (File(it).isFile) return it }
|
|
||||||
} else when (vndkVersion) {
|
|
||||||
"", "current" -> { }
|
|
||||||
else -> "/system/etc/ld.config.$vndkVersion.txt".let { if (File(it).isFile) return it }
|
|
||||||
}
|
|
||||||
return "/system/etc/ld.config.txt"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Based on: https://android.googlesource.com/platform/bionic/+/30f2f05/linker/linker_config.cpp#182
|
|
||||||
*/
|
|
||||||
@RequiresApi(26)
|
|
||||||
fun findLinkerSection(lines: Sequence<String>, binaryRealPath: String): String {
|
|
||||||
for (untrimmed in lines) {
|
|
||||||
val line = untrimmed.substringBefore('#').trim()
|
|
||||||
if (line.isEmpty()) continue
|
|
||||||
if (line[0] == '[' && line.last() == ']') break
|
|
||||||
if (line.contains("+=")) continue
|
|
||||||
val chunks = line.split('=', limit = 2)
|
|
||||||
if (chunks.size < 2) {
|
|
||||||
Logger.me.w("warning: couldn't parse invalid format: $line (ignoring this line)")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var (name, value) = chunks.map { it.trim() }
|
|
||||||
if (!name.startsWith("dir.")) {
|
|
||||||
Logger.me.w("warning: unexpected property name \"$name\", " +
|
|
||||||
"expected format dir.<section_name> (ignoring this line)")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (value.endsWith('/')) value = value.dropLast(1)
|
|
||||||
if (value.isEmpty()) {
|
|
||||||
Logger.me.w("warning: property value is empty (ignoring this line)")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
value = File(value).canonicalPath
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Logger.me.i("warning: path \"$value\" couldn't be resolved: ${e.message}")
|
|
||||||
}
|
|
||||||
if (binaryRealPath.startsWith(value) && binaryRealPath[value.length] == '/') return name.substring(4)
|
|
||||||
}
|
|
||||||
throw IllegalArgumentException("No valid linker section found")
|
|
||||||
}
|
|
||||||
|
|
||||||
val myExe get() = "/proc/${Process.myPid()}/exe"
|
|
||||||
val myExeCanonical get() = try {
|
|
||||||
File("/proc/self/exe").canonicalPath
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Logger.me.i("warning: couldn't resolve self exe: ${e.message}")
|
|
||||||
"/system/bin/app_process"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To workaround Samsung's stupid kernel patch that prevents exec, we need to relocate exe outside of /data.
|
|
||||||
* See also: https://github.com/Chainfire/librootjava/issues/19
|
|
||||||
*
|
|
||||||
* @return The script to be executed to perform relocation and the relocated binary path.
|
|
||||||
*/
|
|
||||||
fun relocateScript(token: String): Pair<StringBuilder, String> {
|
|
||||||
val script = StringBuilder()
|
|
||||||
val (baseDir, relocated) = if (Build.VERSION.SDK_INT < 29) "/dev" to "/dev/app_process_$token" else {
|
|
||||||
val apexPath = "/apex/$token"
|
|
||||||
script.appendLine("[ -d $apexPath ] || " +
|
|
||||||
"mkdir $apexPath && " +
|
|
||||||
// we need to mount a new tmpfs to override noexec flag
|
|
||||||
"mount -t tmpfs -o size=1M tmpfs $apexPath || exit 1")
|
|
||||||
// unfortunately native ld.config.txt only recognizes /data,/system,/system_ext as system directories;
|
|
||||||
// to link correctly, we need to add our path to the linker config too
|
|
||||||
val ldConfig = "$apexPath/etc/ld.config.txt"
|
|
||||||
val masterLdConfig = genericLdConfigFilePath
|
|
||||||
val section = try {
|
|
||||||
File(masterLdConfig).useLines { findLinkerSection(it, myExeCanonical) }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.me.w("Failed to locate system section", e)
|
|
||||||
"system"
|
|
||||||
}
|
|
||||||
script.appendLine("[ -f $ldConfig ] || " +
|
|
||||||
"mkdir -p $apexPath/etc && " +
|
|
||||||
"echo dir.$section = $apexPath >$ldConfig && " +
|
|
||||||
"cat $masterLdConfig >>$ldConfig || exit 1")
|
|
||||||
"$apexPath/bin" to "$apexPath/bin/app_process"
|
|
||||||
}
|
|
||||||
script.appendLine("[ -f $relocated ] || " +
|
|
||||||
"mkdir -p $baseDir && " +
|
|
||||||
"cp $myExe $relocated && " +
|
|
||||||
"chmod 700 $relocated || exit 1")
|
|
||||||
return script to relocated
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute the shell script line that exec into the corresponding [clazz].
|
|
||||||
* Extra params can be simply appended to the string.
|
|
||||||
*/
|
|
||||||
fun launchString(packageCodePath: String, clazz: String, appProcess: String, niceName: String? = null): String {
|
|
||||||
val debugParams = if (Debug.isDebuggerConnected()) when (Build.VERSION.SDK_INT) {
|
|
||||||
in 29..Int.MAX_VALUE -> "-XjdwpProvider:adbconnection"
|
|
||||||
28 -> "-XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y -Xcompiler-option --debuggable"
|
|
||||||
else -> "-Xrunjdwp:transport=dt_android_adb,suspend=n,server=y -Xcompiler-option --debuggable"
|
|
||||||
} else ""
|
|
||||||
val extraParams = if (niceName != null) " --nice-name=$niceName" else ""
|
|
||||||
return "CLASSPATH=$packageCodePath exec $appProcess $debugParams /system/bin$extraParams $clazz"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package be.mygod.librootkotlinx
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
|
|
||||||
interface Logger {
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* Override this variable to change default behavior,
|
|
||||||
* which is to print to [android.util.Log] under tag "RootServer" except for [d].
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
var me = object : Logger { }
|
|
||||||
|
|
||||||
private const val TAG = "RootServer"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun d(m: String?, t: Throwable? = null) { }
|
|
||||||
fun e(m: String?, t: Throwable? = null) {
|
|
||||||
Log.e(TAG, m, t)
|
|
||||||
}
|
|
||||||
fun i(m: String?, t: Throwable? = null) {
|
|
||||||
Log.i(TAG, m, t)
|
|
||||||
}
|
|
||||||
fun w(m: String?, t: Throwable? = null) {
|
|
||||||
Log.w(TAG, m, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,485 +0,0 @@
|
|||||||
package be.mygod.librootkotlinx
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Looper
|
|
||||||
import android.os.Parcelable
|
|
||||||
import android.os.RemoteException
|
|
||||||
import android.system.Os
|
|
||||||
import android.system.OsConstants
|
|
||||||
import androidx.collection.LongSparseArray
|
|
||||||
import androidx.collection.set
|
|
||||||
import androidx.collection.valueIterator
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.channels.*
|
|
||||||
import java.io.*
|
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
class RootServer {
|
|
||||||
private sealed class Callback(private val server: RootServer, private val index: Long,
|
|
||||||
protected val classLoader: ClassLoader?) {
|
|
||||||
var active = true
|
|
||||||
|
|
||||||
abstract fun cancel()
|
|
||||||
abstract fun shouldRemove(result: Byte): Boolean
|
|
||||||
abstract operator fun invoke(input: DataInputStream, result: Byte)
|
|
||||||
fun sendClosed() = server.execute(CancelCommand(index))
|
|
||||||
|
|
||||||
private fun initException(targetClass: Class<*>, message: String): Throwable {
|
|
||||||
@Suppress("NAME_SHADOWING")
|
|
||||||
var targetClass = targetClass
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
// try to find a message constructor
|
|
||||||
return targetClass.getDeclaredConstructor(String::class.java).newInstance(message) as Throwable
|
|
||||||
} catch (_: ReflectiveOperationException) { }
|
|
||||||
targetClass = targetClass.superclass
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun makeRemoteException(cause: Throwable, message: String? = null) =
|
|
||||||
if (cause is CancellationException) cause else RemoteException(message).initCause(cause)
|
|
||||||
protected fun DataInputStream.readException(result: Byte) = when (result.toInt()) {
|
|
||||||
EX_GENERIC -> {
|
|
||||||
val message = readUTF()
|
|
||||||
val name = message.split(':', limit = 2)[0]
|
|
||||||
makeRemoteException(initException(try {
|
|
||||||
classLoader?.loadClass(name)
|
|
||||||
} catch (_: ClassNotFoundException) {
|
|
||||||
null
|
|
||||||
} ?: Class.forName(name), message), message)
|
|
||||||
}
|
|
||||||
EX_PARCELABLE -> makeRemoteException(readParcelable<Parcelable>(classLoader) as Throwable)
|
|
||||||
EX_SERIALIZABLE -> makeRemoteException(readSerializable(classLoader) as Throwable)
|
|
||||||
else -> throw IllegalArgumentException("Unexpected result $result")
|
|
||||||
}
|
|
||||||
|
|
||||||
class Ordinary(server: RootServer, index: Long, classLoader: ClassLoader?,
|
|
||||||
private val callback: CompletableDeferred<Parcelable?>) : Callback(server, index, classLoader) {
|
|
||||||
override fun cancel() = callback.cancel()
|
|
||||||
override fun shouldRemove(result: Byte) = true
|
|
||||||
override fun invoke(input: DataInputStream, result: Byte) {
|
|
||||||
if (result.toInt() == SUCCESS) callback.complete(input.readParcelable(classLoader))
|
|
||||||
else callback.completeExceptionally(input.readException(result))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Channel(server: RootServer, index: Long, classLoader: ClassLoader?,
|
|
||||||
private val channel: SendChannel<Parcelable?>) : Callback(server, index, classLoader) {
|
|
||||||
val finish: CompletableDeferred<Unit> = CompletableDeferred()
|
|
||||||
override fun cancel() = finish.cancel()
|
|
||||||
override fun shouldRemove(result: Byte) = result.toInt() != SUCCESS
|
|
||||||
override fun invoke(input: DataInputStream, result: Byte) {
|
|
||||||
when (result.toInt()) {
|
|
||||||
SUCCESS -> channel.trySend(input.readParcelable(classLoader)).onClosed {
|
|
||||||
active = false
|
|
||||||
sendClosed()
|
|
||||||
finish.completeExceptionally(it
|
|
||||||
?: ClosedSendChannelException("Channel was closed normally"))
|
|
||||||
return
|
|
||||||
}.onFailure { throw it!! } // the channel we are supporting should never block
|
|
||||||
CHANNEL_CONSUMED -> finish.complete(Unit)
|
|
||||||
else -> finish.completeExceptionally(input.readException(result))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UnexpectedExitException : RemoteException("Root process exited unexpectedly")
|
|
||||||
|
|
||||||
private lateinit var process: Process
|
|
||||||
/**
|
|
||||||
* Thread safety: needs to be protected by callbackLookup.
|
|
||||||
*/
|
|
||||||
private lateinit var output: DataOutputStream
|
|
||||||
|
|
||||||
@Volatile
|
|
||||||
var active = false
|
|
||||||
private var counter = 0L
|
|
||||||
private var callbackListenerExit: Deferred<Unit>? = null
|
|
||||||
private val callbackLookup = LongSparseArray<Callback>()
|
|
||||||
|
|
||||||
private fun readUnexpectedStderr(): String? {
|
|
||||||
if (!this::process.isInitialized) return null
|
|
||||||
var available = process.errorStream.available()
|
|
||||||
return if (available <= 0) null else String(ByteArrayOutputStream().apply {
|
|
||||||
try {
|
|
||||||
while (available > 0) {
|
|
||||||
val bytes = ByteArray(available)
|
|
||||||
val len = process.errorStream.read(bytes)
|
|
||||||
if (len < 0) throw EOFException() // should not happen
|
|
||||||
write(bytes, 0, len)
|
|
||||||
available = process.errorStream.available()
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Logger.me.w("Reading stderr was cut short", e)
|
|
||||||
}
|
|
||||||
}.toByteArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun BufferedReader.lookForToken(token: String) {
|
|
||||||
while (true) {
|
|
||||||
val line = readLine() ?: throw EOFException()
|
|
||||||
if (line.endsWith(token)) {
|
|
||||||
val extraLength = line.length - token.length
|
|
||||||
if (extraLength > 0) Logger.me.w(line.substring(0, extraLength))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
Logger.me.w(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun doInit(context: Context, niceName: String) {
|
|
||||||
val (reader, writer) = try {
|
|
||||||
process = ProcessBuilder("su").start()
|
|
||||||
val token1 = UUID.randomUUID().toString()
|
|
||||||
val writer = DataOutputStream(process.outputStream.buffered())
|
|
||||||
writer.writeBytes("echo $token1\n")
|
|
||||||
writer.flush()
|
|
||||||
val reader = process.inputStream.bufferedReader()
|
|
||||||
reader.lookForToken(token1)
|
|
||||||
Logger.me.d("Root shell initialized")
|
|
||||||
reader to writer
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw NoShellException(e)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
val token2 = UUID.randomUUID().toString()
|
|
||||||
val persistence = File(context.codeCacheDir, ".librootkotlinx-uuid")
|
|
||||||
val uuid = context.packageName + '@' + try {
|
|
||||||
persistence.readText()
|
|
||||||
} catch (_: FileNotFoundException) {
|
|
||||||
UUID.randomUUID().toString().also { persistence.writeText(it) }
|
|
||||||
}
|
|
||||||
val (script, relocated) = AppProcess.relocateScript(uuid)
|
|
||||||
script.appendLine(AppProcess.launchString(context.packageCodePath, RootServer::class.java.name, relocated,
|
|
||||||
niceName) + " $token2")
|
|
||||||
writer.writeBytes(script.toString())
|
|
||||||
writer.flush()
|
|
||||||
reader.lookForToken(token2) // wait for ready signal
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw RuntimeException("Failed to launch root daemon", e)
|
|
||||||
}
|
|
||||||
output = writer
|
|
||||||
require(!active)
|
|
||||||
active = true
|
|
||||||
Logger.me.d("Root server initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun callbackSpin() {
|
|
||||||
val input = DataInputStream(process.inputStream.buffered())
|
|
||||||
while (active) {
|
|
||||||
val index = try {
|
|
||||||
input.readLong()
|
|
||||||
} catch (_: EOFException) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
val result = input.readByte()
|
|
||||||
val callback = synchronized(callbackLookup) {
|
|
||||||
if (active) (callbackLookup[index] ?: error("Empty callback #$index")).also {
|
|
||||||
if (it.shouldRemove(result)) {
|
|
||||||
callbackLookup.remove(index)
|
|
||||||
it.active = false
|
|
||||||
}
|
|
||||||
} else null
|
|
||||||
} ?: break
|
|
||||||
Logger.me.d("Received callback #$index: $result")
|
|
||||||
callback(input, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize a RootServer synchronously, can throw a lot of exceptions.
|
|
||||||
*
|
|
||||||
* @param context Any [Context] from the app.
|
|
||||||
* @param niceName Name to call the rooted Java process.
|
|
||||||
*/
|
|
||||||
suspend fun init(context: Context, niceName: String = "${context.packageName}:root") {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
doInit(context, niceName)
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
readUnexpectedStderr()?.let { Logger.me.e(it) }
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Logger.me.e("Failed to read from stderr", e) // avoid the real exception being swallowed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
callbackListenerExit = GlobalScope.async(Dispatchers.IO) {
|
|
||||||
val errorReader = async(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
process.errorStream.bufferedReader().forEachLine(Logger.me::w)
|
|
||||||
} catch (_: IOException) { }
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
callbackSpin()
|
|
||||||
if (active) throw UnexpectedExitException()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
process.destroy()
|
|
||||||
throw e
|
|
||||||
} finally {
|
|
||||||
Logger.me.d("Waiting for exit")
|
|
||||||
withContext(NonCancellable) { errorReader.await() }
|
|
||||||
process.waitFor()
|
|
||||||
closeInternal(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Caller should check for active.
|
|
||||||
*/
|
|
||||||
private fun sendLocked(command: Parcelable) {
|
|
||||||
output.writeParcelable(command)
|
|
||||||
output.flush()
|
|
||||||
Logger.me.d("Sent #$counter: $command")
|
|
||||||
counter++
|
|
||||||
}
|
|
||||||
|
|
||||||
fun execute(command: RootCommandOneWay) = synchronized(callbackLookup) { if (active) sendLocked(command) }
|
|
||||||
@Throws(RemoteException::class)
|
|
||||||
suspend inline fun <reified T : Parcelable?> execute(command: RootCommand<T>) =
|
|
||||||
execute(command, T::class.java.classLoader)
|
|
||||||
@Throws(RemoteException::class)
|
|
||||||
suspend fun <T : Parcelable?> execute(command: RootCommand<T>, classLoader: ClassLoader?): T {
|
|
||||||
val future = CompletableDeferred<T>()
|
|
||||||
val callback = synchronized(callbackLookup) {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val callback = Callback.Ordinary(this, counter, classLoader, future as CompletableDeferred<Parcelable?>)
|
|
||||||
if (active) {
|
|
||||||
callbackLookup[counter] = callback
|
|
||||||
sendLocked(command)
|
|
||||||
} else future.cancel()
|
|
||||||
callback
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return future.await()
|
|
||||||
} finally {
|
|
||||||
if (callback.active) callback.sendClosed()
|
|
||||||
callback.active = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
|
||||||
@Throws(RemoteException::class)
|
|
||||||
inline fun <reified T : Parcelable?> create(command: RootCommandChannel<T>, scope: CoroutineScope) =
|
|
||||||
create(command, scope, T::class.java.classLoader)
|
|
||||||
@ExperimentalCoroutinesApi
|
|
||||||
@Throws(RemoteException::class)
|
|
||||||
fun <T : Parcelable?> create(command: RootCommandChannel<T>, scope: CoroutineScope,
|
|
||||||
classLoader: ClassLoader?) = scope.produce<T>(
|
|
||||||
SupervisorJob(), command.capacity.also {
|
|
||||||
when (it) {
|
|
||||||
Channel.UNLIMITED, Channel.CONFLATED -> { }
|
|
||||||
else -> throw IllegalArgumentException("Unsupported channel capacity $it")
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
val callback = synchronized(callbackLookup) {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val callback = Callback.Channel(this@RootServer, counter, classLoader, this as SendChannel<Parcelable?>)
|
|
||||||
if (active) {
|
|
||||||
callbackLookup[counter] = callback
|
|
||||||
sendLocked(command)
|
|
||||||
} else callback.finish.cancel()
|
|
||||||
callback
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
callback.finish.await()
|
|
||||||
} finally {
|
|
||||||
if (callback.active) callback.sendClosed()
|
|
||||||
callback.active = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun closeInternal(fromWorker: Boolean = false) = synchronized(callbackLookup) {
|
|
||||||
if (active) {
|
|
||||||
active = false
|
|
||||||
Logger.me.d(if (fromWorker) "Shutting down from worker" else "Shutting down from client")
|
|
||||||
try {
|
|
||||||
sendLocked(Shutdown())
|
|
||||||
output.close()
|
|
||||||
process.outputStream.close()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
if (!e.isEBADF) Logger.me.w("send Shutdown failed", e)
|
|
||||||
}
|
|
||||||
Logger.me.d("Client closed")
|
|
||||||
}
|
|
||||||
if (fromWorker) {
|
|
||||||
for (callback in callbackLookup.valueIterator()) callback.cancel()
|
|
||||||
callbackLookup.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Shutdown the instance gracefully.
|
|
||||||
*/
|
|
||||||
suspend fun close() {
|
|
||||||
closeInternal()
|
|
||||||
val callbackListenerExit = callbackListenerExit ?: return
|
|
||||||
try {
|
|
||||||
withTimeout(10000) { callbackListenerExit.await() }
|
|
||||||
} catch (e: TimeoutCancellationException) {
|
|
||||||
Logger.me.w("Closing the instance has timed out", e)
|
|
||||||
if (Build.VERSION.SDK_INT < 26) process.destroy() else if (process.isAlive) process.destroyForcibly()
|
|
||||||
} catch (e: UnexpectedExitException) {
|
|
||||||
Logger.me.w(e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val SUCCESS = 0
|
|
||||||
private const val EX_GENERIC = 1
|
|
||||||
private const val EX_PARCELABLE = 2
|
|
||||||
private const val EX_SERIALIZABLE = 4
|
|
||||||
private const val CHANNEL_CONSUMED = 3
|
|
||||||
|
|
||||||
private fun DataInputStream.readByteArray() = ByteArray(readInt()).also { readFully(it) }
|
|
||||||
|
|
||||||
private inline fun <reified T : Parcelable> DataInputStream.readParcelable(
|
|
||||||
classLoader: ClassLoader? = T::class.java.classLoader) = readByteArray().toParcelable<T>(classLoader)
|
|
||||||
private fun DataOutputStream.writeParcelable(data: Parcelable?, parcelableFlags: Int = 0) {
|
|
||||||
val bytes = data.toByteArray(parcelableFlags)
|
|
||||||
writeInt(bytes.size)
|
|
||||||
write(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun DataInputStream.readSerializable(classLoader: ClassLoader?) =
|
|
||||||
object : ObjectInputStream(ByteArrayInputStream(readByteArray())) {
|
|
||||||
override fun resolveClass(desc: ObjectStreamClass) = Class.forName(desc.name, false, classLoader)
|
|
||||||
}.readObject()
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
|
||||||
Logger.me.e("Uncaught exception from $thread", throwable)
|
|
||||||
throwable.printStackTrace() // stderr will be read by listener
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
rootMain(args)
|
|
||||||
exitProcess(0) // there might be other non-daemon threads
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun DataOutputStream.pushThrowable(callback: Long, e: Throwable) {
|
|
||||||
writeLong(callback)
|
|
||||||
if (e is Parcelable) {
|
|
||||||
writeByte(EX_PARCELABLE)
|
|
||||||
writeParcelable(e)
|
|
||||||
} else try {
|
|
||||||
val bytes = ByteArrayOutputStream().apply {
|
|
||||||
ObjectOutputStream(this).use { it.writeObject(e) }
|
|
||||||
}.toByteArray()
|
|
||||||
writeByte(EX_SERIALIZABLE)
|
|
||||||
writeInt(bytes.size)
|
|
||||||
write(bytes)
|
|
||||||
} catch (_: NotSerializableException) {
|
|
||||||
writeByte(EX_GENERIC)
|
|
||||||
writeUTF(e.stackTraceToString())
|
|
||||||
}
|
|
||||||
flush()
|
|
||||||
}
|
|
||||||
private fun DataOutputStream.pushResult(callback: Long, result: Parcelable?) {
|
|
||||||
writeLong(callback)
|
|
||||||
writeByte(SUCCESS)
|
|
||||||
writeParcelable(result)
|
|
||||||
flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun rootMain(args: Array<String>) {
|
|
||||||
require(args.isNotEmpty())
|
|
||||||
val mainInitialized = CountDownLatch(1)
|
|
||||||
val main = Thread({
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
Looper.prepareMainLooper()
|
|
||||||
mainInitialized.countDown()
|
|
||||||
Looper.loop()
|
|
||||||
}, "main")
|
|
||||||
main.start()
|
|
||||||
val job = Job()
|
|
||||||
val defaultWorker by lazy {
|
|
||||||
mainInitialized.await()
|
|
||||||
CoroutineScope(Dispatchers.Main.immediate + job)
|
|
||||||
}
|
|
||||||
val callbackWorker = newSingleThreadContext("callbackWorker")
|
|
||||||
val cancellables = LongSparseArray<() -> Unit>()
|
|
||||||
|
|
||||||
// thread safety: usage of output should be guarded by callbackWorker
|
|
||||||
val output = DataOutputStream(FileOutputStream(Os.dup(FileDescriptor.out)).buffered().apply {
|
|
||||||
// prevent future write attempts to System.out, possibly from Samsung changes (again)
|
|
||||||
Os.dup2(FileDescriptor.err, OsConstants.STDOUT_FILENO)
|
|
||||||
System.setOut(System.err)
|
|
||||||
val writer = writer()
|
|
||||||
writer.appendLine(args[0]) // echo ready signal
|
|
||||||
writer.flush()
|
|
||||||
})
|
|
||||||
// thread safety: usage of input should be in main thread
|
|
||||||
val input = DataInputStream(System.`in`.buffered())
|
|
||||||
var counter = 0L
|
|
||||||
Logger.me.d("Server entering main loop")
|
|
||||||
loop@ while (true) {
|
|
||||||
val command = try {
|
|
||||||
input.readParcelable<Parcelable>(RootServer::class.java.classLoader)
|
|
||||||
} catch (_: EOFException) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
val callback = counter
|
|
||||||
Logger.me.d("Received #$callback: $command")
|
|
||||||
when (command) {
|
|
||||||
is CancelCommand -> cancellables[command.index]?.invoke()
|
|
||||||
is RootCommandOneWay -> defaultWorker.launch {
|
|
||||||
try {
|
|
||||||
command.execute()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.me.e("Unexpected exception in RootCommandOneWay ($command.javaClass.simpleName)", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is RootCommand<*> -> {
|
|
||||||
val commandJob = Job()
|
|
||||||
cancellables[callback] = { commandJob.cancel() }
|
|
||||||
defaultWorker.launch(commandJob) {
|
|
||||||
val result = try {
|
|
||||||
val result = command.execute();
|
|
||||||
{ output.pushResult(callback, result) }
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
val worker = { output.pushThrowable(callback, e) }
|
|
||||||
worker
|
|
||||||
} finally {
|
|
||||||
cancellables.remove(callback)
|
|
||||||
}
|
|
||||||
withContext(callbackWorker + NonCancellable) { result() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is RootCommandChannel<*> -> defaultWorker.launch {
|
|
||||||
val result = try {
|
|
||||||
coroutineScope {
|
|
||||||
command.create(this).also {
|
|
||||||
cancellables[callback] = { it.cancel() }
|
|
||||||
}.consumeEach { result ->
|
|
||||||
withContext(callbackWorker) { output.pushResult(callback, result) }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext") {
|
|
||||||
output.writeByte(CHANNEL_CONSUMED)
|
|
||||||
output.writeLong(callback)
|
|
||||||
output.flush()
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
val worker = { output.pushThrowable(callback, e) }
|
|
||||||
worker
|
|
||||||
} finally {
|
|
||||||
cancellables.remove(callback)
|
|
||||||
}
|
|
||||||
withContext(callbackWorker + NonCancellable) { result() }
|
|
||||||
}
|
|
||||||
is Shutdown -> break@loop
|
|
||||||
else -> throw IllegalArgumentException("Unrecognized input: $command")
|
|
||||||
}
|
|
||||||
counter++
|
|
||||||
}
|
|
||||||
job.cancel()
|
|
||||||
Logger.me.d("Clean up initiated before exit. Jobs: ${job.children.joinToString()}")
|
|
||||||
if (runBlocking { withTimeoutOrNull(5000) { job.join() } } == null) {
|
|
||||||
Logger.me.w("Clean up timeout: ${job.children.joinToString()}")
|
|
||||||
} else Logger.me.d("Clean up finished, exiting")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
package be.mygod.librootkotlinx
|
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This object manages creation of [RootServer] and times them out automagically, with default timeout of 5 minutes.
|
|
||||||
*/
|
|
||||||
abstract class RootSession {
|
|
||||||
protected abstract suspend fun initServer(server: RootServer)
|
|
||||||
/**
|
|
||||||
* Timeout to close [RootServer] in milliseconds.
|
|
||||||
*/
|
|
||||||
protected open val timeout get() = TimeUnit.MINUTES.toMillis(5)
|
|
||||||
protected open val timeoutContext: CoroutineContext get() = Dispatchers.Default
|
|
||||||
|
|
||||||
private val mutex = Mutex()
|
|
||||||
private var server: RootServer? = null
|
|
||||||
private var timeoutJob: Job? = null
|
|
||||||
private var usersCount = 0L
|
|
||||||
private var closePending = false
|
|
||||||
|
|
||||||
private suspend fun ensureServerLocked(): RootServer {
|
|
||||||
server?.let {
|
|
||||||
if (it.active) return it
|
|
||||||
usersCount = 0
|
|
||||||
closeLocked()
|
|
||||||
}
|
|
||||||
check(usersCount == 0L) { "Unexpected $server, $usersCount" }
|
|
||||||
val server = RootServer()
|
|
||||||
try {
|
|
||||||
initServer(server)
|
|
||||||
this.server = server
|
|
||||||
return server
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
try {
|
|
||||||
server.close()
|
|
||||||
} catch (eClose: Throwable) {
|
|
||||||
e.addSuppressed(eClose)
|
|
||||||
}
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun closeLocked() {
|
|
||||||
closePending = false
|
|
||||||
val server = server
|
|
||||||
this.server = null
|
|
||||||
server?.close()
|
|
||||||
}
|
|
||||||
private fun startTimeoutLocked() {
|
|
||||||
check(timeoutJob == null)
|
|
||||||
timeoutJob = GlobalScope.launch(timeoutContext, CoroutineStart.UNDISPATCHED) {
|
|
||||||
delay(timeout)
|
|
||||||
mutex.withLock {
|
|
||||||
check(usersCount == 0L)
|
|
||||||
timeoutJob = null
|
|
||||||
closeLocked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun haltTimeoutLocked() {
|
|
||||||
timeoutJob?.cancel()
|
|
||||||
timeoutJob = null
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun acquire() = withContext(NonCancellable) {
|
|
||||||
mutex.withLock {
|
|
||||||
haltTimeoutLocked()
|
|
||||||
closePending = false
|
|
||||||
ensureServerLocked().also { ++usersCount }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
suspend fun release(server: RootServer) = withContext(NonCancellable) {
|
|
||||||
mutex.withLock {
|
|
||||||
if (this@RootSession.server != server) return@withLock // outdated reference
|
|
||||||
require(usersCount > 0)
|
|
||||||
when {
|
|
||||||
!server.active -> {
|
|
||||||
usersCount = 0
|
|
||||||
closeLocked()
|
|
||||||
return@withLock
|
|
||||||
}
|
|
||||||
--usersCount > 0L -> return@withLock
|
|
||||||
closePending -> closeLocked()
|
|
||||||
else -> startTimeoutLocked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
suspend inline fun <T> use(block: (RootServer) -> T): T {
|
|
||||||
val server = acquire()
|
|
||||||
try {
|
|
||||||
return block(server)
|
|
||||||
} finally {
|
|
||||||
release(server)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun closeExisting() = mutex.withLock {
|
|
||||||
if (usersCount > 0) closePending = true else {
|
|
||||||
haltTimeoutLocked()
|
|
||||||
closeLocked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package be.mygod.librootkotlinx
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import androidx.annotation.MainThread
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import kotlinx.coroutines.channels.ReceiveChannel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
interface RootCommand<Result : Parcelable?> : Parcelable {
|
|
||||||
/**
|
|
||||||
* If a throwable was thrown, it will be wrapped in RemoteException only if it implements [Parcelable].
|
|
||||||
*/
|
|
||||||
@MainThread
|
|
||||||
suspend fun execute(): Result
|
|
||||||
}
|
|
||||||
|
|
||||||
typealias RootCommandNoResult = RootCommand<Parcelable?>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute a command and discards its result, even if an exception occurs.
|
|
||||||
*
|
|
||||||
* If you want to catch exception, use e.g. [RootCommandNoResult] and return null.
|
|
||||||
*/
|
|
||||||
interface RootCommandOneWay : Parcelable {
|
|
||||||
@MainThread
|
|
||||||
suspend fun execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RootCommandChannel<T : Parcelable?> : Parcelable {
|
|
||||||
/**
|
|
||||||
* The capacity of the channel that is returned by [create] to be used by client.
|
|
||||||
* Only [Channel.UNLIMITED] and [Channel.CONFLATED] is supported for now to avoid blocking the entire connection.
|
|
||||||
*/
|
|
||||||
val capacity: Int get() = Channel.UNLIMITED
|
|
||||||
|
|
||||||
@MainThread
|
|
||||||
fun create(scope: CoroutineScope): ReceiveChannel<T>
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
internal data class CancelCommand(val index: Long) : RootCommandOneWay {
|
|
||||||
override suspend fun execute() = error("Internal implementation")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
internal class Shutdown : Parcelable
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
@file:JvmName("Utils")
|
|
||||||
|
|
||||||
package be.mygod.librootkotlinx
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Parcel
|
|
||||||
import android.os.Parcelable
|
|
||||||
import android.system.ErrnoException
|
|
||||||
import android.system.OsConstants
|
|
||||||
import android.util.*
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
class NoShellException(cause: Throwable) : Exception("Root missing", cause)
|
|
||||||
|
|
||||||
internal val currentInstructionSet by lazy {
|
|
||||||
val classVMRuntime = Class.forName("dalvik.system.VMRuntime")
|
|
||||||
val runtime = classVMRuntime.getDeclaredMethod("getRuntime").invoke(null)
|
|
||||||
classVMRuntime.getDeclaredMethod("getCurrentInstructionSet").invoke(runtime) as String
|
|
||||||
}
|
|
||||||
|
|
||||||
private val classSystemProperties by lazy { Class.forName("android.os.SystemProperties") }
|
|
||||||
@get:RequiresApi(26)
|
|
||||||
internal val isVndkLite by lazy {
|
|
||||||
classSystemProperties.getDeclaredMethod("getBoolean", String::class.java, Boolean::class.java).invoke(null,
|
|
||||||
"ro.vndk.lite", false) as Boolean
|
|
||||||
}
|
|
||||||
@get:RequiresApi(26)
|
|
||||||
internal val vndkVersion by lazy {
|
|
||||||
classSystemProperties.getDeclaredMethod("get", String::class.java, String::class.java).invoke(null,
|
|
||||||
"ro.vndk.version", "") as String
|
|
||||||
}
|
|
||||||
|
|
||||||
val systemContext by lazy {
|
|
||||||
val classActivityThread = Class.forName("android.app.ActivityThread")
|
|
||||||
val activityThread = classActivityThread.getMethod("systemMain").invoke(null)
|
|
||||||
classActivityThread.getMethod("getSystemContext").invoke(activityThread) as Context
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableByte(val value: Byte) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableShort(val value: Short) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableInt(val value: Int) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableLong(val value: Long) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableFloat(val value: Float) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableDouble(val value: Double) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableBoolean(val value: Boolean) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableString(val value: String) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableByteArray(val value: ByteArray) : Parcelable {
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as ParcelableByteArray
|
|
||||||
|
|
||||||
if (!value.contentEquals(other.value)) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return value.contentHashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableIntArray(val value: IntArray) : Parcelable {
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as ParcelableIntArray
|
|
||||||
|
|
||||||
if (!value.contentEquals(other.value)) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return value.contentHashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableLongArray(val value: LongArray) : Parcelable {
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as ParcelableLongArray
|
|
||||||
|
|
||||||
if (!value.contentEquals(other.value)) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return value.contentHashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableFloatArray(val value: FloatArray) : Parcelable {
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as ParcelableFloatArray
|
|
||||||
|
|
||||||
if (!value.contentEquals(other.value)) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return value.contentHashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableDoubleArray(val value: DoubleArray) : Parcelable {
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as ParcelableDoubleArray
|
|
||||||
|
|
||||||
if (!value.contentEquals(other.value)) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return value.contentHashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableBooleanArray(val value: BooleanArray) : Parcelable {
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as ParcelableBooleanArray
|
|
||||||
|
|
||||||
if (!value.contentEquals(other.value)) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return value.contentHashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableStringArray(val value: Array<String>) : Parcelable {
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as ParcelableStringArray
|
|
||||||
|
|
||||||
if (!value.contentEquals(other.value)) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return value.contentHashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableStringList(val value: List<String>) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableSparseIntArray(val value: SparseIntArray) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableSparseLongArray(val value: SparseLongArray) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableSparseBooleanArray(val value: SparseBooleanArray) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableCharSequence(val value: CharSequence) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableSize(val value: Size) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableSizeF(val value: SizeF) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableArray(val value: Array<Parcelable?>) : Parcelable {
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as ParcelableArray
|
|
||||||
|
|
||||||
if (!value.contentEquals(other.value)) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return value.contentHashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableList(val value: List<Parcelable?>) : Parcelable
|
|
||||||
|
|
||||||
@SuppressLint("Recycle")
|
|
||||||
inline fun <T> useParcel(block: (Parcel) -> T) = Parcel.obtain().run {
|
|
||||||
try {
|
|
||||||
block(this)
|
|
||||||
} finally {
|
|
||||||
recycle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Parcelable?.toByteArray(parcelableFlags: Int = 0) = useParcel { p ->
|
|
||||||
p.writeParcelable(this, parcelableFlags)
|
|
||||||
p.marshall()
|
|
||||||
}
|
|
||||||
inline fun <reified T : Parcelable> ByteArray.toParcelable(classLoader: ClassLoader? = T::class.java.classLoader) =
|
|
||||||
useParcel { p ->
|
|
||||||
p.unmarshall(this, 0, size)
|
|
||||||
p.setDataPosition(0)
|
|
||||||
p.readParcelable<T>(classLoader)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stream closed caused in NullOutputStream
|
|
||||||
val IOException.isEBADF get() = message == "Stream closed" || (cause as? ErrnoException)?.errno == OsConstants.EBADF
|
|
||||||
@@ -10,6 +10,7 @@ import androidx.core.os.bundleOf
|
|||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.setFragmentResult
|
import androidx.fragment.app.setFragmentResult
|
||||||
import androidx.fragment.app.setFragmentResultListener
|
import androidx.fragment.app.setFragmentResultListener
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,7 +45,7 @@ abstract class AlertDialogFragment<Arg : Parcelable, Ret : Parcelable> :
|
|||||||
fun key(resultKey: String = javaClass.name) = args().putString(KEY_RESULT, resultKey)
|
fun key(resultKey: String = javaClass.name) = args().putString(KEY_RESULT, resultKey)
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog =
|
override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog =
|
||||||
AlertDialog.Builder(requireContext()).also { it.prepare(this) }.create()
|
MaterialAlertDialogBuilder(requireContext()).also { it.prepare(this) }.create()
|
||||||
|
|
||||||
override fun onClick(dialog: DialogInterface?, which: Int) {
|
override fun onClick(dialog: DialogInterface?, which: Int) {
|
||||||
setFragmentResult(resultKey ?: return, Bundle().apply {
|
setFragmentResult(resultKey ?: return, Bundle().apply {
|
||||||
|
|||||||
@@ -2,18 +2,20 @@ package be.mygod.vpnhotspot
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.location.LocationManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.provider.Settings
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.annotation.Size
|
import androidx.annotation.Size
|
||||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.provider.FontRequest
|
|
||||||
import androidx.emoji.text.EmojiCompat
|
|
||||||
import androidx.emoji.text.FontRequestEmojiCompatConfig
|
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import be.mygod.librootkotlinx.NoShellException
|
import be.mygod.librootkotlinx.NoShellException
|
||||||
import be.mygod.vpnhotspot.net.DhcpWorkaround
|
import be.mygod.vpnhotspot.net.DhcpWorkaround
|
||||||
@@ -21,6 +23,7 @@ import be.mygod.vpnhotspot.room.AppDatabase
|
|||||||
import be.mygod.vpnhotspot.root.RootManager
|
import be.mygod.vpnhotspot.root.RootManager
|
||||||
import be.mygod.vpnhotspot.util.DeviceStorageApp
|
import be.mygod.vpnhotspot.util.DeviceStorageApp
|
||||||
import be.mygod.vpnhotspot.util.Services
|
import be.mygod.vpnhotspot.util.Services
|
||||||
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import com.google.firebase.analytics.ktx.ParametersBuilder
|
import com.google.firebase.analytics.ktx.ParametersBuilder
|
||||||
import com.google.firebase.analytics.ktx.analytics
|
import com.google.firebase.analytics.ktx.analytics
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
@@ -39,15 +42,15 @@ class App : Application() {
|
|||||||
lateinit var app: App
|
lateinit var app: App
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
app = this
|
app = this
|
||||||
if (Build.VERSION.SDK_INT >= 24) @SuppressLint("RestrictedApi") {
|
deviceStorage = DeviceStorageApp(this)
|
||||||
deviceStorage = DeviceStorageApp(this)
|
// alternative to PreferenceManager.getDefaultSharedPreferencesName(this)
|
||||||
// alternative to PreferenceManager.getDefaultSharedPreferencesName(this)
|
deviceStorage.moveSharedPreferencesFrom(this, PreferenceManager(this).sharedPreferencesName)
|
||||||
deviceStorage.moveSharedPreferencesFrom(this, PreferenceManager(this).sharedPreferencesName)
|
deviceStorage.moveDatabaseFrom(this, AppDatabase.DB_NAME)
|
||||||
deviceStorage.moveDatabaseFrom(this, AppDatabase.DB_NAME)
|
BootReceiver.migrateIfNecessary()
|
||||||
} else deviceStorage = this
|
|
||||||
Services.init { this }
|
Services.init { this }
|
||||||
|
|
||||||
// overhead of debug mode is minimal: https://github.com/Kotlin/kotlinx.coroutines/blob/f528898/docs/debugging.md#debug-mode
|
// overhead of debug mode is minimal: https://github.com/Kotlin/kotlinx.coroutines/blob/f528898/docs/debugging.md#debug-mode
|
||||||
@@ -57,7 +60,7 @@ class App : Application() {
|
|||||||
"REL" -> { }
|
"REL" -> { }
|
||||||
else -> FirebaseCrashlytics.getInstance().apply {
|
else -> FirebaseCrashlytics.getInstance().apply {
|
||||||
setCustomKey("codename", codename)
|
setCustomKey("codename", codename)
|
||||||
if (Build.VERSION.SDK_INT >= 23) setCustomKey("preview_sdk", Build.VERSION.PREVIEW_SDK_INT)
|
setCustomKey("preview_sdk", Build.VERSION.PREVIEW_SDK_INT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Timber.plant(object : Timber.DebugTree() {
|
Timber.plant(object : Timber.DebugTree() {
|
||||||
@@ -78,18 +81,6 @@ class App : Application() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
ServiceNotification.updateNotificationChannels()
|
ServiceNotification.updateNotificationChannels()
|
||||||
EmojiCompat.init(FontRequestEmojiCompatConfig(deviceStorage, FontRequest(
|
|
||||||
"com.google.android.gms.fonts",
|
|
||||||
"com.google.android.gms",
|
|
||||||
"Noto Color Emoji Compat",
|
|
||||||
R.array.com_google_android_gms_fonts_certs)).apply {
|
|
||||||
setEmojiSpanIndicatorEnabled(BuildConfig.DEBUG)
|
|
||||||
registerInitCallback(object : EmojiCompat.InitCallback() {
|
|
||||||
override fun onInitialized() = Timber.d("EmojiCompat initialized")
|
|
||||||
override fun onFailed(throwable: Throwable?) = Timber.d(throwable)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
EBegFragment.init()
|
|
||||||
if (DhcpWorkaround.shouldEnable) DhcpWorkaround.enable(true)
|
if (DhcpWorkaround.shouldEnable) DhcpWorkaround.enable(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,6 +107,21 @@ class App : Application() {
|
|||||||
Firebase.analytics.logEvent(event, builder.bundle)
|
Firebase.analytics.logEvent(event, builder.bundle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LOH also requires location to be turned on. So does p2p for some reason. Source:
|
||||||
|
* https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/53e0284/service/java/com/android/server/wifi/WifiServiceImpl.java#1204
|
||||||
|
* https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/53e0284/service/java/com/android/server/wifi/WifiSettingsStore.java#228
|
||||||
|
*/
|
||||||
|
inline fun <reified T> startServiceWithLocation(context: Context) {
|
||||||
|
if (Build.VERSION.SDK_INT < 33 && location?.isLocationEnabled != true) try {
|
||||||
|
context.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
|
||||||
|
Toast.makeText(context, R.string.tethering_location_off, Toast.LENGTH_LONG).show()
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
app.logEvent("location_settings") { param("message", e.toString()) }
|
||||||
|
SmartSnackbar.make(R.string.tethering_location_off).show()
|
||||||
|
} else context.startForegroundService(Intent(context, T::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
lateinit var deviceStorage: Application
|
lateinit var deviceStorage: Application
|
||||||
val english by lazy {
|
val english by lazy {
|
||||||
createConfigurationContext(Configuration(resources.configuration).apply {
|
createConfigurationContext(Configuration(resources.configuration).apply {
|
||||||
@@ -124,16 +130,17 @@ class App : Application() {
|
|||||||
}
|
}
|
||||||
val pref by lazy { PreferenceManager.getDefaultSharedPreferences(deviceStorage) }
|
val pref by lazy { PreferenceManager.getDefaultSharedPreferences(deviceStorage) }
|
||||||
val clipboard by lazy { getSystemService<ClipboardManager>()!! }
|
val clipboard by lazy { getSystemService<ClipboardManager>()!! }
|
||||||
|
val location by lazy { getSystemService<LocationManager>() }
|
||||||
|
|
||||||
val hasTouch by lazy { packageManager.hasSystemFeature("android.hardware.faketouch") }
|
val hasTouch by lazy { packageManager.hasSystemFeature("android.hardware.faketouch") }
|
||||||
val customTabsIntent by lazy {
|
val customTabsIntent by lazy {
|
||||||
CustomTabsIntent.Builder().apply {
|
CustomTabsIntent.Builder().apply {
|
||||||
setColorScheme(CustomTabsIntent.COLOR_SCHEME_SYSTEM)
|
setColorScheme(CustomTabsIntent.COLOR_SCHEME_SYSTEM)
|
||||||
setColorSchemeParams(CustomTabsIntent.COLOR_SCHEME_LIGHT, CustomTabColorSchemeParams.Builder().apply {
|
setColorSchemeParams(CustomTabsIntent.COLOR_SCHEME_LIGHT, CustomTabColorSchemeParams.Builder().apply {
|
||||||
setToolbarColor(ContextCompat.getColor(app, R.color.light_colorPrimary))
|
setToolbarColor(resources.getColor(R.color.light_colorPrimary, theme))
|
||||||
}.build())
|
}.build())
|
||||||
setColorSchemeParams(CustomTabsIntent.COLOR_SCHEME_DARK, CustomTabColorSchemeParams.Builder().apply {
|
setColorSchemeParams(CustomTabsIntent.COLOR_SCHEME_DARK, CustomTabColorSchemeParams.Builder().apply {
|
||||||
setToolbarColor(ContextCompat.getColor(app, R.color.dark_colorPrimary))
|
setToolbarColor(resources.getColor(R.color.dark_colorPrimary, theme))
|
||||||
}.build())
|
}.build())
|
||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,31 +5,114 @@ import android.content.ComponentName
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import androidx.core.content.ContextCompat
|
import android.os.Parcelable
|
||||||
|
import be.mygod.librootkotlinx.toByteArray
|
||||||
|
import be.mygod.librootkotlinx.toParcelable
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.util.Services
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.DataInputStream
|
||||||
|
import java.io.DataOutputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
class BootReceiver : BroadcastReceiver() {
|
class BootReceiver : BroadcastReceiver() {
|
||||||
companion object {
|
companion object {
|
||||||
|
const val KEY = "service.autoStart"
|
||||||
|
|
||||||
private val componentName by lazy { ComponentName(app, BootReceiver::class.java) }
|
private val componentName by lazy { ComponentName(app, BootReceiver::class.java) }
|
||||||
var enabled: Boolean
|
private var enabled: Boolean
|
||||||
get() = app.packageManager.getComponentEnabledSetting(componentName) ==
|
get() = app.packageManager.getComponentEnabledSetting(componentName) ==
|
||||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||||
set(value) = app.packageManager.setComponentEnabledSetting(componentName,
|
set(value) = app.packageManager.setComponentEnabledSetting(componentName,
|
||||||
if (value) PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
if (value) PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||||
else PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP)
|
else PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP)
|
||||||
|
private val userEnabled get() = app.pref.getBoolean(KEY, false)
|
||||||
|
fun onUserSettingUpdated(shouldStart: Boolean) {
|
||||||
|
enabled = shouldStart && try {
|
||||||
|
config
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.w(e)
|
||||||
|
null
|
||||||
|
}?.startables?.isEmpty() == false
|
||||||
|
}
|
||||||
|
private fun onConfigUpdated(isNotEmpty: Boolean) {
|
||||||
|
enabled = isNotEmpty && userEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val FILENAME = "bootconfig"
|
||||||
|
private val configFile by lazy { File(app.deviceStorage.noBackupFilesDir, FILENAME) }
|
||||||
|
private val config: Config? get() = try {
|
||||||
|
DataInputStream(configFile.inputStream()).use { it.readBytes().toParcelable() }
|
||||||
|
} catch (_: FileNotFoundException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
private fun updateConfig(work: Config.() -> Unit) = synchronized(BootReceiver) {
|
||||||
|
val config = try {
|
||||||
|
config
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.i("Boot config corrupted", e)
|
||||||
|
null
|
||||||
|
} ?: Config()
|
||||||
|
config.work()
|
||||||
|
DataOutputStream(configFile.outputStream()).use { it.write(config.toByteArray()) }
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
fun add(key: String, value: Startable) = try {
|
||||||
|
updateConfig { startables[key] = value }
|
||||||
|
onConfigUpdated(true)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.w(e)
|
||||||
|
}
|
||||||
|
fun delete(key: String) = try {
|
||||||
|
onConfigUpdated(updateConfig { startables.remove(key) }.startables.isNotEmpty())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.w(e)
|
||||||
|
}
|
||||||
|
inline fun <reified T> add(value: Startable) = add(T::class.java.name, value)
|
||||||
|
inline fun <reified T> delete() = delete(T::class.java.name)
|
||||||
|
|
||||||
|
fun migrateIfNecessary() {
|
||||||
|
val oldFile = File(app.noBackupFilesDir, FILENAME)
|
||||||
|
if (oldFile.canRead()) try {
|
||||||
|
if (!configFile.exists()) oldFile.copyTo(configFile)
|
||||||
|
if (!oldFile.delete()) oldFile.deleteOnExit()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.w(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
private var started = false
|
private var started = false
|
||||||
|
private fun startIfNecessary() {
|
||||||
|
if (started) return
|
||||||
|
val config = try {
|
||||||
|
synchronized(BootReceiver) { config }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.w(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
if (config == null || config.startables.isEmpty()) {
|
||||||
|
enabled = false
|
||||||
|
} else for (startable in config.startables.values) startable.start(app)
|
||||||
|
started = true
|
||||||
|
}
|
||||||
|
fun startIfEnabled() {
|
||||||
|
if (!started && userEnabled) startIfNecessary()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Startable : Parcelable {
|
||||||
|
fun start(context: Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
private data class Config(var startables: MutableMap<String, Startable> = mutableMapOf()) : Parcelable
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
if (started) return
|
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_LOCKED_BOOT_COMPLETED -> started = true
|
Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_LOCKED_BOOT_COMPLETED, Intent.ACTION_MY_PACKAGE_REPLACED -> {
|
||||||
else -> return
|
if (userEnabled) startIfNecessary() else enabled = false
|
||||||
}
|
}
|
||||||
if (Services.p2p != null) {
|
|
||||||
ContextCompat.startForegroundService(context, Intent(context, RepeaterService::class.java))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,26 +23,30 @@ import timber.log.Timber
|
|||||||
*/
|
*/
|
||||||
class EBegFragment : AppCompatDialogFragment() {
|
class EBegFragment : AppCompatDialogFragment() {
|
||||||
companion object : BillingClientStateListener, PurchasesUpdatedListener {
|
companion object : BillingClientStateListener, PurchasesUpdatedListener {
|
||||||
private lateinit var billingClient: BillingClient
|
private val billingClient by lazy {
|
||||||
|
BillingClient.newBuilder(app).apply {
|
||||||
fun init() {
|
|
||||||
billingClient = BillingClient.newBuilder(app).apply {
|
|
||||||
enablePendingPurchases()
|
enablePendingPurchases()
|
||||||
}.setListener(this).build().also { it.startConnection(this) }
|
}.setListener(this).build()
|
||||||
}
|
}
|
||||||
|
private var instance: EBegFragment? = null
|
||||||
|
|
||||||
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
||||||
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
||||||
Timber.e("onBillingSetupFinished: ${billingResult.responseCode}")
|
Timber.e("onBillingSetupFinished: ${billingResult.responseCode}")
|
||||||
} else GlobalScope.launch(Dispatchers.Main.immediate) {
|
} else {
|
||||||
val result = billingClient.queryPurchasesAsync(BillingClient.SkuType.INAPP)
|
instance?.onBillingConnected()
|
||||||
onPurchasesUpdated(result.billingResult, result.purchasesList)
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
|
val result = billingClient.queryPurchasesAsync(QueryPurchasesParams.newBuilder().apply {
|
||||||
|
setProductType(BillingClient.ProductType.INAPP)
|
||||||
|
}.build())
|
||||||
|
onPurchasesUpdated(result.billingResult, result.purchasesList)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBillingServiceDisconnected() {
|
override fun onBillingServiceDisconnected() {
|
||||||
Timber.e("onBillingServiceDisconnected")
|
Timber.e("onBillingServiceDisconnected")
|
||||||
billingClient.startConnection(this)
|
if (instance != null) billingClient.startConnection(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) {
|
override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) {
|
||||||
@@ -64,12 +68,12 @@ class EBegFragment : AppCompatDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var binding: FragmentEbegBinding
|
private lateinit var binding: FragmentEbegBinding
|
||||||
private var skus: List<SkuDetails>? = null
|
private var productDetails: List<ProductDetails>? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
binding.donationsGoogleAndroidMarketSpinner.apply {
|
binding.donationsGoogleAndroidMarketSpinner.apply {
|
||||||
val adapter = ArrayAdapter(context ?: return, android.R.layout.simple_spinner_item,
|
val adapter = ArrayAdapter(context ?: return, android.R.layout.simple_spinner_item,
|
||||||
value?.map { it.price } ?: listOf("…"))
|
value?.map { it.oneTimePurchaseOfferDetails?.formattedPrice } ?: listOf("…"))
|
||||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
setAdapter(adapter)
|
setAdapter(adapter)
|
||||||
}
|
}
|
||||||
@@ -82,27 +86,46 @@ class EBegFragment : AppCompatDialogFragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
dialog!!.setTitle(R.string.settings_misc_donate)
|
dialog!!.setTitle(R.string.settings_misc_donate)
|
||||||
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
|
|
||||||
billingClient.querySkuDetails(SkuDetailsParams.newBuilder().apply {
|
|
||||||
setSkusList(listOf("donate001", "donate002", "donate005", "donate010", "donate020", "donate050",
|
|
||||||
"donate100", "donate200", "donatemax"))
|
|
||||||
setType(BillingClient.SkuType.INAPP)
|
|
||||||
}.build()).apply {
|
|
||||||
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) skus = skuDetailsList else {
|
|
||||||
Timber.e("onSkuDetailsResponse: ${billingResult.responseCode}")
|
|
||||||
SmartSnackbar.make(R.string.donations__google_android_market_not_supported).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.donationsGoogleAndroidMarketDonateButton.setOnClickListener {
|
binding.donationsGoogleAndroidMarketDonateButton.setOnClickListener {
|
||||||
val sku = skus?.getOrNull(binding.donationsGoogleAndroidMarketSpinner.selectedItemPosition)
|
val product = productDetails?.getOrNull(binding.donationsGoogleAndroidMarketSpinner.selectedItemPosition)
|
||||||
if (sku != null) billingClient.launchBillingFlow(requireActivity(), BillingFlowParams.newBuilder().apply {
|
if (product != null) billingClient.launchBillingFlow(requireActivity(), BillingFlowParams.newBuilder().apply {
|
||||||
setSkuDetails(sku)
|
setProductDetailsParamsList(listOf(BillingFlowParams.ProductDetailsParams.newBuilder().apply {
|
||||||
|
setProductDetails(product)
|
||||||
|
}.build()))
|
||||||
}.build()) else SmartSnackbar.make(R.string.donations__google_android_market_not_supported).show()
|
}.build()) else SmartSnackbar.make(R.string.donations__google_android_market_not_supported).show()
|
||||||
}
|
}
|
||||||
@Suppress("ConstantConditionIf")
|
|
||||||
if (BuildConfig.DONATIONS) (binding.donationsMoreStub.inflate() as Button).setOnClickListener {
|
if (BuildConfig.DONATIONS) (binding.donationsMoreStub.inflate() as Button).setOnClickListener {
|
||||||
requireContext().launchUrl("https://mygod.be/donate/")
|
requireContext().launchUrl("https://mygod.be/donate/")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
instance = this
|
||||||
|
billingClient.startConnection(EBegFragment)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onBillingConnected() = viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
billingClient.queryProductDetails(QueryProductDetailsParams.newBuilder().apply {
|
||||||
|
setProductList(listOf(
|
||||||
|
"donate001", "donate002", "donate005", "donate010", "donate020", "donate050",
|
||||||
|
"donate100", "donate200", "donatemax",
|
||||||
|
).map {
|
||||||
|
QueryProductDetailsParams.Product.newBuilder().apply {
|
||||||
|
setProductId(it)
|
||||||
|
setProductType(BillingClient.ProductType.INAPP)
|
||||||
|
}.build()
|
||||||
|
})
|
||||||
|
}.build()).apply {
|
||||||
|
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
||||||
|
Timber.e("queryProductDetails: ${billingResult.responseCode}")
|
||||||
|
SmartSnackbar.make(R.string.donations__google_android_market_not_supported).show()
|
||||||
|
} else productDetails = productDetailsList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
instance = null
|
||||||
|
super.onStop()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ abstract class IpNeighbourMonitoringService : Service(), IpNeighbourMonitor.Call
|
|||||||
this.neighbours = neighbours
|
this.neighbours = neighbours
|
||||||
updateNotification()
|
updateNotification()
|
||||||
}
|
}
|
||||||
protected fun updateNotification() {
|
protected open fun updateNotification() {
|
||||||
val sizeLookup = neighbours.groupBy { it.dev }.mapValues { (_, neighbours) ->
|
val sizeLookup = neighbours.groupBy { it.dev }.mapValues { (_, neighbours) ->
|
||||||
neighbours
|
neighbours
|
||||||
.filter { it.ip is Inet4Address && it.state == IpNeighbour.State.VALID }
|
.filter { it.ip is Inet4Address && it.state == IpNeighbour.State.VALID }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package be.mygod.vpnhotspot
|
package be.mygod.vpnhotspot
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.net.wifi.WifiManager
|
import android.net.wifi.WifiManager
|
||||||
@@ -8,14 +9,12 @@ import androidx.annotation.RequiresApi
|
|||||||
import be.mygod.librootkotlinx.RootServer
|
import be.mygod.librootkotlinx.RootServer
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.net.IpNeighbour
|
import be.mygod.vpnhotspot.net.IpNeighbour
|
||||||
import be.mygod.vpnhotspot.net.TetherType
|
|
||||||
import be.mygod.vpnhotspot.net.TetheringManager
|
|
||||||
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
|
|
||||||
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
||||||
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
|
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
|
||||||
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
|
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
|
||||||
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
|
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
||||||
|
import be.mygod.vpnhotspot.net.wifi.WifiApManager.wifiApState
|
||||||
import be.mygod.vpnhotspot.root.LocalOnlyHotspotCallbacks
|
import be.mygod.vpnhotspot.root.LocalOnlyHotspotCallbacks
|
||||||
import be.mygod.vpnhotspot.root.RootManager
|
import be.mygod.vpnhotspot.root.RootManager
|
||||||
import be.mygod.vpnhotspot.root.WifiApCommands
|
import be.mygod.vpnhotspot.root.WifiApCommands
|
||||||
@@ -24,10 +23,10 @@ import be.mygod.vpnhotspot.util.StickyEvent1
|
|||||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
import be.mygod.vpnhotspot.util.broadcastReceiver
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.net.Inet4Address
|
import java.net.Inet4Address
|
||||||
|
|
||||||
@RequiresApi(26)
|
|
||||||
class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
||||||
companion object {
|
companion object {
|
||||||
const val KEY_USE_SYSTEM = "service.tempHotspot.useSystem"
|
const val KEY_USE_SYSTEM = "service.tempHotspot.useSystem"
|
||||||
@@ -50,7 +49,15 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
|||||||
null -> return // stopped
|
null -> return // stopped
|
||||||
"" -> WifiApManager.cancelLocalOnlyHotspotRequest()
|
"" -> WifiApManager.cancelLocalOnlyHotspotRequest()
|
||||||
}
|
}
|
||||||
reservation?.close() ?: stopService()
|
reservation?.close()
|
||||||
|
stopService()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
class Starter : BootReceiver.Startable {
|
||||||
|
override fun start(context: Context) {
|
||||||
|
context.startForegroundService(Intent(context, LocalOnlyHotspotService::class.java))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,16 +89,17 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
|||||||
private val binder = Binder()
|
private val binder = Binder()
|
||||||
private var reservation: Reservation? = null
|
private var reservation: Reservation? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
|
if (value == null) field?.close()
|
||||||
field = value
|
field = value
|
||||||
if (value != null && !receiverRegistered) {
|
timeoutMonitor?.close()
|
||||||
|
timeoutMonitor = null
|
||||||
|
if (value != null) {
|
||||||
val configuration = binder.configuration
|
val configuration = binder.configuration
|
||||||
if (Build.VERSION.SDK_INT < 30 && configuration!!.isAutoShutdownEnabled) {
|
if (Build.VERSION.SDK_INT < 30 && configuration!!.isAutoShutdownEnabled) {
|
||||||
timeoutMonitor = TetherTimeoutMonitor(configuration.shutdownTimeoutMillis, coroutineContext) {
|
timeoutMonitor = TetherTimeoutMonitor(configuration.shutdownTimeoutMillis, coroutineContext) {
|
||||||
value.close()
|
value.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
|
||||||
receiverRegistered = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private fun onFrameworkFailed(reason: Int) {
|
private fun onFrameworkFailed(reason: Int) {
|
||||||
@@ -116,33 +124,30 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
|||||||
/**
|
/**
|
||||||
* Writes and critical reads to routingManager should be protected with this context.
|
* Writes and critical reads to routingManager should be protected with this context.
|
||||||
*/
|
*/
|
||||||
private val dispatcher = newSingleThreadContext("LocalOnlyHotspotService")
|
private val dispatcher = Dispatchers.IO.limitedParallelism(1)
|
||||||
override val coroutineContext = dispatcher + Job()
|
override val coroutineContext = dispatcher + Job()
|
||||||
private var routingManager: RoutingManager? = null
|
private var routingManager: RoutingManager? = null
|
||||||
private var timeoutMonitor: TetherTimeoutMonitor? = null
|
private var timeoutMonitor: TetherTimeoutMonitor? = null
|
||||||
private var receiverRegistered = false
|
|
||||||
private val receiver = broadcastReceiver { _, intent ->
|
|
||||||
val ifaces = (intent.localOnlyTetheredIfaces ?: return@broadcastReceiver).filter {
|
|
||||||
TetherType.ofInterface(it) != TetherType.WIFI_P2P
|
|
||||||
}
|
|
||||||
Timber.d("onTetherStateChangedLocked: $ifaces")
|
|
||||||
check(ifaces.size <= 1)
|
|
||||||
val iface = ifaces.singleOrNull()
|
|
||||||
binder.iface = iface
|
|
||||||
if (iface.isNullOrEmpty()) stopService() else launch {
|
|
||||||
val routingManager = routingManager
|
|
||||||
if (routingManager == null) {
|
|
||||||
this@LocalOnlyHotspotService.routingManager = RoutingManager.LocalOnly(this@LocalOnlyHotspotService,
|
|
||||||
iface).apply { start() }
|
|
||||||
IpNeighbourMonitor.registerCallback(this@LocalOnlyHotspotService)
|
|
||||||
} else check(iface == routingManager.downstream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override val activeIfaces get() = binder.iface.let { if (it.isNullOrEmpty()) emptyList() else listOf(it) }
|
override val activeIfaces get() = binder.iface.let { if (it.isNullOrEmpty()) emptyList() else listOf(it) }
|
||||||
|
|
||||||
|
private var lastState: Triple<Int, String?, Int>? = null
|
||||||
|
private val receiver = broadcastReceiver { _, intent -> updateState(intent) }
|
||||||
|
private var receiverRegistered = false
|
||||||
|
private fun updateState(intent: Intent) {
|
||||||
|
// based on: https://android.googlesource.com/platform/packages/services/Car/+/72c71d2/service/src/com/android/car/CarProjectionService.java#160
|
||||||
|
lastState = Triple(intent.wifiApState, intent.getStringExtra(WifiApManager.EXTRA_WIFI_AP_INTERFACE_NAME),
|
||||||
|
intent.getIntExtra(WifiApManager.EXTRA_WIFI_AP_FAILURE_REASON, 0))
|
||||||
|
}
|
||||||
|
private fun unregisterStateReceiver() {
|
||||||
|
if (!receiverRegistered) return
|
||||||
|
receiverRegistered = false
|
||||||
|
unregisterReceiver(receiver)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent?) = binder
|
override fun onBind(intent: Intent?) = binder
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
BootReceiver.startIfEnabled()
|
||||||
if (binder.iface != null) return START_STICKY
|
if (binder.iface != null) return START_STICKY
|
||||||
binder.iface = ""
|
binder.iface = ""
|
||||||
updateNotification() // show invisible foreground notification to avoid being killed
|
updateNotification() // show invisible foreground notification to avoid being killed
|
||||||
@@ -150,6 +155,11 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
|||||||
return START_STICKY
|
return START_STICKY
|
||||||
}
|
}
|
||||||
private suspend fun doStart() {
|
private suspend fun doStart() {
|
||||||
|
if (!receiverRegistered) {
|
||||||
|
receiverRegistered = true
|
||||||
|
registerReceiver(receiver, IntentFilter(WifiApManager.WIFI_AP_STATE_CHANGED_ACTION))
|
||||||
|
?.let(this@LocalOnlyHotspotService::updateState)
|
||||||
|
}
|
||||||
if (Build.VERSION.SDK_INT >= 30 && app.pref.getBoolean(KEY_USE_SYSTEM, false)) try {
|
if (Build.VERSION.SDK_INT >= 30 && app.pref.getBoolean(KEY_USE_SYSTEM, false)) try {
|
||||||
RootManager.use {
|
RootManager.use {
|
||||||
Root(it).apply {
|
Root(it).apply {
|
||||||
@@ -170,6 +180,24 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
|||||||
if (reservation == null) onFailed(-2) else {
|
if (reservation == null) onFailed(-2) else {
|
||||||
this@LocalOnlyHotspotService.reservation = Framework(reservation)
|
this@LocalOnlyHotspotService.reservation = Framework(reservation)
|
||||||
}
|
}
|
||||||
|
registerReceiver(null, IntentFilter(WifiApManager.WIFI_AP_STATE_CHANGED_ACTION))
|
||||||
|
?.let(this@LocalOnlyHotspotService::updateState) // attempt to update again
|
||||||
|
val state = lastState
|
||||||
|
unregisterStateReceiver()
|
||||||
|
checkNotNull(state) { "Failed to obtain latest AP state" }
|
||||||
|
val iface = state.second
|
||||||
|
if (state.first != WifiApManager.WIFI_AP_STATE_ENABLED || iface.isNullOrEmpty()) {
|
||||||
|
if (state.first == WifiApManager.WIFI_AP_STATE_FAILED) {
|
||||||
|
SmartSnackbar.make(getString(R.string.tethering_temp_hotspot_failure,
|
||||||
|
WifiApManager.failureReasonLookup(state.third))).show()
|
||||||
|
}
|
||||||
|
return stopService()
|
||||||
|
}
|
||||||
|
binder.iface = iface
|
||||||
|
BootReceiver.add<LocalOnlyHotspotService>(Starter())
|
||||||
|
check(routingManager == null)
|
||||||
|
routingManager = RoutingManager.LocalOnly(this@LocalOnlyHotspotService, iface).apply { start() }
|
||||||
|
IpNeighbourMonitor.registerCallback(this@LocalOnlyHotspotService)
|
||||||
}
|
}
|
||||||
override fun onStopped() {
|
override fun onStopped() {
|
||||||
reservation = null
|
reservation = null
|
||||||
@@ -191,7 +219,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
|||||||
|
|
||||||
override fun onIpNeighbourAvailable(neighbours: Collection<IpNeighbour>) {
|
override fun onIpNeighbourAvailable(neighbours: Collection<IpNeighbour>) {
|
||||||
super.onIpNeighbourAvailable(neighbours)
|
super.onIpNeighbourAvailable(neighbours)
|
||||||
if (Build.VERSION.SDK_INT >= 28) timeoutMonitor?.onClientsChanged(neighbours.none {
|
timeoutMonitor?.onClientsChanged(neighbours.none {
|
||||||
it.ip is Inet4Address && it.state == IpNeighbour.State.VALID
|
it.ip is Inet4Address && it.state == IpNeighbour.State.VALID
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -203,6 +231,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun stopService() {
|
private fun stopService() {
|
||||||
|
BootReceiver.delete<LocalOnlyHotspotService>()
|
||||||
binder.iface = null
|
binder.iface = null
|
||||||
unregisterReceiver()
|
unregisterReceiver()
|
||||||
ServiceNotification.stopForeground(this)
|
ServiceNotification.stopForeground(this)
|
||||||
@@ -210,22 +239,14 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun unregisterReceiver(exit: Boolean = false) {
|
private fun unregisterReceiver(exit: Boolean = false) {
|
||||||
if (receiverRegistered) {
|
IpNeighbourMonitor.unregisterCallback(this)
|
||||||
unregisterReceiver(receiver)
|
timeoutMonitor?.close()
|
||||||
IpNeighbourMonitor.unregisterCallback(this)
|
timeoutMonitor = null
|
||||||
if (Build.VERSION.SDK_INT >= 28) {
|
|
||||||
timeoutMonitor?.close()
|
|
||||||
timeoutMonitor = null
|
|
||||||
}
|
|
||||||
receiverRegistered = false
|
|
||||||
}
|
|
||||||
launch {
|
launch {
|
||||||
routingManager?.stop()
|
routingManager?.stop()
|
||||||
routingManager = null
|
routingManager = null
|
||||||
if (exit) {
|
unregisterStateReceiver()
|
||||||
cancel()
|
if (exit) cancel()
|
||||||
dispatcher.close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,28 +4,60 @@ import android.os.Bundle
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.graphics.Insets
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import be.mygod.vpnhotspot.client.ClientViewModel
|
import be.mygod.vpnhotspot.client.ClientViewModel
|
||||||
import be.mygod.vpnhotspot.client.ClientsFragment
|
import be.mygod.vpnhotspot.client.ClientsFragment
|
||||||
import be.mygod.vpnhotspot.databinding.ActivityMainBinding
|
import be.mygod.vpnhotspot.databinding.ActivityMainBinding
|
||||||
import be.mygod.vpnhotspot.manage.TetheringFragment
|
import be.mygod.vpnhotspot.manage.TetheringFragment
|
||||||
import be.mygod.vpnhotspot.net.IpNeighbour
|
import be.mygod.vpnhotspot.net.IpNeighbour
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
|
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
|
||||||
|
import be.mygod.vpnhotspot.util.AppUpdate
|
||||||
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
|
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
|
||||||
import be.mygod.vpnhotspot.util.Services
|
import be.mygod.vpnhotspot.util.Services
|
||||||
|
import be.mygod.vpnhotspot.util.UpdateChecker
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
|
import com.google.android.material.badge.BadgeDrawable
|
||||||
import com.google.android.material.navigation.NavigationBarView
|
import com.google.android.material.navigation.NavigationBarView
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
import java.net.Inet4Address
|
import java.net.Inet4Address
|
||||||
|
import java.util.concurrent.CancellationException
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListener {
|
class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListener {
|
||||||
lateinit var binding: ActivityMainBinding
|
lateinit var binding: ActivityMainBinding
|
||||||
|
private lateinit var updateItem: MenuItem
|
||||||
|
private lateinit var updateBadge: BadgeDrawable
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(findViewById(android.R.id.content)) { view, insets ->
|
||||||
|
val statusBarInsets = insets.getInsets(WindowInsetsCompat.Type.statusBars())
|
||||||
|
view.setPadding(statusBarInsets.left, statusBarInsets.top, statusBarInsets.right, statusBarInsets.bottom)
|
||||||
|
WindowInsetsCompat.Builder(insets).apply {
|
||||||
|
setInsets(WindowInsetsCompat.Type.statusBars(), Insets.NONE)
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
binding.navigation.setOnItemSelectedListener(this)
|
binding.navigation.setOnItemSelectedListener(this)
|
||||||
|
val badge = binding.navigation.getOrCreateBadge(R.id.navigation_clients).apply {
|
||||||
|
backgroundColor = resources.getColor(R.color.colorSecondary, theme)
|
||||||
|
badgeTextColor = resources.getColor(androidx.appcompat.R.color.primary_text_default_material_light, theme)
|
||||||
|
}
|
||||||
|
updateItem = binding.navigation.menu.findItem(R.id.navigation_update)
|
||||||
|
updateItem.isCheckable = false
|
||||||
|
updateBadge = binding.navigation.getOrCreateBadge(R.id.navigation_update).apply {
|
||||||
|
backgroundColor = resources.getColor(R.color.colorSecondary, theme)
|
||||||
|
badgeTextColor = resources.getColor(androidx.appcompat.R.color.primary_text_default_material_light, theme)
|
||||||
|
}
|
||||||
if (savedInstanceState == null) displayFragment(TetheringFragment())
|
if (savedInstanceState == null) displayFragment(TetheringFragment())
|
||||||
val model by viewModels<ClientViewModel>()
|
val model by viewModels<ClientViewModel>()
|
||||||
lifecycle.addObserver(model)
|
lifecycle.addObserver(model)
|
||||||
@@ -34,38 +66,69 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
|||||||
val count = clients.count {
|
val count = clients.count {
|
||||||
it.ip.any { (ip, state) -> ip is Inet4Address && state == IpNeighbour.State.VALID }
|
it.ip.any { (ip, state) -> ip is Inet4Address && state == IpNeighbour.State.VALID }
|
||||||
}
|
}
|
||||||
if (count > 0) binding.navigation.getOrCreateBadge(R.id.navigation_clients).apply {
|
badge.isVisible = count > 0
|
||||||
backgroundColor = ContextCompat.getColor(this@MainActivity, R.color.colorSecondary)
|
badge.number = count
|
||||||
badgeTextColor = ContextCompat.getColor(this@MainActivity, R.color.primary_text_default_material_light)
|
|
||||||
number = count
|
|
||||||
} else binding.navigation.removeBadge(R.id.navigation_clients)
|
|
||||||
}
|
}
|
||||||
SmartSnackbar.Register(binding.fragmentHolder)
|
SmartSnackbar.Register(binding.fragmentHolder)
|
||||||
WifiDoubleLock.ActivityListener(this)
|
WifiDoubleLock.ActivityListener(this)
|
||||||
|
lifecycleScope.launch {
|
||||||
|
BootReceiver.startIfEnabled()
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
onAppUpdateAvailable(null)
|
||||||
|
try {
|
||||||
|
UpdateChecker.check().collect(this@MainActivity::onAppUpdateAvailable)
|
||||||
|
} catch (_: CancellationException) {
|
||||||
|
} catch (e: AppUpdate.IgnoredException) {
|
||||||
|
Timber.d(e)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.w(e)
|
||||||
|
SmartSnackbar.make(e).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var lastUpdate: AppUpdate? = null
|
||||||
|
private fun onAppUpdateAvailable(update: AppUpdate?) {
|
||||||
|
lastUpdate = update
|
||||||
|
updateItem.isVisible = update != null
|
||||||
|
if (update == null) {
|
||||||
|
updateItem.isEnabled = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateItem.isEnabled = update.downloaded != false
|
||||||
|
updateItem.setIcon(when (update.downloaded) {
|
||||||
|
null -> R.drawable.ic_action_update
|
||||||
|
false -> R.drawable.ic_file_downloading
|
||||||
|
true -> R.drawable.ic_action_autorenew
|
||||||
|
})
|
||||||
|
updateItem.title = update.message ?: getText(R.string.title_update)
|
||||||
|
updateBadge.isVisible = when (val days = update.stalenessDays) {
|
||||||
|
null -> false
|
||||||
|
else -> {
|
||||||
|
if (days > 0) updateBadge.number = days else updateBadge.clearNumber()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigationItemSelected(item: MenuItem) = when (item.itemId) {
|
override fun onNavigationItemSelected(item: MenuItem) = when (item.itemId) {
|
||||||
R.id.navigation_clients -> {
|
R.id.navigation_clients -> {
|
||||||
if (!item.isChecked) {
|
displayFragment(ClientsFragment())
|
||||||
item.isChecked = true
|
|
||||||
displayFragment(ClientsFragment())
|
|
||||||
}
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.navigation_tethering -> {
|
R.id.navigation_tethering -> {
|
||||||
if (!item.isChecked) {
|
displayFragment(TetheringFragment())
|
||||||
item.isChecked = true
|
|
||||||
displayFragment(TetheringFragment())
|
|
||||||
}
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.navigation_settings -> {
|
R.id.navigation_settings -> {
|
||||||
if (!item.isChecked) {
|
displayFragment(SettingsPreferenceFragment())
|
||||||
item.isChecked = true
|
|
||||||
displayFragment(SettingsPreferenceFragment())
|
|
||||||
}
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
R.id.navigation_update -> {
|
||||||
|
lastUpdate!!.updateForResult(this, 1)
|
||||||
|
false
|
||||||
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ package be.mygod.vpnhotspot
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.annotation.TargetApi
|
import android.annotation.TargetApi
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.location.LocationManager
|
||||||
|
import android.net.MacAddress
|
||||||
|
import android.net.wifi.ScanResult
|
||||||
import android.net.wifi.WpsInfo
|
import android.net.wifi.WpsInfo
|
||||||
import android.net.wifi.p2p.*
|
import android.net.wifi.p2p.*
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -16,19 +20,27 @@ import androidx.annotation.StringRes
|
|||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||||
|
import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toLong
|
||||||
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
|
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
|
||||||
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
|
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
|
||||||
|
import be.mygod.vpnhotspot.net.wifi.VendorElements
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.deletePersistentGroup
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.deletePersistentGroup
|
||||||
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestConnectionInfo
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestDeviceAddress
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestDeviceAddress
|
||||||
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestGroupInfo
|
||||||
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestP2pState
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupInfo
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupInfo
|
||||||
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setVendorElements
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps
|
||||||
|
import be.mygod.vpnhotspot.net.wifi.WifiSsidCompat
|
||||||
import be.mygod.vpnhotspot.root.RepeaterCommands
|
import be.mygod.vpnhotspot.root.RepeaterCommands
|
||||||
import be.mygod.vpnhotspot.root.RootManager
|
import be.mygod.vpnhotspot.root.RootManager
|
||||||
import be.mygod.vpnhotspot.util.*
|
import be.mygod.vpnhotspot.util.*
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
@@ -42,12 +54,14 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
|||||||
const val KEY_SAFE_MODE = "service.repeater.safeMode"
|
const val KEY_SAFE_MODE = "service.repeater.safeMode"
|
||||||
|
|
||||||
private const val KEY_NETWORK_NAME = "service.repeater.networkName"
|
private const val KEY_NETWORK_NAME = "service.repeater.networkName"
|
||||||
|
private const val KEY_NETWORK_NAME_HEX = "service.repeater.networkNameHex"
|
||||||
private const val KEY_PASSPHRASE = "service.repeater.passphrase"
|
private const val KEY_PASSPHRASE = "service.repeater.passphrase"
|
||||||
private const val KEY_OPERATING_BAND = "service.repeater.band.v4"
|
private const val KEY_OPERATING_BAND = "service.repeater.band.v4"
|
||||||
private const val KEY_OPERATING_CHANNEL = "service.repeater.oc.v3"
|
private const val KEY_OPERATING_CHANNEL = "service.repeater.oc.v3"
|
||||||
private const val KEY_AUTO_SHUTDOWN = "service.repeater.autoShutdown"
|
private const val KEY_AUTO_SHUTDOWN = "service.repeater.autoShutdown"
|
||||||
private const val KEY_SHUTDOWN_TIMEOUT = "service.repeater.shutdownTimeout"
|
private const val KEY_SHUTDOWN_TIMEOUT = "service.repeater.shutdownTimeout"
|
||||||
private const val KEY_DEVICE_ADDRESS = "service.repeater.mac"
|
private const val KEY_DEVICE_ADDRESS = "service.repeater.mac"
|
||||||
|
private const val KEY_VENDOR_ELEMENTS = "service.repeater.vendorElements"
|
||||||
|
|
||||||
var persistentSupported = false
|
var persistentSupported = false
|
||||||
|
|
||||||
@@ -61,17 +75,26 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
|||||||
val safeModeConfigurable get() = Build.VERSION.SDK_INT >= 29 && hasP2pValidateName
|
val safeModeConfigurable get() = Build.VERSION.SDK_INT >= 29 && hasP2pValidateName
|
||||||
val safeMode get() = Build.VERSION.SDK_INT >= 29 &&
|
val safeMode get() = Build.VERSION.SDK_INT >= 29 &&
|
||||||
(!hasP2pValidateName || app.pref.getBoolean(KEY_SAFE_MODE, true))
|
(!hasP2pValidateName || app.pref.getBoolean(KEY_SAFE_MODE, true))
|
||||||
private val mNetworkName by lazy { UnblockCentral.WifiP2pConfig_Builder_mNetworkName }
|
@get:RequiresApi(29)
|
||||||
|
private val mNetworkName by lazy @TargetApi(29) { UnblockCentral.WifiP2pConfig_Builder_mNetworkName }
|
||||||
|
|
||||||
var networkName: String?
|
var networkName: WifiSsidCompat?
|
||||||
get() = app.pref.getString(KEY_NETWORK_NAME, null)
|
get() = app.pref.getString(KEY_NETWORK_NAME, null).let { legacy ->
|
||||||
set(value) = app.pref.edit { putString(KEY_NETWORK_NAME, value) }
|
if (legacy != null) WifiSsidCompat.fromUtf8Text(legacy).also {
|
||||||
|
app.pref.edit {
|
||||||
|
putString(KEY_NETWORK_NAME_HEX, it!!.hex)
|
||||||
|
remove(KEY_NETWORK_NAME)
|
||||||
|
}
|
||||||
|
} else WifiSsidCompat.fromHex(app.pref.getString(KEY_NETWORK_NAME_HEX, null))
|
||||||
|
}
|
||||||
|
set(value) = app.pref.edit { putString(KEY_NETWORK_NAME_HEX, value?.hex) }
|
||||||
var passphrase: String?
|
var passphrase: String?
|
||||||
get() = app.pref.getString(KEY_PASSPHRASE, null)
|
get() = app.pref.getString(KEY_PASSPHRASE, null)
|
||||||
set(value) = app.pref.edit { putString(KEY_PASSPHRASE, value) }
|
set(value) = app.pref.edit { putString(KEY_PASSPHRASE, value) }
|
||||||
var operatingBand: Int
|
var operatingBand: Int
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
get() = app.pref.getInt(KEY_OPERATING_BAND, SoftApConfigurationCompat.BAND_LEGACY)
|
get() = app.pref.getInt(KEY_OPERATING_BAND, SoftApConfigurationCompat.BAND_LEGACY) and
|
||||||
|
SoftApConfigurationCompat.BAND_LEGACY
|
||||||
set(value) = app.pref.edit { putInt(KEY_OPERATING_BAND, value) }
|
set(value) = app.pref.edit { putInt(KEY_OPERATING_BAND, value) }
|
||||||
var operatingChannel: Int
|
var operatingChannel: Int
|
||||||
get() {
|
get() {
|
||||||
@@ -85,17 +108,24 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
|||||||
var shutdownTimeoutMillis: Long
|
var shutdownTimeoutMillis: Long
|
||||||
get() = app.pref.getLong(KEY_SHUTDOWN_TIMEOUT, 0)
|
get() = app.pref.getLong(KEY_SHUTDOWN_TIMEOUT, 0)
|
||||||
set(value) = app.pref.edit { putLong(KEY_SHUTDOWN_TIMEOUT, value) }
|
set(value) = app.pref.edit { putLong(KEY_SHUTDOWN_TIMEOUT, value) }
|
||||||
var deviceAddress: MacAddressCompat?
|
var deviceAddress: MacAddress?
|
||||||
get() = try {
|
get() = try {
|
||||||
MacAddressCompat(app.pref.getLong(KEY_DEVICE_ADDRESS, MacAddressCompat.ANY_ADDRESS.addr)).run {
|
MacAddressCompat(app.pref.getLong(KEY_DEVICE_ADDRESS, 2)).run {
|
||||||
validate()
|
require(addr and ((1L shl 48) - 1).inv() == 0L)
|
||||||
if (this == MacAddressCompat.ANY_ADDRESS) null else this
|
if (addr == 2L) null else toPlatform()
|
||||||
}
|
}
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
Timber.w(e)
|
Timber.w(e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
set(value) = app.pref.edit { putLong(KEY_DEVICE_ADDRESS, (value ?: MacAddressCompat.ANY_ADDRESS).addr) }
|
set(value) = app.pref.edit {
|
||||||
|
putLong(KEY_DEVICE_ADDRESS, (value ?: MacAddressCompat.ANY_ADDRESS).toLong())
|
||||||
|
}
|
||||||
|
@get:RequiresApi(33)
|
||||||
|
@set:RequiresApi(33)
|
||||||
|
var vendorElements: List<ScanResult.InformationElement>
|
||||||
|
get() = VendorElements.deserialize(app.pref.getString(KEY_VENDOR_ELEMENTS, null))
|
||||||
|
set(value) = app.pref.edit { putString(KEY_VENDOR_ELEMENTS, VendorElements.serialize(value)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Status {
|
enum class Status {
|
||||||
@@ -110,19 +140,17 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
|||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
groupChanged(value)
|
groupChanged(value)
|
||||||
if (Build.VERSION.SDK_INT >= 28) value?.clientList?.let {
|
value?.clientList?.let { timeoutMonitor?.onClientsChanged(it.isEmpty()) }
|
||||||
timeoutMonitor?.onClientsChanged(it.isEmpty())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val groupChanged = StickyEvent1 { group }
|
val groupChanged = StickyEvent1 { group }
|
||||||
|
|
||||||
suspend fun obtainDeviceAddress(): MacAddressCompat? {
|
suspend fun obtainDeviceAddress(): MacAddress? {
|
||||||
return if (Build.VERSION.SDK_INT >= 29) p2pManager.requestDeviceAddress(channel ?: return null) ?: try {
|
return if (Build.VERSION.SDK_INT >= 29) p2pManager.requestDeviceAddress(channel ?: return null) ?: try {
|
||||||
RootManager.use { it.execute(RepeaterCommands.RequestDeviceAddress()) }
|
RootManager.use { it.execute(RepeaterCommands.RequestDeviceAddress()) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.d(e)
|
Timber.d(e)
|
||||||
null
|
null
|
||||||
}?.let { MacAddressCompat(it.value) } else lastMac?.let { MacAddressCompat.fromString(it) }
|
} else lastMac?.let { MacAddress.fromString(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NewApi") // networkId is available since Android 4.2
|
@SuppressLint("NewApi") // networkId is available since Android 4.2
|
||||||
@@ -134,7 +162,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
|||||||
val ownedGroups = filter {
|
val ownedGroups = filter {
|
||||||
if (!it.isGroupOwner) return@filter false
|
if (!it.isGroupOwner) return@filter false
|
||||||
val address = try {
|
val address = try {
|
||||||
MacAddressCompat.fromString(it.owner.deviceAddress)
|
MacAddress.fromString(it.owner.deviceAddress)
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
Timber.w(e)
|
Timber.w(e)
|
||||||
return@filter true // assuming it was changed due to privacy
|
return@filter true // assuming it was changed due to privacy
|
||||||
@@ -199,6 +227,13 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
class Starter : BootReceiver.Startable {
|
||||||
|
override fun start(context: Context) {
|
||||||
|
context.startForegroundService(Intent(context, RepeaterService::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val p2pManager get() = Services.p2p!!
|
private val p2pManager get() = Services.p2p!!
|
||||||
private var channel: WifiP2pManager.Channel? = null
|
private var channel: WifiP2pManager.Channel? = null
|
||||||
private val binder = Binder()
|
private val binder = Binder()
|
||||||
@@ -212,6 +247,9 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
|||||||
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> onP2pConnectionChanged(
|
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> onP2pConnectionChanged(
|
||||||
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO),
|
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO),
|
||||||
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP))
|
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP))
|
||||||
|
LocationManager.MODE_CHANGED_ACTION -> @TargetApi(30) {
|
||||||
|
onLocationModeChanged(intent.getBooleanExtra(LocationManager.EXTRA_LOCATION_ENABLED, false))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private val deviceListener = broadcastReceiver { _, intent ->
|
private val deviceListener = broadcastReceiver { _, intent ->
|
||||||
@@ -222,7 +260,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
|||||||
/**
|
/**
|
||||||
* Writes and critical reads to routingManager should be protected with this context.
|
* Writes and critical reads to routingManager should be protected with this context.
|
||||||
*/
|
*/
|
||||||
private val dispatcher = newSingleThreadContext("RepeaterService")
|
private val dispatcher = Dispatchers.IO.limitedParallelism(1)
|
||||||
override val coroutineContext = dispatcher + Job()
|
override val coroutineContext = dispatcher + Job()
|
||||||
private var routingManager: RoutingManager? = null
|
private var routingManager: RoutingManager? = null
|
||||||
private var persistNextGroup = false
|
private var persistNextGroup = false
|
||||||
@@ -286,6 +324,36 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
|||||||
} else SmartSnackbar.make(formatReason(R.string.repeater_set_oc_failure, reason)).show()
|
} else SmartSnackbar.make(formatReason(R.string.repeater_set_oc_failure, reason)).show()
|
||||||
} else SmartSnackbar.make(R.string.repeater_failure_disconnected).show()
|
} else SmartSnackbar.make(R.string.repeater_failure_disconnected).show()
|
||||||
}
|
}
|
||||||
|
@RequiresApi(33)
|
||||||
|
private suspend fun setVendorElements(ve: List<ScanResult.InformationElement> = vendorElements) {
|
||||||
|
val channel = channel
|
||||||
|
if (channel != null) {
|
||||||
|
val reason = try {
|
||||||
|
p2pManager.setVendorElements(channel, ve) ?: return
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
SmartSnackbar.make(getString(R.string.repeater_set_vendor_elements_failure, e.message)).show()
|
||||||
|
return
|
||||||
|
} catch (e: UnsupportedOperationException) {
|
||||||
|
if (ve.isNotEmpty()) {
|
||||||
|
SmartSnackbar.make(getString(R.string.repeater_set_vendor_elements_failure, e.message)).show()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (reason == WifiP2pManager.ERROR) {
|
||||||
|
val rootReason = try {
|
||||||
|
RootManager.use {
|
||||||
|
if (deinitPending.getAndSet(false)) it.execute(RepeaterCommands.Deinit())
|
||||||
|
it.execute(RepeaterCommands.SetVendorElements(ve))
|
||||||
|
} ?: return
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.w(e)
|
||||||
|
SmartSnackbar.make(e).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
SmartSnackbar.make(formatReason(R.string.repeater_set_vendor_elements_failure, rootReason.value)).show()
|
||||||
|
} else SmartSnackbar.make(formatReason(R.string.repeater_set_vendor_elements_failure, reason)).show()
|
||||||
|
} else SmartSnackbar.make(R.string.repeater_failure_disconnected).show()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onChannelDisconnected() {
|
override fun onChannelDisconnected() {
|
||||||
channel = null
|
channel = null
|
||||||
@@ -302,9 +370,34 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||||
if (!safeMode) when (key) {
|
when (key) {
|
||||||
KEY_OPERATING_CHANNEL -> launch { setOperatingChannel() }
|
KEY_OPERATING_CHANNEL -> if (!safeMode) launch { setOperatingChannel() }
|
||||||
KEY_SAFE_MODE -> deinitPending.set(true)
|
KEY_VENDOR_ELEMENTS -> if (Build.VERSION.SDK_INT >= 33) launch { setVendorElements() }
|
||||||
|
KEY_SAFE_MODE -> if (!safeMode) deinitPending.set(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var p2pPoller: Job? = null
|
||||||
|
@RequiresApi(30)
|
||||||
|
private fun onLocationModeChanged(enabled: Boolean) = if (enabled) p2pPoller?.cancel() else {
|
||||||
|
SmartSnackbar.make(R.string.repeater_location_off).apply {
|
||||||
|
action(R.string.repeater_location_off_configure) {
|
||||||
|
it.context.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
|
p2pPoller = launch(start = CoroutineStart.UNDISPATCHED) {
|
||||||
|
while (true) {
|
||||||
|
delay(1000)
|
||||||
|
val channel = channel ?: return@launch
|
||||||
|
coroutineScope {
|
||||||
|
launch(start = CoroutineStart.UNDISPATCHED) {
|
||||||
|
if (p2pManager.requestP2pState(channel) == WifiP2pManager.WIFI_P2P_STATE_DISABLED) cleanLocked()
|
||||||
|
}
|
||||||
|
val info = async(start = CoroutineStart.UNDISPATCHED) { p2pManager.requestConnectionInfo(channel) }
|
||||||
|
val group = p2pManager.requestGroupInfo(channel)
|
||||||
|
onP2pConnectionChanged(info.await(), group)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,35 +405,32 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
|||||||
* startService Step 1
|
* startService Step 1
|
||||||
*/
|
*/
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
BootReceiver.startIfEnabled()
|
||||||
if (status != Status.IDLE) return START_NOT_STICKY
|
if (status != Status.IDLE) return START_NOT_STICKY
|
||||||
val channel = channel ?: return START_NOT_STICKY.also { stopSelf() }
|
val channel = channel ?: return START_NOT_STICKY.also { stopSelf() }
|
||||||
status = Status.STARTING
|
status = Status.STARTING
|
||||||
// bump self to foreground location service (API 29+) to use location later, also to avoid getting killed
|
// bump self to foreground location service (API 29+) to use location later, also to avoid getting killed
|
||||||
if (Build.VERSION.SDK_INT >= 26) showNotification()
|
showNotification()
|
||||||
launch {
|
launch {
|
||||||
registerReceiver(receiver, intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
|
val filter = intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
|
||||||
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION))
|
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)
|
||||||
|
if (Build.VERSION.SDK_INT in 30 until 33) filter.addAction(LocationManager.MODE_CHANGED_ACTION)
|
||||||
|
registerReceiver(receiver, filter)
|
||||||
receiverRegistered = true
|
receiverRegistered = true
|
||||||
try {
|
val group = p2pManager.requestGroupInfo(channel)
|
||||||
p2pManager.requestGroupInfo(channel) {
|
when {
|
||||||
when {
|
group == null -> doStart()
|
||||||
it == null -> doStart()
|
group.isGroupOwner -> if (routingManager == null) doStartLocked(group)
|
||||||
it.isGroupOwner -> launch { if (routingManager == null) doStartLocked(it) }
|
else -> {
|
||||||
else -> {
|
Timber.i("Removing old group ($group)")
|
||||||
Timber.i("Removing old group ($it)")
|
p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
|
||||||
p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
|
override fun onSuccess() {
|
||||||
override fun onSuccess() {
|
launch { doStart() }
|
||||||
doStart()
|
|
||||||
}
|
|
||||||
override fun onFailure(reason: Int) =
|
|
||||||
startFailure(formatReason(R.string.repeater_remove_old_group_failure, reason))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
override fun onFailure(reason: Int) =
|
||||||
|
startFailure(formatReason(R.string.repeater_remove_old_group_failure, reason))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} catch (e: SecurityException) {
|
|
||||||
Timber.w(e)
|
|
||||||
startFailure(e.readableMessage)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
@@ -348,56 +438,59 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
|||||||
/**
|
/**
|
||||||
* startService Step 2 (if a group isn't already available)
|
* startService Step 2 (if a group isn't already available)
|
||||||
*/
|
*/
|
||||||
private fun doStart() = launch {
|
private suspend fun doStart() {
|
||||||
val listener = object : WifiP2pManager.ActionListener {
|
val listener = object : WifiP2pManager.ActionListener {
|
||||||
override fun onFailure(reason: Int) {
|
override fun onFailure(reason: Int) {
|
||||||
startFailure(formatReason(R.string.repeater_create_group_failure, reason),
|
startFailure(formatReason(R.string.repeater_create_group_failure, reason),
|
||||||
showWifiEnable = reason == WifiP2pManager.BUSY)
|
showWifiEnable = reason == WifiP2pManager.BUSY)
|
||||||
}
|
}
|
||||||
override fun onSuccess() { } // wait for WIFI_P2P_CONNECTION_CHANGED_ACTION to fire to go to step 3
|
override fun onSuccess() {
|
||||||
|
// wait for WIFI_P2P_CONNECTION_CHANGED_ACTION to fire to go to step 3
|
||||||
|
// in order for this to happen, we need to make sure that the callbacks are firing
|
||||||
|
if (Build.VERSION.SDK_INT in 30 until 33) onLocationModeChanged(app.location?.isLocationEnabled == true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val channel = channel ?: return@launch listener.onFailure(WifiP2pManager.BUSY)
|
val channel = channel ?: return listener.onFailure(WifiP2pManager.BUSY)
|
||||||
if (!safeMode) {
|
if (!safeMode) {
|
||||||
binder.fetchPersistentGroup()
|
binder.fetchPersistentGroup()
|
||||||
setOperatingChannel()
|
setOperatingChannel()
|
||||||
}
|
}
|
||||||
val networkName = networkName
|
if (Build.VERSION.SDK_INT >= 33) setVendorElements()
|
||||||
|
val networkName = networkName?.toString()
|
||||||
val passphrase = passphrase
|
val passphrase = passphrase
|
||||||
try {
|
@SuppressLint("MissingPermission") // missing permission will simply leading to returning ERROR
|
||||||
if (!safeMode || networkName == null || passphrase == null) {
|
if (!safeMode || networkName == null || passphrase.isNullOrEmpty()) {
|
||||||
persistNextGroup = true
|
persistNextGroup = true
|
||||||
p2pManager.createGroup(channel, listener)
|
p2pManager.createGroup(channel, listener)
|
||||||
} else @TargetApi(29) {
|
} else @TargetApi(29) {
|
||||||
p2pManager.createGroup(channel, WifiP2pConfig.Builder().apply {
|
p2pManager.createGroup(channel, WifiP2pConfig.Builder().apply {
|
||||||
|
try {
|
||||||
|
mNetworkName.set(this, networkName) // bypass networkName check
|
||||||
|
} catch (e: ReflectiveOperationException) {
|
||||||
|
Timber.w(e)
|
||||||
try {
|
try {
|
||||||
mNetworkName.set(this, networkName) // bypass networkName check
|
|
||||||
} catch (e: ReflectiveOperationException) {
|
|
||||||
Timber.w(e)
|
|
||||||
setNetworkName(networkName)
|
setNetworkName(networkName)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
Timber.w(e)
|
||||||
|
return startFailure(e.readableMessage)
|
||||||
}
|
}
|
||||||
setPassphrase(passphrase)
|
}
|
||||||
when (val oc = operatingChannel) {
|
setPassphrase(passphrase)
|
||||||
0 -> setGroupOperatingBand(when (val band = operatingBand) {
|
when (val oc = operatingChannel) {
|
||||||
SoftApConfigurationCompat.BAND_2GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_2GHZ
|
0 -> setGroupOperatingBand(when (val band = operatingBand) {
|
||||||
SoftApConfigurationCompat.BAND_5GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_5GHZ
|
SoftApConfigurationCompat.BAND_2GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_2GHZ
|
||||||
else -> {
|
SoftApConfigurationCompat.BAND_5GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_5GHZ
|
||||||
require(SoftApConfigurationCompat.isLegacyEitherBand(band)) { "Unknown band $band" }
|
|
||||||
WifiP2pConfig.GROUP_OWNER_BAND_AUTO
|
|
||||||
}
|
|
||||||
})
|
|
||||||
else -> {
|
else -> {
|
||||||
setGroupOperatingFrequency(SoftApConfigurationCompat.channelToFrequency(operatingBand, oc))
|
require(SoftApConfigurationCompat.isLegacyEitherBand(band)) { "Unknown band $band" }
|
||||||
|
WifiP2pConfig.GROUP_OWNER_BAND_AUTO
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
else -> {
|
||||||
|
setGroupOperatingFrequency(SoftApConfigurationCompat.channelToFrequency(operatingBand, oc))
|
||||||
}
|
}
|
||||||
setDeviceAddress(deviceAddress?.toPlatform())
|
}
|
||||||
}.build(), listener)
|
setDeviceAddress(deviceAddress)
|
||||||
}
|
}.build(), listener)
|
||||||
} catch (e: SecurityException) {
|
|
||||||
Timber.w(e)
|
|
||||||
startFailure(e.readableMessage)
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
Timber.w(e)
|
|
||||||
startFailure(e.readableMessage)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@@ -426,7 +519,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
|||||||
}
|
}
|
||||||
binder.group = group
|
binder.group = group
|
||||||
if (persistNextGroup) {
|
if (persistNextGroup) {
|
||||||
networkName = group.networkName
|
networkName = WifiSsidCompat.fromUtf8Text(group.networkName)
|
||||||
passphrase = group.passphrase
|
passphrase = group.passphrase
|
||||||
persistNextGroup = false
|
persistNextGroup = false
|
||||||
}
|
}
|
||||||
@@ -434,6 +527,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
|||||||
routingManager = RoutingManager.LocalOnly(this@RepeaterService, group.`interface`!!).apply { start() }
|
routingManager = RoutingManager.LocalOnly(this@RepeaterService, group.`interface`!!).apply { start() }
|
||||||
status = Status.ACTIVE
|
status = Status.ACTIVE
|
||||||
showNotification(group)
|
showNotification(group)
|
||||||
|
BootReceiver.add<RepeaterService>(Starter())
|
||||||
}
|
}
|
||||||
private fun startFailure(msg: CharSequence, group: WifiP2pGroup? = null, showWifiEnable: Boolean = false) {
|
private fun startFailure(msg: CharSequence, group: WifiP2pGroup? = null, showWifiEnable: Boolean = false) {
|
||||||
SmartSnackbar.make(msg).apply {
|
SmartSnackbar.make(msg).apply {
|
||||||
@@ -451,7 +545,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
|||||||
if (group == null) emptyMap() else mapOf(Pair(group.`interface`, group.clientList?.size ?: 0)))
|
if (group == null) emptyMap() else mapOf(Pair(group.`interface`, group.clientList?.size ?: 0)))
|
||||||
|
|
||||||
private fun removeGroup() {
|
private fun removeGroup() {
|
||||||
p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
|
p2pManager.removeGroup(channel ?: return, object : WifiP2pManager.ActionListener {
|
||||||
override fun onSuccess() {
|
override fun onSuccess() {
|
||||||
launch { cleanLocked() }
|
launch { cleanLocked() }
|
||||||
}
|
}
|
||||||
@@ -459,19 +553,19 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
|||||||
if (reason != WifiP2pManager.BUSY) {
|
if (reason != WifiP2pManager.BUSY) {
|
||||||
SmartSnackbar.make(formatReason(R.string.repeater_remove_group_failure, reason)).show()
|
SmartSnackbar.make(formatReason(R.string.repeater_remove_group_failure, reason)).show()
|
||||||
} // else assuming it's already gone
|
} // else assuming it's already gone
|
||||||
launch { cleanLocked() }
|
onSuccess()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
private fun cleanLocked() {
|
private fun cleanLocked() {
|
||||||
|
BootReceiver.delete<RepeaterService>()
|
||||||
if (receiverRegistered) {
|
if (receiverRegistered) {
|
||||||
ensureReceiverUnregistered(receiver)
|
ensureReceiverUnregistered(receiver)
|
||||||
|
p2pPoller?.cancel()
|
||||||
receiverRegistered = false
|
receiverRegistered = false
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT >= 28) {
|
timeoutMonitor?.close()
|
||||||
timeoutMonitor?.close()
|
timeoutMonitor = null
|
||||||
timeoutMonitor = null
|
|
||||||
}
|
|
||||||
routingManager?.stop()
|
routingManager?.stop()
|
||||||
routingManager = null
|
routingManager = null
|
||||||
status = Status.IDLE
|
status = Status.IDLE
|
||||||
@@ -484,12 +578,11 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
|||||||
launch { // force clean to prevent leakage
|
launch { // force clean to prevent leakage
|
||||||
cleanLocked()
|
cleanLocked()
|
||||||
cancel()
|
cancel()
|
||||||
dispatcher.close()
|
|
||||||
}
|
}
|
||||||
app.pref.unregisterOnSharedPreferenceChangeListener(this)
|
app.pref.unregisterOnSharedPreferenceChangeListener(this)
|
||||||
if (Build.VERSION.SDK_INT < 29) unregisterReceiver(deviceListener)
|
if (Build.VERSION.SDK_INT < 29) unregisterReceiver(deviceListener)
|
||||||
status = Status.DESTROYED
|
status = Status.DESTROYED
|
||||||
if (Build.VERSION.SDK_INT >= 27) channel?.close()
|
channel?.close()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package be.mygod.vpnhotspot
|
package be.mygod.vpnhotspot
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.net.Routing
|
import be.mygod.vpnhotspot.net.Routing
|
||||||
@@ -15,15 +14,11 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
|
|||||||
companion object {
|
companion object {
|
||||||
private const val KEY_MASQUERADE_MODE = "service.masqueradeMode"
|
private const val KEY_MASQUERADE_MODE = "service.masqueradeMode"
|
||||||
var masqueradeMode: Routing.MasqueradeMode
|
var masqueradeMode: Routing.MasqueradeMode
|
||||||
@TargetApi(28) get() = app.pref.run {
|
get() = app.pref.run {
|
||||||
getString(KEY_MASQUERADE_MODE, null)?.let { return@run Routing.MasqueradeMode.valueOf(it) }
|
getString(KEY_MASQUERADE_MODE, null)?.let { return@run Routing.MasqueradeMode.valueOf(it) }
|
||||||
if (getBoolean("service.masquerade", true)) { // legacy settings
|
if (getBoolean("service.masquerade", true)) { // legacy settings
|
||||||
Routing.MasqueradeMode.Simple
|
Routing.MasqueradeMode.Simple
|
||||||
} else Routing.MasqueradeMode.None
|
} else Routing.MasqueradeMode.None
|
||||||
}.let {
|
|
||||||
// older app version enabled netd for everyone. should check again here
|
|
||||||
if (Build.VERSION.SDK_INT >= 28 || it != Routing.MasqueradeMode.Netd) it
|
|
||||||
else Routing.MasqueradeMode.Simple
|
|
||||||
}
|
}
|
||||||
set(value) = app.pref.edit().putString(KEY_MASQUERADE_MODE, value.name).apply()
|
set(value) = app.pref.edit().putString(KEY_MASQUERADE_MODE, value.name).apply()
|
||||||
|
|
||||||
@@ -66,7 +61,7 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
|
|||||||
private var routing: Routing? = null
|
private var routing: Routing? = null
|
||||||
private var isWifi = forceWifi || TetherType.ofInterface(downstream).isWifi
|
private var isWifi = forceWifi || TetherType.ofInterface(downstream).isWifi
|
||||||
|
|
||||||
fun start() = synchronized(RoutingManager) {
|
fun start(fromMonitor: Boolean = false) = synchronized(RoutingManager) {
|
||||||
started = true
|
started = true
|
||||||
when (val other = active.putIfAbsent(downstream, this)) {
|
when (val other = active.putIfAbsent(downstream, this)) {
|
||||||
null -> {
|
null -> {
|
||||||
@@ -78,14 +73,19 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
|
|||||||
isWifi = isWifiNow
|
isWifi = isWifiNow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
initRoutingLocked()
|
initRoutingLocked(fromMonitor)
|
||||||
}
|
}
|
||||||
this -> true // already started
|
this -> true // already started
|
||||||
else -> error("Double routing detected for $downstream from $caller != ${other.caller}")
|
else -> {
|
||||||
|
val msg = "Double routing detected for $downstream from $caller != ${other.caller}"
|
||||||
|
Timber.w(RuntimeException(msg))
|
||||||
|
SmartSnackbar.make(msg).show()
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initRoutingLocked() = try {
|
private fun initRoutingLocked(fromMonitor: Boolean = false) = try {
|
||||||
routing = Routing(caller, downstream).apply {
|
routing = Routing(caller, downstream).apply {
|
||||||
try {
|
try {
|
||||||
configure()
|
configure()
|
||||||
@@ -97,10 +97,10 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
|
|||||||
true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
when (e) {
|
when (e) {
|
||||||
is Routing.InterfaceNotFoundException -> Timber.d(e)
|
is Routing.InterfaceNotFoundException -> if (!fromMonitor) Timber.d(e)
|
||||||
!is CancellationException -> Timber.w(e)
|
!is CancellationException -> Timber.w(e)
|
||||||
}
|
}
|
||||||
SmartSnackbar.make(e).show()
|
if (e !is Routing.InterfaceNotFoundException || !fromMonitor) SmartSnackbar.make(e).show()
|
||||||
routing = null
|
routing = null
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,35 @@
|
|||||||
package be.mygod.vpnhotspot
|
package be.mygod.vpnhotspot
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
|
||||||
import android.app.*
|
import android.app.*
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
object ServiceNotification {
|
object ServiceNotification {
|
||||||
private const val CHANNEL = "tethering"
|
private const val CHANNEL_ACTIVE = "tethering"
|
||||||
private const val CHANNEL_ID = 1
|
private const val CHANNEL_INACTIVE = "tethering-inactive"
|
||||||
|
private const val NOTIFICATION_ID = 1
|
||||||
|
|
||||||
private val deviceCountsMap = WeakHashMap<Service, Map<String, Int>>()
|
private val deviceCountsMap = WeakHashMap<Service, Map<String, Int>>()
|
||||||
private val inactiveMap = WeakHashMap<Service, List<String>>()
|
private val inactiveMap = WeakHashMap<Service, List<String>>()
|
||||||
private val manager = app.getSystemService<NotificationManager>()!!
|
private val manager = app.getSystemService<NotificationManager>()!!
|
||||||
|
|
||||||
private fun buildNotification(context: Context): Notification {
|
private fun buildNotification(context: Context): Notification {
|
||||||
val builder = NotificationCompat.Builder(context, CHANNEL)
|
|
||||||
.setWhen(0)
|
|
||||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
|
||||||
.setColor(ContextCompat.getColor(context, R.color.colorPrimary))
|
|
||||||
.setContentTitle(context.getText(R.string.notification_tethering_title))
|
|
||||||
.setSmallIcon(R.drawable.ic_quick_settings_tile_on)
|
|
||||||
.setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java),
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
|
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
||||||
val deviceCounts = deviceCountsMap.values.flatMap { it.entries }.sortedBy { it.key }
|
val deviceCounts = deviceCountsMap.values.flatMap { it.entries }.sortedBy { it.key }
|
||||||
val inactive = inactiveMap.values.flatten()
|
val inactive = inactiveMap.values.flatten()
|
||||||
|
val isInactive = inactive.isNotEmpty() && deviceCounts.isEmpty()
|
||||||
|
val builder = Notification.Builder(context, if (isInactive) CHANNEL_INACTIVE else CHANNEL_ACTIVE).apply {
|
||||||
|
setWhen(0)
|
||||||
|
setCategory(Notification.CATEGORY_SERVICE)
|
||||||
|
setColor(context.resources.getColor(R.color.colorPrimary, context.theme))
|
||||||
|
setContentTitle(context.getText(R.string.notification_tethering_title))
|
||||||
|
setSmallIcon(R.drawable.ic_quick_settings_tile_on)
|
||||||
|
setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
|
||||||
|
setVisibility(Notification.VISIBILITY_PUBLIC)
|
||||||
|
}
|
||||||
var lines = deviceCounts.map { (dev, size) ->
|
var lines = deviceCounts.map { (dev, size) ->
|
||||||
context.resources.getQuantityString(R.plurals.notification_connected_devices, size, size, dev)
|
context.resources.getQuantityString(R.plurals.notification_connected_devices, size, size, dev)
|
||||||
}
|
}
|
||||||
@@ -40,13 +39,13 @@ object ServiceNotification {
|
|||||||
return if (lines.size <= 1) builder.setContentText(lines.singleOrNull()).build() else {
|
return if (lines.size <= 1) builder.setContentText(lines.singleOrNull()).build() else {
|
||||||
val deviceCount = deviceCounts.sumOf { it.value }
|
val deviceCount = deviceCounts.sumOf { it.value }
|
||||||
val interfaceCount = deviceCounts.size + inactive.size
|
val interfaceCount = deviceCounts.size + inactive.size
|
||||||
NotificationCompat.BigTextStyle(builder
|
Notification.BigTextStyle().apply {
|
||||||
.setContentText(context.resources.getQuantityString(R.plurals.notification_connected_devices,
|
setBuilder(builder.setContentText(context.resources.getQuantityString(
|
||||||
deviceCount, deviceCount,
|
R.plurals.notification_connected_devices, deviceCount, deviceCount,
|
||||||
context.resources.getQuantityString(R.plurals.notification_interfaces,
|
context.resources.getQuantityString(R.plurals.notification_interfaces,
|
||||||
interfaceCount, interfaceCount))))
|
interfaceCount, interfaceCount))))
|
||||||
.bigText(lines.joinToString("\n"))
|
bigText(lines.joinToString("\n"))
|
||||||
.build()!!
|
}.build()!!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,26 +53,29 @@ object ServiceNotification {
|
|||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
deviceCountsMap[service] = deviceCounts
|
deviceCountsMap[service] = deviceCounts
|
||||||
if (inactive.isEmpty()) inactiveMap.remove(service) else inactiveMap[service] = inactive
|
if (inactive.isEmpty()) inactiveMap.remove(service) else inactiveMap[service] = inactive
|
||||||
service.startForeground(CHANNEL_ID, buildNotification(service))
|
service.startForeground(NOTIFICATION_ID, buildNotification(service))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun stopForeground(service: Service) = synchronized(this) {
|
fun stopForeground(service: Service) = synchronized(this) {
|
||||||
deviceCountsMap.remove(service)
|
deviceCountsMap.remove(service) ?: return@synchronized
|
||||||
if (deviceCountsMap.isEmpty()) service.stopForeground(true) else {
|
val shutdown = deviceCountsMap.isEmpty()
|
||||||
service.stopForeground(false)
|
service.stopForeground(if (shutdown) Service.STOP_FOREGROUND_REMOVE else Service.STOP_FOREGROUND_DETACH)
|
||||||
manager.notify(CHANNEL_ID, buildNotification(service))
|
if (!shutdown) manager.notify(NOTIFICATION_ID, buildNotification(service))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateNotificationChannels() {
|
fun updateNotificationChannels() {
|
||||||
if (Build.VERSION.SDK_INT >= 26) @TargetApi(26) {
|
NotificationChannel(CHANNEL_ACTIVE,
|
||||||
val tethering = NotificationChannel(CHANNEL,
|
app.getText(R.string.notification_channel_tethering), NotificationManager.IMPORTANCE_LOW).apply {
|
||||||
app.getText(R.string.notification_channel_tethering), NotificationManager.IMPORTANCE_LOW)
|
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
||||||
tethering.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
manager.createNotificationChannel(this)
|
||||||
manager.createNotificationChannel(tethering)
|
|
||||||
// remove old service channels
|
|
||||||
manager.deleteNotificationChannel("hotspot")
|
|
||||||
manager.deleteNotificationChannel("repeater")
|
|
||||||
}
|
}
|
||||||
|
NotificationChannel(CHANNEL_INACTIVE,
|
||||||
|
app.getText(R.string.notification_channel_monitor), NotificationManager.IMPORTANCE_LOW).apply {
|
||||||
|
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
||||||
|
manager.createNotificationChannel(this)
|
||||||
|
}
|
||||||
|
// remove old service channels
|
||||||
|
manager.deleteNotificationChannel("hotspot")
|
||||||
|
manager.deleteNotificationChannel("repeater")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package be.mygod.vpnhotspot
|
package be.mygod.vpnhotspot
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -8,20 +7,19 @@ import androidx.core.content.FileProvider
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import androidx.preference.SwitchPreference
|
import androidx.preference.TwoStatePreference
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.net.TetherOffloadManager
|
import be.mygod.vpnhotspot.net.TetherOffloadManager
|
||||||
import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor
|
import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor
|
||||||
import be.mygod.vpnhotspot.net.monitor.IpMonitor
|
import be.mygod.vpnhotspot.net.monitor.IpMonitor
|
||||||
import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
|
import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
|
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
|
||||||
import be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreferenceDialogFragment
|
import be.mygod.vpnhotspot.preference.AutoCompleteNetworkPreferenceDialogFragment
|
||||||
import be.mygod.vpnhotspot.preference.SharedPreferenceDataStore
|
import be.mygod.vpnhotspot.preference.SharedPreferenceDataStore
|
||||||
import be.mygod.vpnhotspot.preference.SummaryFallbackProvider
|
import be.mygod.vpnhotspot.preference.SummaryFallbackProvider
|
||||||
import be.mygod.vpnhotspot.root.Dump
|
import be.mygod.vpnhotspot.root.Dump
|
||||||
import be.mygod.vpnhotspot.root.RootManager
|
import be.mygod.vpnhotspot.root.RootManager
|
||||||
import be.mygod.vpnhotspot.util.Services
|
import be.mygod.vpnhotspot.util.Services
|
||||||
import be.mygod.vpnhotspot.util.allInterfaceNames
|
|
||||||
import be.mygod.vpnhotspot.util.launchUrl
|
import be.mygod.vpnhotspot.util.launchUrl
|
||||||
import be.mygod.vpnhotspot.util.showAllowingStateLoss
|
import be.mygod.vpnhotspot.util.showAllowingStateLoss
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
@@ -40,7 +38,6 @@ import kotlin.system.exitProcess
|
|||||||
|
|
||||||
class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
||||||
private fun Preference.remove() = parent!!.removePreference(this)
|
private fun Preference.remove() = parent!!.removePreference(this)
|
||||||
@TargetApi(26)
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
// handle complicated default value and possible system upgrades
|
// handle complicated default value and possible system upgrades
|
||||||
WifiDoubleLock.mode = WifiDoubleLock.mode
|
WifiDoubleLock.mode = WifiDoubleLock.mode
|
||||||
@@ -50,34 +47,28 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
|||||||
addPreferencesFromResource(R.xml.pref_settings)
|
addPreferencesFromResource(R.xml.pref_settings)
|
||||||
SummaryFallbackProvider(findPreference(UpstreamMonitor.KEY)!!)
|
SummaryFallbackProvider(findPreference(UpstreamMonitor.KEY)!!)
|
||||||
SummaryFallbackProvider(findPreference(FallbackUpstreamMonitor.KEY)!!)
|
SummaryFallbackProvider(findPreference(FallbackUpstreamMonitor.KEY)!!)
|
||||||
findPreference<SwitchPreference>("system.enableTetherOffload")!!.apply {
|
findPreference<TwoStatePreference>("system.enableTetherOffload")!!.apply {
|
||||||
if (TetherOffloadManager.supported) {
|
isChecked = TetherOffloadManager.enabled
|
||||||
isChecked = TetherOffloadManager.enabled
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
if (TetherOffloadManager.enabled != newValue) viewLifecycleOwner.lifecycleScope.launch {
|
||||||
if (TetherOffloadManager.enabled != newValue) viewLifecycleOwner.lifecycleScope.launchWhenCreated {
|
isEnabled = false
|
||||||
isEnabled = false
|
try {
|
||||||
try {
|
TetherOffloadManager.setEnabled(newValue as Boolean)
|
||||||
TetherOffloadManager.setEnabled(newValue as Boolean)
|
} catch (_: CancellationException) {
|
||||||
} catch (_: CancellationException) {
|
} catch (e: Exception) {
|
||||||
} catch (e: Exception) {
|
Timber.w(e)
|
||||||
Timber.w(e)
|
SmartSnackbar.make(e).show()
|
||||||
SmartSnackbar.make(e).show()
|
|
||||||
}
|
|
||||||
isChecked = TetherOffloadManager.enabled
|
|
||||||
isEnabled = true
|
|
||||||
}
|
}
|
||||||
false
|
isChecked = TetherOffloadManager.enabled
|
||||||
|
isEnabled = true
|
||||||
}
|
}
|
||||||
} else remove()
|
false
|
||||||
}
|
|
||||||
val boot = findPreference<SwitchPreference>("service.repeater.startOnBoot")!!
|
|
||||||
if (Services.p2p != null) {
|
|
||||||
boot.setOnPreferenceChangeListener { _, value ->
|
|
||||||
BootReceiver.enabled = value as Boolean
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
boot.isChecked = BootReceiver.enabled
|
}
|
||||||
} else boot.remove()
|
findPreference<TwoStatePreference>(BootReceiver.KEY)!!.setOnPreferenceChangeListener { _, value ->
|
||||||
|
BootReceiver.onUserSettingUpdated(value as Boolean)
|
||||||
|
true
|
||||||
|
}
|
||||||
if (Services.p2p == null || !RepeaterService.safeModeConfigurable) {
|
if (Services.p2p == null || !RepeaterService.safeModeConfigurable) {
|
||||||
val safeMode = findPreference<Preference>(RepeaterService.KEY_SAFE_MODE)!!
|
val safeMode = findPreference<Preference>(RepeaterService.KEY_SAFE_MODE)!!
|
||||||
safeMode.remove()
|
safeMode.remove()
|
||||||
@@ -130,7 +121,7 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
|||||||
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
.putExtra(Intent.EXTRA_STREAM,
|
.putExtra(Intent.EXTRA_STREAM,
|
||||||
FileProvider.getUriForFile(context, "be.mygod.vpnhotspot.log", logFile)),
|
FileProvider.getUriForFile(context, "be.mygod.vpnhotspot.log", logFile)),
|
||||||
context.getString(R.string.abc_shareactionprovider_share_with)))
|
context.getString(androidx.appcompat.R.string.abc_shareactionprovider_share_with)))
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -148,16 +139,12 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDisplayPreferenceDialog(preference: Preference) {
|
override fun onDisplayPreferenceDialog(preference: Preference) = when (preference.key) {
|
||||||
when (preference.key) {
|
UpstreamMonitor.KEY, FallbackUpstreamMonitor.KEY ->
|
||||||
UpstreamMonitor.KEY, FallbackUpstreamMonitor.KEY ->
|
AutoCompleteNetworkPreferenceDialogFragment().apply {
|
||||||
AlwaysAutoCompleteEditTextPreferenceDialogFragment().apply {
|
setArguments(preference.key)
|
||||||
setArguments(preference.key, Services.connectivity.allNetworks.mapNotNull {
|
setTargetFragment(this@SettingsPreferenceFragment, 0)
|
||||||
Services.connectivity.getLinkProperties(it)?.allInterfaceNames
|
}.showAllowingStateLoss(parentFragmentManager, preference.key)
|
||||||
}.flatten().toTypedArray())
|
else -> super.onDisplayPreferenceDialog(preference)
|
||||||
setTargetFragment(this@SettingsPreferenceFragment, 0)
|
|
||||||
}.showAllowingStateLoss(parentFragmentManager, preference.key)
|
|
||||||
else -> super.onDisplayPreferenceDialog(preference)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package be.mygod.vpnhotspot
|
package be.mygod.vpnhotspot
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.net.Routing
|
import be.mygod.vpnhotspot.net.Routing
|
||||||
@@ -10,6 +10,7 @@ import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
|||||||
import be.mygod.vpnhotspot.util.Event0
|
import be.mygod.vpnhotspot.util.Event0
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
|
|||||||
companion object {
|
companion object {
|
||||||
const val EXTRA_ADD_INTERFACES = "interface.add"
|
const val EXTRA_ADD_INTERFACES = "interface.add"
|
||||||
const val EXTRA_ADD_INTERFACE_MONITOR = "interface.add.monitor"
|
const val EXTRA_ADD_INTERFACE_MONITOR = "interface.add.monitor"
|
||||||
|
const val EXTRA_ADD_INTERFACES_MONITOR = "interface.adds.monitor"
|
||||||
const val EXTRA_REMOVE_INTERFACE = "interface.remove"
|
const val EXTRA_REMOVE_INTERFACE = "interface.remove"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,10 +41,19 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class Starter(val monitored: ArrayList<String>) : BootReceiver.Startable {
|
||||||
|
override fun start(context: Context) {
|
||||||
|
context.startForegroundService(Intent(context, TetheringService::class.java).apply {
|
||||||
|
putStringArrayListExtra(EXTRA_ADD_INTERFACES_MONITOR, monitored)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes and critical reads to downstreams should be protected with this context.
|
* Writes and critical reads to downstreams should be protected with this context.
|
||||||
*/
|
*/
|
||||||
private val dispatcher = newSingleThreadContext("TetheringService")
|
private val dispatcher = Dispatchers.IO.limitedParallelism(1)
|
||||||
override val coroutineContext = dispatcher + Job()
|
override val coroutineContext = dispatcher + Job()
|
||||||
private val binder = Binder()
|
private val binder = Binder()
|
||||||
private val downstreams = ConcurrentHashMap<String, Downstream>()
|
private val downstreams = ConcurrentHashMap<String, Downstream>()
|
||||||
@@ -55,7 +66,7 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
|
|||||||
val toRemove = downstreams.toMutableMap() // make a copy
|
val toRemove = downstreams.toMutableMap() // make a copy
|
||||||
for (iface in interfaces) {
|
for (iface in interfaces) {
|
||||||
val downstream = toRemove.remove(iface) ?: continue
|
val downstream = toRemove.remove(iface) ?: continue
|
||||||
if (downstream.monitor) downstream.start()
|
if (downstream.monitor && !downstream.start()) downstream.stop()
|
||||||
}
|
}
|
||||||
for ((iface, downstream) in toRemove) {
|
for ((iface, downstream) in toRemove) {
|
||||||
if (!downstream.monitor) check(downstreams.remove(iface, downstream))
|
if (!downstream.monitor) check(downstreams.remove(iface, downstream))
|
||||||
@@ -81,6 +92,10 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
|
|||||||
ServiceNotification.stopForeground(this)
|
ServiceNotification.stopForeground(this)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
} else {
|
} else {
|
||||||
|
binder.monitoredIfaces.also {
|
||||||
|
if (it.isEmpty()) BootReceiver.delete<TetheringService>()
|
||||||
|
else BootReceiver.add<TetheringService>(Starter(ArrayList(it)))
|
||||||
|
}
|
||||||
if (!callbackRegistered) {
|
if (!callbackRegistered) {
|
||||||
callbackRegistered = true
|
callbackRegistered = true
|
||||||
TetheringManager.registerTetheringEventCallbackCompat(this, this)
|
TetheringManager.registerTetheringEventCallbackCompat(this, this)
|
||||||
@@ -94,8 +109,9 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
|
|||||||
override fun onBind(intent: Intent?) = binder
|
override fun onBind(intent: Intent?) = binder
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
BootReceiver.startIfEnabled()
|
||||||
// call this first just in case we are shutting down immediately
|
// call this first just in case we are shutting down immediately
|
||||||
if (Build.VERSION.SDK_INT >= 26) updateNotification()
|
updateNotification()
|
||||||
launch {
|
launch {
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
for (iface in intent.getStringArrayExtra(EXTRA_ADD_INTERFACES) ?: emptyArray()) {
|
for (iface in intent.getStringArrayExtra(EXTRA_ADD_INTERFACES) ?: emptyArray()) {
|
||||||
@@ -103,10 +119,12 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
|
|||||||
if (start()) check(downstreams.put(iface, this) == null) else stop()
|
if (start()) check(downstreams.put(iface, this) == null) else stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
intent.getStringExtra(EXTRA_ADD_INTERFACE_MONITOR)?.also { iface ->
|
val monitorList = intent.getStringArrayListExtra(EXTRA_ADD_INTERFACES_MONITOR) ?:
|
||||||
|
intent.getStringExtra(EXTRA_ADD_INTERFACE_MONITOR)?.let { listOf(it) }
|
||||||
|
if (!monitorList.isNullOrEmpty()) for (iface in monitorList) {
|
||||||
val downstream = downstreams[iface]
|
val downstream = downstreams[iface]
|
||||||
if (downstream == null) Downstream(this@TetheringService, iface, true).apply {
|
if (downstream == null) Downstream(this@TetheringService, iface, true).apply {
|
||||||
start()
|
if (!start(true)) stop()
|
||||||
check(downstreams.put(iface, this) == null)
|
check(downstreams.put(iface, this) == null)
|
||||||
downstreams[iface] = this
|
downstreams[iface] = this
|
||||||
} else downstream.monitor = true
|
} else downstream.monitor = true
|
||||||
@@ -120,10 +138,10 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
launch {
|
launch {
|
||||||
|
BootReceiver.delete<TetheringService>()
|
||||||
unregisterReceiver()
|
unregisterReceiver()
|
||||||
downstreams.values.forEach { it.stop() } // force clean to prevent leakage
|
downstreams.values.forEach { it.stop() } // force clean to prevent leakage
|
||||||
cancel()
|
cancel()
|
||||||
dispatcher.close()
|
|
||||||
}
|
}
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
@@ -135,4 +153,8 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
|
|||||||
callbackRegistered = false
|
callbackRegistered = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun updateNotification() {
|
||||||
|
launch { super.updateNotification() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package be.mygod.vpnhotspot.client
|
package be.mygod.vpnhotspot.client
|
||||||
|
|
||||||
|
import android.net.MacAddress
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import android.text.style.StrikethroughSpan
|
import android.text.style.StrikethroughSpan
|
||||||
@@ -9,7 +10,6 @@ import be.mygod.vpnhotspot.App.Companion.app
|
|||||||
import be.mygod.vpnhotspot.R
|
import be.mygod.vpnhotspot.R
|
||||||
import be.mygod.vpnhotspot.net.InetAddressComparator
|
import be.mygod.vpnhotspot.net.InetAddressComparator
|
||||||
import be.mygod.vpnhotspot.net.IpNeighbour
|
import be.mygod.vpnhotspot.net.IpNeighbour
|
||||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
|
||||||
import be.mygod.vpnhotspot.net.TetherType
|
import be.mygod.vpnhotspot.net.TetherType
|
||||||
import be.mygod.vpnhotspot.room.AppDatabase
|
import be.mygod.vpnhotspot.room.AppDatabase
|
||||||
import be.mygod.vpnhotspot.room.ClientRecord
|
import be.mygod.vpnhotspot.room.ClientRecord
|
||||||
@@ -18,7 +18,7 @@ import be.mygod.vpnhotspot.util.makeMacSpan
|
|||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
open class Client(val mac: MacAddressCompat, val iface: String) {
|
open class Client(val mac: MacAddress, val iface: String) {
|
||||||
companion object DiffCallback : DiffUtil.ItemCallback<Client>() {
|
companion object DiffCallback : DiffUtil.ItemCallback<Client>() {
|
||||||
override fun areItemsTheSame(oldItem: Client, newItem: Client) =
|
override fun areItemsTheSame(oldItem: Client, newItem: Client) =
|
||||||
oldItem.iface == newItem.iface && oldItem.mac == newItem.mac
|
oldItem.iface == newItem.iface && oldItem.mac == newItem.mac
|
||||||
@@ -42,10 +42,10 @@ open class Client(val mac: MacAddressCompat, val iface: String) {
|
|||||||
* we hijack the get title process to check if we need to perform MacLookup,
|
* we hijack the get title process to check if we need to perform MacLookup,
|
||||||
* as record might not be initialized in other more appropriate places
|
* as record might not be initialized in other more appropriate places
|
||||||
*/
|
*/
|
||||||
SpannableStringBuilder(if (record.nickname.isEmpty()) {
|
SpannableStringBuilder(record.nickname.ifEmpty {
|
||||||
if (record.macLookupPending) MacLookup.perform(mac)
|
if (record.macLookupPending) MacLookup.perform(mac)
|
||||||
macIface
|
macIface
|
||||||
} else emojize(record.nickname)).apply {
|
}).apply {
|
||||||
if (record.blocked) setSpan(StrikethroughSpan(), 0, length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
|
if (record.blocked) setSpan(StrikethroughSpan(), 0, length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ open class Client(val mac: MacAddressCompat, val iface: String) {
|
|||||||
}.trimEnd()
|
}.trimEnd()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun obtainRecord() = record.value ?: ClientRecord(mac.addr)
|
fun obtainRecord() = record.value ?: ClientRecord(mac)
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ package be.mygod.vpnhotspot.client
|
|||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
|
import android.net.MacAddress
|
||||||
import android.net.wifi.p2p.WifiP2pDevice
|
import android.net.wifi.p2p.WifiP2pDevice
|
||||||
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.os.BuildCompat
|
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
@@ -15,8 +16,6 @@ import androidx.lifecycle.ViewModel
|
|||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.RepeaterService
|
import be.mygod.vpnhotspot.RepeaterService
|
||||||
import be.mygod.vpnhotspot.net.IpNeighbour
|
import be.mygod.vpnhotspot.net.IpNeighbour
|
||||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
|
||||||
import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toCompat
|
|
||||||
import be.mygod.vpnhotspot.net.TetherType
|
import be.mygod.vpnhotspot.net.TetherType
|
||||||
import be.mygod.vpnhotspot.net.TetheringManager
|
import be.mygod.vpnhotspot.net.TetheringManager
|
||||||
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
|
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
|
||||||
@@ -38,7 +37,7 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
|
|||||||
|
|
||||||
private var repeater: RepeaterService.Binder? = null
|
private var repeater: RepeaterService.Binder? = null
|
||||||
private var p2p: Collection<WifiP2pDevice> = emptyList()
|
private var p2p: Collection<WifiP2pDevice> = emptyList()
|
||||||
private var wifiAp = emptyList<Pair<String, MacAddressCompat>>()
|
private var wifiAp = emptyList<Pair<String, MacAddress>>()
|
||||||
private var neighbours: Collection<IpNeighbour> = emptyList()
|
private var neighbours: Collection<IpNeighbour> = emptyList()
|
||||||
val clients = MutableLiveData<List<Client>>()
|
val clients = MutableLiveData<List<Client>>()
|
||||||
val fullMode = object : DefaultLifecycleObserver {
|
val fullMode = object : DefaultLifecycleObserver {
|
||||||
@@ -51,10 +50,10 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun populateClients() {
|
private fun populateClients() {
|
||||||
val clients = HashMap<Pair<String, MacAddressCompat>, Client>()
|
val clients = HashMap<Pair<String, MacAddress>, Client>()
|
||||||
repeater?.group?.`interface`?.let { p2pInterface ->
|
repeater?.group?.`interface`?.let { p2pInterface ->
|
||||||
for (client in p2p) {
|
for (client in p2p) {
|
||||||
val addr = MacAddressCompat.fromString(client.deviceAddress!!)
|
val addr = MacAddress.fromString(client.deviceAddress!!)
|
||||||
clients[p2pInterface to addr] = object : Client(addr, p2pInterface) {
|
clients[p2pInterface to addr] = object : Client(addr, p2pInterface) {
|
||||||
override val icon: Int get() = TetherType.WIFI_P2P.icon
|
override val icon: Int get() = TetherType.WIFI_P2P.icon
|
||||||
}
|
}
|
||||||
@@ -87,10 +86,10 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
|
|||||||
override fun onStart(owner: LifecycleOwner) {
|
override fun onStart(owner: LifecycleOwner) {
|
||||||
app.registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
app.registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
||||||
IpNeighbourMonitor.registerCallback(this, false)
|
IpNeighbourMonitor.registerCallback(this, false)
|
||||||
if (BuildCompat.isAtLeastS()) WifiApCommands.registerSoftApCallback(this)
|
if (Build.VERSION.SDK_INT >= 31) WifiApCommands.registerSoftApCallback(this)
|
||||||
}
|
}
|
||||||
override fun onStop(owner: LifecycleOwner) {
|
override fun onStop(owner: LifecycleOwner) {
|
||||||
if (BuildCompat.isAtLeastS()) WifiApCommands.unregisterSoftApCallback(this)
|
if (Build.VERSION.SDK_INT >= 31) WifiApCommands.unregisterSoftApCallback(this)
|
||||||
IpNeighbourMonitor.unregisterCallback(this)
|
IpNeighbourMonitor.unregisterCallback(this)
|
||||||
app.unregisterReceiver(receiver)
|
app.unregisterReceiver(receiver)
|
||||||
}
|
}
|
||||||
@@ -118,7 +117,7 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
|
|||||||
override fun onConnectedClientsChanged(clients: List<Parcelable>) {
|
override fun onConnectedClientsChanged(clients: List<Parcelable>) {
|
||||||
wifiAp = clients.mapNotNull {
|
wifiAp = clients.mapNotNull {
|
||||||
val client = WifiClient(it)
|
val client = WifiClient(it)
|
||||||
client.apInstanceIdentifier?.run { this to client.macAddress.toCompat() }
|
client.apInstanceIdentifier?.run { this to client.macAddress }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package be.mygod.vpnhotspot.client
|
package be.mygod.vpnhotspot.client
|
||||||
|
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
|
import android.net.MacAddress
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
@@ -20,6 +21,7 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.withStarted
|
||||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
@@ -30,7 +32,6 @@ import be.mygod.vpnhotspot.Empty
|
|||||||
import be.mygod.vpnhotspot.R
|
import be.mygod.vpnhotspot.R
|
||||||
import be.mygod.vpnhotspot.databinding.FragmentClientsBinding
|
import be.mygod.vpnhotspot.databinding.FragmentClientsBinding
|
||||||
import be.mygod.vpnhotspot.databinding.ListitemClientBinding
|
import be.mygod.vpnhotspot.databinding.ListitemClientBinding
|
||||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
|
||||||
import be.mygod.vpnhotspot.net.TetherType
|
import be.mygod.vpnhotspot.net.TetherType
|
||||||
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
||||||
import be.mygod.vpnhotspot.net.monitor.TrafficRecorder
|
import be.mygod.vpnhotspot.net.monitor.TrafficRecorder
|
||||||
@@ -41,21 +42,24 @@ import be.mygod.vpnhotspot.util.format
|
|||||||
import be.mygod.vpnhotspot.util.showAllowingStateLoss
|
import be.mygod.vpnhotspot.util.showAllowingStateLoss
|
||||||
import be.mygod.vpnhotspot.util.toPluralInt
|
import be.mygod.vpnhotspot.util.toPluralInt
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.text.NumberFormat
|
import java.text.NumberFormat
|
||||||
|
|
||||||
class ClientsFragment : Fragment() {
|
class ClientsFragment : Fragment() {
|
||||||
// FIXME: value class does not work with Parcelize
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class NicknameArg(val mac: Long, val nickname: CharSequence) : Parcelable
|
data class NicknameArg(val mac: MacAddress, val nickname: CharSequence) : Parcelable
|
||||||
class NicknameDialogFragment : AlertDialogFragment<NicknameArg, Empty>() {
|
class NicknameDialogFragment : AlertDialogFragment<NicknameArg, Empty>() {
|
||||||
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
|
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
|
||||||
setView(R.layout.dialog_nickname)
|
setView(R.layout.dialog_nickname)
|
||||||
setTitle(getString(R.string.clients_nickname_title, MacAddressCompat(arg.mac).toString()))
|
setTitle(getString(R.string.clients_nickname_title, arg.mac))
|
||||||
setPositiveButton(android.R.string.ok, listener)
|
setPositiveButton(android.R.string.ok, listener)
|
||||||
setNegativeButton(android.R.string.cancel, null)
|
setNegativeButton(android.R.string.cancel, null)
|
||||||
setNeutralButton(emojize(getText(R.string.clients_nickname_set_to_vendor)), listener)
|
setNeutralButton(getText(R.string.clients_nickname_set_to_vendor), listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?) = super.onCreateDialog(savedInstanceState).apply {
|
override fun onCreateDialog(savedInstanceState: Bundle?) = super.onCreateDialog(savedInstanceState).apply {
|
||||||
@@ -64,7 +68,7 @@ class ClientsFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(dialog: DialogInterface?, which: Int) {
|
override fun onClick(dialog: DialogInterface?, which: Int) {
|
||||||
val mac = MacAddressCompat(arg.mac)
|
val mac = arg.mac
|
||||||
when (which) {
|
when (which) {
|
||||||
DialogInterface.BUTTON_POSITIVE -> {
|
DialogInterface.BUTTON_POSITIVE -> {
|
||||||
val newNickname = this.dialog!!.findViewById<EditText>(android.R.id.edit).text
|
val newNickname = this.dialog!!.findViewById<EditText>(android.R.id.edit).text
|
||||||
@@ -84,7 +88,7 @@ class ClientsFragment : Fragment() {
|
|||||||
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
|
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
|
||||||
val context = context
|
val context = context
|
||||||
val resources = resources
|
val resources = resources
|
||||||
val locale = resources.configuration.locale
|
val locale = resources.configuration.locales[0]
|
||||||
setTitle(getText(R.string.clients_stats_title).format(locale, arg.title))
|
setTitle(getText(R.string.clients_stats_title).format(locale, arg.title))
|
||||||
val format = NumberFormat.getIntegerInstance(locale)
|
val format = NumberFormat.getIntegerInstance(locale)
|
||||||
setMessage("%s\n%s\n%s".format(
|
setMessage("%s\n%s\n%s".format(
|
||||||
@@ -135,7 +139,7 @@ class ClientsFragment : Fragment() {
|
|||||||
R.id.nickname -> {
|
R.id.nickname -> {
|
||||||
val client = binding.client ?: return false
|
val client = binding.client ?: return false
|
||||||
NicknameDialogFragment().apply {
|
NicknameDialogFragment().apply {
|
||||||
arg(NicknameArg(client.mac.addr, client.nickname))
|
arg(NicknameArg(client.mac, client.nickname))
|
||||||
}.showAllowingStateLoss(parentFragmentManager)
|
}.showAllowingStateLoss(parentFragmentManager)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -155,14 +159,16 @@ class ClientsFragment : Fragment() {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.stats -> {
|
R.id.stats -> {
|
||||||
binding.client?.let { client ->
|
val client = binding.client
|
||||||
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
|
val title = client?.title?.value ?: return false
|
||||||
withContext(Dispatchers.Unconfined) {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
StatsDialogFragment().apply {
|
val stats = withContext(Dispatchers.Unconfined) {
|
||||||
arg(StatsArg(client.title.value ?: return@withContext,
|
AppDatabase.instance.trafficRecordDao.queryStats(client.mac)
|
||||||
AppDatabase.instance.trafficRecordDao.queryStats(client.mac.addr)))
|
}
|
||||||
}.showAllowingStateLoss(parentFragmentManager)
|
withStarted {
|
||||||
}
|
StatsDialogFragment().apply {
|
||||||
|
arg(StatsArg(title, stats))
|
||||||
|
}.showAllowingStateLoss(parentFragmentManager)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
@@ -201,9 +207,7 @@ class ClientsFragment : Fragment() {
|
|||||||
check(newRecord.receivedPackets == oldRecord.receivedPackets)
|
check(newRecord.receivedPackets == oldRecord.receivedPackets)
|
||||||
check(newRecord.receivedBytes == oldRecord.receivedBytes)
|
check(newRecord.receivedBytes == oldRecord.receivedBytes)
|
||||||
} else {
|
} else {
|
||||||
val rate = rates.computeIfAbsent(newRecord.downstream to MacAddressCompat(newRecord.mac)) {
|
val rate = rates.computeIfAbsent(newRecord.downstream to newRecord.mac) { TrafficRate() }
|
||||||
TrafficRate()
|
|
||||||
}
|
|
||||||
if (rate.send < 0 || rate.receive < 0) {
|
if (rate.send < 0 || rate.receive < 0) {
|
||||||
rate.send = 0
|
rate.send = 0
|
||||||
rate.receive = 0
|
rate.receive = 0
|
||||||
@@ -218,7 +222,7 @@ class ClientsFragment : Fragment() {
|
|||||||
|
|
||||||
private lateinit var binding: FragmentClientsBinding
|
private lateinit var binding: FragmentClientsBinding
|
||||||
private val adapter = ClientAdapter()
|
private val adapter = ClientAdapter()
|
||||||
private var rates = mutableMapOf<Pair<String, MacAddressCompat>, TrafficRate>()
|
private var rates = mutableMapOf<Pair<String, MacAddress>, TrafficRate>()
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
binding = FragmentClientsBinding.inflate(inflater, container, false)
|
binding = FragmentClientsBinding.inflate(inflater, container, false)
|
||||||
@@ -237,14 +241,19 @@ class ClientsFragment : Fragment() {
|
|||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
// icon might be changed due to TetherType changes
|
// icon might be changed due to TetherType changes
|
||||||
if (Build.VERSION.SDK_INT >= 30) TetherType.listener[this] = {
|
if (Build.VERSION.SDK_INT >= 30) TetherType.listener[this] = {
|
||||||
lifecycleScope.launchWhenStarted { adapter.notifyItemRangeChanged(0, adapter.size.await()) }
|
lifecycleScope.launch {
|
||||||
|
val size = adapter.size.await()
|
||||||
|
withStarted { adapter.notifyItemRangeChanged(0, size) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
super.onStart()
|
super.onStart()
|
||||||
// we just put these two thing together as this is the only place we need to use this event for now
|
// we just put these two thing together as this is the only place we need to use this event for now
|
||||||
TrafficRecorder.foregroundListeners[this] = { newRecords, oldRecords ->
|
TrafficRecorder.foregroundListeners[this] = { newRecords, oldRecords ->
|
||||||
lifecycleScope.launchWhenStarted { adapter.updateTraffic(newRecords, oldRecords) }
|
lifecycleScope.launch {
|
||||||
|
withStarted { adapter.updateTraffic(newRecords, oldRecords) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
lifecycleScope.launchWhenStarted {
|
lifecycleScope.launch {
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
TrafficRecorder.rescheduleUpdate() // next schedule time might be 1 min, force reschedule to <= 1s
|
TrafficRecorder.rescheduleUpdate() // next schedule time might be 1 min, force reschedule to <= 1s
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package be.mygod.vpnhotspot.client
|
|
||||||
|
|
||||||
import androidx.emoji.text.EmojiCompat
|
|
||||||
|
|
||||||
fun emojize(text: CharSequence?): CharSequence? = if (text == null) null else try {
|
|
||||||
EmojiCompat.get().process(text)
|
|
||||||
} catch (_: IllegalStateException) {
|
|
||||||
text
|
|
||||||
}
|
|
||||||
@@ -1,78 +1,134 @@
|
|||||||
package be.mygod.vpnhotspot.client
|
package be.mygod.vpnhotspot.client
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.net.MacAddress
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.R
|
import be.mygod.vpnhotspot.R
|
||||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
|
||||||
import be.mygod.vpnhotspot.room.AppDatabase
|
import be.mygod.vpnhotspot.room.AppDatabase
|
||||||
|
import be.mygod.vpnhotspot.util.connectCancellable
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.net.HttpURLConnection
|
import java.io.File
|
||||||
import java.net.URL
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.HttpCookie
|
||||||
|
import java.util.Scanner
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class generates a default nickname for new clients.
|
* This class generates a default nickname for new clients.
|
||||||
*/
|
*/
|
||||||
object MacLookup {
|
object MacLookup {
|
||||||
class UnexpectedError(val mac: MacAddressCompat, val error: String) : JSONException("") {
|
class UnexpectedError(val mac: MacAddress, val error: String) : JSONException("") {
|
||||||
private fun formatMessage(context: Context) =
|
private fun formatMessage(context: Context) =
|
||||||
context.getString(R.string.clients_mac_lookup_unexpected_error, mac.toOui(), error)
|
context.getString(R.string.clients_mac_lookup_unexpected_error,
|
||||||
|
mac.toByteArray().joinToString("") { "%02x".format(it) }.substring(0, 9), error)
|
||||||
override val message get() = formatMessage(app.english)
|
override val message get() = formatMessage(app.english)
|
||||||
override fun getLocalizedMessage() = formatMessage(app)
|
override fun getLocalizedMessage() = formatMessage(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val macLookupBusy = mutableMapOf<MacAddressCompat, Pair<HttpURLConnection, Job>>()
|
private object SessionManager {
|
||||||
|
private const val CACHE_FILENAME = "maclookup_sessioncache"
|
||||||
|
private const val COOKIE_SESSION = "mac_address_vendor_lookup_session"
|
||||||
|
private val csrfPattern = Pattern.compile("<meta\\s+name=\"csrf-token\"\\s+content=\"([^\"]*)\"",
|
||||||
|
Pattern.CASE_INSENSITIVE)
|
||||||
|
private var sessionCache: List<String>?
|
||||||
|
get() = try {
|
||||||
|
File(app.deviceStorage.cacheDir, CACHE_FILENAME).readText().split('\n', limit = 2)
|
||||||
|
} catch (_: FileNotFoundException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
set(value) = File(app.deviceStorage.cacheDir, CACHE_FILENAME).run {
|
||||||
|
if (value != null) writeText(value.joinToString("\n")) else if (!delete()) writeText("")
|
||||||
|
}
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
|
private suspend fun refreshSessionCache() = connectCancellable("https://macaddress.io/api") { conn ->
|
||||||
|
val cookies = conn.headerFields["set-cookie"] ?: throw IOException("Missing cookies")
|
||||||
|
var mavls: HttpCookie? = null
|
||||||
|
for (header in cookies) for (cookie in HttpCookie.parse(header)) {
|
||||||
|
if (cookie.name == COOKIE_SESSION) mavls = cookie
|
||||||
|
}
|
||||||
|
if (mavls == null) throw IOException("Missing set-cookie $COOKIE_SESSION")
|
||||||
|
val token = conn.inputStream.use { Scanner(it).findWithinHorizon(csrfPattern, 0) }
|
||||||
|
?: throw IOException("Missing csrf-token")
|
||||||
|
listOf(mavls.toString(), csrfPattern.matcher(token).run {
|
||||||
|
check(matches())
|
||||||
|
group(1)!!
|
||||||
|
}).also { sessionCache = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun obtain(forceNew: Boolean): Pair<HttpCookie, String> = mutex.withLock {
|
||||||
|
val sessionCache = (if (forceNew) null else sessionCache) ?: refreshSessionCache()
|
||||||
|
HttpCookie.parse(sessionCache[0]).single() to sessionCache[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val macLookupBusy = mutableMapOf<MacAddress, Job>()
|
||||||
// http://en.wikipedia.org/wiki/ISO_3166-1
|
// http://en.wikipedia.org/wiki/ISO_3166-1
|
||||||
private val countryCodeRegex = "(?:^|[^A-Z])([A-Z]{2})[\\s\\d]*$".toRegex()
|
private val countryCodeRegex = "(?:^|[^A-Z])([A-Z]{2})[\\s\\d]*$".toRegex()
|
||||||
|
|
||||||
@MainThread
|
@MainThread
|
||||||
fun abort(mac: MacAddressCompat) = macLookupBusy.remove(mac)?.let { (conn, job) ->
|
fun abort(mac: MacAddress) = macLookupBusy.remove(mac)?.cancel()
|
||||||
job.cancel()
|
|
||||||
if (Build.VERSION.SDK_INT < 26) GlobalScope.launch(Dispatchers.IO) { conn.disconnect() } else conn.disconnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainThread
|
@MainThread
|
||||||
fun perform(mac: MacAddressCompat, explicit: Boolean = false) {
|
fun perform(mac: MacAddress, explicit: Boolean = false) {
|
||||||
abort(mac)
|
abort(mac)
|
||||||
val conn = URL("https://macvendors.co/api/$mac").openConnection() as HttpURLConnection
|
macLookupBusy[mac] = GlobalScope.launch(Dispatchers.IO) {
|
||||||
macLookupBusy[mac] = conn to GlobalScope.launch(Dispatchers.IO) {
|
|
||||||
try {
|
try {
|
||||||
val response = conn.inputStream.bufferedReader().readText()
|
var response: String? = null
|
||||||
val obj = JSONObject(response).getJSONObject("result")
|
for (tries in 0 until 5) {
|
||||||
obj.opt("error")?.also { throw UnexpectedError(mac, it.toString()) }
|
val (cookie, csrf) = SessionManager.obtain(tries > 0)
|
||||||
val company = obj.getString("company")
|
response = connectCancellable("https://macaddress.io/mac-address-lookup") { conn ->
|
||||||
val match = extractCountry(mac, response, obj)
|
conn.requestMethod = "POST"
|
||||||
val result = if (match != null) {
|
conn.setRequestProperty("content-type", "application/json")
|
||||||
String(match.groupValues[1].flatMap { listOf('\uD83C', it + 0xDDA5) }.toCharArray()) + ' ' + company
|
conn.setRequestProperty("cookie", "${cookie.name}=${cookie.value}")
|
||||||
} else company
|
conn.setRequestProperty("x-csrf-token", csrf)
|
||||||
|
conn.outputStream.writer().use { it.write("{\"macAddress\":\"$mac\",\"not-web-search\":true}") }
|
||||||
|
when (val responseCode = conn.responseCode) {
|
||||||
|
200 -> conn.inputStream.bufferedReader().readText()
|
||||||
|
419 -> null
|
||||||
|
else -> throw IOException("Unhandled response code $responseCode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (response != null) break
|
||||||
|
}
|
||||||
|
if (response == null) throw IOException("Session creation failure")
|
||||||
|
val obj = JSONObject(response)
|
||||||
|
val result = if (obj.getJSONObject("blockDetails").getBoolean("blockFound")) {
|
||||||
|
val vendor = obj.getJSONObject("vendorDetails")
|
||||||
|
val company = vendor.getString("companyName")
|
||||||
|
val match = extractCountry(mac, response, vendor)
|
||||||
|
if (match != null) {
|
||||||
|
String(match.groupValues[1].flatMap { listOf('\uD83C', it + 0xDDA5) }.toCharArray()) + ' ' +
|
||||||
|
company
|
||||||
|
} else company
|
||||||
|
} else null
|
||||||
AppDatabase.instance.clientRecordDao.upsert(mac) {
|
AppDatabase.instance.clientRecordDao.upsert(mac) {
|
||||||
nickname = result
|
if (result != null) nickname = result
|
||||||
macLookupPending = false
|
macLookupPending = false
|
||||||
}
|
}
|
||||||
} catch (e: JSONException) {
|
} catch (_: CancellationException) {
|
||||||
if ((e as? UnexpectedError)?.error == "no result") {
|
|
||||||
// no vendor found, we should not retry in the future
|
|
||||||
AppDatabase.instance.clientRecordDao.upsert(mac) { macLookupPending = false }
|
|
||||||
} else Timber.w(e)
|
|
||||||
if (explicit) SmartSnackbar.make(e).show()
|
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Timber.d(e)
|
Timber.w(e)
|
||||||
if (explicit) SmartSnackbar.make(e).show()
|
if (explicit) SmartSnackbar.make(e).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractCountry(mac: MacAddressCompat, response: String, obj: JSONObject): MatchResult? {
|
private fun extractCountry(mac: MacAddress, response: String, obj: JSONObject): MatchResult? {
|
||||||
countryCodeRegex.matchEntire(obj.optString("country"))?.also { return it }
|
countryCodeRegex.matchEntire(obj.optString("countryCode"))?.also { return it }
|
||||||
val address = obj.optString("address")
|
val address = obj.optString("companyAddress")
|
||||||
if (address.isBlank()) return null
|
if (address.isBlank()) return null
|
||||||
countryCodeRegex.find(address)?.also { return it }
|
countryCodeRegex.find(address)?.also { return it }
|
||||||
Timber.w(UnexpectedError(mac, response))
|
Timber.w(UnexpectedError(mac, response))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package be.mygod.vpnhotspot.manage
|
package be.mygod.vpnhotspot.manage
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
import android.annotation.SuppressLint
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.bluetooth.BluetoothProfile
|
import android.bluetooth.BluetoothProfile
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
@@ -8,8 +8,6 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.core.os.BuildCompat
|
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.net.TetheringManager
|
import be.mygod.vpnhotspot.net.TetheringManager
|
||||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
import be.mygod.vpnhotspot.util.broadcastReceiver
|
||||||
@@ -18,7 +16,7 @@ import be.mygod.vpnhotspot.widget.SmartSnackbar
|
|||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
|
|
||||||
class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
|
class BluetoothTethering(context: Context, private val adapter: BluetoothAdapter, val stateListener: () -> Unit) :
|
||||||
BluetoothProfile.ServiceListener, AutoCloseable {
|
BluetoothProfile.ServiceListener, AutoCloseable {
|
||||||
companion object : BroadcastReceiver() {
|
companion object : BroadcastReceiver() {
|
||||||
/**
|
/**
|
||||||
@@ -26,17 +24,9 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
|
|||||||
*/
|
*/
|
||||||
private const val PAN = 5
|
private const val PAN = 5
|
||||||
private val clazz by lazy { Class.forName("android.bluetooth.BluetoothPan") }
|
private val clazz by lazy { Class.forName("android.bluetooth.BluetoothPan") }
|
||||||
private val constructor by lazy {
|
|
||||||
clazz.getDeclaredConstructor(Context::class.java, BluetoothProfile.ServiceListener::class.java).apply {
|
|
||||||
isAccessible = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private val isTetheringOn by lazy { clazz.getDeclaredMethod("isTetheringOn") }
|
private val isTetheringOn by lazy { clazz.getDeclaredMethod("isTetheringOn") }
|
||||||
|
|
||||||
fun pan(context: Context, serviceListener: BluetoothProfile.ServiceListener) =
|
private val BluetoothProfile.isTetheringOn get() = isTetheringOn(this) as Boolean
|
||||||
constructor.newInstance(context, serviceListener) as BluetoothProfile
|
|
||||||
val BluetoothProfile.isTetheringOn get() = isTetheringOn(this) as Boolean
|
|
||||||
fun BluetoothProfile.closePan() = BluetoothAdapter.getDefaultAdapter()!!.closeProfileProxy(PAN, this)
|
|
||||||
|
|
||||||
private fun registerBluetoothStateListener(receiver: BroadcastReceiver) =
|
private fun registerBluetoothStateListener(receiver: BroadcastReceiver) =
|
||||||
app.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
|
app.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
|
||||||
@@ -46,7 +36,6 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
|
|||||||
/**
|
/**
|
||||||
* https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/TetherSettings.java#215
|
* https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/TetherSettings.java#215
|
||||||
*/
|
*/
|
||||||
@TargetApi(24)
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
when (intent?.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {
|
when (intent?.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {
|
||||||
BluetoothAdapter.STATE_ON -> {
|
BluetoothAdapter.STATE_ON -> {
|
||||||
@@ -58,28 +47,12 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
|
|||||||
pendingCallback = null
|
pendingCallback = null
|
||||||
app.unregisterReceiver(this)
|
app.unregisterReceiver(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/TetherSettings.java#384
|
|
||||||
*/
|
|
||||||
@RequiresApi(24)
|
|
||||||
fun start(callback: TetheringManager.StartTetheringCallback) {
|
|
||||||
if (pendingCallback != null) return
|
|
||||||
val adapter = BluetoothAdapter.getDefaultAdapter()
|
|
||||||
try {
|
|
||||||
if (adapter?.state == BluetoothAdapter.STATE_OFF) {
|
|
||||||
registerBluetoothStateListener(this)
|
|
||||||
pendingCallback = callback
|
|
||||||
adapter.enable()
|
|
||||||
} else TetheringManager.startTethering(TetheringManager.TETHERING_BLUETOOTH, true, callback)
|
|
||||||
} catch (e: SecurityException) {
|
|
||||||
SmartSnackbar.make(e.readableMessage).shortToast().show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var proxyCreated = false
|
||||||
private var connected = false
|
private var connected = false
|
||||||
private var pan: BluetoothProfile? = null
|
private var pan: BluetoothProfile? = null
|
||||||
|
private var stoppedByUser = false
|
||||||
var activeFailureCause: Throwable? = null
|
var activeFailureCause: Throwable? = null
|
||||||
/**
|
/**
|
||||||
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/78d5efd/src/com/android/settings/TetherSettings.java
|
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/78d5efd/src/com/android/settings/TetherSettings.java
|
||||||
@@ -88,7 +61,7 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
|
|||||||
val pan = pan ?: return null
|
val pan = pan ?: return null
|
||||||
if (!connected) return null
|
if (!connected) return null
|
||||||
activeFailureCause = null
|
activeFailureCause = null
|
||||||
return BluetoothAdapter.getDefaultAdapter()?.state == BluetoothAdapter.STATE_ON && try {
|
val on = adapter.state == BluetoothAdapter.STATE_ON && try {
|
||||||
pan.isTetheringOn
|
pan.isTetheringOn
|
||||||
} catch (e: InvocationTargetException) {
|
} catch (e: InvocationTargetException) {
|
||||||
activeFailureCause = e.cause ?: e
|
activeFailureCause = e.cause ?: e
|
||||||
@@ -96,16 +69,21 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
|
|||||||
else Timber.w(e)
|
else Timber.w(e)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
return if (stoppedByUser) {
|
||||||
|
if (!on) stoppedByUser = false
|
||||||
|
false
|
||||||
|
} else on
|
||||||
}
|
}
|
||||||
|
|
||||||
private val receiver = broadcastReceiver { _, _ -> stateListener() }
|
private val receiver = broadcastReceiver { _, _ -> stateListener() }
|
||||||
|
|
||||||
fun ensureInit(context: Context) {
|
fun ensureInit(context: Context) {
|
||||||
if (pan == null && BluetoothAdapter.getDefaultAdapter() != null) try {
|
activeFailureCause = null
|
||||||
pan = pan(context, this)
|
if (!proxyCreated) try {
|
||||||
} catch (e: InvocationTargetException) {
|
check(adapter.getProfileProxy(context, this, PAN))
|
||||||
if (e.cause is SecurityException && BuildCompat.isAtLeastS()) Timber.d(e.readableMessage)
|
proxyCreated = true
|
||||||
else Timber.w(e)
|
} catch (e: SecurityException) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 31) Timber.d(e.readableMessage) else Timber.w(e)
|
||||||
activeFailureCause = e
|
activeFailureCause = e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,13 +94,38 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
|
|||||||
|
|
||||||
override fun onServiceDisconnected(profile: Int) {
|
override fun onServiceDisconnected(profile: Int) {
|
||||||
connected = false
|
connected = false
|
||||||
|
stoppedByUser = false
|
||||||
}
|
}
|
||||||
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
||||||
|
pan = proxy
|
||||||
connected = true
|
connected = true
|
||||||
stateListener()
|
stateListener()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/TetherSettings.java#384
|
||||||
|
*/
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
fun start(callback: TetheringManager.StartTetheringCallback, context: Context) {
|
||||||
|
if (pendingCallback == null) try {
|
||||||
|
if (adapter.state == BluetoothAdapter.STATE_OFF) {
|
||||||
|
registerBluetoothStateListener(BluetoothTethering)
|
||||||
|
pendingCallback = callback
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
if (!adapter.enable()) context.startActivity(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))
|
||||||
|
} else TetheringManager.startTethering(TetheringManager.TETHERING_BLUETOOTH, true, callback)
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
SmartSnackbar.make(e.readableMessage).shortToast().show()
|
||||||
|
pendingCallback = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun stop(callback: (Exception) -> Unit) {
|
||||||
|
TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH, callback)
|
||||||
|
stoppedByUser = true
|
||||||
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
app.unregisterReceiver(receiver)
|
app.unregisterReceiver(receiver)
|
||||||
pan?.closePan()
|
adapter.closeProfileProxy(PAN, pan)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package be.mygod.vpnhotspot.manage
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import be.mygod.vpnhotspot.R
|
import be.mygod.vpnhotspot.R
|
||||||
import be.mygod.vpnhotspot.TetheringService
|
import be.mygod.vpnhotspot.TetheringService
|
||||||
@@ -25,7 +24,7 @@ class InterfaceManager(private val parent: TetheringFragment, val iface: String)
|
|||||||
val data = binding.data as Data
|
val data = binding.data as Data
|
||||||
if (data.active) context.startService(Intent(context, TetheringService::class.java)
|
if (data.active) context.startService(Intent(context, TetheringService::class.java)
|
||||||
.putExtra(TetheringService.EXTRA_REMOVE_INTERFACE, iface))
|
.putExtra(TetheringService.EXTRA_REMOVE_INTERFACE, iface))
|
||||||
else ContextCompat.startForegroundService(context, Intent(context, TetheringService::class.java)
|
else context.startForegroundService(Intent(context, TetheringService::class.java)
|
||||||
.putExtra(TetheringService.EXTRA_ADD_INTERFACES, arrayOf(iface)))
|
.putExtra(TetheringService.EXTRA_ADD_INTERFACES, arrayOf(iface)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
package be.mygod.vpnhotspot.manage
|
package be.mygod.vpnhotspot.manage
|
||||||
|
|
||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import be.mygod.vpnhotspot.R
|
import be.mygod.vpnhotspot.R
|
||||||
import be.mygod.vpnhotspot.net.IpNeighbour
|
import be.mygod.vpnhotspot.net.IpNeighbour
|
||||||
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
||||||
import be.mygod.vpnhotspot.util.KillableTileService
|
import be.mygod.vpnhotspot.util.KillableTileService
|
||||||
import java.net.Inet4Address
|
import java.net.Inet4Address
|
||||||
|
|
||||||
@RequiresApi(24)
|
|
||||||
abstract class IpNeighbourMonitoringTileService : KillableTileService(), IpNeighbourMonitor.Callback {
|
abstract class IpNeighbourMonitoringTileService : KillableTileService(), IpNeighbourMonitor.Callback {
|
||||||
private var neighbours: Collection<IpNeighbour> = emptyList()
|
private var neighbours: Collection<IpNeighbour> = emptyList()
|
||||||
abstract fun updateTile()
|
abstract fun updateTile()
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
package be.mygod.vpnhotspot.manage
|
package be.mygod.vpnhotspot.manage
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.*
|
import android.content.ComponentName
|
||||||
import android.location.LocationManager
|
import android.content.Context
|
||||||
|
import android.content.ServiceConnection
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.provider.Settings
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.LocalOnlyHotspotService
|
import be.mygod.vpnhotspot.LocalOnlyHotspotService
|
||||||
@@ -17,15 +14,15 @@ import be.mygod.vpnhotspot.R
|
|||||||
import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
|
import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
|
||||||
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
|
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
|
||||||
import be.mygod.vpnhotspot.util.formatAddresses
|
import be.mygod.vpnhotspot.util.formatAddresses
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
|
||||||
import java.net.NetworkInterface
|
import java.net.NetworkInterface
|
||||||
|
|
||||||
@RequiresApi(26)
|
|
||||||
class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager(), ServiceConnection {
|
class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager(), ServiceConnection {
|
||||||
companion object {
|
companion object {
|
||||||
val permission = if (Build.VERSION.SDK_INT >= 29) {
|
val permission = when {
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION
|
Build.VERSION.SDK_INT >= 33 -> Manifest.permission.NEARBY_WIFI_DEVICES
|
||||||
} else Manifest.permission.ACCESS_COARSE_LOCATION
|
Build.VERSION.SDK_INT >= 29 -> Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
|
else -> Manifest.permission.ACCESS_COARSE_LOCATION
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ViewHolder(val binding: ListitemInterfaceBinding) : RecyclerView.ViewHolder(binding.root),
|
class ViewHolder(val binding: ListitemInterfaceBinding) : RecyclerView.ViewHolder(binding.root),
|
||||||
@@ -57,23 +54,7 @@ class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager()
|
|||||||
ServiceForegroundConnector(parent, this, LocalOnlyHotspotService::class)
|
ServiceForegroundConnector(parent, this, LocalOnlyHotspotService::class)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun start(context: Context) = app.startServiceWithLocation<LocalOnlyHotspotService>(context)
|
||||||
* LOH also requires location to be turned on. Source:
|
|
||||||
* https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/53e0284/service/java/com/android/server/wifi/WifiServiceImpl.java#1204
|
|
||||||
* https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/53e0284/service/java/com/android/server/wifi/WifiSettingsStore.java#228
|
|
||||||
*/
|
|
||||||
fun start(context: Context) {
|
|
||||||
if (if (Build.VERSION.SDK_INT < 28) @Suppress("DEPRECATION") {
|
|
||||||
Settings.Secure.getInt(context.contentResolver, Settings.Secure.LOCATION_MODE,
|
|
||||||
Settings.Secure.LOCATION_MODE_OFF) == Settings.Secure.LOCATION_MODE_OFF
|
|
||||||
} else context.getSystemService<LocationManager>()?.isLocationEnabled != true) try {
|
|
||||||
context.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
|
|
||||||
Toast.makeText(context, R.string.tethering_temp_hotspot_location, Toast.LENGTH_LONG).show()
|
|
||||||
} catch (e: ActivityNotFoundException) {
|
|
||||||
app.logEvent("location_settings") { param("message", e.toString()) }
|
|
||||||
SmartSnackbar.make(R.string.tethering_temp_hotspot_location).show()
|
|
||||||
} else context.startForegroundService(Intent(context, LocalOnlyHotspotService::class.java))
|
|
||||||
}
|
|
||||||
|
|
||||||
override val type get() = VIEW_TYPE_LOCAL_ONLY_HOTSPOT
|
override val type get() = VIEW_TYPE_LOCAL_ONLY_HOTSPOT
|
||||||
private val data = Data()
|
private val data = Data()
|
||||||
|
|||||||
@@ -6,12 +6,10 @@ import android.content.Intent
|
|||||||
import android.graphics.drawable.Icon
|
import android.graphics.drawable.Icon
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import be.mygod.vpnhotspot.LocalOnlyHotspotService
|
import be.mygod.vpnhotspot.LocalOnlyHotspotService
|
||||||
import be.mygod.vpnhotspot.R
|
import be.mygod.vpnhotspot.R
|
||||||
import be.mygod.vpnhotspot.util.stopAndUnbind
|
import be.mygod.vpnhotspot.util.stopAndUnbind
|
||||||
|
|
||||||
@RequiresApi(26)
|
|
||||||
class LocalOnlyHotspotTileService : IpNeighbourMonitoringTileService() {
|
class LocalOnlyHotspotTileService : IpNeighbourMonitoringTileService() {
|
||||||
private val tile by lazy { Icon.createWithResource(application, R.drawable.ic_action_perm_scan_wifi) }
|
private val tile by lazy { Icon.createWithResource(application, R.drawable.ic_action_perm_scan_wifi) }
|
||||||
|
|
||||||
@@ -38,7 +36,7 @@ class LocalOnlyHotspotTileService : IpNeighbourMonitoringTileService() {
|
|||||||
label = getText(R.string.tethering_temp_hotspot)
|
label = getText(R.string.tethering_temp_hotspot)
|
||||||
} else {
|
} else {
|
||||||
state = Tile.STATE_ACTIVE
|
state = Tile.STATE_ACTIVE
|
||||||
label = binder.configuration?.ssid ?: getText(R.string.tethering_temp_hotspot)
|
label = binder.configuration?.ssid?.toString() ?: getText(R.string.tethering_temp_hotspot)
|
||||||
subtitleDevices { it == iface }
|
subtitleDevices { it == iface }
|
||||||
}
|
}
|
||||||
updateTile()
|
updateTile()
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ object ManageBar : Manager() {
|
|||||||
private const val SETTINGS_2 = "com.android.settings.TetherSettings"
|
private const val SETTINGS_2 = "com.android.settings.TetherSettings"
|
||||||
|
|
||||||
object Data : BaseObservable() {
|
object Data : BaseObservable() {
|
||||||
val offloadEnabled get() = TetherOffloadManager.supported && TetherOffloadManager.enabled
|
val offloadEnabled get() = TetherOffloadManager.enabled
|
||||||
}
|
}
|
||||||
class ViewHolder(binding: ListitemManageBinding) : RecyclerView.ViewHolder(binding.root), View.OnClickListener {
|
class ViewHolder(binding: ListitemManageBinding) : RecyclerView.ViewHolder(binding.root), View.OnClickListener {
|
||||||
init {
|
init {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package be.mygod.vpnhotspot.manage
|
package be.mygod.vpnhotspot.manage
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.annotation.TargetApi
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
@@ -18,9 +17,6 @@ abstract class Manager {
|
|||||||
const val VIEW_TYPE_USB = 3
|
const val VIEW_TYPE_USB = 3
|
||||||
const val VIEW_TYPE_BLUETOOTH = 4
|
const val VIEW_TYPE_BLUETOOTH = 4
|
||||||
const val VIEW_TYPE_ETHERNET = 8
|
const val VIEW_TYPE_ETHERNET = 8
|
||||||
const val VIEW_TYPE_NCM = 9
|
|
||||||
const val VIEW_TYPE_WIGIG = 10
|
|
||||||
const val VIEW_TYPE_WIFI_LEGACY = 5
|
|
||||||
const val VIEW_TYPE_LOCAL_ONLY_HOTSPOT = 6
|
const val VIEW_TYPE_LOCAL_ONLY_HOTSPOT = 6
|
||||||
const val VIEW_TYPE_REPEATER = 7
|
const val VIEW_TYPE_REPEATER = 7
|
||||||
|
|
||||||
@@ -35,13 +31,10 @@ abstract class Manager {
|
|||||||
VIEW_TYPE_WIFI,
|
VIEW_TYPE_WIFI,
|
||||||
VIEW_TYPE_USB,
|
VIEW_TYPE_USB,
|
||||||
VIEW_TYPE_BLUETOOTH,
|
VIEW_TYPE_BLUETOOTH,
|
||||||
VIEW_TYPE_ETHERNET,
|
VIEW_TYPE_ETHERNET -> {
|
||||||
VIEW_TYPE_NCM,
|
|
||||||
VIEW_TYPE_WIGIG,
|
|
||||||
VIEW_TYPE_WIFI_LEGACY -> {
|
|
||||||
TetherManager.ViewHolder(ListitemInterfaceBinding.inflate(inflater, parent, false))
|
TetherManager.ViewHolder(ListitemInterfaceBinding.inflate(inflater, parent, false))
|
||||||
}
|
}
|
||||||
VIEW_TYPE_LOCAL_ONLY_HOTSPOT -> @TargetApi(26) {
|
VIEW_TYPE_LOCAL_ONLY_HOTSPOT -> {
|
||||||
LocalOnlyHotspotManager.ViewHolder(ListitemInterfaceBinding.inflate(inflater, parent, false))
|
LocalOnlyHotspotManager.ViewHolder(ListitemInterfaceBinding.inflate(inflater, parent, false))
|
||||||
}
|
}
|
||||||
VIEW_TYPE_REPEATER -> RepeaterManager.ViewHolder(ListitemRepeaterBinding.inflate(inflater, parent, false))
|
VIEW_TYPE_REPEATER -> RepeaterManager.ViewHolder(ListitemRepeaterBinding.inflate(inflater, parent, false))
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import android.content.ComponentName
|
|||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.content.pm.PackageManager
|
import android.net.MacAddress
|
||||||
import android.net.wifi.SoftApConfiguration
|
import android.net.wifi.SoftApConfiguration
|
||||||
import android.net.wifi.p2p.WifiP2pGroup
|
import android.net.wifi.p2p.WifiP2pGroup
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -17,25 +17,33 @@ import android.view.WindowManager
|
|||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.databinding.BaseObservable
|
import androidx.databinding.BaseObservable
|
||||||
import androidx.databinding.Bindable
|
import androidx.databinding.Bindable
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.withStarted
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import be.mygod.vpnhotspot.*
|
import be.mygod.vpnhotspot.AlertDialogFragment
|
||||||
|
import be.mygod.vpnhotspot.BR
|
||||||
|
import be.mygod.vpnhotspot.Empty
|
||||||
|
import be.mygod.vpnhotspot.R
|
||||||
|
import be.mygod.vpnhotspot.RepeaterService
|
||||||
import be.mygod.vpnhotspot.databinding.ListitemRepeaterBinding
|
import be.mygod.vpnhotspot.databinding.ListitemRepeaterBinding
|
||||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
|
||||||
import be.mygod.vpnhotspot.net.wifi.P2pSupplicantConfiguration
|
import be.mygod.vpnhotspot.net.wifi.P2pSupplicantConfiguration
|
||||||
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
|
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiApDialogFragment
|
import be.mygod.vpnhotspot.net.wifi.WifiApDialogFragment
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
||||||
|
import be.mygod.vpnhotspot.net.wifi.WifiSsidCompat
|
||||||
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
|
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
|
||||||
import be.mygod.vpnhotspot.util.formatAddresses
|
import be.mygod.vpnhotspot.util.formatAddresses
|
||||||
import be.mygod.vpnhotspot.util.showAllowingStateLoss
|
import be.mygod.vpnhotspot.util.showAllowingStateLoss
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.net.NetworkInterface
|
import java.net.NetworkInterface
|
||||||
@@ -71,6 +79,9 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
|||||||
NetworkInterface.getByName(p2pInterface ?: return "")?.formatAddresses() ?: ""
|
NetworkInterface.getByName(p2pInterface ?: return "")?.formatAddresses() ?: ""
|
||||||
} catch (_: SocketException) {
|
} catch (_: SocketException) {
|
||||||
""
|
""
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.w(e)
|
||||||
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,12 +100,10 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
|||||||
val binder = binder
|
val binder = binder
|
||||||
when (binder?.service?.status) {
|
when (binder?.service?.status) {
|
||||||
RepeaterService.Status.IDLE -> if (Build.VERSION.SDK_INT < 29) parent.requireContext().let { context ->
|
RepeaterService.Status.IDLE -> if (Build.VERSION.SDK_INT < 29) parent.requireContext().let { context ->
|
||||||
ContextCompat.startForegroundService(context, Intent(context, RepeaterService::class.java))
|
context.startForegroundService(Intent(context, RepeaterService::class.java))
|
||||||
} else if (parent.requireContext().checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) ==
|
} else parent.startRepeater.launch(if (Build.VERSION.SDK_INT >= 33) {
|
||||||
PackageManager.PERMISSION_GRANTED ||
|
Manifest.permission.NEARBY_WIFI_DEVICES
|
||||||
parent.shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) {
|
} else Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
parent.startRepeater.launch(Manifest.permission.ACCESS_FINE_LOCATION)
|
|
||||||
} else SmartSnackbar.make(R.string.repeater_missing_location_permissions).shortToast().show()
|
|
||||||
RepeaterService.Status.ACTIVE -> binder.shutdown()
|
RepeaterService.Status.ACTIVE -> binder.shutdown()
|
||||||
else -> { }
|
else -> { }
|
||||||
}
|
}
|
||||||
@@ -148,8 +157,10 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
|||||||
fun configure() {
|
fun configure() {
|
||||||
if (configuring) return
|
if (configuring) return
|
||||||
configuring = true
|
configuring = true
|
||||||
parent.viewLifecycleOwner.lifecycleScope.launchWhenCreated {
|
val owner = parent.viewLifecycleOwner
|
||||||
getConfiguration()?.let { (config, readOnly) ->
|
owner.lifecycleScope.launch {
|
||||||
|
val (config, readOnly) = getConfiguration() ?: return@launch
|
||||||
|
owner.withStarted {
|
||||||
WifiApDialogFragment().apply {
|
WifiApDialogFragment().apply {
|
||||||
arg(WifiApDialogFragment.Arg(config, readOnly, true))
|
arg(WifiApDialogFragment.Arg(config, readOnly, true))
|
||||||
key(this@RepeaterManager.javaClass.name)
|
key(this@RepeaterManager.javaClass.name)
|
||||||
@@ -191,25 +202,33 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
|||||||
val passphrase = RepeaterService.passphrase
|
val passphrase = RepeaterService.passphrase
|
||||||
if (networkName != null && passphrase != null) {
|
if (networkName != null && passphrase != null) {
|
||||||
return SoftApConfigurationCompat(
|
return SoftApConfigurationCompat(
|
||||||
ssid = networkName,
|
ssid = networkName,
|
||||||
passphrase = passphrase,
|
passphrase = passphrase,
|
||||||
securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, // is not actually used
|
securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, // is not actually used
|
||||||
isAutoShutdownEnabled = RepeaterService.isAutoShutdownEnabled,
|
isAutoShutdownEnabled = RepeaterService.isAutoShutdownEnabled,
|
||||||
shutdownTimeoutMillis = RepeaterService.shutdownTimeoutMillis).apply {
|
shutdownTimeoutMillis = RepeaterService.shutdownTimeoutMillis,
|
||||||
|
macRandomizationSetting = if (WifiApManager.p2pMacRandomizationSupported) {
|
||||||
|
SoftApConfigurationCompat.RANDOMIZATION_NON_PERSISTENT
|
||||||
|
} else SoftApConfigurationCompat.RANDOMIZATION_NONE,
|
||||||
|
vendorElements = RepeaterService.vendorElements,
|
||||||
|
).apply {
|
||||||
bssid = RepeaterService.deviceAddress
|
bssid = RepeaterService.deviceAddress
|
||||||
setChannel(RepeaterService.operatingChannel, RepeaterService.operatingBand)
|
setChannel(RepeaterService.operatingChannel, RepeaterService.operatingBand)
|
||||||
setMacRandomizationEnabled(WifiApManager.p2pMacRandomizationSupported)
|
|
||||||
} to false
|
} to false
|
||||||
}
|
}
|
||||||
} else binder?.let { binder ->
|
} else binder?.let { binder ->
|
||||||
val group = binder.group ?: binder.fetchPersistentGroup().let { binder.group }
|
val group = binder.group ?: binder.fetchPersistentGroup().let { binder.group }
|
||||||
if (group != null) return SoftApConfigurationCompat(
|
if (group != null) return SoftApConfigurationCompat(
|
||||||
ssid = group.networkName,
|
ssid = WifiSsidCompat.fromUtf8Text(group.networkName),
|
||||||
securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, // is not actually used
|
securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, // is not actually used
|
||||||
isAutoShutdownEnabled = RepeaterService.isAutoShutdownEnabled,
|
isAutoShutdownEnabled = RepeaterService.isAutoShutdownEnabled,
|
||||||
shutdownTimeoutMillis = RepeaterService.shutdownTimeoutMillis).run {
|
shutdownTimeoutMillis = RepeaterService.shutdownTimeoutMillis,
|
||||||
|
macRandomizationSetting = if (WifiApManager.p2pMacRandomizationSupported) {
|
||||||
|
SoftApConfigurationCompat.RANDOMIZATION_NON_PERSISTENT
|
||||||
|
} else SoftApConfigurationCompat.RANDOMIZATION_NONE,
|
||||||
|
vendorElements = RepeaterService.vendorElements,
|
||||||
|
).run {
|
||||||
setChannel(RepeaterService.operatingChannel)
|
setChannel(RepeaterService.operatingChannel)
|
||||||
setMacRandomizationEnabled(WifiApManager.p2pMacRandomizationSupported)
|
|
||||||
try {
|
try {
|
||||||
val config = P2pSupplicantConfiguration(group)
|
val config = P2pSupplicantConfiguration(group)
|
||||||
config.init(binder.obtainDeviceAddress()?.toString())
|
config.init(binder.obtainDeviceAddress()?.toString())
|
||||||
@@ -221,7 +240,7 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
|||||||
if (e !is CancellationException) Timber.w(e)
|
if (e !is CancellationException) Timber.w(e)
|
||||||
passphrase = group.passphrase
|
passphrase = group.passphrase
|
||||||
try {
|
try {
|
||||||
bssid = group.owner?.deviceAddress?.let(MacAddressCompat.Companion::fromString)
|
bssid = group.owner?.deviceAddress?.let(MacAddress::fromString)
|
||||||
} catch (_: IllegalArgumentException) { }
|
} catch (_: IllegalArgumentException) { }
|
||||||
this to true
|
this to true
|
||||||
}
|
}
|
||||||
@@ -231,15 +250,19 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
private suspend fun updateConfiguration(config: SoftApConfigurationCompat) {
|
private suspend fun updateConfiguration(config: SoftApConfigurationCompat) {
|
||||||
val (band, channel) = config.requireSingleBand()
|
val (band, channel) = SoftApConfigurationCompat.requireSingleBand(config.channels)
|
||||||
if (RepeaterService.safeMode) {
|
if (RepeaterService.safeMode) {
|
||||||
RepeaterService.networkName = config.ssid
|
RepeaterService.networkName = config.ssid
|
||||||
RepeaterService.deviceAddress = config.bssid
|
RepeaterService.deviceAddress = config.bssid
|
||||||
RepeaterService.passphrase = config.passphrase
|
RepeaterService.passphrase = config.passphrase
|
||||||
} else holder.config?.let { master ->
|
} else holder.config?.let { master ->
|
||||||
val binder = binder
|
val binder = binder
|
||||||
if (binder?.group?.networkName != config.ssid || master.psk != config.passphrase ||
|
val mayBeModified = master.psk != config.passphrase || master.bssid != config.bssid || config.ssid.run {
|
||||||
master.bssid != config.bssid) try {
|
if (this != null) decode().let {
|
||||||
|
it == null || binder?.group?.networkName != it
|
||||||
|
} else binder?.group?.networkName != null
|
||||||
|
}
|
||||||
|
if (mayBeModified) try {
|
||||||
withContext(Dispatchers.Default) { master.update(config.ssid!!, config.passphrase!!, config.bssid) }
|
withContext(Dispatchers.Default) { master.update(config.ssid!!, config.passphrase!!, config.bssid) }
|
||||||
(this.binder ?: binder)?.group = null
|
(this.binder ?: binder)?.group = null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -252,5 +275,6 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
|||||||
RepeaterService.operatingChannel = channel
|
RepeaterService.operatingChannel = channel
|
||||||
RepeaterService.isAutoShutdownEnabled = config.isAutoShutdownEnabled
|
RepeaterService.isAutoShutdownEnabled = config.isAutoShutdownEnabled
|
||||||
RepeaterService.shutdownTimeoutMillis = config.shutdownTimeoutMillis
|
RepeaterService.shutdownTimeoutMillis = config.shutdownTimeoutMillis
|
||||||
|
RepeaterService.vendorElements = config.vendorElements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,12 @@ import android.graphics.drawable.Icon
|
|||||||
import android.net.wifi.p2p.WifiP2pGroup
|
import android.net.wifi.p2p.WifiP2pGroup
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import be.mygod.vpnhotspot.R
|
import be.mygod.vpnhotspot.R
|
||||||
import be.mygod.vpnhotspot.RepeaterService
|
import be.mygod.vpnhotspot.RepeaterService
|
||||||
import be.mygod.vpnhotspot.util.KillableTileService
|
import be.mygod.vpnhotspot.util.KillableTileService
|
||||||
import be.mygod.vpnhotspot.util.Services
|
import be.mygod.vpnhotspot.util.Services
|
||||||
import be.mygod.vpnhotspot.util.stopAndUnbind
|
import be.mygod.vpnhotspot.util.stopAndUnbind
|
||||||
|
|
||||||
@RequiresApi(24)
|
|
||||||
class RepeaterTileService : KillableTileService() {
|
class RepeaterTileService : KillableTileService() {
|
||||||
private val tile by lazy { Icon.createWithResource(application, R.drawable.ic_action_settings_input_antenna) }
|
private val tile by lazy { Icon.createWithResource(application, R.drawable.ic_action_settings_input_antenna) }
|
||||||
|
|
||||||
@@ -37,8 +34,7 @@ class RepeaterTileService : KillableTileService() {
|
|||||||
val binder = binder
|
val binder = binder
|
||||||
if (binder == null) tapPending = true else when (binder.service.status) {
|
if (binder == null) tapPending = true else when (binder.service.status) {
|
||||||
RepeaterService.Status.ACTIVE -> binder.shutdown()
|
RepeaterService.Status.ACTIVE -> binder.shutdown()
|
||||||
RepeaterService.Status.IDLE -> ContextCompat.startForegroundService(this,
|
RepeaterService.Status.IDLE -> startForegroundService(Intent(this, RepeaterService::class.java))
|
||||||
Intent(this, RepeaterService::class.java))
|
|
||||||
else -> { }
|
else -> { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package be.mygod.vpnhotspot.manage
|
|||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.annotation.TargetApi
|
import android.annotation.TargetApi
|
||||||
import android.content.ClipData
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
@@ -15,7 +15,6 @@ import android.view.View
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.os.BuildCompat
|
|
||||||
import androidx.core.view.updatePaddingRelative
|
import androidx.core.view.updatePaddingRelative
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
@@ -56,19 +55,23 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
override fun onClick(v: View?) {
|
override fun onClick(v: View?) {
|
||||||
val manager = manager!!
|
val manager = manager!!
|
||||||
val mainActivity = manager.parent.activity as MainActivity
|
val mainActivity = manager.parent.activity as MainActivity
|
||||||
if (Build.VERSION.SDK_INT >= 23 && !Settings.System.canWrite(mainActivity)) try {
|
if (!Settings.System.canWrite(mainActivity)) try {
|
||||||
manager.parent.startActivity(Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS,
|
manager.parent.startActivity(Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS,
|
||||||
"package:${mainActivity.packageName}".toUri()))
|
"package:${mainActivity.packageName}".toUri()))
|
||||||
return
|
return
|
||||||
} catch (e: RuntimeException) {
|
} catch (e: RuntimeException) {
|
||||||
app.logEvent("manage_write_settings") { param("message", e.toString()) }
|
app.logEvent("manage_write_settings") { param("message", e.toString()) }
|
||||||
}
|
}
|
||||||
if (manager.isStarted) try {
|
when (manager.isStarted) {
|
||||||
manager.stop()
|
true -> try {
|
||||||
} catch (e: InvocationTargetException) {
|
manager.stop()
|
||||||
if (e.targetException !is SecurityException) Timber.w(e)
|
} catch (e: InvocationTargetException) {
|
||||||
manager.onException(e)
|
if (e.targetException !is SecurityException) Timber.w(e)
|
||||||
} else manager.start()
|
manager.onException(e)
|
||||||
|
}
|
||||||
|
false -> manager.start()
|
||||||
|
null -> manager.onClickNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,13 +82,14 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
override val icon get() = tetherType.icon
|
override val icon get() = tetherType.icon
|
||||||
override val title get() = this@TetherManager.title
|
override val title get() = this@TetherManager.title
|
||||||
override val text get() = this@TetherManager.text
|
override val text get() = this@TetherManager.text
|
||||||
override val active get() = isStarted
|
override val active get() = isStarted == true
|
||||||
}
|
}
|
||||||
|
|
||||||
val data = Data()
|
val data = Data()
|
||||||
abstract val title: CharSequence
|
abstract val title: CharSequence
|
||||||
abstract val tetherType: TetherType
|
abstract val tetherType: TetherType
|
||||||
open val isStarted get() = parent.enabledTypes.contains(tetherType)
|
open val isStarted: Boolean? get() = parent.enabledTypes.contains(tetherType) ||
|
||||||
|
tetherType == TetherType.USB && parent.enabledTypes.contains(TetherType.NCM)
|
||||||
protected open val text: CharSequence get() = baseError ?: ""
|
protected open val text: CharSequence get() = baseError ?: ""
|
||||||
|
|
||||||
protected var baseError: String? = null
|
protected var baseError: String? = null
|
||||||
@@ -93,6 +97,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
|
|
||||||
protected abstract fun start()
|
protected abstract fun start()
|
||||||
protected abstract fun stop()
|
protected abstract fun stop()
|
||||||
|
protected open fun onClickNull(): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
override fun onTetheringStarted() = data.notifyChange()
|
override fun onTetheringStarted() = data.notifyChange()
|
||||||
override fun onTetheringFailed(error: Int?) {
|
override fun onTetheringFailed(error: Int?) {
|
||||||
@@ -120,21 +125,20 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateErrorMessage(errored: List<String>, lastErrors: Map<String, Int>) {
|
fun updateErrorMessage(errored: List<String>, lastErrors: Map<String, Int>) {
|
||||||
val interested = errored.filter { TetherType.ofInterface(it) == tetherType }
|
val interested = errored.filter { TetherType.ofInterface(it).isA(tetherType) }
|
||||||
baseError = if (interested.isEmpty()) null else interested.joinToString("\n") { iface ->
|
baseError = if (interested.isEmpty()) null else interested.joinToString("\n") { iface ->
|
||||||
"$iface: " + try {
|
"$iface: " + try {
|
||||||
TetheringManager.tetherErrorLookup(if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
|
TetheringManager.tetherErrorLookup(if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
|
||||||
TetheringManager.getLastTetherError(iface)
|
TetheringManager.getLastTetherError(iface)
|
||||||
} else lastErrors[iface] ?: 0)
|
} else lastErrors[iface] ?: 0)
|
||||||
} catch (e: InvocationTargetException) {
|
} catch (e: InvocationTargetException) {
|
||||||
if (Build.VERSION.SDK_INT !in 24..25 || e.cause !is SecurityException) Timber.w(e) else Timber.d(e)
|
if (e.cause !is SecurityException) Timber.w(e) else Timber.d(e)
|
||||||
e.readableMessage
|
e.readableMessage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
data.notifyChange()
|
data.notifyChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(24)
|
|
||||||
class Wifi(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver,
|
class Wifi(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver,
|
||||||
WifiApManager.SoftApCallbackCompat {
|
WifiApManager.SoftApCallbackCompat {
|
||||||
private var failureReason: Int? = null
|
private var failureReason: Int? = null
|
||||||
@@ -143,24 +147,19 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
private var capability: Parcelable? = null
|
private var capability: Parcelable? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (Build.VERSION.SDK_INT >= 28) parent.viewLifecycleOwner.lifecycle.addObserver(this)
|
parent.viewLifecycleOwner.lifecycle.addObserver(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(28)
|
|
||||||
override fun onStart(owner: LifecycleOwner) {
|
override fun onStart(owner: LifecycleOwner) {
|
||||||
WifiApCommands.registerSoftApCallback(this)
|
WifiApCommands.registerSoftApCallback(this)
|
||||||
}
|
}
|
||||||
@TargetApi(28)
|
|
||||||
override fun onStop(owner: LifecycleOwner) {
|
override fun onStop(owner: LifecycleOwner) {
|
||||||
WifiApCommands.unregisterSoftApCallback(this)
|
WifiApCommands.unregisterSoftApCallback(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStateChanged(state: Int, failureReason: Int) {
|
override fun onStateChanged(state: Int, failureReason: Int) {
|
||||||
if (state < 10 || state > 14) {
|
if (!WifiApManager.checkWifiApState(state)) return
|
||||||
Timber.w(Exception("Unknown state $state, $failureReason"))
|
this.failureReason = if (state == WifiApManager.WIFI_AP_STATE_FAILED) failureReason else null
|
||||||
return
|
|
||||||
}
|
|
||||||
this.failureReason = if (state == 14) failureReason else null // WIFI_AP_STATE_FAILED
|
|
||||||
data.notifyChange()
|
data.notifyChange()
|
||||||
}
|
}
|
||||||
override fun onNumClientsChanged(numClients: Int) {
|
override fun onNumClientsChanged(numClients: Int) {
|
||||||
@@ -175,21 +174,6 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
this.capability = capability
|
this.capability = capability
|
||||||
data.notifyChange()
|
data.notifyChange()
|
||||||
}
|
}
|
||||||
@RequiresApi(30)
|
|
||||||
override fun onBlockedClientConnecting(client: Parcelable, blockedReason: Int) {
|
|
||||||
@Suppress("NAME_SHADOWING")
|
|
||||||
val client = WifiClient(client)
|
|
||||||
val macAddress = client.macAddress
|
|
||||||
var name = macAddress.toString()
|
|
||||||
if (BuildCompat.isAtLeastS()) client.apInstanceIdentifier?.let { name += "%$it" }
|
|
||||||
val reason = WifiApManager.clientBlockLookup(blockedReason, true)
|
|
||||||
Timber.i("$name blocked from connecting: $reason ($blockedReason)")
|
|
||||||
SmartSnackbar.make(parent.getString(R.string.tethering_manage_wifi_client_blocked, name, reason)).apply {
|
|
||||||
action(R.string.tethering_manage_wifi_copy_mac) {
|
|
||||||
app.clipboard.setPrimaryClip(ClipData.newPlainText(null, macAddress.toString()))
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
override val title get() = parent.getString(R.string.tethering_manage_wifi)
|
override val title get() = parent.getString(R.string.tethering_manage_wifi)
|
||||||
override val tetherType get() = TetherType.WIFI
|
override val tetherType get() = TetherType.WIFI
|
||||||
@@ -201,7 +185,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
val numClients = numClients
|
val numClients = numClients
|
||||||
val maxClients = capability.maxSupportedClients
|
val maxClients = capability.maxSupportedClients
|
||||||
var features = capability.supportedFeatures
|
var features = capability.supportedFeatures
|
||||||
if (BuildCompat.isAtLeastS()) for ((flag, band) in arrayOf(
|
if (Build.VERSION.SDK_INT >= 31) for ((flag, band) in arrayOf(
|
||||||
SoftApCapability.SOFTAP_FEATURE_BAND_24G_SUPPORTED to SoftApConfigurationCompat.BAND_2GHZ,
|
SoftApCapability.SOFTAP_FEATURE_BAND_24G_SUPPORTED to SoftApConfigurationCompat.BAND_2GHZ,
|
||||||
SoftApCapability.SOFTAP_FEATURE_BAND_5G_SUPPORTED to SoftApConfigurationCompat.BAND_5GHZ,
|
SoftApCapability.SOFTAP_FEATURE_BAND_5G_SUPPORTED to SoftApConfigurationCompat.BAND_5GHZ,
|
||||||
SoftApCapability.SOFTAP_FEATURE_BAND_6G_SUPPORTED to SoftApConfigurationCompat.BAND_6GHZ,
|
SoftApCapability.SOFTAP_FEATURE_BAND_6G_SUPPORTED to SoftApConfigurationCompat.BAND_6GHZ,
|
||||||
@@ -217,7 +201,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
R.string.tethering_manage_wifi_feature_ap_mac_randomization))
|
R.string.tethering_manage_wifi_feature_ap_mac_randomization))
|
||||||
if (Services.wifi.isStaApConcurrencySupported) yield(parent.getText(
|
if (Services.wifi.isStaApConcurrencySupported) yield(parent.getText(
|
||||||
R.string.tethering_manage_wifi_feature_sta_ap_concurrency))
|
R.string.tethering_manage_wifi_feature_sta_ap_concurrency))
|
||||||
if (BuildCompat.isAtLeastS()) {
|
if (Build.VERSION.SDK_INT >= 31) {
|
||||||
if (Services.wifi.isBridgedApConcurrencySupported) yield(parent.getText(
|
if (Services.wifi.isBridgedApConcurrencySupported) yield(parent.getText(
|
||||||
R.string.tethering_manage_wifi_feature_bridged_ap_concurrency))
|
R.string.tethering_manage_wifi_feature_bridged_ap_concurrency))
|
||||||
if (Services.wifi.isStaBridgedApConcurrencySupported) yield(parent.getText(
|
if (Services.wifi.isStaBridgedApConcurrencySupported) yield(parent.getText(
|
||||||
@@ -228,63 +212,46 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
yield(SoftApCapability.featureLookup(bit, true))
|
yield(SoftApCapability.featureLookup(bit, true))
|
||||||
features = features and bit.inv()
|
features = features and bit.inv()
|
||||||
}
|
}
|
||||||
}.joinToSpanned().let {
|
}.joinToSpanned().ifEmpty { parent.getText(R.string.tethering_manage_wifi_no_features) })
|
||||||
if (it.isEmpty()) parent.getText(R.string.tethering_manage_wifi_no_features) else it
|
if (Build.VERSION.SDK_INT >= 31) {
|
||||||
})
|
|
||||||
if (BuildCompat.isAtLeastS()) {
|
|
||||||
val list = SoftApConfigurationCompat.BAND_TYPES.map { band ->
|
val list = SoftApConfigurationCompat.BAND_TYPES.map { band ->
|
||||||
val channels = capability.getSupportedChannelList(band)
|
val channels = capability.getSupportedChannelList(band)
|
||||||
if (channels.isNotEmpty()) StringBuilder().apply {
|
if (channels.isNotEmpty()) {
|
||||||
append(SoftApConfigurationCompat.bandLookup(band, true))
|
"${SoftApConfigurationCompat.bandLookup(band, true)} (${RangeInput.toString(channels)})"
|
||||||
append(" (")
|
|
||||||
channels.sort()
|
|
||||||
var pending: Int? = null
|
|
||||||
var last = channels[0]
|
|
||||||
append(last)
|
|
||||||
for (channel in channels.asSequence().drop(1)) {
|
|
||||||
if (channel == last + 1) pending = channel else {
|
|
||||||
pending?.let {
|
|
||||||
append('-')
|
|
||||||
append(it)
|
|
||||||
pending = null
|
|
||||||
}
|
|
||||||
append(',')
|
|
||||||
append(channel)
|
|
||||||
}
|
|
||||||
last = channel
|
|
||||||
}
|
|
||||||
pending?.let {
|
|
||||||
append('-')
|
|
||||||
append(it)
|
|
||||||
}
|
|
||||||
append(')')
|
|
||||||
} else null
|
} else null
|
||||||
}.filterNotNull()
|
}.filterNotNull()
|
||||||
if (list.isNotEmpty()) result.append(parent.getText(R.string.tethering_manage_wifi_supported_channels)
|
if (list.isNotEmpty()) result.append(parent.getText(R.string.tethering_manage_wifi_supported_channels)
|
||||||
.format(locale, list.joinToString("; ")))
|
.format(locale, list.joinToString("; ")))
|
||||||
|
capability.countryCode?.let {
|
||||||
|
result.append(parent.getText(R.string.tethering_manage_wifi_country_code).format(locale, it))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
} ?: numClients?.let { numClients ->
|
} ?: numClients?.let { numClients ->
|
||||||
app.resources.getQuantityText(R.plurals.tethering_manage_wifi_clients, numClients).format(locale,
|
app.resources.getQuantityText(R.plurals.tethering_manage_wifi_clients, numClients).format(locale,
|
||||||
numClients)
|
numClients)
|
||||||
}
|
}
|
||||||
override val text get() = parent.resources.configuration.locale.let { locale ->
|
override val text get() = parent.resources.configuration.locales[0].let { locale ->
|
||||||
listOfNotNull(failureReason?.let { WifiApManager.failureReasonLookup(it) }, baseError, info.run {
|
listOfNotNull(failureReason?.let { WifiApManager.failureReasonLookup(it) }, baseError, info.run {
|
||||||
if (isEmpty()) null else joinToSpanned("\n") @TargetApi(30) { parcel ->
|
if (isEmpty()) null else joinToSpanned("\n") @TargetApi(30) { parcel ->
|
||||||
val info = SoftApInfo(parcel)
|
val info = SoftApInfo(parcel)
|
||||||
val frequency = info.frequency
|
val frequency = info.frequency
|
||||||
val channel = SoftApConfigurationCompat.frequencyToChannel(frequency)
|
val channel = SoftApConfigurationCompat.frequencyToChannel(frequency)
|
||||||
val bandwidth = SoftApInfo.channelWidthLookup(info.bandwidth, true)
|
val bandwidth = SoftApInfo.channelWidthLookup(info.bandwidth, true)
|
||||||
if (BuildCompat.isAtLeastS()) {
|
if (Build.VERSION.SDK_INT >= 31) {
|
||||||
var bssid = makeMacSpan(info.bssid.toString())
|
val bssid = info.bssid.let { if (it == null) null else makeMacSpan(it.toString()) }
|
||||||
info.apInstanceIdentifier?.let { // take the fast route if possible
|
val bssidAp = info.apInstanceIdentifier?.let {
|
||||||
bssid = if (bssid is String) "$bssid%$it" else SpannableStringBuilder(bssid).append("%$it")
|
when (bssid) {
|
||||||
}
|
null -> it
|
||||||
|
is String -> "$bssid%$it" // take the fast route if possible
|
||||||
|
else -> SpannableStringBuilder(bssid).append("%$it")
|
||||||
|
}
|
||||||
|
} ?: bssid ?: "?"
|
||||||
val timeout = info.autoShutdownTimeoutMillis
|
val timeout = info.autoShutdownTimeoutMillis
|
||||||
parent.getText(if (timeout == 0L) {
|
parent.getText(if (timeout == 0L) {
|
||||||
R.string.tethering_manage_wifi_info_timeout_disabled
|
R.string.tethering_manage_wifi_info_timeout_disabled
|
||||||
} else R.string.tethering_manage_wifi_info_timeout_enabled).format(locale,
|
} else R.string.tethering_manage_wifi_info_timeout_enabled).format(locale,
|
||||||
frequency, channel, bandwidth, bssid, info.wifiStandard,
|
frequency, channel, bandwidth, bssidAp, info.wifiStandard,
|
||||||
// http://unicode.org/cldr/trac/ticket/3407
|
// http://unicode.org/cldr/trac/ticket/3407
|
||||||
DateUtils.formatElapsedTime(timeout / 1000))
|
DateUtils.formatElapsedTime(timeout / 1000))
|
||||||
} else parent.getText(R.string.tethering_manage_wifi_info).format(locale,
|
} else parent.getText(R.string.tethering_manage_wifi_info).format(locale,
|
||||||
@@ -296,7 +263,6 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIFI, true, this)
|
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIFI, true, this)
|
||||||
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI, this::onException)
|
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI, this::onException)
|
||||||
}
|
}
|
||||||
@RequiresApi(24)
|
|
||||||
class Usb(parent: TetheringFragment) : TetherManager(parent) {
|
class Usb(parent: TetheringFragment) : TetherManager(parent) {
|
||||||
override val title get() = parent.getString(R.string.tethering_manage_usb)
|
override val title get() = parent.getString(R.string.tethering_manage_usb)
|
||||||
override val tetherType get() = TetherType.USB
|
override val tetherType get() = TetherType.USB
|
||||||
@@ -305,39 +271,41 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_USB, true, this)
|
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_USB, true, this)
|
||||||
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_USB, this::onException)
|
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_USB, this::onException)
|
||||||
}
|
}
|
||||||
@RequiresApi(24)
|
class Bluetooth(parent: TetheringFragment, adapter: BluetoothAdapter) :
|
||||||
class Bluetooth(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver {
|
TetherManager(parent), DefaultLifecycleObserver {
|
||||||
private val tethering = BluetoothTethering(parent.requireContext()) { data.notifyChange() }
|
private val tethering = BluetoothTethering(parent.requireContext(), adapter) { data.notifyChange() }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
parent.viewLifecycleOwner.lifecycle.addObserver(this)
|
parent.viewLifecycleOwner.lifecycle.addObserver(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ensureInit(context: Context) = tethering.ensureInit(context)
|
fun ensureInit(context: Context) {
|
||||||
|
tethering.ensureInit(context)
|
||||||
|
onTetheringStarted() // force flush
|
||||||
|
}
|
||||||
override fun onResume(owner: LifecycleOwner) {
|
override fun onResume(owner: LifecycleOwner) {
|
||||||
if (!BuildCompat.isAtLeastS() || parent.requireContext().checkSelfPermission(
|
if (Build.VERSION.SDK_INT < 31) return
|
||||||
Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
|
if (parent.requireContext().checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) ==
|
||||||
ensureInit(parent.requireContext())
|
PackageManager.PERMISSION_GRANTED) {
|
||||||
} else if (parent.shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH_CONNECT)) {
|
tethering.ensureInit(parent.requireContext())
|
||||||
parent.requestBluetooth.launch(Manifest.permission.BLUETOOTH_CONNECT)
|
} else parent.requestBluetooth.launch(Manifest.permission.BLUETOOTH_CONNECT)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
override fun onDestroy(owner: LifecycleOwner) = tethering.close()
|
override fun onDestroy(owner: LifecycleOwner) = tethering.close()
|
||||||
|
|
||||||
override val title get() = parent.getString(R.string.tethering_manage_bluetooth)
|
override val title get() = parent.getString(R.string.tethering_manage_bluetooth)
|
||||||
override val tetherType get() = TetherType.BLUETOOTH
|
override val tetherType get() = TetherType.BLUETOOTH
|
||||||
override val type get() = VIEW_TYPE_BLUETOOTH
|
override val type get() = VIEW_TYPE_BLUETOOTH
|
||||||
override val isStarted get() = tethering.active == true
|
override val isStarted get() = tethering.active
|
||||||
override val text get() = listOfNotNull(
|
override val text get() = listOfNotNull(
|
||||||
if (tethering.active == null) tethering.activeFailureCause?.readableMessage else null,
|
if (tethering.active == null) tethering.activeFailureCause?.readableMessage else null,
|
||||||
baseError).joinToString("\n")
|
baseError).joinToString("\n")
|
||||||
|
|
||||||
override fun start() = BluetoothTethering.start(this)
|
override fun start() = tethering.start(this, parent.requireContext())
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH, this::onException)
|
tethering.stop(this::onException)
|
||||||
Thread.sleep(1) // give others a room to breathe
|
|
||||||
onTetheringStarted() // force flush state
|
onTetheringStarted() // force flush state
|
||||||
}
|
}
|
||||||
|
override fun onClickNull() = ManageBar.start(parent.requireContext())
|
||||||
}
|
}
|
||||||
@RequiresApi(30)
|
@RequiresApi(30)
|
||||||
class Ethernet(parent: TetheringFragment) : TetherManager(parent) {
|
class Ethernet(parent: TetheringFragment) : TetherManager(parent) {
|
||||||
@@ -348,41 +316,4 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_ETHERNET, true, this)
|
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_ETHERNET, true, this)
|
||||||
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_ETHERNET, this::onException)
|
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_ETHERNET, this::onException)
|
||||||
}
|
}
|
||||||
@RequiresApi(30)
|
|
||||||
class Ncm(parent: TetheringFragment) : TetherManager(parent) {
|
|
||||||
override val title get() = parent.getString(R.string.tethering_manage_ncm)
|
|
||||||
override val tetherType get() = TetherType.NCM
|
|
||||||
override val type get() = VIEW_TYPE_NCM
|
|
||||||
|
|
||||||
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_NCM, true, this)
|
|
||||||
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_NCM, this::onException)
|
|
||||||
}
|
|
||||||
@RequiresApi(30)
|
|
||||||
class WiGig(parent: TetheringFragment) : TetherManager(parent) {
|
|
||||||
override val title get() = parent.getString(R.string.tethering_manage_wigig)
|
|
||||||
override val tetherType get() = TetherType.WIGIG
|
|
||||||
override val type get() = VIEW_TYPE_WIGIG
|
|
||||||
|
|
||||||
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIGIG, true, this)
|
|
||||||
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIGIG, this::onException)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
@Deprecated("Not usable since API 26, malfunctioning on API 25")
|
|
||||||
class WifiLegacy(parent: TetheringFragment) : TetherManager(parent) {
|
|
||||||
override val title get() = parent.getString(R.string.tethering_manage_wifi_legacy)
|
|
||||||
override val tetherType get() = TetherType.WIFI
|
|
||||||
override val type get() = VIEW_TYPE_WIFI_LEGACY
|
|
||||||
|
|
||||||
override fun start() = try {
|
|
||||||
WifiApManager.start()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
onException(e)
|
|
||||||
}
|
|
||||||
override fun stop() = try {
|
|
||||||
WifiApManager.stop()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
onException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
@file:Suppress("DEPRECATION")
|
|
||||||
|
|
||||||
package be.mygod.vpnhotspot.manage
|
package be.mygod.vpnhotspot.manage
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
import android.annotation.TargetApi
|
||||||
|
import android.bluetooth.BluetoothManager
|
||||||
import android.content.*
|
import android.content.*
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -14,28 +13,35 @@ import android.view.ViewGroup
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.getSystemService
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.withStarted
|
||||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import be.mygod.vpnhotspot.*
|
import be.mygod.vpnhotspot.*
|
||||||
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.databinding.FragmentTetheringBinding
|
import be.mygod.vpnhotspot.databinding.FragmentTetheringBinding
|
||||||
import be.mygod.vpnhotspot.net.TetherType
|
import be.mygod.vpnhotspot.net.TetherType
|
||||||
import be.mygod.vpnhotspot.net.TetheringManager
|
import be.mygod.vpnhotspot.net.TetheringManager
|
||||||
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
|
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
|
||||||
import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces
|
import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces
|
||||||
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
|
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
|
||||||
|
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
|
||||||
|
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiApDialogFragment
|
import be.mygod.vpnhotspot.net.wifi.WifiApDialogFragment
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
||||||
import be.mygod.vpnhotspot.root.RootManager
|
import be.mygod.vpnhotspot.root.RootManager
|
||||||
import be.mygod.vpnhotspot.root.WifiApCommands
|
import be.mygod.vpnhotspot.root.WifiApCommands
|
||||||
import be.mygod.vpnhotspot.util.*
|
import be.mygod.vpnhotspot.util.*
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
import java.net.NetworkInterface
|
import java.net.NetworkInterface
|
||||||
@@ -45,28 +51,29 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
|||||||
inner class ManagerAdapter : ListAdapter<Manager, RecyclerView.ViewHolder>(Manager),
|
inner class ManagerAdapter : ListAdapter<Manager, RecyclerView.ViewHolder>(Manager),
|
||||||
TetheringManager.TetheringEventCallback {
|
TetheringManager.TetheringEventCallback {
|
||||||
internal val repeaterManager by lazy { RepeaterManager(this@TetheringFragment) }
|
internal val repeaterManager by lazy { RepeaterManager(this@TetheringFragment) }
|
||||||
@get:RequiresApi(26)
|
internal val localOnlyHotspotManager by lazy { LocalOnlyHotspotManager(this@TetheringFragment) }
|
||||||
internal val localOnlyHotspotManager by lazy @TargetApi(26) { LocalOnlyHotspotManager(this@TetheringFragment) }
|
internal val bluetoothManager by lazy {
|
||||||
@get:RequiresApi(24)
|
requireContext().getSystemService<BluetoothManager>()?.adapter?.let {
|
||||||
internal val bluetoothManager by lazy @TargetApi(24) { TetherManager.Bluetooth(this@TetheringFragment) }
|
TetherManager.Bluetooth(this@TetheringFragment, it)
|
||||||
@get:RequiresApi(24)
|
}
|
||||||
private val tetherManagers by lazy @TargetApi(24) {
|
}
|
||||||
listOf(TetherManager.Wifi(this@TetheringFragment),
|
private val tetherManagers by lazy {
|
||||||
TetherManager.Usb(this@TetheringFragment),
|
listOfNotNull(
|
||||||
bluetoothManager)
|
TetherManager.Wifi(this@TetheringFragment),
|
||||||
|
TetherManager.Usb(this@TetheringFragment),
|
||||||
|
bluetoothManager,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val tetherManagers30 by lazy @TargetApi(30) {
|
private val ethernetManager by lazy @TargetApi(30) { TetherManager.Ethernet(this@TetheringFragment) }
|
||||||
listOf(TetherManager.Ethernet(this@TetheringFragment),
|
|
||||||
TetherManager.Ncm(this@TetheringFragment),
|
|
||||||
TetherManager.WiGig(this@TetheringFragment))
|
|
||||||
}
|
|
||||||
private val wifiManagerLegacy by lazy { TetherManager.WifiLegacy(this@TetheringFragment) }
|
|
||||||
|
|
||||||
private var enabledIfaces = emptyList<String>()
|
var activeIfaces = emptyList<String>()
|
||||||
|
var localOnlyIfaces = emptyList<String>()
|
||||||
|
var erroredIfaces = emptyList<String>()
|
||||||
private var listDeferred = CompletableDeferred<List<Manager>>(emptyList())
|
private var listDeferred = CompletableDeferred<List<Manager>>(emptyList())
|
||||||
private fun updateEnabledTypes() {
|
fun updateEnabledTypes() {
|
||||||
this@TetheringFragment.enabledTypes = enabledIfaces.map { TetherType.ofInterface(it) }.toSet()
|
this@TetheringFragment.enabledTypes =
|
||||||
|
(activeIfaces + localOnlyIfaces).map { TetherType.ofInterface(it) }.toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
val lastErrors = mutableMapOf<String, Int>()
|
val lastErrors = mutableMapOf<String, Int>()
|
||||||
@@ -74,50 +81,42 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
|||||||
if (error == 0) lastErrors.remove(ifName) else lastErrors[ifName] = error
|
if (error == 0) lastErrors.remove(ifName) else lastErrors[ifName] = error
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun notifyInterfaceChanged(lastList: List<Manager>? = null) {
|
|
||||||
@Suppress("NAME_SHADOWING") val lastList = lastList ?: listDeferred.await()
|
|
||||||
val first = lastList.indexOfFirst { it is InterfaceManager }
|
|
||||||
if (first >= 0) notifyItemRangeChanged(first, lastList.indexOfLast { it is InterfaceManager } - first + 1)
|
|
||||||
}
|
|
||||||
suspend fun notifyTetherTypeChanged() {
|
suspend fun notifyTetherTypeChanged() {
|
||||||
updateEnabledTypes()
|
updateEnabledTypes()
|
||||||
val lastList = listDeferred.await()
|
val lastList = listDeferred.await()
|
||||||
notifyInterfaceChanged(lastList)
|
var first = lastList.indexOfFirst { it is InterfaceManager }
|
||||||
val first = lastList.indexOfLast { it !is TetherManager } + 1
|
withStarted {
|
||||||
notifyItemRangeChanged(first, lastList.size - first)
|
if (first >= 0) {
|
||||||
|
notifyItemRangeChanged(first, lastList.indexOfLast { it is InterfaceManager } - first + 1)
|
||||||
|
}
|
||||||
|
first = lastList.indexOfLast { it !is TetherManager } + 1
|
||||||
|
notifyItemRangeChanged(first, lastList.size - first)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(activeIfaces: List<String>, localOnlyIfaces: List<String>, erroredIfaces: List<String>) {
|
fun update() {
|
||||||
val deferred = CompletableDeferred<List<Manager>>()
|
val deferred = CompletableDeferred<List<Manager>>()
|
||||||
listDeferred = deferred
|
listDeferred = deferred
|
||||||
ifaceLookup = try {
|
ifaceLookup = try {
|
||||||
NetworkInterface.getNetworkInterfaces().asSequence().associateBy { it.name }
|
NetworkInterface.getNetworkInterfaces().asSequence().associateBy { it.name }
|
||||||
} catch (e: SocketException) {
|
} catch (e: Exception) {
|
||||||
Timber.d(e)
|
if (e is SocketException) Timber.d(e) else Timber.w(e)
|
||||||
emptyMap()
|
emptyMap()
|
||||||
}
|
}
|
||||||
enabledIfaces = activeIfaces + localOnlyIfaces
|
|
||||||
updateEnabledTypes()
|
|
||||||
|
|
||||||
val list = ArrayList<Manager>()
|
val list = ArrayList<Manager>()
|
||||||
if (Services.p2p != null) list.add(repeaterManager)
|
if (Services.p2p != null) list.add(repeaterManager)
|
||||||
if (Build.VERSION.SDK_INT >= 26) list.add(localOnlyHotspotManager)
|
list.add(localOnlyHotspotManager)
|
||||||
val monitoredIfaces = binder?.monitoredIfaces ?: emptyList()
|
val monitoredIfaces = binder?.monitoredIfaces ?: emptyList()
|
||||||
updateMonitorList(activeIfaces - monitoredIfaces)
|
updateMonitorList(activeIfaces - monitoredIfaces.toSet())
|
||||||
list.addAll((activeIfaces + monitoredIfaces).toSortedSet()
|
list.addAll((activeIfaces + monitoredIfaces).toSortedSet()
|
||||||
.map { InterfaceManager(this@TetheringFragment, it) })
|
.map { InterfaceManager(this@TetheringFragment, it) })
|
||||||
list.add(ManageBar)
|
list.add(ManageBar)
|
||||||
if (Build.VERSION.SDK_INT >= 24) {
|
list.addAll(tetherManagers)
|
||||||
list.addAll(tetherManagers)
|
tetherManagers.forEach { it.updateErrorMessage(erroredIfaces, lastErrors) }
|
||||||
tetherManagers.forEach { it.updateErrorMessage(erroredIfaces, lastErrors) }
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT >= 30) {
|
if (Build.VERSION.SDK_INT >= 30) {
|
||||||
list.addAll(tetherManagers30)
|
list.add(ethernetManager)
|
||||||
tetherManagers30.forEach { it.updateErrorMessage(erroredIfaces, lastErrors) }
|
ethernetManager.updateErrorMessage(erroredIfaces, lastErrors)
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT < 26) {
|
|
||||||
list.add(wifiManagerLegacy)
|
|
||||||
wifiManagerLegacy.onTetheringStarted()
|
|
||||||
}
|
}
|
||||||
submitList(list) { deferred.complete(list) }
|
submitList(list) { deferred.complete(list) }
|
||||||
}
|
}
|
||||||
@@ -130,15 +129,17 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
|||||||
|
|
||||||
@RequiresApi(29)
|
@RequiresApi(29)
|
||||||
val startRepeater = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
val startRepeater = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||||
if (granted) requireActivity().startForegroundService(Intent(activity, RepeaterService::class.java))
|
if (granted) app.startServiceWithLocation<RepeaterService>(requireContext()) else {
|
||||||
|
Snackbar.make((activity as MainActivity).binding.fragmentHolder,
|
||||||
|
R.string.repeater_missing_location_permissions, Snackbar.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@RequiresApi(26)
|
|
||||||
val startLocalOnlyHotspot = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
val startLocalOnlyHotspot = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||||
adapter.localOnlyHotspotManager.start(requireContext())
|
adapter.localOnlyHotspotManager.start(requireContext())
|
||||||
}
|
}
|
||||||
@RequiresApi(31)
|
@RequiresApi(31)
|
||||||
val requestBluetooth = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
val requestBluetooth = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||||
if (granted) adapter.bluetoothManager.ensureInit(requireContext())
|
if (granted) adapter.bluetoothManager!!.ensureInit(requireContext())
|
||||||
}
|
}
|
||||||
|
|
||||||
var ifaceLookup: Map<String, NetworkInterface> = emptyMap()
|
var ifaceLookup: Map<String, NetworkInterface> = emptyMap()
|
||||||
@@ -147,19 +148,22 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
|||||||
var binder: TetheringService.Binder? = null
|
var binder: TetheringService.Binder? = null
|
||||||
private val adapter = ManagerAdapter()
|
private val adapter = ManagerAdapter()
|
||||||
private val receiver = broadcastReceiver { _, intent ->
|
private val receiver = broadcastReceiver { _, intent ->
|
||||||
adapter.update(intent.tetheredIfaces ?: return@broadcastReceiver,
|
adapter.activeIfaces = intent.tetheredIfaces ?: return@broadcastReceiver
|
||||||
intent.localOnlyTetheredIfaces ?: return@broadcastReceiver,
|
adapter.localOnlyIfaces = intent.localOnlyTetheredIfaces ?: return@broadcastReceiver
|
||||||
intent.getStringArrayListExtra(TetheringManager.EXTRA_ERRORED_TETHER) ?: return@broadcastReceiver)
|
adapter.erroredIfaces = intent.getStringArrayListExtra(TetheringManager.EXTRA_ERRORED_TETHER)
|
||||||
|
?: return@broadcastReceiver
|
||||||
|
adapter.updateEnabledTypes()
|
||||||
|
adapter.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateMonitorList(canMonitor: List<String> = emptyList()) {
|
private fun updateMonitorList(canMonitor: List<String> = emptyList()) {
|
||||||
val activity = activity as? MainActivity
|
val activity = activity as? MainActivity
|
||||||
val item = activity?.binding?.toolbar?.menu?.findItem(R.id.monitor) ?: return // assuming no longer foreground
|
val item = activity?.binding?.toolbar?.menu?.findItem(R.id.monitor) ?: return // assuming no longer foreground
|
||||||
item.isNotGone = canMonitor.isNotEmpty()
|
item.isNotGone = canMonitor.isNotEmpty()
|
||||||
item.subMenu.apply {
|
item.subMenu!!.apply {
|
||||||
clear()
|
clear()
|
||||||
for (iface in canMonitor.sorted()) add(iface).setOnMenuItemClickListener {
|
for (iface in canMonitor.sorted()) add(iface).setOnMenuItemClickListener {
|
||||||
ContextCompat.startForegroundService(activity, Intent(activity, TetheringService::class.java)
|
activity.startForegroundService(Intent(activity, TetheringService::class.java)
|
||||||
.putExtra(TetheringService.EXTRA_ADD_INTERFACE_MONITOR, iface))
|
.putExtra(TetheringService.EXTRA_ADD_INTERFACE_MONITOR, iface))
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -169,10 +173,10 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
|||||||
private var apConfigurationRunning = false
|
private var apConfigurationRunning = false
|
||||||
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
||||||
return when (item?.itemId) {
|
return when (item?.itemId) {
|
||||||
R.id.configuration -> item.subMenu.run {
|
R.id.configuration -> item.subMenu!!.run {
|
||||||
findItem(R.id.configuration_repeater).isNotGone = Services.p2p != null
|
findItem(R.id.configuration_repeater).isNotGone = Services.p2p != null
|
||||||
findItem(R.id.configuration_temp_hotspot).isNotGone =
|
findItem(R.id.configuration_temp_hotspot).isNotGone =
|
||||||
adapter.localOnlyHotspotManager.binder?.configuration != null
|
adapter.localOnlyHotspotManager.binder?.configuration != null
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.configuration_repeater -> {
|
R.id.configuration_repeater -> {
|
||||||
@@ -189,28 +193,34 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
|||||||
}
|
}
|
||||||
R.id.configuration_ap -> if (apConfigurationRunning) false else {
|
R.id.configuration_ap -> if (apConfigurationRunning) false else {
|
||||||
apConfigurationRunning = true
|
apConfigurationRunning = true
|
||||||
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
try {
|
val configuration = try {
|
||||||
WifiApManager.configurationCompat
|
if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
|
||||||
|
WifiApManager.configurationLegacy?.toCompat() ?: SoftApConfigurationCompat()
|
||||||
|
} else WifiApManager.configuration.toCompat()
|
||||||
} catch (e: InvocationTargetException) {
|
} catch (e: InvocationTargetException) {
|
||||||
if (e.targetException !is SecurityException) Timber.w(e)
|
if (e.targetException !is SecurityException) Timber.w(e)
|
||||||
try {
|
try {
|
||||||
RootManager.use { it.execute(WifiApCommands.GetConfiguration()) }
|
if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
|
||||||
|
RootManager.use { it.execute(WifiApCommands.GetConfigurationLegacy()) }?.toCompat()
|
||||||
|
?: SoftApConfigurationCompat()
|
||||||
|
} else RootManager.use { it.execute(WifiApCommands.GetConfiguration()) }.toCompat()
|
||||||
} catch (_: CancellationException) {
|
} catch (_: CancellationException) {
|
||||||
null
|
return@launch
|
||||||
} catch (eRoot: Exception) {
|
} catch (eRoot: Exception) {
|
||||||
eRoot.addSuppressed(e)
|
eRoot.addSuppressed(e)
|
||||||
if (Build.VERSION.SDK_INT !in 26..29 || eRoot.getRootCause() !is SecurityException) {
|
if (Build.VERSION.SDK_INT >= 29 || eRoot.getRootCause() !is SecurityException) {
|
||||||
Timber.w(eRoot)
|
Timber.w(eRoot)
|
||||||
}
|
}
|
||||||
SmartSnackbar.make(eRoot).show()
|
SmartSnackbar.make(eRoot).show()
|
||||||
null
|
return@launch
|
||||||
}
|
}
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
Timber.w(e)
|
Timber.w(e)
|
||||||
SmartSnackbar.make(e).show()
|
SmartSnackbar.make(e).show()
|
||||||
null
|
return@launch
|
||||||
}?.let { configuration ->
|
}
|
||||||
|
withStarted {
|
||||||
WifiApDialogFragment().apply {
|
WifiApDialogFragment().apply {
|
||||||
arg(WifiApDialogFragment.Arg(configuration))
|
arg(WifiApDialogFragment.Arg(configuration))
|
||||||
key()
|
key()
|
||||||
@@ -226,10 +236,10 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
|||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
AlertDialogFragment.setResultListener<WifiApDialogFragment, WifiApDialogFragment.Arg>(this) { which, ret ->
|
AlertDialogFragment.setResultListener<WifiApDialogFragment, WifiApDialogFragment.Arg>(this) { which, ret ->
|
||||||
if (which == DialogInterface.BUTTON_POSITIVE) viewLifecycleOwner.lifecycleScope.launchWhenCreated {
|
if (which == DialogInterface.BUTTON_POSITIVE) GlobalScope.launch {
|
||||||
val configuration = ret!!.configuration
|
val configuration = ret!!.configuration
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
if (Build.VERSION.SDK_INT in 28 until 30 &&
|
if (Build.VERSION.SDK_INT < 30 &&
|
||||||
configuration.isAutoShutdownEnabled != TetherTimeoutMonitor.enabled) try {
|
configuration.isAutoShutdownEnabled != TetherTimeoutMonitor.enabled) try {
|
||||||
TetherTimeoutMonitor.setEnabled(configuration.isAutoShutdownEnabled)
|
TetherTimeoutMonitor.setEnabled(configuration.isAutoShutdownEnabled)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -237,10 +247,18 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
|||||||
SmartSnackbar.make(e).show()
|
SmartSnackbar.make(e).show()
|
||||||
}
|
}
|
||||||
val success = try {
|
val success = try {
|
||||||
WifiApManager.setConfigurationCompat(configuration)
|
if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
|
||||||
|
WifiApManager.setConfiguration(configuration.toWifiConfiguration())
|
||||||
|
} else WifiApManager.setConfiguration(configuration.toPlatform())
|
||||||
} catch (e: InvocationTargetException) {
|
} catch (e: InvocationTargetException) {
|
||||||
try {
|
try {
|
||||||
RootManager.use { it.execute(WifiApCommands.SetConfiguration(configuration)) }
|
if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
|
||||||
|
val wc = configuration.toWifiConfiguration()
|
||||||
|
RootManager.use { it.execute(WifiApCommands.SetConfigurationLegacy(wc)) }
|
||||||
|
} else {
|
||||||
|
val platform = configuration.toPlatform()
|
||||||
|
RootManager.use { it.execute(WifiApCommands.SetConfiguration(platform)) }
|
||||||
|
}
|
||||||
} catch (_: CancellationException) {
|
} catch (_: CancellationException) {
|
||||||
} catch (eRoot: Exception) {
|
} catch (eRoot: Exception) {
|
||||||
eRoot.addSuppressed(e)
|
eRoot.addSuppressed(e)
|
||||||
@@ -256,7 +274,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
|||||||
binding.interfaces.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
binding.interfaces.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
||||||
binding.interfaces.itemAnimator = DefaultItemAnimator()
|
binding.interfaces.itemAnimator = DefaultItemAnimator()
|
||||||
binding.interfaces.adapter = adapter
|
binding.interfaces.adapter = adapter
|
||||||
adapter.update(emptyList(), emptyList(), emptyList())
|
adapter.update()
|
||||||
ServiceForegroundConnector(this, this, TetheringService::class)
|
ServiceForegroundConnector(this, this, TetheringService::class)
|
||||||
(activity as MainActivity).binding.toolbar.apply {
|
(activity as MainActivity).binding.toolbar.apply {
|
||||||
inflateMenu(R.menu.toolbar_tethering)
|
inflateMenu(R.menu.toolbar_tethering)
|
||||||
@@ -275,18 +293,22 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
if (Build.VERSION.SDK_INT >= 27) ManageBar.Data.notifyChange()
|
ManageBar.Data.notifyChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
binder = service as TetheringService.Binder
|
binder = service as TetheringService.Binder
|
||||||
service.routingsChanged[this] = {
|
service.routingsChanged[this] = {
|
||||||
lifecycleScope.launchWhenStarted { adapter.notifyInterfaceChanged() }
|
lifecycleScope.launch {
|
||||||
|
withStarted { adapter.update() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
requireContext().registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
requireContext().registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
||||||
if (Build.VERSION.SDK_INT >= 30) {
|
if (Build.VERSION.SDK_INT >= 30) {
|
||||||
TetheringManager.registerTetheringEventCallback(null, adapter)
|
TetheringManager.registerTetheringEventCallback(null, adapter)
|
||||||
TetherType.listener[this] = { lifecycleScope.launchWhenStarted { adapter.notifyTetherTypeChanged() } }
|
TetherType.listener[this] = {
|
||||||
|
lifecycleScope.launch { adapter.notifyTetherTypeChanged() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package be.mygod.vpnhotspot.manage
|
package be.mygod.vpnhotspot.manage
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothManager
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -10,13 +11,12 @@ import android.os.IBinder
|
|||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.getSystemService
|
||||||
import be.mygod.vpnhotspot.R
|
import be.mygod.vpnhotspot.R
|
||||||
import be.mygod.vpnhotspot.TetheringService
|
import be.mygod.vpnhotspot.TetheringService
|
||||||
import be.mygod.vpnhotspot.net.TetherType
|
import be.mygod.vpnhotspot.net.TetherType
|
||||||
import be.mygod.vpnhotspot.net.TetheringManager
|
import be.mygod.vpnhotspot.net.TetheringManager
|
||||||
import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces
|
import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
|
||||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
import be.mygod.vpnhotspot.util.broadcastReceiver
|
||||||
import be.mygod.vpnhotspot.util.readableMessage
|
import be.mygod.vpnhotspot.util.readableMessage
|
||||||
import be.mygod.vpnhotspot.util.stopAndUnbind
|
import be.mygod.vpnhotspot.util.stopAndUnbind
|
||||||
@@ -25,7 +25,6 @@ import kotlinx.coroutines.GlobalScope
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@RequiresApi(24)
|
|
||||||
sealed class TetheringTileService : IpNeighbourMonitoringTileService(), TetheringManager.StartTetheringCallback {
|
sealed class TetheringTileService : IpNeighbourMonitoringTileService(), TetheringManager.StartTetheringCallback {
|
||||||
protected val tileOff by lazy { Icon.createWithResource(application, icon) }
|
protected val tileOff by lazy { Icon.createWithResource(application, icon) }
|
||||||
protected val tileOn by lazy { Icon.createWithResource(application, R.drawable.ic_quick_settings_tile_on) }
|
protected val tileOn by lazy { Icon.createWithResource(application, R.drawable.ic_quick_settings_tile_on) }
|
||||||
@@ -34,7 +33,7 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
|
|||||||
protected abstract val tetherType: TetherType
|
protected abstract val tetherType: TetherType
|
||||||
protected open val icon get() = tetherType.icon
|
protected open val icon get() = tetherType.icon
|
||||||
private var tethered: List<String>? = null
|
private var tethered: List<String>? = null
|
||||||
protected val interested get() = tethered?.filter { TetherType.ofInterface(it) == tetherType }
|
protected val interested get() = tethered?.filter { TetherType.ofInterface(it).isA(tetherType) }
|
||||||
protected var binder: TetheringService.Binder? = null
|
protected var binder: TetheringService.Binder? = null
|
||||||
|
|
||||||
private val receiver = broadcastReceiver { _, intent ->
|
private val receiver = broadcastReceiver { _, intent ->
|
||||||
@@ -108,7 +107,7 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
|
|||||||
stop()
|
stop()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
onException(e)
|
onException(e)
|
||||||
} else ContextCompat.startForegroundService(this, Intent(this, TetheringService::class.java)
|
} else startForegroundService(Intent(this, TetheringService::class.java)
|
||||||
.putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray()))
|
.putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,15 +150,16 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
|
|||||||
override val labelString get() = R.string.tethering_manage_bluetooth
|
override val labelString get() = R.string.tethering_manage_bluetooth
|
||||||
override val tetherType get() = TetherType.BLUETOOTH
|
override val tetherType get() = TetherType.BLUETOOTH
|
||||||
|
|
||||||
override fun start() = BluetoothTethering.start(this)
|
override fun start() = tethering!!.start(this, this)
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH, this::onException)
|
tethering!!.stop(this::onException)
|
||||||
Thread.sleep(1) // give others a room to breathe
|
|
||||||
onTetheringStarted() // force flush state
|
onTetheringStarted() // force flush state
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
tethering = BluetoothTethering(this) { updateTile() }
|
tethering = getSystemService<BluetoothManager>()?.adapter?.let {
|
||||||
|
BluetoothTethering(this, it) { updateTile() }
|
||||||
|
}
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
}
|
}
|
||||||
override fun onStopListening() {
|
override fun onStopListening() {
|
||||||
@@ -187,7 +187,7 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
|
|||||||
icon = tileOff
|
icon = tileOff
|
||||||
}
|
}
|
||||||
null -> {
|
null -> {
|
||||||
state = Tile.STATE_UNAVAILABLE
|
state = Tile.STATE_INACTIVE
|
||||||
icon = tileOff
|
icon = tileOff
|
||||||
subtitle(tethering?.activeFailureCause?.readableMessage)
|
subtitle(tethering?.activeFailureCause?.readableMessage)
|
||||||
}
|
}
|
||||||
@@ -198,7 +198,8 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
when (tethering?.active) {
|
val tethering = tethering
|
||||||
|
if (tethering == null) tapPending = true else when (tethering.active) {
|
||||||
true -> {
|
true -> {
|
||||||
val binder = binder
|
val binder = binder
|
||||||
if (binder == null) tapPending = true else {
|
if (binder == null) tapPending = true else {
|
||||||
@@ -207,12 +208,12 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
|
|||||||
stop()
|
stop()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
onException(e)
|
onException(e)
|
||||||
} else ContextCompat.startForegroundService(this, Intent(this, TetheringService::class.java)
|
} else startForegroundService(Intent(this, TetheringService::class.java)
|
||||||
.putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray()))
|
.putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false -> start()
|
false -> start()
|
||||||
else -> tapPending = true
|
else -> ManageBar.start(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,39 +225,4 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
|
|||||||
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_ETHERNET, true, this)
|
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_ETHERNET, true, this)
|
||||||
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_ETHERNET, this::onException)
|
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_ETHERNET, this::onException)
|
||||||
}
|
}
|
||||||
@RequiresApi(30)
|
|
||||||
class Ncm : TetheringTileService() {
|
|
||||||
override val labelString get() = R.string.tethering_manage_ncm
|
|
||||||
override val tetherType get() = TetherType.NCM
|
|
||||||
|
|
||||||
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_NCM, true, this)
|
|
||||||
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_NCM, this::onException)
|
|
||||||
}
|
|
||||||
@RequiresApi(30)
|
|
||||||
class WiGig : TetheringTileService() {
|
|
||||||
override val labelString get() = R.string.tethering_manage_wigig
|
|
||||||
override val tetherType get() = TetherType.WIGIG
|
|
||||||
|
|
||||||
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIGIG, true, this)
|
|
||||||
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIGIG, this::onException)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
@Deprecated("Not usable since API 25")
|
|
||||||
class WifiLegacy : TetheringTileService() {
|
|
||||||
override val labelString get() = R.string.tethering_manage_wifi_legacy
|
|
||||||
override val tetherType get() = TetherType.WIFI
|
|
||||||
override val icon get() = R.drawable.ic_device_wifi_tethering
|
|
||||||
|
|
||||||
override fun start() = try {
|
|
||||||
WifiApManager.start()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
onException(e)
|
|
||||||
}
|
|
||||||
override fun stop() = try {
|
|
||||||
WifiApManager.stop()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
onException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package be.mygod.vpnhotspot.net
|
package be.mygod.vpnhotspot.net
|
||||||
|
|
||||||
|
import android.net.MacAddress
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.system.ErrnoException
|
import android.system.ErrnoException
|
||||||
import android.system.Os
|
import android.system.Os
|
||||||
@@ -15,7 +16,7 @@ import java.io.IOException
|
|||||||
import java.net.Inet4Address
|
import java.net.Inet4Address
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
|
|
||||||
data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddressCompat, val state: State) {
|
data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddress, val state: State) {
|
||||||
enum class State {
|
enum class State {
|
||||||
INCOMPLETE, VALID, FAILED, DELETING
|
INCOMPLETE, VALID, FAILED, DELETING
|
||||||
}
|
}
|
||||||
@@ -27,8 +28,8 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddr
|
|||||||
* https://people.cs.clemson.edu/~westall/853/notes/arpstate.pdf
|
* https://people.cs.clemson.edu/~westall/853/notes/arpstate.pdf
|
||||||
* Assumptions: IP addr (key) always present and RTM_GETNEIGH is never used
|
* Assumptions: IP addr (key) always present and RTM_GETNEIGH is never used
|
||||||
*/
|
*/
|
||||||
private val parser = "^(Deleted )?([^ ]+) dev ([^ ]+) (lladdr ([^ ]*))?.*?( ([INCOMPLET,RAHBSDYF]+))?\$"
|
private val parser = ("^(Deleted )?(?:([^ ]+) )?dev ([^ ]+) (?:lladdr ([^ ]*))?.*?" +
|
||||||
.toRegex()
|
"(?: ([INCOMPLET,RAHBSDYF]+))?\$").toRegex()
|
||||||
/**
|
/**
|
||||||
* Fallback format will be used if if_indextoname returns null, which some stupid devices do.
|
* Fallback format will be used if if_indextoname returns null, which some stupid devices do.
|
||||||
*
|
*
|
||||||
@@ -49,14 +50,15 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddr
|
|||||||
suspend fun parse(line: String, fullMode: Boolean): List<IpNeighbour> {
|
suspend fun parse(line: String, fullMode: Boolean): List<IpNeighbour> {
|
||||||
return if (line.isBlank()) emptyList() else try {
|
return if (line.isBlank()) emptyList() else try {
|
||||||
val match = parser.matchEntire(line)!!
|
val match = parser.matchEntire(line)!!
|
||||||
val ip = parseNumericAddress(match.groupValues[2]) // by regex, ip is non-empty
|
if (match.groups[2] == null) return emptyList()
|
||||||
val devs = substituteDev(match.groupValues[3]) // by regex, dev is non-empty as well
|
val ip = parseNumericAddress(match.groupValues[2])
|
||||||
val state = if (match.groupValues[1].isNotEmpty()) State.DELETING else when (match.groupValues[7]) {
|
val devs = substituteDev(match.groupValues[3]) // by regex, dev is non-empty
|
||||||
|
val state = if (match.groupValues[1].isNotEmpty()) State.DELETING else when (match.groupValues[5]) {
|
||||||
"", "INCOMPLETE" -> State.INCOMPLETE
|
"", "INCOMPLETE" -> State.INCOMPLETE
|
||||||
"REACHABLE", "DELAY", "STALE", "PROBE", "PERMANENT" -> State.VALID
|
"REACHABLE", "DELAY", "STALE", "PROBE", "PERMANENT" -> State.VALID
|
||||||
"FAILED" -> State.FAILED
|
"FAILED" -> State.FAILED
|
||||||
"NOARP" -> return emptyList() // skip
|
"NOARP" -> return emptyList() // skip
|
||||||
else -> throw IllegalArgumentException("Unknown state encountered: ${match.groupValues[7]}")
|
else -> throw IllegalArgumentException("Unknown state encountered: ${match.groupValues[5]}")
|
||||||
}
|
}
|
||||||
var lladdr = MacAddressCompat.ALL_ZEROS_ADDRESS
|
var lladdr = MacAddressCompat.ALL_ZEROS_ADDRESS
|
||||||
if (!fullMode && state != State.VALID) {
|
if (!fullMode && state != State.VALID) {
|
||||||
@@ -64,7 +66,7 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddr
|
|||||||
return devs.map { IpNeighbour(ip, it, lladdr, State.DELETING) }
|
return devs.map { IpNeighbour(ip, it, lladdr, State.DELETING) }
|
||||||
}
|
}
|
||||||
if (match.groups[4] != null) try {
|
if (match.groups[4] != null) try {
|
||||||
lladdr = MacAddressCompat.fromString(match.groupValues[5])
|
lladdr = MacAddress.fromString(match.groupValues[4])
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
if (state != State.INCOMPLETE && state != State.DELETING) {
|
if (state != State.INCOMPLETE && state != State.DELETING) {
|
||||||
Timber.w(IOException("Failed to find MAC address for $line", e))
|
Timber.w(IOException("Failed to find MAC address for $line", e))
|
||||||
@@ -78,7 +80,7 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddr
|
|||||||
val list = arp()
|
val list = arp()
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.filter { parseNumericAddress(it[ARP_IP_ADDRESS]) == ip && it[ARP_DEVICE] in devs }
|
.filter { parseNumericAddress(it[ARP_IP_ADDRESS]) == ip && it[ARP_DEVICE] in devs }
|
||||||
.map { MacAddressCompat.fromString(it[ARP_HW_ADDRESS]) }
|
.map { MacAddress.fromString(it[ARP_HW_ADDRESS]) }
|
||||||
.filter { it != MacAddressCompat.ALL_ZEROS_ADDRESS }
|
.filter { it != MacAddressCompat.ALL_ZEROS_ADDRESS }
|
||||||
.distinct()
|
.distinct()
|
||||||
.toList()
|
.toList()
|
||||||
@@ -137,5 +139,4 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddr
|
|||||||
data class IpDev(val ip: InetAddress, val dev: String) {
|
data class IpDev(val ip: InetAddress, val dev: String) {
|
||||||
override fun toString() = "$ip%$dev"
|
override fun toString() = "$ip%$dev"
|
||||||
}
|
}
|
||||||
@Suppress("FunctionName")
|
|
||||||
fun IpDev(neighbour: IpNeighbour) = IpDev(neighbour.ip, neighbour.dev)
|
fun IpDev(neighbour: IpNeighbour) = IpDev(neighbour.ip, neighbour.dev)
|
||||||
|
|||||||
@@ -1,97 +1,34 @@
|
|||||||
package be.mygod.vpnhotspot.net
|
package be.mygod.vpnhotspot.net
|
||||||
|
|
||||||
import android.net.MacAddress
|
import android.net.MacAddress
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compat support class for [MacAddress].
|
* This used to be a compat support class for [MacAddress].
|
||||||
|
* Now it is just a convenient class for backwards compatibility.
|
||||||
*/
|
*/
|
||||||
@JvmInline
|
@JvmInline
|
||||||
value class MacAddressCompat(val addr: Long) {
|
value class MacAddressCompat(val addr: Long) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val ETHER_ADDR_LEN = 6
|
|
||||||
/**
|
/**
|
||||||
* The MacAddress zero MAC address.
|
* The MacAddress zero MAC address.
|
||||||
*
|
*
|
||||||
* Not publicly exposed or treated specially since the OUI 00:00:00 is registered.
|
* Not publicly exposed or treated specially since the OUI 00:00:00 is registered.
|
||||||
* @hide
|
|
||||||
*/
|
*/
|
||||||
val ALL_ZEROS_ADDRESS = MacAddressCompat(0)
|
val ALL_ZEROS_ADDRESS = MacAddress.fromBytes(byteArrayOf(0, 0, 0, 0, 0, 0))
|
||||||
val ANY_ADDRESS = MacAddressCompat(2)
|
val ANY_ADDRESS = MacAddress.fromBytes(byteArrayOf(2, 0, 0, 0, 0, 0))
|
||||||
|
|
||||||
/**
|
fun MacAddress.toLong() = ByteBuffer.allocate(Long.SIZE_BYTES).apply {
|
||||||
* Creates a MacAddress from the given byte array representation.
|
|
||||||
* A valid byte array representation for a MacAddress is a non-null array of length 6.
|
|
||||||
*
|
|
||||||
* @param addr a byte array representation of a MAC address.
|
|
||||||
* @return the MacAddress corresponding to the given byte array representation.
|
|
||||||
* @throws IllegalArgumentException if the given byte array is not a valid representation.
|
|
||||||
*/
|
|
||||||
fun fromBytes(addr: ByteArray) = ByteBuffer.allocate(Long.SIZE_BYTES).run {
|
|
||||||
order(ByteOrder.LITTLE_ENDIAN)
|
order(ByteOrder.LITTLE_ENDIAN)
|
||||||
put(when (addr.size) {
|
put(toByteArray())
|
||||||
ETHER_ADDR_LEN -> addr
|
|
||||||
8 -> {
|
|
||||||
require(addr.take(2).all { it == 0.toByte() }) {
|
|
||||||
"Unrecognized padding " + addr.joinToString(":") { "%02x".format(it) }
|
|
||||||
}
|
|
||||||
addr.drop(2).toByteArray()
|
|
||||||
}
|
|
||||||
else -> throw IllegalArgumentException(addr.joinToString(":") { "%02x".format(it) } +
|
|
||||||
" was not a valid MAC address")
|
|
||||||
})
|
|
||||||
rewind()
|
rewind()
|
||||||
MacAddressCompat(long)
|
}.long
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Creates a MacAddress from the given String representation. A valid String representation
|
|
||||||
* for a MacAddress is a series of 6 values in the range [0,ff] printed in hexadecimal
|
|
||||||
* and joined by ':' characters.
|
|
||||||
*
|
|
||||||
* @param addr a String representation of a MAC address.
|
|
||||||
* @return the MacAddress corresponding to the given String representation.
|
|
||||||
* @throws IllegalArgumentException if the given String is not a valid representation.
|
|
||||||
*/
|
|
||||||
fun fromString(addr: String) = ByteBuffer.allocate(Long.SIZE_BYTES).run {
|
|
||||||
order(ByteOrder.LITTLE_ENDIAN)
|
|
||||||
var start = 0
|
|
||||||
var i = 0
|
|
||||||
while (position() < ETHER_ADDR_LEN && start < addr.length) {
|
|
||||||
val end = i
|
|
||||||
if (addr.getOrElse(i) { ':' } == ':') ++i else if (i < start + 2) {
|
|
||||||
++i
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
put(if (start == end) 0 else try {
|
|
||||||
Integer.parseInt(addr.substring(start, end), 16).toByte()
|
|
||||||
} catch (e: NumberFormatException) {
|
|
||||||
throw IllegalArgumentException(e)
|
|
||||||
})
|
|
||||||
start = i
|
|
||||||
}
|
|
||||||
require(position() == ETHER_ADDR_LEN) { "MAC address too short" }
|
|
||||||
rewind()
|
|
||||||
MacAddressCompat(long)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(28)
|
|
||||||
fun MacAddress.toCompat() = fromBytes(toByteArray())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validate() = require(addr and ((1L shl 48) - 1).inv() == 0L)
|
fun toPlatform() = MacAddress.fromBytes(ByteBuffer.allocate(8).run {
|
||||||
|
|
||||||
fun toList() = ByteBuffer.allocate(8).run {
|
|
||||||
order(ByteOrder.LITTLE_ENDIAN)
|
order(ByteOrder.LITTLE_ENDIAN)
|
||||||
putLong(addr)
|
putLong(addr)
|
||||||
array().take(6)
|
array().take(6)
|
||||||
}
|
}.toByteArray())
|
||||||
|
|
||||||
@RequiresApi(28)
|
|
||||||
fun toPlatform() = MacAddress.fromBytes(toList().toByteArray())
|
|
||||||
|
|
||||||
override fun toString() = toList().joinToString(":") { "%02x".format(it) }
|
|
||||||
|
|
||||||
fun toOui() = toList().joinToString("") { "%02x".format(it) }.substring(0, 9)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package be.mygod.vpnhotspot.net
|
package be.mygod.vpnhotspot.net
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.annotation.TargetApi
|
|
||||||
import android.net.LinkProperties
|
import android.net.LinkProperties
|
||||||
|
import android.net.MacAddress
|
||||||
import android.net.RouteInfo
|
import android.net.RouteInfo
|
||||||
import android.os.Build
|
import android.system.Os
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.R
|
import be.mygod.vpnhotspot.R
|
||||||
import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor
|
import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor
|
||||||
@@ -15,7 +13,9 @@ import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
|
|||||||
import be.mygod.vpnhotspot.room.AppDatabase
|
import be.mygod.vpnhotspot.room.AppDatabase
|
||||||
import be.mygod.vpnhotspot.root.RootManager
|
import be.mygod.vpnhotspot.root.RootManager
|
||||||
import be.mygod.vpnhotspot.root.RoutingCommands
|
import be.mygod.vpnhotspot.root.RoutingCommands
|
||||||
import be.mygod.vpnhotspot.util.*
|
import be.mygod.vpnhotspot.util.RootSession
|
||||||
|
import be.mygod.vpnhotspot.util.allInterfaceNames
|
||||||
|
import be.mygod.vpnhotspot.util.allRoutes
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@@ -125,7 +125,6 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
|
|||||||
*
|
*
|
||||||
* Source: https://android.googlesource.com/platform/system/netd/+/3b47c793ff7ade843b1d85a9be8461c3b4dc693e
|
* Source: https://android.googlesource.com/platform/system/netd/+/3b47c793ff7ade843b1d85a9be8461c3b4dc693e
|
||||||
*/
|
*/
|
||||||
@RequiresApi(28)
|
|
||||||
Netd,
|
Netd,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,35 +150,24 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
|
|||||||
private val upstreams = HashSet<String>()
|
private val upstreams = HashSet<String>()
|
||||||
private class InterfaceGoneException(upstream: String) : IOException("Interface $upstream not found")
|
private class InterfaceGoneException(upstream: String) : IOException("Interface $upstream not found")
|
||||||
private open inner class Upstream(val priority: Int) : UpstreamMonitor.Callback {
|
private open inner class Upstream(val priority: Int) : UpstreamMonitor.Callback {
|
||||||
/**
|
|
||||||
* The only case when upstream is null is on API 23- and we are using system default rules.
|
|
||||||
*/
|
|
||||||
inner class Subrouting(priority: Int, val upstream: String) {
|
inner class Subrouting(priority: Int, val upstream: String) {
|
||||||
val ifindex = if (upstream.isEmpty()) 0 else if_nametoindex(upstream).also {
|
val ifindex = Os.if_nametoindex(upstream).also {
|
||||||
if (it <= 0) throw InterfaceGoneException(upstream)
|
if (it <= 0) throw InterfaceGoneException(upstream)
|
||||||
}
|
}
|
||||||
val transaction = RootSession.beginTransaction().safeguard {
|
val transaction = RootSession.beginTransaction().safeguard {
|
||||||
if (upstream.isEmpty()) {
|
ipRuleLookup(ifindex, priority)
|
||||||
ipRule("goto $RULE_PRIORITY_TETHERING", priority) // skip unreachable rule
|
when (masqueradeMode) {
|
||||||
} else ipRuleLookup(ifindex, priority)
|
|
||||||
@TargetApi(28) when (masqueradeMode) {
|
|
||||||
MasqueradeMode.None -> { } // nothing to be done here
|
MasqueradeMode.None -> { } // nothing to be done here
|
||||||
MasqueradeMode.Simple -> {
|
// note: specifying -i wouldn't work for POSTROUTING
|
||||||
// note: specifying -i wouldn't work for POSTROUTING
|
MasqueradeMode.Simple -> iptablesAdd(
|
||||||
iptablesAdd(if (upstream.isEmpty()) {
|
"vpnhotspot_masquerade -s $hostSubnet -o $upstream -j MASQUERADE", "nat")
|
||||||
"vpnhotspot_masquerade -s $hostSubnet -j MASQUERADE"
|
/**
|
||||||
} else "vpnhotspot_masquerade -s $hostSubnet -o $upstream -j MASQUERADE", "nat")
|
* 0 means that there are no interface addresses coming after, which is unused anyway.
|
||||||
}
|
*
|
||||||
MasqueradeMode.Netd -> {
|
* https://android.googlesource.com/platform/frameworks/base/+/android-5.0.0_r1/services/core/java/com/android/server/NetworkManagementService.java#1251
|
||||||
check(upstream.isNotEmpty()) // fallback is only needed for repeater on API 23 < 28
|
* https://android.googlesource.com/platform/system/netd/+/android-5.0.0_r1/server/CommandListener.cpp#638
|
||||||
/**
|
*/
|
||||||
* 0 means that there are no interface addresses coming after, which is unused anyway.
|
MasqueradeMode.Netd -> ndc("Nat", "ndc nat enable $downstream $upstream 0")
|
||||||
*
|
|
||||||
* https://android.googlesource.com/platform/frameworks/base/+/android-5.0.0_r1/services/core/java/com/android/server/NetworkManagementService.java#1251
|
|
||||||
* https://android.googlesource.com/platform/system/netd/+/android-5.0.0_r1/server/CommandListener.cpp#638
|
|
||||||
*/
|
|
||||||
ndc("Nat", "ndc nat enable $downstream $upstream 0")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,16 +213,10 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
|
|||||||
updateDnsRoute()
|
updateDnsRoute()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private val fallbackUpstream = object : Upstream(RULE_PRIORITY_UPSTREAM_FALLBACK) {
|
private val fallbackUpstream = Upstream(RULE_PRIORITY_UPSTREAM_FALLBACK)
|
||||||
@SuppressLint("NewApi")
|
|
||||||
override fun onFallback() = onAvailable(LinkProperties().apply {
|
|
||||||
interfaceName = ""
|
|
||||||
setDnsServers(listOf(parseNumericAddress("8.8.8.8")))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
private val upstream = Upstream(RULE_PRIORITY_UPSTREAM)
|
private val upstream = Upstream(RULE_PRIORITY_UPSTREAM)
|
||||||
|
|
||||||
private inner class Client(private val ip: Inet4Address, mac: MacAddressCompat) : AutoCloseable {
|
private inner class Client(private val ip: Inet4Address, mac: MacAddress) : AutoCloseable {
|
||||||
private val transaction = RootSession.beginTransaction().safeguard {
|
private val transaction = RootSession.beginTransaction().safeguard {
|
||||||
val address = ip.hostAddress
|
val address = ip.hostAddress
|
||||||
iptablesInsert("vpnhotspot_acl -i $downstream -s $address -j ACCEPT")
|
iptablesInsert("vpnhotspot_acl -i $downstream -s $address -j ACCEPT")
|
||||||
@@ -287,9 +269,9 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
|
|||||||
* but may be broken when system tethering shutdown before local-only interfaces.
|
* but may be broken when system tethering shutdown before local-only interfaces.
|
||||||
*/
|
*/
|
||||||
fun ipForward() {
|
fun ipForward() {
|
||||||
if (Build.VERSION.SDK_INT >= 23) try {
|
try {
|
||||||
transaction.ndc("ipfwd", "ndc ipfwd enable vpnhotspot_$downstream",
|
transaction.ndc("ipfwd", "ndc ipfwd enable vpnhotspot_$downstream",
|
||||||
"ndc ipfwd disable vpnhotspot_$downstream")
|
"ndc ipfwd disable vpnhotspot_$downstream")
|
||||||
return
|
return
|
||||||
} catch (e: RoutingCommands.UnexpectedOutputException) {
|
} catch (e: RoutingCommands.UnexpectedOutputException) {
|
||||||
Timber.w(IOException("ndc ipfwd enable failure", e))
|
Timber.w(IOException("ndc ipfwd enable failure", e))
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
package be.mygod.vpnhotspot.net
|
package be.mygod.vpnhotspot.net
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.root.SettingsGlobalPut
|
import be.mygod.vpnhotspot.root.SettingsGlobalPut
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It's hard to change tethering rules with Tethering hardware acceleration enabled for now.
|
* It's hard to change tethering rules with Tethering hardware acceleration enabled for now.
|
||||||
@@ -15,19 +13,6 @@ import timber.log.Timber
|
|||||||
* https://android.googlesource.com/platform/hardware/qcom/data/ipacfg-mgr/+/master/msm8998/ipacm/src/IPACM_OffloadManager.cpp
|
* https://android.googlesource.com/platform/hardware/qcom/data/ipacfg-mgr/+/master/msm8998/ipacm/src/IPACM_OffloadManager.cpp
|
||||||
*/
|
*/
|
||||||
object TetherOffloadManager {
|
object TetherOffloadManager {
|
||||||
val supported by lazy {
|
|
||||||
Build.VERSION.SDK_INT >= 27 || try {
|
|
||||||
Settings.Global::class.java.getDeclaredField("TETHER_OFFLOAD_DISABLED").get(null).let {
|
|
||||||
require(it == TETHER_OFFLOAD_DISABLED) { "Unknown field $it" }
|
|
||||||
}
|
|
||||||
true
|
|
||||||
} catch (_: NoSuchFieldException) {
|
|
||||||
false
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.w(e)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private const val TETHER_OFFLOAD_DISABLED = "tether_offload_disabled"
|
private const val TETHER_OFFLOAD_DISABLED = "tether_offload_disabled"
|
||||||
val enabled get() = Settings.Global.getInt(app.contentResolver, TETHER_OFFLOAD_DISABLED, 0) == 0
|
val enabled get() = Settings.Global.getInt(app.contentResolver, TETHER_OFFLOAD_DISABLED, 0) == 0
|
||||||
suspend fun setEnabled(value: Boolean) = SettingsGlobalPut.int(TETHER_OFFLOAD_DISABLED, if (value) 0 else 1)
|
suspend fun setEnabled(value: Boolean) = SettingsGlobalPut.int(TETHER_OFFLOAD_DISABLED, if (value) 0 else 1)
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ enum class TetherType(@DrawableRes val icon: Int) {
|
|||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isA(other: TetherType) = this == other || other == USB && this == NCM
|
||||||
|
|
||||||
companion object : TetheringManager.TetheringEventCallback {
|
companion object : TetheringManager.TetheringEventCallback {
|
||||||
private lateinit var usbRegexs: List<Pattern>
|
private lateinit var usbRegexs: List<Pattern>
|
||||||
private lateinit var wifiRegexs: List<Pattern>
|
private lateinit var wifiRegexs: List<Pattern>
|
||||||
@@ -58,6 +60,9 @@ enum class TetherType(@DrawableRes val icon: Int) {
|
|||||||
private fun updateRegexs() = synchronized(this) {
|
private fun updateRegexs() = synchronized(this) {
|
||||||
if (!requiresUpdate) return@synchronized
|
if (!requiresUpdate) return@synchronized
|
||||||
requiresUpdate = false
|
requiresUpdate = false
|
||||||
|
usbRegexs = emptyList()
|
||||||
|
wifiRegexs = emptyList()
|
||||||
|
bluetoothRegexs = emptyList()
|
||||||
TetheringManager.registerTetheringEventCallback(null, this)
|
TetheringManager.registerTetheringEventCallback(null, this)
|
||||||
val info = TetheringManager.resolvedService.serviceInfo
|
val info = TetheringManager.resolvedService.serviceInfo
|
||||||
val tethering = "com.android.networkstack.tethering" to
|
val tethering = "com.android.networkstack.tethering" to
|
||||||
@@ -71,9 +76,9 @@ enum class TetherType(@DrawableRes val icon: Int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(30)
|
@RequiresApi(30)
|
||||||
override fun onTetherableInterfaceRegexpsChanged(args: Array<out Any?>?) = synchronized(this) {
|
override fun onTetherableInterfaceRegexpsChanged(reg: Any?) = synchronized(this) {
|
||||||
if (requiresUpdate) return@synchronized
|
if (requiresUpdate) return@synchronized
|
||||||
Timber.i("onTetherableInterfaceRegexpsChanged: ${args?.contentDeepToString()}")
|
Timber.i("onTetherableInterfaceRegexpsChanged: $reg")
|
||||||
TetheringManager.unregisterTetheringEventCallback(this)
|
TetheringManager.unregisterTetheringEventCallback(this)
|
||||||
requiresUpdate = true
|
requiresUpdate = true
|
||||||
listener()
|
listener()
|
||||||
@@ -104,13 +109,20 @@ enum class TetherType(@DrawableRes val icon: Int) {
|
|||||||
*
|
*
|
||||||
* Based on: https://android.googlesource.com/platform/frameworks/base/+/5d36f01/packages/Tethering/src/com/android/networkstack/tethering/Tethering.java#479
|
* Based on: https://android.googlesource.com/platform/frameworks/base/+/5d36f01/packages/Tethering/src/com/android/networkstack/tethering/Tethering.java#479
|
||||||
*/
|
*/
|
||||||
fun ofInterface(iface: String?, p2pDev: String? = null) = synchronized(this) { ofInterfaceImpl(iface, p2pDev) }
|
fun ofInterface(iface: String?, p2pDev: String? = null) = when (iface) {
|
||||||
private tailrec fun ofInterfaceImpl(iface: String?, p2pDev: String?): TetherType = when {
|
null -> NONE
|
||||||
iface == null -> NONE
|
p2pDev -> WIFI_P2P
|
||||||
iface == p2pDev -> WIFI_P2P
|
else -> try {
|
||||||
|
synchronized(this) { ofInterfaceImpl(iface) }
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
Timber.w(e)
|
||||||
|
NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private tailrec fun ofInterfaceImpl(iface: String): TetherType = when {
|
||||||
requiresUpdate -> {
|
requiresUpdate -> {
|
||||||
if (Build.VERSION.SDK_INT >= 30) updateRegexs() else error("unexpected requiresUpdate")
|
if (Build.VERSION.SDK_INT >= 30) updateRegexs() else error("unexpected requiresUpdate")
|
||||||
ofInterfaceImpl(iface, p2pDev)
|
ofInterfaceImpl(iface)
|
||||||
}
|
}
|
||||||
wifiRegexs.any { it.matcher(iface).matches() } -> WIFI
|
wifiRegexs.any { it.matcher(iface).matches() } -> WIFI
|
||||||
wigigRegexs.any { it.matcher(iface).matches() } -> WIGIG
|
wigigRegexs.any { it.matcher(iface).matches() } -> WIGIG
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import android.content.pm.PackageManager
|
|||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.Network
|
import android.net.Network
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.DeadObjectException
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.os.ExecutorCompat
|
import androidx.core.os.ExecutorCompat
|
||||||
@@ -66,7 +67,11 @@ object TetheringManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private object InPlaceExecutor : Executor {
|
private object InPlaceExecutor : Executor {
|
||||||
override fun execute(command: Runnable) = command.run()
|
override fun execute(command: Runnable) = try {
|
||||||
|
command.run()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.w(e) // prevent Binder stub swallowing the exception
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,9 +94,7 @@ object TetheringManager {
|
|||||||
* https://android.googlesource.com/platform/frameworks/base.git/+/2a091d7aa0c174986387e5d56bf97a87fe075bdb%5E%21/services/java/com/android/server/connectivity/Tethering.java
|
* https://android.googlesource.com/platform/frameworks/base.git/+/2a091d7aa0c174986387e5d56bf97a87fe075bdb%5E%21/services/java/com/android/server/connectivity/Tethering.java
|
||||||
*/
|
*/
|
||||||
const val ACTION_TETHER_STATE_CHANGED = "android.net.conn.TETHER_STATE_CHANGED"
|
const val ACTION_TETHER_STATE_CHANGED = "android.net.conn.TETHER_STATE_CHANGED"
|
||||||
@RequiresApi(26)
|
|
||||||
private const val EXTRA_ACTIVE_LOCAL_ONLY_LEGACY = "localOnlyArray"
|
private const val EXTRA_ACTIVE_LOCAL_ONLY_LEGACY = "localOnlyArray"
|
||||||
private const val EXTRA_ACTIVE_TETHER_LEGACY = "activeArray"
|
|
||||||
/**
|
/**
|
||||||
* gives a String[] listing all the interfaces currently in local-only
|
* gives a String[] listing all the interfaces currently in local-only
|
||||||
* mode (ie, has DHCPv4+IPv6-ULA support and no packet forwarding)
|
* mode (ie, has DHCPv4+IPv6-ULA support and no packet forwarding)
|
||||||
@@ -102,7 +105,6 @@ object TetheringManager {
|
|||||||
* gives a String[] listing all the interfaces currently tethered
|
* gives a String[] listing all the interfaces currently tethered
|
||||||
* (ie, has DHCPv4 support and packets potentially forwarded/NATed)
|
* (ie, has DHCPv4 support and packets potentially forwarded/NATed)
|
||||||
*/
|
*/
|
||||||
@RequiresApi(26)
|
|
||||||
private const val EXTRA_ACTIVE_TETHER = "tetherArray"
|
private const val EXTRA_ACTIVE_TETHER = "tetherArray"
|
||||||
/**
|
/**
|
||||||
* gives a String[] listing all the interfaces we tried to tether and
|
* gives a String[] listing all the interfaces we tried to tether and
|
||||||
@@ -126,7 +128,6 @@ object TetheringManager {
|
|||||||
* Wifi tethering type.
|
* Wifi tethering type.
|
||||||
* @see [startTethering].
|
* @see [startTethering].
|
||||||
*/
|
*/
|
||||||
@RequiresApi(24)
|
|
||||||
const val TETHERING_WIFI = 0
|
const val TETHERING_WIFI = 0
|
||||||
/**
|
/**
|
||||||
* USB tethering type.
|
* USB tethering type.
|
||||||
@@ -134,48 +135,33 @@ object TetheringManager {
|
|||||||
* Requires MANAGE_USB permission, unfortunately.
|
* Requires MANAGE_USB permission, unfortunately.
|
||||||
*
|
*
|
||||||
* Source: https://android.googlesource.com/platform/frameworks/base/+/7ca5d3a/services/usb/java/com/android/server/usb/UsbService.java#389
|
* Source: https://android.googlesource.com/platform/frameworks/base/+/7ca5d3a/services/usb/java/com/android/server/usb/UsbService.java#389
|
||||||
* @see [startTethering].
|
* @see startTethering
|
||||||
*/
|
*/
|
||||||
@RequiresApi(24)
|
|
||||||
const val TETHERING_USB = 1
|
const val TETHERING_USB = 1
|
||||||
/**
|
/**
|
||||||
* Bluetooth tethering type.
|
* Bluetooth tethering type.
|
||||||
*
|
*
|
||||||
* Requires BLUETOOTH permission.
|
* Requires BLUETOOTH permission.
|
||||||
* @see [startTethering].
|
* @see startTethering
|
||||||
*/
|
*/
|
||||||
@RequiresApi(24)
|
|
||||||
const val TETHERING_BLUETOOTH = 2
|
const val TETHERING_BLUETOOTH = 2
|
||||||
/**
|
|
||||||
* Ncm local tethering type.
|
|
||||||
*
|
|
||||||
* @see [startTethering]
|
|
||||||
*/
|
|
||||||
@RequiresApi(30)
|
|
||||||
const val TETHERING_NCM = 4
|
|
||||||
/**
|
/**
|
||||||
* Ethernet tethering type.
|
* Ethernet tethering type.
|
||||||
*
|
*
|
||||||
* Requires MANAGE_USB permission, also.
|
* Requires MANAGE_USB permission, also.
|
||||||
* @see [startTethering]
|
* @see startTethering
|
||||||
*/
|
*/
|
||||||
@RequiresApi(30)
|
@RequiresApi(30)
|
||||||
const val TETHERING_ETHERNET = 5
|
const val TETHERING_ETHERNET = 5
|
||||||
/**
|
@RequiresApi(31) // TETHERING_WIFI_P2P
|
||||||
* WIGIG tethering type. Use a separate type to prevent
|
private val expectedTypes = setOf(TETHERING_WIFI, TETHERING_USB, TETHERING_BLUETOOTH, 3, TETHERING_ETHERNET)
|
||||||
* conflicts with TETHERING_WIFI
|
|
||||||
* This type is only used internally by the tethering module
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@RequiresApi(30)
|
|
||||||
const val TETHERING_WIGIG = 6
|
|
||||||
|
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val clazz by lazy { Class.forName("android.net.TetheringManager") }
|
private val clazz by lazy { Class.forName("android.net.TetheringManager") }
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val instance by lazy @TargetApi(30) {
|
private val instance by lazy @TargetApi(30) {
|
||||||
@SuppressLint("WrongConstant") // hidden services are not included in constants as of R preview 4
|
@SuppressLint("WrongConstant") // hidden services are not included in constants as of R preview 4
|
||||||
val service = Services.context.getSystemService(TETHERING_SERVICE)
|
val service = Services.context.getSystemService(TETHERING_SERVICE)!!
|
||||||
service
|
service
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,20 +174,17 @@ object TetheringManager {
|
|||||||
}
|
}
|
||||||
}.first()
|
}.first()
|
||||||
|
|
||||||
@get:RequiresApi(24)
|
|
||||||
private val classOnStartTetheringCallback by lazy {
|
private val classOnStartTetheringCallback by lazy {
|
||||||
Class.forName("android.net.ConnectivityManager\$OnStartTetheringCallback")
|
Class.forName("android.net.ConnectivityManager\$OnStartTetheringCallback")
|
||||||
}
|
}
|
||||||
@get:RequiresApi(24)
|
|
||||||
private val startTetheringLegacy by lazy {
|
private val startTetheringLegacy by lazy {
|
||||||
ConnectivityManager::class.java.getDeclaredMethod("startTethering",
|
ConnectivityManager::class.java.getDeclaredMethod("startTethering",
|
||||||
Int::class.java, Boolean::class.java, classOnStartTetheringCallback, Handler::class.java)
|
Int::class.java, Boolean::class.java, classOnStartTetheringCallback, Handler::class.java)
|
||||||
}
|
}
|
||||||
@get:RequiresApi(24)
|
|
||||||
private val stopTetheringLegacy by lazy {
|
private val stopTetheringLegacy by lazy {
|
||||||
ConnectivityManager::class.java.getDeclaredMethod("stopTethering", Int::class.java)
|
ConnectivityManager::class.java.getDeclaredMethod("stopTethering", Int::class.java)
|
||||||
}
|
}
|
||||||
private val getLastTetherError by lazy {
|
private val getLastTetherError by lazy @SuppressLint("SoonBlockedPrivateApi") {
|
||||||
ConnectivityManager::class.java.getDeclaredMethod("getLastTetherError", String::class.java)
|
ConnectivityManager::class.java.getDeclaredMethod("getLastTetherError", String::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,50 +193,50 @@ object TetheringManager {
|
|||||||
Class.forName("android.net.TetheringManager\$TetheringRequest\$Builder")
|
Class.forName("android.net.TetheringManager\$TetheringRequest\$Builder")
|
||||||
}
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val newTetheringRequestBuilder by lazy { classTetheringRequestBuilder.getConstructor(Int::class.java) }
|
private val newTetheringRequestBuilder by lazy @TargetApi(30) {
|
||||||
|
classTetheringRequestBuilder.getConstructor(Int::class.java)
|
||||||
|
}
|
||||||
// @get:RequiresApi(30)
|
// @get:RequiresApi(30)
|
||||||
// private val setStaticIpv4Addresses by lazy {
|
// private val setStaticIpv4Addresses by lazy {
|
||||||
// classTetheringRequestBuilder.getDeclaredMethod("setStaticIpv4Addresses",
|
// classTetheringRequestBuilder.getDeclaredMethod("setStaticIpv4Addresses",
|
||||||
// LinkAddress::class.java, LinkAddress::class.java)
|
// LinkAddress::class.java, LinkAddress::class.java)
|
||||||
// }
|
// }
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val setExemptFromEntitlementCheck by lazy {
|
private val setExemptFromEntitlementCheck by lazy @TargetApi(30) {
|
||||||
classTetheringRequestBuilder.getDeclaredMethod("setExemptFromEntitlementCheck", Boolean::class.java)
|
classTetheringRequestBuilder.getDeclaredMethod("setExemptFromEntitlementCheck", Boolean::class.java)
|
||||||
}
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val setShouldShowEntitlementUi by lazy {
|
private val setShouldShowEntitlementUi by lazy @TargetApi(30) {
|
||||||
classTetheringRequestBuilder.getDeclaredMethod("setShouldShowEntitlementUi", Boolean::class.java)
|
classTetheringRequestBuilder.getDeclaredMethod("setShouldShowEntitlementUi", Boolean::class.java)
|
||||||
}
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val build by lazy { classTetheringRequestBuilder.getDeclaredMethod("build") }
|
private val build by lazy @TargetApi(30) { classTetheringRequestBuilder.getDeclaredMethod("build") }
|
||||||
|
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val interfaceStartTetheringCallback by lazy {
|
private val interfaceStartTetheringCallback by lazy {
|
||||||
Class.forName("android.net.TetheringManager\$StartTetheringCallback")
|
Class.forName("android.net.TetheringManager\$StartTetheringCallback")
|
||||||
}
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val startTethering by lazy {
|
private val startTethering by lazy @TargetApi(30) {
|
||||||
clazz.getDeclaredMethod("startTethering", Class.forName("android.net.TetheringManager\$TetheringRequest"),
|
clazz.getDeclaredMethod("startTethering", Class.forName("android.net.TetheringManager\$TetheringRequest"),
|
||||||
Executor::class.java, interfaceStartTetheringCallback)
|
Executor::class.java, interfaceStartTetheringCallback)
|
||||||
}
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val stopTethering by lazy { clazz.getDeclaredMethod("stopTethering", Int::class.java) }
|
private val stopTethering by lazy @TargetApi(30) { clazz.getDeclaredMethod("stopTethering", Int::class.java) }
|
||||||
|
|
||||||
@Deprecated("Legacy API")
|
@Deprecated("Legacy API")
|
||||||
@RequiresApi(24)
|
|
||||||
fun startTetheringLegacy(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
|
fun startTetheringLegacy(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
|
||||||
handler: Handler? = null, cacheDir: File = app.deviceStorage.codeCacheDir) {
|
handler: Handler? = null, cacheDir: File = app.deviceStorage.codeCacheDir) {
|
||||||
val reference = WeakReference(callback)
|
val reference = WeakReference(callback)
|
||||||
val proxy = ProxyBuilder.forClass(classOnStartTetheringCallback).apply {
|
val proxy = ProxyBuilder.forClass(classOnStartTetheringCallback).apply {
|
||||||
dexCache(cacheDir)
|
dexCache(cacheDir)
|
||||||
handler { proxy, method, args ->
|
handler { proxy, method, args ->
|
||||||
if (args.isNotEmpty()) Timber.w("Unexpected args for ${method.name}: $args")
|
|
||||||
@Suppress("NAME_SHADOWING") val callback = reference.get()
|
@Suppress("NAME_SHADOWING") val callback = reference.get()
|
||||||
when (method.name) {
|
if (args.isEmpty()) when (method.name) {
|
||||||
"onTetheringStarted" -> callback?.onTetheringStarted()
|
"onTetheringStarted" -> return@handler callback?.onTetheringStarted()
|
||||||
"onTetheringFailed" -> callback?.onTetheringFailed()
|
"onTetheringFailed" -> return@handler callback?.onTetheringFailed()
|
||||||
else -> ProxyBuilder.callSuper(proxy, method, args)
|
|
||||||
}
|
}
|
||||||
|
ProxyBuilder.callSuper(proxy, method, args)
|
||||||
}
|
}
|
||||||
}.build()
|
}.build()
|
||||||
startTetheringLegacy(Services.connectivity, type, showProvisioningUi, proxy, handler)
|
startTetheringLegacy(Services.connectivity, type, showProvisioningUi, proxy, handler)
|
||||||
@@ -275,13 +258,9 @@ object TetheringManager {
|
|||||||
arrayOf(interfaceStartTetheringCallback), object : InvocationHandler {
|
arrayOf(interfaceStartTetheringCallback), object : InvocationHandler {
|
||||||
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
|
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
|
||||||
@Suppress("NAME_SHADOWING") val callback = reference.get()
|
@Suppress("NAME_SHADOWING") val callback = reference.get()
|
||||||
return when (val name = method.name) {
|
return when {
|
||||||
"onTetheringStarted" -> {
|
method.matches("onTetheringStarted") -> callback?.onTetheringStarted()
|
||||||
if (!args.isNullOrEmpty()) Timber.w("Unexpected args for $name: $args")
|
method.matches("onTetheringFailed", Integer.TYPE) -> {
|
||||||
callback?.onTetheringStarted()
|
|
||||||
}
|
|
||||||
"onTetheringFailed" -> {
|
|
||||||
if (args?.size != 1) Timber.w("Unexpected args for $name: $args")
|
|
||||||
callback?.onTetheringFailed(args?.get(0) as Int)
|
callback?.onTetheringFailed(args?.get(0) as Int)
|
||||||
}
|
}
|
||||||
else -> callSuper(interfaceStartTetheringCallback, proxy, method, args)
|
else -> callSuper(interfaceStartTetheringCallback, proxy, method, args)
|
||||||
@@ -312,7 +291,6 @@ object TetheringManager {
|
|||||||
* configures tethering with the preferred local IPv4 link address to use.
|
* configures tethering with the preferred local IPv4 link address to use.
|
||||||
* *@see setStaticIpv4Addresses
|
* *@see setStaticIpv4Addresses
|
||||||
*/
|
*/
|
||||||
@RequiresApi(24)
|
|
||||||
fun startTethering(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
|
fun startTethering(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
|
||||||
handler: Handler? = null, cacheDir: File = app.deviceStorage.codeCacheDir) {
|
handler: Handler? = null, cacheDir: File = app.deviceStorage.codeCacheDir) {
|
||||||
if (Build.VERSION.SDK_INT >= 30) try {
|
if (Build.VERSION.SDK_INT >= 30) try {
|
||||||
@@ -384,12 +362,10 @@ object TetheringManager {
|
|||||||
* {@link ConnectivityManager.TETHERING_USB}, or
|
* {@link ConnectivityManager.TETHERING_USB}, or
|
||||||
* {@link ConnectivityManager.TETHERING_BLUETOOTH}.
|
* {@link ConnectivityManager.TETHERING_BLUETOOTH}.
|
||||||
*/
|
*/
|
||||||
@RequiresApi(24)
|
|
||||||
fun stopTethering(type: Int) {
|
fun stopTethering(type: Int) {
|
||||||
if (Build.VERSION.SDK_INT >= 30) stopTethering(instance, type)
|
if (Build.VERSION.SDK_INT >= 30) stopTethering(instance, type)
|
||||||
else stopTetheringLegacy(Services.connectivity, type)
|
else stopTetheringLegacy(Services.connectivity, type)
|
||||||
}
|
}
|
||||||
@RequiresApi(24)
|
|
||||||
fun stopTethering(type: Int, callback: (Exception) -> Unit) {
|
fun stopTethering(type: Int, callback: (Exception) -> Unit) {
|
||||||
try {
|
try {
|
||||||
stopTethering(type)
|
stopTethering(type)
|
||||||
@@ -423,6 +399,23 @@ object TetheringManager {
|
|||||||
*/
|
*/
|
||||||
fun onTetheringSupported(supported: Boolean) {}
|
fun onTetheringSupported(supported: Boolean) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when tethering supported status changed.
|
||||||
|
*
|
||||||
|
* This will be called immediately after the callback is registered, and may be called
|
||||||
|
* multiple times later upon changes.
|
||||||
|
*
|
||||||
|
* Tethering may be disabled via system properties, device configuration, or device
|
||||||
|
* policy restrictions.
|
||||||
|
*
|
||||||
|
* @param supportedTypes a set of @TetheringType which is supported.
|
||||||
|
*/
|
||||||
|
@TargetApi(31)
|
||||||
|
fun onSupportedTetheringTypes(supportedTypes: Set<Int?>) {
|
||||||
|
if ((supportedTypes - expectedTypes).isNotEmpty()) Timber.w(Exception(
|
||||||
|
"Unexpected supported tethering types: ${supportedTypes.joinToString()}"))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when tethering upstream changed.
|
* Called when tethering upstream changed.
|
||||||
*
|
*
|
||||||
@@ -445,7 +438,7 @@ object TetheringManager {
|
|||||||
* *@param reg The new regular expressions.
|
* *@param reg The new regular expressions.
|
||||||
* @hide
|
* @hide
|
||||||
*/
|
*/
|
||||||
fun onTetherableInterfaceRegexpsChanged(args: Array<out Any?>?) {}
|
fun onTetherableInterfaceRegexpsChanged(reg: Any?) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when there was a change in the list of tetherable interfaces. Tetherable
|
* Called when there was a change in the list of tetherable interfaces. Tetherable
|
||||||
@@ -507,11 +500,11 @@ object TetheringManager {
|
|||||||
Class.forName("android.net.TetheringManager\$TetheringEventCallback")
|
Class.forName("android.net.TetheringManager\$TetheringEventCallback")
|
||||||
}
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val registerTetheringEventCallback by lazy {
|
private val registerTetheringEventCallback by lazy @TargetApi(30) {
|
||||||
clazz.getDeclaredMethod("registerTetheringEventCallback", Executor::class.java, interfaceTetheringEventCallback)
|
clazz.getDeclaredMethod("registerTetheringEventCallback", Executor::class.java, interfaceTetheringEventCallback)
|
||||||
}
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val unregisterTetheringEventCallback by lazy {
|
private val unregisterTetheringEventCallback by lazy @TargetApi(30) {
|
||||||
clazz.getDeclaredMethod("unregisterTetheringEventCallback", interfaceTetheringEventCallback)
|
clazz.getDeclaredMethod("unregisterTetheringEventCallback", interfaceTetheringEventCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,40 +534,38 @@ object TetheringManager {
|
|||||||
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
|
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
|
||||||
@Suppress("NAME_SHADOWING")
|
@Suppress("NAME_SHADOWING")
|
||||||
val callback = reference.get()
|
val callback = reference.get()
|
||||||
val noArgs = args?.size ?: 0
|
return when {
|
||||||
return when (val name = method.name) {
|
method.matches("onTetheringSupported", Boolean::class.java) -> {
|
||||||
"onTetheringSupported" -> {
|
|
||||||
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
|
|
||||||
callback?.onTetheringSupported(args!![0] as Boolean)
|
callback?.onTetheringSupported(args!![0] as Boolean)
|
||||||
}
|
}
|
||||||
"onUpstreamChanged" -> {
|
method.matches1<java.util.Set<*>>("onSupportedTetheringTypes") -> {
|
||||||
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
callback?.onSupportedTetheringTypes(args!![0] as Set<Int?>)
|
||||||
|
}
|
||||||
|
method.matches1<Network>("onUpstreamChanged") -> {
|
||||||
callback?.onUpstreamChanged(args!![0] as Network?)
|
callback?.onUpstreamChanged(args!![0] as Network?)
|
||||||
}
|
}
|
||||||
"onTetherableInterfaceRegexpsChanged" -> {
|
method.name == "onTetherableInterfaceRegexpsChanged" &&
|
||||||
if (regexpsSent) callback?.onTetherableInterfaceRegexpsChanged(args)
|
method.parameters.singleOrNull()?.type?.name ==
|
||||||
|
"android.net.TetheringManager\$TetheringInterfaceRegexps" -> {
|
||||||
|
if (regexpsSent) callback?.onTetherableInterfaceRegexpsChanged(args!!.single())
|
||||||
regexpsSent = true
|
regexpsSent = true
|
||||||
}
|
}
|
||||||
"onTetherableInterfacesChanged" -> {
|
method.matches1<java.util.List<*>>("onTetherableInterfacesChanged") -> {
|
||||||
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
callback?.onTetherableInterfacesChanged(args!![0] as List<String?>)
|
callback?.onTetherableInterfacesChanged(args!![0] as List<String?>)
|
||||||
}
|
}
|
||||||
"onTetheredInterfacesChanged" -> {
|
method.matches1<java.util.List<*>>("onTetheredInterfacesChanged") -> {
|
||||||
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
callback?.onTetheredInterfacesChanged(args!![0] as List<String?>)
|
callback?.onTetheredInterfacesChanged(args!![0] as List<String?>)
|
||||||
}
|
}
|
||||||
"onError" -> {
|
method.matches("onError", String::class.java, Integer.TYPE) -> {
|
||||||
if (noArgs != 2) Timber.w("Unexpected args for $name: $args")
|
|
||||||
callback?.onError(args!![0] as String, args[1] as Int)
|
callback?.onError(args!![0] as String, args[1] as Int)
|
||||||
}
|
}
|
||||||
"onClientsChanged" -> {
|
method.matches1<java.util.Collection<*>>("onClientsChanged") -> {
|
||||||
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
|
|
||||||
callback?.onClientsChanged(args!![0] as Collection<*>)
|
callback?.onClientsChanged(args!![0] as Collection<*>)
|
||||||
}
|
}
|
||||||
"onOffloadStatusChanged" -> {
|
method.matches("onOffloadStatusChanged", Integer.TYPE) -> {
|
||||||
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
|
|
||||||
callback?.onOffloadStatusChanged(args!![0] as Int)
|
callback?.onOffloadStatusChanged(args!![0] as Int)
|
||||||
}
|
}
|
||||||
else -> callSuper(interfaceTetheringEventCallback, proxy, method, args)
|
else -> callSuper(interfaceTetheringEventCallback, proxy, method, args)
|
||||||
@@ -596,7 +587,11 @@ object TetheringManager {
|
|||||||
@RequiresApi(30)
|
@RequiresApi(30)
|
||||||
fun unregisterTetheringEventCallback(callback: TetheringEventCallback) {
|
fun unregisterTetheringEventCallback(callback: TetheringEventCallback) {
|
||||||
val proxy = synchronized(callbackMap) { callbackMap.remove(callback) } ?: return
|
val proxy = synchronized(callbackMap) { callbackMap.remove(callback) } ?: return
|
||||||
unregisterTetheringEventCallback(instance, proxy)
|
try {
|
||||||
|
unregisterTetheringEventCallback(instance, proxy)
|
||||||
|
} catch (e: InvocationTargetException) {
|
||||||
|
if (!e.targetException.let { it is IllegalStateException && it.cause is DeadObjectException }) throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -636,14 +631,11 @@ object TetheringManager {
|
|||||||
"TETHER_ERROR_UNSUPPORTED", "TETHER_ERROR_UNAVAIL_IFACE", "TETHER_ERROR_MASTER_ERROR",
|
"TETHER_ERROR_UNSUPPORTED", "TETHER_ERROR_UNAVAIL_IFACE", "TETHER_ERROR_MASTER_ERROR",
|
||||||
"TETHER_ERROR_TETHER_IFACE_ERROR", "TETHER_ERROR_UNTETHER_IFACE_ERROR", "TETHER_ERROR_ENABLE_NAT_ERROR",
|
"TETHER_ERROR_TETHER_IFACE_ERROR", "TETHER_ERROR_UNTETHER_IFACE_ERROR", "TETHER_ERROR_ENABLE_NAT_ERROR",
|
||||||
"TETHER_ERROR_DISABLE_NAT_ERROR", "TETHER_ERROR_IFACE_CFG_ERROR", "TETHER_ERROR_PROVISION_FAILED",
|
"TETHER_ERROR_DISABLE_NAT_ERROR", "TETHER_ERROR_IFACE_CFG_ERROR", "TETHER_ERROR_PROVISION_FAILED",
|
||||||
"TETHER_ERROR_DHCPSERVER_ERROR", "TETHER_ERROR_ENTITLEMENT_UNKNOWN") { clazz }
|
"TETHER_ERROR_DHCPSERVER_ERROR", "TETHER_ERROR_ENTITLEMENT_UNKNOWN") @TargetApi(30) { clazz }
|
||||||
@RequiresApi(30)
|
@RequiresApi(30)
|
||||||
const val TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION = 14
|
const val TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION = 14
|
||||||
|
|
||||||
val Intent.tetheredIfaces get() = getStringArrayListExtra(
|
val Intent.tetheredIfaces get() = getStringArrayListExtra(EXTRA_ACTIVE_TETHER)
|
||||||
if (Build.VERSION.SDK_INT >= 26) EXTRA_ACTIVE_TETHER else EXTRA_ACTIVE_TETHER_LEGACY)
|
val Intent.localOnlyTetheredIfaces get() = getStringArrayListExtra(
|
||||||
val Intent.localOnlyTetheredIfaces get() = if (Build.VERSION.SDK_INT >= 26) {
|
if (Build.VERSION.SDK_INT >= 30) EXTRA_ACTIVE_LOCAL_ONLY else EXTRA_ACTIVE_LOCAL_ONLY_LEGACY)
|
||||||
getStringArrayListExtra(
|
|
||||||
if (Build.VERSION.SDK_INT >= 30) EXTRA_ACTIVE_LOCAL_ONLY else EXTRA_ACTIVE_LOCAL_ONLY_LEGACY)
|
|
||||||
} else emptyList<String>()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
package be.mygod.vpnhotspot.net.monitor
|
package be.mygod.vpnhotspot.net.monitor
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.LinkProperties
|
import android.net.LinkProperties
|
||||||
import android.net.Network
|
import android.net.Network
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import be.mygod.vpnhotspot.util.Services
|
import be.mygod.vpnhotspot.util.Services
|
||||||
|
import be.mygod.vpnhotspot.util.globalNetworkRequestBuilder
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -20,10 +18,10 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
|
|||||||
* Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1:
|
* Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1:
|
||||||
* https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
|
* https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
|
||||||
*/
|
*/
|
||||||
private val networkRequest = networkRequestBuilder()
|
private val networkRequest = globalNetworkRequestBuilder().apply {
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
||||||
.build()
|
}.build()
|
||||||
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||||
override fun onAvailable(network: Network) {
|
override fun onAvailable(network: Network) {
|
||||||
val properties = Services.connectivity.getLinkProperties(network)
|
val properties = Services.connectivity.getLinkProperties(network)
|
||||||
@@ -53,23 +51,10 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
|
|||||||
callback.onAvailable(currentLinkProperties)
|
callback.onAvailable(currentLinkProperties)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
when (Build.VERSION.SDK_INT) {
|
if (Build.VERSION.SDK_INT >= 31) {
|
||||||
in 31..Int.MAX_VALUE -> @TargetApi(31) {
|
Services.connectivity.registerBestMatchingNetworkCallback(networkRequest, networkCallback,
|
||||||
Services.connectivity.registerBestMatchingNetworkCallback(networkRequest, networkCallback,
|
Services.mainHandler)
|
||||||
Handler(Looper.getMainLooper()))
|
} else Services.connectivity.requestNetwork(networkRequest, networkCallback, Services.mainHandler)
|
||||||
}
|
|
||||||
in 24..27 -> @TargetApi(24) {
|
|
||||||
Services.connectivity.registerDefaultNetworkCallback(networkCallback)
|
|
||||||
}
|
|
||||||
else -> try {
|
|
||||||
Services.connectivity.requestNetwork(networkRequest, networkCallback)
|
|
||||||
} catch (e: SecurityException) {
|
|
||||||
// SecurityException would be thrown in requestNetwork on Android 6.0 thanks to Google's stupid bug
|
|
||||||
if (Build.VERSION.SDK_INT != 23) throw e
|
|
||||||
GlobalScope.launch { callback.onFallback() }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
registered = true
|
registered = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.net.Network
|
|||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import be.mygod.vpnhotspot.util.Services
|
import be.mygod.vpnhotspot.util.Services
|
||||||
import be.mygod.vpnhotspot.util.allInterfaceNames
|
import be.mygod.vpnhotspot.util.allInterfaceNames
|
||||||
|
import be.mygod.vpnhotspot.util.globalNetworkRequestBuilder
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@@ -18,7 +19,7 @@ class InterfaceMonitor(private val ifaceRegex: String) : UpstreamMonitor() {
|
|||||||
Timber.d(e);
|
Timber.d(e);
|
||||||
{ it == ifaceRegex }
|
{ it == ifaceRegex }
|
||||||
}
|
}
|
||||||
private val request = networkRequestBuilder().apply {
|
private val request = globalNetworkRequestBuilder().apply {
|
||||||
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
||||||
removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
|
removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
|
||||||
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||||
@@ -76,7 +77,7 @@ class InterfaceMonitor(private val ifaceRegex: String) : UpstreamMonitor() {
|
|||||||
callback.onAvailable(currentLinkProperties)
|
callback.onAvailable(currentLinkProperties)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Services.connectivity.registerNetworkCallback(request, networkCallback)
|
Services.registerNetworkCallback(request, networkCallback)
|
||||||
registered = true
|
registered = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import androidx.core.content.edit
|
|||||||
import be.mygod.librootkotlinx.RootServer
|
import be.mygod.librootkotlinx.RootServer
|
||||||
import be.mygod.librootkotlinx.isEBADF
|
import be.mygod.librootkotlinx.isEBADF
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.BuildConfig
|
|
||||||
import be.mygod.vpnhotspot.R
|
import be.mygod.vpnhotspot.R
|
||||||
import be.mygod.vpnhotspot.net.Routing
|
import be.mygod.vpnhotspot.net.Routing
|
||||||
import be.mygod.vpnhotspot.root.ProcessData
|
import be.mygod.vpnhotspot.root.ProcessData
|
||||||
@@ -25,12 +24,12 @@ abstract class IpMonitor {
|
|||||||
companion object {
|
companion object {
|
||||||
const val KEY = "service.ipMonitor"
|
const val KEY = "service.ipMonitor"
|
||||||
// https://android.googlesource.com/platform/external/iproute2/+/7f7a711/lib/libnetlink.c#493
|
// https://android.googlesource.com/platform/external/iproute2/+/7f7a711/lib/libnetlink.c#493
|
||||||
private val errorMatcher = ("(^Cannot bind netlink socket: |" +
|
private val errorMatcher = ("(?:^Cannot (?:bind netlink socket|send dump request): |^request send failed: |" +
|
||||||
"Dump (was interrupted and may be inconsistent.|terminated)$)").toRegex()
|
"Dump (was interrupted and may be inconsistent.|terminated)$)").toRegex()
|
||||||
var currentMode: Mode
|
var currentMode: Mode
|
||||||
get() {
|
get() {
|
||||||
val isLegacy = Build.VERSION.SDK_INT < 30 || BuildConfig.TARGET_SDK < 30
|
// Completely restricted on Android 13: https://github.com/termux/termux-app/issues/2993#issuecomment-1250312777
|
||||||
val defaultMode = if (isLegacy) @Suppress("DEPRECATION") {
|
val defaultMode = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
|
||||||
Mode.Poll
|
Mode.Poll
|
||||||
} else Mode.MonitorRoot
|
} else Mode.MonitorRoot
|
||||||
return Mode.valueOf(app.pref.getString(KEY, defaultMode.toString()) ?: "")
|
return Mode.valueOf(app.pref.getString(KEY, defaultMode.toString()) ?: "")
|
||||||
@@ -114,8 +113,8 @@ abstract class IpMonitor {
|
|||||||
try {
|
try {
|
||||||
RootManager.use { server ->
|
RootManager.use { server ->
|
||||||
// while we only need to use this server once, we need to also keep the server alive
|
// while we only need to use this server once, we need to also keep the server alive
|
||||||
handleChannel(server.create(ProcessListener(errorMatcher, Routing.IP, "monitor", monitoredObject),
|
handleChannel(server.create(ProcessListener(errorMatcher,
|
||||||
this))
|
Routing.IP, "monitor", monitoredObject), this))
|
||||||
}
|
}
|
||||||
} catch (_: CancellationException) {
|
} catch (_: CancellationException) {
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -152,7 +151,7 @@ abstract class IpMonitor {
|
|||||||
fun flushAsync() = GlobalScope.launch(Dispatchers.IO) { flush() }
|
fun flushAsync() = GlobalScope.launch(Dispatchers.IO) { flush() }
|
||||||
|
|
||||||
private suspend fun work(server: RootServer?): RootServer? {
|
private suspend fun work(server: RootServer?): RootServer? {
|
||||||
if (currentMode != Mode.PollRoot) try {
|
if (currentMode != Mode.PollRoot && currentMode != Mode.MonitorRoot) try {
|
||||||
poll()
|
poll()
|
||||||
return server
|
return server
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
|
|||||||
@@ -33,30 +33,41 @@ class TetherTimeoutMonitor(private val timeout: Long = 0,
|
|||||||
private const val MIN_SOFT_AP_TIMEOUT_DELAY_MS = 600_000 // 10 minutes
|
private const val MIN_SOFT_AP_TIMEOUT_DELAY_MS = 600_000 // 10 minutes
|
||||||
|
|
||||||
@Deprecated("Use SoftApConfigurationCompat instead")
|
@Deprecated("Use SoftApConfigurationCompat instead")
|
||||||
@get:RequiresApi(28)
|
|
||||||
val enabled get() = Settings.Global.getInt(app.contentResolver, SOFT_AP_TIMEOUT_ENABLED, 1) == 1
|
val enabled get() = Settings.Global.getInt(app.contentResolver, SOFT_AP_TIMEOUT_ENABLED, 1) == 1
|
||||||
@Deprecated("Use SoftApConfigurationCompat instead")
|
@Deprecated("Use SoftApConfigurationCompat instead")
|
||||||
suspend fun setEnabled(value: Boolean) = SettingsGlobalPut.int(SOFT_AP_TIMEOUT_ENABLED, if (value) 1 else 0)
|
suspend fun setEnabled(value: Boolean) = SettingsGlobalPut.int(SOFT_AP_TIMEOUT_ENABLED, if (value) 1 else 0)
|
||||||
|
|
||||||
val defaultTimeout: Int get() {
|
val defaultTimeout: Int get() {
|
||||||
val delay = if (Build.VERSION.SDK_INT >= 28) try {
|
val delay = try {
|
||||||
if (Build.VERSION.SDK_INT < 30) Resources.getSystem().run {
|
if (Build.VERSION.SDK_INT < 30) Resources.getSystem().run {
|
||||||
getInteger(getIdentifier("config_wifi_framework_soft_ap_timeout_delay", "integer", "android"))
|
getInteger(getIdentifier("config_wifi_framework_soft_ap_timeout_delay", "integer", "android"))
|
||||||
} else {
|
} else {
|
||||||
val info = WifiApManager.resolvedActivity.activityInfo
|
val info = WifiApManager.resolvedActivity.activityInfo
|
||||||
val resources = app.packageManager.getResourcesForApplication(info.applicationInfo)
|
val resources = app.packageManager.getResourcesForApplication(info.applicationInfo)
|
||||||
resources.getInteger(resources.findIdentifier("config_wifiFrameworkSoftApShutDownTimeoutMilliseconds",
|
resources.getInteger(resources.findIdentifier(
|
||||||
"integer", WifiApManager.RESOURCES_PACKAGE, info.packageName))
|
"config_wifiFrameworkSoftApShutDownTimeoutMilliseconds", "integer",
|
||||||
|
WifiApManager.RESOURCES_PACKAGE, info.packageName))
|
||||||
}
|
}
|
||||||
} catch (e: RuntimeException) {
|
} catch (e: RuntimeException) {
|
||||||
Timber.w(e)
|
Timber.w(e)
|
||||||
MIN_SOFT_AP_TIMEOUT_DELAY_MS
|
MIN_SOFT_AP_TIMEOUT_DELAY_MS
|
||||||
} else MIN_SOFT_AP_TIMEOUT_DELAY_MS
|
}
|
||||||
return if (Build.VERSION.SDK_INT < 30 && delay < MIN_SOFT_AP_TIMEOUT_DELAY_MS) {
|
return if (Build.VERSION.SDK_INT < 30 && delay < MIN_SOFT_AP_TIMEOUT_DELAY_MS) {
|
||||||
Timber.w("Overriding timeout delay with minimum limit value: $delay < $MIN_SOFT_AP_TIMEOUT_DELAY_MS")
|
Timber.w("Overriding timeout delay with minimum limit value: $delay < $MIN_SOFT_AP_TIMEOUT_DELAY_MS")
|
||||||
MIN_SOFT_AP_TIMEOUT_DELAY_MS
|
MIN_SOFT_AP_TIMEOUT_DELAY_MS
|
||||||
} else delay
|
} else delay
|
||||||
}
|
}
|
||||||
|
@get:RequiresApi(31)
|
||||||
|
val defaultTimeoutBridged: Int get() = try {
|
||||||
|
val info = WifiApManager.resolvedActivity.activityInfo
|
||||||
|
val resources = app.packageManager.getResourcesForApplication(info.applicationInfo)
|
||||||
|
resources.getInteger(resources.findIdentifier(
|
||||||
|
"config_wifiFrameworkSoftApShutDownIdleInstanceInBridgedModeTimeoutMillisecond", "integer",
|
||||||
|
WifiApManager.RESOURCES_PACKAGE, info.packageName))
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
Timber.w(e)
|
||||||
|
MIN_SOFT_AP_TIMEOUT_DELAY_MS
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var noClient = true
|
private var noClient = true
|
||||||
@@ -74,7 +85,7 @@ class TetherTimeoutMonitor(private val timeout: Long = 0,
|
|||||||
fun onClientsChanged(noClient: Boolean) {
|
fun onClientsChanged(noClient: Boolean) {
|
||||||
this.noClient = noClient
|
this.noClient = noClient
|
||||||
if (!noClient) close() else if (timeoutJob == null) timeoutJob = GlobalScope.launch(context) {
|
if (!noClient) close() else if (timeoutJob == null) timeoutJob = GlobalScope.launch(context) {
|
||||||
delay(if (timeout == 0L) defaultTimeout.toLong() else timeout)
|
delay(if (timeout <= 0L) defaultTimeout.toLong() else timeout)
|
||||||
onTimeout()
|
onTimeout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package be.mygod.vpnhotspot.net.monitor
|
package be.mygod.vpnhotspot.net.monitor
|
||||||
|
|
||||||
|
import android.net.MacAddress
|
||||||
import androidx.collection.LongSparseArray
|
import androidx.collection.LongSparseArray
|
||||||
import androidx.collection.set
|
import androidx.collection.set
|
||||||
import be.mygod.vpnhotspot.net.IpDev
|
import be.mygod.vpnhotspot.net.IpDev
|
||||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
|
||||||
import be.mygod.vpnhotspot.net.Routing.Companion.IPTABLES
|
import be.mygod.vpnhotspot.net.Routing.Companion.IPTABLES
|
||||||
import be.mygod.vpnhotspot.room.AppDatabase
|
import be.mygod.vpnhotspot.room.AppDatabase
|
||||||
import be.mygod.vpnhotspot.room.TrafficRecord
|
import be.mygod.vpnhotspot.room.TrafficRecord
|
||||||
@@ -11,7 +11,12 @@ import be.mygod.vpnhotspot.util.Event2
|
|||||||
import be.mygod.vpnhotspot.util.RootSession
|
import be.mygod.vpnhotspot.util.RootSession
|
||||||
import be.mygod.vpnhotspot.util.parseNumericAddress
|
import be.mygod.vpnhotspot.util.parseNumericAddress
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineStart
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -23,8 +28,8 @@ object TrafficRecorder {
|
|||||||
private val records = mutableMapOf<IpDev, TrafficRecord>()
|
private val records = mutableMapOf<IpDev, TrafficRecord>()
|
||||||
val foregroundListeners = Event2<Collection<TrafficRecord>, LongSparseArray<TrafficRecord>>()
|
val foregroundListeners = Event2<Collection<TrafficRecord>, LongSparseArray<TrafficRecord>>()
|
||||||
|
|
||||||
fun register(ip: InetAddress, downstream: String, mac: MacAddressCompat) {
|
fun register(ip: InetAddress, downstream: String, mac: MacAddress) {
|
||||||
val record = TrafficRecord(mac = mac.addr, ip = ip, downstream = downstream)
|
val record = TrafficRecord(mac = mac, ip = ip, downstream = downstream)
|
||||||
AppDatabase.instance.trafficRecordDao.insert(record)
|
AppDatabase.instance.trafficRecordDao.insert(record)
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
val key = IpDev(ip, downstream)
|
val key = IpDev(ip, downstream)
|
||||||
@@ -107,9 +112,9 @@ object TrafficRecorder {
|
|||||||
record.sentBytes = columns[1].toLong()
|
record.sentBytes = columns[1].toLong()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldRecord.id != null) {
|
oldRecord.id?.let { oldId ->
|
||||||
check(records.put(key, record) == oldRecord)
|
check(records.put(key, record) == oldRecord)
|
||||||
oldRecords[oldRecord.id!!] = oldRecord
|
oldRecords[oldId] = oldRecord
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> check(false)
|
else -> check(false)
|
||||||
@@ -130,6 +135,7 @@ object TrafficRecorder {
|
|||||||
}
|
}
|
||||||
fun update(timeout: Boolean = false) {
|
fun update(timeout: Boolean = false) {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
|
unscheduleUpdateLocked()
|
||||||
if (records.isEmpty()) return
|
if (records.isEmpty()) return
|
||||||
val timestamp = System.currentTimeMillis()
|
val timestamp = System.currentTimeMillis()
|
||||||
if (!timeout && timestamp - lastUpdate <= 100) return
|
if (!timeout && timestamp - lastUpdate <= 100) return
|
||||||
@@ -141,7 +147,6 @@ object TrafficRecorder {
|
|||||||
SmartSnackbar.make(e).show()
|
SmartSnackbar.make(e).show()
|
||||||
}
|
}
|
||||||
lastUpdate = timestamp
|
lastUpdate = timestamp
|
||||||
updateJob = null
|
|
||||||
scheduleUpdateLocked()
|
scheduleUpdateLocked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,5 +161,5 @@ object TrafficRecorder {
|
|||||||
/**
|
/**
|
||||||
* Possibly inefficient. Don't call this too often.
|
* Possibly inefficient. Don't call this too often.
|
||||||
*/
|
*/
|
||||||
fun isWorking(mac: MacAddressCompat) = records.values.any { it.mac == mac.addr }
|
fun isWorking(mac: MacAddress) = records.values.any { it.mac == mac }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ package be.mygod.vpnhotspot.net.monitor
|
|||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.net.LinkProperties
|
import android.net.LinkProperties
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import android.net.NetworkRequest
|
|
||||||
import android.os.Build
|
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -23,13 +20,6 @@ abstract class UpstreamMonitor {
|
|||||||
}
|
}
|
||||||
private var monitor = generateMonitor()
|
private var monitor = generateMonitor()
|
||||||
|
|
||||||
fun networkRequestBuilder() = NetworkRequest.Builder().apply {
|
|
||||||
if (Build.VERSION.SDK_INT == 23) { // workarounds for OEM bugs
|
|
||||||
removeCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
|
||||||
removeCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun registerCallback(callback: Callback) = synchronized(this) { monitor.registerCallback(callback) }
|
fun registerCallback(callback: Callback) = synchronized(this) { monitor.registerCallback(callback) }
|
||||||
fun unregisterCallback(callback: Callback) = synchronized(this) { monitor.unregisterCallback(callback) }
|
fun unregisterCallback(callback: Callback) = synchronized(this) { monitor.unregisterCallback(callback) }
|
||||||
|
|
||||||
@@ -56,15 +46,6 @@ abstract class UpstreamMonitor {
|
|||||||
* Called if some possibly stacked interface is available
|
* Called if some possibly stacked interface is available
|
||||||
*/
|
*/
|
||||||
fun onAvailable(properties: LinkProperties? = null)
|
fun onAvailable(properties: LinkProperties? = null)
|
||||||
/**
|
|
||||||
* Called on API 23- from DefaultNetworkMonitor. This indicates that there isn't a good way of telling the
|
|
||||||
* default network (see DefaultNetworkMonitor) and we are using rules at priority 22000
|
|
||||||
* (RULE_PRIORITY_DEFAULT_NETWORK) as our fallback rules, which would work fine until Android 9.0 broke it in
|
|
||||||
* commit: https://android.googlesource.com/platform/system/netd/+/758627c4d93392190b08e9aaea3bbbfb92a5f364
|
|
||||||
*/
|
|
||||||
fun onFallback() {
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val callbacks = mutableSetOf<Callback>()
|
val callbacks = mutableSetOf<Callback>()
|
||||||
|
|||||||
@@ -5,15 +5,16 @@ import android.net.LinkProperties
|
|||||||
import android.net.Network
|
import android.net.Network
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import be.mygod.vpnhotspot.util.Services
|
import be.mygod.vpnhotspot.util.Services
|
||||||
|
import be.mygod.vpnhotspot.util.globalNetworkRequestBuilder
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
object VpnMonitor : UpstreamMonitor() {
|
object VpnMonitor : UpstreamMonitor() {
|
||||||
private val request = networkRequestBuilder()
|
private val request = globalNetworkRequestBuilder().apply {
|
||||||
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
|
addTransportType(NetworkCapabilities.TRANSPORT_VPN)
|
||||||
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||||
.build()
|
}.build()
|
||||||
private var registered = false
|
private var registered = false
|
||||||
|
|
||||||
private val available = HashMap<Network, LinkProperties?>()
|
private val available = HashMap<Network, LinkProperties?>()
|
||||||
@@ -60,7 +61,7 @@ object VpnMonitor : UpstreamMonitor() {
|
|||||||
callback.onAvailable(currentLinkProperties)
|
callback.onAvailable(currentLinkProperties)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Services.connectivity.registerNetworkCallback(request, networkCallback)
|
Services.registerNetworkCallback(request, networkCallback)
|
||||||
registered = true
|
registered = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package be.mygod.vpnhotspot.net.wifi
|
package be.mygod.vpnhotspot.net.wifi
|
||||||
|
|
||||||
|
import android.net.MacAddress
|
||||||
import android.net.wifi.p2p.WifiP2pGroup
|
import android.net.wifi.p2p.WifiP2pGroup
|
||||||
import be.mygod.vpnhotspot.RepeaterService
|
import be.mygod.vpnhotspot.RepeaterService
|
||||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||||
@@ -53,8 +54,8 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) {
|
|||||||
var bssids = listOfNotNull(group?.owner?.deviceAddress, ownerAddress)
|
var bssids = listOfNotNull(group?.owner?.deviceAddress, ownerAddress)
|
||||||
.distinct()
|
.distinct()
|
||||||
.filter {
|
.filter {
|
||||||
|
val mac = MacAddress.fromString(it)
|
||||||
try {
|
try {
|
||||||
val mac = MacAddressCompat.fromString(it)
|
|
||||||
mac != MacAddressCompat.ALL_ZEROS_ADDRESS && mac != MacAddressCompat.ANY_ADDRESS
|
mac != MacAddressCompat.ALL_ZEROS_ADDRESS && mac != MacAddressCompat.ANY_ADDRESS
|
||||||
} catch (_: IllegalArgumentException) {
|
} catch (_: IllegalArgumentException) {
|
||||||
false
|
false
|
||||||
@@ -75,7 +76,13 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) {
|
|||||||
if (matchedBssid.isEmpty()) {
|
if (matchedBssid.isEmpty()) {
|
||||||
check(block.pskLine == null && block.psk == null)
|
check(block.pskLine == null && block.psk == null)
|
||||||
if (match.groups[5] != null) {
|
if (match.groups[5] != null) {
|
||||||
block.psk = match.groupValues[5].apply { check(length in 8..63) }
|
block.psk = match.groupValues[5].apply {
|
||||||
|
when (length) {
|
||||||
|
in 8..63 -> { }
|
||||||
|
64 -> error("WPA-PSK hex not supported")
|
||||||
|
else -> error("Unknown length $length")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
block.pskLine = block.size
|
block.pskLine = block.size
|
||||||
} else if (bssids.any { matchedBssid.equals(it, true) }) {
|
} else if (bssids.any { matchedBssid.equals(it, true) }) {
|
||||||
@@ -120,7 +127,7 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) {
|
|||||||
add("\tmode=3")
|
add("\tmode=3")
|
||||||
add("\tdisabled=2")
|
add("\tdisabled=2")
|
||||||
add("}")
|
add("}")
|
||||||
if (target == null) target = this
|
target = this
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
content = Content(result, target!!, persistentMacLine, legacy)
|
content = Content(result, target!!, persistentMacLine, legacy)
|
||||||
@@ -135,13 +142,12 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) {
|
|||||||
}
|
}
|
||||||
val psk by lazy { group?.passphrase ?: content.target.psk!! }
|
val psk by lazy { group?.passphrase ?: content.target.psk!! }
|
||||||
val bssid by lazy {
|
val bssid by lazy {
|
||||||
content.target.bssid?.let { MacAddressCompat.fromString(it) }
|
content.target.bssid?.let { MacAddress.fromString(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun update(ssid: String, psk: String, bssid: MacAddressCompat?) {
|
suspend fun update(ssid: WifiSsidCompat, psk: String, bssid: MacAddress?) {
|
||||||
val (lines, block, persistentMacLine, legacy) = content
|
val (lines, block, persistentMacLine, legacy) = content
|
||||||
block[block.ssidLine!!] = "\tssid=" + ssid.toByteArray()
|
block[block.ssidLine!!] = "\tssid=${ssid.hex}"
|
||||||
.joinToString("") { (it.toInt() and 255).toString(16).padStart(2, '0') }
|
|
||||||
block[block.pskLine!!] = "\tpsk=\"$psk\"" // no control chars or weird stuff
|
block[block.pskLine!!] = "\tpsk=\"$psk\"" // no control chars or weird stuff
|
||||||
if (bssid != null) {
|
if (bssid != null) {
|
||||||
persistentMacLine?.let { lines[it] = PERSISTENT_MAC + bssid }
|
persistentMacLine?.let { lines[it] = PERSISTENT_MAC + bssid }
|
||||||
|
|||||||
@@ -1,20 +1,27 @@
|
|||||||
package be.mygod.vpnhotspot.net.wifi
|
package be.mygod.vpnhotspot.net.wifi
|
||||||
|
|
||||||
|
import android.annotation.TargetApi
|
||||||
|
import android.os.Build
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import be.mygod.vpnhotspot.util.LongConstantLookup
|
import be.mygod.vpnhotspot.util.LongConstantLookup
|
||||||
|
import be.mygod.vpnhotspot.util.UnblockCentral
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@JvmInline
|
@JvmInline
|
||||||
@RequiresApi(30)
|
@RequiresApi(30)
|
||||||
value class SoftApCapability(val inner: Parcelable) {
|
value class SoftApCapability(val inner: Parcelable) {
|
||||||
companion object {
|
companion object {
|
||||||
private val clazz by lazy { Class.forName("android.net.wifi.SoftApCapability") }
|
val clazz by lazy { Class.forName("android.net.wifi.SoftApCapability") }
|
||||||
private val getMaxSupportedClients by lazy { clazz.getDeclaredMethod("getMaxSupportedClients") }
|
private val getMaxSupportedClients by lazy { clazz.getDeclaredMethod("getMaxSupportedClients") }
|
||||||
private val areFeaturesSupported by lazy { clazz.getDeclaredMethod("areFeaturesSupported", Long::class.java) }
|
private val areFeaturesSupported by lazy { clazz.getDeclaredMethod("areFeaturesSupported", Long::class.java) }
|
||||||
@get:RequiresApi(31)
|
@get:RequiresApi(31)
|
||||||
private val getSupportedChannelList by lazy {
|
private val getSupportedChannelList by lazy {
|
||||||
clazz.getDeclaredMethod("getSupportedChannelList", Int::class.java)
|
clazz.getDeclaredMethod("getSupportedChannelList", Int::class.java)
|
||||||
}
|
}
|
||||||
|
@get:RequiresApi(31)
|
||||||
|
@get:TargetApi(33)
|
||||||
|
private val getCountryCode by lazy { UnblockCentral.getCountryCode(clazz) }
|
||||||
|
|
||||||
@RequiresApi(31)
|
@RequiresApi(31)
|
||||||
const val SOFTAP_FEATURE_BAND_24G_SUPPORTED = 32L
|
const val SOFTAP_FEATURE_BAND_24G_SUPPORTED = 32L
|
||||||
@@ -38,4 +45,11 @@ value class SoftApCapability(val inner: Parcelable) {
|
|||||||
return supportedFeatures
|
return supportedFeatures
|
||||||
}
|
}
|
||||||
fun getSupportedChannelList(band: Int) = getSupportedChannelList(inner, band) as IntArray
|
fun getSupportedChannelList(band: Int) = getSupportedChannelList(inner, band) as IntArray
|
||||||
|
@get:RequiresApi(31)
|
||||||
|
val countryCode: String? get() = try {
|
||||||
|
getCountryCode(inner) as String?
|
||||||
|
} catch (e: ReflectiveOperationException) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 33) Timber.w(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,59 +3,70 @@ package be.mygod.vpnhotspot.net.wifi
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.annotation.TargetApi
|
import android.annotation.TargetApi
|
||||||
import android.net.MacAddress
|
import android.net.MacAddress
|
||||||
|
import android.net.wifi.ScanResult
|
||||||
import android.net.wifi.SoftApConfiguration
|
import android.net.wifi.SoftApConfiguration
|
||||||
|
import android.net.wifi.WifiSsid
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.util.SparseIntArray
|
import android.util.SparseIntArray
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.os.BuildCompat
|
|
||||||
import androidx.core.util.keyIterator
|
|
||||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
|
||||||
import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toCompat
|
|
||||||
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
|
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
|
||||||
|
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.requireSingleBand
|
||||||
|
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.setChannel
|
||||||
|
import be.mygod.vpnhotspot.net.wifi.WifiSsidCompat.Companion.toCompat
|
||||||
import be.mygod.vpnhotspot.util.ConstantLookup
|
import be.mygod.vpnhotspot.util.ConstantLookup
|
||||||
import be.mygod.vpnhotspot.util.UnblockCentral
|
import be.mygod.vpnhotspot.util.UnblockCentral
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.lang.reflect.InvocationTargetException
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class SoftApConfigurationCompat(
|
data class SoftApConfigurationCompat(
|
||||||
var ssid: String? = null,
|
var ssid: WifiSsidCompat? = null,
|
||||||
@Deprecated("Workaround for using inline class with Parcelize, use bssid")
|
var bssid: MacAddress? = null,
|
||||||
var bssidAddr: Long? = null,
|
var passphrase: String? = null,
|
||||||
var passphrase: String? = null,
|
var isHiddenSsid: Boolean = false,
|
||||||
var isHiddenSsid: Boolean = false,
|
/**
|
||||||
/**
|
* You should probably set or modify this field directly only when you want to use bridged AP,
|
||||||
* To read legacy band/channel pair, use [requireSingleBand]. For easy access, see [getChannel].
|
* see also [android.net.wifi.WifiManager.isBridgedApConcurrencySupported].
|
||||||
*
|
* Otherwise, use [requireSingleBand] and [setChannel].
|
||||||
* You should probably set or modify this field directly only when you want to use bridged AP,
|
*/
|
||||||
* see also [android.net.wifi.WifiManager.isBridgedApConcurrencySupported].
|
var channels: SparseIntArray = SparseIntArray(1).apply { append(BAND_2GHZ, 0) },
|
||||||
* Otherwise, use [optimizeChannels] or [setChannel].
|
var securityType: Int = SoftApConfiguration.SECURITY_TYPE_OPEN,
|
||||||
*/
|
@TargetApi(30)
|
||||||
@TargetApi(23)
|
var maxNumberOfClients: Int = 0,
|
||||||
var channels: SparseIntArray = SparseIntArray(1).apply { put(BAND_2GHZ, 0) },
|
var isAutoShutdownEnabled: Boolean = true,
|
||||||
var securityType: Int = SoftApConfiguration.SECURITY_TYPE_OPEN,
|
var shutdownTimeoutMillis: Long = 0,
|
||||||
@TargetApi(30)
|
@TargetApi(30)
|
||||||
var maxNumberOfClients: Int = 0,
|
var isClientControlByUserEnabled: Boolean = false,
|
||||||
@TargetApi(28)
|
@RequiresApi(30)
|
||||||
var isAutoShutdownEnabled: Boolean = true,
|
var blockedClientList: List<MacAddress> = emptyList(),
|
||||||
@TargetApi(28)
|
@RequiresApi(30)
|
||||||
var shutdownTimeoutMillis: Long = 0,
|
var allowedClientList: List<MacAddress> = emptyList(),
|
||||||
@TargetApi(30)
|
@TargetApi(31)
|
||||||
var isClientControlByUserEnabled: Boolean = false,
|
var macRandomizationSetting: Int = if (Build.VERSION.SDK_INT >= 33) {
|
||||||
@RequiresApi(30)
|
RANDOMIZATION_NON_PERSISTENT
|
||||||
var blockedClientList: List<MacAddress> = emptyList(),
|
} else RANDOMIZATION_PERSISTENT,
|
||||||
@RequiresApi(30)
|
@TargetApi(31)
|
||||||
var allowedClientList: List<MacAddress> = emptyList(),
|
var isBridgedModeOpportunisticShutdownEnabled: Boolean = true,
|
||||||
@TargetApi(31)
|
@TargetApi(31)
|
||||||
var macRandomizationSetting: Int = RANDOMIZATION_PERSISTENT,
|
var isIeee80211axEnabled: Boolean = true,
|
||||||
@TargetApi(31)
|
@TargetApi(33)
|
||||||
var isBridgedModeOpportunisticShutdownEnabled: Boolean = true,
|
var isIeee80211beEnabled: Boolean = true,
|
||||||
@TargetApi(31)
|
@TargetApi(31)
|
||||||
var isIeee80211axEnabled: Boolean = true,
|
var isUserConfiguration: Boolean = true,
|
||||||
@TargetApi(31)
|
@TargetApi(33)
|
||||||
var isUserConfiguration: Boolean = true,
|
var bridgedModeOpportunisticShutdownTimeoutMillis: Long = -1L,
|
||||||
var underlying: Parcelable? = null) : Parcelable {
|
@TargetApi(33)
|
||||||
|
var vendorElements: List<ScanResult.InformationElement> = emptyList(),
|
||||||
|
@TargetApi(33)
|
||||||
|
var persistentRandomizedMacAddress: MacAddress? = null,
|
||||||
|
@TargetApi(33)
|
||||||
|
var allowedAcsChannels: Map<Int, Set<Int>> = emptyMap(),
|
||||||
|
@TargetApi(33)
|
||||||
|
var maxChannelBandwidth: Int = CHANNEL_WIDTH_AUTO,
|
||||||
|
var underlying: Parcelable? = null,
|
||||||
|
) : Parcelable {
|
||||||
companion object {
|
companion object {
|
||||||
const val BAND_2GHZ = 1
|
const val BAND_2GHZ = 1
|
||||||
const val BAND_5GHZ = 2
|
const val BAND_5GHZ = 2
|
||||||
@@ -64,20 +75,32 @@ data class SoftApConfigurationCompat(
|
|||||||
@TargetApi(31)
|
@TargetApi(31)
|
||||||
const val BAND_60GHZ = 8
|
const val BAND_60GHZ = 8
|
||||||
const val BAND_LEGACY = BAND_2GHZ or BAND_5GHZ
|
const val BAND_LEGACY = BAND_2GHZ or BAND_5GHZ
|
||||||
|
@TargetApi(30)
|
||||||
|
const val BAND_ANY_30 = BAND_LEGACY or BAND_6GHZ
|
||||||
|
@TargetApi(31)
|
||||||
|
const val BAND_ANY_31 = BAND_ANY_30 or BAND_60GHZ
|
||||||
val BAND_TYPES by lazy {
|
val BAND_TYPES by lazy {
|
||||||
if (BuildCompat.isAtLeastS()) try {
|
if (Build.VERSION.SDK_INT >= 31) try {
|
||||||
return@lazy UnblockCentral.SoftApConfiguration_BAND_TYPES
|
return@lazy UnblockCentral.SoftApConfiguration_BAND_TYPES
|
||||||
} catch (e: ReflectiveOperationException) {
|
} catch (e: ReflectiveOperationException) {
|
||||||
Timber.w(e)
|
Timber.w(e)
|
||||||
}
|
}
|
||||||
intArrayOf(BAND_2GHZ, BAND_5GHZ, BAND_6GHZ, BAND_60GHZ)
|
intArrayOf(BAND_2GHZ, BAND_5GHZ, BAND_6GHZ, BAND_60GHZ)
|
||||||
}
|
}
|
||||||
val bandLookup = ConstantLookup<SoftApConfiguration>("BAND_", null, "2GHZ", "5GHZ")
|
@RequiresApi(31)
|
||||||
|
val bandLookup = ConstantLookup<SoftApConfiguration>("BAND_")
|
||||||
|
|
||||||
@TargetApi(31)
|
@TargetApi(31)
|
||||||
const val RANDOMIZATION_NONE = 0
|
const val RANDOMIZATION_NONE = 0
|
||||||
@TargetApi(31)
|
@TargetApi(31)
|
||||||
const val RANDOMIZATION_PERSISTENT = 1
|
const val RANDOMIZATION_PERSISTENT = 1
|
||||||
|
@TargetApi(33)
|
||||||
|
const val RANDOMIZATION_NON_PERSISTENT = 2
|
||||||
|
|
||||||
|
@TargetApi(33)
|
||||||
|
const val CHANNEL_WIDTH_AUTO = -1
|
||||||
|
@TargetApi(30)
|
||||||
|
const val CHANNEL_WIDTH_INVALID = 0
|
||||||
|
|
||||||
fun isLegacyEitherBand(band: Int) = band and BAND_LEGACY == BAND_LEGACY
|
fun isLegacyEitherBand(band: Int) = band and BAND_LEGACY == BAND_LEGACY
|
||||||
|
|
||||||
@@ -86,15 +109,19 @@ data class SoftApConfigurationCompat(
|
|||||||
*/
|
*/
|
||||||
private const val LEGACY_WPA2_PSK = 4
|
private const val LEGACY_WPA2_PSK = 4
|
||||||
|
|
||||||
val securityTypes = arrayOf("OPEN", "WPA2-PSK", "WPA3-SAE", "WPA3-SAE Transition mode")
|
val securityTypes = arrayOf(
|
||||||
|
"OPEN",
|
||||||
private val qrSanitizer = Regex("([\\\\\":;,])")
|
"WPA2-PSK",
|
||||||
|
"WPA3-SAE Transition mode",
|
||||||
|
"WPA3-SAE",
|
||||||
|
"WPA3-OWE Transition",
|
||||||
|
"WPA3-OWE",
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Based on:
|
* Based on:
|
||||||
* https://elixir.bootlin.com/linux/v5.12.8/source/net/wireless/util.c#L75
|
* https://elixir.bootlin.com/linux/v5.12.8/source/net/wireless/util.c#L75
|
||||||
* TODO: update for Android 12
|
* https://cs.android.com/android/platform/superproject/+/master:packages/modules/Wifi/framework/java/android/net/wifi/ScanResult.java;l=789;drc=71d758698c45984d3f8de981bf98e56902480f16
|
||||||
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/wifi/java/android/net/wifi/ScanResult.java;l=624;drc=f7ccda05642b55700d67a288462bada488fc7f5e
|
|
||||||
*/
|
*/
|
||||||
fun channelToFrequency(band: Int, chan: Int) = when (band) {
|
fun channelToFrequency(band: Int, chan: Int) = when (band) {
|
||||||
BAND_2GHZ -> when (chan) {
|
BAND_2GHZ -> when (chan) {
|
||||||
@@ -109,7 +136,7 @@ data class SoftApConfigurationCompat(
|
|||||||
}
|
}
|
||||||
BAND_6GHZ -> when (chan) {
|
BAND_6GHZ -> when (chan) {
|
||||||
2 -> 5935
|
2 -> 5935
|
||||||
in 1..233 -> 5950 + chan * 5
|
in 1..253 -> 5950 + chan * 5
|
||||||
else -> throw IllegalArgumentException("Invalid 6GHz channel $chan")
|
else -> throw IllegalArgumentException("Invalid 6GHz channel $chan")
|
||||||
}
|
}
|
||||||
BAND_60GHZ -> {
|
BAND_60GHZ -> {
|
||||||
@@ -134,7 +161,6 @@ data class SoftApConfigurationCompat(
|
|||||||
*
|
*
|
||||||
* https://android.googlesource.com/platform/frameworks/base/+/android-6.0.0_r1/wifi/java/android/net/wifi/WifiConfiguration.java#242
|
* https://android.googlesource.com/platform/frameworks/base/+/android-6.0.0_r1/wifi/java/android/net/wifi/WifiConfiguration.java#242
|
||||||
*/
|
*/
|
||||||
@get:RequiresApi(23)
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
/**
|
/**
|
||||||
* The band which AP resides on
|
* The band which AP resides on
|
||||||
@@ -142,7 +168,6 @@ data class SoftApConfigurationCompat(
|
|||||||
* By default, 2G is chosen
|
* By default, 2G is chosen
|
||||||
*/
|
*/
|
||||||
private val apBand by lazy { android.net.wifi.WifiConfiguration::class.java.getDeclaredField("apBand") }
|
private val apBand by lazy { android.net.wifi.WifiConfiguration::class.java.getDeclaredField("apBand") }
|
||||||
@get:RequiresApi(23)
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
/**
|
/**
|
||||||
* The channel which AP resides on
|
* The channel which AP resides on
|
||||||
@@ -154,6 +179,10 @@ data class SoftApConfigurationCompat(
|
|||||||
android.net.wifi.WifiConfiguration::class.java.getDeclaredField("apChannel")
|
android.net.wifi.WifiConfiguration::class.java.getDeclaredField("apChannel")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@get:RequiresApi(33)
|
||||||
|
private val getAllowedAcsChannels by lazy @TargetApi(33) {
|
||||||
|
SoftApConfiguration::class.java.getDeclaredMethod("getAllowedAcsChannels", Int::class.java)
|
||||||
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val getAllowedClientList by lazy @TargetApi(30) {
|
private val getAllowedClientList by lazy @TargetApi(30) {
|
||||||
SoftApConfiguration::class.java.getDeclaredMethod("getAllowedClientList")
|
SoftApConfiguration::class.java.getDeclaredMethod("getAllowedClientList")
|
||||||
@@ -164,6 +193,10 @@ data class SoftApConfigurationCompat(
|
|||||||
private val getBlockedClientList by lazy @TargetApi(30) {
|
private val getBlockedClientList by lazy @TargetApi(30) {
|
||||||
SoftApConfiguration::class.java.getDeclaredMethod("getBlockedClientList")
|
SoftApConfiguration::class.java.getDeclaredMethod("getBlockedClientList")
|
||||||
}
|
}
|
||||||
|
@get:RequiresApi(33)
|
||||||
|
private val getBridgedModeOpportunisticShutdownTimeoutMillis by lazy @TargetApi(33) {
|
||||||
|
SoftApConfiguration::class.java.getDeclaredMethod("getBridgedModeOpportunisticShutdownTimeoutMillis")
|
||||||
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val getChannel by lazy @TargetApi(30) {
|
private val getChannel by lazy @TargetApi(30) {
|
||||||
SoftApConfiguration::class.java.getDeclaredMethod("getChannel")
|
SoftApConfiguration::class.java.getDeclaredMethod("getChannel")
|
||||||
@@ -176,14 +209,26 @@ data class SoftApConfigurationCompat(
|
|||||||
private val getMacRandomizationSetting by lazy @TargetApi(31) {
|
private val getMacRandomizationSetting by lazy @TargetApi(31) {
|
||||||
SoftApConfiguration::class.java.getDeclaredMethod("getMacRandomizationSetting")
|
SoftApConfiguration::class.java.getDeclaredMethod("getMacRandomizationSetting")
|
||||||
}
|
}
|
||||||
|
@get:RequiresApi(33)
|
||||||
|
private val getMaxChannelBandwidth by lazy @TargetApi(33) {
|
||||||
|
SoftApConfiguration::class.java.getDeclaredMethod("getMaxChannelBandwidth")
|
||||||
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val getMaxNumberOfClients by lazy @TargetApi(30) {
|
private val getMaxNumberOfClients by lazy @TargetApi(30) {
|
||||||
SoftApConfiguration::class.java.getDeclaredMethod("getMaxNumberOfClients")
|
SoftApConfiguration::class.java.getDeclaredMethod("getMaxNumberOfClients")
|
||||||
}
|
}
|
||||||
|
@get:RequiresApi(33)
|
||||||
|
private val getPersistentRandomizedMacAddress by lazy @TargetApi(33) {
|
||||||
|
SoftApConfiguration::class.java.getDeclaredMethod("getPersistentRandomizedMacAddress")
|
||||||
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val getShutdownTimeoutMillis by lazy @TargetApi(30) {
|
private val getShutdownTimeoutMillis by lazy @TargetApi(30) {
|
||||||
SoftApConfiguration::class.java.getDeclaredMethod("getShutdownTimeoutMillis")
|
SoftApConfiguration::class.java.getDeclaredMethod("getShutdownTimeoutMillis")
|
||||||
}
|
}
|
||||||
|
@get:RequiresApi(33)
|
||||||
|
private val getVendorElements by lazy @TargetApi(33) {
|
||||||
|
SoftApConfiguration::class.java.getDeclaredMethod("getVendorElements")
|
||||||
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val isAutoShutdownEnabled by lazy @TargetApi(30) {
|
private val isAutoShutdownEnabled by lazy @TargetApi(30) {
|
||||||
SoftApConfiguration::class.java.getDeclaredMethod("isAutoShutdownEnabled")
|
SoftApConfiguration::class.java.getDeclaredMethod("isAutoShutdownEnabled")
|
||||||
@@ -200,6 +245,10 @@ data class SoftApConfigurationCompat(
|
|||||||
private val isIeee80211axEnabled by lazy @TargetApi(31) {
|
private val isIeee80211axEnabled by lazy @TargetApi(31) {
|
||||||
SoftApConfiguration::class.java.getDeclaredMethod("isIeee80211axEnabled")
|
SoftApConfiguration::class.java.getDeclaredMethod("isIeee80211axEnabled")
|
||||||
}
|
}
|
||||||
|
@get:RequiresApi(33)
|
||||||
|
private val isIeee80211beEnabled by lazy @TargetApi(33) {
|
||||||
|
SoftApConfiguration::class.java.getDeclaredMethod("isIeee80211beEnabled")
|
||||||
|
}
|
||||||
@get:RequiresApi(31)
|
@get:RequiresApi(31)
|
||||||
private val isUserConfiguration by lazy @TargetApi(31) {
|
private val isUserConfiguration by lazy @TargetApi(31) {
|
||||||
SoftApConfiguration::class.java.getDeclaredMethod("isUserConfiguration")
|
SoftApConfiguration::class.java.getDeclaredMethod("isUserConfiguration")
|
||||||
@@ -210,78 +259,106 @@ data class SoftApConfigurationCompat(
|
|||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val newBuilder by lazy @TargetApi(30) { classBuilder.getConstructor(SoftApConfiguration::class.java) }
|
private val newBuilder by lazy @TargetApi(30) { classBuilder.getConstructor(SoftApConfiguration::class.java) }
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val build by lazy { classBuilder.getDeclaredMethod("build") }
|
private val build by lazy @TargetApi(30) { classBuilder.getDeclaredMethod("build") }
|
||||||
|
@get:RequiresApi(33)
|
||||||
|
private val setAllowedAcsChannels by lazy @TargetApi(33) {
|
||||||
|
classBuilder.getDeclaredMethod("setAllowedAcsChannels", Int::class.java, IntArray::class.java)
|
||||||
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val setAllowedClientList by lazy {
|
private val setAllowedClientList by lazy @TargetApi(30) {
|
||||||
classBuilder.getDeclaredMethod("setAllowedClientList", java.util.List::class.java)
|
classBuilder.getDeclaredMethod("setAllowedClientList", java.util.List::class.java)
|
||||||
}
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val setAutoShutdownEnabled by lazy {
|
private val setAutoShutdownEnabled by lazy @TargetApi(30) {
|
||||||
classBuilder.getDeclaredMethod("setAutoShutdownEnabled", Boolean::class.java)
|
classBuilder.getDeclaredMethod("setAutoShutdownEnabled", Boolean::class.java)
|
||||||
}
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val setBand by lazy { classBuilder.getDeclaredMethod("setBand", Int::class.java) }
|
private val setBand by lazy @TargetApi(30) { classBuilder.getDeclaredMethod("setBand", Int::class.java) }
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val setBlockedClientList by lazy {
|
private val setBlockedClientList by lazy @TargetApi(30) {
|
||||||
classBuilder.getDeclaredMethod("setBlockedClientList", java.util.List::class.java)
|
classBuilder.getDeclaredMethod("setBlockedClientList", java.util.List::class.java)
|
||||||
}
|
}
|
||||||
@get:RequiresApi(31)
|
@get:RequiresApi(31)
|
||||||
private val setBridgedModeOpportunisticShutdownEnabled by lazy {
|
private val setBridgedModeOpportunisticShutdownEnabled by lazy @TargetApi(31) {
|
||||||
classBuilder.getDeclaredMethod("setBridgedModeOpportunisticShutdownEnabled", Boolean::class.java)
|
classBuilder.getDeclaredMethod("setBridgedModeOpportunisticShutdownEnabled", Boolean::class.java)
|
||||||
}
|
}
|
||||||
|
@get:RequiresApi(33)
|
||||||
|
private val setBridgedModeOpportunisticShutdownTimeoutMillis by lazy @TargetApi(33) {
|
||||||
|
classBuilder.getDeclaredMethod("setBridgedModeOpportunisticShutdownTimeoutMillis", Long::class.java)
|
||||||
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val setBssid by lazy @TargetApi(30) {
|
private val setBssid by lazy @TargetApi(30) {
|
||||||
classBuilder.getDeclaredMethod("setBssid", MacAddress::class.java)
|
classBuilder.getDeclaredMethod("setBssid", MacAddress::class.java)
|
||||||
}
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val setChannel by lazy {
|
private val setChannel by lazy @TargetApi(30) {
|
||||||
classBuilder.getDeclaredMethod("setChannel", Int::class.java, Int::class.java)
|
classBuilder.getDeclaredMethod("setChannel", Int::class.java, Int::class.java)
|
||||||
}
|
}
|
||||||
@get:RequiresApi(31)
|
@get:RequiresApi(31)
|
||||||
private val setChannels by lazy {
|
private val setChannels by lazy @TargetApi(31) {
|
||||||
classBuilder.getDeclaredMethod("setChannels", SparseIntArray::class.java)
|
classBuilder.getDeclaredMethod("setChannels", SparseIntArray::class.java)
|
||||||
}
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val setClientControlByUserEnabled by lazy {
|
private val setClientControlByUserEnabled by lazy @TargetApi(30) {
|
||||||
classBuilder.getDeclaredMethod("setClientControlByUserEnabled", Boolean::class.java)
|
classBuilder.getDeclaredMethod("setClientControlByUserEnabled", Boolean::class.java)
|
||||||
}
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val setHiddenSsid by lazy { classBuilder.getDeclaredMethod("setHiddenSsid", Boolean::class.java) }
|
private val setHiddenSsid by lazy @TargetApi(30) {
|
||||||
|
classBuilder.getDeclaredMethod("setHiddenSsid", Boolean::class.java)
|
||||||
|
}
|
||||||
@get:RequiresApi(31)
|
@get:RequiresApi(31)
|
||||||
private val setIeee80211axEnabled by lazy {
|
private val setIeee80211axEnabled by lazy @TargetApi(31) {
|
||||||
classBuilder.getDeclaredMethod("setIeee80211axEnabled", Boolean::class.java)
|
classBuilder.getDeclaredMethod("setIeee80211axEnabled", Boolean::class.java)
|
||||||
}
|
}
|
||||||
|
@get:RequiresApi(33)
|
||||||
|
private val setIeee80211beEnabled by lazy @TargetApi(33) {
|
||||||
|
classBuilder.getDeclaredMethod("setIeee80211beEnabled", Boolean::class.java)
|
||||||
|
}
|
||||||
@get:RequiresApi(31)
|
@get:RequiresApi(31)
|
||||||
private val setMacRandomizationSetting by lazy {
|
private val setMacRandomizationSetting by lazy @TargetApi(31) {
|
||||||
classBuilder.getDeclaredMethod("setMacRandomizationSetting", Int::class.java)
|
classBuilder.getDeclaredMethod("setMacRandomizationSetting", Int::class.java)
|
||||||
}
|
}
|
||||||
|
@get:RequiresApi(33)
|
||||||
|
private val setMaxChannelBandwidth by lazy @TargetApi(33) {
|
||||||
|
classBuilder.getDeclaredMethod("setMaxChannelBandwidth", Int::class.java)
|
||||||
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val setMaxNumberOfClients by lazy {
|
private val setMaxNumberOfClients by lazy @TargetApi(31) {
|
||||||
classBuilder.getDeclaredMethod("setMaxNumberOfClients", Int::class.java)
|
classBuilder.getDeclaredMethod("setMaxNumberOfClients", Int::class.java)
|
||||||
}
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val setPassphrase by lazy {
|
private val setPassphrase by lazy @TargetApi(30) {
|
||||||
classBuilder.getDeclaredMethod("setPassphrase", String::class.java, Int::class.java)
|
classBuilder.getDeclaredMethod("setPassphrase", String::class.java, Int::class.java)
|
||||||
}
|
}
|
||||||
|
@get:RequiresApi(33)
|
||||||
|
private val setRandomizedMacAddress by lazy @TargetApi(33) {
|
||||||
|
UnblockCentral.setRandomizedMacAddress(classBuilder)
|
||||||
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val setShutdownTimeoutMillis by lazy {
|
private val setShutdownTimeoutMillis by lazy @TargetApi(30) {
|
||||||
classBuilder.getDeclaredMethod("setShutdownTimeoutMillis", Long::class.java)
|
classBuilder.getDeclaredMethod("setShutdownTimeoutMillis", Long::class.java)
|
||||||
}
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val setSsid by lazy { classBuilder.getDeclaredMethod("setSsid", String::class.java) }
|
private val setSsid by lazy @TargetApi(30) { classBuilder.getDeclaredMethod("setSsid", String::class.java) }
|
||||||
@get:RequiresApi(31)
|
@get:RequiresApi(33)
|
||||||
private val setUserConfiguration by lazy @TargetApi(31) { UnblockCentral.setUserConfiguration(classBuilder) }
|
private val setVendorElements by lazy @TargetApi(33) {
|
||||||
|
classBuilder.getDeclaredMethod("setVendorElements", java.util.List::class.java)
|
||||||
|
}
|
||||||
|
@get:RequiresApi(33)
|
||||||
|
private val setWifiSsid by lazy @TargetApi(33) {
|
||||||
|
classBuilder.getDeclaredMethod("setWifiSsid", WifiSsid::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
@Deprecated("Class deprecated in framework")
|
@Deprecated("Class deprecated in framework")
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
fun android.net.wifi.WifiConfiguration.toCompat() = SoftApConfigurationCompat(
|
fun android.net.wifi.WifiConfiguration.toCompat() = SoftApConfigurationCompat(
|
||||||
SSID,
|
WifiSsidCompat.fromUtf8Text(SSID),
|
||||||
BSSID?.let { MacAddressCompat.fromString(it) }?.addr,
|
BSSID?.let { MacAddress.fromString(it) },
|
||||||
preSharedKey,
|
preSharedKey,
|
||||||
hiddenSSID,
|
hiddenSSID,
|
||||||
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/wifi/java/android/net/wifi/SoftApConfToXmlMigrationUtil.java;l=87;drc=aa6527cf41671d1ed417b8ebdb6b3aa614f62344
|
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/wifi/java/android/net/wifi/SoftApConfToXmlMigrationUtil.java;l=87;drc=aa6527cf41671d1ed417b8ebdb6b3aa614f62344
|
||||||
SparseIntArray(1).apply {
|
SparseIntArray(1).also {
|
||||||
if (Build.VERSION.SDK_INT < 23) put(BAND_LEGACY, 0) else put(when (val band = apBand.getInt(this)) {
|
it.append(when (val band = apBand.getInt(this)) {
|
||||||
0 -> BAND_2GHZ
|
0 -> BAND_2GHZ
|
||||||
1 -> BAND_5GHZ
|
1 -> BAND_5GHZ
|
||||||
-1 -> BAND_LEGACY
|
-1 -> BAND_LEGACY
|
||||||
@@ -302,77 +379,102 @@ data class SoftApConfigurationCompat(
|
|||||||
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
|
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
|
||||||
}
|
}
|
||||||
android.net.wifi.WifiConfiguration.KeyMgmt.SAE -> SoftApConfiguration.SECURITY_TYPE_WPA3_SAE
|
android.net.wifi.WifiConfiguration.KeyMgmt.SAE -> SoftApConfiguration.SECURITY_TYPE_WPA3_SAE
|
||||||
|
android.net.wifi.WifiConfiguration.KeyMgmt.OWE -> SoftApConfiguration.SECURITY_TYPE_WPA3_OWE
|
||||||
else -> android.net.wifi.WifiConfiguration.KeyMgmt.strings
|
else -> android.net.wifi.WifiConfiguration.KeyMgmt.strings
|
||||||
.getOrElse<String>(selected) { "?" }.let {
|
.getOrElse<String>(selected) { "?" }.let {
|
||||||
throw IllegalArgumentException("Unrecognized key management $it ($selected)")
|
throw IllegalArgumentException("Unrecognized key management $it ($selected)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isAutoShutdownEnabled = if (Build.VERSION.SDK_INT >= 28) TetherTimeoutMonitor.enabled else false,
|
isAutoShutdownEnabled = TetherTimeoutMonitor.enabled,
|
||||||
underlying = this)
|
underlying = this)
|
||||||
|
|
||||||
@RequiresApi(30)
|
@RequiresApi(30)
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun SoftApConfiguration.toCompat() = SoftApConfigurationCompat(
|
fun SoftApConfiguration.toCompat() = SoftApConfigurationCompat(
|
||||||
ssid,
|
if (Build.VERSION.SDK_INT >= 33) wifiSsid?.toCompat() else @Suppress("DEPRECATION") {
|
||||||
bssid?.toCompat()?.addr,
|
WifiSsidCompat.fromUtf8Text(ssid)
|
||||||
passphrase,
|
},
|
||||||
isHiddenSsid,
|
bssid,
|
||||||
if (BuildCompat.isAtLeastS()) getChannels(this) as SparseIntArray else SparseIntArray(1).apply {
|
passphrase,
|
||||||
put(getBand(this) as Int, getChannel(this) as Int)
|
isHiddenSsid,
|
||||||
},
|
if (Build.VERSION.SDK_INT >= 31) getChannels(this) as SparseIntArray else SparseIntArray(1).also {
|
||||||
securityType,
|
it.append(getBand(this) as Int, getChannel(this) as Int)
|
||||||
getMaxNumberOfClients(this) as Int,
|
},
|
||||||
isAutoShutdownEnabled(this) as Boolean,
|
securityType,
|
||||||
getShutdownTimeoutMillis(this) as Long,
|
getMaxNumberOfClients(this) as Int,
|
||||||
isClientControlByUserEnabled(this) as Boolean,
|
isAutoShutdownEnabled(this) as Boolean,
|
||||||
getBlockedClientList(this) as List<MacAddress>,
|
getShutdownTimeoutMillis(this) as Long,
|
||||||
getAllowedClientList(this) as List<MacAddress>,
|
isClientControlByUserEnabled(this) as Boolean,
|
||||||
getMacRandomizationSetting(this) as Int,
|
getBlockedClientList(this) as List<MacAddress>,
|
||||||
isBridgedModeOpportunisticShutdownEnabled(this) as Boolean,
|
getAllowedClientList(this) as List<MacAddress>,
|
||||||
isIeee80211axEnabled(this) as Boolean,
|
underlying = this,
|
||||||
isUserConfiguration(this) as Boolean,
|
).also {
|
||||||
this)
|
if (Build.VERSION.SDK_INT < 31) return@also
|
||||||
}
|
it.macRandomizationSetting = getMacRandomizationSetting(this) as Int
|
||||||
|
it.isBridgedModeOpportunisticShutdownEnabled = isBridgedModeOpportunisticShutdownEnabled(this) as Boolean
|
||||||
@Suppress("DEPRECATION")
|
it.isIeee80211axEnabled = isIeee80211axEnabled(this) as Boolean
|
||||||
inline var bssid: MacAddressCompat?
|
it.isUserConfiguration = isUserConfiguration(this) as Boolean
|
||||||
get() = bssidAddr?.let { MacAddressCompat(it) }
|
if (Build.VERSION.SDK_INT < 33) return@also
|
||||||
set(value) {
|
it.isIeee80211beEnabled = isIeee80211beEnabled(this) as Boolean
|
||||||
bssidAddr = value?.addr
|
it.bridgedModeOpportunisticShutdownTimeoutMillis =
|
||||||
|
getBridgedModeOpportunisticShutdownTimeoutMillis(this) as Long
|
||||||
|
it.vendorElements = getVendorElements(this) as List<ScanResult.InformationElement>
|
||||||
|
it.persistentRandomizedMacAddress = getPersistentRandomizedMacAddress(this) as MacAddress?
|
||||||
|
it.allowedAcsChannels = BAND_TYPES.map { bandType ->
|
||||||
|
try {
|
||||||
|
bandType to (getAllowedAcsChannels(this, bandType) as IntArray).toSet()
|
||||||
|
} catch (e: InvocationTargetException) {
|
||||||
|
if (e.targetException !is IllegalArgumentException) throw e
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.filterNotNull().toMap()
|
||||||
|
it.maxChannelBandwidth = getMaxChannelBandwidth(this) as Int
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only single band/channel can be supplied on API 23-30
|
* Only single band/channel can be supplied on API 23-30
|
||||||
*/
|
*/
|
||||||
fun requireSingleBand(): Pair<Int, Int> {
|
fun requireSingleBand(channels: SparseIntArray): Pair<Int, Int> {
|
||||||
require(channels.size() == 1) { "Unsupported number of bands configured" }
|
require(channels.size() == 1) { "Unsupported number of bands configured" }
|
||||||
return channels.keyAt(0) to channels.valueAt(0)
|
return channels.keyAt(0) to channels.valueAt(0)
|
||||||
}
|
|
||||||
fun getChannel(band: Int): Int {
|
|
||||||
var result = -1
|
|
||||||
for (b in channels.keyIterator()) if (band and b == band) {
|
|
||||||
require(result == -1) { "Duplicate band found" }
|
|
||||||
result = channels[b]
|
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
|
@RequiresApi(30)
|
||||||
|
private fun setChannelsCompat(builder: Any, channels: SparseIntArray) = if (Build.VERSION.SDK_INT < 31) {
|
||||||
|
val (band, channel) = requireSingleBand(channels)
|
||||||
|
if (channel == 0) setBand(builder, band) else setChannel(builder, channel, band)
|
||||||
|
} else setChannels(builder, channels)
|
||||||
|
@get:RequiresApi(30)
|
||||||
|
private val staticBuilder by lazy @TargetApi(30) { classBuilder.newInstance() }
|
||||||
|
@RequiresApi(30)
|
||||||
|
fun testPlatformValidity(channels: SparseIntArray) = setChannelsCompat(staticBuilder, channels)
|
||||||
|
@RequiresApi(30)
|
||||||
|
fun testPlatformValidity(bssid: MacAddress) = setBssid(staticBuilder, bssid)
|
||||||
|
@RequiresApi(33)
|
||||||
|
fun testPlatformValidity(vendorElements: List<ScanResult.InformationElement>) =
|
||||||
|
setVendorElements(staticBuilder, vendorElements)
|
||||||
|
@RequiresApi(33)
|
||||||
|
fun testPlatformValidity(band: Int, channels: IntArray) = setAllowedAcsChannels(staticBuilder, band, channels)
|
||||||
|
@RequiresApi(33)
|
||||||
|
fun testPlatformValidity(bandwidth: Int) = setMaxChannelBandwidth(staticBuilder, bandwidth)
|
||||||
|
@RequiresApi(30)
|
||||||
|
fun testPlatformTimeoutValidity(timeout: Long) = setShutdownTimeoutMillis(staticBuilder, timeout)
|
||||||
|
@RequiresApi(33)
|
||||||
|
fun testPlatformBridgedTimeoutValidity(timeout: Long) =
|
||||||
|
setBridgedModeOpportunisticShutdownTimeoutMillis(staticBuilder, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setChannel(channel: Int, band: Int = BAND_LEGACY) {
|
fun setChannel(channel: Int, band: Int = BAND_LEGACY) {
|
||||||
channels = SparseIntArray(1).apply { put(band, channel) }
|
channels = SparseIntArray(1).apply {
|
||||||
}
|
append(when {
|
||||||
fun optimizeChannels(channels: SparseIntArray = this.channels) {
|
channel <= 0 || band != BAND_LEGACY -> band
|
||||||
this.channels = SparseIntArray(channels.size()).apply {
|
channel > 14 -> BAND_5GHZ
|
||||||
var setBand = 0
|
else -> BAND_2GHZ
|
||||||
for (band in channels.keyIterator()) if (channels[band] == 0) setBand = setBand or band
|
}, channel)
|
||||||
if (setBand != 0) put(setBand, 0) // merge all bands into one
|
|
||||||
for (band in channels.keyIterator()) if (band and setBand == 0) put(band, channels[band])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setMacRandomizationEnabled(enabled: Boolean) {
|
|
||||||
macRandomizationSetting = if (enabled) RANDOMIZATION_PERSISTENT else RANDOMIZATION_NONE
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Based on:
|
* Based on:
|
||||||
* https://android.googlesource.com/platform/packages/apps/Settings/+/android-5.0.0_r1/src/com/android/settings/wifi/WifiApDialog.java#88
|
* https://android.googlesource.com/platform/packages/apps/Settings/+/android-5.0.0_r1/src/com/android/settings/wifi/WifiApDialog.java#88
|
||||||
@@ -383,25 +485,22 @@ data class SoftApConfigurationCompat(
|
|||||||
@Deprecated("Class deprecated in framework, use toPlatform().toWifiConfiguration()")
|
@Deprecated("Class deprecated in framework, use toPlatform().toWifiConfiguration()")
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
fun toWifiConfiguration(): android.net.wifi.WifiConfiguration {
|
fun toWifiConfiguration(): android.net.wifi.WifiConfiguration {
|
||||||
val (band, channel) = requireSingleBand()
|
val (band, channel) = requireSingleBand(channels)
|
||||||
val wc = underlying as? android.net.wifi.WifiConfiguration
|
val wc = underlying as? android.net.wifi.WifiConfiguration
|
||||||
val result = if (wc == null) android.net.wifi.WifiConfiguration() else android.net.wifi.WifiConfiguration(wc)
|
val result = if (wc == null) android.net.wifi.WifiConfiguration() else android.net.wifi.WifiConfiguration(wc)
|
||||||
val original = wc?.toCompat()
|
val original = wc?.toCompat()
|
||||||
result.SSID = ssid
|
result.SSID = ssid?.toString()
|
||||||
result.preSharedKey = passphrase
|
result.preSharedKey = passphrase
|
||||||
result.hiddenSSID = isHiddenSsid
|
result.hiddenSSID = isHiddenSsid
|
||||||
if (Build.VERSION.SDK_INT >= 23) {
|
apBand.setInt(result, when (band) {
|
||||||
apBand.setInt(result, when (band) {
|
BAND_2GHZ -> 0
|
||||||
BAND_2GHZ -> 0
|
BAND_5GHZ -> 1
|
||||||
BAND_5GHZ -> 1
|
else -> {
|
||||||
else -> {
|
require(isLegacyEitherBand(band)) { "Convert fail, unsupported band setting :$band" }
|
||||||
require(Build.VERSION.SDK_INT >= 28) { "A band must be specified on this platform" }
|
-1
|
||||||
require(isLegacyEitherBand(band)) { "Convert fail, unsupported band setting :$band" }
|
}
|
||||||
-1
|
})
|
||||||
}
|
apChannel.setInt(result, channel)
|
||||||
})
|
|
||||||
apChannel.setInt(result, channel)
|
|
||||||
} else require(isLegacyEitherBand(band)) { "Specifying band is unsupported on this platform" }
|
|
||||||
if (original?.securityType != securityType) {
|
if (original?.securityType != securityType) {
|
||||||
result.allowedKeyManagement.clear()
|
result.allowedKeyManagement.clear()
|
||||||
result.allowedKeyManagement.set(when (securityType) {
|
result.allowedKeyManagement.set(when (securityType) {
|
||||||
@@ -411,6 +510,8 @@ data class SoftApConfigurationCompat(
|
|||||||
// CHANGED: not actually converted in framework-wifi
|
// CHANGED: not actually converted in framework-wifi
|
||||||
SoftApConfiguration.SECURITY_TYPE_WPA3_SAE,
|
SoftApConfiguration.SECURITY_TYPE_WPA3_SAE,
|
||||||
SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION -> android.net.wifi.WifiConfiguration.KeyMgmt.SAE
|
SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION -> android.net.wifi.WifiConfiguration.KeyMgmt.SAE
|
||||||
|
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE,
|
||||||
|
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE_TRANSITION -> android.net.wifi.WifiConfiguration.KeyMgmt.OWE
|
||||||
else -> throw IllegalArgumentException("Convert fail, unsupported security type :$securityType")
|
else -> throw IllegalArgumentException("Convert fail, unsupported security type :$securityType")
|
||||||
})
|
})
|
||||||
result.allowedAuthAlgorithms.clear()
|
result.allowedAuthAlgorithms.clear()
|
||||||
@@ -425,29 +526,59 @@ data class SoftApConfigurationCompat(
|
|||||||
fun toPlatform(): SoftApConfiguration {
|
fun toPlatform(): SoftApConfiguration {
|
||||||
val sac = underlying as? SoftApConfiguration
|
val sac = underlying as? SoftApConfiguration
|
||||||
val builder = if (sac == null) classBuilder.newInstance() else newBuilder.newInstance(sac)
|
val builder = if (sac == null) classBuilder.newInstance() else newBuilder.newInstance(sac)
|
||||||
setSsid(builder, ssid)
|
if (Build.VERSION.SDK_INT >= 33) {
|
||||||
setPassphrase(builder, if (securityType == SoftApConfiguration.SECURITY_TYPE_OPEN) null else passphrase,
|
setWifiSsid(builder, ssid?.toPlatform())
|
||||||
securityType)
|
} else setSsid(builder, ssid?.toString())
|
||||||
if (BuildCompat.isAtLeastS()) setChannels(builder, channels) else {
|
setPassphrase(builder, when (securityType) {
|
||||||
val (band, channel) = requireSingleBand()
|
SoftApConfiguration.SECURITY_TYPE_OPEN,
|
||||||
if (channel == 0) setBand(builder, band) else setChannel(builder, channel, band)
|
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE_TRANSITION,
|
||||||
}
|
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE -> null
|
||||||
setBssid(builder, bssid?.toPlatform())
|
else -> passphrase
|
||||||
|
}, securityType)
|
||||||
|
setChannelsCompat(builder, channels)
|
||||||
|
setBssid(builder,
|
||||||
|
if (Build.VERSION.SDK_INT < 31 || macRandomizationSetting == RANDOMIZATION_NONE) bssid else null)
|
||||||
setMaxNumberOfClients(builder, maxNumberOfClients)
|
setMaxNumberOfClients(builder, maxNumberOfClients)
|
||||||
setShutdownTimeoutMillis(builder, shutdownTimeoutMillis)
|
try {
|
||||||
|
setShutdownTimeoutMillis(builder, shutdownTimeoutMillis)
|
||||||
|
} catch (e: InvocationTargetException) {
|
||||||
|
if (e.targetException is IllegalArgumentException) try {
|
||||||
|
setShutdownTimeoutMillis(builder, -1 - shutdownTimeoutMillis)
|
||||||
|
} catch (e2: InvocationTargetException) {
|
||||||
|
e2.addSuppressed(e)
|
||||||
|
throw e2
|
||||||
|
} else throw e
|
||||||
|
}
|
||||||
setAutoShutdownEnabled(builder, isAutoShutdownEnabled)
|
setAutoShutdownEnabled(builder, isAutoShutdownEnabled)
|
||||||
setClientControlByUserEnabled(builder, isClientControlByUserEnabled)
|
setClientControlByUserEnabled(builder, isClientControlByUserEnabled)
|
||||||
setHiddenSsid(builder, isHiddenSsid)
|
setHiddenSsid(builder, isHiddenSsid)
|
||||||
setAllowedClientList(builder, allowedClientList)
|
setAllowedClientList(builder, allowedClientList)
|
||||||
setBlockedClientList(builder, blockedClientList)
|
setBlockedClientList(builder, blockedClientList)
|
||||||
if (BuildCompat.isAtLeastS()) {
|
if (Build.VERSION.SDK_INT >= 31) {
|
||||||
setMacRandomizationSetting(builder, macRandomizationSetting)
|
setMacRandomizationSetting(builder, macRandomizationSetting)
|
||||||
setBridgedModeOpportunisticShutdownEnabled(builder, isBridgedModeOpportunisticShutdownEnabled)
|
setBridgedModeOpportunisticShutdownEnabled(builder, isBridgedModeOpportunisticShutdownEnabled)
|
||||||
setIeee80211axEnabled(builder, isIeee80211axEnabled)
|
setIeee80211axEnabled(builder, isIeee80211axEnabled)
|
||||||
if (sac?.let { isUserConfiguration(it) as Boolean } != false != isUserConfiguration) try {
|
if (Build.VERSION.SDK_INT >= 33) {
|
||||||
setUserConfiguration(builder, isUserConfiguration)
|
setIeee80211beEnabled(builder, isIeee80211beEnabled)
|
||||||
} catch (e: ReflectiveOperationException) {
|
setBridgedModeOpportunisticShutdownTimeoutMillis(builder, bridgedModeOpportunisticShutdownTimeoutMillis)
|
||||||
Timber.w(e) // as far as we are concerned, this field is not used anywhere so ignore for now
|
setVendorElements(builder, vendorElements)
|
||||||
|
val needsUpdate = persistentRandomizedMacAddress != null && sac?.let {
|
||||||
|
getPersistentRandomizedMacAddress(it) as MacAddress
|
||||||
|
} != persistentRandomizedMacAddress
|
||||||
|
if (needsUpdate) try {
|
||||||
|
setRandomizedMacAddress(builder, persistentRandomizedMacAddress)
|
||||||
|
} catch (e: ReflectiveOperationException) {
|
||||||
|
Timber.w(e)
|
||||||
|
}
|
||||||
|
for (bandType in BAND_TYPES) {
|
||||||
|
val value = allowedAcsChannels[bandType] ?: emptySet()
|
||||||
|
try {
|
||||||
|
setAllowedAcsChannels(builder, bandType, value.toIntArray())
|
||||||
|
} catch (e: InvocationTargetException) {
|
||||||
|
if (value.isNotEmpty()) throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setMaxChannelBandwidth(builder, maxChannelBandwidth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return build(builder) as SoftApConfiguration
|
return build(builder) as SoftApConfiguration
|
||||||
@@ -458,21 +589,21 @@ data class SoftApConfigurationCompat(
|
|||||||
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/4a5ff58/src/com/android/settings/wifi/dpp/WifiNetworkConfig.java#161
|
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/4a5ff58/src/com/android/settings/wifi/dpp/WifiNetworkConfig.java#161
|
||||||
*/
|
*/
|
||||||
fun toQrCode() = StringBuilder("WIFI:").apply {
|
fun toQrCode() = StringBuilder("WIFI:").apply {
|
||||||
fun String.sanitize() = qrSanitizer.replace(this) { "\\${it.groupValues[1]}" }
|
|
||||||
when (securityType) {
|
when (securityType) {
|
||||||
SoftApConfiguration.SECURITY_TYPE_OPEN -> { }
|
SoftApConfiguration.SECURITY_TYPE_OPEN, SoftApConfiguration.SECURITY_TYPE_WPA3_OWE_TRANSITION,
|
||||||
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK -> append("T:WPA;")
|
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE -> { }
|
||||||
SoftApConfiguration.SECURITY_TYPE_WPA3_SAE, SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION -> {
|
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION -> {
|
||||||
append("T:SAE;")
|
append("T:WPA;")
|
||||||
}
|
}
|
||||||
|
SoftApConfiguration.SECURITY_TYPE_WPA3_SAE -> append("T:SAE;")
|
||||||
else -> throw IllegalArgumentException("Unsupported authentication type")
|
else -> throw IllegalArgumentException("Unsupported authentication type")
|
||||||
}
|
}
|
||||||
append("S:")
|
append("S:")
|
||||||
append(ssid!!.sanitize())
|
append(ssid!!.toMeCard())
|
||||||
append(';')
|
append(';')
|
||||||
passphrase?.let { passphrase ->
|
passphrase?.let { passphrase ->
|
||||||
append("P:")
|
append("P:")
|
||||||
append(passphrase.sanitize())
|
append(WifiSsidCompat.toMeCard(passphrase))
|
||||||
append(';')
|
append(';')
|
||||||
}
|
}
|
||||||
if (isHiddenSsid) append("H:true;")
|
if (isHiddenSsid) append("H:true;")
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import timber.log.Timber
|
|||||||
@RequiresApi(30)
|
@RequiresApi(30)
|
||||||
value class SoftApInfo(val inner: Parcelable) {
|
value class SoftApInfo(val inner: Parcelable) {
|
||||||
companion object {
|
companion object {
|
||||||
private val clazz by lazy { Class.forName("android.net.wifi.SoftApInfo") }
|
val clazz by lazy { Class.forName("android.net.wifi.SoftApInfo") }
|
||||||
private val getFrequency by lazy { clazz.getDeclaredMethod("getFrequency") }
|
private val getFrequency by lazy { clazz.getDeclaredMethod("getFrequency") }
|
||||||
private val getBandwidth by lazy { clazz.getDeclaredMethod("getBandwidth") }
|
private val getBandwidth by lazy { clazz.getDeclaredMethod("getBandwidth") }
|
||||||
@get:RequiresApi(31)
|
@get:RequiresApi(31)
|
||||||
@@ -30,7 +30,7 @@ value class SoftApInfo(val inner: Parcelable) {
|
|||||||
val frequency get() = getFrequency(inner) as Int
|
val frequency get() = getFrequency(inner) as Int
|
||||||
val bandwidth get() = getBandwidth(inner) as Int
|
val bandwidth get() = getBandwidth(inner) as Int
|
||||||
@get:RequiresApi(31)
|
@get:RequiresApi(31)
|
||||||
val bssid get() = getBssid(inner) as MacAddress
|
val bssid get() = getBssid(inner) as MacAddress?
|
||||||
@get:RequiresApi(31)
|
@get:RequiresApi(31)
|
||||||
val wifiStandard get() = getWifiStandard(inner) as Int
|
val wifiStandard get() = getWifiStandard(inner) as Int
|
||||||
@get:RequiresApi(31)
|
@get:RequiresApi(31)
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package be.mygod.vpnhotspot.net.wifi
|
||||||
|
|
||||||
|
import android.net.wifi.ScanResult
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
@RequiresApi(33)
|
||||||
|
object VendorElements {
|
||||||
|
fun serialize(input: List<ScanResult.InformationElement>) = input.joinToString("\n") { element ->
|
||||||
|
element.bytes.let { buffer ->
|
||||||
|
StringBuilder().apply {
|
||||||
|
while (buffer.hasRemaining()) append("%02x".format(buffer.get()))
|
||||||
|
}.toString()
|
||||||
|
}.also {
|
||||||
|
if (element.id != 221 || element.idExt != 0 || it.isEmpty()) Timber.w(Exception(
|
||||||
|
"Unexpected InformationElement ${element.id}, ${element.idExt}, $it"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deserialize(input: CharSequence?) = (input ?: "").split("\n").map { line ->
|
||||||
|
if (line.isBlank()) return@map null
|
||||||
|
require(line.length % 2 == 0) { "Input should be hex: $line" }
|
||||||
|
(0 until line.length / 2).map {
|
||||||
|
Integer.parseInt(line.substring(it * 2, it * 2 + 2), 16).toByte()
|
||||||
|
}.toByteArray()
|
||||||
|
}.filterNotNull().map { ScanResult.InformationElement(221, 0, it) }
|
||||||
|
}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
package be.mygod.vpnhotspot.net.wifi
|
package be.mygod.vpnhotspot.net.wifi
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.annotation.TargetApi
|
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
|
import android.content.ClipDescription
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
|
import android.net.MacAddress
|
||||||
import android.net.wifi.SoftApConfiguration
|
import android.net.wifi.SoftApConfiguration
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
|
import android.text.InputFilter
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.SparseIntArray
|
import android.util.SparseIntArray
|
||||||
@@ -16,10 +18,11 @@ import android.view.View
|
|||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.Spinner
|
import android.widget.Spinner
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.os.BuildCompat
|
import androidx.core.os.persistableBundleOf
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import be.mygod.librootkotlinx.toByteArray
|
import be.mygod.librootkotlinx.toByteArray
|
||||||
import be.mygod.librootkotlinx.toParcelable
|
import be.mygod.librootkotlinx.toParcelable
|
||||||
@@ -28,14 +31,16 @@ import be.mygod.vpnhotspot.App.Companion.app
|
|||||||
import be.mygod.vpnhotspot.R
|
import be.mygod.vpnhotspot.R
|
||||||
import be.mygod.vpnhotspot.RepeaterService
|
import be.mygod.vpnhotspot.RepeaterService
|
||||||
import be.mygod.vpnhotspot.databinding.DialogWifiApBinding
|
import be.mygod.vpnhotspot.databinding.DialogWifiApBinding
|
||||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
|
||||||
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
|
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
|
||||||
import be.mygod.vpnhotspot.util.QRCodeDialog
|
import be.mygod.vpnhotspot.util.QRCodeDialog
|
||||||
|
import be.mygod.vpnhotspot.util.RangeInput
|
||||||
import be.mygod.vpnhotspot.util.readableMessage
|
import be.mygod.vpnhotspot.util.readableMessage
|
||||||
import be.mygod.vpnhotspot.util.showAllowingStateLoss
|
import be.mygod.vpnhotspot.util.showAllowingStateLoss
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.text.DecimalFormat
|
||||||
|
import java.text.DecimalFormatSymbols
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/39b4674/src/com/android/settings/wifi/WifiApDialog.java
|
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/39b4674/src/com/android/settings/wifi/WifiApDialog.java
|
||||||
@@ -48,26 +53,34 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
|||||||
companion object {
|
companion object {
|
||||||
private const val BASE64_FLAGS = Base64.NO_PADDING or Base64.NO_WRAP
|
private const val BASE64_FLAGS = Base64.NO_PADDING or Base64.NO_WRAP
|
||||||
private val nonMacChars = "[^0-9a-fA-F:]+".toRegex()
|
private val nonMacChars = "[^0-9a-fA-F:]+".toRegex()
|
||||||
private val baseOptions by lazy { listOf(ChannelOption.Disabled, ChannelOption.Auto) }
|
private val channels2G = (1..14).map { ChannelOption(SoftApConfigurationCompat.BAND_2GHZ, it) }
|
||||||
private val channels2G by lazy {
|
|
||||||
baseOptions + (1..14).map { ChannelOption(it, SoftApConfigurationCompat.BAND_2GHZ) }
|
|
||||||
}
|
|
||||||
private val channels5G by lazy {
|
private val channels5G by lazy {
|
||||||
baseOptions + (1..196).map { ChannelOption(it, SoftApConfigurationCompat.BAND_5GHZ) }
|
channels2G + (1..196).map { ChannelOption(SoftApConfigurationCompat.BAND_5GHZ, it) }
|
||||||
}
|
|
||||||
@get:RequiresApi(30)
|
|
||||||
private val channels6G by lazy {
|
|
||||||
baseOptions + (1..233).map { ChannelOption(it, SoftApConfigurationCompat.BAND_6GHZ) }
|
|
||||||
}
|
|
||||||
@get:RequiresApi(31)
|
|
||||||
private val channels60G by lazy {
|
|
||||||
baseOptions + (1..6).map { ChannelOption(it, SoftApConfigurationCompat.BAND_60GHZ) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun genAutoOptions(band: Int) = (1..band).filter { it and band == it }.map { ChannelOption(it) }
|
||||||
/**
|
/**
|
||||||
* Source: https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/c2fc6a1/service/java/com/android/server/wifi/p2p/SupplicantP2pIfaceHal.java#1396
|
* Source: https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/c2fc6a1/service/java/com/android/server/wifi/p2p/SupplicantP2pIfaceHal.java#1396
|
||||||
*/
|
*/
|
||||||
private val p2pChannels by lazy {
|
private val p2pUnsafeOptions by lazy {
|
||||||
baseOptions + (15..165).map { ChannelOption(it, SoftApConfigurationCompat.BAND_5GHZ) }
|
listOf(ChannelOption(SoftApConfigurationCompat.BAND_LEGACY)) +
|
||||||
|
channels2G + (15..165).map { ChannelOption(SoftApConfigurationCompat.BAND_5GHZ, it) }
|
||||||
|
}
|
||||||
|
private val p2pSafeOptions by lazy { genAutoOptions(SoftApConfigurationCompat.BAND_LEGACY) + channels5G }
|
||||||
|
private val softApOptions by lazy {
|
||||||
|
if (Build.VERSION.SDK_INT >= 30) {
|
||||||
|
genAutoOptions(SoftApConfigurationCompat.BAND_ANY_31) +
|
||||||
|
channels5G +
|
||||||
|
(1..253).map { ChannelOption(SoftApConfigurationCompat.BAND_6GHZ, it) } +
|
||||||
|
(1..6).map { ChannelOption(SoftApConfigurationCompat.BAND_60GHZ, it) }
|
||||||
|
} else p2pSafeOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:RequiresApi(30)
|
||||||
|
private val bandWidthOptions by lazy {
|
||||||
|
SoftApInfo.channelWidthLookup.lookup.let { lookup ->
|
||||||
|
Array(lookup.size()) { BandWidth(lookup.keyAt(it), lookup.valueAt(it).substring(14)) }.apply { sort() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,62 +93,99 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
|||||||
*/
|
*/
|
||||||
val p2pMode: Boolean = false) : Parcelable
|
val p2pMode: Boolean = false) : Parcelable
|
||||||
|
|
||||||
private open class ChannelOption(val channel: Int = 0, private val band: Int = 0) {
|
private open class ChannelOption(val band: Int = 0, val channel: Int = 0) {
|
||||||
object Disabled : ChannelOption(-1) {
|
object Disabled : ChannelOption(-1) {
|
||||||
override fun toString() = app.getString(R.string.wifi_ap_choose_disabled)
|
override fun toString() = app.getString(R.string.wifi_ap_choose_disabled)
|
||||||
}
|
}
|
||||||
object Auto : ChannelOption() {
|
override fun toString() = if (channel == 0) {
|
||||||
override fun toString() = app.getString(R.string.wifi_ap_choose_auto)
|
val format = DecimalFormat("#.#", DecimalFormatSymbols.getInstance(app.resources.configuration.locales[0]))
|
||||||
}
|
app.getString(R.string.wifi_ap_choose_G, arrayOf(
|
||||||
override fun toString() = "${SoftApConfigurationCompat.channelToFrequency(band, channel)} MHz ($channel)"
|
SoftApConfigurationCompat.BAND_2GHZ to 2.4,
|
||||||
|
SoftApConfigurationCompat.BAND_5GHZ to 5,
|
||||||
|
SoftApConfigurationCompat.BAND_6GHZ to 6,
|
||||||
|
SoftApConfigurationCompat.BAND_60GHZ to 60,
|
||||||
|
).filter { (mask, _) -> band and mask == mask }.joinToString("/") { (_, name) -> format.format(name) })
|
||||||
|
} else "${SoftApConfigurationCompat.channelToFrequency(band, channel)} MHz ($channel)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BandWidth(val width: Int, val name: String = "") : Comparable<BandWidth> {
|
||||||
|
override fun compareTo(other: BandWidth) = width - other.width
|
||||||
|
override fun toString() = name
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var dialogView: DialogWifiApBinding
|
private lateinit var dialogView: DialogWifiApBinding
|
||||||
private lateinit var base: SoftApConfigurationCompat
|
private lateinit var base: SoftApConfigurationCompat
|
||||||
|
private var pasted = false
|
||||||
private var started = false
|
private var started = false
|
||||||
private val currentChannels5G get() = if (arg.p2pMode && !RepeaterService.safeMode) p2pChannels else channels5G
|
private val currentChannels get() = when {
|
||||||
|
!arg.p2pMode -> softApOptions
|
||||||
|
RepeaterService.safeMode -> p2pSafeOptions
|
||||||
|
else -> p2pUnsafeOptions
|
||||||
|
}
|
||||||
|
private val acsList by lazy {
|
||||||
|
listOf(
|
||||||
|
Triple(SoftApConfigurationCompat.BAND_2GHZ, dialogView.acs2g, dialogView.acs2gWrapper),
|
||||||
|
Triple(SoftApConfigurationCompat.BAND_5GHZ, dialogView.acs5g, dialogView.acs5gWrapper),
|
||||||
|
Triple(SoftApConfigurationCompat.BAND_6GHZ, dialogView.acs6g, dialogView.acs6gWrapper),
|
||||||
|
)
|
||||||
|
}
|
||||||
override val ret get() = Arg(generateConfig())
|
override val ret get() = Arg(generateConfig())
|
||||||
|
private val hexToggleable get() = if (arg.p2pMode) !RepeaterService.safeMode else Build.VERSION.SDK_INT >= 33
|
||||||
|
private var hexSsid = false
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
dialogView.ssidWrapper.setEndIconActivated(value)
|
||||||
|
}
|
||||||
|
private val ssid get() =
|
||||||
|
if (hexSsid) WifiSsidCompat.fromHex(dialogView.ssid.text) else WifiSsidCompat.fromUtf8Text(dialogView.ssid.text)
|
||||||
|
|
||||||
|
private fun generateChannels() = SparseIntArray(2).apply {
|
||||||
|
if (!arg.p2pMode && Build.VERSION.SDK_INT >= 31) {
|
||||||
|
(dialogView.bandSecondary.selectedItem as ChannelOption?)?.apply { if (band >= 0) put(band, channel) }
|
||||||
|
}
|
||||||
|
(dialogView.bandPrimary.selectedItem as ChannelOption).apply { put(band, channel) }
|
||||||
|
}
|
||||||
private fun generateConfig(full: Boolean = true) = base.copy(
|
private fun generateConfig(full: Boolean = true) = base.copy(
|
||||||
ssid = dialogView.ssid.text.toString(),
|
ssid = ssid,
|
||||||
passphrase = if (dialogView.password.length() != 0) dialogView.password.text.toString() else null).apply {
|
passphrase = if (dialogView.password.length() != 0) dialogView.password.text.toString() else null).apply {
|
||||||
if (!arg.p2pMode) {
|
if (!arg.p2pMode) {
|
||||||
securityType = dialogView.security.selectedItemPosition
|
securityType = dialogView.security.selectedItemPosition
|
||||||
isHiddenSsid = dialogView.hiddenSsid.isChecked
|
isHiddenSsid = dialogView.hiddenSsid.isChecked
|
||||||
}
|
}
|
||||||
if (full) @TargetApi(28) {
|
if (full) {
|
||||||
isAutoShutdownEnabled = dialogView.autoShutdown.isChecked
|
isAutoShutdownEnabled = dialogView.autoShutdown.isChecked
|
||||||
shutdownTimeoutMillis = dialogView.timeout.text.let { text ->
|
shutdownTimeoutMillis = dialogView.timeout.text.let { text ->
|
||||||
if (text.isNullOrEmpty()) 0 else text.toString().toLong()
|
if (text.isNullOrEmpty()) 0 else text.toString().toLong()
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) {
|
channels = generateChannels()
|
||||||
val channels = SparseIntArray(4)
|
|
||||||
for ((band, spinner) in arrayOf(SoftApConfigurationCompat.BAND_2GHZ to dialogView.band2G,
|
|
||||||
SoftApConfigurationCompat.BAND_5GHZ to dialogView.band5G,
|
|
||||||
SoftApConfigurationCompat.BAND_6GHZ to dialogView.band6G,
|
|
||||||
SoftApConfigurationCompat.BAND_60GHZ to dialogView.band60G)) {
|
|
||||||
val channel = (spinner.selectedItem as ChannelOption?)?.channel
|
|
||||||
if (channel != null && channel >= 0) channels.put(band, channel)
|
|
||||||
}
|
|
||||||
if (!arg.p2pMode && BuildCompat.isAtLeastS() && dialogView.bridgedMode.isChecked) {
|
|
||||||
this.channels = channels
|
|
||||||
} else optimizeChannels(channels)
|
|
||||||
}
|
|
||||||
bssid = if (dialogView.bssid.length() != 0) {
|
|
||||||
MacAddressCompat.fromString(dialogView.bssid.text.toString())
|
|
||||||
} else null
|
|
||||||
maxNumberOfClients = dialogView.maxClient.text.let { text ->
|
maxNumberOfClients = dialogView.maxClient.text.let { text ->
|
||||||
if (text.isNullOrEmpty()) 0 else text.toString().toInt()
|
if (text.isNullOrEmpty()) 0 else text.toString().toInt()
|
||||||
}
|
}
|
||||||
isClientControlByUserEnabled = dialogView.clientUserControl.isChecked
|
isClientControlByUserEnabled = dialogView.clientUserControl.isChecked
|
||||||
allowedClientList = (dialogView.allowedList.text ?: "").split(nonMacChars)
|
allowedClientList = (dialogView.allowedList.text ?: "").split(nonMacChars)
|
||||||
.filter { it.isNotEmpty() }.map { MacAddressCompat.fromString(it).toPlatform() }
|
.filter { it.isNotEmpty() }.map(MacAddress::fromString)
|
||||||
blockedClientList = (dialogView.blockedList.text ?: "").split(nonMacChars)
|
blockedClientList = (dialogView.blockedList.text ?: "").split(nonMacChars)
|
||||||
.filter { it.isNotEmpty() }.map { MacAddressCompat.fromString(it).toPlatform() }
|
.filter { it.isNotEmpty() }.map(MacAddress::fromString)
|
||||||
setMacRandomizationEnabled(dialogView.macRandomization.isChecked)
|
macRandomizationSetting = dialogView.macRandomization.selectedItemPosition
|
||||||
|
bssid = if ((arg.p2pMode || Build.VERSION.SDK_INT < 31 && macRandomizationSetting ==
|
||||||
|
SoftApConfigurationCompat.RANDOMIZATION_NONE) && dialogView.bssid.length() != 0) {
|
||||||
|
MacAddress.fromString(dialogView.bssid.text.toString())
|
||||||
|
} else null
|
||||||
isBridgedModeOpportunisticShutdownEnabled = dialogView.bridgedModeOpportunisticShutdown.isChecked
|
isBridgedModeOpportunisticShutdownEnabled = dialogView.bridgedModeOpportunisticShutdown.isChecked
|
||||||
isIeee80211axEnabled = dialogView.ieee80211ax.isChecked
|
isIeee80211axEnabled = dialogView.ieee80211ax.isChecked
|
||||||
|
isIeee80211beEnabled = dialogView.ieee80211be.isChecked
|
||||||
isUserConfiguration = dialogView.userConfig.isChecked
|
isUserConfiguration = dialogView.userConfig.isChecked
|
||||||
|
bridgedModeOpportunisticShutdownTimeoutMillis = dialogView.bridgedTimeout.text.let { text ->
|
||||||
|
if (text.isNullOrEmpty()) -1L else text.toString().toLong()
|
||||||
|
}
|
||||||
|
vendorElements = VendorElements.deserialize(dialogView.vendorElements.text)
|
||||||
|
persistentRandomizedMacAddress = if (dialogView.persistentRandomizedMac.length() != 0) {
|
||||||
|
MacAddress.fromString(dialogView.persistentRandomizedMac.text.toString())
|
||||||
|
} else null
|
||||||
|
allowedAcsChannels = acsList.associate { (band, text, _) -> band to RangeInput.fromString(text.text) }
|
||||||
|
if (!arg.p2pMode && Build.VERSION.SDK_INT >= 33) {
|
||||||
|
maxChannelBandwidth = (dialogView.maxChannelBandwidth.selectedItem as BandWidth).width
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +198,31 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
|||||||
setNegativeButton(R.string.donations__button_close, null)
|
setNegativeButton(R.string.donations__button_close, null)
|
||||||
dialogView.toolbar.inflateMenu(R.menu.toolbar_configuration)
|
dialogView.toolbar.inflateMenu(R.menu.toolbar_configuration)
|
||||||
dialogView.toolbar.setOnMenuItemClickListener(this@WifiApDialogFragment)
|
dialogView.toolbar.setOnMenuItemClickListener(this@WifiApDialogFragment)
|
||||||
|
dialogView.ssidWrapper.setLengthCounter {
|
||||||
|
try {
|
||||||
|
ssid?.bytes?.size ?: 0
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hexToggleable) dialogView.ssidWrapper.apply {
|
||||||
|
endIconMode = TextInputLayout.END_ICON_CUSTOM
|
||||||
|
setEndIconOnClickListener {
|
||||||
|
val ssid = try {
|
||||||
|
ssid
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
return@setEndIconOnClickListener
|
||||||
|
}
|
||||||
|
val newText = if (hexSsid) ssid?.run {
|
||||||
|
decode().also { if (it == null) return@setEndIconOnClickListener }
|
||||||
|
} else ssid?.hex
|
||||||
|
hexSsid = !hexSsid
|
||||||
|
dialogView.ssid.setText(newText)
|
||||||
|
}
|
||||||
|
findViewById<View>(com.google.android.material.R.id.text_input_end_icon).apply {
|
||||||
|
tooltipText = contentDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!arg.readOnly) dialogView.ssid.addTextChangedListener(this@WifiApDialogFragment)
|
if (!arg.readOnly) dialogView.ssid.addTextChangedListener(this@WifiApDialogFragment)
|
||||||
if (arg.p2pMode) dialogView.securityWrapper.isGone = true else dialogView.security.apply {
|
if (arg.p2pMode) dialogView.securityWrapper.isGone = true else dialogView.security.apply {
|
||||||
adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0,
|
adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0,
|
||||||
@@ -157,99 +232,115 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
|||||||
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) = error("Must select something")
|
override fun onNothingSelected(parent: AdapterView<*>?) = error("Must select something")
|
||||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||||
dialogView.passwordWrapper.isGone = position == SoftApConfiguration.SECURITY_TYPE_OPEN
|
when (position) {
|
||||||
|
SoftApConfiguration.SECURITY_TYPE_OPEN,
|
||||||
|
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE_TRANSITION,
|
||||||
|
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE -> dialogView.passwordWrapper.isGone = true
|
||||||
|
else -> {
|
||||||
|
dialogView.passwordWrapper.isGone = false
|
||||||
|
if (position == SoftApConfiguration.SECURITY_TYPE_WPA3_SAE) {
|
||||||
|
dialogView.passwordWrapper.isCounterEnabled = false
|
||||||
|
dialogView.passwordWrapper.counterMaxLength = 0
|
||||||
|
dialogView.password.filters = emptyArray()
|
||||||
|
} else {
|
||||||
|
dialogView.passwordWrapper.isCounterEnabled = true
|
||||||
|
dialogView.passwordWrapper.counterMaxLength = 63
|
||||||
|
dialogView.password.filters = arrayOf(InputFilter.LengthFilter(63))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
validate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!arg.readOnly) dialogView.password.addTextChangedListener(this@WifiApDialogFragment)
|
if (!arg.readOnly) dialogView.password.addTextChangedListener(this@WifiApDialogFragment)
|
||||||
if (!arg.p2pMode && Build.VERSION.SDK_INT < 28) dialogView.autoShutdown.isGone = true
|
|
||||||
if (arg.p2pMode || Build.VERSION.SDK_INT >= 30) {
|
if (arg.p2pMode || Build.VERSION.SDK_INT >= 30) {
|
||||||
dialogView.timeoutWrapper.helperText = getString(R.string.wifi_hotspot_timeout_default,
|
dialogView.timeoutWrapper.helperText = getString(R.string.wifi_hotspot_timeout_default,
|
||||||
TetherTimeoutMonitor.defaultTimeout)
|
TetherTimeoutMonitor.defaultTimeout)
|
||||||
dialogView.timeout.addTextChangedListener(this@WifiApDialogFragment)
|
if (!arg.readOnly) dialogView.timeout.addTextChangedListener(this@WifiApDialogFragment)
|
||||||
} else dialogView.timeoutWrapper.isGone = true
|
} else dialogView.timeoutWrapper.isGone = true
|
||||||
fun Spinner.configure(options: List<ChannelOption>) {
|
fun Spinner.configure(options: List<ChannelOption>) {
|
||||||
adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0, options).apply {
|
adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0, options).apply {
|
||||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
}
|
}
|
||||||
onItemSelectedListener = this@WifiApDialogFragment
|
if (!arg.readOnly) onItemSelectedListener = this@WifiApDialogFragment
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) {
|
dialogView.bandPrimary.configure(currentChannels)
|
||||||
dialogView.band2G.configure(channels2G)
|
if (Build.VERSION.SDK_INT >= 31 && !arg.p2pMode) {
|
||||||
dialogView.band5G.configure(currentChannels5G)
|
dialogView.bandSecondary.configure(listOf(ChannelOption.Disabled) + currentChannels)
|
||||||
} else {
|
} else dialogView.bandSecondary.isGone = true
|
||||||
dialogView.bandWrapper2G.isGone = true
|
if (arg.p2pMode || Build.VERSION.SDK_INT < 30) dialogView.accessControlGroup.isGone = true
|
||||||
dialogView.bandWrapper5G.isGone = true
|
else if (!arg.readOnly) {
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT >= 30 && !arg.p2pMode) dialogView.band6G.configure(channels6G)
|
|
||||||
else dialogView.bandWrapper6G.isGone = true
|
|
||||||
if (BuildCompat.isAtLeastS() && !arg.p2pMode) dialogView.band60G.configure(channels60G)
|
|
||||||
else dialogView.bandWrapper60G.isGone = true
|
|
||||||
dialogView.bssid.addTextChangedListener(this@WifiApDialogFragment)
|
|
||||||
if (arg.p2pMode) dialogView.hiddenSsid.isGone = true
|
|
||||||
if (arg.p2pMode || Build.VERSION.SDK_INT < 30) {
|
|
||||||
dialogView.maxClientWrapper.isGone = true
|
|
||||||
dialogView.clientUserControl.isGone = true
|
|
||||||
dialogView.blockedListWrapper.isGone = true
|
|
||||||
dialogView.allowedListWrapper.isGone = true
|
|
||||||
} else {
|
|
||||||
dialogView.maxClient.addTextChangedListener(this@WifiApDialogFragment)
|
dialogView.maxClient.addTextChangedListener(this@WifiApDialogFragment)
|
||||||
dialogView.blockedList.addTextChangedListener(this@WifiApDialogFragment)
|
dialogView.blockedList.addTextChangedListener(this@WifiApDialogFragment)
|
||||||
dialogView.allowedList.addTextChangedListener(this@WifiApDialogFragment)
|
dialogView.allowedList.addTextChangedListener(this@WifiApDialogFragment)
|
||||||
}
|
}
|
||||||
|
if (!arg.readOnly) dialogView.bssid.addTextChangedListener(this@WifiApDialogFragment)
|
||||||
|
if (arg.p2pMode) dialogView.hiddenSsid.isGone = true
|
||||||
if (arg.p2pMode && Build.VERSION.SDK_INT >= 29) dialogView.macRandomization.isEnabled = false
|
if (arg.p2pMode && Build.VERSION.SDK_INT >= 29) dialogView.macRandomization.isEnabled = false
|
||||||
else if (arg.p2pMode || !BuildCompat.isAtLeastS()) dialogView.macRandomization.isGone = true
|
else if (arg.p2pMode || Build.VERSION.SDK_INT < 31) dialogView.macRandomizationWrapper.isGone = true
|
||||||
if (arg.p2pMode || !BuildCompat.isAtLeastS()) {
|
else dialogView.macRandomization.onItemSelectedListener = this@WifiApDialogFragment
|
||||||
dialogView.bridgedMode.isGone = true
|
if (arg.p2pMode || Build.VERSION.SDK_INT < 31) {
|
||||||
dialogView.bridgedModeOpportunisticShutdown.isGone = true
|
|
||||||
dialogView.ieee80211ax.isGone = true
|
dialogView.ieee80211ax.isGone = true
|
||||||
|
dialogView.bridgedModeOpportunisticShutdown.isGone = true
|
||||||
dialogView.userConfig.isGone = true
|
dialogView.userConfig.isGone = true
|
||||||
|
dialogView.bridgedTimeoutWrapper.isGone = true
|
||||||
|
} else {
|
||||||
|
dialogView.bridgedTimeoutWrapper.helperText = getString(R.string.wifi_hotspot_timeout_default,
|
||||||
|
TetherTimeoutMonitor.defaultTimeoutBridged)
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT < 33) dialogView.vendorElementsWrapper.isGone = true
|
||||||
|
else if (!arg.readOnly) dialogView.vendorElements.addTextChangedListener(this@WifiApDialogFragment)
|
||||||
|
if (arg.p2pMode || Build.VERSION.SDK_INT < 33) {
|
||||||
|
dialogView.ieee80211be.isGone = true
|
||||||
|
dialogView.bridgedTimeout.isEnabled = false
|
||||||
|
dialogView.persistentRandomizedMacWrapper.isGone = true
|
||||||
|
for ((_, _, wrapper) in acsList) wrapper.isGone = true
|
||||||
|
dialogView.maxChannelBandwidthWrapper.isGone = true
|
||||||
|
} else {
|
||||||
|
dialogView.maxChannelBandwidth.adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0,
|
||||||
|
bandWidthOptions).apply {
|
||||||
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
|
}
|
||||||
|
if (!arg.readOnly) {
|
||||||
|
dialogView.bridgedTimeout.addTextChangedListener(this@WifiApDialogFragment)
|
||||||
|
dialogView.persistentRandomizedMac.addTextChangedListener(this@WifiApDialogFragment)
|
||||||
|
for ((_, text, _) in acsList) text.addTextChangedListener(this@WifiApDialogFragment)
|
||||||
|
dialogView.acs5g.addTextChangedListener(this@WifiApDialogFragment)
|
||||||
|
dialogView.acs6g.addTextChangedListener(this@WifiApDialogFragment)
|
||||||
|
dialogView.maxChannelBandwidth.onItemSelectedListener = this@WifiApDialogFragment
|
||||||
|
}
|
||||||
}
|
}
|
||||||
base = arg.configuration
|
base = arg.configuration
|
||||||
populateFromConfiguration()
|
populateFromConfiguration()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun locate(band: Int, channels: List<ChannelOption>): Int {
|
private fun locate(i: Int): Int {
|
||||||
val channel = base.getChannel(band)
|
val band = base.channels.keyAt(i)
|
||||||
val selection = channels.indexOfFirst { it.channel == channel }
|
val channel = base.channels.valueAt(i)
|
||||||
|
val selection = currentChannels.indexOfFirst { it.band == band && it.channel == channel }
|
||||||
return if (selection == -1) {
|
return if (selection == -1) {
|
||||||
Timber.w(Exception("Unable to locate $band, $channel, ${arg.p2pMode && !RepeaterService.safeMode}"))
|
val msg = "Unable to locate $band, $channel, ${arg.p2pMode && !RepeaterService.safeMode}"
|
||||||
|
if (pasted || arg.p2pMode) Timber.w(msg) else Timber.w(Exception(msg))
|
||||||
0
|
0
|
||||||
} else selection
|
} else selection
|
||||||
}
|
}
|
||||||
private var userBridgedMode = false
|
|
||||||
private fun setBridgedMode() {
|
|
||||||
var auto = 0
|
|
||||||
var set = 0
|
|
||||||
for (s in arrayOf(dialogView.band2G, dialogView.band5G, dialogView.band6G)) when (s.selectedItem) {
|
|
||||||
is ChannelOption.Auto -> auto = 1
|
|
||||||
!is ChannelOption.Disabled -> ++set
|
|
||||||
}
|
|
||||||
if (auto + set > 1) {
|
|
||||||
if (dialogView.bridgedMode.isEnabled) {
|
|
||||||
userBridgedMode = dialogView.bridgedMode.isChecked
|
|
||||||
dialogView.bridgedMode.isEnabled = false
|
|
||||||
dialogView.bridgedMode.isChecked = true
|
|
||||||
}
|
|
||||||
} else if (!dialogView.bridgedMode.isEnabled) {
|
|
||||||
dialogView.bridgedMode.isEnabled = true
|
|
||||||
dialogView.bridgedMode.isChecked = userBridgedMode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun populateFromConfiguration() {
|
private fun populateFromConfiguration() {
|
||||||
dialogView.ssid.setText(base.ssid)
|
dialogView.ssid.setText(base.ssid.let { ssid ->
|
||||||
|
when {
|
||||||
|
ssid == null -> null
|
||||||
|
hexSsid -> ssid.hex
|
||||||
|
hexToggleable -> ssid.decode() ?: ssid.hex.also { hexSsid = true }
|
||||||
|
else -> ssid.toString()
|
||||||
|
}
|
||||||
|
})
|
||||||
if (!arg.p2pMode) dialogView.security.setSelection(base.securityType)
|
if (!arg.p2pMode) dialogView.security.setSelection(base.securityType)
|
||||||
dialogView.password.setText(base.passphrase)
|
dialogView.password.setText(base.passphrase)
|
||||||
dialogView.autoShutdown.isChecked = base.isAutoShutdownEnabled
|
dialogView.autoShutdown.isChecked = base.isAutoShutdownEnabled
|
||||||
dialogView.timeout.setText(base.shutdownTimeoutMillis.let { if (it == 0L) "" else it.toString() })
|
dialogView.timeout.setText(base.shutdownTimeoutMillis.let { if (it <= 0) "" else it.toString() })
|
||||||
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) {
|
dialogView.bandPrimary.setSelection(locate(0))
|
||||||
dialogView.band2G.setSelection(locate(SoftApConfigurationCompat.BAND_2GHZ, channels2G))
|
if (Build.VERSION.SDK_INT >= 31 && !arg.p2pMode) {
|
||||||
dialogView.band5G.setSelection(locate(SoftApConfigurationCompat.BAND_5GHZ, currentChannels5G))
|
dialogView.bandSecondary.setSelection(if (base.channels.size() > 1) locate(1) + 1 else 0)
|
||||||
dialogView.band6G.setSelection(locate(SoftApConfigurationCompat.BAND_6GHZ, channels6G))
|
|
||||||
dialogView.band60G.setSelection(locate(SoftApConfigurationCompat.BAND_60GHZ, channels60G))
|
|
||||||
userBridgedMode = base.channels.size() > 1
|
|
||||||
dialogView.bridgedMode.isChecked = userBridgedMode
|
|
||||||
setBridgedMode()
|
|
||||||
}
|
}
|
||||||
dialogView.bssid.setText(base.bssid?.toString())
|
dialogView.bssid.setText(base.bssid?.toString())
|
||||||
dialogView.hiddenSsid.isChecked = base.isHiddenSsid
|
dialogView.hiddenSsid.isChecked = base.isHiddenSsid
|
||||||
@@ -257,11 +348,22 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
|||||||
dialogView.clientUserControl.isChecked = base.isClientControlByUserEnabled
|
dialogView.clientUserControl.isChecked = base.isClientControlByUserEnabled
|
||||||
dialogView.blockedList.setText(base.blockedClientList.joinToString("\n"))
|
dialogView.blockedList.setText(base.blockedClientList.joinToString("\n"))
|
||||||
dialogView.allowedList.setText(base.allowedClientList.joinToString("\n"))
|
dialogView.allowedList.setText(base.allowedClientList.joinToString("\n"))
|
||||||
dialogView.macRandomization.isChecked =
|
dialogView.macRandomization.setSelection(base.macRandomizationSetting)
|
||||||
base.macRandomizationSetting == SoftApConfigurationCompat.RANDOMIZATION_PERSISTENT
|
|
||||||
dialogView.bridgedModeOpportunisticShutdown.isChecked = base.isBridgedModeOpportunisticShutdownEnabled
|
dialogView.bridgedModeOpportunisticShutdown.isChecked = base.isBridgedModeOpportunisticShutdownEnabled
|
||||||
dialogView.ieee80211ax.isChecked = base.isIeee80211axEnabled
|
dialogView.ieee80211ax.isChecked = base.isIeee80211axEnabled
|
||||||
|
dialogView.ieee80211be.isChecked = base.isIeee80211beEnabled
|
||||||
dialogView.userConfig.isChecked = base.isUserConfiguration
|
dialogView.userConfig.isChecked = base.isUserConfiguration
|
||||||
|
dialogView.bridgedTimeout.setText(base.bridgedModeOpportunisticShutdownTimeoutMillis.let {
|
||||||
|
if (it == -1L) "" else it.toString()
|
||||||
|
})
|
||||||
|
dialogView.vendorElements.setText(VendorElements.serialize(base.vendorElements))
|
||||||
|
dialogView.persistentRandomizedMac.setText(base.persistentRandomizedMacAddress?.toString())
|
||||||
|
for ((band, text, _) in acsList) text.setText(RangeInput.toString(base.allowedAcsChannels[band]))
|
||||||
|
if (Build.VERSION.SDK_INT >= 33) bandWidthOptions.binarySearch(BandWidth(base.maxChannelBandwidth)).let {
|
||||||
|
if (it < 0) {
|
||||||
|
Timber.w(Exception("Cannot locate bandwidth ${base.maxChannelBandwidth}"))
|
||||||
|
} else dialogView.maxChannelBandwidth.setSelection(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
@@ -270,64 +372,62 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
|||||||
validate()
|
validate()
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(28)
|
|
||||||
private fun validate() {
|
private fun validate() {
|
||||||
if (!started) return
|
if (!started) return
|
||||||
val ssidLength = dialogView.ssid.text.toString().toByteArray().size
|
val (ssidOk, ssidError) = 0.let {
|
||||||
dialogView.ssidWrapper.error = if (arg.p2pMode && RepeaterService.safeMode && ssidLength < 9) {
|
val ssid = try {
|
||||||
requireContext().getString(R.string.settings_service_repeater_safe_mode_warning)
|
ssid
|
||||||
} else null
|
} catch (e: IllegalArgumentException) {
|
||||||
|
return@let false to e.readableMessage
|
||||||
|
}
|
||||||
|
val ssidLength = ssid?.bytes?.size ?: 0
|
||||||
|
if (ssidLength in 1..32) true to if (arg.p2pMode && RepeaterService.safeMode && ssidLength < 9) {
|
||||||
|
requireContext().getString(R.string.settings_service_repeater_safe_mode_warning)
|
||||||
|
} else null else false to " "
|
||||||
|
}
|
||||||
|
dialogView.ssidWrapper.error = ssidError
|
||||||
val selectedSecurity = if (arg.p2pMode) {
|
val selectedSecurity = if (arg.p2pMode) {
|
||||||
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
|
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
|
||||||
} else dialogView.security.selectedItemPosition
|
} else dialogView.security.selectedItemPosition
|
||||||
// see also: https://android.googlesource.com/platform/frameworks/base/+/92c8f59/wifi/java/android/net/wifi/SoftApConfiguration.java#688
|
// see also: https://android.googlesource.com/platform/frameworks/base/+/92c8f59/wifi/java/android/net/wifi/SoftApConfiguration.java#688
|
||||||
val passwordValid = when (selectedSecurity) {
|
val passwordValid = when (selectedSecurity) {
|
||||||
|
SoftApConfiguration.SECURITY_TYPE_OPEN,
|
||||||
|
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE_TRANSITION,
|
||||||
|
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE -> true
|
||||||
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION -> {
|
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION -> {
|
||||||
dialogView.password.length() >= 8
|
dialogView.password.length() in 8..63
|
||||||
}
|
}
|
||||||
else -> true // do not try to validate
|
else -> dialogView.password.length() > 0
|
||||||
}
|
}
|
||||||
dialogView.passwordWrapper.error = if (passwordValid) null else " "
|
dialogView.passwordWrapper.error = if (passwordValid) null else " "
|
||||||
val timeoutError = dialogView.timeout.text.let { text ->
|
val timeoutError = dialogView.timeout.text.let { text ->
|
||||||
if (text.isNullOrEmpty()) null else try {
|
if (text.isNullOrEmpty()) null else try {
|
||||||
text.toString().toLong()
|
SoftApConfigurationCompat.testPlatformTimeoutValidity(text.toString().toLong())
|
||||||
null
|
null
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: Exception) {
|
||||||
e.readableMessage
|
e.readableMessage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dialogView.timeoutWrapper.error = timeoutError
|
dialogView.timeoutWrapper.error = timeoutError
|
||||||
val isBandValid = when {
|
val bandError = if (!arg.p2pMode && Build.VERSION.SDK_INT >= 30) {
|
||||||
arg.p2pMode || Build.VERSION.SDK_INT in 23 until 30 -> {
|
try {
|
||||||
val option5G = dialogView.band5G.selectedItem
|
SoftApConfigurationCompat.testPlatformValidity(generateChannels())
|
||||||
when (dialogView.band2G.selectedItem) {
|
null
|
||||||
is ChannelOption.Disabled -> option5G !is ChannelOption.Disabled &&
|
} catch (e: Exception) {
|
||||||
(!arg.p2pMode || RepeaterService.safeMode || option5G !is ChannelOption.Auto)
|
e.readableMessage
|
||||||
is ChannelOption.Auto ->
|
|
||||||
(arg.p2pMode || Build.VERSION.SDK_INT >= 28) && option5G is ChannelOption.Auto ||
|
|
||||||
(!arg.p2pMode || RepeaterService.safeMode) && option5G is ChannelOption.Disabled
|
|
||||||
else -> option5G is ChannelOption.Disabled
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Build.VERSION.SDK_INT == 30 && !BuildCompat.isAtLeastS() -> {
|
} else null
|
||||||
var expected = 1
|
dialogView.bandError.isGone = bandError.isNullOrEmpty()
|
||||||
var set = 0
|
dialogView.bandError.text = bandError
|
||||||
for (s in arrayOf(dialogView.band2G, dialogView.band5G, dialogView.band6G)) when (s.selectedItem) {
|
val hideBssid = !arg.p2pMode && Build.VERSION.SDK_INT >= 31 &&
|
||||||
is ChannelOption.Auto -> expected = 0
|
dialogView.macRandomization.selectedItemPosition != SoftApConfigurationCompat.RANDOMIZATION_NONE
|
||||||
!is ChannelOption.Disabled -> ++set
|
dialogView.bssidWrapper.isGone = hideBssid
|
||||||
}
|
|
||||||
set == expected
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
setBridgedMode()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dialogView.bssidWrapper.error = null
|
dialogView.bssidWrapper.error = null
|
||||||
val bssidValid = dialogView.bssid.length() == 0 || try {
|
val bssidValid = hideBssid || dialogView.bssid.length() == 0 || try {
|
||||||
MacAddressCompat.fromString(dialogView.bssid.text.toString())
|
val mac = MacAddress.fromString(dialogView.bssid.text.toString())
|
||||||
|
if (Build.VERSION.SDK_INT >= 30 && !arg.p2pMode) SoftApConfigurationCompat.testPlatformValidity(mac)
|
||||||
true
|
true
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: Exception) {
|
||||||
dialogView.bssidWrapper.error = e.readableMessage
|
dialogView.bssidWrapper.error = e.readableMessage
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -340,26 +440,80 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
dialogView.maxClientWrapper.error = maxClientError
|
dialogView.maxClientWrapper.error = maxClientError
|
||||||
val blockedListError = try {
|
val listsNoError = if (Build.VERSION.SDK_INT >= 30) {
|
||||||
(dialogView.blockedList.text ?: "").split(nonMacChars)
|
val (blockedList, blockedListError) = try {
|
||||||
.filter { it.isNotEmpty() }.forEach { MacAddressCompat.fromString(it).toPlatform() }
|
(dialogView.blockedList.text ?: "").split(nonMacChars).filter { it.isNotEmpty() }
|
||||||
null
|
.map(MacAddress::fromString).toSet() to null
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
e.readableMessage
|
null to e.readableMessage
|
||||||
|
}
|
||||||
|
dialogView.blockedListWrapper.error = blockedListError
|
||||||
|
val allowedListError = try {
|
||||||
|
(dialogView.allowedList.text ?: "").split(nonMacChars).filter { it.isNotEmpty() }.forEach {
|
||||||
|
val mac = MacAddress.fromString(it)
|
||||||
|
require(blockedList?.contains(mac) != true) { "A MAC address exists in both client lists" }
|
||||||
|
}
|
||||||
|
null
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
e.readableMessage
|
||||||
|
}
|
||||||
|
dialogView.allowedListWrapper.error = allowedListError
|
||||||
|
blockedListError == null && allowedListError == null
|
||||||
|
} else true
|
||||||
|
val bridgedTimeoutError = dialogView.bridgedTimeout.text.let { text ->
|
||||||
|
if (text.isNullOrEmpty()) null else try {
|
||||||
|
SoftApConfigurationCompat.testPlatformBridgedTimeoutValidity(text.toString().toLong())
|
||||||
|
null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.readableMessage
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dialogView.blockedListWrapper.error = blockedListError
|
dialogView.bridgedTimeoutWrapper.error = bridgedTimeoutError
|
||||||
val allowedListError = try {
|
val vendorElementsError = if (Build.VERSION.SDK_INT >= 33) {
|
||||||
(dialogView.allowedList.text ?: "").split(nonMacChars)
|
try {
|
||||||
.filter { it.isNotEmpty() }.forEach { MacAddressCompat.fromString(it).toPlatform() }
|
VendorElements.deserialize(dialogView.vendorElements.text).also {
|
||||||
null
|
if (!arg.p2pMode) SoftApConfigurationCompat.testPlatformValidity(it)
|
||||||
|
}
|
||||||
|
null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.readableMessage
|
||||||
|
}
|
||||||
|
} else null
|
||||||
|
dialogView.vendorElementsWrapper.error = vendorElementsError
|
||||||
|
dialogView.persistentRandomizedMacWrapper.error = null
|
||||||
|
val persistentRandomizedMacValid = dialogView.persistentRandomizedMac.length() == 0 || try {
|
||||||
|
MacAddress.fromString(dialogView.persistentRandomizedMac.text.toString())
|
||||||
|
true
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
e.readableMessage
|
dialogView.persistentRandomizedMacWrapper.error = e.readableMessage
|
||||||
|
false
|
||||||
}
|
}
|
||||||
dialogView.allowedListWrapper.error = allowedListError
|
val acsNoError = if (!arg.p2pMode && Build.VERSION.SDK_INT >= 33) acsList.all { (band, text, wrapper) ->
|
||||||
val canCopy = timeoutError == null && bssidValid && maxClientError == null && blockedListError == null &&
|
try {
|
||||||
allowedListError == null
|
wrapper.error = null
|
||||||
|
SoftApConfigurationCompat.testPlatformValidity(band, RangeInput.fromString(text.text).toIntArray())
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
wrapper.error = e.readableMessage
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else true
|
||||||
|
val bandwidthError = if (!arg.p2pMode && Build.VERSION.SDK_INT >= 33) {
|
||||||
|
try {
|
||||||
|
SoftApConfigurationCompat.testPlatformValidity(
|
||||||
|
(dialogView.maxChannelBandwidth.selectedItem as BandWidth).width)
|
||||||
|
null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.readableMessage
|
||||||
|
}
|
||||||
|
} else null
|
||||||
|
dialogView.maxChannelBandwidthError.isGone = bandwidthError.isNullOrEmpty()
|
||||||
|
dialogView.maxChannelBandwidthError.text = bandwidthError
|
||||||
|
val canCopy = timeoutError == null && bssidValid && maxClientError == null && listsNoError &&
|
||||||
|
bridgedTimeoutError == null && vendorElementsError == null && persistentRandomizedMacValid &&
|
||||||
|
acsNoError && bandwidthError == null
|
||||||
(dialog as? AlertDialog)?.getButton(DialogInterface.BUTTON_POSITIVE)?.isEnabled =
|
(dialog as? AlertDialog)?.getButton(DialogInterface.BUTTON_POSITIVE)?.isEnabled =
|
||||||
ssidLength in 1..32 && passwordValid && isBandValid && canCopy
|
ssidOk && passwordValid && bandError == null && canCopy
|
||||||
dialogView.toolbar.menu.findItem(android.R.id.copy).isEnabled = canCopy
|
dialogView.toolbar.menu.findItem(android.R.id.copy).isEnabled = canCopy
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,10 +526,15 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
|||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
||||||
return when (item?.itemId) {
|
return when (item?.itemId) {
|
||||||
android.R.id.copy -> {
|
android.R.id.copy -> try {
|
||||||
app.clipboard.setPrimaryClip(ClipData.newPlainText(null,
|
app.clipboard.setPrimaryClip(ClipData.newPlainText(null,
|
||||||
Base64.encodeToString(generateConfig().toByteArray(), BASE64_FLAGS)))
|
Base64.encodeToString(generateConfig().toByteArray(), BASE64_FLAGS)).apply {
|
||||||
|
description.extras = persistableBundleOf(ClipDescription.EXTRA_IS_SENSITIVE to true)
|
||||||
|
})
|
||||||
true
|
true
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
Toast.makeText(context, e.readableMessage, Toast.LENGTH_LONG).show()
|
||||||
|
false
|
||||||
}
|
}
|
||||||
android.R.id.paste -> try {
|
android.R.id.paste -> try {
|
||||||
app.clipboard.primaryClip?.getItemAt(0)?.text?.apply {
|
app.clipboard.primaryClip?.getItemAt(0)?.text?.apply {
|
||||||
@@ -385,12 +544,13 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
|||||||
arg.configuration.underlying?.let { check(it.javaClass == newUnderlying.javaClass) }
|
arg.configuration.underlying?.let { check(it.javaClass == newUnderlying.javaClass) }
|
||||||
} else config.underlying = arg.configuration.underlying
|
} else config.underlying = arg.configuration.underlying
|
||||||
base = config
|
base = config
|
||||||
|
pasted = true
|
||||||
populateFromConfiguration()
|
populateFromConfiguration()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: RuntimeException) {
|
||||||
SmartSnackbar.make(e).show()
|
Toast.makeText(context, e.readableMessage, Toast.LENGTH_LONG).show()
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
R.id.share_qr -> {
|
R.id.share_qr -> {
|
||||||
|
|||||||
@@ -10,13 +10,8 @@ import android.os.Build
|
|||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.os.BuildCompat
|
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
|
import be.mygod.vpnhotspot.util.*
|
||||||
import be.mygod.vpnhotspot.util.ConstantLookup
|
|
||||||
import be.mygod.vpnhotspot.util.Services
|
|
||||||
import be.mygod.vpnhotspot.util.callSuper
|
|
||||||
import be.mygod.vpnhotspot.util.findIdentifier
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.lang.reflect.InvocationHandler
|
import java.lang.reflect.InvocationHandler
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
@@ -39,17 +34,22 @@ object WifiApManager {
|
|||||||
PackageManager.MATCH_SYSTEM_ONLY).single()
|
PackageManager.MATCH_SYSTEM_ONLY).single()
|
||||||
|
|
||||||
private const val CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED = "config_wifi_p2p_mac_randomization_supported"
|
private const val CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED = "config_wifi_p2p_mac_randomization_supported"
|
||||||
val p2pMacRandomizationSupported get() = when (Build.VERSION.SDK_INT) {
|
val p2pMacRandomizationSupported get() = try {
|
||||||
29 -> Resources.getSystem().run {
|
when (Build.VERSION.SDK_INT) {
|
||||||
getBoolean(getIdentifier(CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED, "bool", "android"))
|
29 -> Resources.getSystem().run {
|
||||||
|
getBoolean(getIdentifier(CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED, "bool", "android"))
|
||||||
|
}
|
||||||
|
in 30..Int.MAX_VALUE -> @TargetApi(30) {
|
||||||
|
val info = resolvedActivity.activityInfo
|
||||||
|
val resources = app.packageManager.getResourcesForApplication(info.applicationInfo)
|
||||||
|
resources.getBoolean(resources.findIdentifier(CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED, "bool",
|
||||||
|
RESOURCES_PACKAGE, info.packageName))
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
}
|
}
|
||||||
in 30..Int.MAX_VALUE -> @TargetApi(30) {
|
} catch (e: RuntimeException) {
|
||||||
val info = resolvedActivity.activityInfo
|
Timber.w(e)
|
||||||
val resources = app.packageManager.getResourcesForApplication(info.applicationInfo)
|
false
|
||||||
resources.getBoolean(resources.findIdentifier(CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED, "bool",
|
|
||||||
RESOURCES_PACKAGE, info.packageName))
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
@@ -59,6 +59,92 @@ object WifiApManager {
|
|||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
val isApMacRandomizationSupported get() = apMacRandomizationSupported(Services.wifi) as Boolean
|
val isApMacRandomizationSupported get() = apMacRandomizationSupported(Services.wifi) as Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast intent action indicating that Wi-Fi AP has been enabled, disabled,
|
||||||
|
* enabling, disabling, or failed.
|
||||||
|
*/
|
||||||
|
const val WIFI_AP_STATE_CHANGED_ACTION = "android.net.wifi.WIFI_AP_STATE_CHANGED"
|
||||||
|
/**
|
||||||
|
* The lookup key for an int that indicates whether Wi-Fi AP is enabled,
|
||||||
|
* disabled, enabling, disabling, or failed. Retrieve it with [Intent.getIntExtra].
|
||||||
|
*
|
||||||
|
* @see WIFI_AP_STATE_DISABLED
|
||||||
|
* @see WIFI_AP_STATE_DISABLING
|
||||||
|
* @see WIFI_AP_STATE_ENABLED
|
||||||
|
* @see WIFI_AP_STATE_ENABLING
|
||||||
|
* @see WIFI_AP_STATE_FAILED
|
||||||
|
*/
|
||||||
|
private const val EXTRA_WIFI_AP_STATE = "wifi_state"
|
||||||
|
/**
|
||||||
|
* An extra containing the int error code for Soft AP start failure.
|
||||||
|
* Can be obtained from the [WIFI_AP_STATE_CHANGED_ACTION] using [Intent.getIntExtra].
|
||||||
|
* This extra will only be attached if [EXTRA_WIFI_AP_STATE] is
|
||||||
|
* attached and is equal to [WIFI_AP_STATE_FAILED].
|
||||||
|
*
|
||||||
|
* The error code will be one of:
|
||||||
|
* {@link #SAP_START_FAILURE_GENERAL},
|
||||||
|
* {@link #SAP_START_FAILURE_NO_CHANNEL},
|
||||||
|
* {@link #SAP_START_FAILURE_UNSUPPORTED_CONFIGURATION}
|
||||||
|
*
|
||||||
|
* Source: https://android.googlesource.com/platform/frameworks/base/+/android-6.0.0_r1/wifi/java/android/net/wifi/WifiManager.java#210
|
||||||
|
*/
|
||||||
|
val EXTRA_WIFI_AP_FAILURE_REASON get() =
|
||||||
|
if (Build.VERSION.SDK_INT >= 30) "android.net.wifi.extra.WIFI_AP_FAILURE_REASON" else "wifi_ap_error_code"
|
||||||
|
/**
|
||||||
|
* The lookup key for a String extra that stores the interface name used for the Soft AP.
|
||||||
|
* This extra is included in the broadcast [WIFI_AP_STATE_CHANGED_ACTION].
|
||||||
|
* Retrieve its value with [Intent.getStringExtra].
|
||||||
|
*
|
||||||
|
* Source: https://android.googlesource.com/platform/frameworks/base/+/android-8.0.0_r1/wifi/java/android/net/wifi/WifiManager.java#413
|
||||||
|
*/
|
||||||
|
val EXTRA_WIFI_AP_INTERFACE_NAME get() =
|
||||||
|
if (Build.VERSION.SDK_INT >= 30) "android.net.wifi.extra.WIFI_AP_INTERFACE_NAME" else "wifi_ap_interface_name"
|
||||||
|
|
||||||
|
fun checkWifiApState(state: Int) = if (state < WIFI_AP_STATE_DISABLING || state > WIFI_AP_STATE_FAILED) {
|
||||||
|
Timber.w(Exception("Unknown state $state"))
|
||||||
|
false
|
||||||
|
} else true
|
||||||
|
val Intent.wifiApState get() =
|
||||||
|
getIntExtra(EXTRA_WIFI_AP_STATE, WIFI_AP_STATE_DISABLED).also { checkWifiApState(it) }
|
||||||
|
/**
|
||||||
|
* Wi-Fi AP is currently being disabled. The state will change to
|
||||||
|
* [WIFI_AP_STATE_DISABLED] if it finishes successfully.
|
||||||
|
*
|
||||||
|
* @see WIFI_AP_STATE_CHANGED_ACTION
|
||||||
|
* @see #getWifiApState()
|
||||||
|
*/
|
||||||
|
const val WIFI_AP_STATE_DISABLING = 10
|
||||||
|
/**
|
||||||
|
* Wi-Fi AP is disabled.
|
||||||
|
*
|
||||||
|
* @see WIFI_AP_STATE_CHANGED_ACTION
|
||||||
|
* @see #getWifiState()
|
||||||
|
*/
|
||||||
|
const val WIFI_AP_STATE_DISABLED = 11
|
||||||
|
/**
|
||||||
|
* Wi-Fi AP is currently being enabled. The state will change to
|
||||||
|
* {@link #WIFI_AP_STATE_ENABLED} if it finishes successfully.
|
||||||
|
*
|
||||||
|
* @see WIFI_AP_STATE_CHANGED_ACTION
|
||||||
|
* @see #getWifiApState()
|
||||||
|
*/
|
||||||
|
const val WIFI_AP_STATE_ENABLING = 12
|
||||||
|
/**
|
||||||
|
* Wi-Fi AP is enabled.
|
||||||
|
*
|
||||||
|
* @see WIFI_AP_STATE_CHANGED_ACTION
|
||||||
|
* @see #getWifiApState()
|
||||||
|
*/
|
||||||
|
const val WIFI_AP_STATE_ENABLED = 13
|
||||||
|
/**
|
||||||
|
* Wi-Fi AP is in a failed state. This state will occur when an error occurs during
|
||||||
|
* enabling or disabling
|
||||||
|
*
|
||||||
|
* @see WIFI_AP_STATE_CHANGED_ACTION
|
||||||
|
* @see #getWifiApState()
|
||||||
|
*/
|
||||||
|
const val WIFI_AP_STATE_FAILED = 14
|
||||||
|
|
||||||
private val getWifiApConfiguration by lazy { WifiManager::class.java.getDeclaredMethod("getWifiApConfiguration") }
|
private val getWifiApConfiguration by lazy { WifiManager::class.java.getDeclaredMethod("getWifiApConfiguration") }
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
private val setWifiApConfiguration by lazy {
|
private val setWifiApConfiguration by lazy {
|
||||||
@@ -72,29 +158,29 @@ object WifiApManager {
|
|||||||
WifiManager::class.java.getDeclaredMethod("setSoftApConfiguration", SoftApConfiguration::class.java)
|
WifiManager::class.java.getDeclaredMethod("setSoftApConfiguration", SoftApConfiguration::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
@get:RequiresApi(30)
|
|
||||||
val configuration get() = getSoftApConfiguration(Services.wifi) as SoftApConfiguration
|
|
||||||
/**
|
/**
|
||||||
* Requires NETWORK_SETTINGS permission (or root) on API 30+, and OVERRIDE_WIFI_CONFIG on API 29-.
|
* Requires NETWORK_SETTINGS permission (or root) on API 30+, and OVERRIDE_WIFI_CONFIG on API 29-.
|
||||||
*/
|
*/
|
||||||
val configurationCompat get() = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
|
@Deprecated("Use configuration instead", ReplaceWith("configuration"))
|
||||||
(getWifiApConfiguration(Services.wifi) as android.net.wifi.WifiConfiguration?)?.toCompat()
|
@Suppress("DEPRECATION")
|
||||||
?: SoftApConfigurationCompat()
|
val configurationLegacy get() = getWifiApConfiguration(Services.wifi) as android.net.wifi.WifiConfiguration?
|
||||||
} else configuration.toCompat()
|
/**
|
||||||
fun setConfigurationCompat(value: SoftApConfigurationCompat) = (if (Build.VERSION.SDK_INT >= 30) {
|
* Requires NETWORK_SETTINGS permission (or root).
|
||||||
setSoftApConfiguration(Services.wifi, value.toPlatform())
|
*/
|
||||||
} else @Suppress("DEPRECATION") {
|
@get:RequiresApi(30)
|
||||||
setWifiApConfiguration(Services.wifi, value.toWifiConfiguration())
|
val configuration get() = getSoftApConfiguration(Services.wifi) as SoftApConfiguration
|
||||||
}) as Boolean
|
@Deprecated("Use SoftApConfiguration instead")
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
fun setConfiguration(value: android.net.wifi.WifiConfiguration?) =
|
||||||
|
setWifiApConfiguration(Services.wifi, value) as Boolean
|
||||||
|
fun setConfiguration(value: SoftApConfiguration) = setSoftApConfiguration(Services.wifi, value) as Boolean
|
||||||
|
|
||||||
@RequiresApi(28)
|
|
||||||
interface SoftApCallbackCompat {
|
interface SoftApCallbackCompat {
|
||||||
/**
|
/**
|
||||||
* Called when soft AP state changes.
|
* Called when soft AP state changes.
|
||||||
*
|
*
|
||||||
* @param state the new AP state. One of {@link #WIFI_AP_STATE_DISABLED},
|
* @param state the new AP state. One of [WIFI_AP_STATE_DISABLED], [WIFI_AP_STATE_DISABLING],
|
||||||
* {@link #WIFI_AP_STATE_DISABLING}, {@link #WIFI_AP_STATE_ENABLED},
|
* [WIFI_AP_STATE_ENABLED], [WIFI_AP_STATE_ENABLING], [WIFI_AP_STATE_FAILED]
|
||||||
* {@link #WIFI_AP_STATE_ENABLING}, {@link #WIFI_AP_STATE_FAILED}
|
|
||||||
* @param failureReason reason when in failed state. One of
|
* @param failureReason reason when in failed state. One of
|
||||||
* {@link #SAP_START_FAILURE_GENERAL},
|
* {@link #SAP_START_FAILURE_GENERAL},
|
||||||
* {@link #SAP_START_FAILURE_NO_CHANNEL},
|
* {@link #SAP_START_FAILURE_NO_CHANNEL},
|
||||||
@@ -150,7 +236,6 @@ object WifiApManager {
|
|||||||
@RequiresApi(30)
|
@RequiresApi(30)
|
||||||
fun onBlockedClientConnecting(client: Parcelable, blockedReason: Int) { }
|
fun onBlockedClientConnecting(client: Parcelable, blockedReason: Int) { }
|
||||||
}
|
}
|
||||||
@RequiresApi(28)
|
|
||||||
val failureReasonLookup = ConstantLookup<WifiManager>("SAP_START_FAILURE_", "GENERAL", "NO_CHANNEL")
|
val failureReasonLookup = ConstantLookup<WifiManager>("SAP_START_FAILURE_", "GENERAL", "NO_CHANNEL")
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
val clientBlockLookup by lazy { ConstantLookup<WifiManager>("SAP_CLIENT_") }
|
val clientBlockLookup by lazy { ConstantLookup<WifiManager>("SAP_CLIENT_") }
|
||||||
@@ -166,7 +251,6 @@ object WifiApManager {
|
|||||||
WifiManager::class.java.getDeclaredMethod("unregisterSoftApCallback", interfaceSoftApCallback)
|
WifiManager::class.java.getDeclaredMethod("unregisterSoftApCallback", interfaceSoftApCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(28)
|
|
||||||
fun registerSoftApCallback(callback: SoftApCallbackCompat, executor: Executor): Any {
|
fun registerSoftApCallback(callback: SoftApCallbackCompat, executor: Executor): Any {
|
||||||
val proxy = Proxy.newProxyInstance(interfaceSoftApCallback.classLoader,
|
val proxy = Proxy.newProxyInstance(interfaceSoftApCallback.classLoader,
|
||||||
arrayOf(interfaceSoftApCallback), object : InvocationHandler {
|
arrayOf(interfaceSoftApCallback), object : InvocationHandler {
|
||||||
@@ -177,55 +261,36 @@ object WifiApManager {
|
|||||||
} else invokeActual(proxy, method, args)
|
} else invokeActual(proxy, method, args)
|
||||||
|
|
||||||
private fun invokeActual(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
|
private fun invokeActual(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
|
||||||
val noArgs = args?.size ?: 0
|
return when {
|
||||||
return when (val name = method.name) {
|
method.matches("onStateChanged", Integer.TYPE, Integer.TYPE) -> {
|
||||||
"onStateChanged" -> {
|
|
||||||
if (noArgs != 2) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
|
|
||||||
callback.onStateChanged(args!![0] as Int, args[1] as Int)
|
callback.onStateChanged(args!![0] as Int, args[1] as Int)
|
||||||
}
|
}
|
||||||
"onNumClientsChanged" -> @Suppress("DEPRECATION") {
|
method.matches("onNumClientsChanged", Integer.TYPE) -> {
|
||||||
if (Build.VERSION.SDK_INT >= 30) Timber.w(Exception("Unexpected onNumClientsChanged"))
|
if (Build.VERSION.SDK_INT >= 30) Timber.w(Exception("Unexpected onNumClientsChanged"))
|
||||||
if (noArgs != 1) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
|
|
||||||
callback.onNumClientsChanged(args!![0] as Int)
|
callback.onNumClientsChanged(args!![0] as Int)
|
||||||
}
|
}
|
||||||
"onConnectedClientsChanged" -> @TargetApi(30) {
|
method.matches1<java.util.List<*>>("onConnectedClientsChanged") -> @TargetApi(30) {
|
||||||
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onConnectedClientsChanged"))
|
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onConnectedClientsChanged"))
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
when (noArgs) {
|
callback.onConnectedClientsChanged(args!![0] as List<Parcelable>)
|
||||||
1 -> callback.onConnectedClientsChanged(args!![0] as List<Parcelable>)
|
|
||||||
2 -> null // we use the old method which returns all clients in one call
|
|
||||||
else -> {
|
|
||||||
Timber.w("Unexpected args for $name: ${args?.contentToString()}")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"onInfoChanged" -> @TargetApi(30) {
|
method.matches1<java.util.List<*>>("onInfoChanged") -> @TargetApi(31) {
|
||||||
if (noArgs != 1) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
|
if (Build.VERSION.SDK_INT < 31) Timber.w(Exception("Unexpected onInfoChanged API 31+"))
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
callback.onInfoChanged(args!![0] as List<Parcelable>)
|
||||||
|
}
|
||||||
|
Build.VERSION.SDK_INT >= 30 && method.matches("onInfoChanged", SoftApInfo.clazz) -> {
|
||||||
|
if (Build.VERSION.SDK_INT >= 31) return null // ignore old version calls
|
||||||
val arg = args!![0]
|
val arg = args!![0]
|
||||||
if (arg is List<*>) {
|
val info = SoftApInfo(arg as Parcelable)
|
||||||
if (!BuildCompat.isAtLeastS()) Timber.w(Exception("Unexpected onInfoChanged API 31+"))
|
callback.onInfoChanged(if (info.frequency == 0 && info.bandwidth ==
|
||||||
@Suppress("UNCHECKED_CAST")
|
SoftApConfigurationCompat.CHANNEL_WIDTH_INVALID) emptyList() else listOf(arg))
|
||||||
callback.onInfoChanged(arg as List<Parcelable>)
|
|
||||||
} else {
|
|
||||||
when (Build.VERSION.SDK_INT) {
|
|
||||||
30 -> { }
|
|
||||||
in 31..Int.MAX_VALUE -> return null // ignore old version calls
|
|
||||||
else -> Timber.w(Exception("Unexpected onInfoChanged API 30"))
|
|
||||||
}
|
|
||||||
val info = SoftApInfo(arg as Parcelable)
|
|
||||||
callback.onInfoChanged( // check for legacy empty info with CHANNEL_WIDTH_INVALID
|
|
||||||
if (info.frequency == 0 && info.bandwidth == 0) emptyList() else listOf(arg))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"onCapabilityChanged" -> @TargetApi(30) {
|
Build.VERSION.SDK_INT >= 30 && method.matches("onCapabilityChanged", SoftApCapability.clazz) -> {
|
||||||
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onCapabilityChanged"))
|
|
||||||
if (noArgs != 1) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
|
|
||||||
callback.onCapabilityChanged(args!![0] as Parcelable)
|
callback.onCapabilityChanged(args!![0] as Parcelable)
|
||||||
}
|
}
|
||||||
"onBlockedClientConnecting" -> @TargetApi(30) {
|
Build.VERSION.SDK_INT >= 30 && method.matches("onBlockedClientConnecting", WifiClient.clazz,
|
||||||
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onBlockedClientConnecting"))
|
Int::class.java) -> {
|
||||||
if (noArgs != 2) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
|
|
||||||
callback.onBlockedClientConnecting(args!![0] as Parcelable, args[1] as Int)
|
callback.onBlockedClientConnecting(args!![0] as Parcelable, args[1] as Int)
|
||||||
}
|
}
|
||||||
else -> callSuper(interfaceSoftApCallback, proxy, method, args)
|
else -> callSuper(interfaceSoftApCallback, proxy, method, args)
|
||||||
@@ -237,7 +302,6 @@ object WifiApManager {
|
|||||||
} else registerSoftApCallback(Services.wifi, proxy, null)
|
} else registerSoftApCallback(Services.wifi, proxy, null)
|
||||||
return proxy
|
return proxy
|
||||||
}
|
}
|
||||||
@RequiresApi(28)
|
|
||||||
fun unregisterSoftApCallback(key: Any) = unregisterSoftApCallback(Services.wifi, key)
|
fun unregisterSoftApCallback(key: Any) = unregisterSoftApCallback(Services.wifi, key)
|
||||||
|
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
@@ -253,43 +317,9 @@ object WifiApManager {
|
|||||||
private val cancelLocalOnlyHotspotRequest by lazy {
|
private val cancelLocalOnlyHotspotRequest by lazy {
|
||||||
WifiManager::class.java.getDeclaredMethod("cancelLocalOnlyHotspotRequest")
|
WifiManager::class.java.getDeclaredMethod("cancelLocalOnlyHotspotRequest")
|
||||||
}
|
}
|
||||||
@RequiresApi(26)
|
/**
|
||||||
|
* This is the only way to unregister requests besides app exiting.
|
||||||
|
* Therefore, we are happy with crashing the app if reflection fails.
|
||||||
|
*/
|
||||||
fun cancelLocalOnlyHotspotRequest() = cancelLocalOnlyHotspotRequest(Services.wifi)
|
fun cancelLocalOnlyHotspotRequest() = cancelLocalOnlyHotspotRequest(Services.wifi)
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private val setWifiApEnabled by lazy {
|
|
||||||
WifiManager::class.java.getDeclaredMethod("setWifiApEnabled",
|
|
||||||
android.net.wifi.WifiConfiguration::class.java, Boolean::class.java)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Start AccessPoint mode with the specified
|
|
||||||
* configuration. If the radio is already running in
|
|
||||||
* AP mode, update the new configuration
|
|
||||||
* Note that starting in access point mode disables station
|
|
||||||
* mode operation
|
|
||||||
* @param wifiConfig SSID, security and channel details as
|
|
||||||
* part of WifiConfiguration
|
|
||||||
* @return {@code true} if the operation succeeds, {@code false} otherwise
|
|
||||||
*/
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private fun WifiManager.setWifiApEnabled(wifiConfig: android.net.wifi.WifiConfiguration?, enabled: Boolean) =
|
|
||||||
setWifiApEnabled(this, wifiConfig, enabled) as Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Although the functionalities were removed in API 26, it is already not functioning correctly on API 25.
|
|
||||||
*
|
|
||||||
* See also: https://android.googlesource.com/platform/frameworks/base/+/5c0b10a4a9eecc5307bb89a271221f2b20448797%5E%21/
|
|
||||||
*/
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
@Deprecated("Not usable since API 26, malfunctioning on API 25")
|
|
||||||
fun start(wifiConfig: android.net.wifi.WifiConfiguration? = null) {
|
|
||||||
Services.wifi.isWifiEnabled = false
|
|
||||||
Services.wifi.setWifiApEnabled(wifiConfig, true)
|
|
||||||
}
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
@Deprecated("Not usable since API 26")
|
|
||||||
fun stop() {
|
|
||||||
Services.wifi.setWifiApEnabled(null, false)
|
|
||||||
Services.wifi.isWifiEnabled = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import timber.log.Timber
|
|||||||
@RequiresApi(30)
|
@RequiresApi(30)
|
||||||
value class WifiClient(val inner: Parcelable) {
|
value class WifiClient(val inner: Parcelable) {
|
||||||
companion object {
|
companion object {
|
||||||
private val clazz by lazy { Class.forName("android.net.wifi.WifiClient") }
|
val clazz by lazy { Class.forName("android.net.wifi.WifiClient") }
|
||||||
private val getMacAddress by lazy { clazz.getDeclaredMethod("getMacAddress") }
|
private val getMacAddress by lazy { clazz.getDeclaredMethod("getMacAddress") }
|
||||||
@get:RequiresApi(31)
|
@get:RequiresApi(31)
|
||||||
private val getApInstanceIdentifier by lazy @TargetApi(31) { UnblockCentral.getApInstanceIdentifier(clazz) }
|
private val getApInstanceIdentifier by lazy @TargetApi(31) { UnblockCentral.getApInstanceIdentifier(clazz) }
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
package be.mygod.vpnhotspot.net.wifi
|
package be.mygod.vpnhotspot.net.wifi
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.net.MacAddress
|
||||||
|
import android.net.wifi.ScanResult
|
||||||
import android.net.wifi.WpsInfo
|
import android.net.wifi.WpsInfo
|
||||||
import android.net.wifi.p2p.WifiP2pGroup
|
import android.net.wifi.p2p.WifiP2pGroup
|
||||||
|
import android.net.wifi.p2p.WifiP2pInfo
|
||||||
import android.net.wifi.p2p.WifiP2pManager
|
import android.net.wifi.p2p.WifiP2pManager
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||||
import be.mygod.vpnhotspot.util.callSuper
|
import be.mygod.vpnhotspot.util.callSuper
|
||||||
|
import be.mygod.vpnhotspot.util.matches
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import timber.log.Timber
|
|
||||||
import java.lang.reflect.InvocationHandler
|
import java.lang.reflect.InvocationHandler
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
import java.lang.reflect.Proxy
|
import java.lang.reflect.Proxy
|
||||||
@@ -53,6 +56,15 @@ object WifiP2pManagerHelper {
|
|||||||
return result.future.await()
|
return result.future.await()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission") // this method will fail correctly if permission is missing
|
||||||
|
@RequiresApi(33)
|
||||||
|
suspend fun WifiP2pManager.setVendorElements(c: WifiP2pManager.Channel,
|
||||||
|
ve: List<ScanResult.InformationElement>): Int? {
|
||||||
|
val result = ResultListener()
|
||||||
|
setVendorElements(c, ve, result)
|
||||||
|
return result.future.await()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Available since Android 4.3.
|
* Available since Android 4.3.
|
||||||
*
|
*
|
||||||
@@ -98,9 +110,8 @@ object WifiP2pManagerHelper {
|
|||||||
private val interfacePersistentGroupInfoListener by lazy {
|
private val interfacePersistentGroupInfoListener by lazy {
|
||||||
Class.forName("android.net.wifi.p2p.WifiP2pManager\$PersistentGroupInfoListener")
|
Class.forName("android.net.wifi.p2p.WifiP2pManager\$PersistentGroupInfoListener")
|
||||||
}
|
}
|
||||||
private val getGroupList by lazy {
|
private val classWifiP2pGroupList by lazy { Class.forName("android.net.wifi.p2p.WifiP2pGroupList") }
|
||||||
Class.forName("android.net.wifi.p2p.WifiP2pGroupList").getDeclaredMethod("getGroupList")
|
private val getGroupList by lazy { classWifiP2pGroupList.getDeclaredMethod("getGroupList") }
|
||||||
}
|
|
||||||
private val requestPersistentGroupInfo by lazy {
|
private val requestPersistentGroupInfo by lazy {
|
||||||
WifiP2pManager::class.java.getDeclaredMethod("requestPersistentGroupInfo",
|
WifiP2pManager::class.java.getDeclaredMethod("requestPersistentGroupInfo",
|
||||||
WifiP2pManager.Channel::class.java, interfacePersistentGroupInfoListener)
|
WifiP2pManager.Channel::class.java, interfacePersistentGroupInfoListener)
|
||||||
@@ -116,9 +127,8 @@ object WifiP2pManagerHelper {
|
|||||||
val result = CompletableDeferred<Collection<WifiP2pGroup>>()
|
val result = CompletableDeferred<Collection<WifiP2pGroup>>()
|
||||||
requestPersistentGroupInfo(this, c, Proxy.newProxyInstance(interfacePersistentGroupInfoListener.classLoader,
|
requestPersistentGroupInfo(this, c, Proxy.newProxyInstance(interfacePersistentGroupInfoListener.classLoader,
|
||||||
arrayOf(interfacePersistentGroupInfoListener), object : InvocationHandler {
|
arrayOf(interfacePersistentGroupInfoListener), object : InvocationHandler {
|
||||||
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? = when (method.name) {
|
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? = when {
|
||||||
"onPersistentGroupInfoAvailable" -> {
|
method.matches("onPersistentGroupInfoAvailable", classWifiP2pGroupList) -> {
|
||||||
if (args?.size != 1) Timber.w(IllegalArgumentException("Unexpected args: $args"))
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
result.complete(getGroupList(args!![0]) as Collection<WifiP2pGroup>)
|
result.complete(getGroupList(args!![0]) as Collection<WifiP2pGroup>)
|
||||||
}
|
}
|
||||||
@@ -128,14 +138,22 @@ object WifiP2pManagerHelper {
|
|||||||
return result.await()
|
return result.await()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
suspend fun WifiP2pManager.requestConnectionInfo(c: WifiP2pManager.Channel) =
|
||||||
|
CompletableDeferred<WifiP2pInfo?>().apply { requestConnectionInfo(c) { complete(it) } }.await()
|
||||||
|
@SuppressLint("MissingPermission") // missing permission simply leads to null result
|
||||||
@RequiresApi(29)
|
@RequiresApi(29)
|
||||||
suspend fun WifiP2pManager.requestDeviceAddress(c: WifiP2pManager.Channel): MacAddressCompat? {
|
suspend fun WifiP2pManager.requestDeviceAddress(c: WifiP2pManager.Channel): MacAddress? {
|
||||||
val future = CompletableDeferred<String?>()
|
val future = CompletableDeferred<String?>()
|
||||||
requestDeviceInfo(c) { future.complete(it?.deviceAddress) }
|
requestDeviceInfo(c) { future.complete(it?.deviceAddress) }
|
||||||
return future.await()?.let {
|
return future.await()?.let {
|
||||||
val address = if (it.isEmpty()) null else MacAddressCompat.fromString(it)
|
val address = if (it.isEmpty()) null else MacAddress.fromString(it)
|
||||||
if (address == MacAddressCompat.ANY_ADDRESS) null else address
|
if (address == MacAddressCompat.ANY_ADDRESS) null else address
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@SuppressLint("MissingPermission") // missing permission simply leads to null result
|
||||||
|
suspend fun WifiP2pManager.requestGroupInfo(c: WifiP2pManager.Channel) =
|
||||||
|
CompletableDeferred<WifiP2pGroup?>().apply { requestGroupInfo(c) { complete(it) } }.await()
|
||||||
|
@RequiresApi(29)
|
||||||
|
suspend fun WifiP2pManager.requestP2pState(c: WifiP2pManager.Channel) =
|
||||||
|
CompletableDeferred<Int>().apply { requestP2pState(c) { complete(it) } }.await()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package be.mygod.vpnhotspot.net.wifi
|
||||||
|
|
||||||
|
import android.net.wifi.WifiSsid
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import org.jetbrains.annotations.Contract
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.CharBuffer
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.nio.charset.CodingErrorAction
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class WifiSsidCompat(val bytes: ByteArray) : Parcelable {
|
||||||
|
companion object {
|
||||||
|
private val hexTester = Regex("^(?:[0-9a-f]{2})*$", RegexOption.IGNORE_CASE)
|
||||||
|
private val qrSanitizer = Regex("([\\\\\":;,])")
|
||||||
|
|
||||||
|
fun fromHex(hex: CharSequence?) = hex?.run {
|
||||||
|
require(length % 2 == 0) { "Input should be hex: $hex" }
|
||||||
|
WifiSsidCompat((0 until length / 2).map {
|
||||||
|
Integer.parseInt(substring(it * 2, it * 2 + 2), 16).toByte()
|
||||||
|
}.toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Contract("null -> null; !null -> !null")
|
||||||
|
fun fromUtf8Text(text: CharSequence?) = text?.toString()?.toByteArray()?.let { WifiSsidCompat(it) }
|
||||||
|
|
||||||
|
fun toMeCard(text: String) = qrSanitizer.replace(text) { "\\${it.groupValues[1]}" }
|
||||||
|
|
||||||
|
@RequiresApi(33)
|
||||||
|
fun WifiSsid.toCompat() = WifiSsidCompat(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
require(bytes.size <= 32) { "${bytes.size} > 32" }
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(31)
|
||||||
|
fun toPlatform() = WifiSsid.fromBytes(bytes)
|
||||||
|
|
||||||
|
fun decode(charset: Charset = Charsets.UTF_8) = CharBuffer.allocate(32).run {
|
||||||
|
val result = charset.newDecoder().apply {
|
||||||
|
onMalformedInput(CodingErrorAction.REPORT)
|
||||||
|
onUnmappableCharacter(CodingErrorAction.REPORT)
|
||||||
|
}.decode(ByteBuffer.wrap(bytes), this, true)
|
||||||
|
if (result.isError) null else flip().toString()
|
||||||
|
}
|
||||||
|
val hex get() = bytes.joinToString("") { "%02x".format(it.toUByte().toInt()) }
|
||||||
|
|
||||||
|
fun toMeCard(): String {
|
||||||
|
val utf8 = decode() ?: return hex
|
||||||
|
return if (hexTester.matches(utf8)) "\"$utf8\"" else toMeCard(utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString() = String(bytes)
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
other as WifiSsidCompat
|
||||||
|
if (!bytes.contentEquals(other.bytes)) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode() = bytes.contentHashCode()
|
||||||
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package be.mygod.vpnhotspot.preference
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import androidx.core.os.bundleOf
|
|
||||||
import androidx.preference.EditTextPreferenceDialogFragmentCompat
|
|
||||||
import be.mygod.vpnhotspot.R
|
|
||||||
import be.mygod.vpnhotspot.widget.AlwaysAutoCompleteEditText
|
|
||||||
|
|
||||||
class AlwaysAutoCompleteEditTextPreferenceDialogFragment : EditTextPreferenceDialogFragmentCompat() {
|
|
||||||
companion object {
|
|
||||||
private const val ARG_SUGGESTIONS = "suggestions"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setArguments(key: String, suggestions: Array<String>) {
|
|
||||||
arguments = bundleOf(ARG_KEY to key, ARG_SUGGESTIONS to suggestions)
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var editText: AlwaysAutoCompleteEditText
|
|
||||||
|
|
||||||
override fun onCreateDialogView(context: Context) = super.onCreateDialogView(context).apply {
|
|
||||||
editText = AlwaysAutoCompleteEditText(context).apply {
|
|
||||||
id = android.R.id.edit
|
|
||||||
minHeight = resources.getDimensionPixelSize(R.dimen.touch_target_min)
|
|
||||||
}
|
|
||||||
val oldEditText = findViewById<View>(android.R.id.edit)!!
|
|
||||||
val container = oldEditText.parent as ViewGroup
|
|
||||||
container.removeView(oldEditText)
|
|
||||||
container.addView(editText, oldEditText.layoutParams)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindDialogView(view: View) {
|
|
||||||
super.onBindDialogView(view)
|
|
||||||
editText.hint = (preference.summaryProvider as SummaryFallbackProvider).fallback
|
|
||||||
arguments?.getStringArray(ARG_SUGGESTIONS)?.let { suggestions ->
|
|
||||||
editText.setAdapter(ArrayAdapter(view.context, android.R.layout.select_dialog_item, suggestions))
|
|
||||||
}
|
|
||||||
editText.clearFocus() // having focus is buggy currently
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package be.mygod.vpnhotspot.preference
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.LinkProperties
|
||||||
|
import android.net.Network
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.withStarted
|
||||||
|
import androidx.preference.EditTextPreferenceDialogFragmentCompat
|
||||||
|
import be.mygod.vpnhotspot.R
|
||||||
|
import be.mygod.vpnhotspot.util.Services
|
||||||
|
import be.mygod.vpnhotspot.util.allInterfaceNames
|
||||||
|
import be.mygod.vpnhotspot.util.globalNetworkRequestBuilder
|
||||||
|
import be.mygod.vpnhotspot.widget.AlwaysAutoCompleteEditText
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class AutoCompleteNetworkPreferenceDialogFragment : EditTextPreferenceDialogFragmentCompat() {
|
||||||
|
fun setArguments(key: String) {
|
||||||
|
arguments = bundleOf(ARG_KEY to key)
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var editText: AlwaysAutoCompleteEditText
|
||||||
|
private lateinit var adapter: ArrayAdapter<String>
|
||||||
|
private fun updateAdapter() {
|
||||||
|
adapter.clear()
|
||||||
|
adapter.addAll(interfaceNames.flatMap { it.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
private val interfaceNames = mutableMapOf<Network, List<String>>()
|
||||||
|
private val callback = object : ConnectivityManager.NetworkCallback() {
|
||||||
|
override fun onLinkPropertiesChanged(network: Network, properties: LinkProperties) {
|
||||||
|
interfaceNames[network] = properties.allInterfaceNames
|
||||||
|
lifecycleScope.launch {
|
||||||
|
withStarted { updateAdapter() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLost(network: Network) {
|
||||||
|
interfaceNames.remove(network)
|
||||||
|
lifecycleScope.launch {
|
||||||
|
withStarted { updateAdapter() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialogView(context: Context) = super.onCreateDialogView(context)!!.apply {
|
||||||
|
val oldEditText = findViewById<View>(android.R.id.edit)!!
|
||||||
|
val container = oldEditText.parent as ViewGroup
|
||||||
|
container.removeView(oldEditText)
|
||||||
|
container.addView(layoutInflater.inflate(R.layout.preference_widget_edittext_autocomplete, container, false),
|
||||||
|
oldEditText.layoutParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindDialogView(view: View) {
|
||||||
|
super.onBindDialogView(view)
|
||||||
|
editText = view.findViewById(android.R.id.edit)
|
||||||
|
editText.hint = (preference.summaryProvider as SummaryFallbackProvider).fallback
|
||||||
|
adapter = ArrayAdapter(view.context, android.R.layout.select_dialog_item)
|
||||||
|
editText.setAdapter(adapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
Services.registerNetworkCallback(globalNetworkRequestBuilder().apply {
|
||||||
|
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
||||||
|
removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
|
||||||
|
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||||
|
}.build(), callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
Services.connectivity.unregisterNetworkCallback(callback)
|
||||||
|
interfaceNames.clear()
|
||||||
|
updateAdapter()
|
||||||
|
super.onStop()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@ import android.text.style.StyleSpan
|
|||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import be.mygod.vpnhotspot.R
|
import be.mygod.vpnhotspot.R
|
||||||
import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor
|
import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor
|
||||||
@@ -31,7 +30,7 @@ class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(co
|
|||||||
if (internet) SpannableStringBuilder(ifname).apply {
|
if (internet) SpannableStringBuilder(ifname).apply {
|
||||||
setSpan(StyleSpan(Typeface.BOLD), 0, length, 0)
|
setSpan(StyleSpan(Typeface.BOLD), 0, length, 0)
|
||||||
} else ifname
|
} else ifname
|
||||||
}.joinTo(SpannableStringBuilder()).let { if (it.isEmpty()) "∅" else it }
|
}.joinTo(SpannableStringBuilder()).ifEmpty { "∅" }
|
||||||
|
|
||||||
override fun onAvailable(properties: LinkProperties?) {
|
override fun onAvailable(properties: LinkProperties?) {
|
||||||
val result = mutableMapOf<String, Boolean>()
|
val result = mutableMapOf<String, Boolean>()
|
||||||
@@ -51,15 +50,11 @@ class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(co
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val primary = Monitor()
|
private val primary = Monitor()
|
||||||
private val fallback: Monitor = object : Monitor() {
|
private val fallback = Monitor()
|
||||||
override fun onFallback() {
|
|
||||||
currentInterfaces = mapOf("<default>" to true)
|
|
||||||
onUpdate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
(context as LifecycleOwner).lifecycle.addObserver(this)
|
(context as LifecycleOwner).lifecycle.addObserver(this)
|
||||||
|
onUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart(owner: LifecycleOwner) {
|
override fun onStart(owner: LifecycleOwner) {
|
||||||
@@ -71,8 +66,8 @@ class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(co
|
|||||||
FallbackUpstreamMonitor.unregisterCallback(fallback)
|
FallbackUpstreamMonitor.unregisterCallback(fallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onUpdate() = (context as LifecycleOwner).lifecycleScope.launchWhenStarted {
|
private fun onUpdate() {
|
||||||
summary = context.getText(R.string.settings_service_upstream_monitor_summary).format(
|
summary = context.getText(R.string.settings_service_upstream_monitor_summary).format(
|
||||||
context.resources.configuration.locale, primary.charSequence, fallback.charSequence)
|
context.resources.configuration.locales[0], primary.charSequence, fallback.charSequence)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,38 @@
|
|||||||
package be.mygod.vpnhotspot.room
|
package be.mygod.vpnhotspot.room
|
||||||
|
|
||||||
|
import android.net.MacAddress
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.map
|
import androidx.lifecycle.map
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toLong
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
data class ClientRecord(@PrimaryKey
|
data class ClientRecord(@PrimaryKey
|
||||||
val mac: Long,
|
val mac: MacAddress,
|
||||||
var nickname: CharSequence = "",
|
var nickname: CharSequence = "",
|
||||||
var blocked: Boolean = false,
|
var blocked: Boolean = false,
|
||||||
var macLookupPending: Boolean = true) {
|
var macLookupPending: Boolean = true) {
|
||||||
@androidx.room.Dao
|
@androidx.room.Dao
|
||||||
abstract class Dao {
|
abstract class Dao {
|
||||||
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
|
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
|
||||||
protected abstract fun lookupBlocking(mac: Long): ClientRecord?
|
protected abstract fun lookupBlocking(mac: MacAddress): ClientRecord?
|
||||||
fun lookupOrDefaultBlocking(mac: MacAddressCompat) = lookupBlocking(mac.addr) ?: ClientRecord(mac.addr)
|
fun lookupOrDefaultBlocking(mac: MacAddress) = lookupBlocking(mac) ?: ClientRecord(mac)
|
||||||
|
|
||||||
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
|
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
|
||||||
protected abstract suspend fun lookup(mac: Long): ClientRecord?
|
protected abstract suspend fun lookup(mac: MacAddress): ClientRecord?
|
||||||
suspend fun lookupOrDefault(mac: Long) = lookup(mac) ?: ClientRecord(mac)
|
suspend fun lookupOrDefault(mac: MacAddress) = lookup(mac) ?: ClientRecord(mac)
|
||||||
|
|
||||||
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
|
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
|
||||||
protected abstract fun lookupSync(mac: Long): LiveData<ClientRecord?>
|
protected abstract fun lookupSync(mac: MacAddress): LiveData<ClientRecord?>
|
||||||
fun lookupOrDefaultSync(mac: MacAddressCompat) = lookupSync(mac.addr).map { it ?: ClientRecord(mac.addr) }
|
fun lookupOrDefaultSync(mac: MacAddress) = lookupSync(mac).map { it ?: ClientRecord(mac) }
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
protected abstract suspend fun updateInternal(value: ClientRecord): Long
|
protected abstract suspend fun updateInternal(value: ClientRecord): Long
|
||||||
suspend fun update(value: ClientRecord) = check(updateInternal(value) == value.mac)
|
suspend fun update(value: ClientRecord) = check(updateInternal(value) == value.mac.toLong())
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
open suspend fun upsert(mac: MacAddressCompat, operation: suspend ClientRecord.() -> Unit) = lookupOrDefault(
|
open suspend fun upsert(mac: MacAddress, operation: suspend ClientRecord.() -> Unit) = lookupOrDefault(
|
||||||
mac.addr).apply {
|
mac).apply {
|
||||||
operation()
|
operation()
|
||||||
update(this)
|
update(this)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package be.mygod.vpnhotspot.room
|
package be.mygod.vpnhotspot.room
|
||||||
|
|
||||||
|
import android.net.MacAddress
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import be.mygod.librootkotlinx.useParcel
|
import be.mygod.librootkotlinx.useParcel
|
||||||
|
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||||
|
import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toLong
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
|
|
||||||
@@ -27,6 +30,14 @@ object Converters {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@TypeConverter
|
||||||
|
fun persistMacAddress(address: MacAddress) = address.toLong()
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@TypeConverter
|
||||||
|
fun unpersistMacAddress(address: Long) = MacAddressCompat(address).toPlatform()
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun persistInetAddress(address: InetAddress): ByteArray = address.address
|
fun persistInetAddress(address: InetAddress): ByteArray = address.address
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package be.mygod.vpnhotspot.room
|
package be.mygod.vpnhotspot.room
|
||||||
|
|
||||||
|
import android.net.MacAddress
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
@@ -22,7 +23,7 @@ data class TrafficRecord(
|
|||||||
/**
|
/**
|
||||||
* Foreign key/ID for (possibly non-existent, i.e. default) entry in ClientRecord.
|
* Foreign key/ID for (possibly non-existent, i.e. default) entry in ClientRecord.
|
||||||
*/
|
*/
|
||||||
val mac: Long,
|
val mac: MacAddress,
|
||||||
/**
|
/**
|
||||||
* For now only stats for IPv4 will be recorded. But I'm going to put the more general class here just in case.
|
* For now only stats for IPv4 will be recorded. But I'm going to put the more general class here just in case.
|
||||||
*/
|
*/
|
||||||
@@ -58,7 +59,7 @@ data class TrafficRecord(
|
|||||||
/* We only want to find the last record for each chain so that we don't double count */
|
/* We only want to find the last record for each chain so that we don't double count */
|
||||||
WHERE TrafficRecord.mac = :mac AND Next.id IS NULL
|
WHERE TrafficRecord.mac = :mac AND Next.id IS NULL
|
||||||
""")
|
""")
|
||||||
abstract suspend fun queryStats(mac: Long): ClientStats
|
abstract suspend fun queryStats(mac: MacAddress): ClientStats
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package be.mygod.vpnhotspot.root
|
package be.mygod.vpnhotspot.root
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
@@ -30,18 +29,10 @@ fun ProcessBuilder.fixPath(redirect: Boolean = false) = apply {
|
|||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Dump(val path: String, val cacheDir: File = app.deviceStorage.codeCacheDir) : RootCommandNoResult {
|
data class Dump(val path: String, val cacheDir: File = app.deviceStorage.codeCacheDir) : RootCommandNoResult {
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
override suspend fun execute() = withContext(Dispatchers.IO) {
|
override suspend fun execute() = withContext(Dispatchers.IO) {
|
||||||
FileOutputStream(path, true).use { out ->
|
FileOutputStream(path, true).use { out ->
|
||||||
val process = ProcessBuilder("sh").fixPath(true).start()
|
val process = ProcessBuilder("sh").fixPath(true).start()
|
||||||
process.outputStream.bufferedWriter().use { commands ->
|
process.outputStream.bufferedWriter().use { commands ->
|
||||||
// https://android.googlesource.com/platform/external/iptables/+/android-7.0.0_r1/iptables/Android.mk#34
|
|
||||||
val iptablesSave = if (Build.VERSION.SDK_INT < 24) File(cacheDir, "iptables-save").absolutePath.also {
|
|
||||||
commands.appendLine("ln -sf /system/bin/iptables $it")
|
|
||||||
} else "iptables-save"
|
|
||||||
val ip6tablesSave = if (Build.VERSION.SDK_INT < 24) File(cacheDir, "ip6tables-save").absolutePath.also {
|
|
||||||
commands.appendLine("ln -sf /system/bin/ip6tables $it")
|
|
||||||
} else "ip6tables-save"
|
|
||||||
commands.appendLine("""
|
commands.appendLine("""
|
||||||
|echo dumpsys ${Context.WIFI_P2P_SERVICE}
|
|echo dumpsys ${Context.WIFI_P2P_SERVICE}
|
||||||
|dumpsys ${Context.WIFI_P2P_SERVICE}
|
|dumpsys ${Context.WIFI_P2P_SERVICE}
|
||||||
@@ -50,13 +41,13 @@ data class Dump(val path: String, val cacheDir: File = app.deviceStorage.codeCac
|
|||||||
|dumpsys ${Context.CONNECTIVITY_SERVICE} tethering
|
|dumpsys ${Context.CONNECTIVITY_SERVICE} tethering
|
||||||
|echo
|
|echo
|
||||||
|echo iptables -t filter
|
|echo iptables -t filter
|
||||||
|$iptablesSave -t filter
|
|iptables-save -t filter
|
||||||
|echo
|
|echo
|
||||||
|echo iptables -t nat
|
|echo iptables -t nat
|
||||||
|$iptablesSave -t nat
|
|iptables-save -t nat
|
||||||
|echo
|
|echo
|
||||||
|echo ip6tables-save
|
|echo ip6tables-save
|
||||||
|$ip6tablesSave
|
|ip6tables-save
|
||||||
|echo
|
|echo
|
||||||
|echo ip rule
|
|echo ip rule
|
||||||
|$IP rule
|
|$IP rule
|
||||||
@@ -125,7 +116,7 @@ class ProcessListener(private val terminateRegex: Regex,
|
|||||||
parent.join()
|
parent.join()
|
||||||
} finally {
|
} finally {
|
||||||
parent.cancel()
|
parent.cancel()
|
||||||
if (Build.VERSION.SDK_INT < 26) process.destroy() else if (process.isAlive) process.destroyForcibly()
|
if (process.isAlive) process.destroyForcibly()
|
||||||
parent.join()
|
parent.join()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,7 +153,6 @@ data class StartTethering(private val type: Int,
|
|||||||
|
|
||||||
@Deprecated("Old API since API 30")
|
@Deprecated("Old API since API 30")
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@RequiresApi(24)
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
data class StartTetheringLegacy(private val cacheDir: File, private val type: Int,
|
data class StartTetheringLegacy(private val cacheDir: File, private val type: Int,
|
||||||
private val showProvisioningUi: Boolean) : RootCommand<ParcelableBoolean> {
|
private val showProvisioningUi: Boolean) : RootCommand<ParcelableBoolean> {
|
||||||
@@ -184,7 +174,6 @@ data class StartTetheringLegacy(private val cacheDir: File, private val type: In
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@RequiresApi(24)
|
|
||||||
data class StopTethering(private val type: Int) : RootCommandNoResult {
|
data class StopTethering(private val type: Int) : RootCommandNoResult {
|
||||||
override suspend fun execute(): Parcelable? {
|
override suspend fun execute(): Parcelable? {
|
||||||
TetheringManager.stopTethering(type)
|
TetheringManager.stopTethering(type)
|
||||||
@@ -209,12 +198,11 @@ data class SettingsGlobalPut(val name: String, val value: String) : RootCommandN
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
override suspend fun execute() = withContext(Dispatchers.IO) {
|
override suspend fun execute() = withContext(Dispatchers.IO) {
|
||||||
val process = ProcessBuilder("settings", "put", "global", name, value).fixPath(true).start()
|
val process = ProcessBuilder("settings", "put", "global", name, value).fixPath(true).start()
|
||||||
val error = process.inputStream.bufferedReader().readText()
|
val error = process.inputStream.bufferedReader().readText()
|
||||||
check(process.waitFor() == 0)
|
val exit = process.waitFor()
|
||||||
if (error.isNotEmpty()) throw RemoteException(error)
|
if (exit != 0 || error.isNotEmpty()) throw RemoteException("Process exited with $exit: $error")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package be.mygod.vpnhotspot.root
|
package be.mygod.vpnhotspot.root
|
||||||
|
|
||||||
|
import android.net.MacAddress
|
||||||
|
import android.net.wifi.ScanResult
|
||||||
import android.net.wifi.p2p.WifiP2pManager
|
import android.net.wifi.p2p.WifiP2pManager
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
@@ -11,6 +13,7 @@ import be.mygod.librootkotlinx.*
|
|||||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.deletePersistentGroup
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.deletePersistentGroup
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestDeviceAddress
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestDeviceAddress
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupInfo
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupInfo
|
||||||
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setVendorElements
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
|
||||||
import be.mygod.vpnhotspot.util.Services
|
import be.mygod.vpnhotspot.util.Services
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
@@ -35,10 +38,8 @@ object RepeaterCommands {
|
|||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@RequiresApi(29)
|
@RequiresApi(29)
|
||||||
class RequestDeviceAddress : RootCommand<ParcelableLong?> {
|
class RequestDeviceAddress : RootCommand<MacAddress?> {
|
||||||
override suspend fun execute() = Services.p2p!!.run {
|
override suspend fun execute() = Services.p2p!!.run { requestDeviceAddress(obtainChannel()) }
|
||||||
requestDeviceAddress(obtainChannel())?.let { ParcelableLong(it.addr) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@@ -55,6 +56,14 @@ object RepeaterCommands {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
@RequiresApi(33)
|
||||||
|
data class SetVendorElements(private val ve: List<ScanResult.InformationElement>) : RootCommand<ParcelableInt?> {
|
||||||
|
override suspend fun execute() = Services.p2p!!.run {
|
||||||
|
setVendorElements(obtainChannel(), ve)?.let { ParcelableInt(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class WriteP2pConfig(val data: String, val legacy: Boolean) : RootCommandNoResult {
|
data class WriteP2pConfig(val data: String, val legacy: Boolean) : RootCommandNoResult {
|
||||||
override suspend fun execute(): Parcelable? {
|
override suspend fun execute(): Parcelable? {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import android.util.Log
|
|||||||
import be.mygod.librootkotlinx.*
|
import be.mygod.librootkotlinx.*
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.util.Services
|
import be.mygod.vpnhotspot.util.Services
|
||||||
|
import be.mygod.vpnhotspot.util.UnblockCentral
|
||||||
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@@ -31,6 +33,7 @@ object RootManager : RootSession(), Logger {
|
|||||||
})
|
})
|
||||||
Logger.me = RootManager
|
Logger.me = RootManager
|
||||||
Services.init { systemContext }
|
Services.init { systemContext }
|
||||||
|
UnblockCentral.needInit = false
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,7 +45,10 @@ object RootManager : RootSession(), Logger {
|
|||||||
|
|
||||||
override suspend fun initServer(server: RootServer) {
|
override suspend fun initServer(server: RootServer) {
|
||||||
Logger.me = this
|
Logger.me = this
|
||||||
server.init(app.deviceStorage)
|
AppProcess.shouldRelocateHeuristics.let {
|
||||||
|
FirebaseCrashlytics.getInstance().setCustomKey("RootManager.relocateEnabled", it)
|
||||||
|
server.init(app.deviceStorage, it)
|
||||||
|
}
|
||||||
server.execute(RootInit())
|
server.execute(RootInit())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package be.mygod.vpnhotspot.root
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import be.mygod.librootkotlinx.RootCommand
|
import be.mygod.librootkotlinx.RootCommand
|
||||||
import be.mygod.librootkotlinx.RootCommandOneWay
|
import be.mygod.librootkotlinx.RootCommandNoResult
|
||||||
import be.mygod.vpnhotspot.net.Routing
|
import be.mygod.vpnhotspot.net.Routing
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@@ -13,8 +13,7 @@ import timber.log.Timber
|
|||||||
|
|
||||||
object RoutingCommands {
|
object RoutingCommands {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class Clean : RootCommandOneWay {
|
class Clean : RootCommandNoResult {
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
override suspend fun execute() = withContext(Dispatchers.IO) {
|
override suspend fun execute() = withContext(Dispatchers.IO) {
|
||||||
val process = ProcessBuilder("sh").fixPath(true).start()
|
val process = ProcessBuilder("sh").fixPath(true).start()
|
||||||
process.outputStream.bufferedWriter().use(Routing.Companion::appendCleanCommands)
|
process.outputStream.bufferedWriter().use(Routing.Companion::appendCleanCommands)
|
||||||
@@ -23,6 +22,7 @@ object RoutingCommands {
|
|||||||
else -> Timber.w("Unexpected exit code $code")
|
else -> Timber.w("Unexpected exit code $code")
|
||||||
}
|
}
|
||||||
check(process.waitFor() == 0)
|
check(process.waitFor() == 0)
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
package be.mygod.vpnhotspot.root
|
package be.mygod.vpnhotspot.root
|
||||||
|
|
||||||
|
import android.annotation.TargetApi
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.net.wifi.SoftApConfiguration
|
||||||
import android.net.wifi.WifiManager
|
import android.net.wifi.WifiManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import be.mygod.librootkotlinx.ParcelableBoolean
|
import be.mygod.librootkotlinx.ParcelableBoolean
|
||||||
import be.mygod.librootkotlinx.RootCommand
|
import be.mygod.librootkotlinx.RootCommand
|
||||||
import be.mygod.librootkotlinx.RootCommandChannel
|
import be.mygod.librootkotlinx.RootCommandChannel
|
||||||
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
|
import be.mygod.vpnhotspot.R
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
||||||
|
import be.mygod.vpnhotspot.net.wifi.WifiClient
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.*
|
import kotlinx.coroutines.channels.*
|
||||||
@@ -15,7 +21,6 @@ import kotlinx.parcelize.Parcelize
|
|||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
object WifiApCommands {
|
object WifiApCommands {
|
||||||
@RequiresApi(28)
|
|
||||||
sealed class SoftApCallbackParcel : Parcelable {
|
sealed class SoftApCallbackParcel : Parcelable {
|
||||||
abstract fun dispatch(callback: WifiApManager.SoftApCallbackCompat)
|
abstract fun dispatch(callback: WifiApManager.SoftApCallbackCompat)
|
||||||
|
|
||||||
@@ -55,7 +60,6 @@ object WifiApCommands {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@RequiresApi(28)
|
|
||||||
class RegisterSoftApCallback : RootCommandChannel<SoftApCallbackParcel> {
|
class RegisterSoftApCallback : RootCommandChannel<SoftApCallbackParcel> {
|
||||||
override fun create(scope: CoroutineScope) = scope.produce(capacity = capacity) {
|
override fun create(scope: CoroutineScope) = scope.produce(capacity = capacity) {
|
||||||
val finish = CompletableDeferred<Unit>()
|
val finish = CompletableDeferred<Unit>()
|
||||||
@@ -111,7 +115,6 @@ object WifiApCommands {
|
|||||||
private val callbacks = mutableSetOf<WifiApManager.SoftApCallbackCompat>()
|
private val callbacks = mutableSetOf<WifiApManager.SoftApCallbackCompat>()
|
||||||
private val lastCallback = AutoFiringCallbacks()
|
private val lastCallback = AutoFiringCallbacks()
|
||||||
private var rootCallbackJob: Job? = null
|
private var rootCallbackJob: Job? = null
|
||||||
@RequiresApi(28)
|
|
||||||
private suspend fun handleChannel(channel: ReceiveChannel<SoftApCallbackParcel>) = channel.consumeEach { parcel ->
|
private suspend fun handleChannel(channel: ReceiveChannel<SoftApCallbackParcel>) = channel.consumeEach { parcel ->
|
||||||
when (parcel) {
|
when (parcel) {
|
||||||
is SoftApCallbackParcel.OnStateChanged -> synchronized(callbacks) { lastCallback.state = parcel }
|
is SoftApCallbackParcel.OnStateChanged -> synchronized(callbacks) { lastCallback.state = parcel }
|
||||||
@@ -121,10 +124,22 @@ object WifiApCommands {
|
|||||||
}
|
}
|
||||||
is SoftApCallbackParcel.OnInfoChanged -> synchronized(callbacks) { lastCallback.info = parcel }
|
is SoftApCallbackParcel.OnInfoChanged -> synchronized(callbacks) { lastCallback.info = parcel }
|
||||||
is SoftApCallbackParcel.OnCapabilityChanged -> synchronized(callbacks) { lastCallback.capability = parcel }
|
is SoftApCallbackParcel.OnCapabilityChanged -> synchronized(callbacks) { lastCallback.capability = parcel }
|
||||||
|
is SoftApCallbackParcel.OnBlockedClientConnecting -> @TargetApi(30) { // passively consume events
|
||||||
|
val client = WifiClient(parcel.client)
|
||||||
|
val macAddress = client.macAddress
|
||||||
|
var name = macAddress.toString()
|
||||||
|
if (Build.VERSION.SDK_INT >= 31) client.apInstanceIdentifier?.let { name += "%$it" }
|
||||||
|
val reason = WifiApManager.clientBlockLookup(parcel.blockedReason, true)
|
||||||
|
Timber.i("$name blocked from connecting: $reason (${parcel.blockedReason})")
|
||||||
|
SmartSnackbar.make(app.getString(R.string.tethering_manage_wifi_client_blocked, name, reason)).apply {
|
||||||
|
action(R.string.tethering_manage_wifi_copy_mac) {
|
||||||
|
app.clipboard.setPrimaryClip(ClipData.newPlainText(null, macAddress.toString()))
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (callback in synchronized(callbacks) { callbacks.toList() }) parcel.dispatch(callback)
|
for (callback in synchronized(callbacks) { callbacks.toList() }) parcel.dispatch(callback)
|
||||||
}
|
}
|
||||||
@RequiresApi(28)
|
|
||||||
fun registerSoftApCallback(callback: WifiApManager.SoftApCallbackCompat) = synchronized(callbacks) {
|
fun registerSoftApCallback(callback: WifiApManager.SoftApCallbackCompat) = synchronized(callbacks) {
|
||||||
val wasEmpty = callbacks.isEmpty()
|
val wasEmpty = callbacks.isEmpty()
|
||||||
callbacks.add(callback)
|
callbacks.add(callback)
|
||||||
@@ -141,7 +156,6 @@ object WifiApCommands {
|
|||||||
null
|
null
|
||||||
} else lastCallback
|
} else lastCallback
|
||||||
}?.toSequence()?.forEach { it?.dispatch(callback) }
|
}?.toSequence()?.forEach { it?.dispatch(callback) }
|
||||||
@RequiresApi(28)
|
|
||||||
fun unregisterSoftApCallback(callback: WifiApManager.SoftApCallbackCompat) = synchronized(callbacks) {
|
fun unregisterSoftApCallback(callback: WifiApManager.SoftApCallbackCompat) = synchronized(callbacks) {
|
||||||
if (callbacks.remove(callback) && callbacks.isEmpty()) {
|
if (callbacks.remove(callback) && callbacks.isEmpty()) {
|
||||||
rootCallbackJob!!.cancel()
|
rootCallbackJob!!.cancel()
|
||||||
@@ -150,13 +164,29 @@ object WifiApCommands {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class GetConfiguration : RootCommand<SoftApConfigurationCompat> {
|
@Deprecated("Use GetConfiguration instead", ReplaceWith("GetConfiguration"))
|
||||||
override suspend fun execute() = WifiApManager.configurationCompat
|
@Suppress("DEPRECATION")
|
||||||
|
class GetConfigurationLegacy : RootCommand<android.net.wifi.WifiConfiguration?> {
|
||||||
|
override suspend fun execute() = WifiApManager.configurationLegacy
|
||||||
|
}
|
||||||
|
@Parcelize
|
||||||
|
@RequiresApi(30)
|
||||||
|
class GetConfiguration : RootCommand<SoftApConfiguration> {
|
||||||
|
override suspend fun execute() = WifiApManager.configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class SetConfiguration(val configuration: SoftApConfigurationCompat) : RootCommand<ParcelableBoolean> {
|
@Deprecated("Use SetConfiguration instead", ReplaceWith("SetConfiguration"))
|
||||||
override suspend fun execute() = ParcelableBoolean(WifiApManager.setConfigurationCompat(configuration))
|
@Suppress("DEPRECATION")
|
||||||
|
data class SetConfigurationLegacy(
|
||||||
|
val configuration: android.net.wifi.WifiConfiguration?,
|
||||||
|
) : RootCommand<ParcelableBoolean> {
|
||||||
|
override suspend fun execute() = ParcelableBoolean(WifiApManager.setConfiguration(configuration))
|
||||||
|
}
|
||||||
|
@Parcelize
|
||||||
|
@RequiresApi(30)
|
||||||
|
data class SetConfiguration(val configuration: SoftApConfiguration) : RootCommand<ParcelableBoolean> {
|
||||||
|
override suspend fun execute() = ParcelableBoolean(WifiApManager.setConfiguration(configuration))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
|||||||
12
mobile/src/main/java/be/mygod/vpnhotspot/util/AppUpdate.kt
Normal file
12
mobile/src/main/java/be/mygod/vpnhotspot/util/AppUpdate.kt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package be.mygod.vpnhotspot.util
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
|
||||||
|
interface AppUpdate {
|
||||||
|
class IgnoredException(cause: Throwable?) : RuntimeException(cause)
|
||||||
|
|
||||||
|
val downloaded: Boolean? get() = null
|
||||||
|
val message: String? get() = null
|
||||||
|
val stalenessDays: Int? get() = null
|
||||||
|
fun updateForResult(activity: Activity, requestCode: Int): Unit = error("Update not supported")
|
||||||
|
}
|
||||||
@@ -9,10 +9,10 @@ import timber.log.Timber
|
|||||||
|
|
||||||
class ConstantLookup(private val prefix: String, private val lookup29: Array<out String?>,
|
class ConstantLookup(private val prefix: String, private val lookup29: Array<out String?>,
|
||||||
private val clazz: () -> Class<*>) {
|
private val clazz: () -> Class<*>) {
|
||||||
private val lookup by lazy {
|
val lookup by lazy {
|
||||||
SparseArrayCompat<String>().apply {
|
SparseArrayCompat<String>().apply {
|
||||||
for (field in clazz().declaredFields) try {
|
for (field in clazz().declaredFields) try {
|
||||||
if (field.name.startsWith(prefix)) put(field.getInt(null), field.name)
|
if (field?.type == Int::class.java && field.name.startsWith(prefix)) put(field.getInt(null), field.name)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.w(e)
|
Timber.w(e)
|
||||||
}
|
}
|
||||||
@@ -30,17 +30,15 @@ class ConstantLookup(private val prefix: String, private val lookup29: Array<out
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("FunctionName")
|
|
||||||
fun ConstantLookup(prefix: String, vararg lookup29: String?, clazz: () -> Class<*>) =
|
fun ConstantLookup(prefix: String, vararg lookup29: String?, clazz: () -> Class<*>) =
|
||||||
ConstantLookup(prefix, lookup29, clazz)
|
ConstantLookup(prefix, lookup29, clazz)
|
||||||
@Suppress("FunctionName")
|
|
||||||
inline fun <reified T> ConstantLookup(prefix: String, vararg lookup29: String?) =
|
inline fun <reified T> ConstantLookup(prefix: String, vararg lookup29: String?) =
|
||||||
ConstantLookup(prefix, lookup29) { T::class.java }
|
ConstantLookup(prefix, lookup29) { T::class.java }
|
||||||
|
|
||||||
class LongConstantLookup(private val clazz: Class<*>, private val prefix: String) {
|
class LongConstantLookup(private val clazz: Class<*>, private val prefix: String) {
|
||||||
private val lookup = LongSparseArray<String>().apply {
|
private val lookup = LongSparseArray<String>().apply {
|
||||||
for (field in clazz.declaredFields) try {
|
for (field in clazz.declaredFields) try {
|
||||||
if (field.name.startsWith(prefix)) put(field.getLong(null), field.name)
|
if (field.type == Long::class.java && field.name.startsWith(prefix)) put(field.getLong(null), field.name)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.w(e)
|
Timber.w(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
package be.mygod.vpnhotspot.util
|
package be.mygod.vpnhotspot.util
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.annotation.TargetApi
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
||||||
@SuppressLint("Registered")
|
@SuppressLint("Registered")
|
||||||
@TargetApi(24)
|
|
||||||
class DeviceStorageApp(context: Context) : Application() {
|
class DeviceStorageApp(context: Context) : Application() {
|
||||||
init {
|
init {
|
||||||
attachBaseContext(context.createDeviceProtectedStorageContext())
|
attachBaseContext(context.createDeviceProtectedStorageContext())
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
package be.mygod.vpnhotspot.util
|
package be.mygod.vpnhotspot.util
|
||||||
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
|
import android.content.Intent
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.DeadObjectException
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import androidx.annotation.RequiresApi
|
import be.mygod.vpnhotspot.BootReceiver
|
||||||
|
|
||||||
@RequiresApi(24)
|
|
||||||
abstract class KillableTileService : TileService(), ServiceConnection {
|
abstract class KillableTileService : TileService(), ServiceConnection {
|
||||||
protected var tapPending = false
|
protected var tapPending = false
|
||||||
|
|
||||||
@@ -25,4 +26,10 @@ abstract class KillableTileService : TileService(), ServiceConnection {
|
|||||||
onClick()
|
onClick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?) = try {
|
||||||
|
super.onBind(intent)
|
||||||
|
} catch (_: DeadObjectException) {
|
||||||
|
null
|
||||||
|
}.also { BootReceiver.startIfEnabled() }
|
||||||
}
|
}
|
||||||
|
|||||||
43
mobile/src/main/java/be/mygod/vpnhotspot/util/RangeInput.kt
Normal file
43
mobile/src/main/java/be/mygod/vpnhotspot/util/RangeInput.kt
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package be.mygod.vpnhotspot.util
|
||||||
|
|
||||||
|
object RangeInput {
|
||||||
|
fun toString(input: IntArray) = StringBuilder().apply {
|
||||||
|
if (input.isEmpty()) return@apply
|
||||||
|
input.sort()
|
||||||
|
var pending: Int? = null
|
||||||
|
var last = input[0]
|
||||||
|
append(last)
|
||||||
|
for (channel in input.asSequence().drop(1)) {
|
||||||
|
if (channel == last + 1) pending = channel else {
|
||||||
|
pending?.let {
|
||||||
|
append('-')
|
||||||
|
append(it)
|
||||||
|
pending = null
|
||||||
|
}
|
||||||
|
append(",\u200b") // zero-width space to save space
|
||||||
|
append(channel)
|
||||||
|
}
|
||||||
|
last = channel
|
||||||
|
}
|
||||||
|
pending?.let {
|
||||||
|
append('-')
|
||||||
|
append(it)
|
||||||
|
}
|
||||||
|
}.toString()
|
||||||
|
fun toString(input: Set<Int>?) = input?.run { toString(toIntArray()) }
|
||||||
|
|
||||||
|
fun fromString(input: CharSequence?, min: Int = 1, max: Int = 999) = mutableSetOf<Int>().apply {
|
||||||
|
if (input == null) return@apply
|
||||||
|
for (unit in input.split(',')) {
|
||||||
|
if (unit.isBlank()) continue
|
||||||
|
val blocks = unit.split('-', limit = 2).map { i ->
|
||||||
|
i.trim { it == '\u200b' || it.isWhitespace() }.toInt()
|
||||||
|
}
|
||||||
|
require(blocks[0] in min..max) { "Out of range: ${blocks[0]}" }
|
||||||
|
if (blocks.size == 2) {
|
||||||
|
require(blocks[1] in min..max) { "Out of range: ${blocks[1]}" }
|
||||||
|
addAll(blocks[0]..blocks[1])
|
||||||
|
} else add(blocks[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,8 +28,8 @@ class RootSession : AutoCloseable {
|
|||||||
|
|
||||||
private var server: RootServer? = runBlocking { RootManager.acquire() }
|
private var server: RootServer? = runBlocking { RootManager.acquire() }
|
||||||
override fun close() {
|
override fun close() {
|
||||||
server = null
|
|
||||||
server?.let { runBlocking { RootManager.release(it) } }
|
server?.let { runBlocking { RootManager.release(it) } }
|
||||||
|
server = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ package be.mygod.vpnhotspot.util
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkRequest
|
||||||
import android.net.wifi.WifiManager
|
import android.net.wifi.WifiManager
|
||||||
import android.net.wifi.p2p.WifiP2pManager
|
import android.net.wifi.p2p.WifiP2pManager
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@@ -14,6 +17,7 @@ object Services {
|
|||||||
contextInit = context
|
contextInit = context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val mainHandler by lazy { Handler(Looper.getMainLooper()) }
|
||||||
val connectivity by lazy { context.getSystemService<ConnectivityManager>()!! }
|
val connectivity by lazy { context.getSystemService<ConnectivityManager>()!! }
|
||||||
val p2p by lazy {
|
val p2p by lazy {
|
||||||
try {
|
try {
|
||||||
@@ -24,4 +28,7 @@ object Services {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val wifi by lazy { context.getSystemService<WifiManager>()!! }
|
val wifi by lazy { context.getSystemService<WifiManager>()!! }
|
||||||
|
|
||||||
|
fun registerNetworkCallback(request: NetworkRequest, networkCallback: ConnectivityManager.NetworkCallback) =
|
||||||
|
connectivity.registerNetworkCallback(request, networkCallback, mainHandler)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package be.mygod.vpnhotspot.util
|
package be.mygod.vpnhotspot.util
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.net.MacAddress
|
||||||
import android.net.wifi.SoftApConfiguration
|
import android.net.wifi.SoftApConfiguration
|
||||||
import android.net.wifi.p2p.WifiP2pConfig
|
import android.net.wifi.p2p.WifiP2pConfig
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import timber.log.Timber
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
|
import me.weishu.reflection.Reflection
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The central object for accessing all the useful blocked APIs. Thanks Google!
|
* The central object for accessing all the useful blocked APIs. Thanks Google!
|
||||||
@@ -12,26 +14,19 @@ import timber.log.Timber
|
|||||||
* Lazy cannot be used directly as it will create inner classes.
|
* Lazy cannot be used directly as it will create inner classes.
|
||||||
*/
|
*/
|
||||||
@SuppressLint("BlockedPrivateApi", "DiscouragedPrivateApi")
|
@SuppressLint("BlockedPrivateApi", "DiscouragedPrivateApi")
|
||||||
@Suppress("FunctionName")
|
|
||||||
object UnblockCentral {
|
object UnblockCentral {
|
||||||
|
var needInit = true
|
||||||
/**
|
/**
|
||||||
* Retrieve this property before doing dangerous shit.
|
* Retrieve this property before doing dangerous shit.
|
||||||
*/
|
*/
|
||||||
@get:RequiresApi(28)
|
private val init by lazy { if (needInit) check(Reflection.unseal(app.deviceStorage) == 0) }
|
||||||
private val init by lazy {
|
|
||||||
try {
|
|
||||||
Class.forName("dalvik.system.VMDebug").getDeclaredMethod("allowHiddenApiReflectionFrom", Class::class.java)
|
|
||||||
.invoke(null, UnblockCentral::class.java)
|
|
||||||
true
|
|
||||||
} catch (e: ReflectiveOperationException) {
|
|
||||||
Timber.w(e)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(31)
|
@RequiresApi(33)
|
||||||
fun setUserConfiguration(clazz: Class<*>) = init.let {
|
fun getCountryCode(clazz: Class<*>) = init.let { clazz.getDeclaredMethod("getCountryCode") }
|
||||||
clazz.getDeclaredMethod("setUserConfiguration", Boolean::class.java)
|
|
||||||
|
@RequiresApi(33)
|
||||||
|
fun setRandomizedMacAddress(clazz: Class<*>) = init.let {
|
||||||
|
clazz.getDeclaredMethod("setRandomizedMacAddress", MacAddress::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
@get:RequiresApi(31)
|
@get:RequiresApi(31)
|
||||||
|
|||||||
@@ -1,23 +1,16 @@
|
|||||||
package be.mygod.vpnhotspot.util
|
package be.mygod.vpnhotspot.util
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.annotation.TargetApi
|
|
||||||
import android.content.*
|
import android.content.*
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.net.InetAddresses
|
import android.net.*
|
||||||
import android.net.LinkProperties
|
|
||||||
import android.net.RouteInfo
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
import android.system.ErrnoException
|
|
||||||
import android.system.Os
|
|
||||||
import android.system.OsConstants
|
|
||||||
import android.text.*
|
import android.text.*
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.databinding.BindingAdapter
|
import androidx.databinding.BindingAdapter
|
||||||
@@ -26,18 +19,23 @@ import androidx.fragment.app.FragmentManager
|
|||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.io.IOException
|
|
||||||
import java.lang.invoke.MethodHandles
|
import java.lang.invoke.MethodHandles
|
||||||
import java.lang.reflect.InvocationHandler
|
import java.lang.reflect.InvocationHandler
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
|
import java.net.HttpURLConnection
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.NetworkInterface
|
import java.net.NetworkInterface
|
||||||
import java.net.SocketException
|
import java.net.SocketException
|
||||||
import java.util.*
|
import java.net.URL
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
tailrec fun Throwable.getRootCause(): Throwable {
|
tailrec fun Throwable.getRootCause(): Throwable {
|
||||||
if (this is InvocationTargetException || this is RemoteException) return (cause ?: return this).getRootCause()
|
if (this is InvocationTargetException || this is RemoteException) return (cause ?: return this).getRootCause()
|
||||||
@@ -55,6 +53,10 @@ fun Long.toPluralInt(): Int {
|
|||||||
return (this % 1000000000).toInt() + 1000000000
|
return (this % 1000000000).toInt() + 1000000000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Method.matches(name: String, vararg classes: Class<*>) = this.name == name && parameterCount == classes.size &&
|
||||||
|
classes.indices.all { i -> parameters[i].type == classes[i] }
|
||||||
|
inline fun <reified T> Method.matches1(name: String) = matches(name, T::class.java)
|
||||||
|
|
||||||
fun Context.ensureReceiverUnregistered(receiver: BroadcastReceiver) {
|
fun Context.ensureReceiverUnregistered(receiver: BroadcastReceiver) {
|
||||||
try {
|
try {
|
||||||
unregisterReceiver(receiver)
|
unregisterReceiver(receiver)
|
||||||
@@ -143,12 +145,13 @@ fun makeIpSpan(ip: InetAddress) = ip.hostAddress.let {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun makeMacSpan(mac: String) = if (app.hasTouch) SpannableString(mac).apply {
|
fun makeMacSpan(mac: String) = if (app.hasTouch) SpannableString(mac).apply {
|
||||||
setSpan(CustomTabsUrlSpan("https://macvendors.co/results/$mac"), 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
setSpan(CustomTabsUrlSpan("https://maclookup.app/search/result?mac=$mac"), 0, length,
|
||||||
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
} else mac
|
} else mac
|
||||||
|
|
||||||
fun NetworkInterface.formatAddresses(macOnly: Boolean = false) = SpannableStringBuilder().apply {
|
fun NetworkInterface.formatAddresses(macOnly: Boolean = false) = SpannableStringBuilder().apply {
|
||||||
try {
|
try {
|
||||||
val address = hardwareAddress?.let(MacAddressCompat::fromBytes)
|
val address = hardwareAddress?.let(MacAddress::fromBytes)
|
||||||
if (address != null && address != MacAddressCompat.ANY_ADDRESS) appendLine(makeMacSpan(address.toString()))
|
if (address != null && address != MacAddressCompat.ANY_ADDRESS) appendLine(makeMacSpan(address.toString()))
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
Timber.w(e)
|
Timber.w(e)
|
||||||
@@ -200,8 +203,7 @@ fun Resources.findIdentifier(name: String, defType: String, defPackage: String,
|
|||||||
if (alternativePackage != null && it == 0) getIdentifier(name, defType, alternativePackage) else it
|
if (alternativePackage != null && it == 0) getIdentifier(name, defType, alternativePackage) else it
|
||||||
}
|
}
|
||||||
|
|
||||||
@get:RequiresApi(26)
|
private val newLookup by lazy {
|
||||||
private val newLookup by lazy @TargetApi(26) {
|
|
||||||
MethodHandles.Lookup::class.java.getDeclaredConstructor(Class::class.java, Int::class.java).apply {
|
MethodHandles.Lookup::class.java.getDeclaredConstructor(Class::class.java, Int::class.java).apply {
|
||||||
isAccessible = true
|
isAccessible = true
|
||||||
}
|
}
|
||||||
@@ -213,10 +215,14 @@ private val newLookup by lazy @TargetApi(26) {
|
|||||||
* See also: https://stackoverflow.com/a/49532463/2245107
|
* See also: https://stackoverflow.com/a/49532463/2245107
|
||||||
*/
|
*/
|
||||||
fun InvocationHandler.callSuper(interfaceClass: Class<*>, proxy: Any, method: Method, args: Array<out Any?>?) = when {
|
fun InvocationHandler.callSuper(interfaceClass: Class<*>, proxy: Any, method: Method, args: Array<out Any?>?) = when {
|
||||||
Build.VERSION.SDK_INT >= 26 && method.isDefault -> newLookup.newInstance(interfaceClass, 0xf) // ALL_MODES
|
method.isDefault -> try {
|
||||||
.`in`(interfaceClass).unreflectSpecial(method, interfaceClass).bindTo(proxy).run {
|
newLookup.newInstance(interfaceClass, 0xf) // ALL_MODES
|
||||||
if (args == null) invokeWithArguments() else invokeWithArguments(*args)
|
} catch (e: ReflectiveOperationException) {
|
||||||
}
|
Timber.w(e)
|
||||||
|
MethodHandles.lookup().`in`(interfaceClass)
|
||||||
|
}.unreflectSpecial(method, interfaceClass).bindTo(proxy).run {
|
||||||
|
if (args == null) invokeWithArguments() else invokeWithArguments(*args)
|
||||||
|
}
|
||||||
// otherwise, we just redispatch it to InvocationHandler
|
// otherwise, we just redispatch it to InvocationHandler
|
||||||
method.declaringClass.isAssignableFrom(javaClass) -> when {
|
method.declaringClass.isAssignableFrom(javaClass) -> when {
|
||||||
method.declaringClass == Object::class.java -> when (method.name) {
|
method.declaringClass == Object::class.java -> when (method.name) {
|
||||||
@@ -234,13 +240,26 @@ fun InvocationHandler.callSuper(interfaceClass: Class<*>, proxy: Any, method: Me
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("FunctionName")
|
fun globalNetworkRequestBuilder() = NetworkRequest.Builder().apply {
|
||||||
fun if_nametoindex(ifname: String) = if (Build.VERSION.SDK_INT >= 26) {
|
if (Build.VERSION.SDK_INT >= 31) setIncludeOtherUidNetworks(true)
|
||||||
Os.if_nametoindex(ifname)
|
}
|
||||||
} else try {
|
|
||||||
File("/sys/class/net/$ifname/ifindex").inputStream().bufferedReader().use { it.readLine().trim().toInt() }
|
suspend fun <T> connectCancellable(url: String, block: suspend (HttpURLConnection) -> T): T {
|
||||||
} catch (_: FileNotFoundException) {
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
NetworkInterface.getByName(ifname)?.index ?: 0
|
val conn = URL(url).openConnection() as HttpURLConnection
|
||||||
} catch (e: IOException) {
|
return suspendCancellableCoroutine { cont ->
|
||||||
if ((e.cause as? ErrnoException)?.errno == OsConstants.ENODEV) 0 else throw e
|
val job = GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
cont.resume(block(conn)) { cont.resumeWithException(it) }
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
cont.resumeWithException(e)
|
||||||
|
} finally {
|
||||||
|
conn.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cont.invokeOnCancellation {
|
||||||
|
job.cancel(it as? CancellationException)
|
||||||
|
conn.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ import android.graphics.Rect
|
|||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.appcompat.widget.AppCompatAutoCompleteTextView
|
import androidx.appcompat.widget.AppCompatAutoCompleteTextView
|
||||||
import be.mygod.vpnhotspot.R
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Based on: https://gist.github.com/furycomptuers/4961368
|
* Based on: https://gist.github.com/furycomptuers/4961368
|
||||||
*/
|
*/
|
||||||
class AlwaysAutoCompleteEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
|
class AlwaysAutoCompleteEditText @JvmOverloads constructor(
|
||||||
defStyleAttr: Int = R.attr.autoCompleteTextViewStyle) :
|
context: Context,
|
||||||
AppCompatAutoCompleteTextView(context, attrs, defStyleAttr) {
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = androidx.appcompat.R.attr.autoCompleteTextViewStyle,
|
||||||
|
) : AppCompatAutoCompleteTextView(context, attrs, defStyleAttr) {
|
||||||
override fun enoughToFilter() = true
|
override fun enoughToFilter() = true
|
||||||
|
|
||||||
override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
|
override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
|
||||||
|
|||||||
10
mobile/src/main/res/drawable/ic_action_update.xml
Normal file
10
mobile/src/main/res/drawable/ic_action_update.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M21,10.12h-6.78l2.74,-2.82c-2.73,-2.7 -7.15,-2.8 -9.88,-0.1c-2.73,2.71 -2.73,7.08 0,9.79s7.15,2.71 9.88,0C18.32,15.65 19,14.08 19,12.1h2c0,1.98 -0.88,4.55 -2.64,6.29c-3.51,3.48 -9.21,3.48 -12.72,0c-3.5,-3.47 -3.53,-9.11 -0.02,-12.58s9.14,-3.47 12.65,0L21,3V10.12zM12.5,8v4.25l3.5,2.08l-0.72,1.21L11,13V8H12.5z"/>
|
||||||
|
</vector>
|
||||||
5
mobile/src/main/res/drawable/ic_av_closed_caption.xml
Normal file
5
mobile/src/main/res/drawable/ic_av_closed_caption.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#000000"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M19,4L5,4c-1.11,0 -2,0.9 -2,2v12c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.9,-2 -2,-2zM11,11L9.5,11v-0.5h-2v3h2L9.5,13L11,13v1c0,0.55 -0.45,1 -1,1L7,15c-0.55,0 -1,-0.45 -1,-1v-4c0,-0.55 0.45,-1 1,-1h3c0.55,0 1,0.45 1,1v1zM18,11h-1.5v-0.5h-2v3h2L16.5,13L18,13v1c0,0.55 -0.45,1 -1,1h-3c-0.55,0 -1,-0.45 -1,-1v-4c0,-0.55 0.45,-1 1,-1h3c0.55,0 1,0.45 1,1v1z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#000000"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M19.5,5.5v13h-15v-13h15zM19,4L5,4c-1.11,0 -2,0.9 -2,2v12c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.9,-2 -2,-2zM11,11L9.5,11v-0.5h-2v3h2L9.5,13L11,13v1c0,0.55 -0.45,1 -1,1L7,15c-0.55,0 -1,-0.45 -1,-1v-4c0,-0.55 0.45,-1 1,-1h3c0.55,0 1,0.45 1,1v1zM18,11h-1.5v-0.5h-2v3h2L16.5,13L18,13v1c0,0.55 -0.45,1 -1,1h-3c-0.55,0 -1,-0.45 -1,-1v-4c0,-0.55 0.45,-1 1,-1h3c0.55,0 1,0.45 1,1v1z"/>
|
||||||
|
</vector>
|
||||||
10
mobile/src/main/res/drawable/ic_file_downloading.xml
Normal file
10
mobile/src/main/res/drawable/ic_file_downloading.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M18.32,4.26C16.84,3.05 15.01,2.25 13,2.05v2.02c1.46,0.18 2.79,0.76 3.9,1.62L18.32,4.26zM19.93,11h2.02c-0.2,-2.01 -1,-3.84 -2.21,-5.32L18.31,7.1C19.17,8.21 19.75,9.54 19.93,11zM18.31,16.9l1.43,1.43c1.21,-1.48 2.01,-3.32 2.21,-5.32h-2.02C19.75,14.46 19.17,15.79 18.31,16.9zM13,19.93v2.02c2.01,-0.2 3.84,-1 5.32,-2.21l-1.43,-1.43C15.79,19.17 14.46,19.75 13,19.93zM13,12V7h-2v5H7l5,5l5,-5H13zM11,19.93v2.02c-5.05,-0.5 -9,-4.76 -9,-9.95s3.95,-9.45 9,-9.95v2.02C7.05,4.56 4,7.92 4,12S7.05,19.44 11,19.93z"/>
|
||||||
|
</vector>
|
||||||
5
mobile/src/main/res/drawable/ic_launcher_monochrome.xml
Normal file
5
mobile/src/main/res/drawable/ic_launcher_monochrome.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="108dp" android:viewportHeight="108.0"
|
||||||
|
android:viewportWidth="108.0" android:width="108dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#7000" android:pathData="M48,50a4,4 0,1 0,4 4A4,4 0,0 0,48 50ZM60,54A12,12 0,1 0,42 64.38l2,-3.48a8,8 0,1 1,8 0l2,3.48A12,12 0,0 0,60 54ZM48,34A20,20 0,0 0,38 71.3l2,-3.46a16,16 0,1 1,16 0l2,3.46A20,20 0,0 0,48 34Z"/>
|
||||||
|
<path android:fillColor="#000" android:pathData="M59.3,50a12,12 0,1 0,0 8H68v8h8V58h4V50ZM48,58a4,4 0,1 1,4 -4A4,4 0,0 1,48 58Z"/>
|
||||||
|
</vector>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user