diff --git a/.circleci/config.yml b/.circleci/config.yml
index 7bbc304e..93499a59 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -3,7 +3,7 @@ jobs:
build:
working_directory: ~/code
docker:
- - image: circleci/android:api-30
+ - image: cimg/android:2023.02.1
environment:
GRADLE_OPTS: -Dorg.gradle.workers.max=1 -Dorg.gradle.daemon=false -Dkotlin.compiler.execution.strategy="in-process"
steps:
diff --git a/README.md b/README.md
index b5b3ec33..07e1c128 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,19 @@
# VPN Hotspot
[](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/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)
Connecting things to your VPN made simple. Share your VPN connection over hotspot or repeater. (**root required**)
-
,
-sign up for beta
+
+| 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:
@@ -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.
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.
- - (since Android 9) Android Netd Service:
+ - Android Netd Service:
Let your system handle masquerade.
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.
@@ -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
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.
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.
@@ -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 only meant to be an index.
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)
* (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/TetheringManager$TetheringEventCallback;->onTetherableInterfaceRegexpsChanged(Landroid/net/TetheringManager$TetheringInterfaceRegexps;)V,blocked`
-* (since API 30) `Landroid/net/TetheringManager;->TETHERING_WIGIG:I,blocked`
-* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setUserConfiguration(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,blocked`
+* (since API 31) `Landroid/net/TetheringManager$TetheringEventCallback;->onSupportedTetheringTypes(Ljava/util/Set;)V,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/SoftApInfo;->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;->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`
-* (since API 23, 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`
-* (since API 23, 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`
-* (since API 28, 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`
-* (prior to API 26) `Landroid/net/wifi/WifiManager;->setWifiApEnabled(Landroid/net/wifi/WifiConfiguration;Z)Z`
+* (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_5GHZ:I,lo-prio,max-target-o`
+* (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;->apBand:I,unsupported`
+* (prior to API 30) `Landroid/net/wifi/WifiConfiguration;->apChannel:I,unsupported`
+* (prior to API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onNumClientsChanged(I)V,greylist-max-o`
+* `Landroid/net/wifi/WifiManager;->cancelLocalOnlyHotspotRequest()V,unsupported`
* `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`
-* `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`
-* (since API 28, prior to API 30) `Landroid/provider/Settings$Global;->SOFT_AP_TIMEOUT_ENABLED:Ljava/lang/String;,lo-prio,max-target-o`
+* `Landroid/net/wifi/p2p/WifiP2pManager;->startWps(Landroid/net/wifi/p2p/WifiP2pManager$Channel;Landroid/net/wifi/WpsInfo;Landroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,unsupported`
+* (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_usb_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`
-* (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`
-* (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 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 29) `Ldalvik/system/VMDebug;->allowHiddenApiReflectionFrom(Ljava/lang/Class;)V,unsupported`
-* (since API 26) `Ljava/lang/invoke/MethodHandles$Lookup;->(Ljava/lang/Class;I)V,unsupported`
-* (since API 26) `Ljava/lang/invoke/MethodHandles$Lookup;->ALL_MODES:I,lo-prio,max-target-o`
+* `Ljava/lang/invoke/MethodHandles$Lookup;->(Ljava/lang/Class;I)V,unsupported`
+* `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`
Hidden whitelisted APIs: (same catch as above, however, things in this list are less likely to be broken)
-* (since API 24) `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/BluetoothPan;->isTetheringOn()Z,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 24, prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;->()V,sdk,system-api,test-api`
-* (since API 24, 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`
-* (since API 24, 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$OnStartTetheringCallback;->()V,sdk,system-api,test-api`
+* (prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringFailed()V,sdk,system-api,test-api`
+* (prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringStarted()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`
+* (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;->getAllRoutes()Ljava/util/List;,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;->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`
-* (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_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_NCM:I,sdk,system-api,test-api`
-* (since API 24) `Landroid/net/TetheringManager;->TETHERING_USB:I,sdk,system-api,test-api`
-* (since API 24) `Landroid/net/TetheringManager;->TETHERING_WIFI:I,sdk,system-api,test-api`
+* `Landroid/net/TetheringManager;->TETHERING_USB:I,sdk,system-api,test-api`
+* `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`
* (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`
@@ -242,48 +244,63 @@ Greylisted/blacklisted APIs or internal constants: (some constants are hardcoded
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->()V,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->(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 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;->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`
* (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 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`
* (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 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 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 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;->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;->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_5GHZ: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 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_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 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;->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 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 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 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 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 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 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 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 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`
* (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 30) `Landroid/net/wifi/SoftApInfo;->getBandwidth()I,system-api,whitelist`
-* (since API 30) `Landroid/net/wifi/SoftApInfo;->getFrequency()I,system-api,whitelist`
+* (since API 30) `Landroid/net/wifi/SoftApInfo;->getFrequency()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`
* (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`
* (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 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 28) `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;->SAP_START_FAILURE_*: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`
* (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 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`
* (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 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/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`
@@ -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_wigig_regexs`
* (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`
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;
-(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.
Undocumented system binaries are all bundled and executable:
-* (since API 24) `iptables-save`, `ip6tables-save`;
+* `iptables-save`, `ip6tables-save`;
* `echo`;
* `/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 `);
* `sh`;
* `su`.
diff --git a/build.gradle.kts b/build.gradle.kts
index eb766806..e1b38106 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,31 +1,13 @@
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 {
- repositories {
- google()
- mavenCentral()
- }
-
dependencies {
- classpath(kotlin("gradle-plugin", "1.5.10"))
- classpath("com.android.tools.build:gradle:7.0.0-beta03")
- classpath("com.google.firebase:firebase-crashlytics-gradle:2.6.1")
- classpath("com.google.android.gms:oss-licenses-plugin:0.10.4")
- classpath("com.google.gms:google-services:4.3.8")
+ classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.4")
+ classpath("com.google.android.gms:oss-licenses-plugin:0.10.6")
+ classpath("com.google.gms:google-services:4.3.15")
}
}
-
-allprojects {
- repositories {
- google()
- jcenter()
- mavenCentral()
- maven("https://jitpack.io")
- }
-}
-
-tasks.register("clean") {
- delete(rootProject.buildDir)
-}
diff --git a/detekt.yml b/detekt.yml
index b5534fe8..8b24ef89 100644
--- a/detekt.yml
+++ b/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:
active: false
@@ -19,6 +19,16 @@ complexity:
ignoreSingleWhenExpression: false
ignoreSimpleWhenEntries: false
ignoreNestingFunctions: false
+ nestingFunctions:
+ - 'also'
+ - 'apply'
+ - 'forEach'
+ - 'isNotNull'
+ - 'ifNull'
+ - 'let'
+ - 'run'
+ - 'use'
+ - 'with'
LabeledExpression:
active: false
LargeClass:
@@ -33,9 +43,11 @@ complexity:
constructorThreshold: 7
ignoreDefaultParameters: true
ignoreDataClasses: true
- ignoreAnnotated: []
+ ignoreAnnotatedParameter: []
MethodOverloading:
active: false
+ NamedArguments:
+ active: false
NestedBlockDepth:
active: true
threshold: 4
@@ -64,8 +76,12 @@ coroutines:
active: true
GlobalCoroutineUsage:
active: false
+ InjectDispatcher:
+ active: false
RedundantSuspendModifier:
active: true
+ SleepInsteadOfDelay:
+ active: true
SuspendFunWithFlowReturnType:
active: true
@@ -108,13 +124,19 @@ exceptions:
active: true
ExceptionRaisedInUnexpectedLocation:
active: true
- methodNames: [toString, hashCode, equals, finalize]
+ methodNames:
+ - 'equals'
+ - 'finalize'
+ - 'hashCode'
+ - 'toString'
InstanceOfCheckForException:
active: false
NotImplementedDeclaration:
active: true
+ ObjectExtendsThrowable:
+ active: true
PrintStackTrace:
- active: false
+ active: true
RethrowCaughtException:
active: false
ReturnFromFinally:
@@ -123,10 +145,10 @@ exceptions:
SwallowedException:
active: true
ignoredExceptionTypes:
- - InterruptedException
- - NumberFormatException
- - ParseException
- - MalformedURLException
+ - 'InterruptedException'
+ - 'MalformedURLException'
+ - 'NumberFormatException'
+ - 'ParseException'
allowedExceptionNameRegex: '_|(ignore|expected).*'
ThrowingExceptionFromFinally:
active: false
@@ -136,9 +158,15 @@ exceptions:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
exceptions:
- - IllegalArgumentException
- - IllegalStateException
- - IOException
+ - 'ArrayIndexOutOfBoundsException'
+ - 'Exception'
+ - 'IllegalArgumentException'
+ - 'IllegalMonitorStateException'
+ - 'IllegalStateException'
+ - 'IndexOutOfBoundsException'
+ - 'NullPointerException'
+ - 'RuntimeException'
+ - 'Throwable'
ThrowingNewInstanceOfSameException:
active: true
TooGenericExceptionCaught:
@@ -146,10 +174,10 @@ exceptions:
TooGenericExceptionThrown:
active: true
exceptionNames:
- - Error
- - Exception
- - Throwable
- - RuntimeException
+ - 'Error'
+ - 'Exception'
+ - 'RuntimeException'
+ - 'Throwable'
formatting:
active: true
@@ -179,11 +207,13 @@ formatting:
ImportOrdering:
active: true
autoCorrect: true
- layout: 'idea'
+ layout: '*,java.**,javax.**,kotlin.**,^'
Indentation:
active: false
MaximumLineLength:
- active: false
+ active: true
+ maxLineLength: 120
+ ignoreBackTickedIdentifier: false
ModifierOrdering:
active: true
autoCorrect: true
@@ -229,6 +259,9 @@ formatting:
autoCorrect: true
ParameterListWrapping:
active: false
+ SpacingAroundAngleBrackets:
+ active: true
+ autoCorrect: true
SpacingAroundColon:
active: true
autoCorrect: true
@@ -256,6 +289,9 @@ formatting:
SpacingAroundRangeOperator:
active: true
autoCorrect: true
+ SpacingAroundUnaryOperator:
+ active: true
+ autoCorrect: true
SpacingBetweenDeclarationsWithAnnotations:
active: false
SpacingBetweenDeclarationsWithComments:
@@ -266,6 +302,8 @@ formatting:
naming:
active: true
+ BooleanPropertyNaming:
+ active: false
ClassNaming:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
@@ -297,7 +335,6 @@ naming:
functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)'
excludeClassPattern: '$^'
ignoreOverridden: true
- ignoreAnnotated: ['Composable']
FunctionParameterNaming:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
@@ -307,11 +344,17 @@ naming:
InvalidPackageDeclaration:
active: true
rootPackage: ''
+ LambdaParameterNaming:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ parameterPattern: '[a-z][A-Za-z0-9]*|_'
MatchingDeclarationName:
active: true
mustBeFirst: true
MemberNameEqualsClassName:
active: false
+ NoNameShadowing:
+ active: true
NonBooleanPropertyPrefixedWithIs:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
@@ -327,7 +370,7 @@ naming:
packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
TopLevelPropertyNaming:
active: true
- excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ excludes: ['buildSrc/**', '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
constantPattern: '[A-Z][_A-Z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
@@ -359,14 +402,26 @@ performance:
potential-bugs:
active: true
+ AvoidReferentialEquality:
+ active: true
+ forbiddenTypePatterns:
+ - 'kotlin.String'
+ CastToNullableType:
+ active: false
Deprecation:
active: true
+ DontDowncastCollectionTypes:
+ active: true
+ DoubleMutabilityForCollection:
+ active: true
DuplicateCaseInWhenExpression:
active: true
EqualsAlwaysReturnsTrueOrFalse:
active: true
EqualsWithHashCodeExist:
active: true
+ ExitOutsideMain:
+ active: true
ExplicitGarbageCollectionCall:
active: true
HasPlatformType:
@@ -374,7 +429,11 @@ potential-bugs:
IgnoredReturnValue:
active: true
restrictToAnnotatedMethods: true
- returnValueAnnotations: ['*.CheckReturnValue', '*.CheckResult']
+ returnValueAnnotations:
+ - '*.CheckResult'
+ - '*.CheckReturnValue'
+ ignoreReturnValueAnnotations:
+ - '*.CanIgnoreReturnValue'
ImplicitDefaultLocale:
active: true
ImplicitUnitReturnType:
@@ -389,6 +448,9 @@ potential-bugs:
active: false
MapGetWithNotNullAssertionOperator:
active: true
+ MissingPackageDeclaration:
+ active: true
+ excludes: ['buildSrc/**', '**/*.kts']
MissingWhenCase:
active: false
NullableToStringCall:
@@ -398,15 +460,20 @@ potential-bugs:
UnconditionalJumpStatementInLoop:
active: true
UnnecessaryNotNullOperator:
- active: false
+ active: true
UnnecessarySafeCall:
- active: false
+ active: true
+ UnreachableCatchBlock:
+ active: true
UnreachableCode:
active: true
UnsafeCallOnNullableType:
active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
UnsafeCast:
- active: false
+ active: true
+ UnusedUnaryOperator:
+ active: true
UselessPostfixExpression:
active: true
WrongEqualsTypeParameter:
@@ -422,6 +489,8 @@ style:
active: false
DataClassShouldBeImmutable:
active: false
+ DestructuringDeclarationWithTooManyEntries:
+ active: false
EqualsNullCall:
active: true
EqualsOnSignatureLine:
@@ -435,18 +504,27 @@ style:
includeLineWrapping: false
ForbiddenComment:
active: true
- values: ['TODO:', 'FIXME:', 'STOPSHIP:']
+ values:
+ - 'FIXME:'
+ - 'STOPSHIP:'
+ - 'TODO:'
allowedPatterns: ''
+ customMessage: ''
ForbiddenImport:
active: true
imports: []
forbiddenPatterns: ''
ForbiddenMethodCall:
active: true
- methods: ['kotlin.io.println', 'kotlin.io.print']
+ methods:
+ - 'kotlin.io.print'
+ - 'kotlin.io.println'
ForbiddenPublicDataClass:
active: true
- ignorePackages: ['*.internal', '*.internal.*']
+ excludes: ['**']
+ ignorePackages:
+ - '*.internal'
+ - '*.internal.*'
ForbiddenVoid:
active: true
ignoreOverridden: true
@@ -454,12 +532,14 @@ style:
FunctionOnlyReturningConstant:
active: true
ignoreOverridableFunction: true
- excludedFunctions: 'describeContents'
- excludeAnnotatedFunction: ['dagger.Provides']
+ ignoreActualFunction: true
+ excludedFunctions: ''
LibraryCodeMustSpecifyReturnType:
active: true
+ excludes: ['**']
LibraryEntitiesShouldNotBePublic:
active: true
+ excludes: ['**']
LoopWithTooManyJumpStatements:
active: true
maxJumpCount: 1
@@ -479,12 +559,16 @@ style:
active: true
ModifierOrder:
active: true
+ MultilineLambdaItParameter:
+ active: false
NestedClassesVisibility:
active: true
NewLineAtEndOfFile:
active: true
NoTabs:
active: true
+ ObjectLiteralToLambda:
+ active: true
OptionalAbstractKeyword:
active: true
OptionalUnit:
@@ -497,6 +581,8 @@ style:
active: true
RedundantExplicitType:
active: true
+ RedundantHigherOrderMapUsage:
+ active: true
RedundantVisibilityModifierRule:
active: true
ReturnCount:
@@ -510,6 +596,7 @@ style:
ThrowsCount:
active: true
max: 2
+ excludeGuardClauses: false
TrailingWhitespace:
active: true
UnderscoresInNumericLiterals:
@@ -520,6 +607,8 @@ style:
active: true
UnnecessaryApply:
active: true
+ UnnecessaryFilter:
+ active: true
UnnecessaryInheritance:
active: true
UnnecessaryLet:
@@ -535,6 +624,8 @@ style:
UnusedPrivateMember:
active: true
allowedNames: '(_|ignored|expected|serialVersionUID)'
+ UseAnyOrNoneInsteadOfFind:
+ active: true
UseArrayLiteralsInAnnotations:
active: true
UseCheckNotNull:
@@ -545,8 +636,14 @@ style:
active: false
UseEmptyCounterpart:
active: true
+ UseIfEmptyOrIfBlank:
+ active: true
UseIfInsteadOfWhen:
active: false
+ UseIsNullOrEmpty:
+ active: true
+ UseOrEmpty:
+ active: true
UseRequire:
active: true
UseRequireNotNull:
diff --git a/gradle.properties b/gradle.properties
index 6fb3e361..de5a2782 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -13,6 +13,7 @@ android.databinding.incremental=true
android.enableJetifier=true
android.enableR8.fullMode=true
android.enableResourceOptimizations=false
+android.nonTransitiveRClass=true
android.useAndroidX=true
kapt.incremental.apt=true
org.gradle.jvmargs=-Xmx1536m
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index e708b1c0..ccebba77 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 0f80bbf5..fc10b601 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,6 @@
distributionBase=GRADLE_USER_HOME
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
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 4f906e0c..79a61d42 100755
--- a/gradlew
+++ b/gradlew
@@ -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");
# 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
-# 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"
-APP_BASE_NAME=`basename "$0"`
+# Resolve links: $0 may be a link
+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.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
warn () {
echo "$*"
-}
+} >&2
die () {
echo
echo "$*"
echo
exit 1
-}
+} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
- NONSTOP* )
- nonstop=true
- ;;
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
esac
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 [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACMD=$JAVA_HOME/jre/sh/java
else
- JAVACMD="$JAVA_HOME/bin/java"
+ JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
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."
fi
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.
Please set the JAVA_HOME variable in your environment to match the
@@ -106,80 +140,105 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
-fi
-
-# 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" ;;
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
-# Escape application args
-save () {
- for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
- echo " "
-}
-APP_ARGS=`save "$@"`
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --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
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+# For Cygwin or MSYS, switch paths to Windows format before running java
+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" "$@"
diff --git a/gradlew.bat b/gradlew.bat
index ac1b06f9..6689b85b 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
-@if "%DEBUG%" == "" @echo off
+@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +25,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto execute
+if %ERRORLEVEL% equ 0 goto execute
echo.
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
@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
+if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
diff --git a/mobile/build.gradle.kts b/mobile/build.gradle.kts
index 89472845..eede36a7 100644
--- a/mobile/build.gradle.kts
+++ b/mobile/build.gradle.kts
@@ -9,23 +9,24 @@ plugins {
}
android {
- val javaVersion = JavaVersion.VERSION_1_8
- val targetSdk = 29
- buildToolsVersion = "31.0.0-rc4"
+ namespace = "be.mygod.vpnhotspot"
+
+ val javaVersion = 11
+ buildToolsVersion = "33.0.2"
compileOptions {
isCoreLibraryDesugaringEnabled = true
- sourceCompatibility = javaVersion
- targetCompatibility = javaVersion
+ sourceCompatibility(javaVersion)
+ targetCompatibility(javaVersion)
}
- compileSdkPreview = "android-S"
- kotlinOptions.jvmTarget = javaVersion.toString()
+ kotlin.jvmToolchain(javaVersion)
+ compileSdk = 33
defaultConfig {
applicationId = "be.mygod.vpnhotspot"
- minSdk = 21
- if (targetSdk == 31) targetSdkPreview = "S" else this.targetSdk = targetSdk
- resourceConfigurations.addAll(arrayOf("it", "ru", "zh-rCN", "zh-rTW"))
- versionCode = 260
- versionName = "2.11.7"
+ minSdk = 28
+ targetSdk = 33
+ resourceConfigurations.addAll(arrayOf("it", "pt-rBR", "ru", "zh-rCN", "zh-rTW"))
+ versionCode = 1000
+ versionName = "2.16.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions.annotationProcessorOptions.arguments.apply {
put("room.expandProjection", "true")
@@ -33,9 +34,9 @@ android {
put("room.schemaLocation", "$projectDir/schemas")
}
buildConfigField("boolean", "DONATIONS", "true")
- buildConfigField("int", "TARGET_SDK", targetSdk.toString())
}
buildFeatures {
+ buildConfig = true
dataBinding = true
viewBinding = true
}
@@ -57,6 +58,7 @@ android {
}
create("google") {
dimension = "freedom"
+ versionNameSuffix = "-g"
buildConfigField("boolean", "DONATIONS", "false")
}
}
@@ -64,37 +66,39 @@ android {
}
dependencies {
- val lifecycleVersion = "2.3.1"
- val roomVersion = "2.3.0"
+ val lifecycleVersion = "2.6.0-rc01"
+ 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")
implementation(kotlin("stdlib-jdk8"))
- implementation("androidx.appcompat:appcompat:1.3.0") // https://issuetracker.google.com/issues/151603528
- implementation("androidx.browser:browser:1.3.0")
- implementation("androidx.core:core-ktx:1.6.0-beta01")
- implementation("androidx.emoji:emoji:1.1.0")
- implementation("androidx.fragment:fragment-ktx:1.3.4")
- implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
+ implementation("androidx.browser:browser:1.5.0")
+ implementation("androidx.core:core-ktx:1.9.0")
+ implementation("androidx.fragment:fragment-ktx:1.5.5")
implementation("androidx.lifecycle:lifecycle-livedata-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.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.material:material:1.4.0-beta01")
- implementation("com.google.firebase:firebase-analytics-ktx:19.0.0")
- implementation("com.google.firebase:firebase-crashlytics:18.0.0")
- implementation("com.google.zxing:core:3.4.1")
- implementation("com.jakewharton.timber:timber:4.7.1")
- implementation("com.linkedin.dexmaker:dexmaker:2.28.1")
+ implementation("com.google.android.material:material:1.8.0")
+ implementation("com.google.firebase:firebase-analytics-ktx:21.2.0")
+ implementation("com.google.firebase:firebase-crashlytics:18.3.5")
+ implementation("com.google.zxing:core:3.5.1")
+ implementation("com.jakewharton.timber:timber:5.0.1")
+ implementation("com.linkedin.dexmaker:dexmaker:2.28.3")
implementation("com.takisoft.preferencex:preferencex-simplemenu:1.1.0")
- implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.4")
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0")
+ implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5")
+ 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")
androidTestImplementation("androidx.room:room-testing:$roomVersion")
- androidTestImplementation("androidx.test:runner:1.3.0")
- androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0")
- androidTestImplementation("androidx.test.ext:junit-ktx:1.1.2")
+ androidTestImplementation("androidx.test:runner:1.5.2")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
+ androidTestImplementation("androidx.test.ext:junit-ktx:1.1.5")
}
diff --git a/mobile/lint.xml b/mobile/lint.xml
index 069ccd81..1afc060c 100644
--- a/mobile/lint.xml
+++ b/mobile/lint.xml
@@ -4,5 +4,6 @@
+
diff --git a/mobile/proguard-rules.pro b/mobile/proguard-rules.pro
index b49c0c57..97f6f95a 100644
--- a/mobile/proguard-rules.pro
+++ b/mobile/proguard-rules.pro
@@ -20,10 +20,3 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-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[]);
-}
diff --git a/mobile/schemas/be.mygod.vpnhotspot.room.AppDatabase/2.json b/mobile/schemas/be.mygod.vpnhotspot.room.AppDatabase/2.json
index 0ff4cdb6..7bedd0ba 100644
--- a/mobile/schemas/be.mygod.vpnhotspot.room.AppDatabase/2.json
+++ b/mobile/schemas/be.mygod.vpnhotspot.room.AppDatabase/2.json
@@ -34,10 +34,10 @@
}
],
"primaryKey": {
+ "autoGenerate": false,
"columnNames": [
"mac"
- ],
- "autoGenerate": false
+ ]
},
"indices": [],
"foreignKeys": []
@@ -114,10 +114,10 @@
}
],
"primaryKey": {
+ "autoGenerate": true,
"columnNames": [
"id"
- ],
- "autoGenerate": true
+ ]
},
"indices": [
{
@@ -126,7 +126,8 @@
"columnNames": [
"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": [
@@ -144,9 +145,10 @@
]
}
],
+ "views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
- "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"92a6c0406ed7265dbd98eb3c24095651\")"
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '92a6c0406ed7265dbd98eb3c24095651')"
]
}
}
\ No newline at end of file
diff --git a/mobile/src/freedom/java/be/mygod/vpnhotspot/util/UpdateChecker.kt b/mobile/src/freedom/java/be/mygod/vpnhotspot/util/UpdateChecker.kt
new file mode 100644
index 00000000..69fe6576
--- /dev/null
+++ b/mobile/src/freedom/java/be/mygod/vpnhotspot/util/UpdateChecker.kt
@@ -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 {
+ 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 {
+ 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()
+}
diff --git a/mobile/src/google/java/be/mygod/vpnhotspot/util/UpdateChecker.kt b/mobile/src/google/java/be/mygod/vpnhotspot/util/UpdateChecker.kt
new file mode 100644
index 00000000..bf63a8ef
--- /dev/null
+++ b/mobile/src/google/java/be/mygod/vpnhotspot/util/UpdateChecker.kt
@@ -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)
+ }
+ }
+}
diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml
index e694330c..980ab420 100644
--- a/mobile/src/main/AndroidManifest.xml
+++ b/mobile/src/main/AndroidManifest.xml
@@ -1,7 +1,6 @@
+ xmlns:tools="http://schemas.android.com/tools">
-
@@ -24,11 +20,15 @@
+
+
@@ -37,8 +37,11 @@
tools:ignore="ProtectedPermissions" />
+
+
@@ -49,11 +52,22 @@
tools:ignore="ProtectedPermissions"/>
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -71,7 +86,6 @@
@@ -88,8 +102,7 @@
+ android:foregroundServiceType="location|connectedDevice"/>
+ android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
@@ -117,12 +129,10 @@
+ android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
@@ -136,11 +146,13 @@
android:exported="true"
android:icon="@drawable/ic_device_wifi_tethering"
android:label="@string/tethering_manage_wifi"
- android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
- tools:targetApi="24">
+ android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
+
+ android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
+
+ android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/mobile/src/main/java/be/mygod/librootkotlinx/AppProcess.kt b/mobile/src/main/java/be/mygod/librootkotlinx/AppProcess.kt
deleted file mode 100644
index 4d53e722..00000000
--- a/mobile/src/main/java/be/mygod/librootkotlinx/AppProcess.kt
+++ /dev/null
@@ -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, 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. (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 {
- 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"
- }
-}
diff --git a/mobile/src/main/java/be/mygod/librootkotlinx/Logger.kt b/mobile/src/main/java/be/mygod/librootkotlinx/Logger.kt
deleted file mode 100644
index 9207a290..00000000
--- a/mobile/src/main/java/be/mygod/librootkotlinx/Logger.kt
+++ /dev/null
@@ -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)
- }
-}
diff --git a/mobile/src/main/java/be/mygod/librootkotlinx/RootServer.kt b/mobile/src/main/java/be/mygod/librootkotlinx/RootServer.kt
deleted file mode 100644
index 845f825b..00000000
--- a/mobile/src/main/java/be/mygod/librootkotlinx/RootServer.kt
+++ /dev/null
@@ -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(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) : 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) : Callback(server, index, classLoader) {
- val finish: CompletableDeferred = 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? = null
- private val callbackLookup = LongSparseArray()
-
- 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 execute(command: RootCommand) =
- execute(command, T::class.java.classLoader)
- @Throws(RemoteException::class)
- suspend fun execute(command: RootCommand, classLoader: ClassLoader?): T {
- val future = CompletableDeferred()
- val callback = synchronized(callbackLookup) {
- @Suppress("UNCHECKED_CAST")
- val callback = Callback.Ordinary(this, counter, classLoader, future as CompletableDeferred)
- 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 create(command: RootCommandChannel, scope: CoroutineScope) =
- create(command, scope, T::class.java.classLoader)
- @ExperimentalCoroutinesApi
- @Throws(RemoteException::class)
- fun create(command: RootCommandChannel, scope: CoroutineScope,
- classLoader: ClassLoader?) = scope.produce(
- 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)
- 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 DataInputStream.readParcelable(
- classLoader: ClassLoader? = T::class.java.classLoader) = readByteArray().toParcelable(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) {
- 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) {
- 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(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")
- }
- }
-}
diff --git a/mobile/src/main/java/be/mygod/librootkotlinx/RootSession.kt b/mobile/src/main/java/be/mygod/librootkotlinx/RootSession.kt
deleted file mode 100644
index e9a3689b..00000000
--- a/mobile/src/main/java/be/mygod/librootkotlinx/RootSession.kt
+++ /dev/null
@@ -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 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()
- }
- }
-}
diff --git a/mobile/src/main/java/be/mygod/librootkotlinx/ServerCommands.kt b/mobile/src/main/java/be/mygod/librootkotlinx/ServerCommands.kt
deleted file mode 100644
index f5978ba1..00000000
--- a/mobile/src/main/java/be/mygod/librootkotlinx/ServerCommands.kt
+++ /dev/null
@@ -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 : 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
-
-/**
- * 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 : 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
-}
-
-@Parcelize
-internal data class CancelCommand(val index: Long) : RootCommandOneWay {
- override suspend fun execute() = error("Internal implementation")
-}
-
-@Parcelize
-internal class Shutdown : Parcelable
diff --git a/mobile/src/main/java/be/mygod/librootkotlinx/Utils.kt b/mobile/src/main/java/be/mygod/librootkotlinx/Utils.kt
deleted file mode 100644
index 290043d2..00000000
--- a/mobile/src/main/java/be/mygod/librootkotlinx/Utils.kt
+++ /dev/null
@@ -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) : 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) : 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 {
- 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
-
-@SuppressLint("Recycle")
-inline fun 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 ByteArray.toParcelable(classLoader: ClassLoader? = T::class.java.classLoader) =
- useParcel { p ->
- p.unmarshall(this, 0, size)
- p.setDataPosition(0)
- p.readParcelable(classLoader)
- }
-
-// Stream closed caused in NullOutputStream
-val IOException.isEBADF get() = message == "Stream closed" || (cause as? ErrnoException)?.errno == OsConstants.EBADF
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/AlertDialogFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/AlertDialogFragment.kt
index c20b1ace..0f607081 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/AlertDialogFragment.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/AlertDialogFragment.kt
@@ -10,6 +10,7 @@ import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.setFragmentResultListener
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.parcelize.Parcelize
/**
@@ -44,7 +45,7 @@ abstract class AlertDialogFragment :
fun key(resultKey: String = javaClass.name) = args().putString(KEY_RESULT, resultKey)
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) {
setFragmentResult(resultKey ?: return, Bundle().apply {
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/App.kt b/mobile/src/main/java/be/mygod/vpnhotspot/App.kt
index 714a162e..40c2287c 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/App.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/App.kt
@@ -2,18 +2,20 @@ package be.mygod.vpnhotspot
import android.annotation.SuppressLint
import android.app.Application
+import android.content.ActivityNotFoundException
import android.content.ClipboardManager
+import android.content.Context
+import android.content.Intent
import android.content.res.Configuration
+import android.location.LocationManager
import android.os.Build
+import android.provider.Settings
import android.util.Log
+import android.widget.Toast
import androidx.annotation.Size
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
-import androidx.core.content.ContextCompat
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 be.mygod.librootkotlinx.NoShellException
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.util.DeviceStorageApp
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.analytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
@@ -39,15 +42,15 @@ class App : Application() {
lateinit var app: App
}
+ @SuppressLint("RestrictedApi")
override fun onCreate() {
super.onCreate()
app = this
- if (Build.VERSION.SDK_INT >= 24) @SuppressLint("RestrictedApi") {
- deviceStorage = DeviceStorageApp(this)
- // alternative to PreferenceManager.getDefaultSharedPreferencesName(this)
- deviceStorage.moveSharedPreferencesFrom(this, PreferenceManager(this).sharedPreferencesName)
- deviceStorage.moveDatabaseFrom(this, AppDatabase.DB_NAME)
- } else deviceStorage = this
+ deviceStorage = DeviceStorageApp(this)
+ // alternative to PreferenceManager.getDefaultSharedPreferencesName(this)
+ deviceStorage.moveSharedPreferencesFrom(this, PreferenceManager(this).sharedPreferencesName)
+ deviceStorage.moveDatabaseFrom(this, AppDatabase.DB_NAME)
+ BootReceiver.migrateIfNecessary()
Services.init { this }
// 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" -> { }
else -> FirebaseCrashlytics.getInstance().apply {
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() {
@@ -78,18 +81,6 @@ class App : Application() {
}
})
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)
}
@@ -116,6 +107,21 @@ class App : Application() {
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 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
val english by lazy {
createConfigurationContext(Configuration(resources.configuration).apply {
@@ -124,16 +130,17 @@ class App : Application() {
}
val pref by lazy { PreferenceManager.getDefaultSharedPreferences(deviceStorage) }
val clipboard by lazy { getSystemService()!! }
+ val location by lazy { getSystemService() }
val hasTouch by lazy { packageManager.hasSystemFeature("android.hardware.faketouch") }
val customTabsIntent by lazy {
CustomTabsIntent.Builder().apply {
setColorScheme(CustomTabsIntent.COLOR_SCHEME_SYSTEM)
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())
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()
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/BootReceiver.kt b/mobile/src/main/java/be/mygod/vpnhotspot/BootReceiver.kt
index 7bdd97aa..3540e698 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/BootReceiver.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/BootReceiver.kt
@@ -5,31 +5,114 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
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.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() {
companion object {
+ const val KEY = "service.autoStart"
+
private val componentName by lazy { ComponentName(app, BootReceiver::class.java) }
- var enabled: Boolean
+ private var enabled: Boolean
get() = app.packageManager.getComponentEnabledSetting(componentName) ==
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
set(value) = app.packageManager.setComponentEnabledSetting(componentName,
if (value) PackageManager.COMPONENT_ENABLED_STATE_ENABLED
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 add(value: Startable) = add(T::class.java.name, value)
+ inline fun 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 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 = mutableMapOf()) : Parcelable
+
override fun onReceive(context: Context, intent: Intent) {
- if (started) return
when (intent.action) {
- Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_LOCKED_BOOT_COMPLETED -> started = true
- else -> return
- }
- if (Services.p2p != null) {
- ContextCompat.startForegroundService(context, Intent(context, RepeaterService::class.java))
+ Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_LOCKED_BOOT_COMPLETED, Intent.ACTION_MY_PACKAGE_REPLACED -> {
+ if (userEnabled) startIfNecessary() else enabled = false
+ }
}
}
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/EBegFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/EBegFragment.kt
index 66b37dbf..c85588dc 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/EBegFragment.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/EBegFragment.kt
@@ -23,26 +23,30 @@ import timber.log.Timber
*/
class EBegFragment : AppCompatDialogFragment() {
companion object : BillingClientStateListener, PurchasesUpdatedListener {
- private lateinit var billingClient: BillingClient
-
- fun init() {
- billingClient = BillingClient.newBuilder(app).apply {
+ private val billingClient by lazy {
+ BillingClient.newBuilder(app).apply {
enablePendingPurchases()
- }.setListener(this).build().also { it.startConnection(this) }
+ }.setListener(this).build()
}
+ private var instance: EBegFragment? = null
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
Timber.e("onBillingSetupFinished: ${billingResult.responseCode}")
- } else GlobalScope.launch(Dispatchers.Main.immediate) {
- val result = billingClient.queryPurchasesAsync(BillingClient.SkuType.INAPP)
- onPurchasesUpdated(result.billingResult, result.purchasesList)
+ } else {
+ instance?.onBillingConnected()
+ 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() {
Timber.e("onBillingServiceDisconnected")
- billingClient.startConnection(this)
+ if (instance != null) billingClient.startConnection(this)
}
override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List?) {
@@ -64,12 +68,12 @@ class EBegFragment : AppCompatDialogFragment() {
}
private lateinit var binding: FragmentEbegBinding
- private var skus: List? = null
+ private var productDetails: List? = null
set(value) {
field = value
binding.donationsGoogleAndroidMarketSpinner.apply {
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)
setAdapter(adapter)
}
@@ -82,27 +86,46 @@ class EBegFragment : AppCompatDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
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 {
- val sku = skus?.getOrNull(binding.donationsGoogleAndroidMarketSpinner.selectedItemPosition)
- if (sku != null) billingClient.launchBillingFlow(requireActivity(), BillingFlowParams.newBuilder().apply {
- setSkuDetails(sku)
+ val product = productDetails?.getOrNull(binding.donationsGoogleAndroidMarketSpinner.selectedItemPosition)
+ if (product != null) billingClient.launchBillingFlow(requireActivity(), BillingFlowParams.newBuilder().apply {
+ setProductDetailsParamsList(listOf(BillingFlowParams.ProductDetailsParams.newBuilder().apply {
+ setProductDetails(product)
+ }.build()))
}.build()) else SmartSnackbar.make(R.string.donations__google_android_market_not_supported).show()
}
- @Suppress("ConstantConditionIf")
if (BuildConfig.DONATIONS) (binding.donationsMoreStub.inflate() as Button).setOnClickListener {
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()
+ }
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/IpNeighbourMonitoringService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/IpNeighbourMonitoringService.kt
index 9a25e821..c93bd864 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/IpNeighbourMonitoringService.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/IpNeighbourMonitoringService.kt
@@ -15,7 +15,7 @@ abstract class IpNeighbourMonitoringService : Service(), IpNeighbourMonitor.Call
this.neighbours = neighbours
updateNotification()
}
- protected fun updateNotification() {
+ protected open fun updateNotification() {
val sizeLookup = neighbours.groupBy { it.dev }.mapValues { (_, neighbours) ->
neighbours
.filter { it.ip is Inet4Address && it.state == IpNeighbour.State.VALID }
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt
index 97f3f757..5baf4076 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt
@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot
+import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.wifi.WifiManager
@@ -8,14 +9,12 @@ import androidx.annotation.RequiresApi
import be.mygod.librootkotlinx.RootServer
import be.mygod.vpnhotspot.App.Companion.app
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.TetherTimeoutMonitor
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
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.RootManager
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.widget.SmartSnackbar
import kotlinx.coroutines.*
+import kotlinx.parcelize.Parcelize
import timber.log.Timber
import java.net.Inet4Address
-@RequiresApi(26)
class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
companion object {
const val KEY_USE_SYSTEM = "service.tempHotspot.useSystem"
@@ -50,7 +49,15 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
null -> return // stopped
"" -> 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 var reservation: Reservation? = null
set(value) {
+ if (value == null) field?.close()
field = value
- if (value != null && !receiverRegistered) {
+ timeoutMonitor?.close()
+ timeoutMonitor = null
+ if (value != null) {
val configuration = binder.configuration
if (Build.VERSION.SDK_INT < 30 && configuration!!.isAutoShutdownEnabled) {
timeoutMonitor = TetherTimeoutMonitor(configuration.shutdownTimeoutMillis, coroutineContext) {
value.close()
}
}
- registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
- receiverRegistered = true
}
}
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.
*/
- private val dispatcher = newSingleThreadContext("LocalOnlyHotspotService")
+ private val dispatcher = Dispatchers.IO.limitedParallelism(1)
override val coroutineContext = dispatcher + Job()
private var routingManager: RoutingManager? = 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) }
+ private var lastState: Triple? = 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 onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ BootReceiver.startIfEnabled()
if (binder.iface != null) return START_STICKY
binder.iface = ""
updateNotification() // show invisible foreground notification to avoid being killed
@@ -150,6 +155,11 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
return START_STICKY
}
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 {
RootManager.use {
Root(it).apply {
@@ -170,6 +180,24 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
if (reservation == null) onFailed(-2) else {
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(Starter())
+ check(routingManager == null)
+ routingManager = RoutingManager.LocalOnly(this@LocalOnlyHotspotService, iface).apply { start() }
+ IpNeighbourMonitor.registerCallback(this@LocalOnlyHotspotService)
}
override fun onStopped() {
reservation = null
@@ -191,7 +219,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
override fun onIpNeighbourAvailable(neighbours: Collection) {
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
})
}
@@ -203,6 +231,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
}
private fun stopService() {
+ BootReceiver.delete()
binder.iface = null
unregisterReceiver()
ServiceNotification.stopForeground(this)
@@ -210,22 +239,14 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
}
private fun unregisterReceiver(exit: Boolean = false) {
- if (receiverRegistered) {
- unregisterReceiver(receiver)
- IpNeighbourMonitor.unregisterCallback(this)
- if (Build.VERSION.SDK_INT >= 28) {
- timeoutMonitor?.close()
- timeoutMonitor = null
- }
- receiverRegistered = false
- }
+ IpNeighbourMonitor.unregisterCallback(this)
+ timeoutMonitor?.close()
+ timeoutMonitor = null
launch {
routingManager?.stop()
routingManager = null
- if (exit) {
- cancel()
- dispatcher.close()
- }
+ unregisterStateReceiver()
+ if (exit) cancel()
}
}
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/MainActivity.kt b/mobile/src/main/java/be/mygod/vpnhotspot/MainActivity.kt
index d2acd4ae..3b964df7 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/MainActivity.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/MainActivity.kt
@@ -4,28 +4,60 @@ import android.os.Bundle
import android.view.MenuItem
import androidx.activity.viewModels
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.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import be.mygod.vpnhotspot.client.ClientViewModel
import be.mygod.vpnhotspot.client.ClientsFragment
import be.mygod.vpnhotspot.databinding.ActivityMainBinding
import be.mygod.vpnhotspot.manage.TetheringFragment
import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
+import be.mygod.vpnhotspot.util.AppUpdate
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import be.mygod.vpnhotspot.util.Services
+import be.mygod.vpnhotspot.util.UpdateChecker
import be.mygod.vpnhotspot.widget.SmartSnackbar
+import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.navigation.NavigationBarView
+import kotlinx.coroutines.launch
+import timber.log.Timber
import java.net.Inet4Address
+import java.util.concurrent.CancellationException
class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListener {
lateinit var binding: ActivityMainBinding
+ private lateinit var updateItem: MenuItem
+ private lateinit var updateBadge: BadgeDrawable
override fun onCreate(savedInstanceState: Bundle?) {
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)
setContentView(binding.root)
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())
val model by viewModels()
lifecycle.addObserver(model)
@@ -34,38 +66,69 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
val count = clients.count {
it.ip.any { (ip, state) -> ip is Inet4Address && state == IpNeighbour.State.VALID }
}
- if (count > 0) binding.navigation.getOrCreateBadge(R.id.navigation_clients).apply {
- backgroundColor = ContextCompat.getColor(this@MainActivity, R.color.colorSecondary)
- badgeTextColor = ContextCompat.getColor(this@MainActivity, R.color.primary_text_default_material_light)
- number = count
- } else binding.navigation.removeBadge(R.id.navigation_clients)
+ badge.isVisible = count > 0
+ badge.number = count
}
SmartSnackbar.Register(binding.fragmentHolder)
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) {
R.id.navigation_clients -> {
- if (!item.isChecked) {
- item.isChecked = true
- displayFragment(ClientsFragment())
- }
+ displayFragment(ClientsFragment())
true
}
R.id.navigation_tethering -> {
- if (!item.isChecked) {
- item.isChecked = true
- displayFragment(TetheringFragment())
- }
+ displayFragment(TetheringFragment())
true
}
R.id.navigation_settings -> {
- if (!item.isChecked) {
- item.isChecked = true
- displayFragment(SettingsPreferenceFragment())
- }
+ displayFragment(SettingsPreferenceFragment())
true
}
+ R.id.navigation_update -> {
+ lastUpdate!!.updateForResult(this, 1)
+ false
+ }
else -> false
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt
index 683848b0..c02c0945 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt
@@ -3,9 +3,13 @@ package be.mygod.vpnhotspot
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.Service
+import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
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.p2p.*
import android.os.Build
@@ -16,19 +20,27 @@ import androidx.annotation.StringRes
import androidx.core.content.edit
import be.mygod.vpnhotspot.App.Companion.app
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.wifi.SoftApConfigurationCompat
+import be.mygod.vpnhotspot.net.wifi.VendorElements
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper
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.requestGroupInfo
+import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestP2pState
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.startWps
+import be.mygod.vpnhotspot.net.wifi.WifiSsidCompat
import be.mygod.vpnhotspot.root.RepeaterCommands
import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.util.*
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.*
+import kotlinx.parcelize.Parcelize
import timber.log.Timber
import java.lang.reflect.InvocationTargetException
import java.util.concurrent.atomic.AtomicBoolean
@@ -42,12 +54,14 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
const val KEY_SAFE_MODE = "service.repeater.safeMode"
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_OPERATING_BAND = "service.repeater.band.v4"
private const val KEY_OPERATING_CHANNEL = "service.repeater.oc.v3"
private const val KEY_AUTO_SHUTDOWN = "service.repeater.autoShutdown"
private const val KEY_SHUTDOWN_TIMEOUT = "service.repeater.shutdownTimeout"
private const val KEY_DEVICE_ADDRESS = "service.repeater.mac"
+ private const val KEY_VENDOR_ELEMENTS = "service.repeater.vendorElements"
var persistentSupported = false
@@ -61,17 +75,26 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
val safeModeConfigurable get() = Build.VERSION.SDK_INT >= 29 && hasP2pValidateName
val safeMode get() = Build.VERSION.SDK_INT >= 29 &&
(!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?
- get() = app.pref.getString(KEY_NETWORK_NAME, null)
- set(value) = app.pref.edit { putString(KEY_NETWORK_NAME, value) }
+ var networkName: WifiSsidCompat?
+ get() = app.pref.getString(KEY_NETWORK_NAME, null).let { legacy ->
+ 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?
get() = app.pref.getString(KEY_PASSPHRASE, null)
set(value) = app.pref.edit { putString(KEY_PASSPHRASE, value) }
var operatingBand: Int
@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) }
var operatingChannel: Int
get() {
@@ -85,17 +108,24 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
var shutdownTimeoutMillis: Long
get() = app.pref.getLong(KEY_SHUTDOWN_TIMEOUT, 0)
set(value) = app.pref.edit { putLong(KEY_SHUTDOWN_TIMEOUT, value) }
- var deviceAddress: MacAddressCompat?
+ var deviceAddress: MacAddress?
get() = try {
- MacAddressCompat(app.pref.getLong(KEY_DEVICE_ADDRESS, MacAddressCompat.ANY_ADDRESS.addr)).run {
- validate()
- if (this == MacAddressCompat.ANY_ADDRESS) null else this
+ MacAddressCompat(app.pref.getLong(KEY_DEVICE_ADDRESS, 2)).run {
+ require(addr and ((1L shl 48) - 1).inv() == 0L)
+ if (addr == 2L) null else toPlatform()
}
} catch (e: IllegalArgumentException) {
Timber.w(e)
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
+ 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 {
@@ -110,19 +140,17 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
set(value) {
field = value
groupChanged(value)
- if (Build.VERSION.SDK_INT >= 28) value?.clientList?.let {
- timeoutMonitor?.onClientsChanged(it.isEmpty())
- }
+ value?.clientList?.let { timeoutMonitor?.onClientsChanged(it.isEmpty()) }
}
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 {
RootManager.use { it.execute(RepeaterCommands.RequestDeviceAddress()) }
} catch (e: Exception) {
Timber.d(e)
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
@@ -134,7 +162,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
val ownedGroups = filter {
if (!it.isGroupOwner) return@filter false
val address = try {
- MacAddressCompat.fromString(it.owner.deviceAddress)
+ MacAddress.fromString(it.owner.deviceAddress)
} catch (e: IllegalArgumentException) {
Timber.w(e)
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 var channel: WifiP2pManager.Channel? = null
private val binder = Binder()
@@ -212,6 +247,9 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> onP2pConnectionChanged(
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO),
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 ->
@@ -222,7 +260,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
/**
* 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()
private var routingManager: RoutingManager? = null
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(R.string.repeater_failure_disconnected).show()
}
+ @RequiresApi(33)
+ private suspend fun setVendorElements(ve: List = 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() {
channel = null
@@ -302,9 +370,34 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
- if (!safeMode) when (key) {
- KEY_OPERATING_CHANNEL -> launch { setOperatingChannel() }
- KEY_SAFE_MODE -> deinitPending.set(true)
+ when (key) {
+ KEY_OPERATING_CHANNEL -> if (!safeMode) launch { setOperatingChannel() }
+ 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
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ BootReceiver.startIfEnabled()
if (status != Status.IDLE) return START_NOT_STICKY
val channel = channel ?: return START_NOT_STICKY.also { stopSelf() }
status = Status.STARTING
// 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 {
- registerReceiver(receiver, intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
- WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION))
+ val filter = intentFilter(WifiP2pManager.WIFI_P2P_STATE_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
- try {
- p2pManager.requestGroupInfo(channel) {
- when {
- it == null -> doStart()
- it.isGroupOwner -> launch { if (routingManager == null) doStartLocked(it) }
- else -> {
- Timber.i("Removing old group ($it)")
- p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
- override fun onSuccess() {
- doStart()
- }
- override fun onFailure(reason: Int) =
- startFailure(formatReason(R.string.repeater_remove_old_group_failure, reason))
- })
+ val group = p2pManager.requestGroupInfo(channel)
+ when {
+ group == null -> doStart()
+ group.isGroupOwner -> if (routingManager == null) doStartLocked(group)
+ else -> {
+ Timber.i("Removing old group ($group)")
+ p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
+ override fun onSuccess() {
+ launch { doStart() }
}
- }
+ 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
@@ -348,56 +438,59 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
/**
* startService Step 2 (if a group isn't already available)
*/
- private fun doStart() = launch {
+ private suspend fun doStart() {
val listener = object : WifiP2pManager.ActionListener {
override fun onFailure(reason: Int) {
startFailure(formatReason(R.string.repeater_create_group_failure, reason),
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) {
binder.fetchPersistentGroup()
setOperatingChannel()
}
- val networkName = networkName
+ if (Build.VERSION.SDK_INT >= 33) setVendorElements()
+ val networkName = networkName?.toString()
val passphrase = passphrase
- try {
- if (!safeMode || networkName == null || passphrase == null) {
- persistNextGroup = true
- p2pManager.createGroup(channel, listener)
- } else @TargetApi(29) {
- p2pManager.createGroup(channel, WifiP2pConfig.Builder().apply {
+ @SuppressLint("MissingPermission") // missing permission will simply leading to returning ERROR
+ if (!safeMode || networkName == null || passphrase.isNullOrEmpty()) {
+ persistNextGroup = true
+ p2pManager.createGroup(channel, listener)
+ } else @TargetApi(29) {
+ p2pManager.createGroup(channel, WifiP2pConfig.Builder().apply {
+ try {
+ mNetworkName.set(this, networkName) // bypass networkName check
+ } catch (e: ReflectiveOperationException) {
+ Timber.w(e)
try {
- mNetworkName.set(this, networkName) // bypass networkName check
- } catch (e: ReflectiveOperationException) {
- Timber.w(e)
setNetworkName(networkName)
+ } catch (e: IllegalArgumentException) {
+ Timber.w(e)
+ return startFailure(e.readableMessage)
}
- setPassphrase(passphrase)
- when (val oc = operatingChannel) {
- 0 -> setGroupOperatingBand(when (val band = operatingBand) {
- SoftApConfigurationCompat.BAND_2GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_2GHZ
- SoftApConfigurationCompat.BAND_5GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_5GHZ
- else -> {
- require(SoftApConfigurationCompat.isLegacyEitherBand(band)) { "Unknown band $band" }
- WifiP2pConfig.GROUP_OWNER_BAND_AUTO
- }
- })
+ }
+ setPassphrase(passphrase)
+ when (val oc = operatingChannel) {
+ 0 -> setGroupOperatingBand(when (val band = operatingBand) {
+ SoftApConfigurationCompat.BAND_2GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_2GHZ
+ SoftApConfigurationCompat.BAND_5GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_5GHZ
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)
- }
- } catch (e: SecurityException) {
- Timber.w(e)
- startFailure(e.readableMessage)
- } catch (e: IllegalArgumentException) {
- Timber.w(e)
- startFailure(e.readableMessage)
+ }
+ setDeviceAddress(deviceAddress)
+ }.build(), listener)
}
}
/**
@@ -426,7 +519,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
}
binder.group = group
if (persistNextGroup) {
- networkName = group.networkName
+ networkName = WifiSsidCompat.fromUtf8Text(group.networkName)
passphrase = group.passphrase
persistNextGroup = false
}
@@ -434,6 +527,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
routingManager = RoutingManager.LocalOnly(this@RepeaterService, group.`interface`!!).apply { start() }
status = Status.ACTIVE
showNotification(group)
+ BootReceiver.add(Starter())
}
private fun startFailure(msg: CharSequence, group: WifiP2pGroup? = null, showWifiEnable: Boolean = false) {
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)))
private fun removeGroup() {
- p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
+ p2pManager.removeGroup(channel ?: return, object : WifiP2pManager.ActionListener {
override fun onSuccess() {
launch { cleanLocked() }
}
@@ -459,19 +553,19 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
if (reason != WifiP2pManager.BUSY) {
SmartSnackbar.make(formatReason(R.string.repeater_remove_group_failure, reason)).show()
} // else assuming it's already gone
- launch { cleanLocked() }
+ onSuccess()
}
})
}
private fun cleanLocked() {
+ BootReceiver.delete()
if (receiverRegistered) {
ensureReceiverUnregistered(receiver)
+ p2pPoller?.cancel()
receiverRegistered = false
}
- if (Build.VERSION.SDK_INT >= 28) {
- timeoutMonitor?.close()
- timeoutMonitor = null
- }
+ timeoutMonitor?.close()
+ timeoutMonitor = null
routingManager?.stop()
routingManager = null
status = Status.IDLE
@@ -484,12 +578,11 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
launch { // force clean to prevent leakage
cleanLocked()
cancel()
- dispatcher.close()
}
app.pref.unregisterOnSharedPreferenceChangeListener(this)
if (Build.VERSION.SDK_INT < 29) unregisterReceiver(deviceListener)
status = Status.DESTROYED
- if (Build.VERSION.SDK_INT >= 27) channel?.close()
+ channel?.close()
super.onDestroy()
}
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/RoutingManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/RoutingManager.kt
index a4a8514b..1b5f8f81 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/RoutingManager.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/RoutingManager.kt
@@ -1,6 +1,5 @@
package be.mygod.vpnhotspot
-import android.annotation.TargetApi
import android.os.Build
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.Routing
@@ -15,15 +14,11 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
companion object {
private const val KEY_MASQUERADE_MODE = "service.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) }
if (getBoolean("service.masquerade", true)) { // legacy settings
Routing.MasqueradeMode.Simple
} 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()
@@ -66,7 +61,7 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
private var routing: Routing? = null
private var isWifi = forceWifi || TetherType.ofInterface(downstream).isWifi
- fun start() = synchronized(RoutingManager) {
+ fun start(fromMonitor: Boolean = false) = synchronized(RoutingManager) {
started = true
when (val other = active.putIfAbsent(downstream, this)) {
null -> {
@@ -78,14 +73,19 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
isWifi = isWifiNow
}
}
- initRoutingLocked()
+ initRoutingLocked(fromMonitor)
}
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 {
try {
configure()
@@ -97,10 +97,10 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
true
} catch (e: Exception) {
when (e) {
- is Routing.InterfaceNotFoundException -> Timber.d(e)
+ is Routing.InterfaceNotFoundException -> if (!fromMonitor) Timber.d(e)
!is CancellationException -> Timber.w(e)
}
- SmartSnackbar.make(e).show()
+ if (e !is Routing.InterfaceNotFoundException || !fromMonitor) SmartSnackbar.make(e).show()
routing = null
false
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/ServiceNotification.kt b/mobile/src/main/java/be/mygod/vpnhotspot/ServiceNotification.kt
index 6609c2c0..68022e03 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/ServiceNotification.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/ServiceNotification.kt
@@ -1,36 +1,35 @@
package be.mygod.vpnhotspot
-import android.annotation.TargetApi
import android.app.*
import android.content.Context
import android.content.Intent
-import android.os.Build
-import androidx.core.app.NotificationCompat
-import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import be.mygod.vpnhotspot.App.Companion.app
import java.util.*
object ServiceNotification {
- private const val CHANNEL = "tethering"
- private const val CHANNEL_ID = 1
+ private const val CHANNEL_ACTIVE = "tethering"
+ private const val CHANNEL_INACTIVE = "tethering-inactive"
+ private const val NOTIFICATION_ID = 1
private val deviceCountsMap = WeakHashMap>()
private val inactiveMap = WeakHashMap>()
private val manager = app.getSystemService()!!
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 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) ->
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 {
val deviceCount = deviceCounts.sumOf { it.value }
val interfaceCount = deviceCounts.size + inactive.size
- NotificationCompat.BigTextStyle(builder
- .setContentText(context.resources.getQuantityString(R.plurals.notification_connected_devices,
- deviceCount, deviceCount,
- context.resources.getQuantityString(R.plurals.notification_interfaces,
- interfaceCount, interfaceCount))))
- .bigText(lines.joinToString("\n"))
- .build()!!
+ Notification.BigTextStyle().apply {
+ setBuilder(builder.setContentText(context.resources.getQuantityString(
+ R.plurals.notification_connected_devices, deviceCount, deviceCount,
+ context.resources.getQuantityString(R.plurals.notification_interfaces,
+ interfaceCount, interfaceCount))))
+ bigText(lines.joinToString("\n"))
+ }.build()!!
}
}
@@ -54,26 +53,29 @@ object ServiceNotification {
synchronized(this) {
deviceCountsMap[service] = deviceCounts
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) {
- deviceCountsMap.remove(service)
- if (deviceCountsMap.isEmpty()) service.stopForeground(true) else {
- service.stopForeground(false)
- manager.notify(CHANNEL_ID, buildNotification(service))
- }
+ deviceCountsMap.remove(service) ?: return@synchronized
+ val shutdown = deviceCountsMap.isEmpty()
+ service.stopForeground(if (shutdown) Service.STOP_FOREGROUND_REMOVE else Service.STOP_FOREGROUND_DETACH)
+ if (!shutdown) manager.notify(NOTIFICATION_ID, buildNotification(service))
}
fun updateNotificationChannels() {
- if (Build.VERSION.SDK_INT >= 26) @TargetApi(26) {
- val tethering = NotificationChannel(CHANNEL,
- app.getText(R.string.notification_channel_tethering), NotificationManager.IMPORTANCE_LOW)
- tethering.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
- manager.createNotificationChannel(tethering)
- // remove old service channels
- manager.deleteNotificationChannel("hotspot")
- manager.deleteNotificationChannel("repeater")
+ NotificationChannel(CHANNEL_ACTIVE,
+ app.getText(R.string.notification_channel_tethering), NotificationManager.IMPORTANCE_LOW).apply {
+ lockscreenVisibility = Notification.VISIBILITY_PUBLIC
+ manager.createNotificationChannel(this)
}
+ 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")
}
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/SettingsPreferenceFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/SettingsPreferenceFragment.kt
index 1997cd49..244cafa4 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/SettingsPreferenceFragment.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/SettingsPreferenceFragment.kt
@@ -1,6 +1,5 @@
package be.mygod.vpnhotspot
-import android.annotation.TargetApi
import android.content.Intent
import android.os.Build
import android.os.Bundle
@@ -8,20 +7,19 @@ import androidx.core.content.FileProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
-import androidx.preference.SwitchPreference
+import androidx.preference.TwoStatePreference
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.TetherOffloadManager
import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor
import be.mygod.vpnhotspot.net.monitor.IpMonitor
import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
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.SummaryFallbackProvider
import be.mygod.vpnhotspot.root.Dump
import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.util.Services
-import be.mygod.vpnhotspot.util.allInterfaceNames
import be.mygod.vpnhotspot.util.launchUrl
import be.mygod.vpnhotspot.util.showAllowingStateLoss
import be.mygod.vpnhotspot.widget.SmartSnackbar
@@ -40,7 +38,6 @@ import kotlin.system.exitProcess
class SettingsPreferenceFragment : PreferenceFragmentCompat() {
private fun Preference.remove() = parent!!.removePreference(this)
- @TargetApi(26)
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
// handle complicated default value and possible system upgrades
WifiDoubleLock.mode = WifiDoubleLock.mode
@@ -50,34 +47,28 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
addPreferencesFromResource(R.xml.pref_settings)
SummaryFallbackProvider(findPreference(UpstreamMonitor.KEY)!!)
SummaryFallbackProvider(findPreference(FallbackUpstreamMonitor.KEY)!!)
- findPreference("system.enableTetherOffload")!!.apply {
- if (TetherOffloadManager.supported) {
- isChecked = TetherOffloadManager.enabled
- setOnPreferenceChangeListener { _, newValue ->
- if (TetherOffloadManager.enabled != newValue) viewLifecycleOwner.lifecycleScope.launchWhenCreated {
- isEnabled = false
- try {
- TetherOffloadManager.setEnabled(newValue as Boolean)
- } catch (_: CancellationException) {
- } catch (e: Exception) {
- Timber.w(e)
- SmartSnackbar.make(e).show()
- }
- isChecked = TetherOffloadManager.enabled
- isEnabled = true
+ findPreference("system.enableTetherOffload")!!.apply {
+ isChecked = TetherOffloadManager.enabled
+ setOnPreferenceChangeListener { _, newValue ->
+ if (TetherOffloadManager.enabled != newValue) viewLifecycleOwner.lifecycleScope.launch {
+ isEnabled = false
+ try {
+ TetherOffloadManager.setEnabled(newValue as Boolean)
+ } catch (_: CancellationException) {
+ } catch (e: Exception) {
+ Timber.w(e)
+ SmartSnackbar.make(e).show()
}
- false
+ isChecked = TetherOffloadManager.enabled
+ isEnabled = true
}
- } else remove()
- }
- val boot = findPreference("service.repeater.startOnBoot")!!
- if (Services.p2p != null) {
- boot.setOnPreferenceChangeListener { _, value ->
- BootReceiver.enabled = value as Boolean
- true
+ false
}
- boot.isChecked = BootReceiver.enabled
- } else boot.remove()
+ }
+ findPreference(BootReceiver.KEY)!!.setOnPreferenceChangeListener { _, value ->
+ BootReceiver.onUserSettingUpdated(value as Boolean)
+ true
+ }
if (Services.p2p == null || !RepeaterService.safeModeConfigurable) {
val safeMode = findPreference(RepeaterService.KEY_SAFE_MODE)!!
safeMode.remove()
@@ -130,7 +121,7 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putExtra(Intent.EXTRA_STREAM,
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
}
@@ -148,16 +139,12 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
}
}
- override fun onDisplayPreferenceDialog(preference: Preference) {
- when (preference.key) {
- UpstreamMonitor.KEY, FallbackUpstreamMonitor.KEY ->
- AlwaysAutoCompleteEditTextPreferenceDialogFragment().apply {
- setArguments(preference.key, Services.connectivity.allNetworks.mapNotNull {
- Services.connectivity.getLinkProperties(it)?.allInterfaceNames
- }.flatten().toTypedArray())
- setTargetFragment(this@SettingsPreferenceFragment, 0)
- }.showAllowingStateLoss(parentFragmentManager, preference.key)
- else -> super.onDisplayPreferenceDialog(preference)
- }
+ override fun onDisplayPreferenceDialog(preference: Preference) = when (preference.key) {
+ UpstreamMonitor.KEY, FallbackUpstreamMonitor.KEY ->
+ AutoCompleteNetworkPreferenceDialogFragment().apply {
+ setArguments(preference.key)
+ setTargetFragment(this@SettingsPreferenceFragment, 0)
+ }.showAllowingStateLoss(parentFragmentManager, preference.key)
+ else -> super.onDisplayPreferenceDialog(preference)
}
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt
index 8f1ba20b..8dd110b6 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt
@@ -1,7 +1,7 @@
package be.mygod.vpnhotspot
+import android.content.Context
import android.content.Intent
-import android.os.Build
import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.App.Companion.app
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.widget.SmartSnackbar
import kotlinx.coroutines.*
+import kotlinx.parcelize.Parcelize
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
@@ -17,6 +18,7 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
companion object {
const val EXTRA_ADD_INTERFACES = "interface.add"
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"
}
@@ -39,10 +41,19 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
}
}
+ @Parcelize
+ data class Starter(val monitored: ArrayList) : 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.
*/
- private val dispatcher = newSingleThreadContext("TetheringService")
+ private val dispatcher = Dispatchers.IO.limitedParallelism(1)
override val coroutineContext = dispatcher + Job()
private val binder = Binder()
private val downstreams = ConcurrentHashMap()
@@ -55,7 +66,7 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
val toRemove = downstreams.toMutableMap() // make a copy
for (iface in interfaces) {
val downstream = toRemove.remove(iface) ?: continue
- if (downstream.monitor) downstream.start()
+ if (downstream.monitor && !downstream.start()) downstream.stop()
}
for ((iface, downstream) in toRemove) {
if (!downstream.monitor) check(downstreams.remove(iface, downstream))
@@ -81,6 +92,10 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
ServiceNotification.stopForeground(this)
stopSelf()
} else {
+ binder.monitoredIfaces.also {
+ if (it.isEmpty()) BootReceiver.delete()
+ else BootReceiver.add(Starter(ArrayList(it)))
+ }
if (!callbackRegistered) {
callbackRegistered = true
TetheringManager.registerTetheringEventCallbackCompat(this, this)
@@ -94,8 +109,9 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
override fun onBind(intent: Intent?) = binder
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ BootReceiver.startIfEnabled()
// call this first just in case we are shutting down immediately
- if (Build.VERSION.SDK_INT >= 26) updateNotification()
+ updateNotification()
launch {
if (intent != null) {
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()
}
}
- 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]
if (downstream == null) Downstream(this@TetheringService, iface, true).apply {
- start()
+ if (!start(true)) stop()
check(downstreams.put(iface, this) == null)
downstreams[iface] = this
} else downstream.monitor = true
@@ -120,10 +138,10 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
override fun onDestroy() {
launch {
+ BootReceiver.delete()
unregisterReceiver()
downstreams.values.forEach { it.stop() } // force clean to prevent leakage
cancel()
- dispatcher.close()
}
super.onDestroy()
}
@@ -135,4 +153,8 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
callbackRegistered = false
}
}
+
+ override fun updateNotification() {
+ launch { super.updateNotification() }
+ }
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/client/Client.kt b/mobile/src/main/java/be/mygod/vpnhotspot/client/Client.kt
index fe8eac65..a33bcfed 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/client/Client.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/client/Client.kt
@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot.client
+import android.net.MacAddress
import android.text.SpannableStringBuilder
import android.text.Spanned
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.net.InetAddressComparator
import be.mygod.vpnhotspot.net.IpNeighbour
-import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.room.AppDatabase
import be.mygod.vpnhotspot.room.ClientRecord
@@ -18,7 +18,7 @@ import be.mygod.vpnhotspot.util.makeMacSpan
import java.net.InetAddress
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() {
override fun areItemsTheSame(oldItem: Client, newItem: Client) =
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,
* 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)
macIface
- } else emojize(record.nickname)).apply {
+ }).apply {
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()
}
- fun obtainRecord() = record.value ?: ClientRecord(mac.addr)
+ fun obtainRecord() = record.value ?: ClientRecord(mac)
override fun equals(other: Any?): Boolean {
if (this === other) return true
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientViewModel.kt b/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientViewModel.kt
index 53a848dd..307af3e1 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientViewModel.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientViewModel.kt
@@ -3,11 +3,12 @@ package be.mygod.vpnhotspot.client
import android.content.ComponentName
import android.content.IntentFilter
import android.content.ServiceConnection
+import android.net.MacAddress
import android.net.wifi.p2p.WifiP2pDevice
+import android.os.Build
import android.os.IBinder
import android.os.Parcelable
import androidx.annotation.RequiresApi
-import androidx.core.os.BuildCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
@@ -15,8 +16,6 @@ import androidx.lifecycle.ViewModel
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.RepeaterService
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.TetheringManager
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 p2p: Collection = emptyList()
- private var wifiAp = emptyList>()
+ private var wifiAp = emptyList>()
private var neighbours: Collection = emptyList()
val clients = MutableLiveData>()
val fullMode = object : DefaultLifecycleObserver {
@@ -51,10 +50,10 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
}
private fun populateClients() {
- val clients = HashMap, Client>()
+ val clients = HashMap, Client>()
repeater?.group?.`interface`?.let { p2pInterface ->
for (client in p2p) {
- val addr = MacAddressCompat.fromString(client.deviceAddress!!)
+ val addr = MacAddress.fromString(client.deviceAddress!!)
clients[p2pInterface to addr] = object : Client(addr, p2pInterface) {
override val icon: Int get() = TetherType.WIFI_P2P.icon
}
@@ -87,10 +86,10 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
override fun onStart(owner: LifecycleOwner) {
app.registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
IpNeighbourMonitor.registerCallback(this, false)
- if (BuildCompat.isAtLeastS()) WifiApCommands.registerSoftApCallback(this)
+ if (Build.VERSION.SDK_INT >= 31) WifiApCommands.registerSoftApCallback(this)
}
override fun onStop(owner: LifecycleOwner) {
- if (BuildCompat.isAtLeastS()) WifiApCommands.unregisterSoftApCallback(this)
+ if (Build.VERSION.SDK_INT >= 31) WifiApCommands.unregisterSoftApCallback(this)
IpNeighbourMonitor.unregisterCallback(this)
app.unregisterReceiver(receiver)
}
@@ -118,7 +117,7 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
override fun onConnectedClientsChanged(clients: List) {
wifiAp = clients.mapNotNull {
val client = WifiClient(it)
- client.apInstanceIdentifier?.run { this to client.macAddress.toCompat() }
+ client.apInstanceIdentifier?.run { this to client.macAddress }
}
}
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientsFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientsFragment.kt
index db48cb38..33c99af4 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientsFragment.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientsFragment.kt
@@ -1,6 +1,7 @@
package be.mygod.vpnhotspot.client
import android.content.DialogInterface
+import android.net.MacAddress
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
@@ -20,6 +21,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.withStarted
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
@@ -30,7 +32,6 @@ import be.mygod.vpnhotspot.Empty
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.databinding.FragmentClientsBinding
import be.mygod.vpnhotspot.databinding.ListitemClientBinding
-import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
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.toPluralInt
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 java.text.NumberFormat
class ClientsFragment : Fragment() {
- // FIXME: value class does not work with 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() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
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)
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 {
@@ -64,7 +68,7 @@ class ClientsFragment : Fragment() {
}
override fun onClick(dialog: DialogInterface?, which: Int) {
- val mac = MacAddressCompat(arg.mac)
+ val mac = arg.mac
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
val newNickname = this.dialog!!.findViewById(android.R.id.edit).text
@@ -84,7 +88,7 @@ class ClientsFragment : Fragment() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
val context = context
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))
val format = NumberFormat.getIntegerInstance(locale)
setMessage("%s\n%s\n%s".format(
@@ -135,7 +139,7 @@ class ClientsFragment : Fragment() {
R.id.nickname -> {
val client = binding.client ?: return false
NicknameDialogFragment().apply {
- arg(NicknameArg(client.mac.addr, client.nickname))
+ arg(NicknameArg(client.mac, client.nickname))
}.showAllowingStateLoss(parentFragmentManager)
true
}
@@ -155,14 +159,16 @@ class ClientsFragment : Fragment() {
true
}
R.id.stats -> {
- binding.client?.let { client ->
- viewLifecycleOwner.lifecycleScope.launchWhenCreated {
- withContext(Dispatchers.Unconfined) {
- StatsDialogFragment().apply {
- arg(StatsArg(client.title.value ?: return@withContext,
- AppDatabase.instance.trafficRecordDao.queryStats(client.mac.addr)))
- }.showAllowingStateLoss(parentFragmentManager)
- }
+ val client = binding.client
+ val title = client?.title?.value ?: return false
+ viewLifecycleOwner.lifecycleScope.launch {
+ val stats = withContext(Dispatchers.Unconfined) {
+ AppDatabase.instance.trafficRecordDao.queryStats(client.mac)
+ }
+ withStarted {
+ StatsDialogFragment().apply {
+ arg(StatsArg(title, stats))
+ }.showAllowingStateLoss(parentFragmentManager)
}
}
true
@@ -201,9 +207,7 @@ class ClientsFragment : Fragment() {
check(newRecord.receivedPackets == oldRecord.receivedPackets)
check(newRecord.receivedBytes == oldRecord.receivedBytes)
} else {
- val rate = rates.computeIfAbsent(newRecord.downstream to MacAddressCompat(newRecord.mac)) {
- TrafficRate()
- }
+ val rate = rates.computeIfAbsent(newRecord.downstream to newRecord.mac) { TrafficRate() }
if (rate.send < 0 || rate.receive < 0) {
rate.send = 0
rate.receive = 0
@@ -218,7 +222,7 @@ class ClientsFragment : Fragment() {
private lateinit var binding: FragmentClientsBinding
private val adapter = ClientAdapter()
- private var rates = mutableMapOf, TrafficRate>()
+ private var rates = mutableMapOf, TrafficRate>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentClientsBinding.inflate(inflater, container, false)
@@ -237,14 +241,19 @@ class ClientsFragment : Fragment() {
override fun onStart() {
// icon might be changed due to TetherType changes
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()
// 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 ->
- lifecycleScope.launchWhenStarted { adapter.updateTraffic(newRecords, oldRecords) }
+ lifecycleScope.launch {
+ withStarted { adapter.updateTraffic(newRecords, oldRecords) }
+ }
}
- lifecycleScope.launchWhenStarted {
+ lifecycleScope.launch {
withContext(Dispatchers.Default) {
TrafficRecorder.rescheduleUpdate() // next schedule time might be 1 min, force reschedule to <= 1s
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/client/Emojize.kt b/mobile/src/main/java/be/mygod/vpnhotspot/client/Emojize.kt
deleted file mode 100644
index bd5b8a45..00000000
--- a/mobile/src/main/java/be/mygod/vpnhotspot/client/Emojize.kt
+++ /dev/null
@@ -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
-}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/client/MacLookup.kt b/mobile/src/main/java/be/mygod/vpnhotspot/client/MacLookup.kt
index 19ec56c0..2184ced6 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/client/MacLookup.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/client/MacLookup.kt
@@ -1,78 +1,134 @@
package be.mygod.vpnhotspot.client
import android.content.Context
-import android.os.Build
+import android.net.MacAddress
import androidx.annotation.MainThread
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R
-import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.room.AppDatabase
+import be.mygod.vpnhotspot.util.connectCancellable
import be.mygod.vpnhotspot.widget.SmartSnackbar
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
import org.json.JSONException
import org.json.JSONObject
import timber.log.Timber
-import java.net.HttpURLConnection
-import java.net.URL
+import java.io.File
+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.
*/
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) =
- 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 fun getLocalizedMessage() = formatMessage(app)
}
- private val macLookupBusy = mutableMapOf>()
+ 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("?
+ 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 = mutex.withLock {
+ val sessionCache = (if (forceNew) null else sessionCache) ?: refreshSessionCache()
+ HttpCookie.parse(sessionCache[0]).single() to sessionCache[1]
+ }
+ }
+
+ private val macLookupBusy = mutableMapOf()
// http://en.wikipedia.org/wiki/ISO_3166-1
private val countryCodeRegex = "(?:^|[^A-Z])([A-Z]{2})[\\s\\d]*$".toRegex()
@MainThread
- fun abort(mac: MacAddressCompat) = macLookupBusy.remove(mac)?.let { (conn, job) ->
- job.cancel()
- if (Build.VERSION.SDK_INT < 26) GlobalScope.launch(Dispatchers.IO) { conn.disconnect() } else conn.disconnect()
- }
+ fun abort(mac: MacAddress) = macLookupBusy.remove(mac)?.cancel()
@MainThread
- fun perform(mac: MacAddressCompat, explicit: Boolean = false) {
+ fun perform(mac: MacAddress, explicit: Boolean = false) {
abort(mac)
- val conn = URL("https://macvendors.co/api/$mac").openConnection() as HttpURLConnection
- macLookupBusy[mac] = conn to GlobalScope.launch(Dispatchers.IO) {
+ macLookupBusy[mac] = GlobalScope.launch(Dispatchers.IO) {
try {
- val response = conn.inputStream.bufferedReader().readText()
- val obj = JSONObject(response).getJSONObject("result")
- obj.opt("error")?.also { throw UnexpectedError(mac, it.toString()) }
- val company = obj.getString("company")
- val match = extractCountry(mac, response, obj)
- val result = if (match != null) {
- String(match.groupValues[1].flatMap { listOf('\uD83C', it + 0xDDA5) }.toCharArray()) + ' ' + company
- } else company
+ var response: String? = null
+ for (tries in 0 until 5) {
+ val (cookie, csrf) = SessionManager.obtain(tries > 0)
+ response = connectCancellable("https://macaddress.io/mac-address-lookup") { conn ->
+ conn.requestMethod = "POST"
+ conn.setRequestProperty("content-type", "application/json")
+ conn.setRequestProperty("cookie", "${cookie.name}=${cookie.value}")
+ 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) {
- nickname = result
+ if (result != null) nickname = result
macLookupPending = false
}
- } catch (e: JSONException) {
- 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 (_: CancellationException) {
} catch (e: Throwable) {
- Timber.d(e)
+ Timber.w(e)
if (explicit) SmartSnackbar.make(e).show()
}
}
}
- private fun extractCountry(mac: MacAddressCompat, response: String, obj: JSONObject): MatchResult? {
- countryCodeRegex.matchEntire(obj.optString("country"))?.also { return it }
- val address = obj.optString("address")
+ private fun extractCountry(mac: MacAddress, response: String, obj: JSONObject): MatchResult? {
+ countryCodeRegex.matchEntire(obj.optString("countryCode"))?.also { return it }
+ val address = obj.optString("companyAddress")
if (address.isBlank()) return null
countryCodeRegex.find(address)?.also { return it }
Timber.w(UnexpectedError(mac, response))
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/BluetoothTethering.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/BluetoothTethering.kt
index 83325254..fbeb1bad 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/BluetoothTethering.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/BluetoothTethering.kt
@@ -1,6 +1,6 @@
package be.mygod.vpnhotspot.manage
-import android.annotation.TargetApi
+import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothProfile
import android.content.BroadcastReceiver
@@ -8,8 +8,6 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
-import androidx.annotation.RequiresApi
-import androidx.core.os.BuildCompat
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.util.broadcastReceiver
@@ -18,7 +16,7 @@ import be.mygod.vpnhotspot.widget.SmartSnackbar
import timber.log.Timber
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 {
companion object : BroadcastReceiver() {
/**
@@ -26,17 +24,9 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
*/
private const val PAN = 5
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") }
- fun pan(context: Context, serviceListener: BluetoothProfile.ServiceListener) =
- constructor.newInstance(context, serviceListener) as BluetoothProfile
- val BluetoothProfile.isTetheringOn get() = isTetheringOn(this) as Boolean
- fun BluetoothProfile.closePan() = BluetoothAdapter.getDefaultAdapter()!!.closeProfileProxy(PAN, this)
+ private val BluetoothProfile.isTetheringOn get() = isTetheringOn(this) as Boolean
private fun registerBluetoothStateListener(receiver: BroadcastReceiver) =
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
*/
- @TargetApi(24)
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {
BluetoothAdapter.STATE_ON -> {
@@ -58,28 +47,12 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
pendingCallback = null
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 pan: BluetoothProfile? = null
+ private var stoppedByUser = false
var activeFailureCause: Throwable? = null
/**
* 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
if (!connected) return null
activeFailureCause = null
- return BluetoothAdapter.getDefaultAdapter()?.state == BluetoothAdapter.STATE_ON && try {
+ val on = adapter.state == BluetoothAdapter.STATE_ON && try {
pan.isTetheringOn
} catch (e: InvocationTargetException) {
activeFailureCause = e.cause ?: e
@@ -96,16 +69,21 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
else Timber.w(e)
return null
}
+ return if (stoppedByUser) {
+ if (!on) stoppedByUser = false
+ false
+ } else on
}
private val receiver = broadcastReceiver { _, _ -> stateListener() }
fun ensureInit(context: Context) {
- if (pan == null && BluetoothAdapter.getDefaultAdapter() != null) try {
- pan = pan(context, this)
- } catch (e: InvocationTargetException) {
- if (e.cause is SecurityException && BuildCompat.isAtLeastS()) Timber.d(e.readableMessage)
- else Timber.w(e)
+ activeFailureCause = null
+ if (!proxyCreated) try {
+ check(adapter.getProfileProxy(context, this, PAN))
+ proxyCreated = true
+ } catch (e: SecurityException) {
+ if (Build.VERSION.SDK_INT >= 31) Timber.d(e.readableMessage) else Timber.w(e)
activeFailureCause = e
}
}
@@ -116,13 +94,38 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
override fun onServiceDisconnected(profile: Int) {
connected = false
+ stoppedByUser = false
}
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
+ pan = proxy
connected = true
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() {
app.unregisterReceiver(receiver)
- pan?.closePan()
+ adapter.closeProfileProxy(PAN, pan)
}
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/InterfaceManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/InterfaceManager.kt
index b390fe3b..942cfbca 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/InterfaceManager.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/InterfaceManager.kt
@@ -2,7 +2,6 @@ package be.mygod.vpnhotspot.manage
import android.content.Intent
import android.view.View
-import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.TetheringService
@@ -25,7 +24,7 @@ class InterfaceManager(private val parent: TetheringFragment, val iface: String)
val data = binding.data as Data
if (data.active) context.startService(Intent(context, TetheringService::class.java)
.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)))
}
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/IpNeighbourMonitoringTileService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/IpNeighbourMonitoringTileService.kt
index 93e966fb..11c66b6d 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/IpNeighbourMonitoringTileService.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/IpNeighbourMonitoringTileService.kt
@@ -1,14 +1,12 @@
package be.mygod.vpnhotspot.manage
import android.service.quicksettings.Tile
-import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
import be.mygod.vpnhotspot.util.KillableTileService
import java.net.Inet4Address
-@RequiresApi(24)
abstract class IpNeighbourMonitoringTileService : KillableTileService(), IpNeighbourMonitor.Callback {
private var neighbours: Collection = emptyList()
abstract fun updateTile()
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotManager.kt
index 75906a0c..ee94145f 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotManager.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotManager.kt
@@ -1,15 +1,12 @@
package be.mygod.vpnhotspot.manage
import android.Manifest
-import android.content.*
-import android.location.LocationManager
+import android.content.ComponentName
+import android.content.Context
+import android.content.ServiceConnection
import android.os.Build
import android.os.IBinder
-import android.provider.Settings
import android.view.View
-import android.widget.Toast
-import androidx.annotation.RequiresApi
-import androidx.core.content.getSystemService
import androidx.recyclerview.widget.RecyclerView
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.LocalOnlyHotspotService
@@ -17,15 +14,15 @@ import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import be.mygod.vpnhotspot.util.formatAddresses
-import be.mygod.vpnhotspot.widget.SmartSnackbar
import java.net.NetworkInterface
-@RequiresApi(26)
class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager(), ServiceConnection {
companion object {
- val permission = if (Build.VERSION.SDK_INT >= 29) {
- Manifest.permission.ACCESS_FINE_LOCATION
- } else Manifest.permission.ACCESS_COARSE_LOCATION
+ val permission = when {
+ Build.VERSION.SDK_INT >= 33 -> Manifest.permission.NEARBY_WIFI_DEVICES
+ 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),
@@ -57,23 +54,7 @@ class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager()
ServiceForegroundConnector(parent, this, LocalOnlyHotspotService::class)
}
- /**
- * 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()?.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))
- }
+ fun start(context: Context) = app.startServiceWithLocation(context)
override val type get() = VIEW_TYPE_LOCAL_ONLY_HOTSPOT
private val data = Data()
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotTileService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotTileService.kt
index 1469d46c..2a7b7c3e 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotTileService.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotTileService.kt
@@ -6,12 +6,10 @@ import android.content.Intent
import android.graphics.drawable.Icon
import android.os.IBinder
import android.service.quicksettings.Tile
-import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.LocalOnlyHotspotService
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.util.stopAndUnbind
-@RequiresApi(26)
class LocalOnlyHotspotTileService : IpNeighbourMonitoringTileService() {
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)
} else {
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 }
}
updateTile()
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/ManageBar.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/ManageBar.kt
index e1a22612..02c8ed62 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/ManageBar.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/ManageBar.kt
@@ -16,7 +16,7 @@ object ManageBar : Manager() {
private const val SETTINGS_2 = "com.android.settings.TetherSettings"
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 {
init {
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/Manager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/Manager.kt
index d0d5ca83..baa163ba 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/Manager.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/Manager.kt
@@ -1,7 +1,6 @@
package be.mygod.vpnhotspot.manage
import android.annotation.SuppressLint
-import android.annotation.TargetApi
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
@@ -18,9 +17,6 @@ abstract class Manager {
const val VIEW_TYPE_USB = 3
const val VIEW_TYPE_BLUETOOTH = 4
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_REPEATER = 7
@@ -35,13 +31,10 @@ abstract class Manager {
VIEW_TYPE_WIFI,
VIEW_TYPE_USB,
VIEW_TYPE_BLUETOOTH,
- VIEW_TYPE_ETHERNET,
- VIEW_TYPE_NCM,
- VIEW_TYPE_WIGIG,
- VIEW_TYPE_WIFI_LEGACY -> {
+ VIEW_TYPE_ETHERNET -> {
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))
}
VIEW_TYPE_REPEATER -> RepeaterManager.ViewHolder(ListitemRepeaterBinding.inflate(inflater, parent, false))
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt
index 137fd2b1..d0aabcba 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt
@@ -5,7 +5,7 @@ import android.content.ComponentName
import android.content.DialogInterface
import android.content.Intent
import android.content.ServiceConnection
-import android.content.pm.PackageManager
+import android.net.MacAddress
import android.net.wifi.SoftApConfiguration
import android.net.wifi.p2p.WifiP2pGroup
import android.os.Build
@@ -17,25 +17,33 @@ import android.view.WindowManager
import android.widget.EditText
import androidx.annotation.MainThread
import androidx.appcompat.app.AlertDialog
-import androidx.core.content.ContextCompat
import androidx.databinding.BaseObservable
import androidx.databinding.Bindable
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.withStarted
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.net.MacAddressCompat
import be.mygod.vpnhotspot.net.wifi.P2pSupplicantConfiguration
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
import be.mygod.vpnhotspot.net.wifi.WifiApDialogFragment
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.formatAddresses
import be.mygod.vpnhotspot.util.showAllowingStateLoss
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 timber.log.Timber
import java.net.NetworkInterface
@@ -71,6 +79,9 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
NetworkInterface.getByName(p2pInterface ?: return "")?.formatAddresses() ?: ""
} catch (_: SocketException) {
""
+ } catch (e: Exception) {
+ Timber.w(e)
+ ""
}
}
@@ -89,12 +100,10 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
val binder = binder
when (binder?.service?.status) {
RepeaterService.Status.IDLE -> if (Build.VERSION.SDK_INT < 29) parent.requireContext().let { context ->
- ContextCompat.startForegroundService(context, Intent(context, RepeaterService::class.java))
- } else if (parent.requireContext().checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) ==
- PackageManager.PERMISSION_GRANTED ||
- parent.shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) {
- parent.startRepeater.launch(Manifest.permission.ACCESS_FINE_LOCATION)
- } else SmartSnackbar.make(R.string.repeater_missing_location_permissions).shortToast().show()
+ context.startForegroundService(Intent(context, RepeaterService::class.java))
+ } else parent.startRepeater.launch(if (Build.VERSION.SDK_INT >= 33) {
+ Manifest.permission.NEARBY_WIFI_DEVICES
+ } else Manifest.permission.ACCESS_FINE_LOCATION)
RepeaterService.Status.ACTIVE -> binder.shutdown()
else -> { }
}
@@ -148,8 +157,10 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
fun configure() {
if (configuring) return
configuring = true
- parent.viewLifecycleOwner.lifecycleScope.launchWhenCreated {
- getConfiguration()?.let { (config, readOnly) ->
+ val owner = parent.viewLifecycleOwner
+ owner.lifecycleScope.launch {
+ val (config, readOnly) = getConfiguration() ?: return@launch
+ owner.withStarted {
WifiApDialogFragment().apply {
arg(WifiApDialogFragment.Arg(config, readOnly, true))
key(this@RepeaterManager.javaClass.name)
@@ -191,25 +202,33 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
val passphrase = RepeaterService.passphrase
if (networkName != null && passphrase != null) {
return SoftApConfigurationCompat(
- ssid = networkName,
- passphrase = passphrase,
- securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, // is not actually used
- isAutoShutdownEnabled = RepeaterService.isAutoShutdownEnabled,
- shutdownTimeoutMillis = RepeaterService.shutdownTimeoutMillis).apply {
+ ssid = networkName,
+ passphrase = passphrase,
+ securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, // is not actually used
+ isAutoShutdownEnabled = RepeaterService.isAutoShutdownEnabled,
+ shutdownTimeoutMillis = RepeaterService.shutdownTimeoutMillis,
+ macRandomizationSetting = if (WifiApManager.p2pMacRandomizationSupported) {
+ SoftApConfigurationCompat.RANDOMIZATION_NON_PERSISTENT
+ } else SoftApConfigurationCompat.RANDOMIZATION_NONE,
+ vendorElements = RepeaterService.vendorElements,
+ ).apply {
bssid = RepeaterService.deviceAddress
setChannel(RepeaterService.operatingChannel, RepeaterService.operatingBand)
- setMacRandomizationEnabled(WifiApManager.p2pMacRandomizationSupported)
} to false
}
} else binder?.let { binder ->
val group = binder.group ?: binder.fetchPersistentGroup().let { binder.group }
if (group != null) return SoftApConfigurationCompat(
- ssid = group.networkName,
- securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, // is not actually used
- isAutoShutdownEnabled = RepeaterService.isAutoShutdownEnabled,
- shutdownTimeoutMillis = RepeaterService.shutdownTimeoutMillis).run {
+ ssid = WifiSsidCompat.fromUtf8Text(group.networkName),
+ securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, // is not actually used
+ isAutoShutdownEnabled = RepeaterService.isAutoShutdownEnabled,
+ shutdownTimeoutMillis = RepeaterService.shutdownTimeoutMillis,
+ macRandomizationSetting = if (WifiApManager.p2pMacRandomizationSupported) {
+ SoftApConfigurationCompat.RANDOMIZATION_NON_PERSISTENT
+ } else SoftApConfigurationCompat.RANDOMIZATION_NONE,
+ vendorElements = RepeaterService.vendorElements,
+ ).run {
setChannel(RepeaterService.operatingChannel)
- setMacRandomizationEnabled(WifiApManager.p2pMacRandomizationSupported)
try {
val config = P2pSupplicantConfiguration(group)
config.init(binder.obtainDeviceAddress()?.toString())
@@ -221,7 +240,7 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
if (e !is CancellationException) Timber.w(e)
passphrase = group.passphrase
try {
- bssid = group.owner?.deviceAddress?.let(MacAddressCompat.Companion::fromString)
+ bssid = group.owner?.deviceAddress?.let(MacAddress::fromString)
} catch (_: IllegalArgumentException) { }
this to true
}
@@ -231,15 +250,19 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
return null
}
private suspend fun updateConfiguration(config: SoftApConfigurationCompat) {
- val (band, channel) = config.requireSingleBand()
+ val (band, channel) = SoftApConfigurationCompat.requireSingleBand(config.channels)
if (RepeaterService.safeMode) {
RepeaterService.networkName = config.ssid
RepeaterService.deviceAddress = config.bssid
RepeaterService.passphrase = config.passphrase
} else holder.config?.let { master ->
val binder = binder
- if (binder?.group?.networkName != config.ssid || master.psk != config.passphrase ||
- master.bssid != config.bssid) try {
+ val mayBeModified = master.psk != config.passphrase || master.bssid != config.bssid || config.ssid.run {
+ 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) }
(this.binder ?: binder)?.group = null
} catch (e: Exception) {
@@ -252,5 +275,6 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
RepeaterService.operatingChannel = channel
RepeaterService.isAutoShutdownEnabled = config.isAutoShutdownEnabled
RepeaterService.shutdownTimeoutMillis = config.shutdownTimeoutMillis
+ RepeaterService.vendorElements = config.vendorElements
}
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterTileService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterTileService.kt
index d33ed8c9..1b394ac4 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterTileService.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterTileService.kt
@@ -7,15 +7,12 @@ import android.graphics.drawable.Icon
import android.net.wifi.p2p.WifiP2pGroup
import android.os.IBinder
import android.service.quicksettings.Tile
-import androidx.annotation.RequiresApi
-import androidx.core.content.ContextCompat
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.RepeaterService
import be.mygod.vpnhotspot.util.KillableTileService
import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.stopAndUnbind
-@RequiresApi(24)
class RepeaterTileService : KillableTileService() {
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
if (binder == null) tapPending = true else when (binder.service.status) {
RepeaterService.Status.ACTIVE -> binder.shutdown()
- RepeaterService.Status.IDLE -> ContextCompat.startForegroundService(this,
- Intent(this, RepeaterService::class.java))
+ RepeaterService.Status.IDLE -> startForegroundService(Intent(this, RepeaterService::class.java))
else -> { }
}
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt
index 3bb8b282..fb7fd8ae 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt
@@ -2,7 +2,7 @@ package be.mygod.vpnhotspot.manage
import android.Manifest
import android.annotation.TargetApi
-import android.content.ClipData
+import android.bluetooth.BluetoothAdapter
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
@@ -15,7 +15,6 @@ import android.view.View
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.core.net.toUri
-import androidx.core.os.BuildCompat
import androidx.core.view.updatePaddingRelative
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
@@ -56,19 +55,23 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
override fun onClick(v: View?) {
val manager = manager!!
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,
"package:${mainActivity.packageName}".toUri()))
return
} catch (e: RuntimeException) {
app.logEvent("manage_write_settings") { param("message", e.toString()) }
}
- if (manager.isStarted) try {
- manager.stop()
- } catch (e: InvocationTargetException) {
- if (e.targetException !is SecurityException) Timber.w(e)
- manager.onException(e)
- } else manager.start()
+ when (manager.isStarted) {
+ true -> try {
+ manager.stop()
+ } catch (e: InvocationTargetException) {
+ if (e.targetException !is SecurityException) Timber.w(e)
+ 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 title get() = this@TetherManager.title
override val text get() = this@TetherManager.text
- override val active get() = isStarted
+ override val active get() = isStarted == true
}
val data = Data()
abstract val title: CharSequence
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 var baseError: String? = null
@@ -93,6 +97,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
protected abstract fun start()
protected abstract fun stop()
+ protected open fun onClickNull(): Unit = throw UnsupportedOperationException()
override fun onTetheringStarted() = data.notifyChange()
override fun onTetheringFailed(error: Int?) {
@@ -120,21 +125,20 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
}
fun updateErrorMessage(errored: List, lastErrors: Map) {
- 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 ->
"$iface: " + try {
TetheringManager.tetherErrorLookup(if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
TetheringManager.getLastTetherError(iface)
} else lastErrors[iface] ?: 0)
} 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
}
}
data.notifyChange()
}
- @RequiresApi(24)
class Wifi(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver,
WifiApManager.SoftApCallbackCompat {
private var failureReason: Int? = null
@@ -143,24 +147,19 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
private var capability: Parcelable? = null
init {
- if (Build.VERSION.SDK_INT >= 28) parent.viewLifecycleOwner.lifecycle.addObserver(this)
+ parent.viewLifecycleOwner.lifecycle.addObserver(this)
}
- @TargetApi(28)
override fun onStart(owner: LifecycleOwner) {
WifiApCommands.registerSoftApCallback(this)
}
- @TargetApi(28)
override fun onStop(owner: LifecycleOwner) {
WifiApCommands.unregisterSoftApCallback(this)
}
override fun onStateChanged(state: Int, failureReason: Int) {
- if (state < 10 || state > 14) {
- Timber.w(Exception("Unknown state $state, $failureReason"))
- return
- }
- this.failureReason = if (state == 14) failureReason else null // WIFI_AP_STATE_FAILED
+ if (!WifiApManager.checkWifiApState(state)) return
+ this.failureReason = if (state == WifiApManager.WIFI_AP_STATE_FAILED) failureReason else null
data.notifyChange()
}
override fun onNumClientsChanged(numClients: Int) {
@@ -175,21 +174,6 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
this.capability = capability
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 tetherType get() = TetherType.WIFI
@@ -201,7 +185,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
val numClients = numClients
val maxClients = capability.maxSupportedClients
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_5G_SUPPORTED to SoftApConfigurationCompat.BAND_5GHZ,
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))
if (Services.wifi.isStaApConcurrencySupported) yield(parent.getText(
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(
R.string.tethering_manage_wifi_feature_bridged_ap_concurrency))
if (Services.wifi.isStaBridgedApConcurrencySupported) yield(parent.getText(
@@ -228,63 +212,46 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
yield(SoftApCapability.featureLookup(bit, true))
features = features and bit.inv()
}
- }.joinToSpanned().let {
- if (it.isEmpty()) parent.getText(R.string.tethering_manage_wifi_no_features) else it
- })
- if (BuildCompat.isAtLeastS()) {
+ }.joinToSpanned().ifEmpty { parent.getText(R.string.tethering_manage_wifi_no_features) })
+ if (Build.VERSION.SDK_INT >= 31) {
val list = SoftApConfigurationCompat.BAND_TYPES.map { band ->
val channels = capability.getSupportedChannelList(band)
- if (channels.isNotEmpty()) StringBuilder().apply {
- append(SoftApConfigurationCompat.bandLookup(band, true))
- 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(')')
+ if (channels.isNotEmpty()) {
+ "${SoftApConfigurationCompat.bandLookup(band, true)} (${RangeInput.toString(channels)})"
} else null
}.filterNotNull()
if (list.isNotEmpty()) result.append(parent.getText(R.string.tethering_manage_wifi_supported_channels)
.format(locale, list.joinToString("; ")))
+ capability.countryCode?.let {
+ result.append(parent.getText(R.string.tethering_manage_wifi_country_code).format(locale, it))
+ }
}
result
} ?: numClients?.let { numClients ->
app.resources.getQuantityText(R.plurals.tethering_manage_wifi_clients, numClients).format(locale,
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 {
if (isEmpty()) null else joinToSpanned("\n") @TargetApi(30) { parcel ->
val info = SoftApInfo(parcel)
val frequency = info.frequency
val channel = SoftApConfigurationCompat.frequencyToChannel(frequency)
val bandwidth = SoftApInfo.channelWidthLookup(info.bandwidth, true)
- if (BuildCompat.isAtLeastS()) {
- var bssid = makeMacSpan(info.bssid.toString())
- info.apInstanceIdentifier?.let { // take the fast route if possible
- bssid = if (bssid is String) "$bssid%$it" else SpannableStringBuilder(bssid).append("%$it")
- }
+ if (Build.VERSION.SDK_INT >= 31) {
+ val bssid = info.bssid.let { if (it == null) null else makeMacSpan(it.toString()) }
+ val bssidAp = info.apInstanceIdentifier?.let {
+ when (bssid) {
+ null -> it
+ is String -> "$bssid%$it" // take the fast route if possible
+ else -> SpannableStringBuilder(bssid).append("%$it")
+ }
+ } ?: bssid ?: "?"
val timeout = info.autoShutdownTimeoutMillis
parent.getText(if (timeout == 0L) {
R.string.tethering_manage_wifi_info_timeout_disabled
} 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
DateUtils.formatElapsedTime(timeout / 1000))
} 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 stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI, this::onException)
}
- @RequiresApi(24)
class Usb(parent: TetheringFragment) : TetherManager(parent) {
override val title get() = parent.getString(R.string.tethering_manage_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 stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_USB, this::onException)
}
- @RequiresApi(24)
- class Bluetooth(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver {
- private val tethering = BluetoothTethering(parent.requireContext()) { data.notifyChange() }
+ class Bluetooth(parent: TetheringFragment, adapter: BluetoothAdapter) :
+ TetherManager(parent), DefaultLifecycleObserver {
+ private val tethering = BluetoothTethering(parent.requireContext(), adapter) { data.notifyChange() }
init {
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) {
- if (!BuildCompat.isAtLeastS() || parent.requireContext().checkSelfPermission(
- Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
- ensureInit(parent.requireContext())
- } else if (parent.shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH_CONNECT)) {
- parent.requestBluetooth.launch(Manifest.permission.BLUETOOTH_CONNECT)
- }
+ if (Build.VERSION.SDK_INT < 31) return
+ if (parent.requireContext().checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) ==
+ PackageManager.PERMISSION_GRANTED) {
+ tethering.ensureInit(parent.requireContext())
+ } else parent.requestBluetooth.launch(Manifest.permission.BLUETOOTH_CONNECT)
}
override fun onDestroy(owner: LifecycleOwner) = tethering.close()
override val title get() = parent.getString(R.string.tethering_manage_bluetooth)
override val tetherType get() = TetherType.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(
if (tethering.active == null) tethering.activeFailureCause?.readableMessage else null,
baseError).joinToString("\n")
- override fun start() = BluetoothTethering.start(this)
+ override fun start() = tethering.start(this, parent.requireContext())
override fun stop() {
- TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH, this::onException)
- Thread.sleep(1) // give others a room to breathe
+ tethering.stop(this::onException)
onTetheringStarted() // force flush state
}
+ override fun onClickNull() = ManageBar.start(parent.requireContext())
}
@RequiresApi(30)
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 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)
- }
- }
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt
index fb2f2506..a7b3668f 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt
@@ -1,8 +1,7 @@
-@file:Suppress("DEPRECATION")
-
package be.mygod.vpnhotspot.manage
import android.annotation.TargetApi
+import android.bluetooth.BluetoothManager
import android.content.*
import android.os.Build
import android.os.Bundle
@@ -14,28 +13,35 @@ import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar
-import androidx.core.content.ContextCompat
+import androidx.core.content.getSystemService
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.withStarted
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import be.mygod.vpnhotspot.*
+import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.databinding.FragmentTetheringBinding
import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces
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.WifiApManager
import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.root.WifiApCommands
import be.mygod.vpnhotspot.util.*
import be.mygod.vpnhotspot.widget.SmartSnackbar
+import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
import timber.log.Timber
import java.lang.reflect.InvocationTargetException
import java.net.NetworkInterface
@@ -45,28 +51,29 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
inner class ManagerAdapter : ListAdapter(Manager),
TetheringManager.TetheringEventCallback {
internal val repeaterManager by lazy { RepeaterManager(this@TetheringFragment) }
- @get:RequiresApi(26)
- internal val localOnlyHotspotManager by lazy @TargetApi(26) { LocalOnlyHotspotManager(this@TetheringFragment) }
- @get:RequiresApi(24)
- internal val bluetoothManager by lazy @TargetApi(24) { TetherManager.Bluetooth(this@TetheringFragment) }
- @get:RequiresApi(24)
- private val tetherManagers by lazy @TargetApi(24) {
- listOf(TetherManager.Wifi(this@TetheringFragment),
- TetherManager.Usb(this@TetheringFragment),
- bluetoothManager)
+ internal val localOnlyHotspotManager by lazy { LocalOnlyHotspotManager(this@TetheringFragment) }
+ internal val bluetoothManager by lazy {
+ requireContext().getSystemService()?.adapter?.let {
+ TetherManager.Bluetooth(this@TetheringFragment, it)
+ }
+ }
+ private val tetherManagers by lazy {
+ listOfNotNull(
+ TetherManager.Wifi(this@TetheringFragment),
+ TetherManager.Usb(this@TetheringFragment),
+ bluetoothManager,
+ )
}
@get:RequiresApi(30)
- private val tetherManagers30 by lazy @TargetApi(30) {
- listOf(TetherManager.Ethernet(this@TetheringFragment),
- TetherManager.Ncm(this@TetheringFragment),
- TetherManager.WiGig(this@TetheringFragment))
- }
- private val wifiManagerLegacy by lazy { TetherManager.WifiLegacy(this@TetheringFragment) }
+ private val ethernetManager by lazy @TargetApi(30) { TetherManager.Ethernet(this@TetheringFragment) }
- private var enabledIfaces = emptyList()
+ var activeIfaces = emptyList()
+ var localOnlyIfaces = emptyList()
+ var erroredIfaces = emptyList()
private var listDeferred = CompletableDeferred>(emptyList())
- private fun updateEnabledTypes() {
- this@TetheringFragment.enabledTypes = enabledIfaces.map { TetherType.ofInterface(it) }.toSet()
+ fun updateEnabledTypes() {
+ this@TetheringFragment.enabledTypes =
+ (activeIfaces + localOnlyIfaces).map { TetherType.ofInterface(it) }.toSet()
}
val lastErrors = mutableMapOf()
@@ -74,50 +81,42 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
if (error == 0) lastErrors.remove(ifName) else lastErrors[ifName] = error
}
- suspend fun notifyInterfaceChanged(lastList: List? = 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() {
updateEnabledTypes()
val lastList = listDeferred.await()
- notifyInterfaceChanged(lastList)
- val first = lastList.indexOfLast { it !is TetherManager } + 1
- notifyItemRangeChanged(first, lastList.size - first)
+ var first = lastList.indexOfFirst { it is InterfaceManager }
+ withStarted {
+ 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, localOnlyIfaces: List, erroredIfaces: List) {
+ fun update() {
val deferred = CompletableDeferred>()
listDeferred = deferred
ifaceLookup = try {
NetworkInterface.getNetworkInterfaces().asSequence().associateBy { it.name }
- } catch (e: SocketException) {
- Timber.d(e)
+ } catch (e: Exception) {
+ if (e is SocketException) Timber.d(e) else Timber.w(e)
emptyMap()
}
- enabledIfaces = activeIfaces + localOnlyIfaces
- updateEnabledTypes()
val list = ArrayList()
if (Services.p2p != null) list.add(repeaterManager)
- if (Build.VERSION.SDK_INT >= 26) list.add(localOnlyHotspotManager)
+ list.add(localOnlyHotspotManager)
val monitoredIfaces = binder?.monitoredIfaces ?: emptyList()
- updateMonitorList(activeIfaces - monitoredIfaces)
+ updateMonitorList(activeIfaces - monitoredIfaces.toSet())
list.addAll((activeIfaces + monitoredIfaces).toSortedSet()
.map { InterfaceManager(this@TetheringFragment, it) })
list.add(ManageBar)
- if (Build.VERSION.SDK_INT >= 24) {
- list.addAll(tetherManagers)
- tetherManagers.forEach { it.updateErrorMessage(erroredIfaces, lastErrors) }
- }
+ list.addAll(tetherManagers)
+ tetherManagers.forEach { it.updateErrorMessage(erroredIfaces, lastErrors) }
if (Build.VERSION.SDK_INT >= 30) {
- list.addAll(tetherManagers30)
- tetherManagers30.forEach { it.updateErrorMessage(erroredIfaces, lastErrors) }
- }
- if (Build.VERSION.SDK_INT < 26) {
- list.add(wifiManagerLegacy)
- wifiManagerLegacy.onTetheringStarted()
+ list.add(ethernetManager)
+ ethernetManager.updateErrorMessage(erroredIfaces, lastErrors)
}
submitList(list) { deferred.complete(list) }
}
@@ -130,15 +129,17 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
@RequiresApi(29)
val startRepeater = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
- if (granted) requireActivity().startForegroundService(Intent(activity, RepeaterService::class.java))
+ if (granted) app.startServiceWithLocation(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()) {
adapter.localOnlyHotspotManager.start(requireContext())
}
@RequiresApi(31)
val requestBluetooth = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
- if (granted) adapter.bluetoothManager.ensureInit(requireContext())
+ if (granted) adapter.bluetoothManager!!.ensureInit(requireContext())
}
var ifaceLookup: Map = emptyMap()
@@ -147,19 +148,22 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
var binder: TetheringService.Binder? = null
private val adapter = ManagerAdapter()
private val receiver = broadcastReceiver { _, intent ->
- adapter.update(intent.tetheredIfaces ?: return@broadcastReceiver,
- intent.localOnlyTetheredIfaces ?: return@broadcastReceiver,
- intent.getStringArrayListExtra(TetheringManager.EXTRA_ERRORED_TETHER) ?: return@broadcastReceiver)
+ adapter.activeIfaces = intent.tetheredIfaces ?: return@broadcastReceiver
+ adapter.localOnlyIfaces = intent.localOnlyTetheredIfaces ?: return@broadcastReceiver
+ adapter.erroredIfaces = intent.getStringArrayListExtra(TetheringManager.EXTRA_ERRORED_TETHER)
+ ?: return@broadcastReceiver
+ adapter.updateEnabledTypes()
+ adapter.update()
}
private fun updateMonitorList(canMonitor: List = emptyList()) {
val activity = activity as? MainActivity
val item = activity?.binding?.toolbar?.menu?.findItem(R.id.monitor) ?: return // assuming no longer foreground
item.isNotGone = canMonitor.isNotEmpty()
- item.subMenu.apply {
+ item.subMenu!!.apply {
clear()
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))
true
}
@@ -169,10 +173,10 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
private var apConfigurationRunning = false
override fun onMenuItemClick(item: MenuItem?): Boolean {
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_temp_hotspot).isNotGone =
- adapter.localOnlyHotspotManager.binder?.configuration != null
+ adapter.localOnlyHotspotManager.binder?.configuration != null
true
}
R.id.configuration_repeater -> {
@@ -189,28 +193,34 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
}
R.id.configuration_ap -> if (apConfigurationRunning) false else {
apConfigurationRunning = true
- viewLifecycleOwner.lifecycleScope.launchWhenCreated {
- try {
- WifiApManager.configurationCompat
+ viewLifecycleOwner.lifecycleScope.launch {
+ val configuration = try {
+ if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
+ WifiApManager.configurationLegacy?.toCompat() ?: SoftApConfigurationCompat()
+ } else WifiApManager.configuration.toCompat()
} catch (e: InvocationTargetException) {
if (e.targetException !is SecurityException) Timber.w(e)
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) {
- null
+ return@launch
} catch (eRoot: Exception) {
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)
}
SmartSnackbar.make(eRoot).show()
- null
+ return@launch
}
} catch (e: IllegalArgumentException) {
Timber.w(e)
SmartSnackbar.make(e).show()
- null
- }?.let { configuration ->
+ return@launch
+ }
+ withStarted {
WifiApDialogFragment().apply {
arg(WifiApDialogFragment.Arg(configuration))
key()
@@ -226,10 +236,10 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
AlertDialogFragment.setResultListener(this) { which, ret ->
- if (which == DialogInterface.BUTTON_POSITIVE) viewLifecycleOwner.lifecycleScope.launchWhenCreated {
+ if (which == DialogInterface.BUTTON_POSITIVE) GlobalScope.launch {
val configuration = ret!!.configuration
@Suppress("DEPRECATION")
- if (Build.VERSION.SDK_INT in 28 until 30 &&
+ if (Build.VERSION.SDK_INT < 30 &&
configuration.isAutoShutdownEnabled != TetherTimeoutMonitor.enabled) try {
TetherTimeoutMonitor.setEnabled(configuration.isAutoShutdownEnabled)
} catch (e: Exception) {
@@ -237,10 +247,18 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
SmartSnackbar.make(e).show()
}
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) {
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 (eRoot: Exception) {
eRoot.addSuppressed(e)
@@ -256,7 +274,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
binding.interfaces.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
binding.interfaces.itemAnimator = DefaultItemAnimator()
binding.interfaces.adapter = adapter
- adapter.update(emptyList(), emptyList(), emptyList())
+ adapter.update()
ServiceForegroundConnector(this, this, TetheringService::class)
(activity as MainActivity).binding.toolbar.apply {
inflateMenu(R.menu.toolbar_tethering)
@@ -275,18 +293,22 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
override fun onResume() {
super.onResume()
- if (Build.VERSION.SDK_INT >= 27) ManageBar.Data.notifyChange()
+ ManageBar.Data.notifyChange()
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
binder = service as TetheringService.Binder
service.routingsChanged[this] = {
- lifecycleScope.launchWhenStarted { adapter.notifyInterfaceChanged() }
+ lifecycleScope.launch {
+ withStarted { adapter.update() }
+ }
}
requireContext().registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
if (Build.VERSION.SDK_INT >= 30) {
TetheringManager.registerTetheringEventCallback(null, adapter)
- TetherType.listener[this] = { lifecycleScope.launchWhenStarted { adapter.notifyTetherTypeChanged() } }
+ TetherType.listener[this] = {
+ lifecycleScope.launch { adapter.notifyTetherTypeChanged() }
+ }
}
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringTileService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringTileService.kt
index 6483beea..6e8ba3f3 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringTileService.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringTileService.kt
@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot.manage
+import android.bluetooth.BluetoothManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
@@ -10,13 +11,12 @@ import android.os.IBinder
import android.service.quicksettings.Tile
import android.widget.Toast
import androidx.annotation.RequiresApi
-import androidx.core.content.ContextCompat
+import androidx.core.content.getSystemService
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.TetheringService
import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager
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.readableMessage
import be.mygod.vpnhotspot.util.stopAndUnbind
@@ -25,7 +25,6 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
-@RequiresApi(24)
sealed class TetheringTileService : IpNeighbourMonitoringTileService(), TetheringManager.StartTetheringCallback {
protected val tileOff by lazy { Icon.createWithResource(application, icon) }
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 open val icon get() = tetherType.icon
private var tethered: List? = 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
private val receiver = broadcastReceiver { _, intent ->
@@ -108,7 +107,7 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
stop()
} catch (e: Exception) {
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()))
}
}
@@ -151,15 +150,16 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
override val labelString get() = R.string.tethering_manage_bluetooth
override val tetherType get() = TetherType.BLUETOOTH
- override fun start() = BluetoothTethering.start(this)
+ override fun start() = tethering!!.start(this, this)
override fun stop() {
- TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH, this::onException)
- Thread.sleep(1) // give others a room to breathe
+ tethering!!.stop(this::onException)
onTetheringStarted() // force flush state
}
override fun onStartListening() {
- tethering = BluetoothTethering(this) { updateTile() }
+ tethering = getSystemService()?.adapter?.let {
+ BluetoothTethering(this, it) { updateTile() }
+ }
super.onStartListening()
}
override fun onStopListening() {
@@ -187,7 +187,7 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
icon = tileOff
}
null -> {
- state = Tile.STATE_UNAVAILABLE
+ state = Tile.STATE_INACTIVE
icon = tileOff
subtitle(tethering?.activeFailureCause?.readableMessage)
}
@@ -198,7 +198,8 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
}
override fun onClick() {
- when (tethering?.active) {
+ val tethering = tethering
+ if (tethering == null) tapPending = true else when (tethering.active) {
true -> {
val binder = binder
if (binder == null) tapPending = true else {
@@ -207,12 +208,12 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
stop()
} catch (e: Exception) {
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()))
}
}
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 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)
- }
- }
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/IpNeighbour.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/IpNeighbour.kt
index 80179905..99a3d282 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/IpNeighbour.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/IpNeighbour.kt
@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot.net
+import android.net.MacAddress
import android.os.Build
import android.system.ErrnoException
import android.system.Os
@@ -15,7 +16,7 @@ import java.io.IOException
import java.net.Inet4Address
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 {
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
* Assumptions: IP addr (key) always present and RTM_GETNEIGH is never used
*/
- private val parser = "^(Deleted )?([^ ]+) dev ([^ ]+) (lladdr ([^ ]*))?.*?( ([INCOMPLET,RAHBSDYF]+))?\$"
- .toRegex()
+ private val parser = ("^(Deleted )?(?:([^ ]+) )?dev ([^ ]+) (?:lladdr ([^ ]*))?.*?" +
+ "(?: ([INCOMPLET,RAHBSDYF]+))?\$").toRegex()
/**
* 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 {
return if (line.isBlank()) emptyList() else try {
val match = parser.matchEntire(line)!!
- val ip = parseNumericAddress(match.groupValues[2]) // by regex, ip is non-empty
- val devs = substituteDev(match.groupValues[3]) // by regex, dev is non-empty as well
- val state = if (match.groupValues[1].isNotEmpty()) State.DELETING else when (match.groupValues[7]) {
+ if (match.groups[2] == null) return emptyList()
+ val ip = parseNumericAddress(match.groupValues[2])
+ 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
"REACHABLE", "DELAY", "STALE", "PROBE", "PERMANENT" -> State.VALID
"FAILED" -> State.FAILED
"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
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) }
}
if (match.groups[4] != null) try {
- lladdr = MacAddressCompat.fromString(match.groupValues[5])
+ lladdr = MacAddress.fromString(match.groupValues[4])
} catch (e: IllegalArgumentException) {
if (state != State.INCOMPLETE && state != State.DELETING) {
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()
.asSequence()
.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 }
.distinct()
.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) {
override fun toString() = "$ip%$dev"
}
-@Suppress("FunctionName")
fun IpDev(neighbour: IpNeighbour) = IpDev(neighbour.ip, neighbour.dev)
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/MacAddressCompat.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/MacAddressCompat.kt
index 6eb9ae62..8a0b8ba1 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/MacAddressCompat.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/MacAddressCompat.kt
@@ -1,97 +1,34 @@
package be.mygod.vpnhotspot.net
import android.net.MacAddress
-import androidx.annotation.RequiresApi
import java.nio.ByteBuffer
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
value class MacAddressCompat(val addr: Long) {
companion object {
- private const val ETHER_ADDR_LEN = 6
/**
* The MacAddress zero MAC address.
*
* Not publicly exposed or treated specially since the OUI 00:00:00 is registered.
- * @hide
*/
- val ALL_ZEROS_ADDRESS = MacAddressCompat(0)
- val ANY_ADDRESS = MacAddressCompat(2)
+ val ALL_ZEROS_ADDRESS = MacAddress.fromBytes(byteArrayOf(0, 0, 0, 0, 0, 0))
+ val ANY_ADDRESS = MacAddress.fromBytes(byteArrayOf(2, 0, 0, 0, 0, 0))
- /**
- * 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 {
+ fun MacAddress.toLong() = ByteBuffer.allocate(Long.SIZE_BYTES).apply {
order(ByteOrder.LITTLE_ENDIAN)
- put(when (addr.size) {
- 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")
- })
+ put(toByteArray())
rewind()
- MacAddressCompat(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())
+ }.long
}
- fun validate() = require(addr and ((1L shl 48) - 1).inv() == 0L)
-
- fun toList() = ByteBuffer.allocate(8).run {
+ fun toPlatform() = MacAddress.fromBytes(ByteBuffer.allocate(8).run {
order(ByteOrder.LITTLE_ENDIAN)
putLong(addr)
array().take(6)
- }
-
- @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)
+ }.toByteArray())
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt
index d3660f98..8c296277 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt
@@ -1,11 +1,9 @@
package be.mygod.vpnhotspot.net
-import android.annotation.SuppressLint
-import android.annotation.TargetApi
import android.net.LinkProperties
+import android.net.MacAddress
import android.net.RouteInfo
-import android.os.Build
-import androidx.annotation.RequiresApi
+import android.system.Os
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R
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.root.RootManager
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 kotlinx.coroutines.CancellationException
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
*/
- @RequiresApi(28)
Netd,
}
@@ -151,35 +150,24 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
private val upstreams = HashSet()
private class InterfaceGoneException(upstream: String) : IOException("Interface $upstream not found")
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) {
- val ifindex = if (upstream.isEmpty()) 0 else if_nametoindex(upstream).also {
+ val ifindex = Os.if_nametoindex(upstream).also {
if (it <= 0) throw InterfaceGoneException(upstream)
}
val transaction = RootSession.beginTransaction().safeguard {
- if (upstream.isEmpty()) {
- ipRule("goto $RULE_PRIORITY_TETHERING", priority) // skip unreachable rule
- } else ipRuleLookup(ifindex, priority)
- @TargetApi(28) when (masqueradeMode) {
+ ipRuleLookup(ifindex, priority)
+ when (masqueradeMode) {
MasqueradeMode.None -> { } // nothing to be done here
- MasqueradeMode.Simple -> {
- // note: specifying -i wouldn't work for POSTROUTING
- iptablesAdd(if (upstream.isEmpty()) {
- "vpnhotspot_masquerade -s $hostSubnet -j MASQUERADE"
- } else "vpnhotspot_masquerade -s $hostSubnet -o $upstream -j MASQUERADE", "nat")
- }
- MasqueradeMode.Netd -> {
- check(upstream.isNotEmpty()) // fallback is only needed for repeater on API 23 < 28
- /**
- * 0 means that there are no interface addresses coming after, which is unused anyway.
- *
- * 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")
- }
+ // note: specifying -i wouldn't work for POSTROUTING
+ MasqueradeMode.Simple -> iptablesAdd(
+ "vpnhotspot_masquerade -s $hostSubnet -o $upstream -j MASQUERADE", "nat")
+ /**
+ * 0 means that there are no interface addresses coming after, which is unused anyway.
+ *
+ * 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
+ */
+ MasqueradeMode.Netd -> ndc("Nat", "ndc nat enable $downstream $upstream 0")
}
}
}
@@ -225,16 +213,10 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
updateDnsRoute()
}
}
- private val fallbackUpstream = object : Upstream(RULE_PRIORITY_UPSTREAM_FALLBACK) {
- @SuppressLint("NewApi")
- override fun onFallback() = onAvailable(LinkProperties().apply {
- interfaceName = ""
- setDnsServers(listOf(parseNumericAddress("8.8.8.8")))
- })
- }
+ private val fallbackUpstream = Upstream(RULE_PRIORITY_UPSTREAM_FALLBACK)
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 {
val address = ip.hostAddress
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.
*/
fun ipForward() {
- if (Build.VERSION.SDK_INT >= 23) try {
+ try {
transaction.ndc("ipfwd", "ndc ipfwd enable vpnhotspot_$downstream",
- "ndc ipfwd disable vpnhotspot_$downstream")
+ "ndc ipfwd disable vpnhotspot_$downstream")
return
} catch (e: RoutingCommands.UnexpectedOutputException) {
Timber.w(IOException("ndc ipfwd enable failure", e))
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/TetherOffloadManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/TetherOffloadManager.kt
index 73e9f576..573cb8c6 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/TetherOffloadManager.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/TetherOffloadManager.kt
@@ -1,10 +1,8 @@
package be.mygod.vpnhotspot.net
-import android.os.Build
import android.provider.Settings
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.root.SettingsGlobalPut
-import timber.log.Timber
/**
* 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
*/
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"
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)
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/TetherType.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/TetherType.kt
index 18b6ef72..2bdd1549 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/TetherType.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/TetherType.kt
@@ -28,6 +28,8 @@ enum class TetherType(@DrawableRes val icon: Int) {
else -> false
}
+ fun isA(other: TetherType) = this == other || other == USB && this == NCM
+
companion object : TetheringManager.TetheringEventCallback {
private lateinit var usbRegexs: List
private lateinit var wifiRegexs: List
@@ -58,6 +60,9 @@ enum class TetherType(@DrawableRes val icon: Int) {
private fun updateRegexs() = synchronized(this) {
if (!requiresUpdate) return@synchronized
requiresUpdate = false
+ usbRegexs = emptyList()
+ wifiRegexs = emptyList()
+ bluetoothRegexs = emptyList()
TetheringManager.registerTetheringEventCallback(null, this)
val info = TetheringManager.resolvedService.serviceInfo
val tethering = "com.android.networkstack.tethering" to
@@ -71,9 +76,9 @@ enum class TetherType(@DrawableRes val icon: Int) {
}
@RequiresApi(30)
- override fun onTetherableInterfaceRegexpsChanged(args: Array?) = synchronized(this) {
+ override fun onTetherableInterfaceRegexpsChanged(reg: Any?) = synchronized(this) {
if (requiresUpdate) return@synchronized
- Timber.i("onTetherableInterfaceRegexpsChanged: ${args?.contentDeepToString()}")
+ Timber.i("onTetherableInterfaceRegexpsChanged: $reg")
TetheringManager.unregisterTetheringEventCallback(this)
requiresUpdate = true
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
*/
- fun ofInterface(iface: String?, p2pDev: String? = null) = synchronized(this) { ofInterfaceImpl(iface, p2pDev) }
- private tailrec fun ofInterfaceImpl(iface: String?, p2pDev: String?): TetherType = when {
- iface == null -> NONE
- iface == p2pDev -> WIFI_P2P
+ fun ofInterface(iface: String?, p2pDev: String? = null) = when (iface) {
+ null -> NONE
+ 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 -> {
if (Build.VERSION.SDK_INT >= 30) updateRegexs() else error("unexpected requiresUpdate")
- ofInterfaceImpl(iface, p2pDev)
+ ofInterfaceImpl(iface)
}
wifiRegexs.any { it.matcher(iface).matches() } -> WIFI
wigigRegexs.any { it.matcher(iface).matches() } -> WIGIG
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt
index c5f0a250..08f74b24 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt
@@ -10,6 +10,7 @@ import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.Network
import android.os.Build
+import android.os.DeadObjectException
import android.os.Handler
import androidx.annotation.RequiresApi
import androidx.core.os.ExecutorCompat
@@ -66,7 +67,11 @@ object TetheringManager {
}
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
*/
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_TETHER_LEGACY = "activeArray"
/**
* gives a String[] listing all the interfaces currently in local-only
* 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
* (ie, has DHCPv4 support and packets potentially forwarded/NATed)
*/
- @RequiresApi(26)
private const val EXTRA_ACTIVE_TETHER = "tetherArray"
/**
* gives a String[] listing all the interfaces we tried to tether and
@@ -126,7 +128,6 @@ object TetheringManager {
* Wifi tethering type.
* @see [startTethering].
*/
- @RequiresApi(24)
const val TETHERING_WIFI = 0
/**
* USB tethering type.
@@ -134,48 +135,33 @@ object TetheringManager {
* Requires MANAGE_USB permission, unfortunately.
*
* 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
/**
* Bluetooth tethering type.
*
* Requires BLUETOOTH permission.
- * @see [startTethering].
+ * @see startTethering
*/
- @RequiresApi(24)
const val TETHERING_BLUETOOTH = 2
- /**
- * Ncm local tethering type.
- *
- * @see [startTethering]
- */
- @RequiresApi(30)
- const val TETHERING_NCM = 4
/**
* Ethernet tethering type.
*
* Requires MANAGE_USB permission, also.
- * @see [startTethering]
+ * @see startTethering
*/
@RequiresApi(30)
const val TETHERING_ETHERNET = 5
- /**
- * WIGIG tethering type. Use a separate type to prevent
- * conflicts with TETHERING_WIFI
- * This type is only used internally by the tethering module
- * @hide
- */
- @RequiresApi(30)
- const val TETHERING_WIGIG = 6
+ @RequiresApi(31) // TETHERING_WIFI_P2P
+ private val expectedTypes = setOf(TETHERING_WIFI, TETHERING_USB, TETHERING_BLUETOOTH, 3, TETHERING_ETHERNET)
@get:RequiresApi(30)
private val clazz by lazy { Class.forName("android.net.TetheringManager") }
@get:RequiresApi(30)
private val instance by lazy @TargetApi(30) {
@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
}
@@ -188,20 +174,17 @@ object TetheringManager {
}
}.first()
- @get:RequiresApi(24)
private val classOnStartTetheringCallback by lazy {
Class.forName("android.net.ConnectivityManager\$OnStartTetheringCallback")
}
- @get:RequiresApi(24)
private val startTetheringLegacy by lazy {
ConnectivityManager::class.java.getDeclaredMethod("startTethering",
Int::class.java, Boolean::class.java, classOnStartTetheringCallback, Handler::class.java)
}
- @get:RequiresApi(24)
private val stopTetheringLegacy by lazy {
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)
}
@@ -210,50 +193,50 @@ object TetheringManager {
Class.forName("android.net.TetheringManager\$TetheringRequest\$Builder")
}
@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)
// private val setStaticIpv4Addresses by lazy {
// classTetheringRequestBuilder.getDeclaredMethod("setStaticIpv4Addresses",
// LinkAddress::class.java, LinkAddress::class.java)
// }
@get:RequiresApi(30)
- private val setExemptFromEntitlementCheck by lazy {
+ private val setExemptFromEntitlementCheck by lazy @TargetApi(30) {
classTetheringRequestBuilder.getDeclaredMethod("setExemptFromEntitlementCheck", Boolean::class.java)
}
@get:RequiresApi(30)
- private val setShouldShowEntitlementUi by lazy {
+ private val setShouldShowEntitlementUi by lazy @TargetApi(30) {
classTetheringRequestBuilder.getDeclaredMethod("setShouldShowEntitlementUi", Boolean::class.java)
}
@get:RequiresApi(30)
- private val build by lazy { classTetheringRequestBuilder.getDeclaredMethod("build") }
+ private val build by lazy @TargetApi(30) { classTetheringRequestBuilder.getDeclaredMethod("build") }
@get:RequiresApi(30)
private val interfaceStartTetheringCallback by lazy {
Class.forName("android.net.TetheringManager\$StartTetheringCallback")
}
@get:RequiresApi(30)
- private val startTethering by lazy {
+ private val startTethering by lazy @TargetApi(30) {
clazz.getDeclaredMethod("startTethering", Class.forName("android.net.TetheringManager\$TetheringRequest"),
Executor::class.java, interfaceStartTetheringCallback)
}
@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")
- @RequiresApi(24)
fun startTetheringLegacy(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
handler: Handler? = null, cacheDir: File = app.deviceStorage.codeCacheDir) {
val reference = WeakReference(callback)
val proxy = ProxyBuilder.forClass(classOnStartTetheringCallback).apply {
dexCache(cacheDir)
handler { proxy, method, args ->
- if (args.isNotEmpty()) Timber.w("Unexpected args for ${method.name}: $args")
@Suppress("NAME_SHADOWING") val callback = reference.get()
- when (method.name) {
- "onTetheringStarted" -> callback?.onTetheringStarted()
- "onTetheringFailed" -> callback?.onTetheringFailed()
- else -> ProxyBuilder.callSuper(proxy, method, args)
+ if (args.isEmpty()) when (method.name) {
+ "onTetheringStarted" -> return@handler callback?.onTetheringStarted()
+ "onTetheringFailed" -> return@handler callback?.onTetheringFailed()
}
+ ProxyBuilder.callSuper(proxy, method, args)
}
}.build()
startTetheringLegacy(Services.connectivity, type, showProvisioningUi, proxy, handler)
@@ -275,13 +258,9 @@ object TetheringManager {
arrayOf(interfaceStartTetheringCallback), object : InvocationHandler {
override fun invoke(proxy: Any, method: Method, args: Array?): Any? {
@Suppress("NAME_SHADOWING") val callback = reference.get()
- return when (val name = method.name) {
- "onTetheringStarted" -> {
- if (!args.isNullOrEmpty()) Timber.w("Unexpected args for $name: $args")
- callback?.onTetheringStarted()
- }
- "onTetheringFailed" -> {
- if (args?.size != 1) Timber.w("Unexpected args for $name: $args")
+ return when {
+ method.matches("onTetheringStarted") -> callback?.onTetheringStarted()
+ method.matches("onTetheringFailed", Integer.TYPE) -> {
callback?.onTetheringFailed(args?.get(0) as Int)
}
else -> callSuper(interfaceStartTetheringCallback, proxy, method, args)
@@ -312,7 +291,6 @@ object TetheringManager {
* configures tethering with the preferred local IPv4 link address to use.
* *@see setStaticIpv4Addresses
*/
- @RequiresApi(24)
fun startTethering(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
handler: Handler? = null, cacheDir: File = app.deviceStorage.codeCacheDir) {
if (Build.VERSION.SDK_INT >= 30) try {
@@ -384,12 +362,10 @@ object TetheringManager {
* {@link ConnectivityManager.TETHERING_USB}, or
* {@link ConnectivityManager.TETHERING_BLUETOOTH}.
*/
- @RequiresApi(24)
fun stopTethering(type: Int) {
if (Build.VERSION.SDK_INT >= 30) stopTethering(instance, type)
else stopTetheringLegacy(Services.connectivity, type)
}
- @RequiresApi(24)
fun stopTethering(type: Int, callback: (Exception) -> Unit) {
try {
stopTethering(type)
@@ -423,6 +399,23 @@ object TetheringManager {
*/
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) {
+ if ((supportedTypes - expectedTypes).isNotEmpty()) Timber.w(Exception(
+ "Unexpected supported tethering types: ${supportedTypes.joinToString()}"))
+ }
+
/**
* Called when tethering upstream changed.
*
@@ -445,7 +438,7 @@ object TetheringManager {
* *@param reg The new regular expressions.
* @hide
*/
- fun onTetherableInterfaceRegexpsChanged(args: Array?) {}
+ fun onTetherableInterfaceRegexpsChanged(reg: Any?) {}
/**
* 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")
}
@get:RequiresApi(30)
- private val registerTetheringEventCallback by lazy {
+ private val registerTetheringEventCallback by lazy @TargetApi(30) {
clazz.getDeclaredMethod("registerTetheringEventCallback", Executor::class.java, interfaceTetheringEventCallback)
}
@get:RequiresApi(30)
- private val unregisterTetheringEventCallback by lazy {
+ private val unregisterTetheringEventCallback by lazy @TargetApi(30) {
clazz.getDeclaredMethod("unregisterTetheringEventCallback", interfaceTetheringEventCallback)
}
@@ -541,40 +534,38 @@ object TetheringManager {
override fun invoke(proxy: Any, method: Method, args: Array?): Any? {
@Suppress("NAME_SHADOWING")
val callback = reference.get()
- val noArgs = args?.size ?: 0
- return when (val name = method.name) {
- "onTetheringSupported" -> {
- if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
+ return when {
+ method.matches("onTetheringSupported", Boolean::class.java) -> {
callback?.onTetheringSupported(args!![0] as Boolean)
}
- "onUpstreamChanged" -> {
- if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
+ method.matches1>("onSupportedTetheringTypes") -> {
+ @Suppress("UNCHECKED_CAST")
+ callback?.onSupportedTetheringTypes(args!![0] as Set)
+ }
+ method.matches1("onUpstreamChanged") -> {
callback?.onUpstreamChanged(args!![0] as Network?)
}
- "onTetherableInterfaceRegexpsChanged" -> {
- if (regexpsSent) callback?.onTetherableInterfaceRegexpsChanged(args)
+ method.name == "onTetherableInterfaceRegexpsChanged" &&
+ method.parameters.singleOrNull()?.type?.name ==
+ "android.net.TetheringManager\$TetheringInterfaceRegexps" -> {
+ if (regexpsSent) callback?.onTetherableInterfaceRegexpsChanged(args!!.single())
regexpsSent = true
}
- "onTetherableInterfacesChanged" -> {
- if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
+ method.matches1>("onTetherableInterfacesChanged") -> {
@Suppress("UNCHECKED_CAST")
callback?.onTetherableInterfacesChanged(args!![0] as List)
}
- "onTetheredInterfacesChanged" -> {
- if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
+ method.matches1>("onTetheredInterfacesChanged") -> {
@Suppress("UNCHECKED_CAST")
callback?.onTetheredInterfacesChanged(args!![0] as List)
}
- "onError" -> {
- if (noArgs != 2) Timber.w("Unexpected args for $name: $args")
+ method.matches("onError", String::class.java, Integer.TYPE) -> {
callback?.onError(args!![0] as String, args[1] as Int)
}
- "onClientsChanged" -> {
- if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
+ method.matches1>("onClientsChanged") -> {
callback?.onClientsChanged(args!![0] as Collection<*>)
}
- "onOffloadStatusChanged" -> {
- if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
+ method.matches("onOffloadStatusChanged", Integer.TYPE) -> {
callback?.onOffloadStatusChanged(args!![0] as Int)
}
else -> callSuper(interfaceTetheringEventCallback, proxy, method, args)
@@ -596,7 +587,11 @@ object TetheringManager {
@RequiresApi(30)
fun unregisterTetheringEventCallback(callback: TetheringEventCallback) {
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_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_DHCPSERVER_ERROR", "TETHER_ERROR_ENTITLEMENT_UNKNOWN") { clazz }
+ "TETHER_ERROR_DHCPSERVER_ERROR", "TETHER_ERROR_ENTITLEMENT_UNKNOWN") @TargetApi(30) { clazz }
@RequiresApi(30)
const val TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION = 14
- val Intent.tetheredIfaces get() = getStringArrayListExtra(
- if (Build.VERSION.SDK_INT >= 26) EXTRA_ACTIVE_TETHER else EXTRA_ACTIVE_TETHER_LEGACY)
- val Intent.localOnlyTetheredIfaces get() = if (Build.VERSION.SDK_INT >= 26) {
- getStringArrayListExtra(
- if (Build.VERSION.SDK_INT >= 30) EXTRA_ACTIVE_LOCAL_ONLY else EXTRA_ACTIVE_LOCAL_ONLY_LEGACY)
- } else emptyList()
+ val Intent.tetheredIfaces get() = getStringArrayListExtra(EXTRA_ACTIVE_TETHER)
+ val Intent.localOnlyTetheredIfaces get() = getStringArrayListExtra(
+ if (Build.VERSION.SDK_INT >= 30) EXTRA_ACTIVE_LOCAL_ONLY else EXTRA_ACTIVE_LOCAL_ONLY_LEGACY)
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/DefaultNetworkMonitor.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/DefaultNetworkMonitor.kt
index 6597cb9e..919f2f6f 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/DefaultNetworkMonitor.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/DefaultNetworkMonitor.kt
@@ -1,14 +1,12 @@
package be.mygod.vpnhotspot.net.monitor
-import android.annotation.TargetApi
import android.net.ConnectivityManager
import android.net.LinkProperties
import android.net.Network
import android.net.NetworkCapabilities
import android.os.Build
-import android.os.Handler
-import android.os.Looper
import be.mygod.vpnhotspot.util.Services
+import be.mygod.vpnhotspot.util.globalNetworkRequestBuilder
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -20,10 +18,10 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
* Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1:
* https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
*/
- private val networkRequest = networkRequestBuilder()
- .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
- .build()
+ private val networkRequest = globalNetworkRequestBuilder().apply {
+ addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+ }.build()
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
val properties = Services.connectivity.getLinkProperties(network)
@@ -53,23 +51,10 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
callback.onAvailable(currentLinkProperties)
}
} else {
- when (Build.VERSION.SDK_INT) {
- in 31..Int.MAX_VALUE -> @TargetApi(31) {
- Services.connectivity.registerBestMatchingNetworkCallback(networkRequest, networkCallback,
- Handler(Looper.getMainLooper()))
- }
- 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
- }
- }
+ if (Build.VERSION.SDK_INT >= 31) {
+ Services.connectivity.registerBestMatchingNetworkCallback(networkRequest, networkCallback,
+ Services.mainHandler)
+ } else Services.connectivity.requestNetwork(networkRequest, networkCallback, Services.mainHandler)
registered = true
}
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/InterfaceMonitor.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/InterfaceMonitor.kt
index 41b17b2b..f2466bf1 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/InterfaceMonitor.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/InterfaceMonitor.kt
@@ -6,6 +6,7 @@ import android.net.Network
import android.net.NetworkCapabilities
import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.allInterfaceNames
+import be.mygod.vpnhotspot.util.globalNetworkRequestBuilder
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -18,7 +19,7 @@ class InterfaceMonitor(private val ifaceRegex: String) : UpstreamMonitor() {
Timber.d(e);
{ it == ifaceRegex }
}
- private val request = networkRequestBuilder().apply {
+ private val request = globalNetworkRequestBuilder().apply {
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
@@ -76,7 +77,7 @@ class InterfaceMonitor(private val ifaceRegex: String) : UpstreamMonitor() {
callback.onAvailable(currentLinkProperties)
}
} else {
- Services.connectivity.registerNetworkCallback(request, networkCallback)
+ Services.registerNetworkCallback(request, networkCallback)
registered = true
}
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/IpMonitor.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/IpMonitor.kt
index 1999ff65..aafa1392 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/IpMonitor.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/IpMonitor.kt
@@ -5,7 +5,6 @@ import androidx.core.content.edit
import be.mygod.librootkotlinx.RootServer
import be.mygod.librootkotlinx.isEBADF
import be.mygod.vpnhotspot.App.Companion.app
-import be.mygod.vpnhotspot.BuildConfig
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.net.Routing
import be.mygod.vpnhotspot.root.ProcessData
@@ -25,12 +24,12 @@ abstract class IpMonitor {
companion object {
const val KEY = "service.ipMonitor"
// 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()
var currentMode: Mode
get() {
- val isLegacy = Build.VERSION.SDK_INT < 30 || BuildConfig.TARGET_SDK < 30
- val defaultMode = if (isLegacy) @Suppress("DEPRECATION") {
+ // Completely restricted on Android 13: https://github.com/termux/termux-app/issues/2993#issuecomment-1250312777
+ val defaultMode = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
Mode.Poll
} else Mode.MonitorRoot
return Mode.valueOf(app.pref.getString(KEY, defaultMode.toString()) ?: "")
@@ -114,8 +113,8 @@ abstract class IpMonitor {
try {
RootManager.use { server ->
// 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),
- this))
+ handleChannel(server.create(ProcessListener(errorMatcher,
+ Routing.IP, "monitor", monitoredObject), this))
}
} catch (_: CancellationException) {
} catch (e: Exception) {
@@ -152,7 +151,7 @@ abstract class IpMonitor {
fun flushAsync() = GlobalScope.launch(Dispatchers.IO) { flush() }
private suspend fun work(server: RootServer?): RootServer? {
- if (currentMode != Mode.PollRoot) try {
+ if (currentMode != Mode.PollRoot && currentMode != Mode.MonitorRoot) try {
poll()
return server
} catch (e: IOException) {
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/TetherTimeoutMonitor.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/TetherTimeoutMonitor.kt
index abba1855..9823f7d2 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/TetherTimeoutMonitor.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/TetherTimeoutMonitor.kt
@@ -33,30 +33,41 @@ class TetherTimeoutMonitor(private val timeout: Long = 0,
private const val MIN_SOFT_AP_TIMEOUT_DELAY_MS = 600_000 // 10 minutes
@Deprecated("Use SoftApConfigurationCompat instead")
- @get:RequiresApi(28)
val enabled get() = Settings.Global.getInt(app.contentResolver, SOFT_AP_TIMEOUT_ENABLED, 1) == 1
@Deprecated("Use SoftApConfigurationCompat instead")
suspend fun setEnabled(value: Boolean) = SettingsGlobalPut.int(SOFT_AP_TIMEOUT_ENABLED, if (value) 1 else 0)
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 {
getInteger(getIdentifier("config_wifi_framework_soft_ap_timeout_delay", "integer", "android"))
} else {
val info = WifiApManager.resolvedActivity.activityInfo
val resources = app.packageManager.getResourcesForApplication(info.applicationInfo)
- resources.getInteger(resources.findIdentifier("config_wifiFrameworkSoftApShutDownTimeoutMilliseconds",
- "integer", WifiApManager.RESOURCES_PACKAGE, info.packageName))
+ resources.getInteger(resources.findIdentifier(
+ "config_wifiFrameworkSoftApShutDownTimeoutMilliseconds", "integer",
+ WifiApManager.RESOURCES_PACKAGE, info.packageName))
}
} catch (e: RuntimeException) {
Timber.w(e)
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) {
Timber.w("Overriding timeout delay with minimum limit value: $delay < $MIN_SOFT_AP_TIMEOUT_DELAY_MS")
MIN_SOFT_AP_TIMEOUT_DELAY_MS
} 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
@@ -74,7 +85,7 @@ class TetherTimeoutMonitor(private val timeout: Long = 0,
fun onClientsChanged(noClient: Boolean) {
this.noClient = noClient
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()
}
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/TrafficRecorder.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/TrafficRecorder.kt
index 52e3dbf8..c37596d5 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/TrafficRecorder.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/TrafficRecorder.kt
@@ -1,9 +1,9 @@
package be.mygod.vpnhotspot.net.monitor
+import android.net.MacAddress
import androidx.collection.LongSparseArray
import androidx.collection.set
import be.mygod.vpnhotspot.net.IpDev
-import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.Routing.Companion.IPTABLES
import be.mygod.vpnhotspot.room.AppDatabase
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.parseNumericAddress
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 java.net.InetAddress
import java.util.concurrent.TimeUnit
@@ -23,8 +28,8 @@ object TrafficRecorder {
private val records = mutableMapOf()
val foregroundListeners = Event2, LongSparseArray>()
- fun register(ip: InetAddress, downstream: String, mac: MacAddressCompat) {
- val record = TrafficRecord(mac = mac.addr, ip = ip, downstream = downstream)
+ fun register(ip: InetAddress, downstream: String, mac: MacAddress) {
+ val record = TrafficRecord(mac = mac, ip = ip, downstream = downstream)
AppDatabase.instance.trafficRecordDao.insert(record)
synchronized(this) {
val key = IpDev(ip, downstream)
@@ -107,9 +112,9 @@ object TrafficRecorder {
record.sentBytes = columns[1].toLong()
}
}
- if (oldRecord.id != null) {
+ oldRecord.id?.let { oldId ->
check(records.put(key, record) == oldRecord)
- oldRecords[oldRecord.id!!] = oldRecord
+ oldRecords[oldId] = oldRecord
}
}
else -> check(false)
@@ -130,6 +135,7 @@ object TrafficRecorder {
}
fun update(timeout: Boolean = false) {
synchronized(this) {
+ unscheduleUpdateLocked()
if (records.isEmpty()) return
val timestamp = System.currentTimeMillis()
if (!timeout && timestamp - lastUpdate <= 100) return
@@ -141,7 +147,6 @@ object TrafficRecorder {
SmartSnackbar.make(e).show()
}
lastUpdate = timestamp
- updateJob = null
scheduleUpdateLocked()
}
}
@@ -156,5 +161,5 @@ object TrafficRecorder {
/**
* 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 }
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/UpstreamMonitor.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/UpstreamMonitor.kt
index c8629496..07e54dc4 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/UpstreamMonitor.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/UpstreamMonitor.kt
@@ -2,9 +2,6 @@ package be.mygod.vpnhotspot.net.monitor
import android.content.SharedPreferences
import android.net.LinkProperties
-import android.net.NetworkCapabilities
-import android.net.NetworkRequest
-import android.os.Build
import be.mygod.vpnhotspot.App.Companion.app
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -23,13 +20,6 @@ abstract class UpstreamMonitor {
}
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 unregisterCallback(callback: Callback) = synchronized(this) { monitor.unregisterCallback(callback) }
@@ -56,15 +46,6 @@ abstract class UpstreamMonitor {
* Called if some possibly stacked interface is available
*/
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()
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/VpnMonitor.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/VpnMonitor.kt
index 03ef2575..1e3997ca 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/VpnMonitor.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/VpnMonitor.kt
@@ -5,15 +5,16 @@ import android.net.LinkProperties
import android.net.Network
import android.net.NetworkCapabilities
import be.mygod.vpnhotspot.util.Services
+import be.mygod.vpnhotspot.util.globalNetworkRequestBuilder
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
object VpnMonitor : UpstreamMonitor() {
- private val request = networkRequestBuilder()
- .addTransportType(NetworkCapabilities.TRANSPORT_VPN)
- .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
- .build()
+ private val request = globalNetworkRequestBuilder().apply {
+ addTransportType(NetworkCapabilities.TRANSPORT_VPN)
+ removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+ }.build()
private var registered = false
private val available = HashMap()
@@ -60,7 +61,7 @@ object VpnMonitor : UpstreamMonitor() {
callback.onAvailable(currentLinkProperties)
}
} else {
- Services.connectivity.registerNetworkCallback(request, networkCallback)
+ Services.registerNetworkCallback(request, networkCallback)
registered = true
}
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/P2pSupplicantConfiguration.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/P2pSupplicantConfiguration.kt
index 00f22e56..8f178127 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/P2pSupplicantConfiguration.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/P2pSupplicantConfiguration.kt
@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot.net.wifi
+import android.net.MacAddress
import android.net.wifi.p2p.WifiP2pGroup
import be.mygod.vpnhotspot.RepeaterService
import be.mygod.vpnhotspot.net.MacAddressCompat
@@ -53,8 +54,8 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) {
var bssids = listOfNotNull(group?.owner?.deviceAddress, ownerAddress)
.distinct()
.filter {
+ val mac = MacAddress.fromString(it)
try {
- val mac = MacAddressCompat.fromString(it)
mac != MacAddressCompat.ALL_ZEROS_ADDRESS && mac != MacAddressCompat.ANY_ADDRESS
} catch (_: IllegalArgumentException) {
false
@@ -75,7 +76,13 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) {
if (matchedBssid.isEmpty()) {
check(block.pskLine == null && block.psk == 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
} else if (bssids.any { matchedBssid.equals(it, true) }) {
@@ -120,7 +127,7 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) {
add("\tmode=3")
add("\tdisabled=2")
add("}")
- if (target == null) target = this
+ target = this
})
}
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 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
- block[block.ssidLine!!] = "\tssid=" + ssid.toByteArray()
- .joinToString("") { (it.toInt() and 255).toString(16).padStart(2, '0') }
+ block[block.ssidLine!!] = "\tssid=${ssid.hex}"
block[block.pskLine!!] = "\tpsk=\"$psk\"" // no control chars or weird stuff
if (bssid != null) {
persistentMacLine?.let { lines[it] = PERSISTENT_MAC + bssid }
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApCapability.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApCapability.kt
index 24432405..556e4bc5 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApCapability.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApCapability.kt
@@ -1,20 +1,27 @@
package be.mygod.vpnhotspot.net.wifi
+import android.annotation.TargetApi
+import android.os.Build
import android.os.Parcelable
import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.util.LongConstantLookup
+import be.mygod.vpnhotspot.util.UnblockCentral
+import timber.log.Timber
@JvmInline
@RequiresApi(30)
value class SoftApCapability(val inner: Parcelable) {
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 areFeaturesSupported by lazy { clazz.getDeclaredMethod("areFeaturesSupported", Long::class.java) }
@get:RequiresApi(31)
private val getSupportedChannelList by lazy {
clazz.getDeclaredMethod("getSupportedChannelList", Int::class.java)
}
+ @get:RequiresApi(31)
+ @get:TargetApi(33)
+ private val getCountryCode by lazy { UnblockCentral.getCountryCode(clazz) }
@RequiresApi(31)
const val SOFTAP_FEATURE_BAND_24G_SUPPORTED = 32L
@@ -38,4 +45,11 @@ value class SoftApCapability(val inner: Parcelable) {
return supportedFeatures
}
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
+ }
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApConfigurationCompat.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApConfigurationCompat.kt
index 59a60d05..f82b4d8a 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApConfigurationCompat.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApConfigurationCompat.kt
@@ -3,59 +3,70 @@ package be.mygod.vpnhotspot.net.wifi
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.net.MacAddress
+import android.net.wifi.ScanResult
import android.net.wifi.SoftApConfiguration
+import android.net.wifi.WifiSsid
import android.os.Build
import android.os.Parcelable
import android.util.SparseIntArray
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.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.UnblockCentral
import kotlinx.parcelize.Parcelize
import timber.log.Timber
+import java.lang.reflect.InvocationTargetException
@Parcelize
data class SoftApConfigurationCompat(
- var ssid: String? = null,
- @Deprecated("Workaround for using inline class with Parcelize, use bssid")
- var bssidAddr: Long? = null,
- var passphrase: String? = null,
- var isHiddenSsid: Boolean = false,
- /**
- * To read legacy band/channel pair, use [requireSingleBand]. For easy access, see [getChannel].
- *
- * You should probably set or modify this field directly only when you want to use bridged AP,
- * see also [android.net.wifi.WifiManager.isBridgedApConcurrencySupported].
- * Otherwise, use [optimizeChannels] or [setChannel].
- */
- @TargetApi(23)
- var channels: SparseIntArray = SparseIntArray(1).apply { put(BAND_2GHZ, 0) },
- var securityType: Int = SoftApConfiguration.SECURITY_TYPE_OPEN,
- @TargetApi(30)
- var maxNumberOfClients: Int = 0,
- @TargetApi(28)
- var isAutoShutdownEnabled: Boolean = true,
- @TargetApi(28)
- var shutdownTimeoutMillis: Long = 0,
- @TargetApi(30)
- var isClientControlByUserEnabled: Boolean = false,
- @RequiresApi(30)
- var blockedClientList: List = emptyList(),
- @RequiresApi(30)
- var allowedClientList: List = emptyList(),
- @TargetApi(31)
- var macRandomizationSetting: Int = RANDOMIZATION_PERSISTENT,
- @TargetApi(31)
- var isBridgedModeOpportunisticShutdownEnabled: Boolean = true,
- @TargetApi(31)
- var isIeee80211axEnabled: Boolean = true,
- @TargetApi(31)
- var isUserConfiguration: Boolean = true,
- var underlying: Parcelable? = null) : Parcelable {
+ var ssid: WifiSsidCompat? = null,
+ var bssid: MacAddress? = null,
+ var passphrase: String? = null,
+ var isHiddenSsid: Boolean = false,
+ /**
+ * You should probably set or modify this field directly only when you want to use bridged AP,
+ * see also [android.net.wifi.WifiManager.isBridgedApConcurrencySupported].
+ * Otherwise, use [requireSingleBand] and [setChannel].
+ */
+ var channels: SparseIntArray = SparseIntArray(1).apply { append(BAND_2GHZ, 0) },
+ var securityType: Int = SoftApConfiguration.SECURITY_TYPE_OPEN,
+ @TargetApi(30)
+ var maxNumberOfClients: Int = 0,
+ var isAutoShutdownEnabled: Boolean = true,
+ var shutdownTimeoutMillis: Long = 0,
+ @TargetApi(30)
+ var isClientControlByUserEnabled: Boolean = false,
+ @RequiresApi(30)
+ var blockedClientList: List = emptyList(),
+ @RequiresApi(30)
+ var allowedClientList: List = emptyList(),
+ @TargetApi(31)
+ var macRandomizationSetting: Int = if (Build.VERSION.SDK_INT >= 33) {
+ RANDOMIZATION_NON_PERSISTENT
+ } else RANDOMIZATION_PERSISTENT,
+ @TargetApi(31)
+ var isBridgedModeOpportunisticShutdownEnabled: Boolean = true,
+ @TargetApi(31)
+ var isIeee80211axEnabled: Boolean = true,
+ @TargetApi(33)
+ var isIeee80211beEnabled: Boolean = true,
+ @TargetApi(31)
+ var isUserConfiguration: Boolean = true,
+ @TargetApi(33)
+ var bridgedModeOpportunisticShutdownTimeoutMillis: Long = -1L,
+ @TargetApi(33)
+ var vendorElements: List = emptyList(),
+ @TargetApi(33)
+ var persistentRandomizedMacAddress: MacAddress? = null,
+ @TargetApi(33)
+ var allowedAcsChannels: Map> = emptyMap(),
+ @TargetApi(33)
+ var maxChannelBandwidth: Int = CHANNEL_WIDTH_AUTO,
+ var underlying: Parcelable? = null,
+) : Parcelable {
companion object {
const val BAND_2GHZ = 1
const val BAND_5GHZ = 2
@@ -64,20 +75,32 @@ data class SoftApConfigurationCompat(
@TargetApi(31)
const val BAND_60GHZ = 8
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 {
- if (BuildCompat.isAtLeastS()) try {
+ if (Build.VERSION.SDK_INT >= 31) try {
return@lazy UnblockCentral.SoftApConfiguration_BAND_TYPES
} catch (e: ReflectiveOperationException) {
Timber.w(e)
}
intArrayOf(BAND_2GHZ, BAND_5GHZ, BAND_6GHZ, BAND_60GHZ)
}
- val bandLookup = ConstantLookup("BAND_", null, "2GHZ", "5GHZ")
+ @RequiresApi(31)
+ val bandLookup = ConstantLookup("BAND_")
@TargetApi(31)
const val RANDOMIZATION_NONE = 0
@TargetApi(31)
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
@@ -86,15 +109,19 @@ data class SoftApConfigurationCompat(
*/
private const val LEGACY_WPA2_PSK = 4
- val securityTypes = arrayOf("OPEN", "WPA2-PSK", "WPA3-SAE", "WPA3-SAE Transition mode")
-
- private val qrSanitizer = Regex("([\\\\\":;,])")
+ val securityTypes = arrayOf(
+ "OPEN",
+ "WPA2-PSK",
+ "WPA3-SAE Transition mode",
+ "WPA3-SAE",
+ "WPA3-OWE Transition",
+ "WPA3-OWE",
+ )
/**
* Based on:
* 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:frameworks/base/wifi/java/android/net/wifi/ScanResult.java;l=624;drc=f7ccda05642b55700d67a288462bada488fc7f5e
+ * https://cs.android.com/android/platform/superproject/+/master:packages/modules/Wifi/framework/java/android/net/wifi/ScanResult.java;l=789;drc=71d758698c45984d3f8de981bf98e56902480f16
*/
fun channelToFrequency(band: Int, chan: Int) = when (band) {
BAND_2GHZ -> when (chan) {
@@ -109,7 +136,7 @@ data class SoftApConfigurationCompat(
}
BAND_6GHZ -> when (chan) {
2 -> 5935
- in 1..233 -> 5950 + chan * 5
+ in 1..253 -> 5950 + chan * 5
else -> throw IllegalArgumentException("Invalid 6GHz channel $chan")
}
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
*/
- @get:RequiresApi(23)
@Suppress("DEPRECATION")
/**
* The band which AP resides on
@@ -142,7 +168,6 @@ data class SoftApConfigurationCompat(
* By default, 2G is chosen
*/
private val apBand by lazy { android.net.wifi.WifiConfiguration::class.java.getDeclaredField("apBand") }
- @get:RequiresApi(23)
@Suppress("DEPRECATION")
/**
* The channel which AP resides on
@@ -154,6 +179,10 @@ data class SoftApConfigurationCompat(
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)
private val getAllowedClientList by lazy @TargetApi(30) {
SoftApConfiguration::class.java.getDeclaredMethod("getAllowedClientList")
@@ -164,6 +193,10 @@ data class SoftApConfigurationCompat(
private val getBlockedClientList by lazy @TargetApi(30) {
SoftApConfiguration::class.java.getDeclaredMethod("getBlockedClientList")
}
+ @get:RequiresApi(33)
+ private val getBridgedModeOpportunisticShutdownTimeoutMillis by lazy @TargetApi(33) {
+ SoftApConfiguration::class.java.getDeclaredMethod("getBridgedModeOpportunisticShutdownTimeoutMillis")
+ }
@get:RequiresApi(30)
private val getChannel by lazy @TargetApi(30) {
SoftApConfiguration::class.java.getDeclaredMethod("getChannel")
@@ -176,14 +209,26 @@ data class SoftApConfigurationCompat(
private val getMacRandomizationSetting by lazy @TargetApi(31) {
SoftApConfiguration::class.java.getDeclaredMethod("getMacRandomizationSetting")
}
+ @get:RequiresApi(33)
+ private val getMaxChannelBandwidth by lazy @TargetApi(33) {
+ SoftApConfiguration::class.java.getDeclaredMethod("getMaxChannelBandwidth")
+ }
@get:RequiresApi(30)
private val getMaxNumberOfClients by lazy @TargetApi(30) {
SoftApConfiguration::class.java.getDeclaredMethod("getMaxNumberOfClients")
}
+ @get:RequiresApi(33)
+ private val getPersistentRandomizedMacAddress by lazy @TargetApi(33) {
+ SoftApConfiguration::class.java.getDeclaredMethod("getPersistentRandomizedMacAddress")
+ }
@get:RequiresApi(30)
private val getShutdownTimeoutMillis by lazy @TargetApi(30) {
SoftApConfiguration::class.java.getDeclaredMethod("getShutdownTimeoutMillis")
}
+ @get:RequiresApi(33)
+ private val getVendorElements by lazy @TargetApi(33) {
+ SoftApConfiguration::class.java.getDeclaredMethod("getVendorElements")
+ }
@get:RequiresApi(30)
private val isAutoShutdownEnabled by lazy @TargetApi(30) {
SoftApConfiguration::class.java.getDeclaredMethod("isAutoShutdownEnabled")
@@ -200,6 +245,10 @@ data class SoftApConfigurationCompat(
private val isIeee80211axEnabled by lazy @TargetApi(31) {
SoftApConfiguration::class.java.getDeclaredMethod("isIeee80211axEnabled")
}
+ @get:RequiresApi(33)
+ private val isIeee80211beEnabled by lazy @TargetApi(33) {
+ SoftApConfiguration::class.java.getDeclaredMethod("isIeee80211beEnabled")
+ }
@get:RequiresApi(31)
private val isUserConfiguration by lazy @TargetApi(31) {
SoftApConfiguration::class.java.getDeclaredMethod("isUserConfiguration")
@@ -210,78 +259,106 @@ data class SoftApConfigurationCompat(
@get:RequiresApi(30)
private val newBuilder by lazy @TargetApi(30) { classBuilder.getConstructor(SoftApConfiguration::class.java) }
@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)
- private val setAllowedClientList by lazy {
+ private val setAllowedClientList by lazy @TargetApi(30) {
classBuilder.getDeclaredMethod("setAllowedClientList", java.util.List::class.java)
}
@get:RequiresApi(30)
- private val setAutoShutdownEnabled by lazy {
+ private val setAutoShutdownEnabled by lazy @TargetApi(30) {
classBuilder.getDeclaredMethod("setAutoShutdownEnabled", Boolean::class.java)
}
@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)
- private val setBlockedClientList by lazy {
+ private val setBlockedClientList by lazy @TargetApi(30) {
classBuilder.getDeclaredMethod("setBlockedClientList", java.util.List::class.java)
}
@get:RequiresApi(31)
- private val setBridgedModeOpportunisticShutdownEnabled by lazy {
+ private val setBridgedModeOpportunisticShutdownEnabled by lazy @TargetApi(31) {
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)
private val setBssid by lazy @TargetApi(30) {
classBuilder.getDeclaredMethod("setBssid", MacAddress::class.java)
}
@get:RequiresApi(30)
- private val setChannel by lazy {
+ private val setChannel by lazy @TargetApi(30) {
classBuilder.getDeclaredMethod("setChannel", Int::class.java, Int::class.java)
}
@get:RequiresApi(31)
- private val setChannels by lazy {
+ private val setChannels by lazy @TargetApi(31) {
classBuilder.getDeclaredMethod("setChannels", SparseIntArray::class.java)
}
@get:RequiresApi(30)
- private val setClientControlByUserEnabled by lazy {
+ private val setClientControlByUserEnabled by lazy @TargetApi(30) {
classBuilder.getDeclaredMethod("setClientControlByUserEnabled", Boolean::class.java)
}
@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)
- private val setIeee80211axEnabled by lazy {
+ private val setIeee80211axEnabled by lazy @TargetApi(31) {
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)
- private val setMacRandomizationSetting by lazy {
+ private val setMacRandomizationSetting by lazy @TargetApi(31) {
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)
- private val setMaxNumberOfClients by lazy {
+ private val setMaxNumberOfClients by lazy @TargetApi(31) {
classBuilder.getDeclaredMethod("setMaxNumberOfClients", Int::class.java)
}
@get:RequiresApi(30)
- private val setPassphrase by lazy {
+ private val setPassphrase by lazy @TargetApi(30) {
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)
- private val setShutdownTimeoutMillis by lazy {
+ private val setShutdownTimeoutMillis by lazy @TargetApi(30) {
classBuilder.getDeclaredMethod("setShutdownTimeoutMillis", Long::class.java)
}
@get:RequiresApi(30)
- private val setSsid by lazy { classBuilder.getDeclaredMethod("setSsid", String::class.java) }
- @get:RequiresApi(31)
- private val setUserConfiguration by lazy @TargetApi(31) { UnblockCentral.setUserConfiguration(classBuilder) }
+ private val setSsid by lazy @TargetApi(30) { classBuilder.getDeclaredMethod("setSsid", String::class.java) }
+ @get:RequiresApi(33)
+ 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")
@Suppress("DEPRECATION")
fun android.net.wifi.WifiConfiguration.toCompat() = SoftApConfigurationCompat(
- SSID,
- BSSID?.let { MacAddressCompat.fromString(it) }?.addr,
+ WifiSsidCompat.fromUtf8Text(SSID),
+ BSSID?.let { MacAddress.fromString(it) },
preSharedKey,
hiddenSSID,
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/wifi/java/android/net/wifi/SoftApConfToXmlMigrationUtil.java;l=87;drc=aa6527cf41671d1ed417b8ebdb6b3aa614f62344
- SparseIntArray(1).apply {
- if (Build.VERSION.SDK_INT < 23) put(BAND_LEGACY, 0) else put(when (val band = apBand.getInt(this)) {
+ SparseIntArray(1).also {
+ it.append(when (val band = apBand.getInt(this)) {
0 -> BAND_2GHZ
1 -> BAND_5GHZ
-1 -> BAND_LEGACY
@@ -302,77 +379,102 @@ data class SoftApConfigurationCompat(
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
}
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
.getOrElse(selected) { "?" }.let {
throw IllegalArgumentException("Unrecognized key management $it ($selected)")
}
}
},
- isAutoShutdownEnabled = if (Build.VERSION.SDK_INT >= 28) TetherTimeoutMonitor.enabled else false,
+ isAutoShutdownEnabled = TetherTimeoutMonitor.enabled,
underlying = this)
@RequiresApi(30)
@Suppress("UNCHECKED_CAST")
fun SoftApConfiguration.toCompat() = SoftApConfigurationCompat(
- ssid,
- bssid?.toCompat()?.addr,
- passphrase,
- isHiddenSsid,
- if (BuildCompat.isAtLeastS()) getChannels(this) as SparseIntArray else SparseIntArray(1).apply {
- put(getBand(this) as Int, getChannel(this) as Int)
- },
- securityType,
- getMaxNumberOfClients(this) as Int,
- isAutoShutdownEnabled(this) as Boolean,
- getShutdownTimeoutMillis(this) as Long,
- isClientControlByUserEnabled(this) as Boolean,
- getBlockedClientList(this) as List,
- getAllowedClientList(this) as List,
- getMacRandomizationSetting(this) as Int,
- isBridgedModeOpportunisticShutdownEnabled(this) as Boolean,
- isIeee80211axEnabled(this) as Boolean,
- isUserConfiguration(this) as Boolean,
- this)
- }
-
- @Suppress("DEPRECATION")
- inline var bssid: MacAddressCompat?
- get() = bssidAddr?.let { MacAddressCompat(it) }
- set(value) {
- bssidAddr = value?.addr
+ if (Build.VERSION.SDK_INT >= 33) wifiSsid?.toCompat() else @Suppress("DEPRECATION") {
+ WifiSsidCompat.fromUtf8Text(ssid)
+ },
+ bssid,
+ passphrase,
+ isHiddenSsid,
+ if (Build.VERSION.SDK_INT >= 31) getChannels(this) as SparseIntArray else SparseIntArray(1).also {
+ it.append(getBand(this) as Int, getChannel(this) as Int)
+ },
+ securityType,
+ getMaxNumberOfClients(this) as Int,
+ isAutoShutdownEnabled(this) as Boolean,
+ getShutdownTimeoutMillis(this) as Long,
+ isClientControlByUserEnabled(this) as Boolean,
+ getBlockedClientList(this) as List,
+ getAllowedClientList(this) as List,
+ underlying = this,
+ ).also {
+ if (Build.VERSION.SDK_INT < 31) return@also
+ it.macRandomizationSetting = getMacRandomizationSetting(this) as Int
+ it.isBridgedModeOpportunisticShutdownEnabled = isBridgedModeOpportunisticShutdownEnabled(this) as Boolean
+ it.isIeee80211axEnabled = isIeee80211axEnabled(this) as Boolean
+ it.isUserConfiguration = isUserConfiguration(this) as Boolean
+ if (Build.VERSION.SDK_INT < 33) return@also
+ it.isIeee80211beEnabled = isIeee80211beEnabled(this) as Boolean
+ it.bridgedModeOpportunisticShutdownTimeoutMillis =
+ getBridgedModeOpportunisticShutdownTimeoutMillis(this) as Long
+ it.vendorElements = getVendorElements(this) as List
+ 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
- */
- fun requireSingleBand(): Pair {
- require(channels.size() == 1) { "Unsupported number of bands configured" }
- 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]
+ /**
+ * Only single band/channel can be supplied on API 23-30
+ */
+ fun requireSingleBand(channels: SparseIntArray): Pair {
+ require(channels.size() == 1) { "Unsupported number of bands configured" }
+ return channels.keyAt(0) to channels.valueAt(0)
}
- 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) =
+ 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) {
- channels = SparseIntArray(1).apply { put(band, channel) }
- }
- fun optimizeChannels(channels: SparseIntArray = this.channels) {
- this.channels = SparseIntArray(channels.size()).apply {
- var setBand = 0
- for (band in channels.keyIterator()) if (channels[band] == 0) setBand = setBand or band
- 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])
+ channels = SparseIntArray(1).apply {
+ append(when {
+ channel <= 0 || band != BAND_LEGACY -> band
+ channel > 14 -> BAND_5GHZ
+ else -> BAND_2GHZ
+ }, channel)
}
}
- fun setMacRandomizationEnabled(enabled: Boolean) {
- macRandomizationSetting = if (enabled) RANDOMIZATION_PERSISTENT else RANDOMIZATION_NONE
- }
-
/**
* Based on:
* 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()")
@Suppress("DEPRECATION")
fun toWifiConfiguration(): android.net.wifi.WifiConfiguration {
- val (band, channel) = requireSingleBand()
+ val (band, channel) = requireSingleBand(channels)
val wc = underlying as? android.net.wifi.WifiConfiguration
val result = if (wc == null) android.net.wifi.WifiConfiguration() else android.net.wifi.WifiConfiguration(wc)
val original = wc?.toCompat()
- result.SSID = ssid
+ result.SSID = ssid?.toString()
result.preSharedKey = passphrase
result.hiddenSSID = isHiddenSsid
- if (Build.VERSION.SDK_INT >= 23) {
- apBand.setInt(result, when (band) {
- BAND_2GHZ -> 0
- BAND_5GHZ -> 1
- else -> {
- require(Build.VERSION.SDK_INT >= 28) { "A band must be specified on this platform" }
- require(isLegacyEitherBand(band)) { "Convert fail, unsupported band setting :$band" }
- -1
- }
- })
- apChannel.setInt(result, channel)
- } else require(isLegacyEitherBand(band)) { "Specifying band is unsupported on this platform" }
+ apBand.setInt(result, when (band) {
+ BAND_2GHZ -> 0
+ BAND_5GHZ -> 1
+ else -> {
+ require(isLegacyEitherBand(band)) { "Convert fail, unsupported band setting :$band" }
+ -1
+ }
+ })
+ apChannel.setInt(result, channel)
if (original?.securityType != securityType) {
result.allowedKeyManagement.clear()
result.allowedKeyManagement.set(when (securityType) {
@@ -411,6 +510,8 @@ data class SoftApConfigurationCompat(
// CHANGED: not actually converted in framework-wifi
SoftApConfiguration.SECURITY_TYPE_WPA3_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")
})
result.allowedAuthAlgorithms.clear()
@@ -425,29 +526,59 @@ data class SoftApConfigurationCompat(
fun toPlatform(): SoftApConfiguration {
val sac = underlying as? SoftApConfiguration
val builder = if (sac == null) classBuilder.newInstance() else newBuilder.newInstance(sac)
- setSsid(builder, ssid)
- setPassphrase(builder, if (securityType == SoftApConfiguration.SECURITY_TYPE_OPEN) null else passphrase,
- securityType)
- if (BuildCompat.isAtLeastS()) setChannels(builder, channels) else {
- val (band, channel) = requireSingleBand()
- if (channel == 0) setBand(builder, band) else setChannel(builder, channel, band)
- }
- setBssid(builder, bssid?.toPlatform())
+ if (Build.VERSION.SDK_INT >= 33) {
+ setWifiSsid(builder, ssid?.toPlatform())
+ } else setSsid(builder, ssid?.toString())
+ setPassphrase(builder, when (securityType) {
+ SoftApConfiguration.SECURITY_TYPE_OPEN,
+ SoftApConfiguration.SECURITY_TYPE_WPA3_OWE_TRANSITION,
+ SoftApConfiguration.SECURITY_TYPE_WPA3_OWE -> null
+ else -> passphrase
+ }, securityType)
+ setChannelsCompat(builder, channels)
+ setBssid(builder,
+ if (Build.VERSION.SDK_INT < 31 || macRandomizationSetting == RANDOMIZATION_NONE) bssid else null)
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)
setClientControlByUserEnabled(builder, isClientControlByUserEnabled)
setHiddenSsid(builder, isHiddenSsid)
setAllowedClientList(builder, allowedClientList)
setBlockedClientList(builder, blockedClientList)
- if (BuildCompat.isAtLeastS()) {
+ if (Build.VERSION.SDK_INT >= 31) {
setMacRandomizationSetting(builder, macRandomizationSetting)
setBridgedModeOpportunisticShutdownEnabled(builder, isBridgedModeOpportunisticShutdownEnabled)
setIeee80211axEnabled(builder, isIeee80211axEnabled)
- if (sac?.let { isUserConfiguration(it) as Boolean } != false != isUserConfiguration) try {
- setUserConfiguration(builder, isUserConfiguration)
- } catch (e: ReflectiveOperationException) {
- Timber.w(e) // as far as we are concerned, this field is not used anywhere so ignore for now
+ if (Build.VERSION.SDK_INT >= 33) {
+ setIeee80211beEnabled(builder, isIeee80211beEnabled)
+ setBridgedModeOpportunisticShutdownTimeoutMillis(builder, bridgedModeOpportunisticShutdownTimeoutMillis)
+ 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
@@ -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
*/
fun toQrCode() = StringBuilder("WIFI:").apply {
- fun String.sanitize() = qrSanitizer.replace(this) { "\\${it.groupValues[1]}" }
when (securityType) {
- SoftApConfiguration.SECURITY_TYPE_OPEN -> { }
- SoftApConfiguration.SECURITY_TYPE_WPA2_PSK -> append("T:WPA;")
- SoftApConfiguration.SECURITY_TYPE_WPA3_SAE, SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION -> {
- append("T:SAE;")
+ SoftApConfiguration.SECURITY_TYPE_OPEN, SoftApConfiguration.SECURITY_TYPE_WPA3_OWE_TRANSITION,
+ SoftApConfiguration.SECURITY_TYPE_WPA3_OWE -> { }
+ SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION -> {
+ append("T:WPA;")
}
+ SoftApConfiguration.SECURITY_TYPE_WPA3_SAE -> append("T:SAE;")
else -> throw IllegalArgumentException("Unsupported authentication type")
}
append("S:")
- append(ssid!!.sanitize())
+ append(ssid!!.toMeCard())
append(';')
passphrase?.let { passphrase ->
append("P:")
- append(passphrase.sanitize())
+ append(WifiSsidCompat.toMeCard(passphrase))
append(';')
}
if (isHiddenSsid) append("H:true;")
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApInfo.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApInfo.kt
index ccec77cf..167534d5 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApInfo.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApInfo.kt
@@ -12,7 +12,7 @@ import timber.log.Timber
@RequiresApi(30)
value class SoftApInfo(val inner: Parcelable) {
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 getBandwidth by lazy { clazz.getDeclaredMethod("getBandwidth") }
@get:RequiresApi(31)
@@ -30,7 +30,7 @@ value class SoftApInfo(val inner: Parcelable) {
val frequency get() = getFrequency(inner) as Int
val bandwidth get() = getBandwidth(inner) as Int
@get:RequiresApi(31)
- val bssid get() = getBssid(inner) as MacAddress
+ val bssid get() = getBssid(inner) as MacAddress?
@get:RequiresApi(31)
val wifiStandard get() = getWifiStandard(inner) as Int
@get:RequiresApi(31)
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/VendorElements.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/VendorElements.kt
new file mode 100644
index 00000000..9b527d29
--- /dev/null
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/VendorElements.kt
@@ -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) = 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) }
+}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialogFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialogFragment.kt
index 2715258f..cbb249c9 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialogFragment.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialogFragment.kt
@@ -1,13 +1,15 @@
package be.mygod.vpnhotspot.net.wifi
import android.annotation.SuppressLint
-import android.annotation.TargetApi
import android.content.ClipData
+import android.content.ClipDescription
import android.content.DialogInterface
+import android.net.MacAddress
import android.net.wifi.SoftApConfiguration
import android.os.Build
import android.os.Parcelable
import android.text.Editable
+import android.text.InputFilter
import android.text.TextWatcher
import android.util.Base64
import android.util.SparseIntArray
@@ -16,10 +18,11 @@ import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
+import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
-import androidx.core.os.BuildCompat
+import androidx.core.os.persistableBundleOf
import androidx.core.view.isGone
import be.mygod.librootkotlinx.toByteArray
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.RepeaterService
import be.mygod.vpnhotspot.databinding.DialogWifiApBinding
-import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
import be.mygod.vpnhotspot.util.QRCodeDialog
+import be.mygod.vpnhotspot.util.RangeInput
import be.mygod.vpnhotspot.util.readableMessage
import be.mygod.vpnhotspot.util.showAllowingStateLoss
-import be.mygod.vpnhotspot.widget.SmartSnackbar
+import com.google.android.material.textfield.TextInputLayout
import kotlinx.parcelize.Parcelize
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
@@ -48,26 +53,34 @@ class WifiApDialogFragment : AlertDialogFragment= 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 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 {
+ override fun compareTo(other: BandWidth) = width - other.width
+ override fun toString() = name
}
private lateinit var dialogView: DialogWifiApBinding
private lateinit var base: SoftApConfigurationCompat
+ private var pasted = 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())
+ 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(
- ssid = dialogView.ssid.text.toString(),
+ ssid = ssid,
passphrase = if (dialogView.password.length() != 0) dialogView.password.text.toString() else null).apply {
if (!arg.p2pMode) {
securityType = dialogView.security.selectedItemPosition
isHiddenSsid = dialogView.hiddenSsid.isChecked
}
- if (full) @TargetApi(28) {
+ if (full) {
isAutoShutdownEnabled = dialogView.autoShutdown.isChecked
shutdownTimeoutMillis = dialogView.timeout.text.let { text ->
if (text.isNullOrEmpty()) 0 else text.toString().toLong()
}
- if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) {
- 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
+ channels = generateChannels()
maxNumberOfClients = dialogView.maxClient.text.let { text ->
if (text.isNullOrEmpty()) 0 else text.toString().toInt()
}
isClientControlByUserEnabled = dialogView.clientUserControl.isChecked
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)
- .filter { it.isNotEmpty() }.map { MacAddressCompat.fromString(it).toPlatform() }
- setMacRandomizationEnabled(dialogView.macRandomization.isChecked)
+ .filter { it.isNotEmpty() }.map(MacAddress::fromString)
+ 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
isIeee80211axEnabled = dialogView.ieee80211ax.isChecked
+ isIeee80211beEnabled = dialogView.ieee80211be.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(com.google.android.material.R.id.text_input_end_icon).apply {
+ tooltipText = contentDescription
+ }
+ }
if (!arg.readOnly) dialogView.ssid.addTextChangedListener(this@WifiApDialogFragment)
if (arg.p2pMode) dialogView.securityWrapper.isGone = true else dialogView.security.apply {
adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0,
@@ -157,99 +232,115 @@ class WifiApDialogFragment : AlertDialogFragment?) = error("Must select something")
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.p2pMode && Build.VERSION.SDK_INT < 28) dialogView.autoShutdown.isGone = true
if (arg.p2pMode || Build.VERSION.SDK_INT >= 30) {
dialogView.timeoutWrapper.helperText = getString(R.string.wifi_hotspot_timeout_default,
TetherTimeoutMonitor.defaultTimeout)
- dialogView.timeout.addTextChangedListener(this@WifiApDialogFragment)
+ if (!arg.readOnly) dialogView.timeout.addTextChangedListener(this@WifiApDialogFragment)
} else dialogView.timeoutWrapper.isGone = true
fun Spinner.configure(options: List) {
adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0, options).apply {
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.band2G.configure(channels2G)
- dialogView.band5G.configure(currentChannels5G)
- } else {
- dialogView.bandWrapper2G.isGone = true
- dialogView.bandWrapper5G.isGone = true
- }
- 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.bandPrimary.configure(currentChannels)
+ if (Build.VERSION.SDK_INT >= 31 && !arg.p2pMode) {
+ dialogView.bandSecondary.configure(listOf(ChannelOption.Disabled) + currentChannels)
+ } else dialogView.bandSecondary.isGone = true
+ if (arg.p2pMode || Build.VERSION.SDK_INT < 30) dialogView.accessControlGroup.isGone = true
+ else if (!arg.readOnly) {
dialogView.maxClient.addTextChangedListener(this@WifiApDialogFragment)
dialogView.blockedList.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
- else if (arg.p2pMode || !BuildCompat.isAtLeastS()) dialogView.macRandomization.isGone = true
- if (arg.p2pMode || !BuildCompat.isAtLeastS()) {
- dialogView.bridgedMode.isGone = true
- dialogView.bridgedModeOpportunisticShutdown.isGone = true
+ else if (arg.p2pMode || Build.VERSION.SDK_INT < 31) dialogView.macRandomizationWrapper.isGone = true
+ else dialogView.macRandomization.onItemSelectedListener = this@WifiApDialogFragment
+ if (arg.p2pMode || Build.VERSION.SDK_INT < 31) {
dialogView.ieee80211ax.isGone = true
+ dialogView.bridgedModeOpportunisticShutdown.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
populateFromConfiguration()
}
- private fun locate(band: Int, channels: List): Int {
- val channel = base.getChannel(band)
- val selection = channels.indexOfFirst { it.channel == channel }
+ private fun locate(i: Int): Int {
+ val band = base.channels.keyAt(i)
+ val channel = base.channels.valueAt(i)
+ val selection = currentChannels.indexOfFirst { it.band == band && it.channel == channel }
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
} 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() {
- 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)
dialogView.password.setText(base.passphrase)
dialogView.autoShutdown.isChecked = base.isAutoShutdownEnabled
- dialogView.timeout.setText(base.shutdownTimeoutMillis.let { if (it == 0L) "" else it.toString() })
- if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) {
- dialogView.band2G.setSelection(locate(SoftApConfigurationCompat.BAND_2GHZ, channels2G))
- dialogView.band5G.setSelection(locate(SoftApConfigurationCompat.BAND_5GHZ, currentChannels5G))
- 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.timeout.setText(base.shutdownTimeoutMillis.let { if (it <= 0) "" else it.toString() })
+ dialogView.bandPrimary.setSelection(locate(0))
+ if (Build.VERSION.SDK_INT >= 31 && !arg.p2pMode) {
+ dialogView.bandSecondary.setSelection(if (base.channels.size() > 1) locate(1) + 1 else 0)
}
dialogView.bssid.setText(base.bssid?.toString())
dialogView.hiddenSsid.isChecked = base.isHiddenSsid
@@ -257,11 +348,22 @@ class WifiApDialogFragment : AlertDialogFragment= 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() {
@@ -270,64 +372,62 @@ class WifiApDialogFragment : AlertDialogFragment true
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 " "
val timeoutError = dialogView.timeout.text.let { text ->
if (text.isNullOrEmpty()) null else try {
- text.toString().toLong()
+ SoftApConfigurationCompat.testPlatformTimeoutValidity(text.toString().toLong())
null
- } catch (e: NumberFormatException) {
+ } catch (e: Exception) {
e.readableMessage
}
}
dialogView.timeoutWrapper.error = timeoutError
- val isBandValid = when {
- arg.p2pMode || Build.VERSION.SDK_INT in 23 until 30 -> {
- val option5G = dialogView.band5G.selectedItem
- when (dialogView.band2G.selectedItem) {
- is ChannelOption.Disabled -> option5G !is ChannelOption.Disabled &&
- (!arg.p2pMode || RepeaterService.safeMode || option5G !is ChannelOption.Auto)
- 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
- }
+ val bandError = if (!arg.p2pMode && Build.VERSION.SDK_INT >= 30) {
+ try {
+ SoftApConfigurationCompat.testPlatformValidity(generateChannels())
+ null
+ } catch (e: Exception) {
+ e.readableMessage
}
- Build.VERSION.SDK_INT == 30 && !BuildCompat.isAtLeastS() -> {
- var expected = 1
- var set = 0
- for (s in arrayOf(dialogView.band2G, dialogView.band5G, dialogView.band6G)) when (s.selectedItem) {
- is ChannelOption.Auto -> expected = 0
- !is ChannelOption.Disabled -> ++set
- }
- set == expected
- }
- else -> {
- setBridgedMode()
- true
- }
- }
+ } else null
+ dialogView.bandError.isGone = bandError.isNullOrEmpty()
+ dialogView.bandError.text = bandError
+ val hideBssid = !arg.p2pMode && Build.VERSION.SDK_INT >= 31 &&
+ dialogView.macRandomization.selectedItemPosition != SoftApConfigurationCompat.RANDOMIZATION_NONE
+ dialogView.bssidWrapper.isGone = hideBssid
dialogView.bssidWrapper.error = null
- val bssidValid = dialogView.bssid.length() == 0 || try {
- MacAddressCompat.fromString(dialogView.bssid.text.toString())
+ val bssidValid = hideBssid || dialogView.bssid.length() == 0 || try {
+ val mac = MacAddress.fromString(dialogView.bssid.text.toString())
+ if (Build.VERSION.SDK_INT >= 30 && !arg.p2pMode) SoftApConfigurationCompat.testPlatformValidity(mac)
true
- } catch (e: IllegalArgumentException) {
+ } catch (e: Exception) {
dialogView.bssidWrapper.error = e.readableMessage
false
}
@@ -340,26 +440,80 @@ class WifiApDialogFragment : AlertDialogFragment= 30) {
+ val (blockedList, blockedListError) = try {
+ (dialogView.blockedList.text ?: "").split(nonMacChars).filter { it.isNotEmpty() }
+ .map(MacAddress::fromString).toSet() to null
+ } catch (e: IllegalArgumentException) {
+ 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
- val allowedListError = try {
- (dialogView.allowedList.text ?: "").split(nonMacChars)
- .filter { it.isNotEmpty() }.forEach { MacAddressCompat.fromString(it).toPlatform() }
- null
+ dialogView.bridgedTimeoutWrapper.error = bridgedTimeoutError
+ val vendorElementsError = if (Build.VERSION.SDK_INT >= 33) {
+ try {
+ VendorElements.deserialize(dialogView.vendorElements.text).also {
+ 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) {
- e.readableMessage
+ dialogView.persistentRandomizedMacWrapper.error = e.readableMessage
+ false
}
- dialogView.allowedListWrapper.error = allowedListError
- val canCopy = timeoutError == null && bssidValid && maxClientError == null && blockedListError == null &&
- allowedListError == null
+ val acsNoError = if (!arg.p2pMode && Build.VERSION.SDK_INT >= 33) acsList.all { (band, text, wrapper) ->
+ try {
+ 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 =
- ssidLength in 1..32 && passwordValid && isBandValid && canCopy
+ ssidOk && passwordValid && bandError == null && canCopy
dialogView.toolbar.menu.findItem(android.R.id.copy).isEnabled = canCopy
}
@@ -372,10 +526,15 @@ class WifiApDialogFragment : AlertDialogFragment {
+ android.R.id.copy -> try {
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
+ } catch (e: RuntimeException) {
+ Toast.makeText(context, e.readableMessage, Toast.LENGTH_LONG).show()
+ false
}
android.R.id.paste -> try {
app.clipboard.primaryClip?.getItemAt(0)?.text?.apply {
@@ -385,12 +544,13 @@ class WifiApDialogFragment : AlertDialogFragment {
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt
index 847ba9f6..f65c94b4 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt
@@ -10,13 +10,8 @@ import android.os.Build
import android.os.Handler
import android.os.Parcelable
import androidx.annotation.RequiresApi
-import androidx.core.os.BuildCompat
import be.mygod.vpnhotspot.App.Companion.app
-import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
-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 be.mygod.vpnhotspot.util.*
import timber.log.Timber
import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
@@ -39,17 +34,22 @@ object WifiApManager {
PackageManager.MATCH_SYSTEM_ONLY).single()
private const val CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED = "config_wifi_p2p_mac_randomization_supported"
- val p2pMacRandomizationSupported get() = when (Build.VERSION.SDK_INT) {
- 29 -> Resources.getSystem().run {
- getBoolean(getIdentifier(CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED, "bool", "android"))
+ val p2pMacRandomizationSupported get() = try {
+ when (Build.VERSION.SDK_INT) {
+ 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) {
- 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
+ } catch (e: RuntimeException) {
+ Timber.w(e)
+ false
}
@get:RequiresApi(30)
@@ -59,6 +59,92 @@ object WifiApManager {
@get:RequiresApi(30)
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") }
@Suppress("DEPRECATION")
private val setWifiApConfiguration by lazy {
@@ -72,29 +158,29 @@ object WifiApManager {
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-.
*/
- val configurationCompat get() = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
- (getWifiApConfiguration(Services.wifi) as android.net.wifi.WifiConfiguration?)?.toCompat()
- ?: SoftApConfigurationCompat()
- } else configuration.toCompat()
- fun setConfigurationCompat(value: SoftApConfigurationCompat) = (if (Build.VERSION.SDK_INT >= 30) {
- setSoftApConfiguration(Services.wifi, value.toPlatform())
- } else @Suppress("DEPRECATION") {
- setWifiApConfiguration(Services.wifi, value.toWifiConfiguration())
- }) as Boolean
+ @Deprecated("Use configuration instead", ReplaceWith("configuration"))
+ @Suppress("DEPRECATION")
+ val configurationLegacy get() = getWifiApConfiguration(Services.wifi) as android.net.wifi.WifiConfiguration?
+ /**
+ * Requires NETWORK_SETTINGS permission (or root).
+ */
+ @get:RequiresApi(30)
+ val configuration get() = getSoftApConfiguration(Services.wifi) as SoftApConfiguration
+ @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 {
/**
* Called when soft AP state changes.
*
- * @param state the new AP state. One of {@link #WIFI_AP_STATE_DISABLED},
- * {@link #WIFI_AP_STATE_DISABLING}, {@link #WIFI_AP_STATE_ENABLED},
- * {@link #WIFI_AP_STATE_ENABLING}, {@link #WIFI_AP_STATE_FAILED}
+ * @param state the new AP state. One of [WIFI_AP_STATE_DISABLED], [WIFI_AP_STATE_DISABLING],
+ * [WIFI_AP_STATE_ENABLED], [WIFI_AP_STATE_ENABLING], [WIFI_AP_STATE_FAILED]
* @param failureReason reason when in failed state. One of
* {@link #SAP_START_FAILURE_GENERAL},
* {@link #SAP_START_FAILURE_NO_CHANNEL},
@@ -150,7 +236,6 @@ object WifiApManager {
@RequiresApi(30)
fun onBlockedClientConnecting(client: Parcelable, blockedReason: Int) { }
}
- @RequiresApi(28)
val failureReasonLookup = ConstantLookup("SAP_START_FAILURE_", "GENERAL", "NO_CHANNEL")
@get:RequiresApi(30)
val clientBlockLookup by lazy { ConstantLookup("SAP_CLIENT_") }
@@ -166,7 +251,6 @@ object WifiApManager {
WifiManager::class.java.getDeclaredMethod("unregisterSoftApCallback", interfaceSoftApCallback)
}
- @RequiresApi(28)
fun registerSoftApCallback(callback: SoftApCallbackCompat, executor: Executor): Any {
val proxy = Proxy.newProxyInstance(interfaceSoftApCallback.classLoader,
arrayOf(interfaceSoftApCallback), object : InvocationHandler {
@@ -177,55 +261,36 @@ object WifiApManager {
} else invokeActual(proxy, method, args)
private fun invokeActual(proxy: Any, method: Method, args: Array?): Any? {
- val noArgs = args?.size ?: 0
- return when (val name = method.name) {
- "onStateChanged" -> {
- if (noArgs != 2) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
+ return when {
+ method.matches("onStateChanged", Integer.TYPE, Integer.TYPE) -> {
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 (noArgs != 1) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
callback.onNumClientsChanged(args!![0] as Int)
}
- "onConnectedClientsChanged" -> @TargetApi(30) {
+ method.matches1>("onConnectedClientsChanged") -> @TargetApi(30) {
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onConnectedClientsChanged"))
@Suppress("UNCHECKED_CAST")
- when (noArgs) {
- 1 -> callback.onConnectedClientsChanged(args!![0] as List)
- 2 -> null // we use the old method which returns all clients in one call
- else -> {
- Timber.w("Unexpected args for $name: ${args?.contentToString()}")
- null
- }
- }
+ callback.onConnectedClientsChanged(args!![0] as List)
}
- "onInfoChanged" -> @TargetApi(30) {
- if (noArgs != 1) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
+ method.matches1>("onInfoChanged") -> @TargetApi(31) {
+ if (Build.VERSION.SDK_INT < 31) Timber.w(Exception("Unexpected onInfoChanged API 31+"))
+ @Suppress("UNCHECKED_CAST")
+ callback.onInfoChanged(args!![0] as List)
+ }
+ 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]
- if (arg is List<*>) {
- if (!BuildCompat.isAtLeastS()) Timber.w(Exception("Unexpected onInfoChanged API 31+"))
- @Suppress("UNCHECKED_CAST")
- callback.onInfoChanged(arg as List)
- } 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))
- }
+ val info = SoftApInfo(arg as Parcelable)
+ callback.onInfoChanged(if (info.frequency == 0 && info.bandwidth ==
+ SoftApConfigurationCompat.CHANNEL_WIDTH_INVALID) emptyList() else listOf(arg))
}
- "onCapabilityChanged" -> @TargetApi(30) {
- if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onCapabilityChanged"))
- if (noArgs != 1) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
+ Build.VERSION.SDK_INT >= 30 && method.matches("onCapabilityChanged", SoftApCapability.clazz) -> {
callback.onCapabilityChanged(args!![0] as Parcelable)
}
- "onBlockedClientConnecting" -> @TargetApi(30) {
- if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onBlockedClientConnecting"))
- if (noArgs != 2) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
+ Build.VERSION.SDK_INT >= 30 && method.matches("onBlockedClientConnecting", WifiClient.clazz,
+ Int::class.java) -> {
callback.onBlockedClientConnecting(args!![0] as Parcelable, args[1] as Int)
}
else -> callSuper(interfaceSoftApCallback, proxy, method, args)
@@ -237,7 +302,6 @@ object WifiApManager {
} else registerSoftApCallback(Services.wifi, proxy, null)
return proxy
}
- @RequiresApi(28)
fun unregisterSoftApCallback(key: Any) = unregisterSoftApCallback(Services.wifi, key)
@get:RequiresApi(30)
@@ -253,43 +317,9 @@ object WifiApManager {
private val cancelLocalOnlyHotspotRequest by lazy {
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)
-
- @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
- }
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiClient.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiClient.kt
index 57edc1a3..b106db9d 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiClient.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiClient.kt
@@ -11,7 +11,7 @@ import timber.log.Timber
@RequiresApi(30)
value class WifiClient(val inner: Parcelable) {
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") }
@get:RequiresApi(31)
private val getApInstanceIdentifier by lazy @TargetApi(31) { UnblockCentral.getApInstanceIdentifier(clazz) }
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiP2pManagerHelper.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiP2pManagerHelper.kt
index e43dce63..4baa0ad5 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiP2pManagerHelper.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiP2pManagerHelper.kt
@@ -1,15 +1,18 @@
package be.mygod.vpnhotspot.net.wifi
import android.annotation.SuppressLint
+import android.net.MacAddress
+import android.net.wifi.ScanResult
import android.net.wifi.WpsInfo
import android.net.wifi.p2p.WifiP2pGroup
+import android.net.wifi.p2p.WifiP2pInfo
import android.net.wifi.p2p.WifiP2pManager
import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.util.callSuper
+import be.mygod.vpnhotspot.util.matches
import kotlinx.coroutines.CompletableDeferred
-import timber.log.Timber
import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy
@@ -53,6 +56,15 @@ object WifiP2pManagerHelper {
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): Int? {
+ val result = ResultListener()
+ setVendorElements(c, ve, result)
+ return result.future.await()
+ }
+
/**
* Available since Android 4.3.
*
@@ -98,9 +110,8 @@ object WifiP2pManagerHelper {
private val interfacePersistentGroupInfoListener by lazy {
Class.forName("android.net.wifi.p2p.WifiP2pManager\$PersistentGroupInfoListener")
}
- private val getGroupList by lazy {
- Class.forName("android.net.wifi.p2p.WifiP2pGroupList").getDeclaredMethod("getGroupList")
- }
+ private val classWifiP2pGroupList by lazy { Class.forName("android.net.wifi.p2p.WifiP2pGroupList") }
+ private val getGroupList by lazy { classWifiP2pGroupList.getDeclaredMethod("getGroupList") }
private val requestPersistentGroupInfo by lazy {
WifiP2pManager::class.java.getDeclaredMethod("requestPersistentGroupInfo",
WifiP2pManager.Channel::class.java, interfacePersistentGroupInfoListener)
@@ -116,9 +127,8 @@ object WifiP2pManagerHelper {
val result = CompletableDeferred>()
requestPersistentGroupInfo(this, c, Proxy.newProxyInstance(interfacePersistentGroupInfoListener.classLoader,
arrayOf(interfacePersistentGroupInfoListener), object : InvocationHandler {
- override fun invoke(proxy: Any, method: Method, args: Array?): Any? = when (method.name) {
- "onPersistentGroupInfoAvailable" -> {
- if (args?.size != 1) Timber.w(IllegalArgumentException("Unexpected args: $args"))
+ override fun invoke(proxy: Any, method: Method, args: Array?): Any? = when {
+ method.matches("onPersistentGroupInfoAvailable", classWifiP2pGroupList) -> {
@Suppress("UNCHECKED_CAST")
result.complete(getGroupList(args!![0]) as Collection)
}
@@ -128,14 +138,22 @@ object WifiP2pManagerHelper {
return result.await()
}
- @SuppressLint("MissingPermission")
+ suspend fun WifiP2pManager.requestConnectionInfo(c: WifiP2pManager.Channel) =
+ CompletableDeferred().apply { requestConnectionInfo(c) { complete(it) } }.await()
+ @SuppressLint("MissingPermission") // missing permission simply leads to null result
@RequiresApi(29)
- suspend fun WifiP2pManager.requestDeviceAddress(c: WifiP2pManager.Channel): MacAddressCompat? {
+ suspend fun WifiP2pManager.requestDeviceAddress(c: WifiP2pManager.Channel): MacAddress? {
val future = CompletableDeferred()
requestDeviceInfo(c) { future.complete(it?.deviceAddress) }
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
}
}
+ @SuppressLint("MissingPermission") // missing permission simply leads to null result
+ suspend fun WifiP2pManager.requestGroupInfo(c: WifiP2pManager.Channel) =
+ CompletableDeferred().apply { requestGroupInfo(c) { complete(it) } }.await()
+ @RequiresApi(29)
+ suspend fun WifiP2pManager.requestP2pState(c: WifiP2pManager.Channel) =
+ CompletableDeferred().apply { requestP2pState(c) { complete(it) } }.await()
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiSsidCompat.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiSsidCompat.kt
new file mode 100644
index 00000000..060242ba
--- /dev/null
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiSsidCompat.kt
@@ -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()
+}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/preference/AlwaysAutoCompleteEditTextPreferenceDialogFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/preference/AlwaysAutoCompleteEditTextPreferenceDialogFragment.kt
deleted file mode 100644
index c1fce475..00000000
--- a/mobile/src/main/java/be/mygod/vpnhotspot/preference/AlwaysAutoCompleteEditTextPreferenceDialogFragment.kt
+++ /dev/null
@@ -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) {
- 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(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
- }
-}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/preference/AutoCompleteNetworkPreferenceDialogFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/preference/AutoCompleteNetworkPreferenceDialogFragment.kt
new file mode 100644
index 00000000..33df6097
--- /dev/null
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/preference/AutoCompleteNetworkPreferenceDialogFragment.kt
@@ -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
+ private fun updateAdapter() {
+ adapter.clear()
+ adapter.addAll(interfaceNames.flatMap { it.value })
+ }
+
+ private val interfaceNames = mutableMapOf>()
+ 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(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()
+ }
+}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/preference/UpstreamsPreference.kt b/mobile/src/main/java/be/mygod/vpnhotspot/preference/UpstreamsPreference.kt
index bc6e7fec..b486674a 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/preference/UpstreamsPreference.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/preference/UpstreamsPreference.kt
@@ -8,7 +8,6 @@ import android.text.style.StyleSpan
import android.util.AttributeSet
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor
@@ -31,7 +30,7 @@ class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(co
if (internet) SpannableStringBuilder(ifname).apply {
setSpan(StyleSpan(Typeface.BOLD), 0, length, 0)
} else ifname
- }.joinTo(SpannableStringBuilder()).let { if (it.isEmpty()) "∅" else it }
+ }.joinTo(SpannableStringBuilder()).ifEmpty { "∅" }
override fun onAvailable(properties: LinkProperties?) {
val result = mutableMapOf()
@@ -51,15 +50,11 @@ class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(co
}
private val primary = Monitor()
- private val fallback: Monitor = object : Monitor() {
- override fun onFallback() {
- currentInterfaces = mapOf("" to true)
- onUpdate()
- }
- }
+ private val fallback = Monitor()
init {
(context as LifecycleOwner).lifecycle.addObserver(this)
+ onUpdate()
}
override fun onStart(owner: LifecycleOwner) {
@@ -71,8 +66,8 @@ class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(co
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(
- context.resources.configuration.locale, primary.charSequence, fallback.charSequence)
+ context.resources.configuration.locales[0], primary.charSequence, fallback.charSequence)
}
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/room/ClientRecord.kt b/mobile/src/main/java/be/mygod/vpnhotspot/room/ClientRecord.kt
index 64dd9750..1fec6f22 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/room/ClientRecord.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/room/ClientRecord.kt
@@ -1,37 +1,38 @@
package be.mygod.vpnhotspot.room
+import android.net.MacAddress
import androidx.lifecycle.LiveData
import androidx.lifecycle.map
import androidx.room.*
-import be.mygod.vpnhotspot.net.MacAddressCompat
+import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toLong
@Entity
data class ClientRecord(@PrimaryKey
- val mac: Long,
+ val mac: MacAddress,
var nickname: CharSequence = "",
var blocked: Boolean = false,
var macLookupPending: Boolean = true) {
@androidx.room.Dao
abstract class Dao {
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
- protected abstract fun lookupBlocking(mac: Long): ClientRecord?
- fun lookupOrDefaultBlocking(mac: MacAddressCompat) = lookupBlocking(mac.addr) ?: ClientRecord(mac.addr)
+ protected abstract fun lookupBlocking(mac: MacAddress): ClientRecord?
+ fun lookupOrDefaultBlocking(mac: MacAddress) = lookupBlocking(mac) ?: ClientRecord(mac)
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
- protected abstract suspend fun lookup(mac: Long): ClientRecord?
- suspend fun lookupOrDefault(mac: Long) = lookup(mac) ?: ClientRecord(mac)
+ protected abstract suspend fun lookup(mac: MacAddress): ClientRecord?
+ suspend fun lookupOrDefault(mac: MacAddress) = lookup(mac) ?: ClientRecord(mac)
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
- protected abstract fun lookupSync(mac: Long): LiveData
- fun lookupOrDefaultSync(mac: MacAddressCompat) = lookupSync(mac.addr).map { it ?: ClientRecord(mac.addr) }
+ protected abstract fun lookupSync(mac: MacAddress): LiveData
+ fun lookupOrDefaultSync(mac: MacAddress) = lookupSync(mac).map { it ?: ClientRecord(mac) }
@Insert(onConflict = OnConflictStrategy.REPLACE)
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
- open suspend fun upsert(mac: MacAddressCompat, operation: suspend ClientRecord.() -> Unit) = lookupOrDefault(
- mac.addr).apply {
+ open suspend fun upsert(mac: MacAddress, operation: suspend ClientRecord.() -> Unit) = lookupOrDefault(
+ mac).apply {
operation()
update(this)
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/room/Converters.kt b/mobile/src/main/java/be/mygod/vpnhotspot/room/Converters.kt
index 56c612f1..ebfcf246 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/room/Converters.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/room/Converters.kt
@@ -1,8 +1,11 @@
package be.mygod.vpnhotspot.room
+import android.net.MacAddress
import android.text.TextUtils
import androidx.room.TypeConverter
import be.mygod.librootkotlinx.useParcel
+import be.mygod.vpnhotspot.net.MacAddressCompat
+import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toLong
import timber.log.Timber
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
@TypeConverter
fun persistInetAddress(address: InetAddress): ByteArray = address.address
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/room/TrafficRecord.kt b/mobile/src/main/java/be/mygod/vpnhotspot/room/TrafficRecord.kt
index 2a4dfac0..d0b6e315 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/room/TrafficRecord.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/room/TrafficRecord.kt
@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot.room
+import android.net.MacAddress
import android.os.Parcelable
import androidx.room.*
import kotlinx.parcelize.Parcelize
@@ -22,7 +23,7 @@ data class TrafficRecord(
/**
* 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.
*/
@@ -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 */
WHERE TrafficRecord.mac = :mac AND Next.id IS NULL
""")
- abstract suspend fun queryStats(mac: Long): ClientStats
+ abstract suspend fun queryStats(mac: MacAddress): ClientStats
}
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/root/MiscCommands.kt b/mobile/src/main/java/be/mygod/vpnhotspot/root/MiscCommands.kt
index 7f588c79..ea8a17d4 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/root/MiscCommands.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/root/MiscCommands.kt
@@ -1,7 +1,6 @@
package be.mygod.vpnhotspot.root
import android.content.Context
-import android.os.Build
import android.os.Parcelable
import android.os.RemoteException
import android.provider.Settings
@@ -30,18 +29,10 @@ fun ProcessBuilder.fixPath(redirect: Boolean = false) = apply {
@Parcelize
data class Dump(val path: String, val cacheDir: File = app.deviceStorage.codeCacheDir) : RootCommandNoResult {
- @Suppress("BlockingMethodInNonBlockingContext")
override suspend fun execute() = withContext(Dispatchers.IO) {
FileOutputStream(path, true).use { out ->
val process = ProcessBuilder("sh").fixPath(true).start()
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("""
|echo 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
|echo
|echo iptables -t filter
- |$iptablesSave -t filter
+ |iptables-save -t filter
|echo
|echo iptables -t nat
- |$iptablesSave -t nat
+ |iptables-save -t nat
|echo
|echo ip6tables-save
- |$ip6tablesSave
+ |ip6tables-save
|echo
|echo ip rule
|$IP rule
@@ -125,7 +116,7 @@ class ProcessListener(private val terminateRegex: Regex,
parent.join()
} finally {
parent.cancel()
- if (Build.VERSION.SDK_INT < 26) process.destroy() else if (process.isAlive) process.destroyForcibly()
+ if (process.isAlive) process.destroyForcibly()
parent.join()
}
}
@@ -162,7 +153,6 @@ data class StartTethering(private val type: Int,
@Deprecated("Old API since API 30")
@Parcelize
-@RequiresApi(24)
@Suppress("DEPRECATION")
data class StartTetheringLegacy(private val cacheDir: File, private val type: Int,
private val showProvisioningUi: Boolean) : RootCommand {
@@ -184,7 +174,6 @@ data class StartTetheringLegacy(private val cacheDir: File, private val type: In
}
@Parcelize
-@RequiresApi(24)
data class StopTethering(private val type: Int) : RootCommandNoResult {
override suspend fun execute(): Parcelable? {
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) {
val process = ProcessBuilder("settings", "put", "global", name, value).fixPath(true).start()
val error = process.inputStream.bufferedReader().readText()
- check(process.waitFor() == 0)
- if (error.isNotEmpty()) throw RemoteException(error)
+ val exit = process.waitFor()
+ if (exit != 0 || error.isNotEmpty()) throw RemoteException("Process exited with $exit: $error")
null
}
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/root/RepeaterCommands.kt b/mobile/src/main/java/be/mygod/vpnhotspot/root/RepeaterCommands.kt
index 0e84a5eb..1ead2991 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/root/RepeaterCommands.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/root/RepeaterCommands.kt
@@ -1,5 +1,7 @@
package be.mygod.vpnhotspot.root
+import android.net.MacAddress
+import android.net.wifi.ScanResult
import android.net.wifi.p2p.WifiP2pManager
import android.os.Looper
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.requestDeviceAddress
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.util.Services
import kotlinx.parcelize.Parcelize
@@ -35,10 +38,8 @@ object RepeaterCommands {
@Parcelize
@RequiresApi(29)
- class RequestDeviceAddress : RootCommand {
- override suspend fun execute() = Services.p2p!!.run {
- requestDeviceAddress(obtainChannel())?.let { ParcelableLong(it.addr) }
- }
+ class RequestDeviceAddress : RootCommand {
+ override suspend fun execute() = Services.p2p!!.run { requestDeviceAddress(obtainChannel()) }
}
@Parcelize
@@ -55,6 +56,14 @@ object RepeaterCommands {
}
}
+ @Parcelize
+ @RequiresApi(33)
+ data class SetVendorElements(private val ve: List) : RootCommand {
+ override suspend fun execute() = Services.p2p!!.run {
+ setVendorElements(obtainChannel(), ve)?.let { ParcelableInt(it) }
+ }
+ }
+
@Parcelize
data class WriteP2pConfig(val data: String, val legacy: Boolean) : RootCommandNoResult {
override suspend fun execute(): Parcelable? {
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/root/RootManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/root/RootManager.kt
index a9bebf3b..b4f3baa1 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/root/RootManager.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/root/RootManager.kt
@@ -6,6 +6,8 @@ import android.util.Log
import be.mygod.librootkotlinx.*
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.util.Services
+import be.mygod.vpnhotspot.util.UnblockCentral
+import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.parcelize.Parcelize
import timber.log.Timber
@@ -31,6 +33,7 @@ object RootManager : RootSession(), Logger {
})
Logger.me = RootManager
Services.init { systemContext }
+ UnblockCentral.needInit = false
return null
}
}
@@ -42,7 +45,10 @@ object RootManager : RootSession(), Logger {
override suspend fun initServer(server: RootServer) {
Logger.me = this
- server.init(app.deviceStorage)
+ AppProcess.shouldRelocateHeuristics.let {
+ FirebaseCrashlytics.getInstance().setCustomKey("RootManager.relocateEnabled", it)
+ server.init(app.deviceStorage, it)
+ }
server.execute(RootInit())
}
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/root/RoutingCommands.kt b/mobile/src/main/java/be/mygod/vpnhotspot/root/RoutingCommands.kt
index f43fbad9..87b57535 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/root/RoutingCommands.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/root/RoutingCommands.kt
@@ -2,7 +2,7 @@ package be.mygod.vpnhotspot.root
import android.os.Parcelable
import be.mygod.librootkotlinx.RootCommand
-import be.mygod.librootkotlinx.RootCommandOneWay
+import be.mygod.librootkotlinx.RootCommandNoResult
import be.mygod.vpnhotspot.net.Routing
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
@@ -13,8 +13,7 @@ import timber.log.Timber
object RoutingCommands {
@Parcelize
- class Clean : RootCommandOneWay {
- @Suppress("BlockingMethodInNonBlockingContext")
+ class Clean : RootCommandNoResult {
override suspend fun execute() = withContext(Dispatchers.IO) {
val process = ProcessBuilder("sh").fixPath(true).start()
process.outputStream.bufferedWriter().use(Routing.Companion::appendCleanCommands)
@@ -23,6 +22,7 @@ object RoutingCommands {
else -> Timber.w("Unexpected exit code $code")
}
check(process.waitFor() == 0)
+ null
}
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/root/WifiApCommands.kt b/mobile/src/main/java/be/mygod/vpnhotspot/root/WifiApCommands.kt
index 0b6b6794..318ab08c 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/root/WifiApCommands.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/root/WifiApCommands.kt
@@ -1,13 +1,19 @@
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.os.Build
import android.os.Parcelable
import androidx.annotation.RequiresApi
import be.mygod.librootkotlinx.ParcelableBoolean
import be.mygod.librootkotlinx.RootCommand
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.WifiClient
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
@@ -15,7 +21,6 @@ import kotlinx.parcelize.Parcelize
import timber.log.Timber
object WifiApCommands {
- @RequiresApi(28)
sealed class SoftApCallbackParcel : Parcelable {
abstract fun dispatch(callback: WifiApManager.SoftApCallbackCompat)
@@ -55,7 +60,6 @@ object WifiApCommands {
}
@Parcelize
- @RequiresApi(28)
class RegisterSoftApCallback : RootCommandChannel {
override fun create(scope: CoroutineScope) = scope.produce(capacity = capacity) {
val finish = CompletableDeferred()
@@ -111,7 +115,6 @@ object WifiApCommands {
private val callbacks = mutableSetOf()
private val lastCallback = AutoFiringCallbacks()
private var rootCallbackJob: Job? = null
- @RequiresApi(28)
private suspend fun handleChannel(channel: ReceiveChannel) = channel.consumeEach { parcel ->
when (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.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)
}
- @RequiresApi(28)
fun registerSoftApCallback(callback: WifiApManager.SoftApCallbackCompat) = synchronized(callbacks) {
val wasEmpty = callbacks.isEmpty()
callbacks.add(callback)
@@ -141,7 +156,6 @@ object WifiApCommands {
null
} else lastCallback
}?.toSequence()?.forEach { it?.dispatch(callback) }
- @RequiresApi(28)
fun unregisterSoftApCallback(callback: WifiApManager.SoftApCallbackCompat) = synchronized(callbacks) {
if (callbacks.remove(callback) && callbacks.isEmpty()) {
rootCallbackJob!!.cancel()
@@ -150,13 +164,29 @@ object WifiApCommands {
}
@Parcelize
- class GetConfiguration : RootCommand {
- override suspend fun execute() = WifiApManager.configurationCompat
+ @Deprecated("Use GetConfiguration instead", ReplaceWith("GetConfiguration"))
+ @Suppress("DEPRECATION")
+ class GetConfigurationLegacy : RootCommand {
+ override suspend fun execute() = WifiApManager.configurationLegacy
+ }
+ @Parcelize
+ @RequiresApi(30)
+ class GetConfiguration : RootCommand {
+ override suspend fun execute() = WifiApManager.configuration
}
@Parcelize
- data class SetConfiguration(val configuration: SoftApConfigurationCompat) : RootCommand {
- override suspend fun execute() = ParcelableBoolean(WifiApManager.setConfigurationCompat(configuration))
+ @Deprecated("Use SetConfiguration instead", ReplaceWith("SetConfiguration"))
+ @Suppress("DEPRECATION")
+ data class SetConfigurationLegacy(
+ val configuration: android.net.wifi.WifiConfiguration?,
+ ) : RootCommand {
+ override suspend fun execute() = ParcelableBoolean(WifiApManager.setConfiguration(configuration))
+ }
+ @Parcelize
+ @RequiresApi(30)
+ data class SetConfiguration(val configuration: SoftApConfiguration) : RootCommand {
+ override suspend fun execute() = ParcelableBoolean(WifiApManager.setConfiguration(configuration))
}
@Parcelize
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/util/AppUpdate.kt b/mobile/src/main/java/be/mygod/vpnhotspot/util/AppUpdate.kt
new file mode 100644
index 00000000..aacb9fa8
--- /dev/null
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/util/AppUpdate.kt
@@ -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")
+}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/util/ConstantLookup.kt b/mobile/src/main/java/be/mygod/vpnhotspot/util/ConstantLookup.kt
index 0e18da97..422a3ba0 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/util/ConstantLookup.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/util/ConstantLookup.kt
@@ -9,10 +9,10 @@ import timber.log.Timber
class ConstantLookup(private val prefix: String, private val lookup29: Array,
private val clazz: () -> Class<*>) {
- private val lookup by lazy {
+ val lookup by lazy {
SparseArrayCompat().apply {
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) {
Timber.w(e)
}
@@ -30,17 +30,15 @@ class ConstantLookup(private val prefix: String, private val lookup29: Array Class<*>) =
ConstantLookup(prefix, lookup29, clazz)
-@Suppress("FunctionName")
inline fun ConstantLookup(prefix: String, vararg lookup29: String?) =
ConstantLookup(prefix, lookup29) { T::class.java }
class LongConstantLookup(private val clazz: Class<*>, private val prefix: String) {
private val lookup = LongSparseArray().apply {
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) {
Timber.w(e)
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/util/DeviceStorageApp.kt b/mobile/src/main/java/be/mygod/vpnhotspot/util/DeviceStorageApp.kt
index 3df4e7f8..2309fba4 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/util/DeviceStorageApp.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/util/DeviceStorageApp.kt
@@ -1,12 +1,10 @@
package be.mygod.vpnhotspot.util
import android.annotation.SuppressLint
-import android.annotation.TargetApi
import android.app.Application
import android.content.Context
@SuppressLint("Registered")
-@TargetApi(24)
class DeviceStorageApp(context: Context) : Application() {
init {
attachBaseContext(context.createDeviceProtectedStorageContext())
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/util/KillableTileService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/util/KillableTileService.kt
index 78224b21..3ab14bf0 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/util/KillableTileService.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/util/KillableTileService.kt
@@ -1,14 +1,15 @@
package be.mygod.vpnhotspot.util
import android.content.ComponentName
+import android.content.Intent
import android.content.ServiceConnection
import android.os.Build
+import android.os.DeadObjectException
import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
-import androidx.annotation.RequiresApi
+import be.mygod.vpnhotspot.BootReceiver
-@RequiresApi(24)
abstract class KillableTileService : TileService(), ServiceConnection {
protected var tapPending = false
@@ -25,4 +26,10 @@ abstract class KillableTileService : TileService(), ServiceConnection {
onClick()
}
}
+
+ override fun onBind(intent: Intent?) = try {
+ super.onBind(intent)
+ } catch (_: DeadObjectException) {
+ null
+ }.also { BootReceiver.startIfEnabled() }
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/util/RangeInput.kt b/mobile/src/main/java/be/mygod/vpnhotspot/util/RangeInput.kt
new file mode 100644
index 00000000..d14ae02d
--- /dev/null
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/util/RangeInput.kt
@@ -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?) = input?.run { toString(toIntArray()) }
+
+ fun fromString(input: CharSequence?, min: Int = 1, max: Int = 999) = mutableSetOf().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])
+ }
+ }
+}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/util/RootSession.kt b/mobile/src/main/java/be/mygod/vpnhotspot/util/RootSession.kt
index 7b38a814..8cad8b51 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/util/RootSession.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/util/RootSession.kt
@@ -28,8 +28,8 @@ class RootSession : AutoCloseable {
private var server: RootServer? = runBlocking { RootManager.acquire() }
override fun close() {
- server = null
server?.let { runBlocking { RootManager.release(it) } }
+ server = null
}
/**
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/util/Services.kt b/mobile/src/main/java/be/mygod/vpnhotspot/util/Services.kt
index 4d17d675..38ae4328 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/util/Services.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/util/Services.kt
@@ -2,8 +2,11 @@ package be.mygod.vpnhotspot.util
import android.content.Context
import android.net.ConnectivityManager
+import android.net.NetworkRequest
import android.net.wifi.WifiManager
import android.net.wifi.p2p.WifiP2pManager
+import android.os.Handler
+import android.os.Looper
import androidx.core.content.getSystemService
import timber.log.Timber
@@ -14,6 +17,7 @@ object Services {
contextInit = context
}
+ val mainHandler by lazy { Handler(Looper.getMainLooper()) }
val connectivity by lazy { context.getSystemService()!! }
val p2p by lazy {
try {
@@ -24,4 +28,7 @@ object Services {
}
}
val wifi by lazy { context.getSystemService()!! }
+
+ fun registerNetworkCallback(request: NetworkRequest, networkCallback: ConnectivityManager.NetworkCallback) =
+ connectivity.registerNetworkCallback(request, networkCallback, mainHandler)
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/util/UnblockCentral.kt b/mobile/src/main/java/be/mygod/vpnhotspot/util/UnblockCentral.kt
index 0a9a7daa..2a8827e6 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/util/UnblockCentral.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/util/UnblockCentral.kt
@@ -1,10 +1,12 @@
package be.mygod.vpnhotspot.util
import android.annotation.SuppressLint
+import android.net.MacAddress
import android.net.wifi.SoftApConfiguration
import android.net.wifi.p2p.WifiP2pConfig
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!
@@ -12,26 +14,19 @@ import timber.log.Timber
* Lazy cannot be used directly as it will create inner classes.
*/
@SuppressLint("BlockedPrivateApi", "DiscouragedPrivateApi")
-@Suppress("FunctionName")
object UnblockCentral {
+ var needInit = true
/**
* Retrieve this property before doing dangerous shit.
*/
- @get:RequiresApi(28)
- 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
- }
- }
+ private val init by lazy { if (needInit) check(Reflection.unseal(app.deviceStorage) == 0) }
- @RequiresApi(31)
- fun setUserConfiguration(clazz: Class<*>) = init.let {
- clazz.getDeclaredMethod("setUserConfiguration", Boolean::class.java)
+ @RequiresApi(33)
+ fun getCountryCode(clazz: Class<*>) = init.let { clazz.getDeclaredMethod("getCountryCode") }
+
+ @RequiresApi(33)
+ fun setRandomizedMacAddress(clazz: Class<*>) = init.let {
+ clazz.getDeclaredMethod("setRandomizedMacAddress", MacAddress::class.java)
}
@get:RequiresApi(31)
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/util/Utils.kt b/mobile/src/main/java/be/mygod/vpnhotspot/util/Utils.kt
index 7e6e3ab9..d859931d 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/util/Utils.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/util/Utils.kt
@@ -1,23 +1,16 @@
package be.mygod.vpnhotspot.util
import android.annotation.SuppressLint
-import android.annotation.TargetApi
import android.content.*
import android.content.res.Resources
-import android.net.InetAddresses
-import android.net.LinkProperties
-import android.net.RouteInfo
+import android.net.*
import android.os.Build
import android.os.RemoteException
-import android.system.ErrnoException
-import android.system.Os
-import android.system.OsConstants
import android.text.*
import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import androidx.annotation.DrawableRes
-import androidx.annotation.RequiresApi
import androidx.core.net.toUri
import androidx.core.view.isVisible
import androidx.databinding.BindingAdapter
@@ -26,18 +19,23 @@ import androidx.fragment.app.FragmentManager
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.MacAddressCompat
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 java.io.File
-import java.io.FileNotFoundException
-import java.io.IOException
import java.lang.invoke.MethodHandles
import java.lang.reflect.InvocationHandler
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
+import java.net.HttpURLConnection
import java.net.InetAddress
import java.net.NetworkInterface
import java.net.SocketException
-import java.util.*
+import java.net.URL
+import java.util.Locale
+import kotlin.coroutines.resumeWithException
tailrec fun Throwable.getRootCause(): Throwable {
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
}
+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 Method.matches1(name: String) = matches(name, T::class.java)
+
fun Context.ensureReceiverUnregistered(receiver: BroadcastReceiver) {
try {
unregisterReceiver(receiver)
@@ -143,12 +145,13 @@ fun makeIpSpan(ip: InetAddress) = ip.hostAddress.let {
}
}
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
fun NetworkInterface.formatAddresses(macOnly: Boolean = false) = SpannableStringBuilder().apply {
try {
- val address = hardwareAddress?.let(MacAddressCompat::fromBytes)
+ val address = hardwareAddress?.let(MacAddress::fromBytes)
if (address != null && address != MacAddressCompat.ANY_ADDRESS) appendLine(makeMacSpan(address.toString()))
} catch (e: IllegalArgumentException) {
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
}
-@get:RequiresApi(26)
-private val newLookup by lazy @TargetApi(26) {
+private val newLookup by lazy {
MethodHandles.Lookup::class.java.getDeclaredConstructor(Class::class.java, Int::class.java).apply {
isAccessible = true
}
@@ -213,10 +215,14 @@ private val newLookup by lazy @TargetApi(26) {
* See also: https://stackoverflow.com/a/49532463/2245107
*/
fun InvocationHandler.callSuper(interfaceClass: Class<*>, proxy: Any, method: Method, args: Array?) = when {
- Build.VERSION.SDK_INT >= 26 && method.isDefault -> newLookup.newInstance(interfaceClass, 0xf) // ALL_MODES
- .`in`(interfaceClass).unreflectSpecial(method, interfaceClass).bindTo(proxy).run {
- if (args == null) invokeWithArguments() else invokeWithArguments(*args)
- }
+ method.isDefault -> try {
+ newLookup.newInstance(interfaceClass, 0xf) // ALL_MODES
+ } 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
method.declaringClass.isAssignableFrom(javaClass) -> when {
method.declaringClass == Object::class.java -> when (method.name) {
@@ -234,13 +240,26 @@ fun InvocationHandler.callSuper(interfaceClass: Class<*>, proxy: Any, method: Me
}
}
-@Suppress("FunctionName")
-fun if_nametoindex(ifname: String) = if (Build.VERSION.SDK_INT >= 26) {
- Os.if_nametoindex(ifname)
-} else try {
- File("/sys/class/net/$ifname/ifindex").inputStream().bufferedReader().use { it.readLine().trim().toInt() }
-} catch (_: FileNotFoundException) {
- NetworkInterface.getByName(ifname)?.index ?: 0
-} catch (e: IOException) {
- if ((e.cause as? ErrnoException)?.errno == OsConstants.ENODEV) 0 else throw e
+fun globalNetworkRequestBuilder() = NetworkRequest.Builder().apply {
+ if (Build.VERSION.SDK_INT >= 31) setIncludeOtherUidNetworks(true)
+}
+
+suspend fun connectCancellable(url: String, block: suspend (HttpURLConnection) -> T): T {
+ @Suppress("BlockingMethodInNonBlockingContext")
+ val conn = URL(url).openConnection() as HttpURLConnection
+ return suspendCancellableCoroutine { cont ->
+ 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()
+ }
+ }
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/widget/AlwaysAutoCompleteEditText.kt b/mobile/src/main/java/be/mygod/vpnhotspot/widget/AlwaysAutoCompleteEditText.kt
index 1a189581..de3e7cef 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/widget/AlwaysAutoCompleteEditText.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/widget/AlwaysAutoCompleteEditText.kt
@@ -5,14 +5,15 @@ import android.graphics.Rect
import android.util.AttributeSet
import android.view.View
import androidx.appcompat.widget.AppCompatAutoCompleteTextView
-import be.mygod.vpnhotspot.R
/**
* Based on: https://gist.github.com/furycomptuers/4961368
*/
-class AlwaysAutoCompleteEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
- defStyleAttr: Int = R.attr.autoCompleteTextViewStyle) :
- AppCompatAutoCompleteTextView(context, attrs, defStyleAttr) {
+class AlwaysAutoCompleteEditText @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = androidx.appcompat.R.attr.autoCompleteTextViewStyle,
+) : AppCompatAutoCompleteTextView(context, attrs, defStyleAttr) {
override fun enoughToFilter() = true
override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
diff --git a/mobile/src/main/res/drawable/ic_action_update.xml b/mobile/src/main/res/drawable/ic_action_update.xml
new file mode 100644
index 00000000..3b923020
--- /dev/null
+++ b/mobile/src/main/res/drawable/ic_action_update.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/ic_av_closed_caption.xml b/mobile/src/main/res/drawable/ic_av_closed_caption.xml
new file mode 100644
index 00000000..359f268b
--- /dev/null
+++ b/mobile/src/main/res/drawable/ic_av_closed_caption.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/ic_av_closed_caption_off.xml b/mobile/src/main/res/drawable/ic_av_closed_caption_off.xml
new file mode 100644
index 00000000..a21d5963
--- /dev/null
+++ b/mobile/src/main/res/drawable/ic_av_closed_caption_off.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/ic_file_downloading.xml b/mobile/src/main/res/drawable/ic_file_downloading.xml
new file mode 100644
index 00000000..03774184
--- /dev/null
+++ b/mobile/src/main/res/drawable/ic_file_downloading.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/ic_launcher_monochrome.xml b/mobile/src/main/res/drawable/ic_launcher_monochrome.xml
new file mode 100644
index 00000000..27a05108
--- /dev/null
+++ b/mobile/src/main/res/drawable/ic_launcher_monochrome.xml
@@ -0,0 +1,5 @@
+
+
+
+
diff --git a/mobile/src/main/res/drawable/toggle_hex.xml b/mobile/src/main/res/drawable/toggle_hex.xml
new file mode 100644
index 00000000..fa5ce92c
--- /dev/null
+++ b/mobile/src/main/res/drawable/toggle_hex.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/mobile/src/main/res/layout/activity_main.xml b/mobile/src/main/res/layout/activity_main.xml
index e0afe413..aedb7a28 100644
--- a/mobile/src/main/res/layout/activity_main.xml
+++ b/mobile/src/main/res/layout/activity_main.xml
@@ -32,7 +32,6 @@
android:id="@+id/navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:background="?android:attr/windowBackground"
app:menu="@menu/navigation"/>
diff --git a/mobile/src/main/res/layout/dialog_nickname.xml b/mobile/src/main/res/layout/dialog_nickname.xml
index 4800be2d..b67eaa09 100644
--- a/mobile/src/main/res/layout/dialog_nickname.xml
+++ b/mobile/src/main/res/layout/dialog_nickname.xml
@@ -5,7 +5,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:viewBindingIgnore="true">
-
-
+
diff --git a/mobile/src/main/res/layout/dialog_wifi_ap.xml b/mobile/src/main/res/layout/dialog_wifi_ap.xml
index 650b82a5..94604f2b 100644
--- a/mobile/src/main/res/layout/dialog_wifi_ap.xml
+++ b/mobile/src/main/res/layout/dialog_wifi_ap.xml
@@ -16,11 +16,13 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
+
+ style="@style/wifi_item_edit_content" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ app:suffixText="ms">
-
-
-
-
-
+ android:maxLength="19" />
-
-
-
+
-
-
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:orientation="vertical">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/src/main/res/layout/fragment_ebeg.xml b/mobile/src/main/res/layout/fragment_ebeg.xml
index deb5090a..0d5d035a 100644
--- a/mobile/src/main/res/layout/fragment_ebeg.xml
+++ b/mobile/src/main/res/layout/fragment_ebeg.xml
@@ -13,6 +13,8 @@
android:layout_height="wrap_content"
android:isScrollContainer="true"
android:orientation="vertical"
+ android:clipChildren="false"
+ android:clipToPadding="false"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:paddingLeft="24dp"
@@ -28,6 +30,8 @@
android:id="@+id/donations__google"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:clipChildren="false"
+ android:clipToPadding="false"
android:orientation="vertical">
diff --git a/mobile/src/main/res/layout/listitem_interface.xml b/mobile/src/main/res/layout/listitem_interface.xml
index a7d433b0..9a7d42dd 100644
--- a/mobile/src/main/res/layout/listitem_interface.xml
+++ b/mobile/src/main/res/layout/listitem_interface.xml
@@ -12,7 +12,8 @@
android:layout_height="wrap_content"
android:focusable="true"
android:background="?android:attr/selectableItemBackground"
- android:padding="16dp">
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp">
@@ -50,7 +53,7 @@
tools:text="192.168.43.1/24\n01:23:45:ab:cd:ef"/>
-
-
+
+
+
diff --git a/mobile/src/main/res/layout/preference_widget_material_switch.xml b/mobile/src/main/res/layout/preference_widget_material_switch.xml
new file mode 100644
index 00000000..7ede5c1b
--- /dev/null
+++ b/mobile/src/main/res/layout/preference_widget_material_switch.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
diff --git a/mobile/src/main/res/menu/navigation.xml b/mobile/src/main/res/menu/navigation.xml
index 34f64818..ed92125e 100644
--- a/mobile/src/main/res/menu/navigation.xml
+++ b/mobile/src/main/res/menu/navigation.xml
@@ -16,4 +16,10 @@
android:icon="@drawable/ic_action_settings"
android:title="@string/title_settings"/>
+
+
diff --git a/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
deleted file mode 100644
index bbd3e021..00000000
--- a/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/mobile/src/main/res/mipmap-hdpi/banner.webp b/mobile/src/main/res/mipmap-hdpi/banner.webp
deleted file mode 100644
index 7fe32e6b..00000000
Binary files a/mobile/src/main/res/mipmap-hdpi/banner.webp and /dev/null differ
diff --git a/mobile/src/main/res/mipmap-hdpi/ic_launcher.webp b/mobile/src/main/res/mipmap-hdpi/ic_launcher.webp
deleted file mode 100644
index 806ef1c2..00000000
Binary files a/mobile/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ
diff --git a/mobile/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/mobile/src/main/res/mipmap-hdpi/ic_launcher_round.webp
deleted file mode 100644
index efd60e71..00000000
Binary files a/mobile/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/mobile/src/main/res/mipmap-mdpi/banner.webp b/mobile/src/main/res/mipmap-mdpi/banner.webp
deleted file mode 100644
index 8a5dff40..00000000
Binary files a/mobile/src/main/res/mipmap-mdpi/banner.webp and /dev/null differ
diff --git a/mobile/src/main/res/mipmap-mdpi/ic_launcher.webp b/mobile/src/main/res/mipmap-mdpi/ic_launcher.webp
deleted file mode 100644
index 4f1a4320..00000000
Binary files a/mobile/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ
diff --git a/mobile/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/mobile/src/main/res/mipmap-mdpi/ic_launcher_round.webp
deleted file mode 100644
index e805e1ec..00000000
Binary files a/mobile/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/mobile/src/main/res/mipmap-xhdpi/banner.webp b/mobile/src/main/res/mipmap-xhdpi/banner.webp
deleted file mode 100644
index 084dc285..00000000
Binary files a/mobile/src/main/res/mipmap-xhdpi/banner.webp and /dev/null differ
diff --git a/mobile/src/main/res/mipmap-xhdpi/ic_launcher.webp b/mobile/src/main/res/mipmap-xhdpi/ic_launcher.webp
deleted file mode 100644
index d195932d..00000000
Binary files a/mobile/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ
diff --git a/mobile/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/mobile/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
deleted file mode 100644
index 7db19dcd..00000000
Binary files a/mobile/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/mobile/src/main/res/mipmap-xxhdpi/banner.webp b/mobile/src/main/res/mipmap-xxhdpi/banner.webp
deleted file mode 100644
index bb1e4d21..00000000
Binary files a/mobile/src/main/res/mipmap-xxhdpi/banner.webp and /dev/null differ
diff --git a/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.webp
deleted file mode 100644
index 04c3c80e..00000000
Binary files a/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 8fc28158..00000000
Binary files a/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/mobile/src/main/res/mipmap-xxxhdpi/banner.webp b/mobile/src/main/res/mipmap-xxxhdpi/banner.webp
deleted file mode 100644
index 669943fd..00000000
Binary files a/mobile/src/main/res/mipmap-xxxhdpi/banner.webp and /dev/null differ
diff --git a/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
deleted file mode 100644
index 8ce694e3..00000000
Binary files a/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 2b9c554f..00000000
Binary files a/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/mobile/src/main/res/mipmap-anydpi-v24/banner.xml b/mobile/src/main/res/mipmap/banner.xml
similarity index 100%
rename from mobile/src/main/res/mipmap-anydpi-v24/banner.xml
rename to mobile/src/main/res/mipmap/banner.xml
diff --git a/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile/src/main/res/mipmap/ic_launcher.xml
similarity index 74%
rename from mobile/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
rename to mobile/src/main/res/mipmap/ic_launcher.xml
index bbd3e021..80faec84 100644
--- a/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/mobile/src/main/res/mipmap/ic_launcher.xml
@@ -2,4 +2,5 @@
-
\ No newline at end of file
+
+
diff --git a/mobile/src/main/res/values-it/strings.xml b/mobile/src/main/res/values-it/strings.xml
index 877ecab7..0488c1fd 100644
--- a/mobile/src/main/res/values-it/strings.xml
+++ b/mobile/src/main/res/values-it/strings.xml
@@ -38,7 +38,7 @@
Servizio non disponibile. Riprova dopo
Hotspot Wi\u2011Fi temporaneo
- L\'hotspot temporaneo richiede che la localizzazione sia attiva.
+ L\'hotspot temporaneo richiede che la localizzazione sia attiva.
Avvio dell\'hotspot fallito (causa: %s)
nessun canale
errore generico
@@ -53,7 +53,6 @@
se il tethering VPN non funziona.
Tethering USB
Hotspot Wi\u2011Fi
- Hotspot Wi\u2011Fi (legacy)
Tethering Bluetooth
"Tethering Ethernet"
@@ -91,7 +90,6 @@
Servizio Android Netd
Disabilita tethering IPv6
Abilitando questa funzione si preveniranno perdite della VPN via IPv6.
- Avvia ripetitore all\'avvio
Tieni il Wi\u2011Fi attivo
Default di sistema
Attivo
@@ -154,9 +152,7 @@
Password
"L\'hotspot Wi‑Fi viene disattivato se non ci sono dispositivi collegati"
Banda AP
- "Automatica"
- "Banda a 2,4 GHz"
- "Banda a 5 GHz"
+ "Banda a %s GHz"
"Indirizzo MAC"
"Rete nascosta"
Salva
diff --git a/mobile/src/main/res/values-notnight-v27/colors.xml b/mobile/src/main/res/values-notnight-v27/colors.xml
deleted file mode 100644
index da37fa17..00000000
--- a/mobile/src/main/res/values-notnight-v27/colors.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- @android:color/white
-
diff --git a/mobile/src/main/res/values-pt-rBR/strings.xml b/mobile/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 00000000..ef8501af
--- /dev/null
+++ b/mobile/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,221 @@
+
+
+
+ VPN Hotspot
+ Repetidor
+ Tethering
+ Clientes
+ Configurações
+ Atualização
+
+ Repetidor (%1$d MHz, channel %2$d)
+ WPS (inseguro)
+ Insira um PIN
+ Push Button
+ Por favor, use o botão WPS dentro de 2 minutos para conectar seu dispositivo.
+ PIN registrado.
+ Ocorreu um erro ao acionar o WPS (motivo: %s)
+ Não foi encontrada uma configuração válida. Por favor, inicie o repetidor primeiro.
+ Falha ao remover grupo P2P redundante (motivo: %s)
+
+ Wi\u2011Fi direct não disponível, por favor, ative o Wi\u2011Fi
+ Ativar
+ Falha ao criar grupo P2P (motivo: %s)
+ Falha ao remover grupo P2P (motivo: %s)
+ Falha ao remover antigo grupo P2P (motivo: %s)
+ Falha ao definir canal operacional (motivo: %s)
+ Falha ao definir elementos do fornecedor (motivo: %s)
+
+ erro interno
+ Wi\u2011Fi direct não suportado
+ Nenhuma solicitação de serviço adicionada
+ Operação não suportada
+ Serviço indisponível. Tente novamente mais tarde
+ O repetidor necessita de permissões para acessar esta localização
+ Por restrições de sistema, desativar a localização pode deixar o repetidor ineficiente e aumentar o consumo de bateria
+ Configurar
+
+ Hotspot Wi\u2011Fi temporário
+ Essa função precisa que sua localização esteja ativada para funcionar.
+ Houve uma falha ao iniciar o hotspot (motivo: %s)
+ sem canal
+ erro genérico
+ modo incompatível
+ tethering não permitido
+
+ Monitor…
+ %s (monitorado)
+
+ Gerenciar tethering do sistema…
+ Por favor, desative a Aceleração de hardware de tethering nas configurações de desenvolvedor caso o VPN Hotspot não funcionar corretamente.
+ Tethering USB
+ Ponto de acesso Wi\u2011Fi
+ Tethering Bluetooth
+ Tethering Ethernet
+ Tethering USB (NCM)
+ %1$d MHz, canal %2$d, largura %3$s
+ %4$s: Wi\u2011Fi %5$d, %1$d MHz, canal %2$d,
+ largura %3$s, tempo ocioso em %6$s
+ %4$s: Wi\u2011Fi %5$d, %1$d MHz, largura %2$d,
+ largura %3$s, tempo ocioso desabilitado
+
+ - %1$s/%2$d cliente conectado\nFunções suportadas: %3$s
+ - %1$s/%2$d clientes conectados\nFunções suportadas: %3$s
+
+
+ - %d cliente conectado
+ - %1d clientes conectados
+
+ \nCanais suportados: %s
+ \nCódigo do país do Driver: %s
+ MAC AP aleatório
+ Simultaneidade de AP em ponte
+ STA + AP simultâneos
+ STA + Bridged AP simultâneos
+ Nenhum
+ Bloqueado %1$s: %2$s
+ Copiar MAC
+
+ " (conectando)"
+ " (alcançável)"
+ " (perdido)"
+
+ Apelido…
+ Bloquear
+ Ative o serviço para esta interface para bloquear o cliente.
+ Desbloquear
+ Status…
+ O servidor retornou um erro para %1$s: %2$s
+ Apelido para %s
+ ← 🏳️🌈 Fornecedor
+ Status para %s
+
+ - Conectado 1 vez desde %2$s
+ - Conectado %1$s vezes desde %2$s
+
+
+ - Enviado 1 pacote, %2$s
+ - Enviados %1$s pacotes, %2$s
+
+
+ - Recebido 1 pacote, %2$s
+ - Recebidos %1$s pacotes, %2$s
+
+
+ Upstream
+ Downstream
+ Modo de máscara de IP
+ Nenhum
+ Simples
+ Serviço Android Netd
+ Desative Tethering IPv6
+ Ativar esta opção evitará vazamentos de VPN via IPv6.
+ Auto inicialização de serviços
+ Restaurar serviços que estavam em execução antes do aplicativo / dispositivo reiniciar ou atualizar
+ Modo de repetidor seguro
+ Não faz alterações no seu sistema mas pode causar mal funcionamento com nomes de rede curtos.
+ Nomes curtos de rede podem exigir o desligamento do modo seguro.
+ Deixe o Wi\u2011Fi ativo
+ Padrão do sistema
+ Ativo
+ Modo de alta performance
+ Desativar o modo de economia de energia
+ Modo de baixa latência
+ Modo de monitoramento de rede
+ Monitor netlink
+ Monitor netlink com root
+ Poll
+ Poll com root
+ Upstreams atuais
+ %1$s; fallback: %2$s
+ Interface de upstream de rede
+ Detectar automaticamente VPN de sistema
+ Interface upstream de fallback
+ Detectar automaticamente a rede padrão do sistema
+ Ativar DHCP workaround
+ Use isso se o cliente não consegue obter endereço IP.
+ Limpar/re-aplicar regras de roteamento
+ Atualize as configurações alteradas para os serviços ativos atualmente. Isso pode corrigir condições raras.
+ Aceleração de Tethering do hardware
+ Atalho para a opção de desenvolvedor
+ Diversos
+ Ajuda
+ Exportar configurações de depuração
+ Muito útil… Uau
+ Página do GitHub do projeto
+ Leia o manual, marque com estrela, envie problemas e contribua (Tipo o Konny :D)
+ Doe
+ Eu amo dinheiro!
+ PayPal, Flattr, e mais…
+ Reinicie o aplicativo para aplicar essa função.
+ Sair
+
+ VPN tethering
+ Serviço de Tethering de VPN
+ Monitorar interfaces inativas
+
+ - %d dispotivivo conectado ao %s
+ - %d dispositivos conectados ao %s
+
+
+ - %d interface
+ - %d interfaces
+
+ Inativo: %s
+
+ - %d dispositivo
+ - %d dispositivos
+
+
+ desconhecido #%d
+ Fatal: Interface de downstream não encontrada
+ Algo se saiu errado. Por favor, verifique o log de depuração
+ Permissão faltando.
+
+ Configuração Wi\u2011Fi
+ Compartilhar via Código QR
+ O sistema Android recusa essa configuração. (olhe o logcat)
+ Nome da rede
+ Segurança
+ Senha
+ Desative o roteamento se não houver nenhum dispositivo conectado
+ Tempo inativo
+ Tempo inativo padrão: %dms
+ Banda do AP
+ Desativado
+ %s GHz
+ Canais permitidos 2.4 GHz ACS
+ Canais permitidos 5 GHz ACS
+ Canais permitidos 6 GHz ACS
+ Maximum channel bandwidth
+ Controle de acesso
+ Opções de AP avançadas
+ Endereço MAC
+ Endereço MAC aleatório persistente
+ Rede oculta
+ Número máximo de clientes
+ Controle de qual cliente pode usar o hotspot
+ Lista de clientes bloqueados
+ Lista de clientes permitidos
+ Usar um MAC aleatório
+ Nenhum
+ Persistente
+ Não persistente
+ Habilite o desligamento oportunista de uma instância no AP em ponte
+ Tempo limite inativo para uma instância em ponte
+ Ative Wi\u2011Fi 6
+ Ative Wi\u2011Fi 7
+ Configuração fornecida pelo usuário
+ Elementos do fornecedor
+ Salvar
+
+
+ Fechar
+ Você acha esse aplicativo útil?\nDê um apoio para o desenvolvedor, envie um mimo!
+ Google Play Store
+ Acho que algo está errado com os pagamentos pelo aplicativo. Certifique-se que sua Google Play Store está instalada corretamente.
+ O Google cobra uma taxa de 30%
+ Doe!
+ Quanto?
+ Obrigado mesmo por doar!\nEu lhe agradeço muito!
+
diff --git a/mobile/src/main/res/values-ru/strings.xml b/mobile/src/main/res/values-ru/strings.xml
index 09fb0188..ccf26072 100644
--- a/mobile/src/main/res/values-ru/strings.xml
+++ b/mobile/src/main/res/values-ru/strings.xml
@@ -59,9 +59,7 @@
"Пароль"
"Выключать точку доступа Wi‑Fi автоматически, если к ней не подключено ни одного устройства"
"Диапазон частот Wi-Fi"
- "Авто"
- "2,4 ГГц"
- "5,0 ГГц"
+ "%s ГГц"
"MAC-адрес"
"Скрытая сеть"
"Сохранить"
diff --git a/mobile/src/main/res/values-v25/bools.xml b/mobile/src/main/res/values-v25/bools.xml
deleted file mode 100644
index 34904e41..00000000
--- a/mobile/src/main/res/values-v25/bools.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- false
-
diff --git a/mobile/src/main/res/values-v26/bools.xml b/mobile/src/main/res/values-v26/bools.xml
deleted file mode 100644
index e3b6da48..00000000
--- a/mobile/src/main/res/values-v26/bools.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- true
-
diff --git a/mobile/src/main/res/values-v28/arrays.xml b/mobile/src/main/res/values-v28/arrays.xml
deleted file mode 100644
index d37b03dd..00000000
--- a/mobile/src/main/res/values-v28/arrays.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
- - @string/settings_service_masquerade_none
- - @string/settings_service_masquerade_simple
- - @string/settings_service_masquerade_netd
-
-
- - None
- - Simple
- - Netd
-
-
diff --git a/mobile/src/main/res/values-v29/colors.xml b/mobile/src/main/res/values-v29/colors.xml
new file mode 100644
index 00000000..e6a17f31
--- /dev/null
+++ b/mobile/src/main/res/values-v29/colors.xml
@@ -0,0 +1,4 @@
+
+
+ @android:color/transparent
+
diff --git a/mobile/src/main/res/values-zh-rCN/strings.xml b/mobile/src/main/res/values-zh-rCN/strings.xml
index 0cc49c35..5036e867 100644
--- a/mobile/src/main/res/values-zh-rCN/strings.xml
+++ b/mobile/src/main/res/values-zh-rCN/strings.xml
@@ -5,6 +5,7 @@
共享管理
已连设备
设置选项
+ 更新应用
无线中继 (%1$d MHz, 频道 %2$d)
WPS(不安全)
@@ -22,6 +23,7 @@
关闭已有 P2P 群组失败(原因:%s)
关闭 P2P 群组失败(原因:%s)
设置运行频段失败(原因:%s)
+ 设置供应商特定元素失败(原因:%s)
内部异常
设备不支持 Wi\u2011Fi 直连
@@ -29,9 +31,11 @@
不支持此操作
服务不可用,请稍后重试
无线中继需要精确位置权限
+ 由于系统限制,关闭位置信息服务可能产生问题并导致续航缩短
+ 进入设置
临时 WLAN 热点
- 使用临时热点需要打开位置服务。
+ 使用此功能需要打开位置服务。
打开热点失败 (原因:%s)
无频段
通用错误
@@ -53,11 +57,9 @@
-->
USB 网络共享
WLAN 热点
- WLAN 热点 (旧 API)
蓝牙网络共享
"以太网络共享"
USB 网络共享 (NCM)
- WiGig 热点
%1$d MHz, 频道 %2$d, 频宽 %3$s
%4$s: Wi\u2011Fi %5$d, %1$d MHz, 频道 %2$d, 频宽 %3$s,
关闭延迟 %6$s
@@ -70,6 +72,7 @@
- 已连接 %d 个设备
\n支持频道: %s
+ \n驱动国家代码:%s
随机接入点 MAC
桥接 AP 并发
STA/AP 并发
@@ -109,7 +112,8 @@
Android Netd 服务
禁用 IPv6 共享
防止 VPN 通过 IPv6 泄漏。
- 开机自启动中继
+ 自动启动服务
+ 设备重启或应用升级后自动恢复之前运行的服务
中继安全模式
不对系统配置进行修改,但是可能须要较长的网络名称。
使用短名称可能需要关闭安全模式。
@@ -152,6 +156,7 @@
VPN 共享已启用
VPN 共享服务
+ 监视不活跃接口
- %d 个设备已连接到 %s
@@ -172,6 +177,7 @@
使用 QR 码分享
Android 系统拒绝使用此配置。(详情参见日志)
"网络名称"
+ 切换十六进制显示
"安全性"
"密码"
未连接任何设备时自动关闭 WLAN 热点
@@ -179,22 +185,29 @@
默认延迟:%d 毫秒
"AP 频段"
Disabled
- "自动"
- "2.4 GHz 频段"
- "5 GHz 频段"
- 6 GHz 频段
- 60 GHz 频段
+ "%s GHz 频段"
+ 2.4 GHz ACS 可选频段
+ 5 GHz ACS 可选频段
+ 6 GHz ACS 可选频段
+ 最大频宽
+ 访问控制
+ 高级接入点设置
"MAC 地址"
+ 持久性随机 MAC 地址
"隐藏的网络"
允许连接设备数上限
过滤可以连接的设备
设备黑名单
设备白名单
随机生成 MAC 地址
- 启用无线接入点桥接模式
+ 无
+ 持久化
+ 不持久化
启用桥接模式伺机关闭
启用 Wi\u2011Fi 6
+ 启用 Wi\u2011Fi 7
用户提供配置
+ 供应商特定元素
"保存"
diff --git a/mobile/src/main/res/values-zh-rTW/strings.xml b/mobile/src/main/res/values-zh-rTW/strings.xml
index ac37f4b9..c6b21b25 100644
--- a/mobile/src/main/res/values-zh-rTW/strings.xml
+++ b/mobile/src/main/res/values-zh-rTW/strings.xml
@@ -13,45 +13,49 @@
VPN 無線基地台
中繼器
網路共用
- 客戶端
+ 用戶端
設定
+ 更新
- 中繼器 (%1$d MHz, 頻道 %2$d)
+ 中繼器 (%1$d MHz,頻道 %2$d)
WPS(不安全)
輸入 PIN 碼
- 一鍵加密
- 請在兩分鐘內在需要連接的裝置上,使用一鍵加密以連接到此中繼器
+ WPS 按鈕
+ 請在兩分鐘內在需要連線的裝置上,按下 WPS 按鈕以連線到此中繼器
PIN 已設定
- 開啟 WPS 失敗 (原因: %s)
- 找不到有效的配置,請先啟動中繼器
- 刪除多餘 P2P 群組失敗 (原因: %s)
+ 開啟 WPS 失敗 (原因:%s)
+ 未找到有效的組態,請先啟動中繼器
+ 刪除冗餘 P2P 群組失敗 (原因:%s)
Wi\u2011Fi Direct 不可用,請啟用 Wi\u2011Fi
開啟
- 創建 P2P 群組失敗 (原因: %s)
- 移除 P2P 群組失敗 (原因: %s)
- 移除 舊P2P 群組失敗 (原因: %s)
- 設定工作頻道失敗 (原因: %s)
+ 建立 P2P 群組失敗 (原因:%s)
+ 移除 P2P 群組失敗 (原因:%s)
+ 移除舊 P2P 群組失敗 (原因:%s)
+ 設定工作頻道失敗 (原因:%s)
內部錯誤
Wi\u2011Fi Direct 不支援
未添加服務請求
- 不支援的操作
+ 不支援的作業
服務不可用,請稍後再試
+ 中繼器需要精確位置存取權
+ 由於系統限制,關閉定位服務可能會導致問題並增加電池使用量
+ 設定
臨時 Wi\u2011Fi 無線基地台
- 開啟臨時無線基地台須打開定位
- 啟動無線基地台失敗 (原因: %s)
+ 需要開啟定位
+ 啟動無線基地台失敗 (原因:%s)
沒有頻道
一般錯誤
不相容的模式
- 禁止網路共用
+ 網路共用已禁止
- 檢測…
- %s (受檢測)
+ 監視…
+ %s (受監視)
系統網路共用管理…
- 如果 VPN 網路共用不起作用請在開發人員選項中關閉數據連線硬體加速
+ 如果 VPN 網路共用不起作用,請在「開發人員選項」中關閉「網路共用硬體加速」
USB 網路共用
Wi\u2011Fi 無線基地台
- Wi\u2011Fi 無線基地台 (舊式)
藍牙網路共用
"乙太網路網路共用"
USB 網路共用 (NCM)
- WiGig 無線基地台
- %1$d MHz, 頻道 %2$d, 頻寬 %3$s
+ %1$d MHz,頻道 %2$d,頻寬 %3$s
+ %4$s:Wi\u2011Fi %5$d,%1$d MHz,頻道 %2$d,頻寬 %3$s,
+ 閒置逾時 %6$s
+ %4$s:Wi\u2011Fi %5$d,%1$d MHz,頻道 %2$d,頻寬 %3$s,
+ 閒置逾時已停用
- - 已連接 %1$s/%2$d 個設備\n支持功能:%3$s
+ - 已連線 %1$s/%2$d 個裝置\n支援功能:%3$s
- - 已連接 %d 個設備
+ - 已連線 %d 個裝置
+ \n支援頻道:%s
+ 隨機 AP MAC
+ 橋接 AP 並行
+ STA/AP 並行
+ STA/橋接 AP 並行
無
已隱藏 %1$s:%2$s
複製 MAC
- (連接中)
- (已連接)
- (未連接)
+ (正在連線)
+ (已連線)
+ (已中斷)
暱稱…
黑名單
- 打開此服務來阻止裝置連線
+ 開啟此服務以阻止裝置連線
解除黑名單
狀態…
- 伺服器錯誤 %1$s: %2$s
+ 伺服器錯誤 %1$s:%2$s
暱稱 %s
← 🏳️🌈 供應商
狀態 %s
- - 自 %2$s 以來連接了 %1$s 次
+ - 自 %2$s 以來連線了 %1$s 次
- 上傳 %1$s 個封包,%2$s
- - 下載 %1$s 個封包, %2$s
+ - 下載 %1$s 個封包,%2$s
上游
下游
- IP 遮蔽模式
+ IP 偽裝模式
無
簡易
Android Netd 服務
停用 IPv6 共用
- 防止 VPN 通過 IPv6 洩漏
- 開機時自動啟動中繼器
+ 防止 VPN 透過 IPv6 洩漏
+ 自動啟動服務
+ 裝置重新啟動或應用升級後自動恢復之前執行的服務
中繼安全模式
- 不對系統設定值進行任何修改,但是可能需要較長的 SSID。
+ 不對系統組態做任何變更,但是可能需要較長的 SSID
使用短 SSID 可能需要關閉安全模式。
保持 Wi\u2011Fi 開啟
系統預設
@@ -119,42 +131,43 @@
高效能模式
關閉省電模式
低延遲模式
- 網路監聽模式
- Netlink 監聽
- Netlink 監聽 (root)
+ 網路狀態監視模式
+ Netlink 監視
+ Netlink 監視 (root)
輪詢
輪詢 (root)
- 目前上游接口
- %1$s; 備用: %2$s
- 上游網路接口
+ 目前上游介面
+ %1$s;後援:%2$s
+ 上游網路介面
自動檢測系統 VPN 服務
- 備用上游接口
+ 後援上游介面
自動檢測系統預設網路
清理/重新套用路由規則
- 將修改的設定套用到目前啟用的服務上。也可用於修復偶爾會發生的競態條件。
- 嘗試修復 DHCP
- 如果裝置無法取得 IP 地址,嘗試打開這選項。
- 數據連線硬體加速
- 系統"開發人員選項"的快捷方式
+ 將修改的設定套用到目前啟用的服務上,也可用於修復偶爾會發生的競態條件
+ 啟用 DHCP 因應措施
+ 如果裝置無法取得 IP 位址,嘗試開啟這選項
+ 網路共用硬體加速
+ 系統「開發人員選項」的捷徑
雜項
- 幫助
- 匯出 debug 所需資訊
- 這種非常有用啊(^O^)/
- 產品主頁 @ GitHub
- 閱讀使用說明, star, 提交 issues, 合作
+ 說明
+ 匯出偵錯資訊
+ 這種非常有用啊 (^O^)/
+ 專案首頁 @ GitHub
+ 閱讀使用說明,對 GitHub repo 加星號,提交問題並合作參與開發
抖內
我喜歡錢
- PayPal, Flattr, 等其他方式…
+ PayPal、Flattr 等其他方式…
重啟應用程式以套用設定值
離開
VPN 無線基地台已啟用
VPN 無線基地台服務
+ 監視非作用中介面
- - %d 個裝置已連接到 %s
+ - %d 個裝置已連線到 %s
- - %d 個接口
+ - %d 個介面
停用:%s
@@ -162,44 +175,44 @@
未知 #%d
- 錯誤: 找不到下游接口
- 發生異常,詳情請查看 log。
+ 錯誤:找不到下游介面
+ 發生錯誤,請檢查偵錯資訊。
權限不足
- 設定 WIFI
- 使用 QR Code 來分享
- Android 系统拒绝使用此設定。(詳情請參考 log)
+ Wi\u2011Fi 組態
+ 使用 QR Code 分享
+ Android 系統拒絕使用此組態。(請檢視 Logcat)
網路名稱
安全性
密碼
在沒有任何裝置連線時關閉 WIFI 無線基地台
- 關閉延遲時間
- 默認延遲:%d 毫秒
+ 非使用中關閉逾時
+ 預設逾時:%d 毫秒
AP 頻帶
- Disabled
- 自動
- 2.4 GHz 頻帶
- 5 GHz 頻帶
- 6 GHz 頻帶
- 60 GHz 頻帶
- "MAC 地址"
+ 停用
+ %s GHz 頻帶
+ 存取控制
+ 進階 AP 選項
+ "MAC 位址"
"隱藏的網路"
- 允許的連接裝置數量
- 過濾可以連接的裝置
+ 最大連線裝置數量
+ 過濾可以連線的裝置
裝置黑名單
裝置白名單
隨機化 MAC 位址
+ 啟用橋接模式隨機關閉
啟用 Wi\u2011Fi 6
+ 啟用 Wi\u2011Fi 7
+ 使用者提供組態
儲存
關閉
Google Play 商店
- 不支援應用程式內購。Google Play 商店是否安裝正確?
+ 不支援應用程式內購,Play 商店是否正確安裝?
抖內!
抖內多少錢
- 感謝抖內!\n十分感謝您!
-
- 這個程式有用嗎?\n小額的抖內給開發者用來支持此應用程式的開發
+ 感謝抖內!\n十分感謝您!
+ 這個應用程式有用嗎?\n小額的抖內給開發人員用來支援此應用程式的開發
Google 將收取 30% 的費用
diff --git a/mobile/src/main/res/values/arrays.xml b/mobile/src/main/res/values/arrays.xml
index fc606373..1b313614 100644
--- a/mobile/src/main/res/values/arrays.xml
+++ b/mobile/src/main/res/values/arrays.xml
@@ -1,12 +1,20 @@
+
+ - @string/wifi_mac_randomization_none
+ - @string/wifi_mac_randomization_persistent
+ - @string/wifi_mac_randomization_non_persistent
+
+
- @string/settings_service_masquerade_none
- @string/settings_service_masquerade_simple
+ - @string/settings_service_masquerade_netd
- None
- Simple
+ - Netd
diff --git a/mobile/src/main/res/values/bools.xml b/mobile/src/main/res/values/bools.xml
index a84b400b..3c344c71 100644
--- a/mobile/src/main/res/values/bools.xml
+++ b/mobile/src/main/res/values/bools.xml
@@ -1,7 +1,5 @@
false
- false
- true
true
diff --git a/mobile/src/main/res/values/colors.xml b/mobile/src/main/res/values/colors.xml
index 329530c6..9e294101 100644
--- a/mobile/src/main/res/values/colors.xml
+++ b/mobile/src/main/res/values/colors.xml
@@ -6,5 +6,5 @@
@color/light_colorPrimary
#087f23
#AEEA00
- @android:color/black
+ #6000
diff --git a/mobile/src/main/res/values/font_certs.xml b/mobile/src/main/res/values/font_certs.xml
deleted file mode 100644
index d63462fc..00000000
--- a/mobile/src/main/res/values/font_certs.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
- - @array/com_google_android_gms_fonts_certs_dev
- - @array/com_google_android_gms_fonts_certs_prod
-
-
- -
- MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
-
-
-
- -
- MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK
-
-
-
diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml
index 692f58ad..842561b4 100644
--- a/mobile/src/main/res/values/strings.xml
+++ b/mobile/src/main/res/values/strings.xml
@@ -21,6 +21,7 @@
Tethering
Clients
Settings
+ Update
Repeater (%1$d MHz, channel %2$d)
WPS (insecure)
@@ -39,6 +40,7 @@
Failed to remove P2P group (reason: %s)
Failed to remove old P2P group (reason: %s)
Failed to set operating channel (reason: %s)
+ Failed to set vendor elements (reason: %s)
internal error
Wi\u2011Fi direct unsupported
@@ -47,9 +49,12 @@
Service unavailable. Try again later
Repeater requires permissions for accessing fine
location
+ Due to system restrictions, turning Location off may lead to things not working
+ properly and increased battery usage
+ Configure
Temporary Wi\u2011Fi hotspot
- Temporary hotspot requires location to be turned on.
+ This feature requires location to be turned on.
Failed to start hotspot (reason: %s)
no channel
generic error
@@ -64,11 +69,9 @@
if VPN tethering does not work.
USB tethering
Wi\u2011Fi hotspot
- Wi\u2011Fi hotspot (legacy)
Bluetooth tethering
Ethernet tethering
USB tethering (NCM)
- WiGig hotspot
%1$d MHz, channel %2$d, width %3$s
%4$s: Wi\u2011Fi %5$d, %1$d MHz, channel %2$d,
width %3$s, idle timeout in %6$s
@@ -83,6 +86,7 @@
- %1d clients connected
\nSupported channels: %s
+ \nDriver country code: %s
Randomized AP MAC
Bridged AP concurrency
STA + AP concurrency
@@ -125,7 +129,9 @@
Android Netd Service
Disable IPv6 tethering
Enabling this option will prevent VPN leaks via IPv6.
- Start repeater on boot
+ Auto start services
+ Restore services if they were running before device reboot or app
+ update
Repeater safe mode
Makes no changes to your system configuration but might
not work with short network names.
@@ -170,8 +176,9 @@
Restart this app to apply this setting.
Exit
- VPN tethering active
+ VPN tethering
VPN Tethering Service
+ Monitor Inactive Interfaces
- %d device connected to %s
- %d devices connected to %s
@@ -195,6 +202,7 @@
Share via QR code
Android system refuses such configuration. (see logcat)
Network name
+ Toggle hex display
Security
Password
Turn off hotspot automatically when no devices are connected
@@ -202,22 +210,30 @@
Default timeout: %dms
AP Band
Disabled
- Auto
- 2.4 GHz Band
- 5 GHz Band
- 6 GHz Band
- 60 GHz Band
+ %s GHz Band
+ Allowed 2.4 GHz ACS channels
+ Allowed 5 GHz ACS channels
+ Allowed 6 GHz ACS channels
+ Maximum channel bandwidth
+ Access Control
+ Advanced AP Options
MAC address
+ Persistent Randomized MAC address
Hidden network
Maximum number of clients
Control which client can use hotspot
Blocked list of clients
Allowed list of clients
Use randomized MAC
- Enable Bridged Access point (AP) concurrency
- Enable Bridged mode opportunistic shutdown
+ None
+ Persistent
+ Non-persistent
+ Enable opportunistic shutdown of an instance in bridged AP
+ Inactive timeout for a bridged instance
Enable Wi\u2011Fi 6
- User Supplied Configuration
+ Enable Wi\u2011Fi 7
+ User supplied configuration
+ Vendor elements
Save
diff --git a/mobile/src/main/res/values/styles.xml b/mobile/src/main/res/values/styles.xml
index 013d57fa..4104fb57 100644
--- a/mobile/src/main/res/values/styles.xml
+++ b/mobile/src/main/res/values/styles.xml
@@ -1,12 +1,13 @@
-
+
+
+
diff --git a/mobile/src/main/res/xml/locales_config.xml b/mobile/src/main/res/xml/locales_config.xml
new file mode 100644
index 00000000..4695e232
--- /dev/null
+++ b/mobile/src/main/res/xml/locales_config.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/mobile/src/main/res/xml/pref_settings.xml b/mobile/src/main/res/xml/pref_settings.xml
index 0cc8b2bd..2f796fbc 100644
--- a/mobile/src/main/res/xml/pref_settings.xml
+++ b/mobile/src/main/res/xml/pref_settings.xml
@@ -31,19 +31,19 @@
-
-
-
-
-
+
-