Merge branch 'master' into temp-hotspot-use-system

This commit is contained in:
Mygod
2023-03-02 23:19:51 -05:00
148 changed files with 3939 additions and 3409 deletions

View File

@@ -3,7 +3,7 @@ jobs:
build: build:
working_directory: ~/code working_directory: ~/code
docker: docker:
- image: circleci/android:api-30 - image: cimg/android:2023.02.1
environment: environment:
GRADLE_OPTS: -Dorg.gradle.workers.max=1 -Dorg.gradle.daemon=false -Dkotlin.compiler.execution.strategy="in-process" GRADLE_OPTS: -Dorg.gradle.workers.max=1 -Dorg.gradle.daemon=false -Dkotlin.compiler.execution.strategy="in-process"
steps: steps:

119
README.md
View File

@@ -1,15 +1,19 @@
# VPN Hotspot # VPN Hotspot
[![CircleCI](https://circleci.com/gh/Mygod/VPNHotspot.svg?style=shield)](https://circleci.com/gh/Mygod/VPNHotspot) [![CircleCI](https://circleci.com/gh/Mygod/VPNHotspot.svg?style=shield)](https://circleci.com/gh/Mygod/VPNHotspot)
[![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21) [![API](https://img.shields.io/badge/API-28%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=28)
[![Releases](https://img.shields.io/github/downloads/Mygod/VPNHotspot/total.svg)](https://github.com/Mygod/VPNHotspot/releases) [![Releases](https://img.shields.io/github/downloads/Mygod/VPNHotspot/total.svg)](https://github.com/Mygod/VPNHotspot/releases)
[![Language: Kotlin](https://img.shields.io/github/languages/top/Mygod/VPNHotspot.svg)](https://github.com/Mygod/VPNHotspot/search?l=kotlin) [![Language: Kotlin](https://img.shields.io/github/languages/top/Mygod/VPNHotspot.svg)](https://github.com/Mygod/VPNHotspot/search?l=kotlin)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/e70e52b1a58045819b505c09edcae816)](https://www.codacy.com/app/Mygod/VPNHotspot?utm_source=github.com&utm_medium=referral&utm_content=Mygod/VPNHotspot&utm_campaign=Badge_Grade) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/e70e52b1a58045819b505c09edcae816)](https://www.codacy.com/gh/Mygod/VPNHotspot/dashboard?utm_source=github.com&utm_medium=referral&utm_content=Mygod/VPNHotspot&utm_campaign=Badge_Grade)
[![License](https://img.shields.io/github/license/Mygod/VPNHotspot.svg)](LICENSE) [![License](https://img.shields.io/github/license/Mygod/VPNHotspot.svg)](LICENSE)
Connecting things to your VPN made simple. Share your VPN connection over hotspot or repeater. (**root required**) Connecting things to your VPN made simple. Share your VPN connection over hotspot or repeater. (**root required**)
<a href="https://play.google.com/store/apps/details?id=be.mygod.vpnhotspot" target="_blank"><img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" height="60"></a>,
<a href="https://appdistribution.firebase.dev/i/FUCPGdzm" target="_blank">sign up for beta</a> | Release channel | [GitHub](https://github.com/Mygod/VPNHotspot/releases) | [Google Play](https://play.google.com/store/apps/details?id=be.mygod.vpnhotspot) ([beta](https://play.google.com/apps/testing/be.mygod.vpnhotspot)) |
|---------------------------------------------------------|:------------------------------------------------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------:|
| Auto update | Email updates via watching releases | ✓ |
| In-app update channel | GitHub | Google Play |
| [Sponsor/Donation](https://github.com/sponsors/Mygod) | ✓ | Google Play In-App Purchases only |
This app is useful for: This app is useful for:
@@ -69,7 +73,7 @@ Default settings are picked to suit general use cases and maximize compatibility
I find turning this option off sometimes works better for dummy VPNs like ad-blockers and socksifiers than Simple mode, e.g. Shadowsocks. I find turning this option off sometimes works better for dummy VPNs like ad-blockers and socksifiers than Simple mode, e.g. Shadowsocks.
But you should never use this for real VPNs like OpenVPN, etc. But you should never use this for real VPNs like OpenVPN, etc.
- Simple: Source address/port from downstream packets will be remapped and that's about it. - Simple: Source address/port from downstream packets will be remapped and that's about it.
- (since Android 9) Android Netd Service: - Android Netd Service:
Let your system handle masquerade. Let your system handle masquerade.
Android system will do a few extra things to make things like FTP and tethering traffic counter work. Android system will do a few extra things to make things like FTP and tethering traffic counter work.
You should probably not use this if you are trying to hide your tethering activity from your carrier. You should probably not use this if you are trying to hide your tethering activity from your carrier.
@@ -78,7 +82,7 @@ Default settings are picked to suit general use cases and maximize compatibility
* Disable IPv6 tethering: Turning this option on will disable IPv6 for system tethering. Useful for stopping IPv6 leaks * Disable IPv6 tethering: Turning this option on will disable IPv6 for system tethering. Useful for stopping IPv6 leaks
as this app currently doesn't handle IPv6 VPN tethering (see [#6](https://github.com/Mygod/VPNHotspot/issues/6)). as this app currently doesn't handle IPv6 VPN tethering (see [#6](https://github.com/Mygod/VPNHotspot/issues/6)).
* (since Android 8.1) Tethering hardware acceleration: * Tethering hardware acceleration:
This is a shortcut to the same setting in system Developer options. This is a shortcut to the same setting in system Developer options.
Turning this option off is probably a must for making VPN tethering over system tethering work, Turning this option off is probably a must for making VPN tethering over system tethering work,
but it might also decrease your battery life while tethering is enabled. but it might also decrease your battery life while tethering is enabled.
@@ -146,58 +150,56 @@ _a.k.a. things that can go wrong if this app doesn't work._
This is a list of stuff that might impact this app's functionality if unavailable. This is a list of stuff that might impact this app's functionality if unavailable.
This is only meant to be an index. This is only meant to be an index.
You can read more in the source code. You can read more in the source code.
API restrictions are updated up to [commit `ebe7044`](https://android.googlesource.com/platform/prebuilts/runtime/+/ebe7044/appcompat/hiddenapi-flags.csv). API restrictions are updated up to [SHA-256 checksum `233a277aa8ac475b6df61bffd95665d86aac6eb2ad187b90bf42a98f5f2a11a3`](https://dl.google.com/developers/android/tm/non-sdk/hiddenapi-flags.csv).
Greylisted/blacklisted APIs or internal constants: (some constants are hardcoded or implicitly used) Greylisted/blacklisted APIs or internal constants: (some constants are hardcoded or implicitly used)
* (prior to API 30) `Landroid/net/ConnectivityManager;->getLastTetherError(Ljava/lang/String;)I,max-target-r` * (prior to API 30) `Landroid/net/ConnectivityManager;->getLastTetherError(Ljava/lang/String;)I,max-target-r`
* (since API 30) `Landroid/net/ConnectivityModuleConnector;->IN_PROCESS_SUFFIX:Ljava/lang/String;` * (since API 30) `Landroid/net/ConnectivityModuleConnector;->IN_PROCESS_SUFFIX:Ljava/lang/String;`
* (since API 30) `Landroid/net/TetheringManager$TetheringEventCallback;->onTetherableInterfaceRegexpsChanged(Landroid/net/TetheringManager$TetheringInterfaceRegexps;)V,blocked` * (since API 30) `Landroid/net/TetheringManager$TetheringEventCallback;->onTetherableInterfaceRegexpsChanged(Landroid/net/TetheringManager$TetheringInterfaceRegexps;)V,blocked`
* (since API 30) `Landroid/net/TetheringManager;->TETHERING_WIGIG:I,blocked` * (since API 31) `Landroid/net/TetheringManager$TetheringEventCallback;->onSupportedTetheringTypes(Ljava/util/Set;)V,blocked`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setUserConfiguration(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,blocked` * (since API 31) `Landroid/net/wifi/SoftApCapability;->getCountryCode()Ljava/lang/String;,blocked`
* (since API 33) `Landroid/net/wifi/SoftApConfiguration$Builder;->setRandomizedMacAddress(Landroid/net/MacAddress;)Landroid/net/wifi/SoftApConfiguration$Builder;,blocked`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->BAND_TYPES:[I,blocked` * (since API 31) `Landroid/net/wifi/SoftApConfiguration;->BAND_TYPES:[I,blocked`
* (since API 31) `Landroid/net/wifi/SoftApInfo;->getApInstanceIdentifier()Ljava/lang/String;,blocked` * (since API 31) `Landroid/net/wifi/SoftApInfo;->getApInstanceIdentifier()Ljava/lang/String;,blocked`
* (since API 31) `Landroid/net/wifi/WifiClient;->getApInstanceIdentifier()Ljava/lang/String;,blocked` * (since API 31) `Landroid/net/wifi/WifiClient;->getApInstanceIdentifier()Ljava/lang/String;,blocked`
* (prior to API 30) `Landroid/net/wifi/WifiConfiguration$KeyMgmt;->FT_PSK:I,lo-prio,max-target-o` * (prior to API 30) `Landroid/net/wifi/WifiConfiguration$KeyMgmt;->FT_PSK:I,lo-prio,max-target-o`
* (prior to API 30) `Landroid/net/wifi/WifiConfiguration$KeyMgmt;->WPA_PSK_SHA256:I,blocked` * (prior to API 30) `Landroid/net/wifi/WifiConfiguration$KeyMgmt;->WPA_PSK_SHA256:I,blocked`
* (since API 23, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->AP_BAND_2GHZ:I,lo-prio,max-target-o` * (prior to API 30) `Landroid/net/wifi/WifiConfiguration;->AP_BAND_2GHZ:I,lo-prio,max-target-o`
* (since API 23, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->AP_BAND_5GHZ:I,lo-prio,max-target-o` * (prior to API 30) `Landroid/net/wifi/WifiConfiguration;->AP_BAND_5GHZ:I,lo-prio,max-target-o`
* (since API 28, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->AP_BAND_ANY:I,lo-prio,max-target-o` * (prior to API 30) `Landroid/net/wifi/WifiConfiguration;->AP_BAND_ANY:I,lo-prio,max-target-o`
* (since API 23, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->apBand:I,unsupported` * (prior to API 30) `Landroid/net/wifi/WifiConfiguration;->apBand:I,unsupported`
* (since API 23, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->apChannel:I,unsupported` * (prior to API 30) `Landroid/net/wifi/WifiConfiguration;->apChannel:I,unsupported`
* (since API 28, prior to API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onNumClientsChanged(I)V,greylist-max-o` * (prior to API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onNumClientsChanged(I)V,greylist-max-o`
* (since API 26) `Landroid/net/wifi/WifiManager;->cancelLocalOnlyHotspotRequest()V,unsupported` * `Landroid/net/wifi/WifiManager;->cancelLocalOnlyHotspotRequest()V,unsupported`
* (prior to API 26) `Landroid/net/wifi/WifiManager;->setWifiApEnabled(Landroid/net/wifi/WifiConfiguration;Z)Z`
* `Landroid/net/wifi/p2p/WifiP2pConfig$Builder;->MAC_ANY_ADDRESS:Landroid/net/MacAddress;,blocked` * `Landroid/net/wifi/p2p/WifiP2pConfig$Builder;->MAC_ANY_ADDRESS:Landroid/net/MacAddress;,blocked`
* (since API 29) `Landroid/net/wifi/p2p/WifiP2pConfig$Builder;->mNetworkName:Ljava/lang/String;,blocked` * (since API 29) `Landroid/net/wifi/p2p/WifiP2pConfig$Builder;->mNetworkName:Ljava/lang/String;,blocked`
* `Landroid/net/wifi/p2p/WifiP2pManager;->startWps(Landroid/net/wifi/p2p/WifiP2pManager$Channel;Landroid/net/wifi/WpsInfo;Landroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,max-target-r` * `Landroid/net/wifi/p2p/WifiP2pManager;->startWps(Landroid/net/wifi/p2p/WifiP2pManager$Channel;Landroid/net/wifi/WpsInfo;Landroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,unsupported`
* (since API 28, prior to API 30) `Landroid/provider/Settings$Global;->SOFT_AP_TIMEOUT_ENABLED:Ljava/lang/String;,lo-prio,max-target-o` * (prior to API 30) `Landroid/provider/Settings$Global;->SOFT_AP_TIMEOUT_ENABLED:Ljava/lang/String;,lo-prio,max-target-o`
* (prior to API 30) `Lcom/android/internal/R$array;->config_tether_bluetooth_regexs:I,max-target-q` * (prior to API 30) `Lcom/android/internal/R$array;->config_tether_bluetooth_regexs:I,max-target-q`
* (prior to API 30) `Lcom/android/internal/R$array;->config_tether_usb_regexs:I,max-target-q` * (prior to API 30) `Lcom/android/internal/R$array;->config_tether_usb_regexs:I,max-target-q`
* (prior to API 30) `Lcom/android/internal/R$array;->config_tether_wifi_regexs:I,max-target-q` * (prior to API 30) `Lcom/android/internal/R$array;->config_tether_wifi_regexs:I,max-target-q`
* (on API 29) `Lcom/android/internal/R$bool;->config_wifi_p2p_mac_randomization_supported:I,blacklist` * (on API 29) `Lcom/android/internal/R$bool;->config_wifi_p2p_mac_randomization_supported:I,blacklist`
* (since API 28, prior to API 30) `Lcom/android/internal/R$integer;->config_wifi_framework_soft_ap_timeout_delay:I,greylist-max-o` * (prior to API 30) `Lcom/android/internal/R$integer;->config_wifi_framework_soft_ap_timeout_delay:I,greylist-max-o`
* `Lcom/android/internal/R$string;->config_ethernet_iface_regex:I,lo-prio,max-target-o` * `Lcom/android/internal/R$string;->config_ethernet_iface_regex:I,lo-prio,max-target-o`
* (since API 27) `Lcom/android/server/connectivity/tethering/OffloadHardwareInterface;->DEFAULT_TETHER_OFFLOAD_DISABLED:I`
* (since API 30) `Lcom/android/server/wifi/WifiContext;->ACTION_RESOURCES_APK:Ljava/lang/String;` * (since API 30) `Lcom/android/server/wifi/WifiContext;->ACTION_RESOURCES_APK:Ljava/lang/String;`
* (since API 29) `Lcom/android/server/wifi/p2p/WifiP2pServiceImpl;->ANONYMIZED_DEVICE_ADDRESS:Ljava/lang/String;` * (since API 29) `Lcom/android/server/wifi/p2p/WifiP2pServiceImpl;->ANONYMIZED_DEVICE_ADDRESS:Ljava/lang/String;`
* (since API 30) `Lcom/android/server/SystemServer;->TETHERING_CONNECTOR_CLASS:Ljava/lang/String;` * (since API 30) `Lcom/android/server/SystemServer;->TETHERING_CONNECTOR_CLASS:Ljava/lang/String;`
* (since API 29) `Ldalvik/system/VMDebug;->allowHiddenApiReflectionFrom(Ljava/lang/Class;)V,unsupported` * `Ljava/lang/invoke/MethodHandles$Lookup;-><init>(Ljava/lang/Class;I)V,unsupported`
* (since API 26) `Ljava/lang/invoke/MethodHandles$Lookup;-><init>(Ljava/lang/Class;I)V,unsupported` * `Ljava/lang/invoke/MethodHandles$Lookup;->ALL_MODES:I,lo-prio,max-target-o`
* (since API 26) `Ljava/lang/invoke/MethodHandles$Lookup;->ALL_MODES:I,lo-prio,max-target-o`
* (prior to API 29) `Ljava/net/InetAddress;->parseNumericAddress(Ljava/lang/String;)Ljava/net/InetAddress;,core-platform-api,max-target-p` * (prior to API 29) `Ljava/net/InetAddress;->parseNumericAddress(Ljava/lang/String;)Ljava/net/InetAddress;,core-platform-api,max-target-p`
<details> <details>
<summary>Hidden whitelisted APIs: (same catch as above, however, things in this list are less likely to be broken)</summary> <summary>Hidden whitelisted APIs: (same catch as above, however, things in this list are less likely to be broken)</summary>
* (since API 24) `Landroid/bluetooth/BluetoothPan;->isTetheringOn()Z,sdk,system-api,test-api` * `Landroid/bluetooth/BluetoothPan;->isTetheringOn()Z,sdk,system-api,test-api`
* (since API 24) `Landroid/bluetooth/BluetoothProfile;->PAN:I,sdk,system-api,test-api` * `Landroid/bluetooth/BluetoothProfile;->PAN:I,sdk,system-api,test-api`
* (since API 30) `Landroid/content/Context;->TETHERING_SERVICE:Ljava/lang/String;,sdk,system-api,test-api` * (since API 30) `Landroid/content/Context;->TETHERING_SERVICE:Ljava/lang/String;,sdk,system-api,test-api`
* (since API 24, prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;-><init>()V,sdk,system-api,test-api` * (prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;-><init>()V,sdk,system-api,test-api`
* (since API 24, prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringFailed()V,sdk,system-api,test-api` * (prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringFailed()V,sdk,system-api,test-api`
* (since API 24, prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringStarted()V,sdk,system-api,test-api` * (prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringStarted()V,sdk,system-api,test-api`
* (since API 24, prior to API 30) `Landroid/net/ConnectivityManager;->startTethering(IZLandroid/net/ConnectivityManager$OnStartTetheringCallback;Landroid/os/Handler;)V,sdk,system-api,test-api` * (prior to API 30) `Landroid/net/ConnectivityManager;->startTethering(IZLandroid/net/ConnectivityManager$OnStartTetheringCallback;Landroid/os/Handler;)V,sdk,system-api,test-api`
* (since API 24, prior to API 30) `Landroid/net/ConnectivityManager;->stopTethering(I)V,sdk,system-api,test-api` * (prior to API 30) `Landroid/net/ConnectivityManager;->stopTethering(I)V,sdk,system-api,test-api`
* `Landroid/net/LinkProperties;->getAllInterfaceNames()Ljava/util/List;,sdk,system-api,test-api` * `Landroid/net/LinkProperties;->getAllInterfaceNames()Ljava/util/List;,sdk,system-api,test-api`
* `Landroid/net/LinkProperties;->getAllRoutes()Ljava/util/List;,sdk,system-api,test-api` * `Landroid/net/LinkProperties;->getAllRoutes()Ljava/util/List;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager$StartTetheringCallback;->onTetheringFailed(I)V,sdk,system-api,test-api` * (since API 30) `Landroid/net/TetheringManager$StartTetheringCallback;->onTetheringFailed(I)V,sdk,system-api,test-api`
@@ -214,14 +216,14 @@ Greylisted/blacklisted APIs or internal constants: (some constants are hardcoded
* (since API 30) `Landroid/net/TetheringManager$TetheringRequest$Builder;->setExemptFromEntitlementCheck(Z)Landroid/net/TetheringManager$TetheringRequest$Builder;,sdk,system-api,test-api` * (since API 30) `Landroid/net/TetheringManager$TetheringRequest$Builder;->setExemptFromEntitlementCheck(Z)Landroid/net/TetheringManager$TetheringRequest$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager$TetheringRequest$Builder;->setShouldShowEntitlementUi(Z)Landroid/net/TetheringManager$TetheringRequest$Builder;,sdk,system-api,test-api` * (since API 30) `Landroid/net/TetheringManager$TetheringRequest$Builder;->setShouldShowEntitlementUi(Z)Landroid/net/TetheringManager$TetheringRequest$Builder;,sdk,system-api,test-api`
* `Landroid/net/TetheringManager;->ACTION_TETHER_STATE_CHANGED:Ljava/lang/String;,sdk,system-api,test-api` * `Landroid/net/TetheringManager;->ACTION_TETHER_STATE_CHANGED:Ljava/lang/String;,sdk,system-api,test-api`
* (since API 26) `Landroid/net/TetheringManager;->EXTRA_ACTIVE_LOCAL_ONLY:Ljava/lang/String;,sdk,system-api,test-api` * `Landroid/net/TetheringManager;->EXTRA_ACTIVE_LOCAL_ONLY:Ljava/lang/String;,sdk,system-api,test-api`
* `Landroid/net/TetheringManager;->EXTRA_ACTIVE_TETHER:Ljava/lang/String;,sdk,system-api,test-api` * `Landroid/net/TetheringManager;->EXTRA_ACTIVE_TETHER:Ljava/lang/String;,sdk,system-api,test-api`
* `Landroid/net/TetheringManager;->EXTRA_ERRORED_TETHER:Ljava/lang/String;,sdk,system-api,test-api` * `Landroid/net/TetheringManager;->EXTRA_ERRORED_TETHER:Ljava/lang/String;,sdk,system-api,test-api`
* (since API 24) `Landroid/net/TetheringManager;->TETHERING_BLUETOOTH:I,sdk,system-api,test-api` * `Landroid/net/TetheringManager;->TETHERING_BLUETOOTH:I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager;->TETHERING_ETHERNET:I,sdk,system-api,test-api` * (since API 30) `Landroid/net/TetheringManager;->TETHERING_ETHERNET:I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager;->TETHERING_NCM:I,sdk,system-api,test-api` * `Landroid/net/TetheringManager;->TETHERING_USB:I,sdk,system-api,test-api`
* (since API 24) `Landroid/net/TetheringManager;->TETHERING_USB:I,sdk,system-api,test-api` * `Landroid/net/TetheringManager;->TETHERING_WIFI:I,sdk,system-api,test-api`
* (since API 24) `Landroid/net/TetheringManager;->TETHERING_WIFI:I,sdk,system-api,test-api` * (since API 31) `Landroid/net/TetheringManager;->TETHERING_WIFI_P2P:I,sdk,system-api,test-api`
* `Landroid/net/TetheringManager;->TETHER_ERROR_*:I,sdk,system-api,test-api` * `Landroid/net/TetheringManager;->TETHER_ERROR_*:I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager;->TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION:I,sdk,system-api,test-api` * (since API 30) `Landroid/net/TetheringManager;->TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION:I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager;->TETHER_HARDWARE_OFFLOAD_FAILED:I,sdk,system-api,test-api` * (since API 30) `Landroid/net/TetheringManager;->TETHER_HARDWARE_OFFLOAD_FAILED:I,sdk,system-api,test-api`
@@ -242,48 +244,63 @@ Greylisted/blacklisted APIs or internal constants: (some constants are hardcoded
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;-><init>()V,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;-><init>()V,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;-><init>(Landroid/net/wifi/SoftApConfiguration;)V,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;-><init>(Landroid/net/wifi/SoftApConfiguration;)V,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->build()Landroid/net/wifi/SoftApConfiguration;,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->build()Landroid/net/wifi/SoftApConfiguration;,sdk,system-api,test-api`
* (since API 33) `Landroid/net/wifi/SoftApConfiguration$Builder;->setAllowedAcsChannels(I[I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setAllowedClientList(Ljava/util/List;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setAllowedClientList(Ljava/util/List;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setAutoShutdownEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setAutoShutdownEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (on API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBand(I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` * (on API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBand(I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBlockedClientList(Ljava/util/List;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBlockedClientList(Ljava/util/List;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBridgedModeOpportunisticShutdownEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` * (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBridgedModeOpportunisticShutdownEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 33) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBridgedModeOpportunisticShutdownTimeoutMillis(J)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBssid(Landroid/net/MacAddress;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBssid(Landroid/net/MacAddress;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (on API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setChannel(II)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` * (on API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setChannel(II)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setChannels(Landroid/util/SparseIntArray;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` * (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setChannels(Landroid/util/SparseIntArray;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setClientControlByUserEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setClientControlByUserEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setHiddenSsid(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setHiddenSsid(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setIeee80211axEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` * (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setIeee80211axEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 33) `Landroid/net/wifi/SoftApConfiguration$Builder;->setIeee80211beEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setMacRandomizationSetting(I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` * (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setMacRandomizationSetting(I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 33) `Landroid/net/wifi/SoftApConfiguration$Builder;->setMaxChannelBandwidth(I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setMaxNumberOfClients(I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setMaxNumberOfClients(I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setPassphrase(Ljava/lang/String;I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setPassphrase(Ljava/lang/String;I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setShutdownTimeoutMillis(J)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setShutdownTimeoutMillis(J)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setSsid(Ljava/lang/String;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` * (since API 30, prior to API 33) `Landroid/net/wifi/SoftApConfiguration$Builder;->setSsid(Ljava/lang/String;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 33) `Landroid/net/wifi/SoftApConfiguration$Builder;->setVendorElements(Ljava/util/List;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 33) `Landroid/net/wifi/SoftApConfiguration$Builder;->setWifiSsid(Landroid/net/wifi/WifiSsid;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->BAND_2GHZ:I,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration;->BAND_2GHZ:I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->BAND_5GHZ:I,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration;->BAND_5GHZ:I,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->BAND_60GHZ:I,sdk,system-api,test-api` * (since API 31) `Landroid/net/wifi/SoftApConfiguration;->BAND_60GHZ:I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->BAND_6GHZ:I,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration;->BAND_6GHZ:I,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->BAND_*:I,sdk,system-api,test-api` * (since API 31) `Landroid/net/wifi/SoftApConfiguration;->BAND_*:I,sdk,system-api,test-api`
* (since API 33) `Landroid/net/wifi/SoftApConfiguration;->DEFAULT_TIMEOUT:J,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->RANDOMIZATION_NONE:I,sdk,system-api,test-api` * (since API 31) `Landroid/net/wifi/SoftApConfiguration;->RANDOMIZATION_NONE:I,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->RANDOMIZATION_NON_PERSISTENT:I,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->RANDOMIZATION_PERSISTENT:I,sdk,system-api,test-api` * (since API 31) `Landroid/net/wifi/SoftApConfiguration;->RANDOMIZATION_PERSISTENT:I,sdk,system-api,test-api`
* (since API 33) `Landroid/net/wifi/SoftApConfiguration;->getAllowedAcsChannels(I)[I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getAllowedClientList()Ljava/util/List;,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getAllowedClientList()Ljava/util/List;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getBand()I,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getBand()I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getBlockedClientList()Ljava/util/List;,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getBlockedClientList()Ljava/util/List;,sdk,system-api,test-api`
* (since API 33) `Landroid/net/wifi/SoftApConfiguration;->getBridgedModeOpportunisticShutdownTimeoutMillis()J,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getChannel()I,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getChannel()I,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->getChannels()Landroid/util/SparseIntArray;,sdk,system-api,test-api` * (since API 31) `Landroid/net/wifi/SoftApConfiguration;->getChannels()Landroid/util/SparseIntArray;,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->getMacRandomizationSetting()I,sdk,system-api,test-api` * (since API 31) `Landroid/net/wifi/SoftApConfiguration;->getMacRandomizationSetting()I,sdk,system-api,test-api`
* (since API 33) `Landroid/net/wifi/SoftApConfiguration;->getMaxChannelBandwidth()I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getMaxNumberOfClients()I,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getMaxNumberOfClients()I,sdk,system-api,test-api`
* (since API 33) `Landroid/net/wifi/SoftApConfiguration;->getPersistentRandomizedMacAddress()Landroid/net/MacAddress;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getShutdownTimeoutMillis()J,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getShutdownTimeoutMillis()J,sdk,system-api,test-api`
* (since API 33) `Landroid/net/wifi/SoftApConfiguration;->getVendorElements()Ljava/util/List;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->isAutoShutdownEnabled()Z,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration;->isAutoShutdownEnabled()Z,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->isBridgedModeOpportunisticShutdownEnabled()Z,sdk,system-api,test-api` * (since API 31) `Landroid/net/wifi/SoftApConfiguration;->isBridgedModeOpportunisticShutdownEnabled()Z,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->isClientControlByUserEnabled()Z,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApConfiguration;->isClientControlByUserEnabled()Z,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->isIeee80211axEnabled()Z,sdk,system-api,test-api` * (since API 31) `Landroid/net/wifi/SoftApConfiguration;->isIeee80211axEnabled()Z,sdk,system-api,test-api`
* (since API 33) `Landroid/net/wifi/SoftApConfiguration;->isIeee80211beEnabled()Z,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->isUserConfiguration()Z,sdk,system-api,test-api` * (since API 31) `Landroid/net/wifi/SoftApConfiguration;->isUserConfiguration()Z,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApInfo;->CHANNEL_WIDTH_*:I,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/SoftApInfo;->CHANNEL_WIDTH_*:I,sdk,system-api,test-api`
* (since API 33) `Landroid/net/wifi/SoftApInfo;->CHANNEL_WIDTH_AUTO:I,sdk,system-api,test-api`
* (on API 30) `Landroid/net/wifi/SoftApInfo;->CHANNEL_WIDTH_INVALID:I,sdk,system-api,test-api` * (on API 30) `Landroid/net/wifi/SoftApInfo;->CHANNEL_WIDTH_INVALID:I,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApInfo;->getAutoShutdownTimeoutMillis()J,sdk,system-api,test-api` * (since API 31) `Landroid/net/wifi/SoftApInfo;->getAutoShutdownTimeoutMillis()J,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApInfo;->getBandwidth()I,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApInfo;->getBssid()Landroid/net/MacAddress;,sdk,system-api,test-api` * (since API 31) `Landroid/net/wifi/SoftApInfo;->getBssid()Landroid/net/MacAddress;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApInfo;->getBandwidth()I,system-api,whitelist` * (since API 30) `Landroid/net/wifi/SoftApInfo;->getFrequency()I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApInfo;->getFrequency()I,system-api,whitelist`
* (since API 31) `Landroid/net/wifi/SoftApInfo;->getWifiStandard()I,sdk,system-api,test-api` * (since API 31) `Landroid/net/wifi/SoftApInfo;->getWifiStandard()I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/WifiClient;->getMacAddress()Landroid/net/MacAddress;,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/WifiClient;->getMacAddress()Landroid/net/MacAddress;,sdk,system-api,test-api`
* (prior to API 30) `Landroid/net/wifi/WifiConfiguration$KeyMgmt;->WPA2_PSK:I,sdk,system-api,test-api` * (prior to API 30) `Landroid/net/wifi/WifiConfiguration$KeyMgmt;->WPA2_PSK:I,sdk,system-api,test-api`
@@ -292,18 +309,26 @@ Greylisted/blacklisted APIs or internal constants: (some constants are hardcoded
* (since API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onConnectedClientsChanged(Ljava/util/List;)V,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onConnectedClientsChanged(Ljava/util/List;)V,sdk,system-api,test-api`
* (on API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onInfoChanged(Landroid/net/wifi/SoftApInfo;)V,sdk,system-api,test-api` * (on API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onInfoChanged(Landroid/net/wifi/SoftApInfo;)V,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/WifiManager$SoftApCallback;->onInfoChanged(Ljava/util/List;)V,sdk,system-api,test-api` * (since API 31) `Landroid/net/wifi/WifiManager$SoftApCallback;->onInfoChanged(Ljava/util/List;)V,sdk,system-api,test-api`
* (since API 28) `Landroid/net/wifi/WifiManager$SoftApCallback;->onStateChanged(II)V,sdk,system-api,test-api` * `Landroid/net/wifi/WifiManager$SoftApCallback;->onStateChanged(II)V,sdk,system-api,test-api`
* `Landroid/net/wifi/WifiManager;->EXTRA_WIFI_AP_FAILURE_REASON:Ljava/lang/String;,sdk,system-api,test-api`
* `Landroid/net/wifi/WifiManager;->EXTRA_WIFI_AP_INTERFACE_NAME:Ljava/lang/String;,sdk,system-api,test-api`
* `Landroid/net/wifi/WifiManager;->EXTRA_WIFI_AP_STATE:Ljava/lang/String;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/WifiManager;->SAP_CLIENT_BLOCK_REASON_CODE_*:I,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/WifiManager;->SAP_CLIENT_BLOCK_REASON_CODE_*:I,sdk,system-api,test-api`
* (since API 28) `Landroid/net/wifi/WifiManager;->SAP_START_FAILURE_*:I,sdk,system-api,test-api` * `Landroid/net/wifi/WifiManager;->SAP_START_FAILURE_*:I,sdk,system-api,test-api`
* (since API 28) `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_FAILED:I,sdk,system-api,test-api` * `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_CHANGED_ACTION:Ljava/lang/String;,sdk,system-api,test-api`
* `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_DISABLED:I,sdk,system-api,test-api`
* `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_DISABLING:I,sdk,system-api,test-api`
* `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_ENABLED:I,sdk,system-api,test-api`
* `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_ENABLING:I,sdk,system-api,test-api`
* `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_FAILED:I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/WifiManager;->getSoftApConfiguration()Landroid/net/wifi/SoftApConfiguration;,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/WifiManager;->getSoftApConfiguration()Landroid/net/wifi/SoftApConfiguration;,sdk,system-api,test-api`
* (prior to API 30) `Landroid/net/wifi/WifiManager;->getWifiApConfiguration()Landroid/net/wifi/WifiConfiguration;,sdk,system-api,test-api` * (prior to API 30) `Landroid/net/wifi/WifiManager;->getWifiApConfiguration()Landroid/net/wifi/WifiConfiguration;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/WifiManager;->isApMacRandomizationSupported()Z,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/WifiManager;->isApMacRandomizationSupported()Z,sdk,system-api,test-api`
* (since API 28) `Landroid/net/wifi/WifiManager;->registerSoftApCallback(Ljava/util/concurrent/Executor;Landroid/net/wifi/WifiManager$SoftApCallback;)V,sdk,system-api,test-api` * `Landroid/net/wifi/WifiManager;->registerSoftApCallback(Ljava/util/concurrent/Executor;Landroid/net/wifi/WifiManager$SoftApCallback;)V,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/WifiManager;->setSoftApConfiguration(Landroid/net/wifi/SoftApConfiguration;)Z,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/WifiManager;->setSoftApConfiguration(Landroid/net/wifi/SoftApConfiguration;)Z,sdk,system-api,test-api`
* (prior to API 30) `Landroid/net/wifi/WifiManager;->setWifiApConfiguration(Landroid/net/wifi/WifiConfiguration;)Z,sdk,system-api,test-api` * (prior to API 30) `Landroid/net/wifi/WifiManager;->setWifiApConfiguration(Landroid/net/wifi/WifiConfiguration;)Z,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/WifiManager;->startLocalOnlyHotspot(Landroid/net/wifi/SoftApConfiguration;Ljava/util/concurrent/Executor;Landroid/net/wifi/WifiManager$LocalOnlyHotspotCallback;)V,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/WifiManager;->startLocalOnlyHotspot(Landroid/net/wifi/SoftApConfiguration;Ljava/util/concurrent/Executor;Landroid/net/wifi/WifiManager$LocalOnlyHotspotCallback;)V,sdk,system-api,test-api`
* (since API 28) `Landroid/net/wifi/WifiManager;->unregisterSoftApCallback(Landroid/net/wifi/WifiManager$SoftApCallback;)V,sdk,system-api,test-api` * `Landroid/net/wifi/WifiManager;->unregisterSoftApCallback(Landroid/net/wifi/WifiManager$SoftApCallback;)V,sdk,system-api,test-api`
* `Landroid/net/wifi/p2p/WifiP2pGroupList;->getGroupList()Ljava/util/List;,sdk,system-api,test-api` * `Landroid/net/wifi/p2p/WifiP2pGroupList;->getGroupList()Ljava/util/List;,sdk,system-api,test-api`
* `Landroid/net/wifi/p2p/WifiP2pManager$PersistentGroupInfoListener;->onPersistentGroupInfoAvailable(Landroid/net/wifi/p2p/WifiP2pGroupList;)V,sdk,system-api,test-api` * `Landroid/net/wifi/p2p/WifiP2pManager$PersistentGroupInfoListener;->onPersistentGroupInfoAvailable(Landroid/net/wifi/p2p/WifiP2pGroupList;)V,sdk,system-api,test-api`
* `Landroid/net/wifi/p2p/WifiP2pManager;->deletePersistentGroup(Landroid/net/wifi/p2p/WifiP2pManager$Channel;ILandroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,sdk,system-api,test-api` * `Landroid/net/wifi/p2p/WifiP2pManager;->deletePersistentGroup(Landroid/net/wifi/p2p/WifiP2pManager$Channel;ILandroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,sdk,system-api,test-api`
@@ -322,20 +347,20 @@ Nonexported system resources:
* (since API 30) `@com.android.networkstack.tethering:array/config_tether_wifi_regexs` * (since API 30) `@com.android.networkstack.tethering:array/config_tether_wifi_regexs`
* (since API 30) `@com.android.networkstack.tethering:array/config_tether_wigig_regexs` * (since API 30) `@com.android.networkstack.tethering:array/config_tether_wigig_regexs`
* (since API 30) `@com.android.wifi.resources:bool/config_wifi_p2p_mac_randomization_supported` * (since API 30) `@com.android.wifi.resources:bool/config_wifi_p2p_mac_randomization_supported`
* (since API 31) `@com.android.wifi.resources:integer/config_wifiFrameworkSoftApShutDownIdleInstanceInBridgedModeTimeoutMillisecond`
* (since API 30) `@com.android.wifi.resources:integer/config_wifiFrameworkSoftApShutDownTimeoutMilliseconds` * (since API 30) `@com.android.wifi.resources:integer/config_wifiFrameworkSoftApShutDownTimeoutMilliseconds`
Other: Activity `com.android.settings/.Settings$TetherSettingsActivity` is assumed to be exported. Other: Activity `com.android.settings/.Settings$TetherSettingsActivity` is assumed to be exported.
For `ip rule` priorities, `RULE_PRIORITY_SECURE_VPN` and `RULE_PRIORITY_TETHERING` is assumed to be 12000 and 18000 respectively; For `ip rule` priorities, `RULE_PRIORITY_SECURE_VPN` and `RULE_PRIORITY_TETHERING` is assumed to be 12000 and 18000 respectively;
(prior to API 24) `RULE_PRIORITY_DEFAULT_NETWORK` is assumed to be 22000 (or at least > 18000).
DHCP server like `dnsmasq` is assumed to run and send DHCP packets as root. DHCP server like `dnsmasq` is assumed to run and send DHCP packets as root.
Undocumented system binaries are all bundled and executable: Undocumented system binaries are all bundled and executable:
* (since API 24) `iptables-save`, `ip6tables-save`; * `iptables-save`, `ip6tables-save`;
* `echo`; * `echo`;
* `/system/bin/ip` (`monitor neigh rule unreachable`); * `/system/bin/ip` (`monitor neigh rule unreachable`);
* `ndc` (`ipfwd` since API 23, `nat` since API 28); * `ndc` (`ipfwd nat`);
* `iptables`, `ip6tables` (with correct version corresponding to API level, `-nvx -L <chain>`); * `iptables`, `ip6tables` (with correct version corresponding to API level, `-nvx -L <chain>`);
* `sh`; * `sh`;
* `su`. * `su`.

View File

@@ -1,31 +1,13 @@
plugins { plugins {
id("com.github.ben-manes.versions") version "0.39.0" id("com.android.application") version "8.0.0-beta04" apply false
id("com.github.ben-manes.versions") version "0.46.0"
id("org.jetbrains.kotlin.android") version "1.8.10" apply false
} }
buildscript { buildscript {
repositories {
google()
mavenCentral()
}
dependencies { dependencies {
classpath(kotlin("gradle-plugin", "1.5.10")) classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.4")
classpath("com.android.tools.build:gradle:7.0.0-beta03") classpath("com.google.android.gms:oss-licenses-plugin:0.10.6")
classpath("com.google.firebase:firebase-crashlytics-gradle:2.6.1") classpath("com.google.gms:google-services:4.3.15")
classpath("com.google.android.gms:oss-licenses-plugin:0.10.4")
classpath("com.google.gms:google-services:4.3.8")
} }
} }
allprojects {
repositories {
google()
jcenter()
mavenCentral()
maven("https://jitpack.io")
}
}
tasks.register<Delete>("clean") {
delete(rootProject.buildDir)
}

View File

@@ -1,4 +1,4 @@
# https://github.com/detekt/detekt/blob/v1.14.2/detekt-core/src/main/resources/default-detekt-config.yml # https://github.com/detekt/detekt/blob/v1.19.0/detekt-core/src/main/resources/default-detekt-config.yml
comments: comments:
active: false active: false
@@ -19,6 +19,16 @@ complexity:
ignoreSingleWhenExpression: false ignoreSingleWhenExpression: false
ignoreSimpleWhenEntries: false ignoreSimpleWhenEntries: false
ignoreNestingFunctions: false ignoreNestingFunctions: false
nestingFunctions:
- 'also'
- 'apply'
- 'forEach'
- 'isNotNull'
- 'ifNull'
- 'let'
- 'run'
- 'use'
- 'with'
LabeledExpression: LabeledExpression:
active: false active: false
LargeClass: LargeClass:
@@ -33,9 +43,11 @@ complexity:
constructorThreshold: 7 constructorThreshold: 7
ignoreDefaultParameters: true ignoreDefaultParameters: true
ignoreDataClasses: true ignoreDataClasses: true
ignoreAnnotated: [] ignoreAnnotatedParameter: []
MethodOverloading: MethodOverloading:
active: false active: false
NamedArguments:
active: false
NestedBlockDepth: NestedBlockDepth:
active: true active: true
threshold: 4 threshold: 4
@@ -64,8 +76,12 @@ coroutines:
active: true active: true
GlobalCoroutineUsage: GlobalCoroutineUsage:
active: false active: false
InjectDispatcher:
active: false
RedundantSuspendModifier: RedundantSuspendModifier:
active: true active: true
SleepInsteadOfDelay:
active: true
SuspendFunWithFlowReturnType: SuspendFunWithFlowReturnType:
active: true active: true
@@ -108,13 +124,19 @@ exceptions:
active: true active: true
ExceptionRaisedInUnexpectedLocation: ExceptionRaisedInUnexpectedLocation:
active: true active: true
methodNames: [toString, hashCode, equals, finalize] methodNames:
- 'equals'
- 'finalize'
- 'hashCode'
- 'toString'
InstanceOfCheckForException: InstanceOfCheckForException:
active: false active: false
NotImplementedDeclaration: NotImplementedDeclaration:
active: true active: true
ObjectExtendsThrowable:
active: true
PrintStackTrace: PrintStackTrace:
active: false active: true
RethrowCaughtException: RethrowCaughtException:
active: false active: false
ReturnFromFinally: ReturnFromFinally:
@@ -123,10 +145,10 @@ exceptions:
SwallowedException: SwallowedException:
active: true active: true
ignoredExceptionTypes: ignoredExceptionTypes:
- InterruptedException - 'InterruptedException'
- NumberFormatException - 'MalformedURLException'
- ParseException - 'NumberFormatException'
- MalformedURLException - 'ParseException'
allowedExceptionNameRegex: '_|(ignore|expected).*' allowedExceptionNameRegex: '_|(ignore|expected).*'
ThrowingExceptionFromFinally: ThrowingExceptionFromFinally:
active: false active: false
@@ -136,9 +158,15 @@ exceptions:
active: true active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
exceptions: exceptions:
- IllegalArgumentException - 'ArrayIndexOutOfBoundsException'
- IllegalStateException - 'Exception'
- IOException - 'IllegalArgumentException'
- 'IllegalMonitorStateException'
- 'IllegalStateException'
- 'IndexOutOfBoundsException'
- 'NullPointerException'
- 'RuntimeException'
- 'Throwable'
ThrowingNewInstanceOfSameException: ThrowingNewInstanceOfSameException:
active: true active: true
TooGenericExceptionCaught: TooGenericExceptionCaught:
@@ -146,10 +174,10 @@ exceptions:
TooGenericExceptionThrown: TooGenericExceptionThrown:
active: true active: true
exceptionNames: exceptionNames:
- Error - 'Error'
- Exception - 'Exception'
- Throwable - 'RuntimeException'
- RuntimeException - 'Throwable'
formatting: formatting:
active: true active: true
@@ -179,11 +207,13 @@ formatting:
ImportOrdering: ImportOrdering:
active: true active: true
autoCorrect: true autoCorrect: true
layout: 'idea' layout: '*,java.**,javax.**,kotlin.**,^'
Indentation: Indentation:
active: false active: false
MaximumLineLength: MaximumLineLength:
active: false active: true
maxLineLength: 120
ignoreBackTickedIdentifier: false
ModifierOrdering: ModifierOrdering:
active: true active: true
autoCorrect: true autoCorrect: true
@@ -229,6 +259,9 @@ formatting:
autoCorrect: true autoCorrect: true
ParameterListWrapping: ParameterListWrapping:
active: false active: false
SpacingAroundAngleBrackets:
active: true
autoCorrect: true
SpacingAroundColon: SpacingAroundColon:
active: true active: true
autoCorrect: true autoCorrect: true
@@ -256,6 +289,9 @@ formatting:
SpacingAroundRangeOperator: SpacingAroundRangeOperator:
active: true active: true
autoCorrect: true autoCorrect: true
SpacingAroundUnaryOperator:
active: true
autoCorrect: true
SpacingBetweenDeclarationsWithAnnotations: SpacingBetweenDeclarationsWithAnnotations:
active: false active: false
SpacingBetweenDeclarationsWithComments: SpacingBetweenDeclarationsWithComments:
@@ -266,6 +302,8 @@ formatting:
naming: naming:
active: true active: true
BooleanPropertyNaming:
active: false
ClassNaming: ClassNaming:
active: true active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
@@ -297,7 +335,6 @@ naming:
functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)' functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)'
excludeClassPattern: '$^' excludeClassPattern: '$^'
ignoreOverridden: true ignoreOverridden: true
ignoreAnnotated: ['Composable']
FunctionParameterNaming: FunctionParameterNaming:
active: true active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
@@ -307,11 +344,17 @@ naming:
InvalidPackageDeclaration: InvalidPackageDeclaration:
active: true active: true
rootPackage: '' rootPackage: ''
LambdaParameterNaming:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
parameterPattern: '[a-z][A-Za-z0-9]*|_'
MatchingDeclarationName: MatchingDeclarationName:
active: true active: true
mustBeFirst: true mustBeFirst: true
MemberNameEqualsClassName: MemberNameEqualsClassName:
active: false active: false
NoNameShadowing:
active: true
NonBooleanPropertyPrefixedWithIs: NonBooleanPropertyPrefixedWithIs:
active: true active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
@@ -327,7 +370,7 @@ naming:
packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
TopLevelPropertyNaming: TopLevelPropertyNaming:
active: true active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] excludes: ['buildSrc/**', '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
constantPattern: '[A-Z][_A-Z0-9]*' constantPattern: '[A-Z][_A-Z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*' propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
@@ -359,14 +402,26 @@ performance:
potential-bugs: potential-bugs:
active: true active: true
AvoidReferentialEquality:
active: true
forbiddenTypePatterns:
- 'kotlin.String'
CastToNullableType:
active: false
Deprecation: Deprecation:
active: true active: true
DontDowncastCollectionTypes:
active: true
DoubleMutabilityForCollection:
active: true
DuplicateCaseInWhenExpression: DuplicateCaseInWhenExpression:
active: true active: true
EqualsAlwaysReturnsTrueOrFalse: EqualsAlwaysReturnsTrueOrFalse:
active: true active: true
EqualsWithHashCodeExist: EqualsWithHashCodeExist:
active: true active: true
ExitOutsideMain:
active: true
ExplicitGarbageCollectionCall: ExplicitGarbageCollectionCall:
active: true active: true
HasPlatformType: HasPlatformType:
@@ -374,7 +429,11 @@ potential-bugs:
IgnoredReturnValue: IgnoredReturnValue:
active: true active: true
restrictToAnnotatedMethods: true restrictToAnnotatedMethods: true
returnValueAnnotations: ['*.CheckReturnValue', '*.CheckResult'] returnValueAnnotations:
- '*.CheckResult'
- '*.CheckReturnValue'
ignoreReturnValueAnnotations:
- '*.CanIgnoreReturnValue'
ImplicitDefaultLocale: ImplicitDefaultLocale:
active: true active: true
ImplicitUnitReturnType: ImplicitUnitReturnType:
@@ -389,6 +448,9 @@ potential-bugs:
active: false active: false
MapGetWithNotNullAssertionOperator: MapGetWithNotNullAssertionOperator:
active: true active: true
MissingPackageDeclaration:
active: true
excludes: ['buildSrc/**', '**/*.kts']
MissingWhenCase: MissingWhenCase:
active: false active: false
NullableToStringCall: NullableToStringCall:
@@ -398,15 +460,20 @@ potential-bugs:
UnconditionalJumpStatementInLoop: UnconditionalJumpStatementInLoop:
active: true active: true
UnnecessaryNotNullOperator: UnnecessaryNotNullOperator:
active: false active: true
UnnecessarySafeCall: UnnecessarySafeCall:
active: false active: true
UnreachableCatchBlock:
active: true
UnreachableCode: UnreachableCode:
active: true active: true
UnsafeCallOnNullableType: UnsafeCallOnNullableType:
active: true active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
UnsafeCast: UnsafeCast:
active: false active: true
UnusedUnaryOperator:
active: true
UselessPostfixExpression: UselessPostfixExpression:
active: true active: true
WrongEqualsTypeParameter: WrongEqualsTypeParameter:
@@ -422,6 +489,8 @@ style:
active: false active: false
DataClassShouldBeImmutable: DataClassShouldBeImmutable:
active: false active: false
DestructuringDeclarationWithTooManyEntries:
active: false
EqualsNullCall: EqualsNullCall:
active: true active: true
EqualsOnSignatureLine: EqualsOnSignatureLine:
@@ -435,18 +504,27 @@ style:
includeLineWrapping: false includeLineWrapping: false
ForbiddenComment: ForbiddenComment:
active: true active: true
values: ['TODO:', 'FIXME:', 'STOPSHIP:'] values:
- 'FIXME:'
- 'STOPSHIP:'
- 'TODO:'
allowedPatterns: '' allowedPatterns: ''
customMessage: ''
ForbiddenImport: ForbiddenImport:
active: true active: true
imports: [] imports: []
forbiddenPatterns: '' forbiddenPatterns: ''
ForbiddenMethodCall: ForbiddenMethodCall:
active: true active: true
methods: ['kotlin.io.println', 'kotlin.io.print'] methods:
- 'kotlin.io.print'
- 'kotlin.io.println'
ForbiddenPublicDataClass: ForbiddenPublicDataClass:
active: true active: true
ignorePackages: ['*.internal', '*.internal.*'] excludes: ['**']
ignorePackages:
- '*.internal'
- '*.internal.*'
ForbiddenVoid: ForbiddenVoid:
active: true active: true
ignoreOverridden: true ignoreOverridden: true
@@ -454,12 +532,14 @@ style:
FunctionOnlyReturningConstant: FunctionOnlyReturningConstant:
active: true active: true
ignoreOverridableFunction: true ignoreOverridableFunction: true
excludedFunctions: 'describeContents' ignoreActualFunction: true
excludeAnnotatedFunction: ['dagger.Provides'] excludedFunctions: ''
LibraryCodeMustSpecifyReturnType: LibraryCodeMustSpecifyReturnType:
active: true active: true
excludes: ['**']
LibraryEntitiesShouldNotBePublic: LibraryEntitiesShouldNotBePublic:
active: true active: true
excludes: ['**']
LoopWithTooManyJumpStatements: LoopWithTooManyJumpStatements:
active: true active: true
maxJumpCount: 1 maxJumpCount: 1
@@ -479,12 +559,16 @@ style:
active: true active: true
ModifierOrder: ModifierOrder:
active: true active: true
MultilineLambdaItParameter:
active: false
NestedClassesVisibility: NestedClassesVisibility:
active: true active: true
NewLineAtEndOfFile: NewLineAtEndOfFile:
active: true active: true
NoTabs: NoTabs:
active: true active: true
ObjectLiteralToLambda:
active: true
OptionalAbstractKeyword: OptionalAbstractKeyword:
active: true active: true
OptionalUnit: OptionalUnit:
@@ -497,6 +581,8 @@ style:
active: true active: true
RedundantExplicitType: RedundantExplicitType:
active: true active: true
RedundantHigherOrderMapUsage:
active: true
RedundantVisibilityModifierRule: RedundantVisibilityModifierRule:
active: true active: true
ReturnCount: ReturnCount:
@@ -510,6 +596,7 @@ style:
ThrowsCount: ThrowsCount:
active: true active: true
max: 2 max: 2
excludeGuardClauses: false
TrailingWhitespace: TrailingWhitespace:
active: true active: true
UnderscoresInNumericLiterals: UnderscoresInNumericLiterals:
@@ -520,6 +607,8 @@ style:
active: true active: true
UnnecessaryApply: UnnecessaryApply:
active: true active: true
UnnecessaryFilter:
active: true
UnnecessaryInheritance: UnnecessaryInheritance:
active: true active: true
UnnecessaryLet: UnnecessaryLet:
@@ -535,6 +624,8 @@ style:
UnusedPrivateMember: UnusedPrivateMember:
active: true active: true
allowedNames: '(_|ignored|expected|serialVersionUID)' allowedNames: '(_|ignored|expected|serialVersionUID)'
UseAnyOrNoneInsteadOfFind:
active: true
UseArrayLiteralsInAnnotations: UseArrayLiteralsInAnnotations:
active: true active: true
UseCheckNotNull: UseCheckNotNull:
@@ -545,8 +636,14 @@ style:
active: false active: false
UseEmptyCounterpart: UseEmptyCounterpart:
active: true active: true
UseIfEmptyOrIfBlank:
active: true
UseIfInsteadOfWhen: UseIfInsteadOfWhen:
active: false active: false
UseIsNullOrEmpty:
active: true
UseOrEmpty:
active: true
UseRequire: UseRequire:
active: true active: true
UseRequireNotNull: UseRequireNotNull:

View File

@@ -13,6 +13,7 @@ android.databinding.incremental=true
android.enableJetifier=true android.enableJetifier=true
android.enableR8.fullMode=true android.enableR8.fullMode=true
android.enableResourceOptimizations=false android.enableResourceOptimizations=false
android.nonTransitiveRClass=true
android.useAndroidX=true android.useAndroidX=true
kapt.incremental.apt=true kapt.incremental.apt=true
org.gradle.jvmargs=-Xmx1536m org.gradle.jvmargs=-Xmx1536m

Binary file not shown.

View File

@@ -1,5 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

285
gradlew vendored
View File

@@ -1,7 +1,7 @@
#!/usr/bin/env sh #!/bin/sh
# #
# Copyright 2015 the original author or authors. # Copyright © 2015-2021 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -17,67 +17,101 @@
# #
############################################################################## ##############################################################################
## #
## Gradle start up script for UN*X # Gradle start up script for POSIX generated by Gradle.
## #
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
############################################################################## ##############################################################################
# Attempt to set APP_HOME # Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle" # Resolve links: $0 may be a link
APP_BASE_NAME=`basename "$0"` app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum" MAX_FD=maximum
warn () { warn () {
echo "$*" echo "$*"
} } >&2
die () { die () {
echo echo
echo "$*" echo "$*"
echo echo
exit 1 exit 1
} } >&2
# OS specific support (must be 'true' or 'false'). # OS specific support (must be 'true' or 'false').
cygwin=false cygwin=false
msys=false msys=false
darwin=false darwin=false
nonstop=false nonstop=false
case "`uname`" in case "$( uname )" in #(
CYGWIN* ) CYGWIN* ) cygwin=true ;; #(
cygwin=true Darwin* ) darwin=true ;; #(
;; MSYS* | MINGW* ) msys=true ;; #(
Darwin* ) NONSTOP* ) nonstop=true ;;
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables # IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java" JAVACMD=$JAVA_HOME/jre/sh/java
else else
JAVACMD="$JAVA_HOME/bin/java" JAVACMD=$JAVA_HOME/bin/java
fi fi
if [ ! -x "$JAVACMD" ] ; then if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi fi
else else
JAVACMD="java" JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
@@ -106,80 +140,105 @@ location of your Java installation."
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
MAX_FD_LIMIT=`ulimit -H -n` case $MAX_FD in #(
if [ $? -eq 0 ] ; then max*)
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
MAX_FD="$MAX_FD_LIMIT" # shellcheck disable=SC3045
fi MAX_FD=$( ulimit -H -n ) ||
ulimit -n $MAX_FD warn "Could not query maximum file descriptor limit"
if [ $? -ne 0 ] ; then esac
warn "Could not set maximum file descriptor limit: $MAX_FD" case $MAX_FD in #(
fi '' | soft) :;; #(
else *)
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
fi # shellcheck disable=SC3045
fi ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac esac
fi fi
# Escape application args # Collect all arguments for the java command, stacking in reverse order:
save () { # * args from the command line
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done # * the main class name
echo " " # * -classpath
} # * -D...appname settings
APP_ARGS=`save "$@"` # * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules # For Cygwin or MSYS, switch paths to Windows format before running java
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@" exec "$JAVACMD" "$@"

11
gradlew.bat vendored
View File

@@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd if %ERRORLEVEL% equ 0 goto mainEnd
:fail :fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code! rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 set EXIT_CODE=%ERRORLEVEL%
exit /b 1 if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal

View File

@@ -9,23 +9,24 @@ plugins {
} }
android { android {
val javaVersion = JavaVersion.VERSION_1_8 namespace = "be.mygod.vpnhotspot"
val targetSdk = 29
buildToolsVersion = "31.0.0-rc4" val javaVersion = 11
buildToolsVersion = "33.0.2"
compileOptions { compileOptions {
isCoreLibraryDesugaringEnabled = true isCoreLibraryDesugaringEnabled = true
sourceCompatibility = javaVersion sourceCompatibility(javaVersion)
targetCompatibility = javaVersion targetCompatibility(javaVersion)
} }
compileSdkPreview = "android-S" kotlin.jvmToolchain(javaVersion)
kotlinOptions.jvmTarget = javaVersion.toString() compileSdk = 33
defaultConfig { defaultConfig {
applicationId = "be.mygod.vpnhotspot" applicationId = "be.mygod.vpnhotspot"
minSdk = 21 minSdk = 28
if (targetSdk == 31) targetSdkPreview = "S" else this.targetSdk = targetSdk targetSdk = 33
resourceConfigurations.addAll(arrayOf("it", "ru", "zh-rCN", "zh-rTW")) resourceConfigurations.addAll(arrayOf("it", "pt-rBR", "ru", "zh-rCN", "zh-rTW"))
versionCode = 260 versionCode = 1000
versionName = "2.11.7" versionName = "2.16.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions.annotationProcessorOptions.arguments.apply { javaCompileOptions.annotationProcessorOptions.arguments.apply {
put("room.expandProjection", "true") put("room.expandProjection", "true")
@@ -33,9 +34,9 @@ android {
put("room.schemaLocation", "$projectDir/schemas") put("room.schemaLocation", "$projectDir/schemas")
} }
buildConfigField("boolean", "DONATIONS", "true") buildConfigField("boolean", "DONATIONS", "true")
buildConfigField("int", "TARGET_SDK", targetSdk.toString())
} }
buildFeatures { buildFeatures {
buildConfig = true
dataBinding = true dataBinding = true
viewBinding = true viewBinding = true
} }
@@ -57,6 +58,7 @@ android {
} }
create("google") { create("google") {
dimension = "freedom" dimension = "freedom"
versionNameSuffix = "-g"
buildConfigField("boolean", "DONATIONS", "false") buildConfigField("boolean", "DONATIONS", "false")
} }
} }
@@ -64,37 +66,39 @@ android {
} }
dependencies { dependencies {
val lifecycleVersion = "2.3.1" val lifecycleVersion = "2.6.0-rc01"
val roomVersion = "2.3.0" val roomVersion = "2.5.0"
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.2")
kapt("androidx.room:room-compiler:$roomVersion") kapt("androidx.room:room-compiler:$roomVersion")
implementation(kotlin("stdlib-jdk8")) implementation(kotlin("stdlib-jdk8"))
implementation("androidx.appcompat:appcompat:1.3.0") // https://issuetracker.google.com/issues/151603528 implementation("androidx.browser:browser:1.5.0")
implementation("androidx.browser:browser:1.3.0") implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.core:core-ktx:1.6.0-beta01") implementation("androidx.fragment:fragment-ktx:1.5.5")
implementation("androidx.emoji:emoji:1.1.0")
implementation("androidx.fragment:fragment-ktx:1.3.4")
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
implementation("androidx.preference:preference:1.1.1") implementation("androidx.preference:preference:1.2.0")
implementation("androidx.room:room-ktx:$roomVersion") implementation("androidx.room:room-ktx:$roomVersion")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("com.android.billingclient:billing-ktx:4.0.0") implementation("be.mygod.librootkotlinx:librootkotlinx:1.0.1")
implementation("com.android.billingclient:billing-ktx:5.1.0")
implementation("com.github.tiann:FreeReflection:3.1.0")
implementation("com.google.android.gms:play-services-base:18.2.0") // fix for GoogleApiActivity crash
implementation("com.google.android.gms:play-services-oss-licenses:17.0.0") implementation("com.google.android.gms:play-services-oss-licenses:17.0.0")
implementation("com.google.android.material:material:1.4.0-beta01") implementation("com.google.android.material:material:1.8.0")
implementation("com.google.firebase:firebase-analytics-ktx:19.0.0") implementation("com.google.firebase:firebase-analytics-ktx:21.2.0")
implementation("com.google.firebase:firebase-crashlytics:18.0.0") implementation("com.google.firebase:firebase-crashlytics:18.3.5")
implementation("com.google.zxing:core:3.4.1") implementation("com.google.zxing:core:3.5.1")
implementation("com.jakewharton.timber:timber:4.7.1") implementation("com.jakewharton.timber:timber:5.0.1")
implementation("com.linkedin.dexmaker:dexmaker:2.28.1") implementation("com.linkedin.dexmaker:dexmaker:2.28.3")
implementation("com.takisoft.preferencex:preferencex-simplemenu:1.1.0") implementation("com.takisoft.preferencex:preferencex-simplemenu:1.1.0")
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.4") implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
add("googleImplementation", "com.google.android.play:core:1.10.3")
add("googleImplementation", "com.google.android.play:core-ktx:1.8.1")
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.room:room-testing:$roomVersion") androidTestImplementation("androidx.room:room-testing:$roomVersion")
androidTestImplementation("androidx.test:runner:1.3.0") androidTestImplementation("androidx.test:runner:1.5.2")
androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.ext:junit-ktx:1.1.2") androidTestImplementation("androidx.test.ext:junit-ktx:1.1.5")
} }

View File

@@ -4,5 +4,6 @@
<ignore regexp="org.mockito.*" /> <ignore regexp="org.mockito.*" />
</issue> </issue>
<issue id="MissingTranslation" severity="ignore" /> <issue id="MissingTranslation" severity="ignore" />
<issue id="NewApi" severity="warning" />
<issue id="UseAppTint" severity="informational" /> <issue id="UseAppTint" severity="informational" />
</lint> </lint>

View File

@@ -20,10 +20,3 @@
# If you keep the line number information, uncomment this to # If you keep the line number information, uncomment this to
# hide the original source file name. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
-if public class be.mygod.librootkotlinx.RootServer {
private void doInit(android.content.Context, java.lang.String);
}
-keep class be.mygod.librootkotlinx.RootServer {
public static void main(java.lang.String[]);
}

View File

@@ -34,10 +34,10 @@
} }
], ],
"primaryKey": { "primaryKey": {
"autoGenerate": false,
"columnNames": [ "columnNames": [
"mac" "mac"
], ]
"autoGenerate": false
}, },
"indices": [], "indices": [],
"foreignKeys": [] "foreignKeys": []
@@ -114,10 +114,10 @@
} }
], ],
"primaryKey": { "primaryKey": {
"autoGenerate": true,
"columnNames": [ "columnNames": [
"id" "id"
], ]
"autoGenerate": true
}, },
"indices": [ "indices": [
{ {
@@ -126,7 +126,8 @@
"columnNames": [ "columnNames": [
"previousId" "previousId"
], ],
"createSql": "CREATE UNIQUE INDEX `index_TrafficRecord_previousId` ON `${TABLE_NAME}` (`previousId`)" "orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TrafficRecord_previousId` ON `${TABLE_NAME}` (`previousId`)"
} }
], ],
"foreignKeys": [ "foreignKeys": [
@@ -144,9 +145,10 @@
] ]
} }
], ],
"views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"92a6c0406ed7265dbd98eb3c24095651\")" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '92a6c0406ed7265dbd98eb3c24095651')"
] ]
} }
} }

View File

@@ -0,0 +1,111 @@
package be.mygod.vpnhotspot.util
import android.app.Activity
import android.net.Uri
import androidx.core.content.edit
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.BuildConfig
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.flow.flow
import org.json.JSONArray
import timber.log.Timber
import java.io.IOException
import java.time.Instant
import java.util.concurrent.CancellationException
import java.util.concurrent.TimeUnit
import kotlin.math.max
import kotlin.math.min
object UpdateChecker {
private const val KEY_LAST_FETCHED = "update.lastFetched"
private const val KEY_VERSION = "update.version"
private const val KEY_PUBLISHED = "update.published"
private const val UPDATE_INTERVAL = 1000 * 60 * 60 * 6
private data class GitHubUpdate(override val message: String, val published: Long) : AppUpdate {
override val stalenessDays get() = max(0,
TimeUnit.DAYS.convert(System.currentTimeMillis() - published, TimeUnit.MILLISECONDS)).toInt()
override fun updateForResult(activity: Activity, requestCode: Int) {
app.customTabsIntent.launchUrl(activity, Uri.parse("https://github.com/Mygod/VPNHotspot/releases"))
}
}
private data class SemVer(val major: Int, val minor: Int, val revision: Int) : Comparable<SemVer> {
override fun compareTo(other: SemVer): Int {
var result = major - other.major
if (result != 0) return result
result = minor - other.minor
if (result != 0) return result
return revision - other.revision
}
}
private val semverParser = "^v?(\\d+)\\.(\\d+)\\.(\\d+)(?:-|$)".toPattern()
private fun CharSequence.toSemVer() = semverParser.matcher(this).let { matcher ->
require(matcher.find()) { "Unrecognized version $this" }
SemVer(matcher.group(1)!!.toInt(), matcher.group(2)!!.toInt(), matcher.group(3)!!.toInt())
}
private val myVer = BuildConfig.VERSION_NAME.toSemVer()
private fun findUpdate(response: JSONArray): GitHubUpdate? {
var latest: String? = null
var latestVer = myVer
var earliest = Long.MAX_VALUE
for (i in 0 until response.length()) {
val obj = response.getJSONObject(i)
val name = obj.getString("name")
val semver = try {
name.toSemVer()
} catch (e: IllegalArgumentException) {
Timber.w(e)
continue
}
if (semver <= myVer) continue
if (semver > latestVer) {
latest = name
latestVer = semver
}
earliest = min(earliest, Instant.parse(obj.getString("published_at")).toEpochMilli())
}
return latest?.let { GitHubUpdate(it, earliest) }
}
fun check() = flow<AppUpdate?> {
emit(app.pref.getString(KEY_VERSION, null)?.let {
if (myVer >= it.toSemVer()) null else GitHubUpdate(it, app.pref.getLong(KEY_PUBLISHED, -1))
})
while (true) {
val now = System.currentTimeMillis()
val lastFetched = app.pref.getLong(KEY_LAST_FETCHED, -1)
if (lastFetched in 0..now) delay(lastFetched + UPDATE_INTERVAL - now)
currentCoroutineContext().ensureActive()
var reset: Long? = null
app.pref.edit {
try {
val update = findUpdate(JSONArray(connectCancellable(
"https://api.github.com/repos/Mygod/VPNHotspot/releases?per_page=100") { conn ->
conn.setRequestProperty("Accept", "application/vnd.github.v3+json")
reset = conn.getHeaderField("X-RateLimit-Reset")?.toLongOrNull()
conn.inputStream.bufferedReader().readText()
}))
putString(KEY_VERSION, update?.let {
putLong(KEY_PUBLISHED, update.published)
it.message
})
emit(update)
} catch (_: CancellationException) {
return@flow
} catch (e: IOException) {
Timber.d(e)
} catch (e: Exception) {
Timber.w(e)
} finally {
putLong(KEY_LAST_FETCHED, System.currentTimeMillis())
}
}
reset?.let { delay(System.currentTimeMillis() - it * 1000) }
}
}.cancellable()
}

View File

@@ -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)
}
}
}

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools">
package="be.mygod.vpnhotspot">
<uses-feature <uses-feature
android:name="android.hardware.bluetooth" android:name="android.hardware.bluetooth"
@@ -9,9 +8,6 @@
<uses-feature <uses-feature
android:name="android.hardware.ethernet" android:name="android.hardware.ethernet"
android:required="false"/> android:required="false"/>
<uses-feature
android:name="android.software.leanback"
android:required="false"/>
<uses-feature <uses-feature
android:name="android.hardware.touchscreen" android:name="android.hardware.touchscreen"
android:required="false"/> android:required="false"/>
@@ -24,11 +20,15 @@
<uses-feature <uses-feature
android:name="android.hardware.wifi.direct" android:name="android.hardware.wifi.direct"
android:required="false"/> android:required="false"/>
<uses-feature
android:name="android.software.leanback"
android:required="false"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
@@ -37,8 +37,11 @@
tools:ignore="ProtectedPermissions" /> tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.MANAGE_USB" <uses-permission android:name="android.permission.MANAGE_USB"
tools:ignore="ProtectedPermissions"/> tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"
android:usesPermissionFlags="neverForLocation"/>
<uses-permission android:name="android.permission.OVERRIDE_WIFI_CONFIG" <uses-permission android:name="android.permission.OVERRIDE_WIFI_CONFIG"
tools:ignore="ProtectedPermissions"/> tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.READ_WIFI_CREDENTIAL" <uses-permission android:name="android.permission.READ_WIFI_CREDENTIAL"
tools:ignore="ProtectedPermissions"/> tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
@@ -49,11 +52,22 @@
tools:ignore="ProtectedPermissions"/> tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.permission.WRITE_SETTINGS" <uses-permission android:name="android.permission.WRITE_SETTINGS"
tools:ignore="ProtectedPermissions"/> tools:ignore="ProtectedPermissions"/>
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"
<!-- Required since API 29 --> android:maxSdkVersion="32"/>
<uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION"
<!-- Required since API 31 --> android:maxSdkVersion="32"/>
<uses-permission-sdk-23 android:name="android.permission.BLUETOOTH_CONNECT"/>
<queries>
<intent>
<action android:name="android.net.ITetheringConnector" />
</intent>
<intent>
<action android:name="android.net.ITetheringConnector.InProcess" />
</intent>
<intent>
<action android:name="com.android.server.wifi.intent.action.SERVICE_WIFI_RESOURCES_APK" />
</intent>
</queries>
<application <application
android:name=".App" android:name=".App"
@@ -63,7 +77,8 @@
android:banner="@mipmap/banner" android:banner="@mipmap/banner"
android:hasFragileUserData="true" android:hasFragileUserData="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round" android:localeConfig="@xml/locales_config"
android:enableOnBackInvokedCallback="true"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning"> tools:ignore="GoogleAppIndexingWarning">
@@ -71,7 +86,6 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:label="@string/app_name"
android:launchMode="singleTask" android:launchMode="singleTask"
android:windowSoftInputMode="stateAlwaysHidden"> android:windowSoftInputMode="stateAlwaysHidden">
<intent-filter> <intent-filter>
@@ -88,8 +102,7 @@
<service <service
android:name=".LocalOnlyHotspotService" android:name=".LocalOnlyHotspotService"
android:directBootAware="true" android:directBootAware="true"
android:foregroundServiceType="location|connectedDevice" android:foregroundServiceType="location|connectedDevice"/>
tools:targetApi="26"/>
<service <service
android:name=".RepeaterService" android:name=".RepeaterService"
android:directBootAware="true" android:directBootAware="true"
@@ -105,8 +118,7 @@
android:exported="true" android:exported="true"
android:icon="@drawable/ic_action_settings_input_antenna" android:icon="@drawable/ic_action_settings_input_antenna"
android:label="@string/title_repeater" android:label="@string/title_repeater"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE" android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
tools:targetApi="24">
<intent-filter> <intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" /> <action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter> </intent-filter>
@@ -117,12 +129,10 @@
<service <service
android:name=".manage.LocalOnlyHotspotTileService" android:name=".manage.LocalOnlyHotspotTileService"
android:directBootAware="true" android:directBootAware="true"
android:enabled="@bool/api_ge_26"
android:exported="true" android:exported="true"
android:icon="@drawable/ic_action_perm_scan_wifi" android:icon="@drawable/ic_action_perm_scan_wifi"
android:label="@string/tethering_temp_hotspot" android:label="@string/tethering_temp_hotspot"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE" android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
tools:targetApi="26">
<intent-filter> <intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" /> <action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter> </intent-filter>
@@ -136,11 +146,13 @@
android:exported="true" android:exported="true"
android:icon="@drawable/ic_device_wifi_tethering" android:icon="@drawable/ic_device_wifi_tethering"
android:label="@string/tethering_manage_wifi" android:label="@string/tethering_manage_wifi"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE" android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
tools:targetApi="24">
<intent-filter> <intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" /> <action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter> </intent-filter>
<meta-data
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
</service> </service>
<service <service
android:name=".manage.TetheringTileService$Usb" android:name=".manage.TetheringTileService$Usb"
@@ -148,11 +160,13 @@
android:exported="true" android:exported="true"
android:icon="@drawable/ic_device_usb" android:icon="@drawable/ic_device_usb"
android:label="@string/tethering_manage_usb" android:label="@string/tethering_manage_usb"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE" android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
tools:targetApi="24">
<intent-filter> <intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" /> <action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter> </intent-filter>
<meta-data
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
</service> </service>
<service <service
android:name=".manage.TetheringTileService$Bluetooth" android:name=".manage.TetheringTileService$Bluetooth"
@@ -160,11 +174,13 @@
android:exported="true" android:exported="true"
android:icon="@drawable/ic_device_bluetooth" android:icon="@drawable/ic_device_bluetooth"
android:label="@string/tethering_manage_bluetooth" android:label="@string/tethering_manage_bluetooth"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE" android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
tools:targetApi="24">
<intent-filter> <intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" /> <action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter> </intent-filter>
<meta-data
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
</service> </service>
<service <service
android:name=".manage.TetheringTileService$Ethernet" android:name=".manage.TetheringTileService$Ethernet"
@@ -178,46 +194,9 @@
<intent-filter> <intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" /> <action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter> </intent-filter>
</service> <meta-data
<service android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:name=".manage.TetheringTileService$Ncm" android:value="true" />
android:directBootAware="true"
android:enabled="@bool/api_ge_30"
android:exported="true"
android:icon="@drawable/ic_action_settings_ethernet"
android:label="@string/tethering_manage_ncm"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
tools:targetApi="30">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".manage.TetheringTileService$WiGig"
android:directBootAware="true"
android:enabled="@bool/api_ge_30"
android:exported="true"
android:icon="@drawable/ic_image_flash_on"
android:label="@string/tethering_manage_wigig"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
tools:targetApi="30">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<!--suppress DeprecatedClassUsageInspection -->
<service
android:name=".manage.TetheringTileService$WifiLegacy"
android:directBootAware="true"
android:enabled="@bool/api_lt_25"
android:exported="true"
android:icon="@drawable/ic_device_wifi_tethering"
android:label="@string/tethering_manage_wifi_legacy"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
tools:targetApi="24">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service> </service>
<receiver <receiver
@@ -228,6 +207,7 @@
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" /> <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter> </intent-filter>
</receiver> </receiver>

View File

@@ -1,124 +0,0 @@
package be.mygod.librootkotlinx
import android.os.Build
import android.os.Debug
import android.os.Process
import androidx.annotation.RequiresApi
import java.io.File
import java.io.IOException
object AppProcess {
/**
* Based on: https://android.googlesource.com/platform/bionic/+/aff9a34/linker/linker.cpp#3397
*/
@get:RequiresApi(28)
val genericLdConfigFilePath: String get() {
"/system/etc/ld.config.$currentInstructionSet.txt".let { if (File(it).isFile) return it }
if (Build.VERSION.SDK_INT >= 30) "/linkerconfig/ld.config.txt".let {
if (File(it).isFile) return it
Logger.me.w("Failed to find generated linker configuration from \"$it\"")
}
if (isVndkLite) {
"/system/etc/ld.config.vndk_lite.txt".let { if (File(it).isFile) return it }
} else when (vndkVersion) {
"", "current" -> { }
else -> "/system/etc/ld.config.$vndkVersion.txt".let { if (File(it).isFile) return it }
}
return "/system/etc/ld.config.txt"
}
/**
* Based on: https://android.googlesource.com/platform/bionic/+/30f2f05/linker/linker_config.cpp#182
*/
@RequiresApi(26)
fun findLinkerSection(lines: Sequence<String>, binaryRealPath: String): String {
for (untrimmed in lines) {
val line = untrimmed.substringBefore('#').trim()
if (line.isEmpty()) continue
if (line[0] == '[' && line.last() == ']') break
if (line.contains("+=")) continue
val chunks = line.split('=', limit = 2)
if (chunks.size < 2) {
Logger.me.w("warning: couldn't parse invalid format: $line (ignoring this line)")
continue
}
var (name, value) = chunks.map { it.trim() }
if (!name.startsWith("dir.")) {
Logger.me.w("warning: unexpected property name \"$name\", " +
"expected format dir.<section_name> (ignoring this line)")
continue
}
if (value.endsWith('/')) value = value.dropLast(1)
if (value.isEmpty()) {
Logger.me.w("warning: property value is empty (ignoring this line)")
continue
}
try {
value = File(value).canonicalPath
} catch (e: IOException) {
Logger.me.i("warning: path \"$value\" couldn't be resolved: ${e.message}")
}
if (binaryRealPath.startsWith(value) && binaryRealPath[value.length] == '/') return name.substring(4)
}
throw IllegalArgumentException("No valid linker section found")
}
val myExe get() = "/proc/${Process.myPid()}/exe"
val myExeCanonical get() = try {
File("/proc/self/exe").canonicalPath
} catch (e: IOException) {
Logger.me.i("warning: couldn't resolve self exe: ${e.message}")
"/system/bin/app_process"
}
/**
* To workaround Samsung's stupid kernel patch that prevents exec, we need to relocate exe outside of /data.
* See also: https://github.com/Chainfire/librootjava/issues/19
*
* @return The script to be executed to perform relocation and the relocated binary path.
*/
fun relocateScript(token: String): Pair<StringBuilder, String> {
val script = StringBuilder()
val (baseDir, relocated) = if (Build.VERSION.SDK_INT < 29) "/dev" to "/dev/app_process_$token" else {
val apexPath = "/apex/$token"
script.appendLine("[ -d $apexPath ] || " +
"mkdir $apexPath && " +
// we need to mount a new tmpfs to override noexec flag
"mount -t tmpfs -o size=1M tmpfs $apexPath || exit 1")
// unfortunately native ld.config.txt only recognizes /data,/system,/system_ext as system directories;
// to link correctly, we need to add our path to the linker config too
val ldConfig = "$apexPath/etc/ld.config.txt"
val masterLdConfig = genericLdConfigFilePath
val section = try {
File(masterLdConfig).useLines { findLinkerSection(it, myExeCanonical) }
} catch (e: Exception) {
Logger.me.w("Failed to locate system section", e)
"system"
}
script.appendLine("[ -f $ldConfig ] || " +
"mkdir -p $apexPath/etc && " +
"echo dir.$section = $apexPath >$ldConfig && " +
"cat $masterLdConfig >>$ldConfig || exit 1")
"$apexPath/bin" to "$apexPath/bin/app_process"
}
script.appendLine("[ -f $relocated ] || " +
"mkdir -p $baseDir && " +
"cp $myExe $relocated && " +
"chmod 700 $relocated || exit 1")
return script to relocated
}
/**
* Compute the shell script line that exec into the corresponding [clazz].
* Extra params can be simply appended to the string.
*/
fun launchString(packageCodePath: String, clazz: String, appProcess: String, niceName: String? = null): String {
val debugParams = if (Debug.isDebuggerConnected()) when (Build.VERSION.SDK_INT) {
in 29..Int.MAX_VALUE -> "-XjdwpProvider:adbconnection"
28 -> "-XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y -Xcompiler-option --debuggable"
else -> "-Xrunjdwp:transport=dt_android_adb,suspend=n,server=y -Xcompiler-option --debuggable"
} else ""
val extraParams = if (niceName != null) " --nice-name=$niceName" else ""
return "CLASSPATH=$packageCodePath exec $appProcess $debugParams /system/bin$extraParams $clazz"
}
}

View File

@@ -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)
}
}

View File

@@ -1,485 +0,0 @@
package be.mygod.librootkotlinx
import android.content.Context
import android.os.Build
import android.os.Looper
import android.os.Parcelable
import android.os.RemoteException
import android.system.Os
import android.system.OsConstants
import androidx.collection.LongSparseArray
import androidx.collection.set
import androidx.collection.valueIterator
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import java.io.*
import java.util.*
import java.util.concurrent.CountDownLatch
import kotlin.system.exitProcess
class RootServer {
private sealed class Callback(private val server: RootServer, private val index: Long,
protected val classLoader: ClassLoader?) {
var active = true
abstract fun cancel()
abstract fun shouldRemove(result: Byte): Boolean
abstract operator fun invoke(input: DataInputStream, result: Byte)
fun sendClosed() = server.execute(CancelCommand(index))
private fun initException(targetClass: Class<*>, message: String): Throwable {
@Suppress("NAME_SHADOWING")
var targetClass = targetClass
while (true) {
try {
// try to find a message constructor
return targetClass.getDeclaredConstructor(String::class.java).newInstance(message) as Throwable
} catch (_: ReflectiveOperationException) { }
targetClass = targetClass.superclass
}
}
private fun makeRemoteException(cause: Throwable, message: String? = null) =
if (cause is CancellationException) cause else RemoteException(message).initCause(cause)
protected fun DataInputStream.readException(result: Byte) = when (result.toInt()) {
EX_GENERIC -> {
val message = readUTF()
val name = message.split(':', limit = 2)[0]
makeRemoteException(initException(try {
classLoader?.loadClass(name)
} catch (_: ClassNotFoundException) {
null
} ?: Class.forName(name), message), message)
}
EX_PARCELABLE -> makeRemoteException(readParcelable<Parcelable>(classLoader) as Throwable)
EX_SERIALIZABLE -> makeRemoteException(readSerializable(classLoader) as Throwable)
else -> throw IllegalArgumentException("Unexpected result $result")
}
class Ordinary(server: RootServer, index: Long, classLoader: ClassLoader?,
private val callback: CompletableDeferred<Parcelable?>) : Callback(server, index, classLoader) {
override fun cancel() = callback.cancel()
override fun shouldRemove(result: Byte) = true
override fun invoke(input: DataInputStream, result: Byte) {
if (result.toInt() == SUCCESS) callback.complete(input.readParcelable(classLoader))
else callback.completeExceptionally(input.readException(result))
}
}
class Channel(server: RootServer, index: Long, classLoader: ClassLoader?,
private val channel: SendChannel<Parcelable?>) : Callback(server, index, classLoader) {
val finish: CompletableDeferred<Unit> = CompletableDeferred()
override fun cancel() = finish.cancel()
override fun shouldRemove(result: Byte) = result.toInt() != SUCCESS
override fun invoke(input: DataInputStream, result: Byte) {
when (result.toInt()) {
SUCCESS -> channel.trySend(input.readParcelable(classLoader)).onClosed {
active = false
sendClosed()
finish.completeExceptionally(it
?: ClosedSendChannelException("Channel was closed normally"))
return
}.onFailure { throw it!! } // the channel we are supporting should never block
CHANNEL_CONSUMED -> finish.complete(Unit)
else -> finish.completeExceptionally(input.readException(result))
}
}
}
}
class UnexpectedExitException : RemoteException("Root process exited unexpectedly")
private lateinit var process: Process
/**
* Thread safety: needs to be protected by callbackLookup.
*/
private lateinit var output: DataOutputStream
@Volatile
var active = false
private var counter = 0L
private var callbackListenerExit: Deferred<Unit>? = null
private val callbackLookup = LongSparseArray<Callback>()
private fun readUnexpectedStderr(): String? {
if (!this::process.isInitialized) return null
var available = process.errorStream.available()
return if (available <= 0) null else String(ByteArrayOutputStream().apply {
try {
while (available > 0) {
val bytes = ByteArray(available)
val len = process.errorStream.read(bytes)
if (len < 0) throw EOFException() // should not happen
write(bytes, 0, len)
available = process.errorStream.available()
}
} catch (e: IOException) {
Logger.me.w("Reading stderr was cut short", e)
}
}.toByteArray())
}
private fun BufferedReader.lookForToken(token: String) {
while (true) {
val line = readLine() ?: throw EOFException()
if (line.endsWith(token)) {
val extraLength = line.length - token.length
if (extraLength > 0) Logger.me.w(line.substring(0, extraLength))
break
}
Logger.me.w(line)
}
}
private fun doInit(context: Context, niceName: String) {
val (reader, writer) = try {
process = ProcessBuilder("su").start()
val token1 = UUID.randomUUID().toString()
val writer = DataOutputStream(process.outputStream.buffered())
writer.writeBytes("echo $token1\n")
writer.flush()
val reader = process.inputStream.bufferedReader()
reader.lookForToken(token1)
Logger.me.d("Root shell initialized")
reader to writer
} catch (e: Exception) {
throw NoShellException(e)
}
try {
val token2 = UUID.randomUUID().toString()
val persistence = File(context.codeCacheDir, ".librootkotlinx-uuid")
val uuid = context.packageName + '@' + try {
persistence.readText()
} catch (_: FileNotFoundException) {
UUID.randomUUID().toString().also { persistence.writeText(it) }
}
val (script, relocated) = AppProcess.relocateScript(uuid)
script.appendLine(AppProcess.launchString(context.packageCodePath, RootServer::class.java.name, relocated,
niceName) + " $token2")
writer.writeBytes(script.toString())
writer.flush()
reader.lookForToken(token2) // wait for ready signal
} catch (e: Exception) {
throw RuntimeException("Failed to launch root daemon", e)
}
output = writer
require(!active)
active = true
Logger.me.d("Root server initialized")
}
private fun callbackSpin() {
val input = DataInputStream(process.inputStream.buffered())
while (active) {
val index = try {
input.readLong()
} catch (_: EOFException) {
break
}
val result = input.readByte()
val callback = synchronized(callbackLookup) {
if (active) (callbackLookup[index] ?: error("Empty callback #$index")).also {
if (it.shouldRemove(result)) {
callbackLookup.remove(index)
it.active = false
}
} else null
} ?: break
Logger.me.d("Received callback #$index: $result")
callback(input, result)
}
}
/**
* Initialize a RootServer synchronously, can throw a lot of exceptions.
*
* @param context Any [Context] from the app.
* @param niceName Name to call the rooted Java process.
*/
suspend fun init(context: Context, niceName: String = "${context.packageName}:root") {
withContext(Dispatchers.IO) {
try {
doInit(context, niceName)
} finally {
try {
readUnexpectedStderr()?.let { Logger.me.e(it) }
} catch (e: IOException) {
Logger.me.e("Failed to read from stderr", e) // avoid the real exception being swallowed
}
}
}
callbackListenerExit = GlobalScope.async(Dispatchers.IO) {
val errorReader = async(Dispatchers.IO) {
try {
process.errorStream.bufferedReader().forEachLine(Logger.me::w)
} catch (_: IOException) { }
}
try {
callbackSpin()
if (active) throw UnexpectedExitException()
} catch (e: Throwable) {
process.destroy()
throw e
} finally {
Logger.me.d("Waiting for exit")
withContext(NonCancellable) { errorReader.await() }
process.waitFor()
closeInternal(true)
}
}
}
/**
* Caller should check for active.
*/
private fun sendLocked(command: Parcelable) {
output.writeParcelable(command)
output.flush()
Logger.me.d("Sent #$counter: $command")
counter++
}
fun execute(command: RootCommandOneWay) = synchronized(callbackLookup) { if (active) sendLocked(command) }
@Throws(RemoteException::class)
suspend inline fun <reified T : Parcelable?> execute(command: RootCommand<T>) =
execute(command, T::class.java.classLoader)
@Throws(RemoteException::class)
suspend fun <T : Parcelable?> execute(command: RootCommand<T>, classLoader: ClassLoader?): T {
val future = CompletableDeferred<T>()
val callback = synchronized(callbackLookup) {
@Suppress("UNCHECKED_CAST")
val callback = Callback.Ordinary(this, counter, classLoader, future as CompletableDeferred<Parcelable?>)
if (active) {
callbackLookup[counter] = callback
sendLocked(command)
} else future.cancel()
callback
}
try {
return future.await()
} finally {
if (callback.active) callback.sendClosed()
callback.active = false
}
}
@ExperimentalCoroutinesApi
@Throws(RemoteException::class)
inline fun <reified T : Parcelable?> create(command: RootCommandChannel<T>, scope: CoroutineScope) =
create(command, scope, T::class.java.classLoader)
@ExperimentalCoroutinesApi
@Throws(RemoteException::class)
fun <T : Parcelable?> create(command: RootCommandChannel<T>, scope: CoroutineScope,
classLoader: ClassLoader?) = scope.produce<T>(
SupervisorJob(), command.capacity.also {
when (it) {
Channel.UNLIMITED, Channel.CONFLATED -> { }
else -> throw IllegalArgumentException("Unsupported channel capacity $it")
}
}) {
val callback = synchronized(callbackLookup) {
@Suppress("UNCHECKED_CAST")
val callback = Callback.Channel(this@RootServer, counter, classLoader, this as SendChannel<Parcelable?>)
if (active) {
callbackLookup[counter] = callback
sendLocked(command)
} else callback.finish.cancel()
callback
}
try {
callback.finish.await()
} finally {
if (callback.active) callback.sendClosed()
callback.active = false
}
}
private fun closeInternal(fromWorker: Boolean = false) = synchronized(callbackLookup) {
if (active) {
active = false
Logger.me.d(if (fromWorker) "Shutting down from worker" else "Shutting down from client")
try {
sendLocked(Shutdown())
output.close()
process.outputStream.close()
} catch (e: IOException) {
if (!e.isEBADF) Logger.me.w("send Shutdown failed", e)
}
Logger.me.d("Client closed")
}
if (fromWorker) {
for (callback in callbackLookup.valueIterator()) callback.cancel()
callbackLookup.clear()
}
}
/**
* Shutdown the instance gracefully.
*/
suspend fun close() {
closeInternal()
val callbackListenerExit = callbackListenerExit ?: return
try {
withTimeout(10000) { callbackListenerExit.await() }
} catch (e: TimeoutCancellationException) {
Logger.me.w("Closing the instance has timed out", e)
if (Build.VERSION.SDK_INT < 26) process.destroy() else if (process.isAlive) process.destroyForcibly()
} catch (e: UnexpectedExitException) {
Logger.me.w(e.message)
}
}
companion object {
private const val SUCCESS = 0
private const val EX_GENERIC = 1
private const val EX_PARCELABLE = 2
private const val EX_SERIALIZABLE = 4
private const val CHANNEL_CONSUMED = 3
private fun DataInputStream.readByteArray() = ByteArray(readInt()).also { readFully(it) }
private inline fun <reified T : Parcelable> DataInputStream.readParcelable(
classLoader: ClassLoader? = T::class.java.classLoader) = readByteArray().toParcelable<T>(classLoader)
private fun DataOutputStream.writeParcelable(data: Parcelable?, parcelableFlags: Int = 0) {
val bytes = data.toByteArray(parcelableFlags)
writeInt(bytes.size)
write(bytes)
}
private fun DataInputStream.readSerializable(classLoader: ClassLoader?) =
object : ObjectInputStream(ByteArrayInputStream(readByteArray())) {
override fun resolveClass(desc: ObjectStreamClass) = Class.forName(desc.name, false, classLoader)
}.readObject()
@JvmStatic
fun main(args: Array<String>) {
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
Logger.me.e("Uncaught exception from $thread", throwable)
throwable.printStackTrace() // stderr will be read by listener
exitProcess(1)
}
rootMain(args)
exitProcess(0) // there might be other non-daemon threads
}
private fun DataOutputStream.pushThrowable(callback: Long, e: Throwable) {
writeLong(callback)
if (e is Parcelable) {
writeByte(EX_PARCELABLE)
writeParcelable(e)
} else try {
val bytes = ByteArrayOutputStream().apply {
ObjectOutputStream(this).use { it.writeObject(e) }
}.toByteArray()
writeByte(EX_SERIALIZABLE)
writeInt(bytes.size)
write(bytes)
} catch (_: NotSerializableException) {
writeByte(EX_GENERIC)
writeUTF(e.stackTraceToString())
}
flush()
}
private fun DataOutputStream.pushResult(callback: Long, result: Parcelable?) {
writeLong(callback)
writeByte(SUCCESS)
writeParcelable(result)
flush()
}
private fun rootMain(args: Array<String>) {
require(args.isNotEmpty())
val mainInitialized = CountDownLatch(1)
val main = Thread({
@Suppress("DEPRECATION")
Looper.prepareMainLooper()
mainInitialized.countDown()
Looper.loop()
}, "main")
main.start()
val job = Job()
val defaultWorker by lazy {
mainInitialized.await()
CoroutineScope(Dispatchers.Main.immediate + job)
}
val callbackWorker = newSingleThreadContext("callbackWorker")
val cancellables = LongSparseArray<() -> Unit>()
// thread safety: usage of output should be guarded by callbackWorker
val output = DataOutputStream(FileOutputStream(Os.dup(FileDescriptor.out)).buffered().apply {
// prevent future write attempts to System.out, possibly from Samsung changes (again)
Os.dup2(FileDescriptor.err, OsConstants.STDOUT_FILENO)
System.setOut(System.err)
val writer = writer()
writer.appendLine(args[0]) // echo ready signal
writer.flush()
})
// thread safety: usage of input should be in main thread
val input = DataInputStream(System.`in`.buffered())
var counter = 0L
Logger.me.d("Server entering main loop")
loop@ while (true) {
val command = try {
input.readParcelable<Parcelable>(RootServer::class.java.classLoader)
} catch (_: EOFException) {
break
}
val callback = counter
Logger.me.d("Received #$callback: $command")
when (command) {
is CancelCommand -> cancellables[command.index]?.invoke()
is RootCommandOneWay -> defaultWorker.launch {
try {
command.execute()
} catch (e: Throwable) {
Logger.me.e("Unexpected exception in RootCommandOneWay ($command.javaClass.simpleName)", e)
}
}
is RootCommand<*> -> {
val commandJob = Job()
cancellables[callback] = { commandJob.cancel() }
defaultWorker.launch(commandJob) {
val result = try {
val result = command.execute();
{ output.pushResult(callback, result) }
} catch (e: Throwable) {
val worker = { output.pushThrowable(callback, e) }
worker
} finally {
cancellables.remove(callback)
}
withContext(callbackWorker + NonCancellable) { result() }
}
}
is RootCommandChannel<*> -> defaultWorker.launch {
val result = try {
coroutineScope {
command.create(this).also {
cancellables[callback] = { it.cancel() }
}.consumeEach { result ->
withContext(callbackWorker) { output.pushResult(callback, result) }
}
};
@Suppress("BlockingMethodInNonBlockingContext") {
output.writeByte(CHANNEL_CONSUMED)
output.writeLong(callback)
output.flush()
}
} catch (e: Throwable) {
val worker = { output.pushThrowable(callback, e) }
worker
} finally {
cancellables.remove(callback)
}
withContext(callbackWorker + NonCancellable) { result() }
}
is Shutdown -> break@loop
else -> throw IllegalArgumentException("Unrecognized input: $command")
}
counter++
}
job.cancel()
Logger.me.d("Clean up initiated before exit. Jobs: ${job.children.joinToString()}")
if (runBlocking { withTimeoutOrNull(5000) { job.join() } } == null) {
Logger.me.w("Clean up timeout: ${job.children.joinToString()}")
} else Logger.me.d("Clean up finished, exiting")
}
}
}

View File

@@ -1,108 +0,0 @@
package be.mygod.librootkotlinx
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.concurrent.TimeUnit
import kotlin.coroutines.CoroutineContext
/**
* This object manages creation of [RootServer] and times them out automagically, with default timeout of 5 minutes.
*/
abstract class RootSession {
protected abstract suspend fun initServer(server: RootServer)
/**
* Timeout to close [RootServer] in milliseconds.
*/
protected open val timeout get() = TimeUnit.MINUTES.toMillis(5)
protected open val timeoutContext: CoroutineContext get() = Dispatchers.Default
private val mutex = Mutex()
private var server: RootServer? = null
private var timeoutJob: Job? = null
private var usersCount = 0L
private var closePending = false
private suspend fun ensureServerLocked(): RootServer {
server?.let {
if (it.active) return it
usersCount = 0
closeLocked()
}
check(usersCount == 0L) { "Unexpected $server, $usersCount" }
val server = RootServer()
try {
initServer(server)
this.server = server
return server
} catch (e: Throwable) {
try {
server.close()
} catch (eClose: Throwable) {
e.addSuppressed(eClose)
}
throw e
}
}
private suspend fun closeLocked() {
closePending = false
val server = server
this.server = null
server?.close()
}
private fun startTimeoutLocked() {
check(timeoutJob == null)
timeoutJob = GlobalScope.launch(timeoutContext, CoroutineStart.UNDISPATCHED) {
delay(timeout)
mutex.withLock {
check(usersCount == 0L)
timeoutJob = null
closeLocked()
}
}
}
private fun haltTimeoutLocked() {
timeoutJob?.cancel()
timeoutJob = null
}
suspend fun acquire() = withContext(NonCancellable) {
mutex.withLock {
haltTimeoutLocked()
closePending = false
ensureServerLocked().also { ++usersCount }
}
}
suspend fun release(server: RootServer) = withContext(NonCancellable) {
mutex.withLock {
if (this@RootSession.server != server) return@withLock // outdated reference
require(usersCount > 0)
when {
!server.active -> {
usersCount = 0
closeLocked()
return@withLock
}
--usersCount > 0L -> return@withLock
closePending -> closeLocked()
else -> startTimeoutLocked()
}
}
}
suspend inline fun <T> use(block: (RootServer) -> T): T {
val server = acquire()
try {
return block(server)
} finally {
release(server)
}
}
suspend fun closeExisting() = mutex.withLock {
if (usersCount > 0) closePending = true else {
haltTimeoutLocked()
closeLocked()
}
}
}

View File

@@ -1,47 +0,0 @@
package be.mygod.librootkotlinx
import android.os.Parcelable
import androidx.annotation.MainThread
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.parcelize.Parcelize
interface RootCommand<Result : Parcelable?> : Parcelable {
/**
* If a throwable was thrown, it will be wrapped in RemoteException only if it implements [Parcelable].
*/
@MainThread
suspend fun execute(): Result
}
typealias RootCommandNoResult = RootCommand<Parcelable?>
/**
* Execute a command and discards its result, even if an exception occurs.
*
* If you want to catch exception, use e.g. [RootCommandNoResult] and return null.
*/
interface RootCommandOneWay : Parcelable {
@MainThread
suspend fun execute()
}
interface RootCommandChannel<T : Parcelable?> : Parcelable {
/**
* The capacity of the channel that is returned by [create] to be used by client.
* Only [Channel.UNLIMITED] and [Channel.CONFLATED] is supported for now to avoid blocking the entire connection.
*/
val capacity: Int get() = Channel.UNLIMITED
@MainThread
fun create(scope: CoroutineScope): ReceiveChannel<T>
}
@Parcelize
internal data class CancelCommand(val index: Long) : RootCommandOneWay {
override suspend fun execute() = error("Internal implementation")
}
@Parcelize
internal class Shutdown : Parcelable

View File

@@ -1,255 +0,0 @@
@file:JvmName("Utils")
package be.mygod.librootkotlinx
import android.annotation.SuppressLint
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import android.system.ErrnoException
import android.system.OsConstants
import android.util.*
import androidx.annotation.RequiresApi
import kotlinx.parcelize.Parcelize
import java.io.IOException
class NoShellException(cause: Throwable) : Exception("Root missing", cause)
internal val currentInstructionSet by lazy {
val classVMRuntime = Class.forName("dalvik.system.VMRuntime")
val runtime = classVMRuntime.getDeclaredMethod("getRuntime").invoke(null)
classVMRuntime.getDeclaredMethod("getCurrentInstructionSet").invoke(runtime) as String
}
private val classSystemProperties by lazy { Class.forName("android.os.SystemProperties") }
@get:RequiresApi(26)
internal val isVndkLite by lazy {
classSystemProperties.getDeclaredMethod("getBoolean", String::class.java, Boolean::class.java).invoke(null,
"ro.vndk.lite", false) as Boolean
}
@get:RequiresApi(26)
internal val vndkVersion by lazy {
classSystemProperties.getDeclaredMethod("get", String::class.java, String::class.java).invoke(null,
"ro.vndk.version", "") as String
}
val systemContext by lazy {
val classActivityThread = Class.forName("android.app.ActivityThread")
val activityThread = classActivityThread.getMethod("systemMain").invoke(null)
classActivityThread.getMethod("getSystemContext").invoke(activityThread) as Context
}
@Parcelize
data class ParcelableByte(val value: Byte) : Parcelable
@Parcelize
data class ParcelableShort(val value: Short) : Parcelable
@Parcelize
data class ParcelableInt(val value: Int) : Parcelable
@Parcelize
data class ParcelableLong(val value: Long) : Parcelable
@Parcelize
data class ParcelableFloat(val value: Float) : Parcelable
@Parcelize
data class ParcelableDouble(val value: Double) : Parcelable
@Parcelize
data class ParcelableBoolean(val value: Boolean) : Parcelable
@Parcelize
data class ParcelableString(val value: String) : Parcelable
@Parcelize
data class ParcelableByteArray(val value: ByteArray) : Parcelable {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ParcelableByteArray
if (!value.contentEquals(other.value)) return false
return true
}
override fun hashCode(): Int {
return value.contentHashCode()
}
}
@Parcelize
data class ParcelableIntArray(val value: IntArray) : Parcelable {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ParcelableIntArray
if (!value.contentEquals(other.value)) return false
return true
}
override fun hashCode(): Int {
return value.contentHashCode()
}
}
@Parcelize
data class ParcelableLongArray(val value: LongArray) : Parcelable {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ParcelableLongArray
if (!value.contentEquals(other.value)) return false
return true
}
override fun hashCode(): Int {
return value.contentHashCode()
}
}
@Parcelize
data class ParcelableFloatArray(val value: FloatArray) : Parcelable {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ParcelableFloatArray
if (!value.contentEquals(other.value)) return false
return true
}
override fun hashCode(): Int {
return value.contentHashCode()
}
}
@Parcelize
data class ParcelableDoubleArray(val value: DoubleArray) : Parcelable {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ParcelableDoubleArray
if (!value.contentEquals(other.value)) return false
return true
}
override fun hashCode(): Int {
return value.contentHashCode()
}
}
@Parcelize
data class ParcelableBooleanArray(val value: BooleanArray) : Parcelable {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ParcelableBooleanArray
if (!value.contentEquals(other.value)) return false
return true
}
override fun hashCode(): Int {
return value.contentHashCode()
}
}
@Parcelize
data class ParcelableStringArray(val value: Array<String>) : Parcelable {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ParcelableStringArray
if (!value.contentEquals(other.value)) return false
return true
}
override fun hashCode(): Int {
return value.contentHashCode()
}
}
@Parcelize
data class ParcelableStringList(val value: List<String>) : Parcelable
@Parcelize
data class ParcelableSparseIntArray(val value: SparseIntArray) : Parcelable
@Parcelize
data class ParcelableSparseLongArray(val value: SparseLongArray) : Parcelable
@Parcelize
data class ParcelableSparseBooleanArray(val value: SparseBooleanArray) : Parcelable
@Parcelize
data class ParcelableCharSequence(val value: CharSequence) : Parcelable
@Parcelize
data class ParcelableSize(val value: Size) : Parcelable
@Parcelize
data class ParcelableSizeF(val value: SizeF) : Parcelable
@Parcelize
data class ParcelableArray(val value: Array<Parcelable?>) : Parcelable {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ParcelableArray
if (!value.contentEquals(other.value)) return false
return true
}
override fun hashCode(): Int {
return value.contentHashCode()
}
}
@Parcelize
data class ParcelableList(val value: List<Parcelable?>) : Parcelable
@SuppressLint("Recycle")
inline fun <T> useParcel(block: (Parcel) -> T) = Parcel.obtain().run {
try {
block(this)
} finally {
recycle()
}
}
fun Parcelable?.toByteArray(parcelableFlags: Int = 0) = useParcel { p ->
p.writeParcelable(this, parcelableFlags)
p.marshall()
}
inline fun <reified T : Parcelable> ByteArray.toParcelable(classLoader: ClassLoader? = T::class.java.classLoader) =
useParcel { p ->
p.unmarshall(this, 0, size)
p.setDataPosition(0)
p.readParcelable<T>(classLoader)
}
// Stream closed caused in NullOutputStream
val IOException.isEBADF get() = message == "Stream closed" || (cause as? ErrnoException)?.errno == OsConstants.EBADF

View File

@@ -10,6 +10,7 @@ import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResult import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.setFragmentResultListener
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
/** /**
@@ -44,7 +45,7 @@ abstract class AlertDialogFragment<Arg : Parcelable, Ret : Parcelable> :
fun key(resultKey: String = javaClass.name) = args().putString(KEY_RESULT, resultKey) fun key(resultKey: String = javaClass.name) = args().putString(KEY_RESULT, resultKey)
override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog = override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog =
AlertDialog.Builder(requireContext()).also { it.prepare(this) }.create() MaterialAlertDialogBuilder(requireContext()).also { it.prepare(this) }.create()
override fun onClick(dialog: DialogInterface?, which: Int) { override fun onClick(dialog: DialogInterface?, which: Int) {
setFragmentResult(resultKey ?: return, Bundle().apply { setFragmentResult(resultKey ?: return, Bundle().apply {

View File

@@ -2,18 +2,20 @@ package be.mygod.vpnhotspot
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.content.ActivityNotFoundException
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.location.LocationManager
import android.os.Build import android.os.Build
import android.provider.Settings
import android.util.Log import android.util.Log
import android.widget.Toast
import androidx.annotation.Size import androidx.annotation.Size
import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.provider.FontRequest
import androidx.emoji.text.EmojiCompat
import androidx.emoji.text.FontRequestEmojiCompatConfig
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import be.mygod.librootkotlinx.NoShellException import be.mygod.librootkotlinx.NoShellException
import be.mygod.vpnhotspot.net.DhcpWorkaround import be.mygod.vpnhotspot.net.DhcpWorkaround
@@ -21,6 +23,7 @@ import be.mygod.vpnhotspot.room.AppDatabase
import be.mygod.vpnhotspot.root.RootManager import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.util.DeviceStorageApp import be.mygod.vpnhotspot.util.DeviceStorageApp
import be.mygod.vpnhotspot.util.Services import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.widget.SmartSnackbar
import com.google.firebase.analytics.ktx.ParametersBuilder import com.google.firebase.analytics.ktx.ParametersBuilder
import com.google.firebase.analytics.ktx.analytics import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
@@ -39,15 +42,15 @@ class App : Application() {
lateinit var app: App lateinit var app: App
} }
@SuppressLint("RestrictedApi")
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
app = this app = this
if (Build.VERSION.SDK_INT >= 24) @SuppressLint("RestrictedApi") {
deviceStorage = DeviceStorageApp(this) deviceStorage = DeviceStorageApp(this)
// alternative to PreferenceManager.getDefaultSharedPreferencesName(this) // alternative to PreferenceManager.getDefaultSharedPreferencesName(this)
deviceStorage.moveSharedPreferencesFrom(this, PreferenceManager(this).sharedPreferencesName) deviceStorage.moveSharedPreferencesFrom(this, PreferenceManager(this).sharedPreferencesName)
deviceStorage.moveDatabaseFrom(this, AppDatabase.DB_NAME) deviceStorage.moveDatabaseFrom(this, AppDatabase.DB_NAME)
} else deviceStorage = this BootReceiver.migrateIfNecessary()
Services.init { this } Services.init { this }
// overhead of debug mode is minimal: https://github.com/Kotlin/kotlinx.coroutines/blob/f528898/docs/debugging.md#debug-mode // overhead of debug mode is minimal: https://github.com/Kotlin/kotlinx.coroutines/blob/f528898/docs/debugging.md#debug-mode
@@ -57,7 +60,7 @@ class App : Application() {
"REL" -> { } "REL" -> { }
else -> FirebaseCrashlytics.getInstance().apply { else -> FirebaseCrashlytics.getInstance().apply {
setCustomKey("codename", codename) setCustomKey("codename", codename)
if (Build.VERSION.SDK_INT >= 23) setCustomKey("preview_sdk", Build.VERSION.PREVIEW_SDK_INT) setCustomKey("preview_sdk", Build.VERSION.PREVIEW_SDK_INT)
} }
} }
Timber.plant(object : Timber.DebugTree() { Timber.plant(object : Timber.DebugTree() {
@@ -78,18 +81,6 @@ class App : Application() {
} }
}) })
ServiceNotification.updateNotificationChannels() ServiceNotification.updateNotificationChannels()
EmojiCompat.init(FontRequestEmojiCompatConfig(deviceStorage, FontRequest(
"com.google.android.gms.fonts",
"com.google.android.gms",
"Noto Color Emoji Compat",
R.array.com_google_android_gms_fonts_certs)).apply {
setEmojiSpanIndicatorEnabled(BuildConfig.DEBUG)
registerInitCallback(object : EmojiCompat.InitCallback() {
override fun onInitialized() = Timber.d("EmojiCompat initialized")
override fun onFailed(throwable: Throwable?) = Timber.d(throwable)
})
})
EBegFragment.init()
if (DhcpWorkaround.shouldEnable) DhcpWorkaround.enable(true) if (DhcpWorkaround.shouldEnable) DhcpWorkaround.enable(true)
} }
@@ -116,6 +107,21 @@ class App : Application() {
Firebase.analytics.logEvent(event, builder.bundle) Firebase.analytics.logEvent(event, builder.bundle)
} }
/**
* LOH also requires location to be turned on. So does p2p for some reason. Source:
* https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/53e0284/service/java/com/android/server/wifi/WifiServiceImpl.java#1204
* https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/53e0284/service/java/com/android/server/wifi/WifiSettingsStore.java#228
*/
inline fun <reified T> startServiceWithLocation(context: Context) {
if (Build.VERSION.SDK_INT < 33 && location?.isLocationEnabled != true) try {
context.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
Toast.makeText(context, R.string.tethering_location_off, Toast.LENGTH_LONG).show()
} catch (e: ActivityNotFoundException) {
app.logEvent("location_settings") { param("message", e.toString()) }
SmartSnackbar.make(R.string.tethering_location_off).show()
} else context.startForegroundService(Intent(context, T::class.java))
}
lateinit var deviceStorage: Application lateinit var deviceStorage: Application
val english by lazy { val english by lazy {
createConfigurationContext(Configuration(resources.configuration).apply { createConfigurationContext(Configuration(resources.configuration).apply {
@@ -124,16 +130,17 @@ class App : Application() {
} }
val pref by lazy { PreferenceManager.getDefaultSharedPreferences(deviceStorage) } val pref by lazy { PreferenceManager.getDefaultSharedPreferences(deviceStorage) }
val clipboard by lazy { getSystemService<ClipboardManager>()!! } val clipboard by lazy { getSystemService<ClipboardManager>()!! }
val location by lazy { getSystemService<LocationManager>() }
val hasTouch by lazy { packageManager.hasSystemFeature("android.hardware.faketouch") } val hasTouch by lazy { packageManager.hasSystemFeature("android.hardware.faketouch") }
val customTabsIntent by lazy { val customTabsIntent by lazy {
CustomTabsIntent.Builder().apply { CustomTabsIntent.Builder().apply {
setColorScheme(CustomTabsIntent.COLOR_SCHEME_SYSTEM) setColorScheme(CustomTabsIntent.COLOR_SCHEME_SYSTEM)
setColorSchemeParams(CustomTabsIntent.COLOR_SCHEME_LIGHT, CustomTabColorSchemeParams.Builder().apply { setColorSchemeParams(CustomTabsIntent.COLOR_SCHEME_LIGHT, CustomTabColorSchemeParams.Builder().apply {
setToolbarColor(ContextCompat.getColor(app, R.color.light_colorPrimary)) setToolbarColor(resources.getColor(R.color.light_colorPrimary, theme))
}.build()) }.build())
setColorSchemeParams(CustomTabsIntent.COLOR_SCHEME_DARK, CustomTabColorSchemeParams.Builder().apply { setColorSchemeParams(CustomTabsIntent.COLOR_SCHEME_DARK, CustomTabColorSchemeParams.Builder().apply {
setToolbarColor(ContextCompat.getColor(app, R.color.dark_colorPrimary)) setToolbarColor(resources.getColor(R.color.dark_colorPrimary, theme))
}.build()) }.build())
}.build() }.build()
} }

View File

@@ -5,31 +5,114 @@ import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import androidx.core.content.ContextCompat import android.os.Parcelable
import be.mygod.librootkotlinx.toByteArray
import be.mygod.librootkotlinx.toParcelable
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.util.Services import kotlinx.parcelize.Parcelize
import timber.log.Timber
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.File
import java.io.FileNotFoundException
class BootReceiver : BroadcastReceiver() { class BootReceiver : BroadcastReceiver() {
companion object { companion object {
const val KEY = "service.autoStart"
private val componentName by lazy { ComponentName(app, BootReceiver::class.java) } private val componentName by lazy { ComponentName(app, BootReceiver::class.java) }
var enabled: Boolean private var enabled: Boolean
get() = app.packageManager.getComponentEnabledSetting(componentName) == get() = app.packageManager.getComponentEnabledSetting(componentName) ==
PackageManager.COMPONENT_ENABLED_STATE_ENABLED PackageManager.COMPONENT_ENABLED_STATE_ENABLED
set(value) = app.packageManager.setComponentEnabledSetting(componentName, set(value) = app.packageManager.setComponentEnabledSetting(componentName,
if (value) PackageManager.COMPONENT_ENABLED_STATE_ENABLED if (value) PackageManager.COMPONENT_ENABLED_STATE_ENABLED
else PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP) else PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP)
private val userEnabled get() = app.pref.getBoolean(KEY, false)
private var started = false fun onUserSettingUpdated(shouldStart: Boolean) {
enabled = shouldStart && try {
config
} catch (e: Exception) {
Timber.w(e)
null
}?.startables?.isEmpty() == false
} }
private fun onConfigUpdated(isNotEmpty: Boolean) {
enabled = isNotEmpty && userEnabled
}
private const val FILENAME = "bootconfig"
private val configFile by lazy { File(app.deviceStorage.noBackupFilesDir, FILENAME) }
private val config: Config? get() = try {
DataInputStream(configFile.inputStream()).use { it.readBytes().toParcelable() }
} catch (_: FileNotFoundException) {
null
}
private fun updateConfig(work: Config.() -> Unit) = synchronized(BootReceiver) {
val config = try {
config
} catch (e: Exception) {
Timber.i("Boot config corrupted", e)
null
} ?: Config()
config.work()
DataOutputStream(configFile.outputStream()).use { it.write(config.toByteArray()) }
config
}
fun add(key: String, value: Startable) = try {
updateConfig { startables[key] = value }
onConfigUpdated(true)
} catch (e: Exception) {
Timber.w(e)
}
fun delete(key: String) = try {
onConfigUpdated(updateConfig { startables.remove(key) }.startables.isNotEmpty())
} catch (e: Exception) {
Timber.w(e)
}
inline fun <reified T> add(value: Startable) = add(T::class.java.name, value)
inline fun <reified T> delete() = delete(T::class.java.name)
fun migrateIfNecessary() {
val oldFile = File(app.noBackupFilesDir, FILENAME)
if (oldFile.canRead()) try {
if (!configFile.exists()) oldFile.copyTo(configFile)
if (!oldFile.delete()) oldFile.deleteOnExit()
} catch (e: Exception) {
Timber.w(e)
}
}
private var started = false
private fun startIfNecessary() {
if (started) return
val config = try {
synchronized(BootReceiver) { config }
} catch (e: Exception) {
Timber.w(e)
null
}
if (config == null || config.startables.isEmpty()) {
enabled = false
} else for (startable in config.startables.values) startable.start(app)
started = true
}
fun startIfEnabled() {
if (!started && userEnabled) startIfNecessary()
}
}
interface Startable : Parcelable {
fun start(context: Context)
}
@Parcelize
private data class Config(var startables: MutableMap<String, Startable> = mutableMapOf()) : Parcelable
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
if (started) return
when (intent.action) { when (intent.action) {
Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_LOCKED_BOOT_COMPLETED -> started = true Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_LOCKED_BOOT_COMPLETED, Intent.ACTION_MY_PACKAGE_REPLACED -> {
else -> return if (userEnabled) startIfNecessary() else enabled = false
} }
if (Services.p2p != null) {
ContextCompat.startForegroundService(context, Intent(context, RepeaterService::class.java))
} }
} }
} }

View File

@@ -23,26 +23,30 @@ import timber.log.Timber
*/ */
class EBegFragment : AppCompatDialogFragment() { class EBegFragment : AppCompatDialogFragment() {
companion object : BillingClientStateListener, PurchasesUpdatedListener { companion object : BillingClientStateListener, PurchasesUpdatedListener {
private lateinit var billingClient: BillingClient private val billingClient by lazy {
BillingClient.newBuilder(app).apply {
fun init() {
billingClient = BillingClient.newBuilder(app).apply {
enablePendingPurchases() enablePendingPurchases()
}.setListener(this).build().also { it.startConnection(this) } }.setListener(this).build()
} }
private var instance: EBegFragment? = null
override fun onBillingSetupFinished(billingResult: BillingResult) { override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) { if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
Timber.e("onBillingSetupFinished: ${billingResult.responseCode}") Timber.e("onBillingSetupFinished: ${billingResult.responseCode}")
} else GlobalScope.launch(Dispatchers.Main.immediate) { } else {
val result = billingClient.queryPurchasesAsync(BillingClient.SkuType.INAPP) instance?.onBillingConnected()
GlobalScope.launch(Dispatchers.Main.immediate) {
val result = billingClient.queryPurchasesAsync(QueryPurchasesParams.newBuilder().apply {
setProductType(BillingClient.ProductType.INAPP)
}.build())
onPurchasesUpdated(result.billingResult, result.purchasesList) onPurchasesUpdated(result.billingResult, result.purchasesList)
} }
} }
}
override fun onBillingServiceDisconnected() { override fun onBillingServiceDisconnected() {
Timber.e("onBillingServiceDisconnected") Timber.e("onBillingServiceDisconnected")
billingClient.startConnection(this) if (instance != null) billingClient.startConnection(this)
} }
override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) { override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) {
@@ -64,12 +68,12 @@ class EBegFragment : AppCompatDialogFragment() {
} }
private lateinit var binding: FragmentEbegBinding private lateinit var binding: FragmentEbegBinding
private var skus: List<SkuDetails>? = null private var productDetails: List<ProductDetails>? = null
set(value) { set(value) {
field = value field = value
binding.donationsGoogleAndroidMarketSpinner.apply { binding.donationsGoogleAndroidMarketSpinner.apply {
val adapter = ArrayAdapter(context ?: return, android.R.layout.simple_spinner_item, val adapter = ArrayAdapter(context ?: return, android.R.layout.simple_spinner_item,
value?.map { it.price } ?: listOf("")) value?.map { it.oneTimePurchaseOfferDetails?.formattedPrice } ?: listOf(""))
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
setAdapter(adapter) setAdapter(adapter)
} }
@@ -82,27 +86,46 @@ class EBegFragment : AppCompatDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
dialog!!.setTitle(R.string.settings_misc_donate) dialog!!.setTitle(R.string.settings_misc_donate)
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
billingClient.querySkuDetails(SkuDetailsParams.newBuilder().apply {
setSkusList(listOf("donate001", "donate002", "donate005", "donate010", "donate020", "donate050",
"donate100", "donate200", "donatemax"))
setType(BillingClient.SkuType.INAPP)
}.build()).apply {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) skus = skuDetailsList else {
Timber.e("onSkuDetailsResponse: ${billingResult.responseCode}")
SmartSnackbar.make(R.string.donations__google_android_market_not_supported).show()
}
}
}
binding.donationsGoogleAndroidMarketDonateButton.setOnClickListener { binding.donationsGoogleAndroidMarketDonateButton.setOnClickListener {
val sku = skus?.getOrNull(binding.donationsGoogleAndroidMarketSpinner.selectedItemPosition) val product = productDetails?.getOrNull(binding.donationsGoogleAndroidMarketSpinner.selectedItemPosition)
if (sku != null) billingClient.launchBillingFlow(requireActivity(), BillingFlowParams.newBuilder().apply { if (product != null) billingClient.launchBillingFlow(requireActivity(), BillingFlowParams.newBuilder().apply {
setSkuDetails(sku) setProductDetailsParamsList(listOf(BillingFlowParams.ProductDetailsParams.newBuilder().apply {
setProductDetails(product)
}.build()))
}.build()) else SmartSnackbar.make(R.string.donations__google_android_market_not_supported).show() }.build()) else SmartSnackbar.make(R.string.donations__google_android_market_not_supported).show()
} }
@Suppress("ConstantConditionIf")
if (BuildConfig.DONATIONS) (binding.donationsMoreStub.inflate() as Button).setOnClickListener { if (BuildConfig.DONATIONS) (binding.donationsMoreStub.inflate() as Button).setOnClickListener {
requireContext().launchUrl("https://mygod.be/donate/") requireContext().launchUrl("https://mygod.be/donate/")
} }
} }
override fun onStart() {
super.onStart()
instance = this
billingClient.startConnection(EBegFragment)
}
private fun onBillingConnected() = viewLifecycleOwner.lifecycleScope.launch {
billingClient.queryProductDetails(QueryProductDetailsParams.newBuilder().apply {
setProductList(listOf(
"donate001", "donate002", "donate005", "donate010", "donate020", "donate050",
"donate100", "donate200", "donatemax",
).map {
QueryProductDetailsParams.Product.newBuilder().apply {
setProductId(it)
setProductType(BillingClient.ProductType.INAPP)
}.build()
})
}.build()).apply {
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
Timber.e("queryProductDetails: ${billingResult.responseCode}")
SmartSnackbar.make(R.string.donations__google_android_market_not_supported).show()
} else productDetails = productDetailsList
}
}
override fun onStop() {
instance = null
super.onStop()
}
} }

View File

@@ -15,7 +15,7 @@ abstract class IpNeighbourMonitoringService : Service(), IpNeighbourMonitor.Call
this.neighbours = neighbours this.neighbours = neighbours
updateNotification() updateNotification()
} }
protected fun updateNotification() { protected open fun updateNotification() {
val sizeLookup = neighbours.groupBy { it.dev }.mapValues { (_, neighbours) -> val sizeLookup = neighbours.groupBy { it.dev }.mapValues { (_, neighbours) ->
neighbours neighbours
.filter { it.ip is Inet4Address && it.state == IpNeighbour.State.VALID } .filter { it.ip is Inet4Address && it.state == IpNeighbour.State.VALID }

View File

@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot package be.mygod.vpnhotspot
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
@@ -8,14 +9,12 @@ import androidx.annotation.RequiresApi
import be.mygod.librootkotlinx.RootServer import be.mygod.librootkotlinx.RootServer
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.IpNeighbour import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
import be.mygod.vpnhotspot.net.wifi.WifiApManager import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.net.wifi.WifiApManager.wifiApState
import be.mygod.vpnhotspot.root.LocalOnlyHotspotCallbacks import be.mygod.vpnhotspot.root.LocalOnlyHotspotCallbacks
import be.mygod.vpnhotspot.root.RootManager import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.root.WifiApCommands import be.mygod.vpnhotspot.root.WifiApCommands
@@ -24,10 +23,10 @@ import be.mygod.vpnhotspot.util.StickyEvent1
import be.mygod.vpnhotspot.util.broadcastReceiver import be.mygod.vpnhotspot.util.broadcastReceiver
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
import java.net.Inet4Address import java.net.Inet4Address
@RequiresApi(26)
class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope { class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
companion object { companion object {
const val KEY_USE_SYSTEM = "service.tempHotspot.useSystem" const val KEY_USE_SYSTEM = "service.tempHotspot.useSystem"
@@ -50,7 +49,15 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
null -> return // stopped null -> return // stopped
"" -> WifiApManager.cancelLocalOnlyHotspotRequest() "" -> WifiApManager.cancelLocalOnlyHotspotRequest()
} }
reservation?.close() ?: stopService() reservation?.close()
stopService()
}
}
@Parcelize
class Starter : BootReceiver.Startable {
override fun start(context: Context) {
context.startForegroundService(Intent(context, LocalOnlyHotspotService::class.java))
} }
} }
@@ -82,16 +89,17 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
private val binder = Binder() private val binder = Binder()
private var reservation: Reservation? = null private var reservation: Reservation? = null
set(value) { set(value) {
if (value == null) field?.close()
field = value field = value
if (value != null && !receiverRegistered) { timeoutMonitor?.close()
timeoutMonitor = null
if (value != null) {
val configuration = binder.configuration val configuration = binder.configuration
if (Build.VERSION.SDK_INT < 30 && configuration!!.isAutoShutdownEnabled) { if (Build.VERSION.SDK_INT < 30 && configuration!!.isAutoShutdownEnabled) {
timeoutMonitor = TetherTimeoutMonitor(configuration.shutdownTimeoutMillis, coroutineContext) { timeoutMonitor = TetherTimeoutMonitor(configuration.shutdownTimeoutMillis, coroutineContext) {
value.close() value.close()
} }
} }
registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
receiverRegistered = true
} }
} }
private fun onFrameworkFailed(reason: Int) { private fun onFrameworkFailed(reason: Int) {
@@ -116,33 +124,30 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
/** /**
* Writes and critical reads to routingManager should be protected with this context. * Writes and critical reads to routingManager should be protected with this context.
*/ */
private val dispatcher = newSingleThreadContext("LocalOnlyHotspotService") private val dispatcher = Dispatchers.IO.limitedParallelism(1)
override val coroutineContext = dispatcher + Job() override val coroutineContext = dispatcher + Job()
private var routingManager: RoutingManager? = null private var routingManager: RoutingManager? = null
private var timeoutMonitor: TetherTimeoutMonitor? = null private var timeoutMonitor: TetherTimeoutMonitor? = null
private var receiverRegistered = false
private val receiver = broadcastReceiver { _, intent ->
val ifaces = (intent.localOnlyTetheredIfaces ?: return@broadcastReceiver).filter {
TetherType.ofInterface(it) != TetherType.WIFI_P2P
}
Timber.d("onTetherStateChangedLocked: $ifaces")
check(ifaces.size <= 1)
val iface = ifaces.singleOrNull()
binder.iface = iface
if (iface.isNullOrEmpty()) stopService() else launch {
val routingManager = routingManager
if (routingManager == null) {
this@LocalOnlyHotspotService.routingManager = RoutingManager.LocalOnly(this@LocalOnlyHotspotService,
iface).apply { start() }
IpNeighbourMonitor.registerCallback(this@LocalOnlyHotspotService)
} else check(iface == routingManager.downstream)
}
}
override val activeIfaces get() = binder.iface.let { if (it.isNullOrEmpty()) emptyList() else listOf(it) } override val activeIfaces get() = binder.iface.let { if (it.isNullOrEmpty()) emptyList() else listOf(it) }
private var lastState: Triple<Int, String?, Int>? = null
private val receiver = broadcastReceiver { _, intent -> updateState(intent) }
private var receiverRegistered = false
private fun updateState(intent: Intent) {
// based on: https://android.googlesource.com/platform/packages/services/Car/+/72c71d2/service/src/com/android/car/CarProjectionService.java#160
lastState = Triple(intent.wifiApState, intent.getStringExtra(WifiApManager.EXTRA_WIFI_AP_INTERFACE_NAME),
intent.getIntExtra(WifiApManager.EXTRA_WIFI_AP_FAILURE_REASON, 0))
}
private fun unregisterStateReceiver() {
if (!receiverRegistered) return
receiverRegistered = false
unregisterReceiver(receiver)
}
override fun onBind(intent: Intent?) = binder override fun onBind(intent: Intent?) = binder
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
BootReceiver.startIfEnabled()
if (binder.iface != null) return START_STICKY if (binder.iface != null) return START_STICKY
binder.iface = "" binder.iface = ""
updateNotification() // show invisible foreground notification to avoid being killed updateNotification() // show invisible foreground notification to avoid being killed
@@ -150,6 +155,11 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
return START_STICKY return START_STICKY
} }
private suspend fun doStart() { private suspend fun doStart() {
if (!receiverRegistered) {
receiverRegistered = true
registerReceiver(receiver, IntentFilter(WifiApManager.WIFI_AP_STATE_CHANGED_ACTION))
?.let(this@LocalOnlyHotspotService::updateState)
}
if (Build.VERSION.SDK_INT >= 30 && app.pref.getBoolean(KEY_USE_SYSTEM, false)) try { if (Build.VERSION.SDK_INT >= 30 && app.pref.getBoolean(KEY_USE_SYSTEM, false)) try {
RootManager.use { RootManager.use {
Root(it).apply { Root(it).apply {
@@ -170,6 +180,24 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
if (reservation == null) onFailed(-2) else { if (reservation == null) onFailed(-2) else {
this@LocalOnlyHotspotService.reservation = Framework(reservation) this@LocalOnlyHotspotService.reservation = Framework(reservation)
} }
registerReceiver(null, IntentFilter(WifiApManager.WIFI_AP_STATE_CHANGED_ACTION))
?.let(this@LocalOnlyHotspotService::updateState) // attempt to update again
val state = lastState
unregisterStateReceiver()
checkNotNull(state) { "Failed to obtain latest AP state" }
val iface = state.second
if (state.first != WifiApManager.WIFI_AP_STATE_ENABLED || iface.isNullOrEmpty()) {
if (state.first == WifiApManager.WIFI_AP_STATE_FAILED) {
SmartSnackbar.make(getString(R.string.tethering_temp_hotspot_failure,
WifiApManager.failureReasonLookup(state.third))).show()
}
return stopService()
}
binder.iface = iface
BootReceiver.add<LocalOnlyHotspotService>(Starter())
check(routingManager == null)
routingManager = RoutingManager.LocalOnly(this@LocalOnlyHotspotService, iface).apply { start() }
IpNeighbourMonitor.registerCallback(this@LocalOnlyHotspotService)
} }
override fun onStopped() { override fun onStopped() {
reservation = null reservation = null
@@ -191,7 +219,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
override fun onIpNeighbourAvailable(neighbours: Collection<IpNeighbour>) { override fun onIpNeighbourAvailable(neighbours: Collection<IpNeighbour>) {
super.onIpNeighbourAvailable(neighbours) super.onIpNeighbourAvailable(neighbours)
if (Build.VERSION.SDK_INT >= 28) timeoutMonitor?.onClientsChanged(neighbours.none { timeoutMonitor?.onClientsChanged(neighbours.none {
it.ip is Inet4Address && it.state == IpNeighbour.State.VALID it.ip is Inet4Address && it.state == IpNeighbour.State.VALID
}) })
} }
@@ -203,6 +231,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
} }
private fun stopService() { private fun stopService() {
BootReceiver.delete<LocalOnlyHotspotService>()
binder.iface = null binder.iface = null
unregisterReceiver() unregisterReceiver()
ServiceNotification.stopForeground(this) ServiceNotification.stopForeground(this)
@@ -210,22 +239,14 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
} }
private fun unregisterReceiver(exit: Boolean = false) { private fun unregisterReceiver(exit: Boolean = false) {
if (receiverRegistered) {
unregisterReceiver(receiver)
IpNeighbourMonitor.unregisterCallback(this) IpNeighbourMonitor.unregisterCallback(this)
if (Build.VERSION.SDK_INT >= 28) {
timeoutMonitor?.close() timeoutMonitor?.close()
timeoutMonitor = null timeoutMonitor = null
}
receiverRegistered = false
}
launch { launch {
routingManager?.stop() routingManager?.stop()
routingManager = null routingManager = null
if (exit) { unregisterStateReceiver()
cancel() if (exit) cancel()
dispatcher.close()
}
} }
} }
} }

View File

@@ -4,28 +4,60 @@ import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import be.mygod.vpnhotspot.client.ClientViewModel import be.mygod.vpnhotspot.client.ClientViewModel
import be.mygod.vpnhotspot.client.ClientsFragment import be.mygod.vpnhotspot.client.ClientsFragment
import be.mygod.vpnhotspot.databinding.ActivityMainBinding import be.mygod.vpnhotspot.databinding.ActivityMainBinding
import be.mygod.vpnhotspot.manage.TetheringFragment import be.mygod.vpnhotspot.manage.TetheringFragment
import be.mygod.vpnhotspot.net.IpNeighbour import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
import be.mygod.vpnhotspot.util.AppUpdate
import be.mygod.vpnhotspot.util.ServiceForegroundConnector import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import be.mygod.vpnhotspot.util.Services import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.UpdateChecker
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.navigation.NavigationBarView import com.google.android.material.navigation.NavigationBarView
import kotlinx.coroutines.launch
import timber.log.Timber
import java.net.Inet4Address import java.net.Inet4Address
import java.util.concurrent.CancellationException
class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListener { class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListener {
lateinit var binding: ActivityMainBinding lateinit var binding: ActivityMainBinding
private lateinit var updateItem: MenuItem
private lateinit var updateBadge: BadgeDrawable
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(android.R.id.content)) { view, insets ->
val statusBarInsets = insets.getInsets(WindowInsetsCompat.Type.statusBars())
view.setPadding(statusBarInsets.left, statusBarInsets.top, statusBarInsets.right, statusBarInsets.bottom)
WindowInsetsCompat.Builder(insets).apply {
setInsets(WindowInsetsCompat.Type.statusBars(), Insets.NONE)
}.build()
}
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
binding.navigation.setOnItemSelectedListener(this) binding.navigation.setOnItemSelectedListener(this)
val badge = binding.navigation.getOrCreateBadge(R.id.navigation_clients).apply {
backgroundColor = resources.getColor(R.color.colorSecondary, theme)
badgeTextColor = resources.getColor(androidx.appcompat.R.color.primary_text_default_material_light, theme)
}
updateItem = binding.navigation.menu.findItem(R.id.navigation_update)
updateItem.isCheckable = false
updateBadge = binding.navigation.getOrCreateBadge(R.id.navigation_update).apply {
backgroundColor = resources.getColor(R.color.colorSecondary, theme)
badgeTextColor = resources.getColor(androidx.appcompat.R.color.primary_text_default_material_light, theme)
}
if (savedInstanceState == null) displayFragment(TetheringFragment()) if (savedInstanceState == null) displayFragment(TetheringFragment())
val model by viewModels<ClientViewModel>() val model by viewModels<ClientViewModel>()
lifecycle.addObserver(model) lifecycle.addObserver(model)
@@ -34,38 +66,69 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
val count = clients.count { val count = clients.count {
it.ip.any { (ip, state) -> ip is Inet4Address && state == IpNeighbour.State.VALID } it.ip.any { (ip, state) -> ip is Inet4Address && state == IpNeighbour.State.VALID }
} }
if (count > 0) binding.navigation.getOrCreateBadge(R.id.navigation_clients).apply { badge.isVisible = count > 0
backgroundColor = ContextCompat.getColor(this@MainActivity, R.color.colorSecondary) badge.number = count
badgeTextColor = ContextCompat.getColor(this@MainActivity, R.color.primary_text_default_material_light)
number = count
} else binding.navigation.removeBadge(R.id.navigation_clients)
} }
SmartSnackbar.Register(binding.fragmentHolder) SmartSnackbar.Register(binding.fragmentHolder)
WifiDoubleLock.ActivityListener(this) WifiDoubleLock.ActivityListener(this)
lifecycleScope.launch {
BootReceiver.startIfEnabled()
repeatOnLifecycle(Lifecycle.State.STARTED) {
onAppUpdateAvailable(null)
try {
UpdateChecker.check().collect(this@MainActivity::onAppUpdateAvailable)
} catch (_: CancellationException) {
} catch (e: AppUpdate.IgnoredException) {
Timber.d(e)
} catch (e: Exception) {
Timber.w(e)
SmartSnackbar.make(e).show()
}
}
}
}
private var lastUpdate: AppUpdate? = null
private fun onAppUpdateAvailable(update: AppUpdate?) {
lastUpdate = update
updateItem.isVisible = update != null
if (update == null) {
updateItem.isEnabled = false
return
}
updateItem.isEnabled = update.downloaded != false
updateItem.setIcon(when (update.downloaded) {
null -> R.drawable.ic_action_update
false -> R.drawable.ic_file_downloading
true -> R.drawable.ic_action_autorenew
})
updateItem.title = update.message ?: getText(R.string.title_update)
updateBadge.isVisible = when (val days = update.stalenessDays) {
null -> false
else -> {
if (days > 0) updateBadge.number = days else updateBadge.clearNumber()
true
}
}
} }
override fun onNavigationItemSelected(item: MenuItem) = when (item.itemId) { override fun onNavigationItemSelected(item: MenuItem) = when (item.itemId) {
R.id.navigation_clients -> { R.id.navigation_clients -> {
if (!item.isChecked) {
item.isChecked = true
displayFragment(ClientsFragment()) displayFragment(ClientsFragment())
}
true true
} }
R.id.navigation_tethering -> { R.id.navigation_tethering -> {
if (!item.isChecked) {
item.isChecked = true
displayFragment(TetheringFragment()) displayFragment(TetheringFragment())
}
true true
} }
R.id.navigation_settings -> { R.id.navigation_settings -> {
if (!item.isChecked) {
item.isChecked = true
displayFragment(SettingsPreferenceFragment()) displayFragment(SettingsPreferenceFragment())
}
true true
} }
R.id.navigation_update -> {
lastUpdate!!.updateForResult(this, 1)
false
}
else -> false else -> false
} }

View File

@@ -3,9 +3,13 @@ package be.mygod.vpnhotspot
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.annotation.TargetApi import android.annotation.TargetApi
import android.app.Service import android.app.Service
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.location.LocationManager
import android.net.MacAddress
import android.net.wifi.ScanResult
import android.net.wifi.WpsInfo import android.net.wifi.WpsInfo
import android.net.wifi.p2p.* import android.net.wifi.p2p.*
import android.os.Build import android.os.Build
@@ -16,19 +20,27 @@ import androidx.annotation.StringRes
import androidx.core.content.edit import androidx.core.content.edit
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.MacAddressCompat import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toLong
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
import be.mygod.vpnhotspot.net.wifi.VendorElements
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.deletePersistentGroup import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.deletePersistentGroup
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestConnectionInfo
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestDeviceAddress import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestDeviceAddress
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestGroupInfo
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestP2pState
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupInfo import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupInfo
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setVendorElements
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps
import be.mygod.vpnhotspot.net.wifi.WifiSsidCompat
import be.mygod.vpnhotspot.root.RepeaterCommands import be.mygod.vpnhotspot.root.RepeaterCommands
import be.mygod.vpnhotspot.root.RootManager import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.util.* import be.mygod.vpnhotspot.util.*
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@@ -42,12 +54,14 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
const val KEY_SAFE_MODE = "service.repeater.safeMode" const val KEY_SAFE_MODE = "service.repeater.safeMode"
private const val KEY_NETWORK_NAME = "service.repeater.networkName" private const val KEY_NETWORK_NAME = "service.repeater.networkName"
private const val KEY_NETWORK_NAME_HEX = "service.repeater.networkNameHex"
private const val KEY_PASSPHRASE = "service.repeater.passphrase" private const val KEY_PASSPHRASE = "service.repeater.passphrase"
private const val KEY_OPERATING_BAND = "service.repeater.band.v4" private const val KEY_OPERATING_BAND = "service.repeater.band.v4"
private const val KEY_OPERATING_CHANNEL = "service.repeater.oc.v3" private const val KEY_OPERATING_CHANNEL = "service.repeater.oc.v3"
private const val KEY_AUTO_SHUTDOWN = "service.repeater.autoShutdown" private const val KEY_AUTO_SHUTDOWN = "service.repeater.autoShutdown"
private const val KEY_SHUTDOWN_TIMEOUT = "service.repeater.shutdownTimeout" private const val KEY_SHUTDOWN_TIMEOUT = "service.repeater.shutdownTimeout"
private const val KEY_DEVICE_ADDRESS = "service.repeater.mac" private const val KEY_DEVICE_ADDRESS = "service.repeater.mac"
private const val KEY_VENDOR_ELEMENTS = "service.repeater.vendorElements"
var persistentSupported = false var persistentSupported = false
@@ -61,17 +75,26 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
val safeModeConfigurable get() = Build.VERSION.SDK_INT >= 29 && hasP2pValidateName val safeModeConfigurable get() = Build.VERSION.SDK_INT >= 29 && hasP2pValidateName
val safeMode get() = Build.VERSION.SDK_INT >= 29 && val safeMode get() = Build.VERSION.SDK_INT >= 29 &&
(!hasP2pValidateName || app.pref.getBoolean(KEY_SAFE_MODE, true)) (!hasP2pValidateName || app.pref.getBoolean(KEY_SAFE_MODE, true))
private val mNetworkName by lazy { UnblockCentral.WifiP2pConfig_Builder_mNetworkName } @get:RequiresApi(29)
private val mNetworkName by lazy @TargetApi(29) { UnblockCentral.WifiP2pConfig_Builder_mNetworkName }
var networkName: String? var networkName: WifiSsidCompat?
get() = app.pref.getString(KEY_NETWORK_NAME, null) get() = app.pref.getString(KEY_NETWORK_NAME, null).let { legacy ->
set(value) = app.pref.edit { putString(KEY_NETWORK_NAME, value) } if (legacy != null) WifiSsidCompat.fromUtf8Text(legacy).also {
app.pref.edit {
putString(KEY_NETWORK_NAME_HEX, it!!.hex)
remove(KEY_NETWORK_NAME)
}
} else WifiSsidCompat.fromHex(app.pref.getString(KEY_NETWORK_NAME_HEX, null))
}
set(value) = app.pref.edit { putString(KEY_NETWORK_NAME_HEX, value?.hex) }
var passphrase: String? var passphrase: String?
get() = app.pref.getString(KEY_PASSPHRASE, null) get() = app.pref.getString(KEY_PASSPHRASE, null)
set(value) = app.pref.edit { putString(KEY_PASSPHRASE, value) } set(value) = app.pref.edit { putString(KEY_PASSPHRASE, value) }
var operatingBand: Int var operatingBand: Int
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
get() = app.pref.getInt(KEY_OPERATING_BAND, SoftApConfigurationCompat.BAND_LEGACY) get() = app.pref.getInt(KEY_OPERATING_BAND, SoftApConfigurationCompat.BAND_LEGACY) and
SoftApConfigurationCompat.BAND_LEGACY
set(value) = app.pref.edit { putInt(KEY_OPERATING_BAND, value) } set(value) = app.pref.edit { putInt(KEY_OPERATING_BAND, value) }
var operatingChannel: Int var operatingChannel: Int
get() { get() {
@@ -85,17 +108,24 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
var shutdownTimeoutMillis: Long var shutdownTimeoutMillis: Long
get() = app.pref.getLong(KEY_SHUTDOWN_TIMEOUT, 0) get() = app.pref.getLong(KEY_SHUTDOWN_TIMEOUT, 0)
set(value) = app.pref.edit { putLong(KEY_SHUTDOWN_TIMEOUT, value) } set(value) = app.pref.edit { putLong(KEY_SHUTDOWN_TIMEOUT, value) }
var deviceAddress: MacAddressCompat? var deviceAddress: MacAddress?
get() = try { get() = try {
MacAddressCompat(app.pref.getLong(KEY_DEVICE_ADDRESS, MacAddressCompat.ANY_ADDRESS.addr)).run { MacAddressCompat(app.pref.getLong(KEY_DEVICE_ADDRESS, 2)).run {
validate() require(addr and ((1L shl 48) - 1).inv() == 0L)
if (this == MacAddressCompat.ANY_ADDRESS) null else this if (addr == 2L) null else toPlatform()
} }
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
Timber.w(e) Timber.w(e)
null null
} }
set(value) = app.pref.edit { putLong(KEY_DEVICE_ADDRESS, (value ?: MacAddressCompat.ANY_ADDRESS).addr) } set(value) = app.pref.edit {
putLong(KEY_DEVICE_ADDRESS, (value ?: MacAddressCompat.ANY_ADDRESS).toLong())
}
@get:RequiresApi(33)
@set:RequiresApi(33)
var vendorElements: List<ScanResult.InformationElement>
get() = VendorElements.deserialize(app.pref.getString(KEY_VENDOR_ELEMENTS, null))
set(value) = app.pref.edit { putString(KEY_VENDOR_ELEMENTS, VendorElements.serialize(value)) }
} }
enum class Status { enum class Status {
@@ -110,19 +140,17 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
set(value) { set(value) {
field = value field = value
groupChanged(value) groupChanged(value)
if (Build.VERSION.SDK_INT >= 28) value?.clientList?.let { value?.clientList?.let { timeoutMonitor?.onClientsChanged(it.isEmpty()) }
timeoutMonitor?.onClientsChanged(it.isEmpty())
}
} }
val groupChanged = StickyEvent1 { group } val groupChanged = StickyEvent1 { group }
suspend fun obtainDeviceAddress(): MacAddressCompat? { suspend fun obtainDeviceAddress(): MacAddress? {
return if (Build.VERSION.SDK_INT >= 29) p2pManager.requestDeviceAddress(channel ?: return null) ?: try { return if (Build.VERSION.SDK_INT >= 29) p2pManager.requestDeviceAddress(channel ?: return null) ?: try {
RootManager.use { it.execute(RepeaterCommands.RequestDeviceAddress()) } RootManager.use { it.execute(RepeaterCommands.RequestDeviceAddress()) }
} catch (e: Exception) { } catch (e: Exception) {
Timber.d(e) Timber.d(e)
null null
}?.let { MacAddressCompat(it.value) } else lastMac?.let { MacAddressCompat.fromString(it) } } else lastMac?.let { MacAddress.fromString(it) }
} }
@SuppressLint("NewApi") // networkId is available since Android 4.2 @SuppressLint("NewApi") // networkId is available since Android 4.2
@@ -134,7 +162,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
val ownedGroups = filter { val ownedGroups = filter {
if (!it.isGroupOwner) return@filter false if (!it.isGroupOwner) return@filter false
val address = try { val address = try {
MacAddressCompat.fromString(it.owner.deviceAddress) MacAddress.fromString(it.owner.deviceAddress)
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
Timber.w(e) Timber.w(e)
return@filter true // assuming it was changed due to privacy return@filter true // assuming it was changed due to privacy
@@ -199,6 +227,13 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
} }
} }
@Parcelize
class Starter : BootReceiver.Startable {
override fun start(context: Context) {
context.startForegroundService(Intent(context, RepeaterService::class.java))
}
}
private val p2pManager get() = Services.p2p!! private val p2pManager get() = Services.p2p!!
private var channel: WifiP2pManager.Channel? = null private var channel: WifiP2pManager.Channel? = null
private val binder = Binder() private val binder = Binder()
@@ -212,6 +247,9 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> onP2pConnectionChanged( WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> onP2pConnectionChanged(
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO), intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO),
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP)) intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP))
LocationManager.MODE_CHANGED_ACTION -> @TargetApi(30) {
onLocationModeChanged(intent.getBooleanExtra(LocationManager.EXTRA_LOCATION_ENABLED, false))
}
} }
} }
private val deviceListener = broadcastReceiver { _, intent -> private val deviceListener = broadcastReceiver { _, intent ->
@@ -222,7 +260,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
/** /**
* Writes and critical reads to routingManager should be protected with this context. * Writes and critical reads to routingManager should be protected with this context.
*/ */
private val dispatcher = newSingleThreadContext("RepeaterService") private val dispatcher = Dispatchers.IO.limitedParallelism(1)
override val coroutineContext = dispatcher + Job() override val coroutineContext = dispatcher + Job()
private var routingManager: RoutingManager? = null private var routingManager: RoutingManager? = null
private var persistNextGroup = false private var persistNextGroup = false
@@ -286,6 +324,36 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
} else SmartSnackbar.make(formatReason(R.string.repeater_set_oc_failure, reason)).show() } else SmartSnackbar.make(formatReason(R.string.repeater_set_oc_failure, reason)).show()
} else SmartSnackbar.make(R.string.repeater_failure_disconnected).show() } else SmartSnackbar.make(R.string.repeater_failure_disconnected).show()
} }
@RequiresApi(33)
private suspend fun setVendorElements(ve: List<ScanResult.InformationElement> = vendorElements) {
val channel = channel
if (channel != null) {
val reason = try {
p2pManager.setVendorElements(channel, ve) ?: return
} catch (e: IllegalArgumentException) {
SmartSnackbar.make(getString(R.string.repeater_set_vendor_elements_failure, e.message)).show()
return
} catch (e: UnsupportedOperationException) {
if (ve.isNotEmpty()) {
SmartSnackbar.make(getString(R.string.repeater_set_vendor_elements_failure, e.message)).show()
}
return
}
if (reason == WifiP2pManager.ERROR) {
val rootReason = try {
RootManager.use {
if (deinitPending.getAndSet(false)) it.execute(RepeaterCommands.Deinit())
it.execute(RepeaterCommands.SetVendorElements(ve))
} ?: return
} catch (e: Exception) {
Timber.w(e)
SmartSnackbar.make(e).show()
return
}
SmartSnackbar.make(formatReason(R.string.repeater_set_vendor_elements_failure, rootReason.value)).show()
} else SmartSnackbar.make(formatReason(R.string.repeater_set_vendor_elements_failure, reason)).show()
} else SmartSnackbar.make(R.string.repeater_failure_disconnected).show()
}
override fun onChannelDisconnected() { override fun onChannelDisconnected() {
channel = null channel = null
@@ -302,9 +370,34 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
} }
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (!safeMode) when (key) { when (key) {
KEY_OPERATING_CHANNEL -> launch { setOperatingChannel() } KEY_OPERATING_CHANNEL -> if (!safeMode) launch { setOperatingChannel() }
KEY_SAFE_MODE -> deinitPending.set(true) KEY_VENDOR_ELEMENTS -> if (Build.VERSION.SDK_INT >= 33) launch { setVendorElements() }
KEY_SAFE_MODE -> if (!safeMode) deinitPending.set(true)
}
}
private var p2pPoller: Job? = null
@RequiresApi(30)
private fun onLocationModeChanged(enabled: Boolean) = if (enabled) p2pPoller?.cancel() else {
SmartSnackbar.make(R.string.repeater_location_off).apply {
action(R.string.repeater_location_off_configure) {
it.context.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
}
}.show()
p2pPoller = launch(start = CoroutineStart.UNDISPATCHED) {
while (true) {
delay(1000)
val channel = channel ?: return@launch
coroutineScope {
launch(start = CoroutineStart.UNDISPATCHED) {
if (p2pManager.requestP2pState(channel) == WifiP2pManager.WIFI_P2P_STATE_DISABLED) cleanLocked()
}
val info = async(start = CoroutineStart.UNDISPATCHED) { p2pManager.requestConnectionInfo(channel) }
val group = p2pManager.requestGroupInfo(channel)
onP2pConnectionChanged(info.await(), group)
}
}
} }
} }
@@ -312,25 +405,27 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
* startService Step 1 * startService Step 1
*/ */
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
BootReceiver.startIfEnabled()
if (status != Status.IDLE) return START_NOT_STICKY if (status != Status.IDLE) return START_NOT_STICKY
val channel = channel ?: return START_NOT_STICKY.also { stopSelf() } val channel = channel ?: return START_NOT_STICKY.also { stopSelf() }
status = Status.STARTING status = Status.STARTING
// bump self to foreground location service (API 29+) to use location later, also to avoid getting killed // bump self to foreground location service (API 29+) to use location later, also to avoid getting killed
if (Build.VERSION.SDK_INT >= 26) showNotification() showNotification()
launch { launch {
registerReceiver(receiver, intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION, val filter = intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)) WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)
if (Build.VERSION.SDK_INT in 30 until 33) filter.addAction(LocationManager.MODE_CHANGED_ACTION)
registerReceiver(receiver, filter)
receiverRegistered = true receiverRegistered = true
try { val group = p2pManager.requestGroupInfo(channel)
p2pManager.requestGroupInfo(channel) {
when { when {
it == null -> doStart() group == null -> doStart()
it.isGroupOwner -> launch { if (routingManager == null) doStartLocked(it) } group.isGroupOwner -> if (routingManager == null) doStartLocked(group)
else -> { else -> {
Timber.i("Removing old group ($it)") Timber.i("Removing old group ($group)")
p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener { p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
override fun onSuccess() { override fun onSuccess() {
doStart() launch { doStart() }
} }
override fun onFailure(reason: Int) = override fun onFailure(reason: Int) =
startFailure(formatReason(R.string.repeater_remove_old_group_failure, reason)) startFailure(formatReason(R.string.repeater_remove_old_group_failure, reason))
@@ -338,33 +433,33 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
} }
} }
} }
} catch (e: SecurityException) {
Timber.w(e)
startFailure(e.readableMessage)
}
}
return START_NOT_STICKY return START_NOT_STICKY
} }
/** /**
* startService Step 2 (if a group isn't already available) * startService Step 2 (if a group isn't already available)
*/ */
private fun doStart() = launch { private suspend fun doStart() {
val listener = object : WifiP2pManager.ActionListener { val listener = object : WifiP2pManager.ActionListener {
override fun onFailure(reason: Int) { override fun onFailure(reason: Int) {
startFailure(formatReason(R.string.repeater_create_group_failure, reason), startFailure(formatReason(R.string.repeater_create_group_failure, reason),
showWifiEnable = reason == WifiP2pManager.BUSY) showWifiEnable = reason == WifiP2pManager.BUSY)
} }
override fun onSuccess() { } // wait for WIFI_P2P_CONNECTION_CHANGED_ACTION to fire to go to step 3 override fun onSuccess() {
// wait for WIFI_P2P_CONNECTION_CHANGED_ACTION to fire to go to step 3
// in order for this to happen, we need to make sure that the callbacks are firing
if (Build.VERSION.SDK_INT in 30 until 33) onLocationModeChanged(app.location?.isLocationEnabled == true)
} }
val channel = channel ?: return@launch listener.onFailure(WifiP2pManager.BUSY) }
val channel = channel ?: return listener.onFailure(WifiP2pManager.BUSY)
if (!safeMode) { if (!safeMode) {
binder.fetchPersistentGroup() binder.fetchPersistentGroup()
setOperatingChannel() setOperatingChannel()
} }
val networkName = networkName if (Build.VERSION.SDK_INT >= 33) setVendorElements()
val networkName = networkName?.toString()
val passphrase = passphrase val passphrase = passphrase
try { @SuppressLint("MissingPermission") // missing permission will simply leading to returning ERROR
if (!safeMode || networkName == null || passphrase == null) { if (!safeMode || networkName == null || passphrase.isNullOrEmpty()) {
persistNextGroup = true persistNextGroup = true
p2pManager.createGroup(channel, listener) p2pManager.createGroup(channel, listener)
} else @TargetApi(29) { } else @TargetApi(29) {
@@ -373,7 +468,12 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
mNetworkName.set(this, networkName) // bypass networkName check mNetworkName.set(this, networkName) // bypass networkName check
} catch (e: ReflectiveOperationException) { } catch (e: ReflectiveOperationException) {
Timber.w(e) Timber.w(e)
try {
setNetworkName(networkName) setNetworkName(networkName)
} catch (e: IllegalArgumentException) {
Timber.w(e)
return startFailure(e.readableMessage)
}
} }
setPassphrase(passphrase) setPassphrase(passphrase)
when (val oc = operatingChannel) { when (val oc = operatingChannel) {
@@ -389,16 +489,9 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
setGroupOperatingFrequency(SoftApConfigurationCompat.channelToFrequency(operatingBand, oc)) setGroupOperatingFrequency(SoftApConfigurationCompat.channelToFrequency(operatingBand, oc))
} }
} }
setDeviceAddress(deviceAddress?.toPlatform()) setDeviceAddress(deviceAddress)
}.build(), listener) }.build(), listener)
} }
} catch (e: SecurityException) {
Timber.w(e)
startFailure(e.readableMessage)
} catch (e: IllegalArgumentException) {
Timber.w(e)
startFailure(e.readableMessage)
}
} }
/** /**
* Used during step 2, also called when connection changed * Used during step 2, also called when connection changed
@@ -426,7 +519,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
} }
binder.group = group binder.group = group
if (persistNextGroup) { if (persistNextGroup) {
networkName = group.networkName networkName = WifiSsidCompat.fromUtf8Text(group.networkName)
passphrase = group.passphrase passphrase = group.passphrase
persistNextGroup = false persistNextGroup = false
} }
@@ -434,6 +527,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
routingManager = RoutingManager.LocalOnly(this@RepeaterService, group.`interface`!!).apply { start() } routingManager = RoutingManager.LocalOnly(this@RepeaterService, group.`interface`!!).apply { start() }
status = Status.ACTIVE status = Status.ACTIVE
showNotification(group) showNotification(group)
BootReceiver.add<RepeaterService>(Starter())
} }
private fun startFailure(msg: CharSequence, group: WifiP2pGroup? = null, showWifiEnable: Boolean = false) { private fun startFailure(msg: CharSequence, group: WifiP2pGroup? = null, showWifiEnable: Boolean = false) {
SmartSnackbar.make(msg).apply { SmartSnackbar.make(msg).apply {
@@ -451,7 +545,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
if (group == null) emptyMap() else mapOf(Pair(group.`interface`, group.clientList?.size ?: 0))) if (group == null) emptyMap() else mapOf(Pair(group.`interface`, group.clientList?.size ?: 0)))
private fun removeGroup() { private fun removeGroup() {
p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener { p2pManager.removeGroup(channel ?: return, object : WifiP2pManager.ActionListener {
override fun onSuccess() { override fun onSuccess() {
launch { cleanLocked() } launch { cleanLocked() }
} }
@@ -459,19 +553,19 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
if (reason != WifiP2pManager.BUSY) { if (reason != WifiP2pManager.BUSY) {
SmartSnackbar.make(formatReason(R.string.repeater_remove_group_failure, reason)).show() SmartSnackbar.make(formatReason(R.string.repeater_remove_group_failure, reason)).show()
} // else assuming it's already gone } // else assuming it's already gone
launch { cleanLocked() } onSuccess()
} }
}) })
} }
private fun cleanLocked() { private fun cleanLocked() {
BootReceiver.delete<RepeaterService>()
if (receiverRegistered) { if (receiverRegistered) {
ensureReceiverUnregistered(receiver) ensureReceiverUnregistered(receiver)
p2pPoller?.cancel()
receiverRegistered = false receiverRegistered = false
} }
if (Build.VERSION.SDK_INT >= 28) {
timeoutMonitor?.close() timeoutMonitor?.close()
timeoutMonitor = null timeoutMonitor = null
}
routingManager?.stop() routingManager?.stop()
routingManager = null routingManager = null
status = Status.IDLE status = Status.IDLE
@@ -484,12 +578,11 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
launch { // force clean to prevent leakage launch { // force clean to prevent leakage
cleanLocked() cleanLocked()
cancel() cancel()
dispatcher.close()
} }
app.pref.unregisterOnSharedPreferenceChangeListener(this) app.pref.unregisterOnSharedPreferenceChangeListener(this)
if (Build.VERSION.SDK_INT < 29) unregisterReceiver(deviceListener) if (Build.VERSION.SDK_INT < 29) unregisterReceiver(deviceListener)
status = Status.DESTROYED status = Status.DESTROYED
if (Build.VERSION.SDK_INT >= 27) channel?.close() channel?.close()
super.onDestroy() super.onDestroy()
} }
} }

View File

@@ -1,6 +1,5 @@
package be.mygod.vpnhotspot package be.mygod.vpnhotspot
import android.annotation.TargetApi
import android.os.Build import android.os.Build
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.Routing import be.mygod.vpnhotspot.net.Routing
@@ -15,15 +14,11 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
companion object { companion object {
private const val KEY_MASQUERADE_MODE = "service.masqueradeMode" private const val KEY_MASQUERADE_MODE = "service.masqueradeMode"
var masqueradeMode: Routing.MasqueradeMode var masqueradeMode: Routing.MasqueradeMode
@TargetApi(28) get() = app.pref.run { get() = app.pref.run {
getString(KEY_MASQUERADE_MODE, null)?.let { return@run Routing.MasqueradeMode.valueOf(it) } getString(KEY_MASQUERADE_MODE, null)?.let { return@run Routing.MasqueradeMode.valueOf(it) }
if (getBoolean("service.masquerade", true)) { // legacy settings if (getBoolean("service.masquerade", true)) { // legacy settings
Routing.MasqueradeMode.Simple Routing.MasqueradeMode.Simple
} else Routing.MasqueradeMode.None } else Routing.MasqueradeMode.None
}.let {
// older app version enabled netd for everyone. should check again here
if (Build.VERSION.SDK_INT >= 28 || it != Routing.MasqueradeMode.Netd) it
else Routing.MasqueradeMode.Simple
} }
set(value) = app.pref.edit().putString(KEY_MASQUERADE_MODE, value.name).apply() set(value) = app.pref.edit().putString(KEY_MASQUERADE_MODE, value.name).apply()
@@ -66,7 +61,7 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
private var routing: Routing? = null private var routing: Routing? = null
private var isWifi = forceWifi || TetherType.ofInterface(downstream).isWifi private var isWifi = forceWifi || TetherType.ofInterface(downstream).isWifi
fun start() = synchronized(RoutingManager) { fun start(fromMonitor: Boolean = false) = synchronized(RoutingManager) {
started = true started = true
when (val other = active.putIfAbsent(downstream, this)) { when (val other = active.putIfAbsent(downstream, this)) {
null -> { null -> {
@@ -78,14 +73,19 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
isWifi = isWifiNow isWifi = isWifiNow
} }
} }
initRoutingLocked() initRoutingLocked(fromMonitor)
} }
this -> true // already started this -> true // already started
else -> error("Double routing detected for $downstream from $caller != ${other.caller}") else -> {
val msg = "Double routing detected for $downstream from $caller != ${other.caller}"
Timber.w(RuntimeException(msg))
SmartSnackbar.make(msg).show()
false
}
} }
} }
private fun initRoutingLocked() = try { private fun initRoutingLocked(fromMonitor: Boolean = false) = try {
routing = Routing(caller, downstream).apply { routing = Routing(caller, downstream).apply {
try { try {
configure() configure()
@@ -97,10 +97,10 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
true true
} catch (e: Exception) { } catch (e: Exception) {
when (e) { when (e) {
is Routing.InterfaceNotFoundException -> Timber.d(e) is Routing.InterfaceNotFoundException -> if (!fromMonitor) Timber.d(e)
!is CancellationException -> Timber.w(e) !is CancellationException -> Timber.w(e)
} }
SmartSnackbar.make(e).show() if (e !is Routing.InterfaceNotFoundException || !fromMonitor) SmartSnackbar.make(e).show()
routing = null routing = null
false false
} }

View File

@@ -1,36 +1,35 @@
package be.mygod.vpnhotspot package be.mygod.vpnhotspot
import android.annotation.TargetApi
import android.app.* import android.app.*
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import java.util.* import java.util.*
object ServiceNotification { object ServiceNotification {
private const val CHANNEL = "tethering" private const val CHANNEL_ACTIVE = "tethering"
private const val CHANNEL_ID = 1 private const val CHANNEL_INACTIVE = "tethering-inactive"
private const val NOTIFICATION_ID = 1
private val deviceCountsMap = WeakHashMap<Service, Map<String, Int>>() private val deviceCountsMap = WeakHashMap<Service, Map<String, Int>>()
private val inactiveMap = WeakHashMap<Service, List<String>>() private val inactiveMap = WeakHashMap<Service, List<String>>()
private val manager = app.getSystemService<NotificationManager>()!! private val manager = app.getSystemService<NotificationManager>()!!
private fun buildNotification(context: Context): Notification { private fun buildNotification(context: Context): Notification {
val builder = NotificationCompat.Builder(context, CHANNEL)
.setWhen(0)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setColor(ContextCompat.getColor(context, R.color.colorPrimary))
.setContentTitle(context.getText(R.string.notification_tethering_title))
.setSmallIcon(R.drawable.ic_quick_settings_tile_on)
.setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
val deviceCounts = deviceCountsMap.values.flatMap { it.entries }.sortedBy { it.key } val deviceCounts = deviceCountsMap.values.flatMap { it.entries }.sortedBy { it.key }
val inactive = inactiveMap.values.flatten() val inactive = inactiveMap.values.flatten()
val isInactive = inactive.isNotEmpty() && deviceCounts.isEmpty()
val builder = Notification.Builder(context, if (isInactive) CHANNEL_INACTIVE else CHANNEL_ACTIVE).apply {
setWhen(0)
setCategory(Notification.CATEGORY_SERVICE)
setColor(context.resources.getColor(R.color.colorPrimary, context.theme))
setContentTitle(context.getText(R.string.notification_tethering_title))
setSmallIcon(R.drawable.ic_quick_settings_tile_on)
setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
setVisibility(Notification.VISIBILITY_PUBLIC)
}
var lines = deviceCounts.map { (dev, size) -> var lines = deviceCounts.map { (dev, size) ->
context.resources.getQuantityString(R.plurals.notification_connected_devices, size, size, dev) context.resources.getQuantityString(R.plurals.notification_connected_devices, size, size, dev)
} }
@@ -40,13 +39,13 @@ object ServiceNotification {
return if (lines.size <= 1) builder.setContentText(lines.singleOrNull()).build() else { return if (lines.size <= 1) builder.setContentText(lines.singleOrNull()).build() else {
val deviceCount = deviceCounts.sumOf { it.value } val deviceCount = deviceCounts.sumOf { it.value }
val interfaceCount = deviceCounts.size + inactive.size val interfaceCount = deviceCounts.size + inactive.size
NotificationCompat.BigTextStyle(builder Notification.BigTextStyle().apply {
.setContentText(context.resources.getQuantityString(R.plurals.notification_connected_devices, setBuilder(builder.setContentText(context.resources.getQuantityString(
deviceCount, deviceCount, R.plurals.notification_connected_devices, deviceCount, deviceCount,
context.resources.getQuantityString(R.plurals.notification_interfaces, context.resources.getQuantityString(R.plurals.notification_interfaces,
interfaceCount, interfaceCount)))) interfaceCount, interfaceCount))))
.bigText(lines.joinToString("\n")) bigText(lines.joinToString("\n"))
.build()!! }.build()!!
} }
} }
@@ -54,26 +53,29 @@ object ServiceNotification {
synchronized(this) { synchronized(this) {
deviceCountsMap[service] = deviceCounts deviceCountsMap[service] = deviceCounts
if (inactive.isEmpty()) inactiveMap.remove(service) else inactiveMap[service] = inactive if (inactive.isEmpty()) inactiveMap.remove(service) else inactiveMap[service] = inactive
service.startForeground(CHANNEL_ID, buildNotification(service)) service.startForeground(NOTIFICATION_ID, buildNotification(service))
} }
} }
fun stopForeground(service: Service) = synchronized(this) { fun stopForeground(service: Service) = synchronized(this) {
deviceCountsMap.remove(service) deviceCountsMap.remove(service) ?: return@synchronized
if (deviceCountsMap.isEmpty()) service.stopForeground(true) else { val shutdown = deviceCountsMap.isEmpty()
service.stopForeground(false) service.stopForeground(if (shutdown) Service.STOP_FOREGROUND_REMOVE else Service.STOP_FOREGROUND_DETACH)
manager.notify(CHANNEL_ID, buildNotification(service)) if (!shutdown) manager.notify(NOTIFICATION_ID, buildNotification(service))
}
} }
fun updateNotificationChannels() { fun updateNotificationChannels() {
if (Build.VERSION.SDK_INT >= 26) @TargetApi(26) { NotificationChannel(CHANNEL_ACTIVE,
val tethering = NotificationChannel(CHANNEL, app.getText(R.string.notification_channel_tethering), NotificationManager.IMPORTANCE_LOW).apply {
app.getText(R.string.notification_channel_tethering), NotificationManager.IMPORTANCE_LOW) lockscreenVisibility = Notification.VISIBILITY_PUBLIC
tethering.lockscreenVisibility = Notification.VISIBILITY_PUBLIC manager.createNotificationChannel(this)
manager.createNotificationChannel(tethering) }
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 // remove old service channels
manager.deleteNotificationChannel("hotspot") manager.deleteNotificationChannel("hotspot")
manager.deleteNotificationChannel("repeater") manager.deleteNotificationChannel("repeater")
} }
} }
}

View File

@@ -1,6 +1,5 @@
package be.mygod.vpnhotspot package be.mygod.vpnhotspot
import android.annotation.TargetApi
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@@ -8,20 +7,19 @@ import androidx.core.content.FileProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference import androidx.preference.TwoStatePreference
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.TetherOffloadManager import be.mygod.vpnhotspot.net.TetherOffloadManager
import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor
import be.mygod.vpnhotspot.net.monitor.IpMonitor import be.mygod.vpnhotspot.net.monitor.IpMonitor
import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
import be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreferenceDialogFragment import be.mygod.vpnhotspot.preference.AutoCompleteNetworkPreferenceDialogFragment
import be.mygod.vpnhotspot.preference.SharedPreferenceDataStore import be.mygod.vpnhotspot.preference.SharedPreferenceDataStore
import be.mygod.vpnhotspot.preference.SummaryFallbackProvider import be.mygod.vpnhotspot.preference.SummaryFallbackProvider
import be.mygod.vpnhotspot.root.Dump import be.mygod.vpnhotspot.root.Dump
import be.mygod.vpnhotspot.root.RootManager import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.util.Services import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.allInterfaceNames
import be.mygod.vpnhotspot.util.launchUrl import be.mygod.vpnhotspot.util.launchUrl
import be.mygod.vpnhotspot.util.showAllowingStateLoss import be.mygod.vpnhotspot.util.showAllowingStateLoss
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
@@ -40,7 +38,6 @@ import kotlin.system.exitProcess
class SettingsPreferenceFragment : PreferenceFragmentCompat() { class SettingsPreferenceFragment : PreferenceFragmentCompat() {
private fun Preference.remove() = parent!!.removePreference(this) private fun Preference.remove() = parent!!.removePreference(this)
@TargetApi(26)
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
// handle complicated default value and possible system upgrades // handle complicated default value and possible system upgrades
WifiDoubleLock.mode = WifiDoubleLock.mode WifiDoubleLock.mode = WifiDoubleLock.mode
@@ -50,11 +47,10 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
addPreferencesFromResource(R.xml.pref_settings) addPreferencesFromResource(R.xml.pref_settings)
SummaryFallbackProvider(findPreference(UpstreamMonitor.KEY)!!) SummaryFallbackProvider(findPreference(UpstreamMonitor.KEY)!!)
SummaryFallbackProvider(findPreference(FallbackUpstreamMonitor.KEY)!!) SummaryFallbackProvider(findPreference(FallbackUpstreamMonitor.KEY)!!)
findPreference<SwitchPreference>("system.enableTetherOffload")!!.apply { findPreference<TwoStatePreference>("system.enableTetherOffload")!!.apply {
if (TetherOffloadManager.supported) {
isChecked = TetherOffloadManager.enabled isChecked = TetherOffloadManager.enabled
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
if (TetherOffloadManager.enabled != newValue) viewLifecycleOwner.lifecycleScope.launchWhenCreated { if (TetherOffloadManager.enabled != newValue) viewLifecycleOwner.lifecycleScope.launch {
isEnabled = false isEnabled = false
try { try {
TetherOffloadManager.setEnabled(newValue as Boolean) TetherOffloadManager.setEnabled(newValue as Boolean)
@@ -68,16 +64,11 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
} }
false false
} }
} else remove()
} }
val boot = findPreference<SwitchPreference>("service.repeater.startOnBoot")!! findPreference<TwoStatePreference>(BootReceiver.KEY)!!.setOnPreferenceChangeListener { _, value ->
if (Services.p2p != null) { BootReceiver.onUserSettingUpdated(value as Boolean)
boot.setOnPreferenceChangeListener { _, value ->
BootReceiver.enabled = value as Boolean
true true
} }
boot.isChecked = BootReceiver.enabled
} else boot.remove()
if (Services.p2p == null || !RepeaterService.safeModeConfigurable) { if (Services.p2p == null || !RepeaterService.safeModeConfigurable) {
val safeMode = findPreference<Preference>(RepeaterService.KEY_SAFE_MODE)!! val safeMode = findPreference<Preference>(RepeaterService.KEY_SAFE_MODE)!!
safeMode.remove() safeMode.remove()
@@ -130,7 +121,7 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putExtra(Intent.EXTRA_STREAM, .putExtra(Intent.EXTRA_STREAM,
FileProvider.getUriForFile(context, "be.mygod.vpnhotspot.log", logFile)), FileProvider.getUriForFile(context, "be.mygod.vpnhotspot.log", logFile)),
context.getString(R.string.abc_shareactionprovider_share_with))) context.getString(androidx.appcompat.R.string.abc_shareactionprovider_share_with)))
} }
true true
} }
@@ -148,16 +139,12 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
} }
} }
override fun onDisplayPreferenceDialog(preference: Preference) { override fun onDisplayPreferenceDialog(preference: Preference) = when (preference.key) {
when (preference.key) {
UpstreamMonitor.KEY, FallbackUpstreamMonitor.KEY -> UpstreamMonitor.KEY, FallbackUpstreamMonitor.KEY ->
AlwaysAutoCompleteEditTextPreferenceDialogFragment().apply { AutoCompleteNetworkPreferenceDialogFragment().apply {
setArguments(preference.key, Services.connectivity.allNetworks.mapNotNull { setArguments(preference.key)
Services.connectivity.getLinkProperties(it)?.allInterfaceNames
}.flatten().toTypedArray())
setTargetFragment(this@SettingsPreferenceFragment, 0) setTargetFragment(this@SettingsPreferenceFragment, 0)
}.showAllowingStateLoss(parentFragmentManager, preference.key) }.showAllowingStateLoss(parentFragmentManager, preference.key)
else -> super.onDisplayPreferenceDialog(preference) else -> super.onDisplayPreferenceDialog(preference)
} }
} }
}

View File

@@ -1,7 +1,7 @@
package be.mygod.vpnhotspot package be.mygod.vpnhotspot
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.Routing import be.mygod.vpnhotspot.net.Routing
@@ -10,6 +10,7 @@ import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
import be.mygod.vpnhotspot.util.Event0 import be.mygod.vpnhotspot.util.Event0
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@@ -17,6 +18,7 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
companion object { companion object {
const val EXTRA_ADD_INTERFACES = "interface.add" const val EXTRA_ADD_INTERFACES = "interface.add"
const val EXTRA_ADD_INTERFACE_MONITOR = "interface.add.monitor" const val EXTRA_ADD_INTERFACE_MONITOR = "interface.add.monitor"
const val EXTRA_ADD_INTERFACES_MONITOR = "interface.adds.monitor"
const val EXTRA_REMOVE_INTERFACE = "interface.remove" const val EXTRA_REMOVE_INTERFACE = "interface.remove"
} }
@@ -39,10 +41,19 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
} }
} }
@Parcelize
data class Starter(val monitored: ArrayList<String>) : BootReceiver.Startable {
override fun start(context: Context) {
context.startForegroundService(Intent(context, TetheringService::class.java).apply {
putStringArrayListExtra(EXTRA_ADD_INTERFACES_MONITOR, monitored)
})
}
}
/** /**
* Writes and critical reads to downstreams should be protected with this context. * Writes and critical reads to downstreams should be protected with this context.
*/ */
private val dispatcher = newSingleThreadContext("TetheringService") private val dispatcher = Dispatchers.IO.limitedParallelism(1)
override val coroutineContext = dispatcher + Job() override val coroutineContext = dispatcher + Job()
private val binder = Binder() private val binder = Binder()
private val downstreams = ConcurrentHashMap<String, Downstream>() private val downstreams = ConcurrentHashMap<String, Downstream>()
@@ -55,7 +66,7 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
val toRemove = downstreams.toMutableMap() // make a copy val toRemove = downstreams.toMutableMap() // make a copy
for (iface in interfaces) { for (iface in interfaces) {
val downstream = toRemove.remove(iface) ?: continue val downstream = toRemove.remove(iface) ?: continue
if (downstream.monitor) downstream.start() if (downstream.monitor && !downstream.start()) downstream.stop()
} }
for ((iface, downstream) in toRemove) { for ((iface, downstream) in toRemove) {
if (!downstream.monitor) check(downstreams.remove(iface, downstream)) if (!downstream.monitor) check(downstreams.remove(iface, downstream))
@@ -81,6 +92,10 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
ServiceNotification.stopForeground(this) ServiceNotification.stopForeground(this)
stopSelf() stopSelf()
} else { } else {
binder.monitoredIfaces.also {
if (it.isEmpty()) BootReceiver.delete<TetheringService>()
else BootReceiver.add<TetheringService>(Starter(ArrayList(it)))
}
if (!callbackRegistered) { if (!callbackRegistered) {
callbackRegistered = true callbackRegistered = true
TetheringManager.registerTetheringEventCallbackCompat(this, this) TetheringManager.registerTetheringEventCallbackCompat(this, this)
@@ -94,8 +109,9 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
override fun onBind(intent: Intent?) = binder override fun onBind(intent: Intent?) = binder
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
BootReceiver.startIfEnabled()
// call this first just in case we are shutting down immediately // call this first just in case we are shutting down immediately
if (Build.VERSION.SDK_INT >= 26) updateNotification() updateNotification()
launch { launch {
if (intent != null) { if (intent != null) {
for (iface in intent.getStringArrayExtra(EXTRA_ADD_INTERFACES) ?: emptyArray()) { for (iface in intent.getStringArrayExtra(EXTRA_ADD_INTERFACES) ?: emptyArray()) {
@@ -103,10 +119,12 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
if (start()) check(downstreams.put(iface, this) == null) else stop() if (start()) check(downstreams.put(iface, this) == null) else stop()
} }
} }
intent.getStringExtra(EXTRA_ADD_INTERFACE_MONITOR)?.also { iface -> val monitorList = intent.getStringArrayListExtra(EXTRA_ADD_INTERFACES_MONITOR) ?:
intent.getStringExtra(EXTRA_ADD_INTERFACE_MONITOR)?.let { listOf(it) }
if (!monitorList.isNullOrEmpty()) for (iface in monitorList) {
val downstream = downstreams[iface] val downstream = downstreams[iface]
if (downstream == null) Downstream(this@TetheringService, iface, true).apply { if (downstream == null) Downstream(this@TetheringService, iface, true).apply {
start() if (!start(true)) stop()
check(downstreams.put(iface, this) == null) check(downstreams.put(iface, this) == null)
downstreams[iface] = this downstreams[iface] = this
} else downstream.monitor = true } else downstream.monitor = true
@@ -120,10 +138,10 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
override fun onDestroy() { override fun onDestroy() {
launch { launch {
BootReceiver.delete<TetheringService>()
unregisterReceiver() unregisterReceiver()
downstreams.values.forEach { it.stop() } // force clean to prevent leakage downstreams.values.forEach { it.stop() } // force clean to prevent leakage
cancel() cancel()
dispatcher.close()
} }
super.onDestroy() super.onDestroy()
} }
@@ -135,4 +153,8 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
callbackRegistered = false callbackRegistered = false
} }
} }
override fun updateNotification() {
launch { super.updateNotification() }
}
} }

View File

@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot.client package be.mygod.vpnhotspot.client
import android.net.MacAddress
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.Spanned import android.text.Spanned
import android.text.style.StrikethroughSpan import android.text.style.StrikethroughSpan
@@ -9,7 +10,6 @@ import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.net.InetAddressComparator import be.mygod.vpnhotspot.net.InetAddressComparator
import be.mygod.vpnhotspot.net.IpNeighbour import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.TetherType import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.room.AppDatabase import be.mygod.vpnhotspot.room.AppDatabase
import be.mygod.vpnhotspot.room.ClientRecord import be.mygod.vpnhotspot.room.ClientRecord
@@ -18,7 +18,7 @@ import be.mygod.vpnhotspot.util.makeMacSpan
import java.net.InetAddress import java.net.InetAddress
import java.util.* import java.util.*
open class Client(val mac: MacAddressCompat, val iface: String) { open class Client(val mac: MacAddress, val iface: String) {
companion object DiffCallback : DiffUtil.ItemCallback<Client>() { companion object DiffCallback : DiffUtil.ItemCallback<Client>() {
override fun areItemsTheSame(oldItem: Client, newItem: Client) = override fun areItemsTheSame(oldItem: Client, newItem: Client) =
oldItem.iface == newItem.iface && oldItem.mac == newItem.mac oldItem.iface == newItem.iface && oldItem.mac == newItem.mac
@@ -42,10 +42,10 @@ open class Client(val mac: MacAddressCompat, val iface: String) {
* we hijack the get title process to check if we need to perform MacLookup, * we hijack the get title process to check if we need to perform MacLookup,
* as record might not be initialized in other more appropriate places * as record might not be initialized in other more appropriate places
*/ */
SpannableStringBuilder(if (record.nickname.isEmpty()) { SpannableStringBuilder(record.nickname.ifEmpty {
if (record.macLookupPending) MacLookup.perform(mac) if (record.macLookupPending) MacLookup.perform(mac)
macIface macIface
} else emojize(record.nickname)).apply { }).apply {
if (record.blocked) setSpan(StrikethroughSpan(), 0, length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) if (record.blocked) setSpan(StrikethroughSpan(), 0, length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
} }
} }
@@ -65,7 +65,7 @@ open class Client(val mac: MacAddressCompat, val iface: String) {
}.trimEnd() }.trimEnd()
} }
fun obtainRecord() = record.value ?: ClientRecord(mac.addr) fun obtainRecord() = record.value ?: ClientRecord(mac)
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true

View File

@@ -3,11 +3,12 @@ package be.mygod.vpnhotspot.client
import android.content.ComponentName import android.content.ComponentName
import android.content.IntentFilter import android.content.IntentFilter
import android.content.ServiceConnection import android.content.ServiceConnection
import android.net.MacAddress
import android.net.wifi.p2p.WifiP2pDevice import android.net.wifi.p2p.WifiP2pDevice
import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.Parcelable import android.os.Parcelable
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.os.BuildCompat
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@@ -15,8 +16,6 @@ import androidx.lifecycle.ViewModel
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.RepeaterService import be.mygod.vpnhotspot.RepeaterService
import be.mygod.vpnhotspot.net.IpNeighbour import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toCompat
import be.mygod.vpnhotspot.net.TetherType import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
@@ -38,7 +37,7 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
private var repeater: RepeaterService.Binder? = null private var repeater: RepeaterService.Binder? = null
private var p2p: Collection<WifiP2pDevice> = emptyList() private var p2p: Collection<WifiP2pDevice> = emptyList()
private var wifiAp = emptyList<Pair<String, MacAddressCompat>>() private var wifiAp = emptyList<Pair<String, MacAddress>>()
private var neighbours: Collection<IpNeighbour> = emptyList() private var neighbours: Collection<IpNeighbour> = emptyList()
val clients = MutableLiveData<List<Client>>() val clients = MutableLiveData<List<Client>>()
val fullMode = object : DefaultLifecycleObserver { val fullMode = object : DefaultLifecycleObserver {
@@ -51,10 +50,10 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
} }
private fun populateClients() { private fun populateClients() {
val clients = HashMap<Pair<String, MacAddressCompat>, Client>() val clients = HashMap<Pair<String, MacAddress>, Client>()
repeater?.group?.`interface`?.let { p2pInterface -> repeater?.group?.`interface`?.let { p2pInterface ->
for (client in p2p) { for (client in p2p) {
val addr = MacAddressCompat.fromString(client.deviceAddress!!) val addr = MacAddress.fromString(client.deviceAddress!!)
clients[p2pInterface to addr] = object : Client(addr, p2pInterface) { clients[p2pInterface to addr] = object : Client(addr, p2pInterface) {
override val icon: Int get() = TetherType.WIFI_P2P.icon override val icon: Int get() = TetherType.WIFI_P2P.icon
} }
@@ -87,10 +86,10 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
override fun onStart(owner: LifecycleOwner) { override fun onStart(owner: LifecycleOwner) {
app.registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED)) app.registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
IpNeighbourMonitor.registerCallback(this, false) IpNeighbourMonitor.registerCallback(this, false)
if (BuildCompat.isAtLeastS()) WifiApCommands.registerSoftApCallback(this) if (Build.VERSION.SDK_INT >= 31) WifiApCommands.registerSoftApCallback(this)
} }
override fun onStop(owner: LifecycleOwner) { override fun onStop(owner: LifecycleOwner) {
if (BuildCompat.isAtLeastS()) WifiApCommands.unregisterSoftApCallback(this) if (Build.VERSION.SDK_INT >= 31) WifiApCommands.unregisterSoftApCallback(this)
IpNeighbourMonitor.unregisterCallback(this) IpNeighbourMonitor.unregisterCallback(this)
app.unregisterReceiver(receiver) app.unregisterReceiver(receiver)
} }
@@ -118,7 +117,7 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
override fun onConnectedClientsChanged(clients: List<Parcelable>) { override fun onConnectedClientsChanged(clients: List<Parcelable>) {
wifiAp = clients.mapNotNull { wifiAp = clients.mapNotNull {
val client = WifiClient(it) val client = WifiClient(it)
client.apInstanceIdentifier?.run { this to client.macAddress.toCompat() } client.apInstanceIdentifier?.run { this to client.macAddress }
} }
} }
} }

View File

@@ -1,6 +1,7 @@
package be.mygod.vpnhotspot.client package be.mygod.vpnhotspot.client
import android.content.DialogInterface import android.content.DialogInterface
import android.net.MacAddress
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
@@ -20,6 +21,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.withStarted
import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
@@ -30,7 +32,6 @@ import be.mygod.vpnhotspot.Empty
import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.databinding.FragmentClientsBinding import be.mygod.vpnhotspot.databinding.FragmentClientsBinding
import be.mygod.vpnhotspot.databinding.ListitemClientBinding import be.mygod.vpnhotspot.databinding.ListitemClientBinding
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.TetherType import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
import be.mygod.vpnhotspot.net.monitor.TrafficRecorder import be.mygod.vpnhotspot.net.monitor.TrafficRecorder
@@ -41,21 +42,24 @@ import be.mygod.vpnhotspot.util.format
import be.mygod.vpnhotspot.util.showAllowingStateLoss import be.mygod.vpnhotspot.util.showAllowingStateLoss
import be.mygod.vpnhotspot.util.toPluralInt import be.mygod.vpnhotspot.util.toPluralInt
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.* import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.text.NumberFormat import java.text.NumberFormat
class ClientsFragment : Fragment() { class ClientsFragment : Fragment() {
// FIXME: value class does not work with Parcelize
@Parcelize @Parcelize
data class NicknameArg(val mac: Long, val nickname: CharSequence) : Parcelable data class NicknameArg(val mac: MacAddress, val nickname: CharSequence) : Parcelable
class NicknameDialogFragment : AlertDialogFragment<NicknameArg, Empty>() { class NicknameDialogFragment : AlertDialogFragment<NicknameArg, Empty>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) { override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
setView(R.layout.dialog_nickname) setView(R.layout.dialog_nickname)
setTitle(getString(R.string.clients_nickname_title, MacAddressCompat(arg.mac).toString())) setTitle(getString(R.string.clients_nickname_title, arg.mac))
setPositiveButton(android.R.string.ok, listener) setPositiveButton(android.R.string.ok, listener)
setNegativeButton(android.R.string.cancel, null) setNegativeButton(android.R.string.cancel, null)
setNeutralButton(emojize(getText(R.string.clients_nickname_set_to_vendor)), listener) setNeutralButton(getText(R.string.clients_nickname_set_to_vendor), listener)
} }
override fun onCreateDialog(savedInstanceState: Bundle?) = super.onCreateDialog(savedInstanceState).apply { override fun onCreateDialog(savedInstanceState: Bundle?) = super.onCreateDialog(savedInstanceState).apply {
@@ -64,7 +68,7 @@ class ClientsFragment : Fragment() {
} }
override fun onClick(dialog: DialogInterface?, which: Int) { override fun onClick(dialog: DialogInterface?, which: Int) {
val mac = MacAddressCompat(arg.mac) val mac = arg.mac
when (which) { when (which) {
DialogInterface.BUTTON_POSITIVE -> { DialogInterface.BUTTON_POSITIVE -> {
val newNickname = this.dialog!!.findViewById<EditText>(android.R.id.edit).text val newNickname = this.dialog!!.findViewById<EditText>(android.R.id.edit).text
@@ -84,7 +88,7 @@ class ClientsFragment : Fragment() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) { override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
val context = context val context = context
val resources = resources val resources = resources
val locale = resources.configuration.locale val locale = resources.configuration.locales[0]
setTitle(getText(R.string.clients_stats_title).format(locale, arg.title)) setTitle(getText(R.string.clients_stats_title).format(locale, arg.title))
val format = NumberFormat.getIntegerInstance(locale) val format = NumberFormat.getIntegerInstance(locale)
setMessage("%s\n%s\n%s".format( setMessage("%s\n%s\n%s".format(
@@ -135,7 +139,7 @@ class ClientsFragment : Fragment() {
R.id.nickname -> { R.id.nickname -> {
val client = binding.client ?: return false val client = binding.client ?: return false
NicknameDialogFragment().apply { NicknameDialogFragment().apply {
arg(NicknameArg(client.mac.addr, client.nickname)) arg(NicknameArg(client.mac, client.nickname))
}.showAllowingStateLoss(parentFragmentManager) }.showAllowingStateLoss(parentFragmentManager)
true true
} }
@@ -155,14 +159,16 @@ class ClientsFragment : Fragment() {
true true
} }
R.id.stats -> { R.id.stats -> {
binding.client?.let { client -> val client = binding.client
viewLifecycleOwner.lifecycleScope.launchWhenCreated { val title = client?.title?.value ?: return false
withContext(Dispatchers.Unconfined) { viewLifecycleOwner.lifecycleScope.launch {
StatsDialogFragment().apply { val stats = withContext(Dispatchers.Unconfined) {
arg(StatsArg(client.title.value ?: return@withContext, AppDatabase.instance.trafficRecordDao.queryStats(client.mac)
AppDatabase.instance.trafficRecordDao.queryStats(client.mac.addr)))
}.showAllowingStateLoss(parentFragmentManager)
} }
withStarted {
StatsDialogFragment().apply {
arg(StatsArg(title, stats))
}.showAllowingStateLoss(parentFragmentManager)
} }
} }
true true
@@ -201,9 +207,7 @@ class ClientsFragment : Fragment() {
check(newRecord.receivedPackets == oldRecord.receivedPackets) check(newRecord.receivedPackets == oldRecord.receivedPackets)
check(newRecord.receivedBytes == oldRecord.receivedBytes) check(newRecord.receivedBytes == oldRecord.receivedBytes)
} else { } else {
val rate = rates.computeIfAbsent(newRecord.downstream to MacAddressCompat(newRecord.mac)) { val rate = rates.computeIfAbsent(newRecord.downstream to newRecord.mac) { TrafficRate() }
TrafficRate()
}
if (rate.send < 0 || rate.receive < 0) { if (rate.send < 0 || rate.receive < 0) {
rate.send = 0 rate.send = 0
rate.receive = 0 rate.receive = 0
@@ -218,7 +222,7 @@ class ClientsFragment : Fragment() {
private lateinit var binding: FragmentClientsBinding private lateinit var binding: FragmentClientsBinding
private val adapter = ClientAdapter() private val adapter = ClientAdapter()
private var rates = mutableMapOf<Pair<String, MacAddressCompat>, TrafficRate>() private var rates = mutableMapOf<Pair<String, MacAddress>, TrafficRate>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentClientsBinding.inflate(inflater, container, false) binding = FragmentClientsBinding.inflate(inflater, container, false)
@@ -237,14 +241,19 @@ class ClientsFragment : Fragment() {
override fun onStart() { override fun onStart() {
// icon might be changed due to TetherType changes // icon might be changed due to TetherType changes
if (Build.VERSION.SDK_INT >= 30) TetherType.listener[this] = { if (Build.VERSION.SDK_INT >= 30) TetherType.listener[this] = {
lifecycleScope.launchWhenStarted { adapter.notifyItemRangeChanged(0, adapter.size.await()) } lifecycleScope.launch {
val size = adapter.size.await()
withStarted { adapter.notifyItemRangeChanged(0, size) }
}
} }
super.onStart() super.onStart()
// we just put these two thing together as this is the only place we need to use this event for now // we just put these two thing together as this is the only place we need to use this event for now
TrafficRecorder.foregroundListeners[this] = { newRecords, oldRecords -> TrafficRecorder.foregroundListeners[this] = { newRecords, oldRecords ->
lifecycleScope.launchWhenStarted { adapter.updateTraffic(newRecords, oldRecords) } lifecycleScope.launch {
withStarted { adapter.updateTraffic(newRecords, oldRecords) }
} }
lifecycleScope.launchWhenStarted { }
lifecycleScope.launch {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
TrafficRecorder.rescheduleUpdate() // next schedule time might be 1 min, force reschedule to <= 1s TrafficRecorder.rescheduleUpdate() // next schedule time might be 1 min, force reschedule to <= 1s
} }

View File

@@ -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
}

View File

@@ -1,78 +1,134 @@
package be.mygod.vpnhotspot.client package be.mygod.vpnhotspot.client
import android.content.Context import android.content.Context
import android.os.Build import android.net.MacAddress
import androidx.annotation.MainThread import androidx.annotation.MainThread
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.room.AppDatabase import be.mygod.vpnhotspot.room.AppDatabase
import be.mygod.vpnhotspot.util.connectCancellable
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import timber.log.Timber import timber.log.Timber
import java.net.HttpURLConnection import java.io.File
import java.net.URL import java.io.FileNotFoundException
import java.io.IOException
import java.net.HttpCookie
import java.util.Scanner
import java.util.regex.Pattern
/** /**
* This class generates a default nickname for new clients. * This class generates a default nickname for new clients.
*/ */
object MacLookup { object MacLookup {
class UnexpectedError(val mac: MacAddressCompat, val error: String) : JSONException("") { class UnexpectedError(val mac: MacAddress, val error: String) : JSONException("") {
private fun formatMessage(context: Context) = private fun formatMessage(context: Context) =
context.getString(R.string.clients_mac_lookup_unexpected_error, mac.toOui(), error) context.getString(R.string.clients_mac_lookup_unexpected_error,
mac.toByteArray().joinToString("") { "%02x".format(it) }.substring(0, 9), error)
override val message get() = formatMessage(app.english) override val message get() = formatMessage(app.english)
override fun getLocalizedMessage() = formatMessage(app) override fun getLocalizedMessage() = formatMessage(app)
} }
private val macLookupBusy = mutableMapOf<MacAddressCompat, Pair<HttpURLConnection, Job>>() private object SessionManager {
private const val CACHE_FILENAME = "maclookup_sessioncache"
private const val COOKIE_SESSION = "mac_address_vendor_lookup_session"
private val csrfPattern = Pattern.compile("<meta\\s+name=\"csrf-token\"\\s+content=\"([^\"]*)\"",
Pattern.CASE_INSENSITIVE)
private var sessionCache: List<String>?
get() = try {
File(app.deviceStorage.cacheDir, CACHE_FILENAME).readText().split('\n', limit = 2)
} catch (_: FileNotFoundException) {
null
}
set(value) = File(app.deviceStorage.cacheDir, CACHE_FILENAME).run {
if (value != null) writeText(value.joinToString("\n")) else if (!delete()) writeText("")
}
private val mutex = Mutex()
private suspend fun refreshSessionCache() = connectCancellable("https://macaddress.io/api") { conn ->
val cookies = conn.headerFields["set-cookie"] ?: throw IOException("Missing cookies")
var mavls: HttpCookie? = null
for (header in cookies) for (cookie in HttpCookie.parse(header)) {
if (cookie.name == COOKIE_SESSION) mavls = cookie
}
if (mavls == null) throw IOException("Missing set-cookie $COOKIE_SESSION")
val token = conn.inputStream.use { Scanner(it).findWithinHorizon(csrfPattern, 0) }
?: throw IOException("Missing csrf-token")
listOf(mavls.toString(), csrfPattern.matcher(token).run {
check(matches())
group(1)!!
}).also { sessionCache = it }
}
suspend fun obtain(forceNew: Boolean): Pair<HttpCookie, String> = mutex.withLock {
val sessionCache = (if (forceNew) null else sessionCache) ?: refreshSessionCache()
HttpCookie.parse(sessionCache[0]).single() to sessionCache[1]
}
}
private val macLookupBusy = mutableMapOf<MacAddress, Job>()
// http://en.wikipedia.org/wiki/ISO_3166-1 // http://en.wikipedia.org/wiki/ISO_3166-1
private val countryCodeRegex = "(?:^|[^A-Z])([A-Z]{2})[\\s\\d]*$".toRegex() private val countryCodeRegex = "(?:^|[^A-Z])([A-Z]{2})[\\s\\d]*$".toRegex()
@MainThread @MainThread
fun abort(mac: MacAddressCompat) = macLookupBusy.remove(mac)?.let { (conn, job) -> fun abort(mac: MacAddress) = macLookupBusy.remove(mac)?.cancel()
job.cancel()
if (Build.VERSION.SDK_INT < 26) GlobalScope.launch(Dispatchers.IO) { conn.disconnect() } else conn.disconnect()
}
@MainThread @MainThread
fun perform(mac: MacAddressCompat, explicit: Boolean = false) { fun perform(mac: MacAddress, explicit: Boolean = false) {
abort(mac) abort(mac)
val conn = URL("https://macvendors.co/api/$mac").openConnection() as HttpURLConnection macLookupBusy[mac] = GlobalScope.launch(Dispatchers.IO) {
macLookupBusy[mac] = conn to GlobalScope.launch(Dispatchers.IO) {
try { try {
val response = conn.inputStream.bufferedReader().readText() var response: String? = null
val obj = JSONObject(response).getJSONObject("result") for (tries in 0 until 5) {
obj.opt("error")?.also { throw UnexpectedError(mac, it.toString()) } val (cookie, csrf) = SessionManager.obtain(tries > 0)
val company = obj.getString("company") response = connectCancellable("https://macaddress.io/mac-address-lookup") { conn ->
val match = extractCountry(mac, response, obj) conn.requestMethod = "POST"
val result = if (match != null) { conn.setRequestProperty("content-type", "application/json")
String(match.groupValues[1].flatMap { listOf('\uD83C', it + 0xDDA5) }.toCharArray()) + ' ' + company conn.setRequestProperty("cookie", "${cookie.name}=${cookie.value}")
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 company
} else null
AppDatabase.instance.clientRecordDao.upsert(mac) { AppDatabase.instance.clientRecordDao.upsert(mac) {
nickname = result if (result != null) nickname = result
macLookupPending = false macLookupPending = false
} }
} catch (e: JSONException) { } catch (_: CancellationException) {
if ((e as? UnexpectedError)?.error == "no result") {
// no vendor found, we should not retry in the future
AppDatabase.instance.clientRecordDao.upsert(mac) { macLookupPending = false }
} else Timber.w(e)
if (explicit) SmartSnackbar.make(e).show()
} catch (e: Throwable) { } catch (e: Throwable) {
Timber.d(e) Timber.w(e)
if (explicit) SmartSnackbar.make(e).show() if (explicit) SmartSnackbar.make(e).show()
} }
} }
} }
private fun extractCountry(mac: MacAddressCompat, response: String, obj: JSONObject): MatchResult? { private fun extractCountry(mac: MacAddress, response: String, obj: JSONObject): MatchResult? {
countryCodeRegex.matchEntire(obj.optString("country"))?.also { return it } countryCodeRegex.matchEntire(obj.optString("countryCode"))?.also { return it }
val address = obj.optString("address") val address = obj.optString("companyAddress")
if (address.isBlank()) return null if (address.isBlank()) return null
countryCodeRegex.find(address)?.also { return it } countryCodeRegex.find(address)?.also { return it }
Timber.w(UnexpectedError(mac, response)) Timber.w(UnexpectedError(mac, response))

View File

@@ -1,6 +1,6 @@
package be.mygod.vpnhotspot.manage package be.mygod.vpnhotspot.manage
import android.annotation.TargetApi import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothProfile import android.bluetooth.BluetoothProfile
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
@@ -8,8 +8,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.os.BuildCompat
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.util.broadcastReceiver import be.mygod.vpnhotspot.util.broadcastReceiver
@@ -18,7 +16,7 @@ import be.mygod.vpnhotspot.widget.SmartSnackbar
import timber.log.Timber import timber.log.Timber
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
class BluetoothTethering(context: Context, val stateListener: () -> Unit) : class BluetoothTethering(context: Context, private val adapter: BluetoothAdapter, val stateListener: () -> Unit) :
BluetoothProfile.ServiceListener, AutoCloseable { BluetoothProfile.ServiceListener, AutoCloseable {
companion object : BroadcastReceiver() { companion object : BroadcastReceiver() {
/** /**
@@ -26,17 +24,9 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
*/ */
private const val PAN = 5 private const val PAN = 5
private val clazz by lazy { Class.forName("android.bluetooth.BluetoothPan") } private val clazz by lazy { Class.forName("android.bluetooth.BluetoothPan") }
private val constructor by lazy {
clazz.getDeclaredConstructor(Context::class.java, BluetoothProfile.ServiceListener::class.java).apply {
isAccessible = true
}
}
private val isTetheringOn by lazy { clazz.getDeclaredMethod("isTetheringOn") } private val isTetheringOn by lazy { clazz.getDeclaredMethod("isTetheringOn") }
fun pan(context: Context, serviceListener: BluetoothProfile.ServiceListener) = private val BluetoothProfile.isTetheringOn get() = isTetheringOn(this) as Boolean
constructor.newInstance(context, serviceListener) as BluetoothProfile
val BluetoothProfile.isTetheringOn get() = isTetheringOn(this) as Boolean
fun BluetoothProfile.closePan() = BluetoothAdapter.getDefaultAdapter()!!.closeProfileProxy(PAN, this)
private fun registerBluetoothStateListener(receiver: BroadcastReceiver) = private fun registerBluetoothStateListener(receiver: BroadcastReceiver) =
app.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)) app.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
@@ -46,7 +36,6 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
/** /**
* https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/TetherSettings.java#215 * https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/TetherSettings.java#215
*/ */
@TargetApi(24)
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) { when (intent?.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {
BluetoothAdapter.STATE_ON -> { BluetoothAdapter.STATE_ON -> {
@@ -58,28 +47,12 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
pendingCallback = null pendingCallback = null
app.unregisterReceiver(this) app.unregisterReceiver(this)
} }
/**
* https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/TetherSettings.java#384
*/
@RequiresApi(24)
fun start(callback: TetheringManager.StartTetheringCallback) {
if (pendingCallback != null) return
val adapter = BluetoothAdapter.getDefaultAdapter()
try {
if (adapter?.state == BluetoothAdapter.STATE_OFF) {
registerBluetoothStateListener(this)
pendingCallback = callback
adapter.enable()
} else TetheringManager.startTethering(TetheringManager.TETHERING_BLUETOOTH, true, callback)
} catch (e: SecurityException) {
SmartSnackbar.make(e.readableMessage).shortToast().show()
}
}
} }
private var proxyCreated = false
private var connected = false private var connected = false
private var pan: BluetoothProfile? = null private var pan: BluetoothProfile? = null
private var stoppedByUser = false
var activeFailureCause: Throwable? = null var activeFailureCause: Throwable? = null
/** /**
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/78d5efd/src/com/android/settings/TetherSettings.java * Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/78d5efd/src/com/android/settings/TetherSettings.java
@@ -88,7 +61,7 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
val pan = pan ?: return null val pan = pan ?: return null
if (!connected) return null if (!connected) return null
activeFailureCause = null activeFailureCause = null
return BluetoothAdapter.getDefaultAdapter()?.state == BluetoothAdapter.STATE_ON && try { val on = adapter.state == BluetoothAdapter.STATE_ON && try {
pan.isTetheringOn pan.isTetheringOn
} catch (e: InvocationTargetException) { } catch (e: InvocationTargetException) {
activeFailureCause = e.cause ?: e activeFailureCause = e.cause ?: e
@@ -96,16 +69,21 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
else Timber.w(e) else Timber.w(e)
return null return null
} }
return if (stoppedByUser) {
if (!on) stoppedByUser = false
false
} else on
} }
private val receiver = broadcastReceiver { _, _ -> stateListener() } private val receiver = broadcastReceiver { _, _ -> stateListener() }
fun ensureInit(context: Context) { fun ensureInit(context: Context) {
if (pan == null && BluetoothAdapter.getDefaultAdapter() != null) try { activeFailureCause = null
pan = pan(context, this) if (!proxyCreated) try {
} catch (e: InvocationTargetException) { check(adapter.getProfileProxy(context, this, PAN))
if (e.cause is SecurityException && BuildCompat.isAtLeastS()) Timber.d(e.readableMessage) proxyCreated = true
else Timber.w(e) } catch (e: SecurityException) {
if (Build.VERSION.SDK_INT >= 31) Timber.d(e.readableMessage) else Timber.w(e)
activeFailureCause = e activeFailureCause = e
} }
} }
@@ -116,13 +94,38 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
override fun onServiceDisconnected(profile: Int) { override fun onServiceDisconnected(profile: Int) {
connected = false connected = false
stoppedByUser = false
} }
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) { override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
pan = proxy
connected = true connected = true
stateListener() stateListener()
} }
/**
* https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/TetherSettings.java#384
*/
@SuppressLint("MissingPermission")
fun start(callback: TetheringManager.StartTetheringCallback, context: Context) {
if (pendingCallback == null) try {
if (adapter.state == BluetoothAdapter.STATE_OFF) {
registerBluetoothStateListener(BluetoothTethering)
pendingCallback = callback
@Suppress("DEPRECATION")
if (!adapter.enable()) context.startActivity(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))
} else TetheringManager.startTethering(TetheringManager.TETHERING_BLUETOOTH, true, callback)
} catch (e: SecurityException) {
SmartSnackbar.make(e.readableMessage).shortToast().show()
pendingCallback = null
}
}
fun stop(callback: (Exception) -> Unit) {
TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH, callback)
stoppedByUser = true
}
override fun close() { override fun close() {
app.unregisterReceiver(receiver) app.unregisterReceiver(receiver)
pan?.closePan() adapter.closeProfileProxy(PAN, pan)
} }
} }

View File

@@ -2,7 +2,6 @@ package be.mygod.vpnhotspot.manage
import android.content.Intent import android.content.Intent
import android.view.View import android.view.View
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.TetheringService import be.mygod.vpnhotspot.TetheringService
@@ -25,7 +24,7 @@ class InterfaceManager(private val parent: TetheringFragment, val iface: String)
val data = binding.data as Data val data = binding.data as Data
if (data.active) context.startService(Intent(context, TetheringService::class.java) if (data.active) context.startService(Intent(context, TetheringService::class.java)
.putExtra(TetheringService.EXTRA_REMOVE_INTERFACE, iface)) .putExtra(TetheringService.EXTRA_REMOVE_INTERFACE, iface))
else ContextCompat.startForegroundService(context, Intent(context, TetheringService::class.java) else context.startForegroundService(Intent(context, TetheringService::class.java)
.putExtra(TetheringService.EXTRA_ADD_INTERFACES, arrayOf(iface))) .putExtra(TetheringService.EXTRA_ADD_INTERFACES, arrayOf(iface)))
} }
} }

View File

@@ -1,14 +1,12 @@
package be.mygod.vpnhotspot.manage package be.mygod.vpnhotspot.manage
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.net.IpNeighbour import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
import be.mygod.vpnhotspot.util.KillableTileService import be.mygod.vpnhotspot.util.KillableTileService
import java.net.Inet4Address import java.net.Inet4Address
@RequiresApi(24)
abstract class IpNeighbourMonitoringTileService : KillableTileService(), IpNeighbourMonitor.Callback { abstract class IpNeighbourMonitoringTileService : KillableTileService(), IpNeighbourMonitor.Callback {
private var neighbours: Collection<IpNeighbour> = emptyList() private var neighbours: Collection<IpNeighbour> = emptyList()
abstract fun updateTile() abstract fun updateTile()

View File

@@ -1,15 +1,12 @@
package be.mygod.vpnhotspot.manage package be.mygod.vpnhotspot.manage
import android.Manifest import android.Manifest
import android.content.* import android.content.ComponentName
import android.location.LocationManager import android.content.Context
import android.content.ServiceConnection
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.provider.Settings
import android.view.View import android.view.View
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.LocalOnlyHotspotService import be.mygod.vpnhotspot.LocalOnlyHotspotService
@@ -17,15 +14,15 @@ import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
import be.mygod.vpnhotspot.util.ServiceForegroundConnector import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import be.mygod.vpnhotspot.util.formatAddresses import be.mygod.vpnhotspot.util.formatAddresses
import be.mygod.vpnhotspot.widget.SmartSnackbar
import java.net.NetworkInterface import java.net.NetworkInterface
@RequiresApi(26)
class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager(), ServiceConnection { class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager(), ServiceConnection {
companion object { companion object {
val permission = if (Build.VERSION.SDK_INT >= 29) { val permission = when {
Manifest.permission.ACCESS_FINE_LOCATION Build.VERSION.SDK_INT >= 33 -> Manifest.permission.NEARBY_WIFI_DEVICES
} else Manifest.permission.ACCESS_COARSE_LOCATION Build.VERSION.SDK_INT >= 29 -> Manifest.permission.ACCESS_FINE_LOCATION
else -> Manifest.permission.ACCESS_COARSE_LOCATION
}
} }
class ViewHolder(val binding: ListitemInterfaceBinding) : RecyclerView.ViewHolder(binding.root), class ViewHolder(val binding: ListitemInterfaceBinding) : RecyclerView.ViewHolder(binding.root),
@@ -57,23 +54,7 @@ class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager()
ServiceForegroundConnector(parent, this, LocalOnlyHotspotService::class) ServiceForegroundConnector(parent, this, LocalOnlyHotspotService::class)
} }
/** fun start(context: Context) = app.startServiceWithLocation<LocalOnlyHotspotService>(context)
* LOH also requires location to be turned on. Source:
* https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/53e0284/service/java/com/android/server/wifi/WifiServiceImpl.java#1204
* https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/53e0284/service/java/com/android/server/wifi/WifiSettingsStore.java#228
*/
fun start(context: Context) {
if (if (Build.VERSION.SDK_INT < 28) @Suppress("DEPRECATION") {
Settings.Secure.getInt(context.contentResolver, Settings.Secure.LOCATION_MODE,
Settings.Secure.LOCATION_MODE_OFF) == Settings.Secure.LOCATION_MODE_OFF
} else context.getSystemService<LocationManager>()?.isLocationEnabled != true) try {
context.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
Toast.makeText(context, R.string.tethering_temp_hotspot_location, Toast.LENGTH_LONG).show()
} catch (e: ActivityNotFoundException) {
app.logEvent("location_settings") { param("message", e.toString()) }
SmartSnackbar.make(R.string.tethering_temp_hotspot_location).show()
} else context.startForegroundService(Intent(context, LocalOnlyHotspotService::class.java))
}
override val type get() = VIEW_TYPE_LOCAL_ONLY_HOTSPOT override val type get() = VIEW_TYPE_LOCAL_ONLY_HOTSPOT
private val data = Data() private val data = Data()

View File

@@ -6,12 +6,10 @@ import android.content.Intent
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
import android.os.IBinder import android.os.IBinder
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.LocalOnlyHotspotService import be.mygod.vpnhotspot.LocalOnlyHotspotService
import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.util.stopAndUnbind import be.mygod.vpnhotspot.util.stopAndUnbind
@RequiresApi(26)
class LocalOnlyHotspotTileService : IpNeighbourMonitoringTileService() { class LocalOnlyHotspotTileService : IpNeighbourMonitoringTileService() {
private val tile by lazy { Icon.createWithResource(application, R.drawable.ic_action_perm_scan_wifi) } private val tile by lazy { Icon.createWithResource(application, R.drawable.ic_action_perm_scan_wifi) }
@@ -38,7 +36,7 @@ class LocalOnlyHotspotTileService : IpNeighbourMonitoringTileService() {
label = getText(R.string.tethering_temp_hotspot) label = getText(R.string.tethering_temp_hotspot)
} else { } else {
state = Tile.STATE_ACTIVE state = Tile.STATE_ACTIVE
label = binder.configuration?.ssid ?: getText(R.string.tethering_temp_hotspot) label = binder.configuration?.ssid?.toString() ?: getText(R.string.tethering_temp_hotspot)
subtitleDevices { it == iface } subtitleDevices { it == iface }
} }
updateTile() updateTile()

View File

@@ -16,7 +16,7 @@ object ManageBar : Manager() {
private const val SETTINGS_2 = "com.android.settings.TetherSettings" private const val SETTINGS_2 = "com.android.settings.TetherSettings"
object Data : BaseObservable() { object Data : BaseObservable() {
val offloadEnabled get() = TetherOffloadManager.supported && TetherOffloadManager.enabled val offloadEnabled get() = TetherOffloadManager.enabled
} }
class ViewHolder(binding: ListitemManageBinding) : RecyclerView.ViewHolder(binding.root), View.OnClickListener { class ViewHolder(binding: ListitemManageBinding) : RecyclerView.ViewHolder(binding.root), View.OnClickListener {
init { init {

View File

@@ -1,7 +1,6 @@
package be.mygod.vpnhotspot.manage package be.mygod.vpnhotspot.manage
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
@@ -18,9 +17,6 @@ abstract class Manager {
const val VIEW_TYPE_USB = 3 const val VIEW_TYPE_USB = 3
const val VIEW_TYPE_BLUETOOTH = 4 const val VIEW_TYPE_BLUETOOTH = 4
const val VIEW_TYPE_ETHERNET = 8 const val VIEW_TYPE_ETHERNET = 8
const val VIEW_TYPE_NCM = 9
const val VIEW_TYPE_WIGIG = 10
const val VIEW_TYPE_WIFI_LEGACY = 5
const val VIEW_TYPE_LOCAL_ONLY_HOTSPOT = 6 const val VIEW_TYPE_LOCAL_ONLY_HOTSPOT = 6
const val VIEW_TYPE_REPEATER = 7 const val VIEW_TYPE_REPEATER = 7
@@ -35,13 +31,10 @@ abstract class Manager {
VIEW_TYPE_WIFI, VIEW_TYPE_WIFI,
VIEW_TYPE_USB, VIEW_TYPE_USB,
VIEW_TYPE_BLUETOOTH, VIEW_TYPE_BLUETOOTH,
VIEW_TYPE_ETHERNET, VIEW_TYPE_ETHERNET -> {
VIEW_TYPE_NCM,
VIEW_TYPE_WIGIG,
VIEW_TYPE_WIFI_LEGACY -> {
TetherManager.ViewHolder(ListitemInterfaceBinding.inflate(inflater, parent, false)) TetherManager.ViewHolder(ListitemInterfaceBinding.inflate(inflater, parent, false))
} }
VIEW_TYPE_LOCAL_ONLY_HOTSPOT -> @TargetApi(26) { VIEW_TYPE_LOCAL_ONLY_HOTSPOT -> {
LocalOnlyHotspotManager.ViewHolder(ListitemInterfaceBinding.inflate(inflater, parent, false)) LocalOnlyHotspotManager.ViewHolder(ListitemInterfaceBinding.inflate(inflater, parent, false))
} }
VIEW_TYPE_REPEATER -> RepeaterManager.ViewHolder(ListitemRepeaterBinding.inflate(inflater, parent, false)) VIEW_TYPE_REPEATER -> RepeaterManager.ViewHolder(ListitemRepeaterBinding.inflate(inflater, parent, false))

View File

@@ -5,7 +5,7 @@ import android.content.ComponentName
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.content.pm.PackageManager import android.net.MacAddress
import android.net.wifi.SoftApConfiguration import android.net.wifi.SoftApConfiguration
import android.net.wifi.p2p.WifiP2pGroup import android.net.wifi.p2p.WifiP2pGroup
import android.os.Build import android.os.Build
@@ -17,25 +17,33 @@ import android.view.WindowManager
import android.widget.EditText import android.widget.EditText
import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.databinding.BaseObservable import androidx.databinding.BaseObservable
import androidx.databinding.Bindable import androidx.databinding.Bindable
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.withStarted
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import be.mygod.vpnhotspot.* import be.mygod.vpnhotspot.AlertDialogFragment
import be.mygod.vpnhotspot.BR
import be.mygod.vpnhotspot.Empty
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.RepeaterService
import be.mygod.vpnhotspot.databinding.ListitemRepeaterBinding import be.mygod.vpnhotspot.databinding.ListitemRepeaterBinding
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.wifi.P2pSupplicantConfiguration import be.mygod.vpnhotspot.net.wifi.P2pSupplicantConfiguration
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
import be.mygod.vpnhotspot.net.wifi.WifiApDialogFragment import be.mygod.vpnhotspot.net.wifi.WifiApDialogFragment
import be.mygod.vpnhotspot.net.wifi.WifiApManager import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.net.wifi.WifiSsidCompat
import be.mygod.vpnhotspot.util.ServiceForegroundConnector import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import be.mygod.vpnhotspot.util.formatAddresses import be.mygod.vpnhotspot.util.formatAddresses
import be.mygod.vpnhotspot.util.showAllowingStateLoss import be.mygod.vpnhotspot.util.showAllowingStateLoss
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.* import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
import java.net.NetworkInterface import java.net.NetworkInterface
@@ -71,6 +79,9 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
NetworkInterface.getByName(p2pInterface ?: return "")?.formatAddresses() ?: "" NetworkInterface.getByName(p2pInterface ?: return "")?.formatAddresses() ?: ""
} catch (_: SocketException) { } catch (_: SocketException) {
"" ""
} catch (e: Exception) {
Timber.w(e)
""
} }
} }
@@ -89,12 +100,10 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
val binder = binder val binder = binder
when (binder?.service?.status) { when (binder?.service?.status) {
RepeaterService.Status.IDLE -> if (Build.VERSION.SDK_INT < 29) parent.requireContext().let { context -> RepeaterService.Status.IDLE -> if (Build.VERSION.SDK_INT < 29) parent.requireContext().let { context ->
ContextCompat.startForegroundService(context, Intent(context, RepeaterService::class.java)) context.startForegroundService(Intent(context, RepeaterService::class.java))
} else if (parent.requireContext().checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == } else parent.startRepeater.launch(if (Build.VERSION.SDK_INT >= 33) {
PackageManager.PERMISSION_GRANTED || Manifest.permission.NEARBY_WIFI_DEVICES
parent.shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) { } else Manifest.permission.ACCESS_FINE_LOCATION)
parent.startRepeater.launch(Manifest.permission.ACCESS_FINE_LOCATION)
} else SmartSnackbar.make(R.string.repeater_missing_location_permissions).shortToast().show()
RepeaterService.Status.ACTIVE -> binder.shutdown() RepeaterService.Status.ACTIVE -> binder.shutdown()
else -> { } else -> { }
} }
@@ -148,8 +157,10 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
fun configure() { fun configure() {
if (configuring) return if (configuring) return
configuring = true configuring = true
parent.viewLifecycleOwner.lifecycleScope.launchWhenCreated { val owner = parent.viewLifecycleOwner
getConfiguration()?.let { (config, readOnly) -> owner.lifecycleScope.launch {
val (config, readOnly) = getConfiguration() ?: return@launch
owner.withStarted {
WifiApDialogFragment().apply { WifiApDialogFragment().apply {
arg(WifiApDialogFragment.Arg(config, readOnly, true)) arg(WifiApDialogFragment.Arg(config, readOnly, true))
key(this@RepeaterManager.javaClass.name) key(this@RepeaterManager.javaClass.name)
@@ -195,21 +206,29 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
passphrase = passphrase, passphrase = passphrase,
securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, // is not actually used securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, // is not actually used
isAutoShutdownEnabled = RepeaterService.isAutoShutdownEnabled, isAutoShutdownEnabled = RepeaterService.isAutoShutdownEnabled,
shutdownTimeoutMillis = RepeaterService.shutdownTimeoutMillis).apply { shutdownTimeoutMillis = RepeaterService.shutdownTimeoutMillis,
macRandomizationSetting = if (WifiApManager.p2pMacRandomizationSupported) {
SoftApConfigurationCompat.RANDOMIZATION_NON_PERSISTENT
} else SoftApConfigurationCompat.RANDOMIZATION_NONE,
vendorElements = RepeaterService.vendorElements,
).apply {
bssid = RepeaterService.deviceAddress bssid = RepeaterService.deviceAddress
setChannel(RepeaterService.operatingChannel, RepeaterService.operatingBand) setChannel(RepeaterService.operatingChannel, RepeaterService.operatingBand)
setMacRandomizationEnabled(WifiApManager.p2pMacRandomizationSupported)
} to false } to false
} }
} else binder?.let { binder -> } else binder?.let { binder ->
val group = binder.group ?: binder.fetchPersistentGroup().let { binder.group } val group = binder.group ?: binder.fetchPersistentGroup().let { binder.group }
if (group != null) return SoftApConfigurationCompat( if (group != null) return SoftApConfigurationCompat(
ssid = group.networkName, ssid = WifiSsidCompat.fromUtf8Text(group.networkName),
securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, // is not actually used securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, // is not actually used
isAutoShutdownEnabled = RepeaterService.isAutoShutdownEnabled, isAutoShutdownEnabled = RepeaterService.isAutoShutdownEnabled,
shutdownTimeoutMillis = RepeaterService.shutdownTimeoutMillis).run { shutdownTimeoutMillis = RepeaterService.shutdownTimeoutMillis,
macRandomizationSetting = if (WifiApManager.p2pMacRandomizationSupported) {
SoftApConfigurationCompat.RANDOMIZATION_NON_PERSISTENT
} else SoftApConfigurationCompat.RANDOMIZATION_NONE,
vendorElements = RepeaterService.vendorElements,
).run {
setChannel(RepeaterService.operatingChannel) setChannel(RepeaterService.operatingChannel)
setMacRandomizationEnabled(WifiApManager.p2pMacRandomizationSupported)
try { try {
val config = P2pSupplicantConfiguration(group) val config = P2pSupplicantConfiguration(group)
config.init(binder.obtainDeviceAddress()?.toString()) config.init(binder.obtainDeviceAddress()?.toString())
@@ -221,7 +240,7 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
if (e !is CancellationException) Timber.w(e) if (e !is CancellationException) Timber.w(e)
passphrase = group.passphrase passphrase = group.passphrase
try { try {
bssid = group.owner?.deviceAddress?.let(MacAddressCompat.Companion::fromString) bssid = group.owner?.deviceAddress?.let(MacAddress::fromString)
} catch (_: IllegalArgumentException) { } } catch (_: IllegalArgumentException) { }
this to true this to true
} }
@@ -231,15 +250,19 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
return null return null
} }
private suspend fun updateConfiguration(config: SoftApConfigurationCompat) { private suspend fun updateConfiguration(config: SoftApConfigurationCompat) {
val (band, channel) = config.requireSingleBand() val (band, channel) = SoftApConfigurationCompat.requireSingleBand(config.channels)
if (RepeaterService.safeMode) { if (RepeaterService.safeMode) {
RepeaterService.networkName = config.ssid RepeaterService.networkName = config.ssid
RepeaterService.deviceAddress = config.bssid RepeaterService.deviceAddress = config.bssid
RepeaterService.passphrase = config.passphrase RepeaterService.passphrase = config.passphrase
} else holder.config?.let { master -> } else holder.config?.let { master ->
val binder = binder val binder = binder
if (binder?.group?.networkName != config.ssid || master.psk != config.passphrase || val mayBeModified = master.psk != config.passphrase || master.bssid != config.bssid || config.ssid.run {
master.bssid != config.bssid) try { if (this != null) decode().let {
it == null || binder?.group?.networkName != it
} else binder?.group?.networkName != null
}
if (mayBeModified) try {
withContext(Dispatchers.Default) { master.update(config.ssid!!, config.passphrase!!, config.bssid) } withContext(Dispatchers.Default) { master.update(config.ssid!!, config.passphrase!!, config.bssid) }
(this.binder ?: binder)?.group = null (this.binder ?: binder)?.group = null
} catch (e: Exception) { } catch (e: Exception) {
@@ -252,5 +275,6 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
RepeaterService.operatingChannel = channel RepeaterService.operatingChannel = channel
RepeaterService.isAutoShutdownEnabled = config.isAutoShutdownEnabled RepeaterService.isAutoShutdownEnabled = config.isAutoShutdownEnabled
RepeaterService.shutdownTimeoutMillis = config.shutdownTimeoutMillis RepeaterService.shutdownTimeoutMillis = config.shutdownTimeoutMillis
RepeaterService.vendorElements = config.vendorElements
} }
} }

View File

@@ -7,15 +7,12 @@ import android.graphics.drawable.Icon
import android.net.wifi.p2p.WifiP2pGroup import android.net.wifi.p2p.WifiP2pGroup
import android.os.IBinder import android.os.IBinder
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.RepeaterService import be.mygod.vpnhotspot.RepeaterService
import be.mygod.vpnhotspot.util.KillableTileService import be.mygod.vpnhotspot.util.KillableTileService
import be.mygod.vpnhotspot.util.Services import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.stopAndUnbind import be.mygod.vpnhotspot.util.stopAndUnbind
@RequiresApi(24)
class RepeaterTileService : KillableTileService() { class RepeaterTileService : KillableTileService() {
private val tile by lazy { Icon.createWithResource(application, R.drawable.ic_action_settings_input_antenna) } private val tile by lazy { Icon.createWithResource(application, R.drawable.ic_action_settings_input_antenna) }
@@ -37,8 +34,7 @@ class RepeaterTileService : KillableTileService() {
val binder = binder val binder = binder
if (binder == null) tapPending = true else when (binder.service.status) { if (binder == null) tapPending = true else when (binder.service.status) {
RepeaterService.Status.ACTIVE -> binder.shutdown() RepeaterService.Status.ACTIVE -> binder.shutdown()
RepeaterService.Status.IDLE -> ContextCompat.startForegroundService(this, RepeaterService.Status.IDLE -> startForegroundService(Intent(this, RepeaterService::class.java))
Intent(this, RepeaterService::class.java))
else -> { } else -> { }
} }
} }

View File

@@ -2,7 +2,7 @@ package be.mygod.vpnhotspot.manage
import android.Manifest import android.Manifest
import android.annotation.TargetApi import android.annotation.TargetApi
import android.content.ClipData import android.bluetooth.BluetoothAdapter
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
@@ -15,7 +15,6 @@ import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.os.BuildCompat
import androidx.core.view.updatePaddingRelative import androidx.core.view.updatePaddingRelative
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
@@ -56,19 +55,23 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
override fun onClick(v: View?) { override fun onClick(v: View?) {
val manager = manager!! val manager = manager!!
val mainActivity = manager.parent.activity as MainActivity val mainActivity = manager.parent.activity as MainActivity
if (Build.VERSION.SDK_INT >= 23 && !Settings.System.canWrite(mainActivity)) try { if (!Settings.System.canWrite(mainActivity)) try {
manager.parent.startActivity(Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS, manager.parent.startActivity(Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS,
"package:${mainActivity.packageName}".toUri())) "package:${mainActivity.packageName}".toUri()))
return return
} catch (e: RuntimeException) { } catch (e: RuntimeException) {
app.logEvent("manage_write_settings") { param("message", e.toString()) } app.logEvent("manage_write_settings") { param("message", e.toString()) }
} }
if (manager.isStarted) try { when (manager.isStarted) {
true -> try {
manager.stop() manager.stop()
} catch (e: InvocationTargetException) { } catch (e: InvocationTargetException) {
if (e.targetException !is SecurityException) Timber.w(e) if (e.targetException !is SecurityException) Timber.w(e)
manager.onException(e) manager.onException(e)
} else manager.start() }
false -> manager.start()
null -> manager.onClickNull()
}
} }
} }
@@ -79,13 +82,14 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
override val icon get() = tetherType.icon override val icon get() = tetherType.icon
override val title get() = this@TetherManager.title override val title get() = this@TetherManager.title
override val text get() = this@TetherManager.text override val text get() = this@TetherManager.text
override val active get() = isStarted override val active get() = isStarted == true
} }
val data = Data() val data = Data()
abstract val title: CharSequence abstract val title: CharSequence
abstract val tetherType: TetherType abstract val tetherType: TetherType
open val isStarted get() = parent.enabledTypes.contains(tetherType) open val isStarted: Boolean? get() = parent.enabledTypes.contains(tetherType) ||
tetherType == TetherType.USB && parent.enabledTypes.contains(TetherType.NCM)
protected open val text: CharSequence get() = baseError ?: "" protected open val text: CharSequence get() = baseError ?: ""
protected var baseError: String? = null protected var baseError: String? = null
@@ -93,6 +97,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
protected abstract fun start() protected abstract fun start()
protected abstract fun stop() protected abstract fun stop()
protected open fun onClickNull(): Unit = throw UnsupportedOperationException()
override fun onTetheringStarted() = data.notifyChange() override fun onTetheringStarted() = data.notifyChange()
override fun onTetheringFailed(error: Int?) { override fun onTetheringFailed(error: Int?) {
@@ -120,21 +125,20 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
} }
fun updateErrorMessage(errored: List<String>, lastErrors: Map<String, Int>) { fun updateErrorMessage(errored: List<String>, lastErrors: Map<String, Int>) {
val interested = errored.filter { TetherType.ofInterface(it) == tetherType } val interested = errored.filter { TetherType.ofInterface(it).isA(tetherType) }
baseError = if (interested.isEmpty()) null else interested.joinToString("\n") { iface -> baseError = if (interested.isEmpty()) null else interested.joinToString("\n") { iface ->
"$iface: " + try { "$iface: " + try {
TetheringManager.tetherErrorLookup(if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") { TetheringManager.tetherErrorLookup(if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
TetheringManager.getLastTetherError(iface) TetheringManager.getLastTetherError(iface)
} else lastErrors[iface] ?: 0) } else lastErrors[iface] ?: 0)
} catch (e: InvocationTargetException) { } catch (e: InvocationTargetException) {
if (Build.VERSION.SDK_INT !in 24..25 || e.cause !is SecurityException) Timber.w(e) else Timber.d(e) if (e.cause !is SecurityException) Timber.w(e) else Timber.d(e)
e.readableMessage e.readableMessage
} }
} }
data.notifyChange() data.notifyChange()
} }
@RequiresApi(24)
class Wifi(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver, class Wifi(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver,
WifiApManager.SoftApCallbackCompat { WifiApManager.SoftApCallbackCompat {
private var failureReason: Int? = null private var failureReason: Int? = null
@@ -143,24 +147,19 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
private var capability: Parcelable? = null private var capability: Parcelable? = null
init { init {
if (Build.VERSION.SDK_INT >= 28) parent.viewLifecycleOwner.lifecycle.addObserver(this) parent.viewLifecycleOwner.lifecycle.addObserver(this)
} }
@TargetApi(28)
override fun onStart(owner: LifecycleOwner) { override fun onStart(owner: LifecycleOwner) {
WifiApCommands.registerSoftApCallback(this) WifiApCommands.registerSoftApCallback(this)
} }
@TargetApi(28)
override fun onStop(owner: LifecycleOwner) { override fun onStop(owner: LifecycleOwner) {
WifiApCommands.unregisterSoftApCallback(this) WifiApCommands.unregisterSoftApCallback(this)
} }
override fun onStateChanged(state: Int, failureReason: Int) { override fun onStateChanged(state: Int, failureReason: Int) {
if (state < 10 || state > 14) { if (!WifiApManager.checkWifiApState(state)) return
Timber.w(Exception("Unknown state $state, $failureReason")) this.failureReason = if (state == WifiApManager.WIFI_AP_STATE_FAILED) failureReason else null
return
}
this.failureReason = if (state == 14) failureReason else null // WIFI_AP_STATE_FAILED
data.notifyChange() data.notifyChange()
} }
override fun onNumClientsChanged(numClients: Int) { override fun onNumClientsChanged(numClients: Int) {
@@ -175,21 +174,6 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
this.capability = capability this.capability = capability
data.notifyChange() data.notifyChange()
} }
@RequiresApi(30)
override fun onBlockedClientConnecting(client: Parcelable, blockedReason: Int) {
@Suppress("NAME_SHADOWING")
val client = WifiClient(client)
val macAddress = client.macAddress
var name = macAddress.toString()
if (BuildCompat.isAtLeastS()) client.apInstanceIdentifier?.let { name += "%$it" }
val reason = WifiApManager.clientBlockLookup(blockedReason, true)
Timber.i("$name blocked from connecting: $reason ($blockedReason)")
SmartSnackbar.make(parent.getString(R.string.tethering_manage_wifi_client_blocked, name, reason)).apply {
action(R.string.tethering_manage_wifi_copy_mac) {
app.clipboard.setPrimaryClip(ClipData.newPlainText(null, macAddress.toString()))
}
}.show()
}
override val title get() = parent.getString(R.string.tethering_manage_wifi) override val title get() = parent.getString(R.string.tethering_manage_wifi)
override val tetherType get() = TetherType.WIFI override val tetherType get() = TetherType.WIFI
@@ -201,7 +185,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
val numClients = numClients val numClients = numClients
val maxClients = capability.maxSupportedClients val maxClients = capability.maxSupportedClients
var features = capability.supportedFeatures var features = capability.supportedFeatures
if (BuildCompat.isAtLeastS()) for ((flag, band) in arrayOf( if (Build.VERSION.SDK_INT >= 31) for ((flag, band) in arrayOf(
SoftApCapability.SOFTAP_FEATURE_BAND_24G_SUPPORTED to SoftApConfigurationCompat.BAND_2GHZ, SoftApCapability.SOFTAP_FEATURE_BAND_24G_SUPPORTED to SoftApConfigurationCompat.BAND_2GHZ,
SoftApCapability.SOFTAP_FEATURE_BAND_5G_SUPPORTED to SoftApConfigurationCompat.BAND_5GHZ, SoftApCapability.SOFTAP_FEATURE_BAND_5G_SUPPORTED to SoftApConfigurationCompat.BAND_5GHZ,
SoftApCapability.SOFTAP_FEATURE_BAND_6G_SUPPORTED to SoftApConfigurationCompat.BAND_6GHZ, SoftApCapability.SOFTAP_FEATURE_BAND_6G_SUPPORTED to SoftApConfigurationCompat.BAND_6GHZ,
@@ -217,7 +201,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
R.string.tethering_manage_wifi_feature_ap_mac_randomization)) R.string.tethering_manage_wifi_feature_ap_mac_randomization))
if (Services.wifi.isStaApConcurrencySupported) yield(parent.getText( if (Services.wifi.isStaApConcurrencySupported) yield(parent.getText(
R.string.tethering_manage_wifi_feature_sta_ap_concurrency)) R.string.tethering_manage_wifi_feature_sta_ap_concurrency))
if (BuildCompat.isAtLeastS()) { if (Build.VERSION.SDK_INT >= 31) {
if (Services.wifi.isBridgedApConcurrencySupported) yield(parent.getText( if (Services.wifi.isBridgedApConcurrencySupported) yield(parent.getText(
R.string.tethering_manage_wifi_feature_bridged_ap_concurrency)) R.string.tethering_manage_wifi_feature_bridged_ap_concurrency))
if (Services.wifi.isStaBridgedApConcurrencySupported) yield(parent.getText( if (Services.wifi.isStaBridgedApConcurrencySupported) yield(parent.getText(
@@ -228,63 +212,46 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
yield(SoftApCapability.featureLookup(bit, true)) yield(SoftApCapability.featureLookup(bit, true))
features = features and bit.inv() features = features and bit.inv()
} }
}.joinToSpanned().let { }.joinToSpanned().ifEmpty { parent.getText(R.string.tethering_manage_wifi_no_features) })
if (it.isEmpty()) parent.getText(R.string.tethering_manage_wifi_no_features) else it if (Build.VERSION.SDK_INT >= 31) {
})
if (BuildCompat.isAtLeastS()) {
val list = SoftApConfigurationCompat.BAND_TYPES.map { band -> val list = SoftApConfigurationCompat.BAND_TYPES.map { band ->
val channels = capability.getSupportedChannelList(band) val channels = capability.getSupportedChannelList(band)
if (channels.isNotEmpty()) StringBuilder().apply { if (channels.isNotEmpty()) {
append(SoftApConfigurationCompat.bandLookup(band, true)) "${SoftApConfigurationCompat.bandLookup(band, true)} (${RangeInput.toString(channels)})"
append(" (")
channels.sort()
var pending: Int? = null
var last = channels[0]
append(last)
for (channel in channels.asSequence().drop(1)) {
if (channel == last + 1) pending = channel else {
pending?.let {
append('-')
append(it)
pending = null
}
append(',')
append(channel)
}
last = channel
}
pending?.let {
append('-')
append(it)
}
append(')')
} else null } else null
}.filterNotNull() }.filterNotNull()
if (list.isNotEmpty()) result.append(parent.getText(R.string.tethering_manage_wifi_supported_channels) if (list.isNotEmpty()) result.append(parent.getText(R.string.tethering_manage_wifi_supported_channels)
.format(locale, list.joinToString("; "))) .format(locale, list.joinToString("; ")))
capability.countryCode?.let {
result.append(parent.getText(R.string.tethering_manage_wifi_country_code).format(locale, it))
}
} }
result result
} ?: numClients?.let { numClients -> } ?: numClients?.let { numClients ->
app.resources.getQuantityText(R.plurals.tethering_manage_wifi_clients, numClients).format(locale, app.resources.getQuantityText(R.plurals.tethering_manage_wifi_clients, numClients).format(locale,
numClients) numClients)
} }
override val text get() = parent.resources.configuration.locale.let { locale -> override val text get() = parent.resources.configuration.locales[0].let { locale ->
listOfNotNull(failureReason?.let { WifiApManager.failureReasonLookup(it) }, baseError, info.run { listOfNotNull(failureReason?.let { WifiApManager.failureReasonLookup(it) }, baseError, info.run {
if (isEmpty()) null else joinToSpanned("\n") @TargetApi(30) { parcel -> if (isEmpty()) null else joinToSpanned("\n") @TargetApi(30) { parcel ->
val info = SoftApInfo(parcel) val info = SoftApInfo(parcel)
val frequency = info.frequency val frequency = info.frequency
val channel = SoftApConfigurationCompat.frequencyToChannel(frequency) val channel = SoftApConfigurationCompat.frequencyToChannel(frequency)
val bandwidth = SoftApInfo.channelWidthLookup(info.bandwidth, true) val bandwidth = SoftApInfo.channelWidthLookup(info.bandwidth, true)
if (BuildCompat.isAtLeastS()) { if (Build.VERSION.SDK_INT >= 31) {
var bssid = makeMacSpan(info.bssid.toString()) val bssid = info.bssid.let { if (it == null) null else makeMacSpan(it.toString()) }
info.apInstanceIdentifier?.let { // take the fast route if possible val bssidAp = info.apInstanceIdentifier?.let {
bssid = if (bssid is String) "$bssid%$it" else SpannableStringBuilder(bssid).append("%$it") when (bssid) {
null -> it
is String -> "$bssid%$it" // take the fast route if possible
else -> SpannableStringBuilder(bssid).append("%$it")
} }
} ?: bssid ?: "?"
val timeout = info.autoShutdownTimeoutMillis val timeout = info.autoShutdownTimeoutMillis
parent.getText(if (timeout == 0L) { parent.getText(if (timeout == 0L) {
R.string.tethering_manage_wifi_info_timeout_disabled R.string.tethering_manage_wifi_info_timeout_disabled
} else R.string.tethering_manage_wifi_info_timeout_enabled).format(locale, } else R.string.tethering_manage_wifi_info_timeout_enabled).format(locale,
frequency, channel, bandwidth, bssid, info.wifiStandard, frequency, channel, bandwidth, bssidAp, info.wifiStandard,
// http://unicode.org/cldr/trac/ticket/3407 // http://unicode.org/cldr/trac/ticket/3407
DateUtils.formatElapsedTime(timeout / 1000)) DateUtils.formatElapsedTime(timeout / 1000))
} else parent.getText(R.string.tethering_manage_wifi_info).format(locale, } else parent.getText(R.string.tethering_manage_wifi_info).format(locale,
@@ -296,7 +263,6 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIFI, true, this) override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIFI, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI, this::onException) override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI, this::onException)
} }
@RequiresApi(24)
class Usb(parent: TetheringFragment) : TetherManager(parent) { class Usb(parent: TetheringFragment) : TetherManager(parent) {
override val title get() = parent.getString(R.string.tethering_manage_usb) override val title get() = parent.getString(R.string.tethering_manage_usb)
override val tetherType get() = TetherType.USB override val tetherType get() = TetherType.USB
@@ -305,39 +271,41 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_USB, true, this) override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_USB, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_USB, this::onException) override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_USB, this::onException)
} }
@RequiresApi(24) class Bluetooth(parent: TetheringFragment, adapter: BluetoothAdapter) :
class Bluetooth(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver { TetherManager(parent), DefaultLifecycleObserver {
private val tethering = BluetoothTethering(parent.requireContext()) { data.notifyChange() } private val tethering = BluetoothTethering(parent.requireContext(), adapter) { data.notifyChange() }
init { init {
parent.viewLifecycleOwner.lifecycle.addObserver(this) parent.viewLifecycleOwner.lifecycle.addObserver(this)
} }
fun ensureInit(context: Context) = tethering.ensureInit(context) fun ensureInit(context: Context) {
override fun onResume(owner: LifecycleOwner) { tethering.ensureInit(context)
if (!BuildCompat.isAtLeastS() || parent.requireContext().checkSelfPermission( onTetheringStarted() // force flush
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)
} }
override fun onResume(owner: LifecycleOwner) {
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 fun onDestroy(owner: LifecycleOwner) = tethering.close()
override val title get() = parent.getString(R.string.tethering_manage_bluetooth) override val title get() = parent.getString(R.string.tethering_manage_bluetooth)
override val tetherType get() = TetherType.BLUETOOTH override val tetherType get() = TetherType.BLUETOOTH
override val type get() = VIEW_TYPE_BLUETOOTH override val type get() = VIEW_TYPE_BLUETOOTH
override val isStarted get() = tethering.active == true override val isStarted get() = tethering.active
override val text get() = listOfNotNull( override val text get() = listOfNotNull(
if (tethering.active == null) tethering.activeFailureCause?.readableMessage else null, if (tethering.active == null) tethering.activeFailureCause?.readableMessage else null,
baseError).joinToString("\n") baseError).joinToString("\n")
override fun start() = BluetoothTethering.start(this) override fun start() = tethering.start(this, parent.requireContext())
override fun stop() { override fun stop() {
TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH, this::onException) tethering.stop(this::onException)
Thread.sleep(1) // give others a room to breathe
onTetheringStarted() // force flush state onTetheringStarted() // force flush state
} }
override fun onClickNull() = ManageBar.start(parent.requireContext())
} }
@RequiresApi(30) @RequiresApi(30)
class Ethernet(parent: TetheringFragment) : TetherManager(parent) { class Ethernet(parent: TetheringFragment) : TetherManager(parent) {
@@ -348,41 +316,4 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_ETHERNET, true, this) override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_ETHERNET, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_ETHERNET, this::onException) override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_ETHERNET, this::onException)
} }
@RequiresApi(30)
class Ncm(parent: TetheringFragment) : TetherManager(parent) {
override val title get() = parent.getString(R.string.tethering_manage_ncm)
override val tetherType get() = TetherType.NCM
override val type get() = VIEW_TYPE_NCM
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_NCM, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_NCM, this::onException)
}
@RequiresApi(30)
class WiGig(parent: TetheringFragment) : TetherManager(parent) {
override val title get() = parent.getString(R.string.tethering_manage_wigig)
override val tetherType get() = TetherType.WIGIG
override val type get() = VIEW_TYPE_WIGIG
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIGIG, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIGIG, this::onException)
}
@Suppress("DEPRECATION")
@Deprecated("Not usable since API 26, malfunctioning on API 25")
class WifiLegacy(parent: TetheringFragment) : TetherManager(parent) {
override val title get() = parent.getString(R.string.tethering_manage_wifi_legacy)
override val tetherType get() = TetherType.WIFI
override val type get() = VIEW_TYPE_WIFI_LEGACY
override fun start() = try {
WifiApManager.start()
} catch (e: Exception) {
onException(e)
}
override fun stop() = try {
WifiApManager.stop()
} catch (e: Exception) {
onException(e)
}
}
} }

View File

@@ -1,8 +1,7 @@
@file:Suppress("DEPRECATION")
package be.mygod.vpnhotspot.manage package be.mygod.vpnhotspot.manage
import android.annotation.TargetApi import android.annotation.TargetApi
import android.bluetooth.BluetoothManager
import android.content.* import android.content.*
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@@ -14,28 +13,35 @@ import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat import androidx.core.content.getSystemService
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.withStarted
import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import be.mygod.vpnhotspot.* import be.mygod.vpnhotspot.*
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.databinding.FragmentTetheringBinding import be.mygod.vpnhotspot.databinding.FragmentTetheringBinding
import be.mygod.vpnhotspot.net.TetherType import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
import be.mygod.vpnhotspot.net.wifi.WifiApDialogFragment import be.mygod.vpnhotspot.net.wifi.WifiApDialogFragment
import be.mygod.vpnhotspot.net.wifi.WifiApManager import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.root.RootManager import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.root.WifiApCommands import be.mygod.vpnhotspot.root.WifiApCommands
import be.mygod.vpnhotspot.util.* import be.mygod.vpnhotspot.util.*
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.net.NetworkInterface import java.net.NetworkInterface
@@ -45,28 +51,29 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
inner class ManagerAdapter : ListAdapter<Manager, RecyclerView.ViewHolder>(Manager), inner class ManagerAdapter : ListAdapter<Manager, RecyclerView.ViewHolder>(Manager),
TetheringManager.TetheringEventCallback { TetheringManager.TetheringEventCallback {
internal val repeaterManager by lazy { RepeaterManager(this@TetheringFragment) } internal val repeaterManager by lazy { RepeaterManager(this@TetheringFragment) }
@get:RequiresApi(26) internal val localOnlyHotspotManager by lazy { LocalOnlyHotspotManager(this@TetheringFragment) }
internal val localOnlyHotspotManager by lazy @TargetApi(26) { LocalOnlyHotspotManager(this@TetheringFragment) } internal val bluetoothManager by lazy {
@get:RequiresApi(24) requireContext().getSystemService<BluetoothManager>()?.adapter?.let {
internal val bluetoothManager by lazy @TargetApi(24) { TetherManager.Bluetooth(this@TetheringFragment) } TetherManager.Bluetooth(this@TetheringFragment, it)
@get:RequiresApi(24) }
private val tetherManagers by lazy @TargetApi(24) { }
listOf(TetherManager.Wifi(this@TetheringFragment), private val tetherManagers by lazy {
listOfNotNull(
TetherManager.Wifi(this@TetheringFragment),
TetherManager.Usb(this@TetheringFragment), TetherManager.Usb(this@TetheringFragment),
bluetoothManager) bluetoothManager,
)
} }
@get:RequiresApi(30) @get:RequiresApi(30)
private val tetherManagers30 by lazy @TargetApi(30) { private val ethernetManager by lazy @TargetApi(30) { TetherManager.Ethernet(this@TetheringFragment) }
listOf(TetherManager.Ethernet(this@TetheringFragment),
TetherManager.Ncm(this@TetheringFragment),
TetherManager.WiGig(this@TetheringFragment))
}
private val wifiManagerLegacy by lazy { TetherManager.WifiLegacy(this@TetheringFragment) }
private var enabledIfaces = emptyList<String>() var activeIfaces = emptyList<String>()
var localOnlyIfaces = emptyList<String>()
var erroredIfaces = emptyList<String>()
private var listDeferred = CompletableDeferred<List<Manager>>(emptyList()) private var listDeferred = CompletableDeferred<List<Manager>>(emptyList())
private fun updateEnabledTypes() { fun updateEnabledTypes() {
this@TetheringFragment.enabledTypes = enabledIfaces.map { TetherType.ofInterface(it) }.toSet() this@TetheringFragment.enabledTypes =
(activeIfaces + localOnlyIfaces).map { TetherType.ofInterface(it) }.toSet()
} }
val lastErrors = mutableMapOf<String, Int>() val lastErrors = mutableMapOf<String, Int>()
@@ -74,50 +81,42 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
if (error == 0) lastErrors.remove(ifName) else lastErrors[ifName] = error if (error == 0) lastErrors.remove(ifName) else lastErrors[ifName] = error
} }
suspend fun notifyInterfaceChanged(lastList: List<Manager>? = null) {
@Suppress("NAME_SHADOWING") val lastList = lastList ?: listDeferred.await()
val first = lastList.indexOfFirst { it is InterfaceManager }
if (first >= 0) notifyItemRangeChanged(first, lastList.indexOfLast { it is InterfaceManager } - first + 1)
}
suspend fun notifyTetherTypeChanged() { suspend fun notifyTetherTypeChanged() {
updateEnabledTypes() updateEnabledTypes()
val lastList = listDeferred.await() val lastList = listDeferred.await()
notifyInterfaceChanged(lastList) var first = lastList.indexOfFirst { it is InterfaceManager }
val first = lastList.indexOfLast { it !is TetherManager } + 1 withStarted {
if (first >= 0) {
notifyItemRangeChanged(first, lastList.indexOfLast { it is InterfaceManager } - first + 1)
}
first = lastList.indexOfLast { it !is TetherManager } + 1
notifyItemRangeChanged(first, lastList.size - first) notifyItemRangeChanged(first, lastList.size - first)
} }
}
fun update(activeIfaces: List<String>, localOnlyIfaces: List<String>, erroredIfaces: List<String>) { fun update() {
val deferred = CompletableDeferred<List<Manager>>() val deferred = CompletableDeferred<List<Manager>>()
listDeferred = deferred listDeferred = deferred
ifaceLookup = try { ifaceLookup = try {
NetworkInterface.getNetworkInterfaces().asSequence().associateBy { it.name } NetworkInterface.getNetworkInterfaces().asSequence().associateBy { it.name }
} catch (e: SocketException) { } catch (e: Exception) {
Timber.d(e) if (e is SocketException) Timber.d(e) else Timber.w(e)
emptyMap() emptyMap()
} }
enabledIfaces = activeIfaces + localOnlyIfaces
updateEnabledTypes()
val list = ArrayList<Manager>() val list = ArrayList<Manager>()
if (Services.p2p != null) list.add(repeaterManager) if (Services.p2p != null) list.add(repeaterManager)
if (Build.VERSION.SDK_INT >= 26) list.add(localOnlyHotspotManager) list.add(localOnlyHotspotManager)
val monitoredIfaces = binder?.monitoredIfaces ?: emptyList() val monitoredIfaces = binder?.monitoredIfaces ?: emptyList()
updateMonitorList(activeIfaces - monitoredIfaces) updateMonitorList(activeIfaces - monitoredIfaces.toSet())
list.addAll((activeIfaces + monitoredIfaces).toSortedSet() list.addAll((activeIfaces + monitoredIfaces).toSortedSet()
.map { InterfaceManager(this@TetheringFragment, it) }) .map { InterfaceManager(this@TetheringFragment, it) })
list.add(ManageBar) list.add(ManageBar)
if (Build.VERSION.SDK_INT >= 24) {
list.addAll(tetherManagers) list.addAll(tetherManagers)
tetherManagers.forEach { it.updateErrorMessage(erroredIfaces, lastErrors) } tetherManagers.forEach { it.updateErrorMessage(erroredIfaces, lastErrors) }
}
if (Build.VERSION.SDK_INT >= 30) { if (Build.VERSION.SDK_INT >= 30) {
list.addAll(tetherManagers30) list.add(ethernetManager)
tetherManagers30.forEach { it.updateErrorMessage(erroredIfaces, lastErrors) } ethernetManager.updateErrorMessage(erroredIfaces, lastErrors)
}
if (Build.VERSION.SDK_INT < 26) {
list.add(wifiManagerLegacy)
wifiManagerLegacy.onTetheringStarted()
} }
submitList(list) { deferred.complete(list) } submitList(list) { deferred.complete(list) }
} }
@@ -130,15 +129,17 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
@RequiresApi(29) @RequiresApi(29)
val startRepeater = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> val startRepeater = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) requireActivity().startForegroundService(Intent(activity, RepeaterService::class.java)) if (granted) app.startServiceWithLocation<RepeaterService>(requireContext()) else {
Snackbar.make((activity as MainActivity).binding.fragmentHolder,
R.string.repeater_missing_location_permissions, Snackbar.LENGTH_LONG).show()
}
} }
@RequiresApi(26)
val startLocalOnlyHotspot = registerForActivityResult(ActivityResultContracts.RequestPermission()) { val startLocalOnlyHotspot = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
adapter.localOnlyHotspotManager.start(requireContext()) adapter.localOnlyHotspotManager.start(requireContext())
} }
@RequiresApi(31) @RequiresApi(31)
val requestBluetooth = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> val requestBluetooth = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) adapter.bluetoothManager.ensureInit(requireContext()) if (granted) adapter.bluetoothManager!!.ensureInit(requireContext())
} }
var ifaceLookup: Map<String, NetworkInterface> = emptyMap() var ifaceLookup: Map<String, NetworkInterface> = emptyMap()
@@ -147,19 +148,22 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
var binder: TetheringService.Binder? = null var binder: TetheringService.Binder? = null
private val adapter = ManagerAdapter() private val adapter = ManagerAdapter()
private val receiver = broadcastReceiver { _, intent -> private val receiver = broadcastReceiver { _, intent ->
adapter.update(intent.tetheredIfaces ?: return@broadcastReceiver, adapter.activeIfaces = intent.tetheredIfaces ?: return@broadcastReceiver
intent.localOnlyTetheredIfaces ?: return@broadcastReceiver, adapter.localOnlyIfaces = intent.localOnlyTetheredIfaces ?: return@broadcastReceiver
intent.getStringArrayListExtra(TetheringManager.EXTRA_ERRORED_TETHER) ?: return@broadcastReceiver) adapter.erroredIfaces = intent.getStringArrayListExtra(TetheringManager.EXTRA_ERRORED_TETHER)
?: return@broadcastReceiver
adapter.updateEnabledTypes()
adapter.update()
} }
private fun updateMonitorList(canMonitor: List<String> = emptyList()) { private fun updateMonitorList(canMonitor: List<String> = emptyList()) {
val activity = activity as? MainActivity val activity = activity as? MainActivity
val item = activity?.binding?.toolbar?.menu?.findItem(R.id.monitor) ?: return // assuming no longer foreground val item = activity?.binding?.toolbar?.menu?.findItem(R.id.monitor) ?: return // assuming no longer foreground
item.isNotGone = canMonitor.isNotEmpty() item.isNotGone = canMonitor.isNotEmpty()
item.subMenu.apply { item.subMenu!!.apply {
clear() clear()
for (iface in canMonitor.sorted()) add(iface).setOnMenuItemClickListener { for (iface in canMonitor.sorted()) add(iface).setOnMenuItemClickListener {
ContextCompat.startForegroundService(activity, Intent(activity, TetheringService::class.java) activity.startForegroundService(Intent(activity, TetheringService::class.java)
.putExtra(TetheringService.EXTRA_ADD_INTERFACE_MONITOR, iface)) .putExtra(TetheringService.EXTRA_ADD_INTERFACE_MONITOR, iface))
true true
} }
@@ -169,7 +173,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
private var apConfigurationRunning = false private var apConfigurationRunning = false
override fun onMenuItemClick(item: MenuItem?): Boolean { override fun onMenuItemClick(item: MenuItem?): Boolean {
return when (item?.itemId) { return when (item?.itemId) {
R.id.configuration -> item.subMenu.run { R.id.configuration -> item.subMenu!!.run {
findItem(R.id.configuration_repeater).isNotGone = Services.p2p != null findItem(R.id.configuration_repeater).isNotGone = Services.p2p != null
findItem(R.id.configuration_temp_hotspot).isNotGone = findItem(R.id.configuration_temp_hotspot).isNotGone =
adapter.localOnlyHotspotManager.binder?.configuration != null adapter.localOnlyHotspotManager.binder?.configuration != null
@@ -189,28 +193,34 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
} }
R.id.configuration_ap -> if (apConfigurationRunning) false else { R.id.configuration_ap -> if (apConfigurationRunning) false else {
apConfigurationRunning = true apConfigurationRunning = true
viewLifecycleOwner.lifecycleScope.launchWhenCreated { viewLifecycleOwner.lifecycleScope.launch {
try { val configuration = try {
WifiApManager.configurationCompat if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
WifiApManager.configurationLegacy?.toCompat() ?: SoftApConfigurationCompat()
} else WifiApManager.configuration.toCompat()
} catch (e: InvocationTargetException) { } catch (e: InvocationTargetException) {
if (e.targetException !is SecurityException) Timber.w(e) if (e.targetException !is SecurityException) Timber.w(e)
try { try {
RootManager.use { it.execute(WifiApCommands.GetConfiguration()) } if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
RootManager.use { it.execute(WifiApCommands.GetConfigurationLegacy()) }?.toCompat()
?: SoftApConfigurationCompat()
} else RootManager.use { it.execute(WifiApCommands.GetConfiguration()) }.toCompat()
} catch (_: CancellationException) { } catch (_: CancellationException) {
null return@launch
} catch (eRoot: Exception) { } catch (eRoot: Exception) {
eRoot.addSuppressed(e) eRoot.addSuppressed(e)
if (Build.VERSION.SDK_INT !in 26..29 || eRoot.getRootCause() !is SecurityException) { if (Build.VERSION.SDK_INT >= 29 || eRoot.getRootCause() !is SecurityException) {
Timber.w(eRoot) Timber.w(eRoot)
} }
SmartSnackbar.make(eRoot).show() SmartSnackbar.make(eRoot).show()
null return@launch
} }
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
Timber.w(e) Timber.w(e)
SmartSnackbar.make(e).show() SmartSnackbar.make(e).show()
null return@launch
}?.let { configuration -> }
withStarted {
WifiApDialogFragment().apply { WifiApDialogFragment().apply {
arg(WifiApDialogFragment.Arg(configuration)) arg(WifiApDialogFragment.Arg(configuration))
key() key()
@@ -226,10 +236,10 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
AlertDialogFragment.setResultListener<WifiApDialogFragment, WifiApDialogFragment.Arg>(this) { which, ret -> AlertDialogFragment.setResultListener<WifiApDialogFragment, WifiApDialogFragment.Arg>(this) { which, ret ->
if (which == DialogInterface.BUTTON_POSITIVE) viewLifecycleOwner.lifecycleScope.launchWhenCreated { if (which == DialogInterface.BUTTON_POSITIVE) GlobalScope.launch {
val configuration = ret!!.configuration val configuration = ret!!.configuration
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT in 28 until 30 && if (Build.VERSION.SDK_INT < 30 &&
configuration.isAutoShutdownEnabled != TetherTimeoutMonitor.enabled) try { configuration.isAutoShutdownEnabled != TetherTimeoutMonitor.enabled) try {
TetherTimeoutMonitor.setEnabled(configuration.isAutoShutdownEnabled) TetherTimeoutMonitor.setEnabled(configuration.isAutoShutdownEnabled)
} catch (e: Exception) { } catch (e: Exception) {
@@ -237,10 +247,18 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
SmartSnackbar.make(e).show() SmartSnackbar.make(e).show()
} }
val success = try { val success = try {
WifiApManager.setConfigurationCompat(configuration) if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
WifiApManager.setConfiguration(configuration.toWifiConfiguration())
} else WifiApManager.setConfiguration(configuration.toPlatform())
} catch (e: InvocationTargetException) { } catch (e: InvocationTargetException) {
try { try {
RootManager.use { it.execute(WifiApCommands.SetConfiguration(configuration)) } if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
val wc = configuration.toWifiConfiguration()
RootManager.use { it.execute(WifiApCommands.SetConfigurationLegacy(wc)) }
} else {
val platform = configuration.toPlatform()
RootManager.use { it.execute(WifiApCommands.SetConfiguration(platform)) }
}
} catch (_: CancellationException) { } catch (_: CancellationException) {
} catch (eRoot: Exception) { } catch (eRoot: Exception) {
eRoot.addSuppressed(e) eRoot.addSuppressed(e)
@@ -256,7 +274,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
binding.interfaces.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) binding.interfaces.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
binding.interfaces.itemAnimator = DefaultItemAnimator() binding.interfaces.itemAnimator = DefaultItemAnimator()
binding.interfaces.adapter = adapter binding.interfaces.adapter = adapter
adapter.update(emptyList(), emptyList(), emptyList()) adapter.update()
ServiceForegroundConnector(this, this, TetheringService::class) ServiceForegroundConnector(this, this, TetheringService::class)
(activity as MainActivity).binding.toolbar.apply { (activity as MainActivity).binding.toolbar.apply {
inflateMenu(R.menu.toolbar_tethering) inflateMenu(R.menu.toolbar_tethering)
@@ -275,18 +293,22 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (Build.VERSION.SDK_INT >= 27) ManageBar.Data.notifyChange() ManageBar.Data.notifyChange()
} }
override fun onServiceConnected(name: ComponentName?, service: IBinder?) { override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
binder = service as TetheringService.Binder binder = service as TetheringService.Binder
service.routingsChanged[this] = { service.routingsChanged[this] = {
lifecycleScope.launchWhenStarted { adapter.notifyInterfaceChanged() } lifecycleScope.launch {
withStarted { adapter.update() }
}
} }
requireContext().registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED)) requireContext().registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
if (Build.VERSION.SDK_INT >= 30) { if (Build.VERSION.SDK_INT >= 30) {
TetheringManager.registerTetheringEventCallback(null, adapter) TetheringManager.registerTetheringEventCallback(null, adapter)
TetherType.listener[this] = { lifecycleScope.launchWhenStarted { adapter.notifyTetherTypeChanged() } } TetherType.listener[this] = {
lifecycleScope.launch { adapter.notifyTetherTypeChanged() }
}
} }
} }

View File

@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot.manage package be.mygod.vpnhotspot.manage
import android.bluetooth.BluetoothManager
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -10,13 +11,12 @@ import android.os.IBinder
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat import androidx.core.content.getSystemService
import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.TetheringService import be.mygod.vpnhotspot.TetheringService
import be.mygod.vpnhotspot.net.TetherType import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces
import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.util.broadcastReceiver import be.mygod.vpnhotspot.util.broadcastReceiver
import be.mygod.vpnhotspot.util.readableMessage import be.mygod.vpnhotspot.util.readableMessage
import be.mygod.vpnhotspot.util.stopAndUnbind import be.mygod.vpnhotspot.util.stopAndUnbind
@@ -25,7 +25,6 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
@RequiresApi(24)
sealed class TetheringTileService : IpNeighbourMonitoringTileService(), TetheringManager.StartTetheringCallback { sealed class TetheringTileService : IpNeighbourMonitoringTileService(), TetheringManager.StartTetheringCallback {
protected val tileOff by lazy { Icon.createWithResource(application, icon) } protected val tileOff by lazy { Icon.createWithResource(application, icon) }
protected val tileOn by lazy { Icon.createWithResource(application, R.drawable.ic_quick_settings_tile_on) } protected val tileOn by lazy { Icon.createWithResource(application, R.drawable.ic_quick_settings_tile_on) }
@@ -34,7 +33,7 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
protected abstract val tetherType: TetherType protected abstract val tetherType: TetherType
protected open val icon get() = tetherType.icon protected open val icon get() = tetherType.icon
private var tethered: List<String>? = null private var tethered: List<String>? = null
protected val interested get() = tethered?.filter { TetherType.ofInterface(it) == tetherType } protected val interested get() = tethered?.filter { TetherType.ofInterface(it).isA(tetherType) }
protected var binder: TetheringService.Binder? = null protected var binder: TetheringService.Binder? = null
private val receiver = broadcastReceiver { _, intent -> private val receiver = broadcastReceiver { _, intent ->
@@ -108,7 +107,7 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
stop() stop()
} catch (e: Exception) { } catch (e: Exception) {
onException(e) onException(e)
} else ContextCompat.startForegroundService(this, Intent(this, TetheringService::class.java) } else startForegroundService(Intent(this, TetheringService::class.java)
.putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray())) .putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray()))
} }
} }
@@ -151,15 +150,16 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
override val labelString get() = R.string.tethering_manage_bluetooth override val labelString get() = R.string.tethering_manage_bluetooth
override val tetherType get() = TetherType.BLUETOOTH override val tetherType get() = TetherType.BLUETOOTH
override fun start() = BluetoothTethering.start(this) override fun start() = tethering!!.start(this, this)
override fun stop() { override fun stop() {
TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH, this::onException) tethering!!.stop(this::onException)
Thread.sleep(1) // give others a room to breathe
onTetheringStarted() // force flush state onTetheringStarted() // force flush state
} }
override fun onStartListening() { override fun onStartListening() {
tethering = BluetoothTethering(this) { updateTile() } tethering = getSystemService<BluetoothManager>()?.adapter?.let {
BluetoothTethering(this, it) { updateTile() }
}
super.onStartListening() super.onStartListening()
} }
override fun onStopListening() { override fun onStopListening() {
@@ -187,7 +187,7 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
icon = tileOff icon = tileOff
} }
null -> { null -> {
state = Tile.STATE_UNAVAILABLE state = Tile.STATE_INACTIVE
icon = tileOff icon = tileOff
subtitle(tethering?.activeFailureCause?.readableMessage) subtitle(tethering?.activeFailureCause?.readableMessage)
} }
@@ -198,7 +198,8 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
} }
override fun onClick() { override fun onClick() {
when (tethering?.active) { val tethering = tethering
if (tethering == null) tapPending = true else when (tethering.active) {
true -> { true -> {
val binder = binder val binder = binder
if (binder == null) tapPending = true else { if (binder == null) tapPending = true else {
@@ -207,12 +208,12 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
stop() stop()
} catch (e: Exception) { } catch (e: Exception) {
onException(e) onException(e)
} else ContextCompat.startForegroundService(this, Intent(this, TetheringService::class.java) } else startForegroundService(Intent(this, TetheringService::class.java)
.putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray())) .putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray()))
} }
} }
false -> start() false -> start()
else -> tapPending = true else -> ManageBar.start(this)
} }
} }
} }
@@ -224,39 +225,4 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_ETHERNET, true, this) override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_ETHERNET, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_ETHERNET, this::onException) override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_ETHERNET, this::onException)
} }
@RequiresApi(30)
class Ncm : TetheringTileService() {
override val labelString get() = R.string.tethering_manage_ncm
override val tetherType get() = TetherType.NCM
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_NCM, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_NCM, this::onException)
}
@RequiresApi(30)
class WiGig : TetheringTileService() {
override val labelString get() = R.string.tethering_manage_wigig
override val tetherType get() = TetherType.WIGIG
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIGIG, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIGIG, this::onException)
}
@Suppress("DEPRECATION")
@Deprecated("Not usable since API 25")
class WifiLegacy : TetheringTileService() {
override val labelString get() = R.string.tethering_manage_wifi_legacy
override val tetherType get() = TetherType.WIFI
override val icon get() = R.drawable.ic_device_wifi_tethering
override fun start() = try {
WifiApManager.start()
} catch (e: Exception) {
onException(e)
}
override fun stop() = try {
WifiApManager.stop()
} catch (e: Exception) {
onException(e)
}
}
} }

View File

@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot.net package be.mygod.vpnhotspot.net
import android.net.MacAddress
import android.os.Build import android.os.Build
import android.system.ErrnoException import android.system.ErrnoException
import android.system.Os import android.system.Os
@@ -15,7 +16,7 @@ import java.io.IOException
import java.net.Inet4Address import java.net.Inet4Address
import java.net.InetAddress import java.net.InetAddress
data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddressCompat, val state: State) { data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddress, val state: State) {
enum class State { enum class State {
INCOMPLETE, VALID, FAILED, DELETING INCOMPLETE, VALID, FAILED, DELETING
} }
@@ -27,8 +28,8 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddr
* https://people.cs.clemson.edu/~westall/853/notes/arpstate.pdf * https://people.cs.clemson.edu/~westall/853/notes/arpstate.pdf
* Assumptions: IP addr (key) always present and RTM_GETNEIGH is never used * Assumptions: IP addr (key) always present and RTM_GETNEIGH is never used
*/ */
private val parser = "^(Deleted )?([^ ]+) dev ([^ ]+) (lladdr ([^ ]*))?.*?( ([INCOMPLET,RAHBSDYF]+))?\$" private val parser = ("^(Deleted )?(?:([^ ]+) )?dev ([^ ]+) (?:lladdr ([^ ]*))?.*?" +
.toRegex() "(?: ([INCOMPLET,RAHBSDYF]+))?\$").toRegex()
/** /**
* Fallback format will be used if if_indextoname returns null, which some stupid devices do. * Fallback format will be used if if_indextoname returns null, which some stupid devices do.
* *
@@ -49,14 +50,15 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddr
suspend fun parse(line: String, fullMode: Boolean): List<IpNeighbour> { suspend fun parse(line: String, fullMode: Boolean): List<IpNeighbour> {
return if (line.isBlank()) emptyList() else try { return if (line.isBlank()) emptyList() else try {
val match = parser.matchEntire(line)!! val match = parser.matchEntire(line)!!
val ip = parseNumericAddress(match.groupValues[2]) // by regex, ip is non-empty if (match.groups[2] == null) return emptyList()
val devs = substituteDev(match.groupValues[3]) // by regex, dev is non-empty as well val ip = parseNumericAddress(match.groupValues[2])
val state = if (match.groupValues[1].isNotEmpty()) State.DELETING else when (match.groupValues[7]) { val devs = substituteDev(match.groupValues[3]) // by regex, dev is non-empty
val state = if (match.groupValues[1].isNotEmpty()) State.DELETING else when (match.groupValues[5]) {
"", "INCOMPLETE" -> State.INCOMPLETE "", "INCOMPLETE" -> State.INCOMPLETE
"REACHABLE", "DELAY", "STALE", "PROBE", "PERMANENT" -> State.VALID "REACHABLE", "DELAY", "STALE", "PROBE", "PERMANENT" -> State.VALID
"FAILED" -> State.FAILED "FAILED" -> State.FAILED
"NOARP" -> return emptyList() // skip "NOARP" -> return emptyList() // skip
else -> throw IllegalArgumentException("Unknown state encountered: ${match.groupValues[7]}") else -> throw IllegalArgumentException("Unknown state encountered: ${match.groupValues[5]}")
} }
var lladdr = MacAddressCompat.ALL_ZEROS_ADDRESS var lladdr = MacAddressCompat.ALL_ZEROS_ADDRESS
if (!fullMode && state != State.VALID) { if (!fullMode && state != State.VALID) {
@@ -64,7 +66,7 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddr
return devs.map { IpNeighbour(ip, it, lladdr, State.DELETING) } return devs.map { IpNeighbour(ip, it, lladdr, State.DELETING) }
} }
if (match.groups[4] != null) try { if (match.groups[4] != null) try {
lladdr = MacAddressCompat.fromString(match.groupValues[5]) lladdr = MacAddress.fromString(match.groupValues[4])
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
if (state != State.INCOMPLETE && state != State.DELETING) { if (state != State.INCOMPLETE && state != State.DELETING) {
Timber.w(IOException("Failed to find MAC address for $line", e)) Timber.w(IOException("Failed to find MAC address for $line", e))
@@ -78,7 +80,7 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddr
val list = arp() val list = arp()
.asSequence() .asSequence()
.filter { parseNumericAddress(it[ARP_IP_ADDRESS]) == ip && it[ARP_DEVICE] in devs } .filter { parseNumericAddress(it[ARP_IP_ADDRESS]) == ip && it[ARP_DEVICE] in devs }
.map { MacAddressCompat.fromString(it[ARP_HW_ADDRESS]) } .map { MacAddress.fromString(it[ARP_HW_ADDRESS]) }
.filter { it != MacAddressCompat.ALL_ZEROS_ADDRESS } .filter { it != MacAddressCompat.ALL_ZEROS_ADDRESS }
.distinct() .distinct()
.toList() .toList()
@@ -137,5 +139,4 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddr
data class IpDev(val ip: InetAddress, val dev: String) { data class IpDev(val ip: InetAddress, val dev: String) {
override fun toString() = "$ip%$dev" override fun toString() = "$ip%$dev"
} }
@Suppress("FunctionName")
fun IpDev(neighbour: IpNeighbour) = IpDev(neighbour.ip, neighbour.dev) fun IpDev(neighbour: IpNeighbour) = IpDev(neighbour.ip, neighbour.dev)

View File

@@ -1,97 +1,34 @@
package be.mygod.vpnhotspot.net package be.mygod.vpnhotspot.net
import android.net.MacAddress import android.net.MacAddress
import androidx.annotation.RequiresApi
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
/** /**
* Compat support class for [MacAddress]. * This used to be a compat support class for [MacAddress].
* Now it is just a convenient class for backwards compatibility.
*/ */
@JvmInline @JvmInline
value class MacAddressCompat(val addr: Long) { value class MacAddressCompat(val addr: Long) {
companion object { companion object {
private const val ETHER_ADDR_LEN = 6
/** /**
* The MacAddress zero MAC address. * The MacAddress zero MAC address.
* *
* Not publicly exposed or treated specially since the OUI 00:00:00 is registered. * Not publicly exposed or treated specially since the OUI 00:00:00 is registered.
* @hide
*/ */
val ALL_ZEROS_ADDRESS = MacAddressCompat(0) val ALL_ZEROS_ADDRESS = MacAddress.fromBytes(byteArrayOf(0, 0, 0, 0, 0, 0))
val ANY_ADDRESS = MacAddressCompat(2) val ANY_ADDRESS = MacAddress.fromBytes(byteArrayOf(2, 0, 0, 0, 0, 0))
/** fun MacAddress.toLong() = ByteBuffer.allocate(Long.SIZE_BYTES).apply {
* Creates a MacAddress from the given byte array representation.
* A valid byte array representation for a MacAddress is a non-null array of length 6.
*
* @param addr a byte array representation of a MAC address.
* @return the MacAddress corresponding to the given byte array representation.
* @throws IllegalArgumentException if the given byte array is not a valid representation.
*/
fun fromBytes(addr: ByteArray) = ByteBuffer.allocate(Long.SIZE_BYTES).run {
order(ByteOrder.LITTLE_ENDIAN) order(ByteOrder.LITTLE_ENDIAN)
put(when (addr.size) { put(toByteArray())
ETHER_ADDR_LEN -> addr
8 -> {
require(addr.take(2).all { it == 0.toByte() }) {
"Unrecognized padding " + addr.joinToString(":") { "%02x".format(it) }
}
addr.drop(2).toByteArray()
}
else -> throw IllegalArgumentException(addr.joinToString(":") { "%02x".format(it) } +
" was not a valid MAC address")
})
rewind() rewind()
MacAddressCompat(long) }.long
}
/**
* Creates a MacAddress from the given String representation. A valid String representation
* for a MacAddress is a series of 6 values in the range [0,ff] printed in hexadecimal
* and joined by ':' characters.
*
* @param addr a String representation of a MAC address.
* @return the MacAddress corresponding to the given String representation.
* @throws IllegalArgumentException if the given String is not a valid representation.
*/
fun fromString(addr: String) = ByteBuffer.allocate(Long.SIZE_BYTES).run {
order(ByteOrder.LITTLE_ENDIAN)
var start = 0
var i = 0
while (position() < ETHER_ADDR_LEN && start < addr.length) {
val end = i
if (addr.getOrElse(i) { ':' } == ':') ++i else if (i < start + 2) {
++i
continue
}
put(if (start == end) 0 else try {
Integer.parseInt(addr.substring(start, end), 16).toByte()
} catch (e: NumberFormatException) {
throw IllegalArgumentException(e)
})
start = i
}
require(position() == ETHER_ADDR_LEN) { "MAC address too short" }
rewind()
MacAddressCompat(long)
} }
@RequiresApi(28) fun toPlatform() = MacAddress.fromBytes(ByteBuffer.allocate(8).run {
fun MacAddress.toCompat() = fromBytes(toByteArray())
}
fun validate() = require(addr and ((1L shl 48) - 1).inv() == 0L)
fun toList() = ByteBuffer.allocate(8).run {
order(ByteOrder.LITTLE_ENDIAN) order(ByteOrder.LITTLE_ENDIAN)
putLong(addr) putLong(addr)
array().take(6) array().take(6)
} }.toByteArray())
@RequiresApi(28)
fun toPlatform() = MacAddress.fromBytes(toList().toByteArray())
override fun toString() = toList().joinToString(":") { "%02x".format(it) }
fun toOui() = toList().joinToString("") { "%02x".format(it) }.substring(0, 9)
} }

View File

@@ -1,11 +1,9 @@
package be.mygod.vpnhotspot.net package be.mygod.vpnhotspot.net
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.net.LinkProperties import android.net.LinkProperties
import android.net.MacAddress
import android.net.RouteInfo import android.net.RouteInfo
import android.os.Build import android.system.Os
import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor
@@ -15,7 +13,9 @@ import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
import be.mygod.vpnhotspot.room.AppDatabase import be.mygod.vpnhotspot.room.AppDatabase
import be.mygod.vpnhotspot.root.RootManager import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.root.RoutingCommands import be.mygod.vpnhotspot.root.RoutingCommands
import be.mygod.vpnhotspot.util.* import be.mygod.vpnhotspot.util.RootSession
import be.mygod.vpnhotspot.util.allInterfaceNames
import be.mygod.vpnhotspot.util.allRoutes
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import timber.log.Timber import timber.log.Timber
@@ -125,7 +125,6 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
* *
* Source: https://android.googlesource.com/platform/system/netd/+/3b47c793ff7ade843b1d85a9be8461c3b4dc693e * Source: https://android.googlesource.com/platform/system/netd/+/3b47c793ff7ade843b1d85a9be8461c3b4dc693e
*/ */
@RequiresApi(28)
Netd, Netd,
} }
@@ -151,35 +150,24 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
private val upstreams = HashSet<String>() private val upstreams = HashSet<String>()
private class InterfaceGoneException(upstream: String) : IOException("Interface $upstream not found") private class InterfaceGoneException(upstream: String) : IOException("Interface $upstream not found")
private open inner class Upstream(val priority: Int) : UpstreamMonitor.Callback { private open inner class Upstream(val priority: Int) : UpstreamMonitor.Callback {
/**
* The only case when upstream is null is on API 23- and we are using system default rules.
*/
inner class Subrouting(priority: Int, val upstream: String) { inner class Subrouting(priority: Int, val upstream: String) {
val ifindex = if (upstream.isEmpty()) 0 else if_nametoindex(upstream).also { val ifindex = Os.if_nametoindex(upstream).also {
if (it <= 0) throw InterfaceGoneException(upstream) if (it <= 0) throw InterfaceGoneException(upstream)
} }
val transaction = RootSession.beginTransaction().safeguard { val transaction = RootSession.beginTransaction().safeguard {
if (upstream.isEmpty()) { ipRuleLookup(ifindex, priority)
ipRule("goto $RULE_PRIORITY_TETHERING", priority) // skip unreachable rule when (masqueradeMode) {
} else ipRuleLookup(ifindex, priority)
@TargetApi(28) when (masqueradeMode) {
MasqueradeMode.None -> { } // nothing to be done here MasqueradeMode.None -> { } // nothing to be done here
MasqueradeMode.Simple -> {
// note: specifying -i wouldn't work for POSTROUTING // note: specifying -i wouldn't work for POSTROUTING
iptablesAdd(if (upstream.isEmpty()) { MasqueradeMode.Simple -> iptablesAdd(
"vpnhotspot_masquerade -s $hostSubnet -j MASQUERADE" "vpnhotspot_masquerade -s $hostSubnet -o $upstream -j MASQUERADE", "nat")
} 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. * 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/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 * https://android.googlesource.com/platform/system/netd/+/android-5.0.0_r1/server/CommandListener.cpp#638
*/ */
ndc("Nat", "ndc nat enable $downstream $upstream 0") 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() updateDnsRoute()
} }
} }
private val fallbackUpstream = object : Upstream(RULE_PRIORITY_UPSTREAM_FALLBACK) { private val fallbackUpstream = Upstream(RULE_PRIORITY_UPSTREAM_FALLBACK)
@SuppressLint("NewApi")
override fun onFallback() = onAvailable(LinkProperties().apply {
interfaceName = ""
setDnsServers(listOf(parseNumericAddress("8.8.8.8")))
})
}
private val upstream = Upstream(RULE_PRIORITY_UPSTREAM) private val upstream = Upstream(RULE_PRIORITY_UPSTREAM)
private inner class Client(private val ip: Inet4Address, mac: MacAddressCompat) : AutoCloseable { private inner class Client(private val ip: Inet4Address, mac: MacAddress) : AutoCloseable {
private val transaction = RootSession.beginTransaction().safeguard { private val transaction = RootSession.beginTransaction().safeguard {
val address = ip.hostAddress val address = ip.hostAddress
iptablesInsert("vpnhotspot_acl -i $downstream -s $address -j ACCEPT") iptablesInsert("vpnhotspot_acl -i $downstream -s $address -j ACCEPT")
@@ -287,7 +269,7 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
* but may be broken when system tethering shutdown before local-only interfaces. * but may be broken when system tethering shutdown before local-only interfaces.
*/ */
fun ipForward() { fun ipForward() {
if (Build.VERSION.SDK_INT >= 23) try { try {
transaction.ndc("ipfwd", "ndc ipfwd enable vpnhotspot_$downstream", transaction.ndc("ipfwd", "ndc ipfwd enable vpnhotspot_$downstream",
"ndc ipfwd disable vpnhotspot_$downstream") "ndc ipfwd disable vpnhotspot_$downstream")
return return

View File

@@ -1,10 +1,8 @@
package be.mygod.vpnhotspot.net package be.mygod.vpnhotspot.net
import android.os.Build
import android.provider.Settings import android.provider.Settings
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.root.SettingsGlobalPut import be.mygod.vpnhotspot.root.SettingsGlobalPut
import timber.log.Timber
/** /**
* It's hard to change tethering rules with Tethering hardware acceleration enabled for now. * It's hard to change tethering rules with Tethering hardware acceleration enabled for now.
@@ -15,19 +13,6 @@ import timber.log.Timber
* https://android.googlesource.com/platform/hardware/qcom/data/ipacfg-mgr/+/master/msm8998/ipacm/src/IPACM_OffloadManager.cpp * https://android.googlesource.com/platform/hardware/qcom/data/ipacfg-mgr/+/master/msm8998/ipacm/src/IPACM_OffloadManager.cpp
*/ */
object TetherOffloadManager { object TetherOffloadManager {
val supported by lazy {
Build.VERSION.SDK_INT >= 27 || try {
Settings.Global::class.java.getDeclaredField("TETHER_OFFLOAD_DISABLED").get(null).let {
require(it == TETHER_OFFLOAD_DISABLED) { "Unknown field $it" }
}
true
} catch (_: NoSuchFieldException) {
false
} catch (e: Exception) {
Timber.w(e)
false
}
}
private const val TETHER_OFFLOAD_DISABLED = "tether_offload_disabled" private const val TETHER_OFFLOAD_DISABLED = "tether_offload_disabled"
val enabled get() = Settings.Global.getInt(app.contentResolver, TETHER_OFFLOAD_DISABLED, 0) == 0 val enabled get() = Settings.Global.getInt(app.contentResolver, TETHER_OFFLOAD_DISABLED, 0) == 0
suspend fun setEnabled(value: Boolean) = SettingsGlobalPut.int(TETHER_OFFLOAD_DISABLED, if (value) 0 else 1) suspend fun setEnabled(value: Boolean) = SettingsGlobalPut.int(TETHER_OFFLOAD_DISABLED, if (value) 0 else 1)

View File

@@ -28,6 +28,8 @@ enum class TetherType(@DrawableRes val icon: Int) {
else -> false else -> false
} }
fun isA(other: TetherType) = this == other || other == USB && this == NCM
companion object : TetheringManager.TetheringEventCallback { companion object : TetheringManager.TetheringEventCallback {
private lateinit var usbRegexs: List<Pattern> private lateinit var usbRegexs: List<Pattern>
private lateinit var wifiRegexs: List<Pattern> private lateinit var wifiRegexs: List<Pattern>
@@ -58,6 +60,9 @@ enum class TetherType(@DrawableRes val icon: Int) {
private fun updateRegexs() = synchronized(this) { private fun updateRegexs() = synchronized(this) {
if (!requiresUpdate) return@synchronized if (!requiresUpdate) return@synchronized
requiresUpdate = false requiresUpdate = false
usbRegexs = emptyList()
wifiRegexs = emptyList()
bluetoothRegexs = emptyList()
TetheringManager.registerTetheringEventCallback(null, this) TetheringManager.registerTetheringEventCallback(null, this)
val info = TetheringManager.resolvedService.serviceInfo val info = TetheringManager.resolvedService.serviceInfo
val tethering = "com.android.networkstack.tethering" to val tethering = "com.android.networkstack.tethering" to
@@ -71,9 +76,9 @@ enum class TetherType(@DrawableRes val icon: Int) {
} }
@RequiresApi(30) @RequiresApi(30)
override fun onTetherableInterfaceRegexpsChanged(args: Array<out Any?>?) = synchronized(this) { override fun onTetherableInterfaceRegexpsChanged(reg: Any?) = synchronized(this) {
if (requiresUpdate) return@synchronized if (requiresUpdate) return@synchronized
Timber.i("onTetherableInterfaceRegexpsChanged: ${args?.contentDeepToString()}") Timber.i("onTetherableInterfaceRegexpsChanged: $reg")
TetheringManager.unregisterTetheringEventCallback(this) TetheringManager.unregisterTetheringEventCallback(this)
requiresUpdate = true requiresUpdate = true
listener() listener()
@@ -104,13 +109,20 @@ enum class TetherType(@DrawableRes val icon: Int) {
* *
* Based on: https://android.googlesource.com/platform/frameworks/base/+/5d36f01/packages/Tethering/src/com/android/networkstack/tethering/Tethering.java#479 * Based on: https://android.googlesource.com/platform/frameworks/base/+/5d36f01/packages/Tethering/src/com/android/networkstack/tethering/Tethering.java#479
*/ */
fun ofInterface(iface: String?, p2pDev: String? = null) = synchronized(this) { ofInterfaceImpl(iface, p2pDev) } fun ofInterface(iface: String?, p2pDev: String? = null) = when (iface) {
private tailrec fun ofInterfaceImpl(iface: String?, p2pDev: String?): TetherType = when { null -> NONE
iface == null -> NONE p2pDev -> WIFI_P2P
iface == p2pDev -> WIFI_P2P else -> try {
synchronized(this) { ofInterfaceImpl(iface) }
} catch (e: RuntimeException) {
Timber.w(e)
NONE
}
}
private tailrec fun ofInterfaceImpl(iface: String): TetherType = when {
requiresUpdate -> { requiresUpdate -> {
if (Build.VERSION.SDK_INT >= 30) updateRegexs() else error("unexpected requiresUpdate") if (Build.VERSION.SDK_INT >= 30) updateRegexs() else error("unexpected requiresUpdate")
ofInterfaceImpl(iface, p2pDev) ofInterfaceImpl(iface)
} }
wifiRegexs.any { it.matcher(iface).matches() } -> WIFI wifiRegexs.any { it.matcher(iface).matches() } -> WIFI
wigigRegexs.any { it.matcher(iface).matches() } -> WIGIG wigigRegexs.any { it.matcher(iface).matches() } -> WIGIG

View File

@@ -10,6 +10,7 @@ import android.content.pm.PackageManager
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.Network import android.net.Network
import android.os.Build import android.os.Build
import android.os.DeadObjectException
import android.os.Handler import android.os.Handler
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.os.ExecutorCompat import androidx.core.os.ExecutorCompat
@@ -66,7 +67,11 @@ object TetheringManager {
} }
private object InPlaceExecutor : Executor { private object InPlaceExecutor : Executor {
override fun execute(command: Runnable) = command.run() override fun execute(command: Runnable) = try {
command.run()
} catch (e: Exception) {
Timber.w(e) // prevent Binder stub swallowing the exception
}
} }
/** /**
@@ -89,9 +94,7 @@ object TetheringManager {
* https://android.googlesource.com/platform/frameworks/base.git/+/2a091d7aa0c174986387e5d56bf97a87fe075bdb%5E%21/services/java/com/android/server/connectivity/Tethering.java * https://android.googlesource.com/platform/frameworks/base.git/+/2a091d7aa0c174986387e5d56bf97a87fe075bdb%5E%21/services/java/com/android/server/connectivity/Tethering.java
*/ */
const val ACTION_TETHER_STATE_CHANGED = "android.net.conn.TETHER_STATE_CHANGED" const val ACTION_TETHER_STATE_CHANGED = "android.net.conn.TETHER_STATE_CHANGED"
@RequiresApi(26)
private const val EXTRA_ACTIVE_LOCAL_ONLY_LEGACY = "localOnlyArray" private const val EXTRA_ACTIVE_LOCAL_ONLY_LEGACY = "localOnlyArray"
private const val EXTRA_ACTIVE_TETHER_LEGACY = "activeArray"
/** /**
* gives a String[] listing all the interfaces currently in local-only * gives a String[] listing all the interfaces currently in local-only
* mode (ie, has DHCPv4+IPv6-ULA support and no packet forwarding) * mode (ie, has DHCPv4+IPv6-ULA support and no packet forwarding)
@@ -102,7 +105,6 @@ object TetheringManager {
* gives a String[] listing all the interfaces currently tethered * gives a String[] listing all the interfaces currently tethered
* (ie, has DHCPv4 support and packets potentially forwarded/NATed) * (ie, has DHCPv4 support and packets potentially forwarded/NATed)
*/ */
@RequiresApi(26)
private const val EXTRA_ACTIVE_TETHER = "tetherArray" private const val EXTRA_ACTIVE_TETHER = "tetherArray"
/** /**
* gives a String[] listing all the interfaces we tried to tether and * gives a String[] listing all the interfaces we tried to tether and
@@ -126,7 +128,6 @@ object TetheringManager {
* Wifi tethering type. * Wifi tethering type.
* @see [startTethering]. * @see [startTethering].
*/ */
@RequiresApi(24)
const val TETHERING_WIFI = 0 const val TETHERING_WIFI = 0
/** /**
* USB tethering type. * USB tethering type.
@@ -134,48 +135,33 @@ object TetheringManager {
* Requires MANAGE_USB permission, unfortunately. * Requires MANAGE_USB permission, unfortunately.
* *
* Source: https://android.googlesource.com/platform/frameworks/base/+/7ca5d3a/services/usb/java/com/android/server/usb/UsbService.java#389 * Source: https://android.googlesource.com/platform/frameworks/base/+/7ca5d3a/services/usb/java/com/android/server/usb/UsbService.java#389
* @see [startTethering]. * @see startTethering
*/ */
@RequiresApi(24)
const val TETHERING_USB = 1 const val TETHERING_USB = 1
/** /**
* Bluetooth tethering type. * Bluetooth tethering type.
* *
* Requires BLUETOOTH permission. * Requires BLUETOOTH permission.
* @see [startTethering]. * @see startTethering
*/ */
@RequiresApi(24)
const val TETHERING_BLUETOOTH = 2 const val TETHERING_BLUETOOTH = 2
/**
* Ncm local tethering type.
*
* @see [startTethering]
*/
@RequiresApi(30)
const val TETHERING_NCM = 4
/** /**
* Ethernet tethering type. * Ethernet tethering type.
* *
* Requires MANAGE_USB permission, also. * Requires MANAGE_USB permission, also.
* @see [startTethering] * @see startTethering
*/ */
@RequiresApi(30) @RequiresApi(30)
const val TETHERING_ETHERNET = 5 const val TETHERING_ETHERNET = 5
/** @RequiresApi(31) // TETHERING_WIFI_P2P
* WIGIG tethering type. Use a separate type to prevent private val expectedTypes = setOf(TETHERING_WIFI, TETHERING_USB, TETHERING_BLUETOOTH, 3, TETHERING_ETHERNET)
* conflicts with TETHERING_WIFI
* This type is only used internally by the tethering module
* @hide
*/
@RequiresApi(30)
const val TETHERING_WIGIG = 6
@get:RequiresApi(30) @get:RequiresApi(30)
private val clazz by lazy { Class.forName("android.net.TetheringManager") } private val clazz by lazy { Class.forName("android.net.TetheringManager") }
@get:RequiresApi(30) @get:RequiresApi(30)
private val instance by lazy @TargetApi(30) { private val instance by lazy @TargetApi(30) {
@SuppressLint("WrongConstant") // hidden services are not included in constants as of R preview 4 @SuppressLint("WrongConstant") // hidden services are not included in constants as of R preview 4
val service = Services.context.getSystemService(TETHERING_SERVICE) val service = Services.context.getSystemService(TETHERING_SERVICE)!!
service service
} }
@@ -188,20 +174,17 @@ object TetheringManager {
} }
}.first() }.first()
@get:RequiresApi(24)
private val classOnStartTetheringCallback by lazy { private val classOnStartTetheringCallback by lazy {
Class.forName("android.net.ConnectivityManager\$OnStartTetheringCallback") Class.forName("android.net.ConnectivityManager\$OnStartTetheringCallback")
} }
@get:RequiresApi(24)
private val startTetheringLegacy by lazy { private val startTetheringLegacy by lazy {
ConnectivityManager::class.java.getDeclaredMethod("startTethering", ConnectivityManager::class.java.getDeclaredMethod("startTethering",
Int::class.java, Boolean::class.java, classOnStartTetheringCallback, Handler::class.java) Int::class.java, Boolean::class.java, classOnStartTetheringCallback, Handler::class.java)
} }
@get:RequiresApi(24)
private val stopTetheringLegacy by lazy { private val stopTetheringLegacy by lazy {
ConnectivityManager::class.java.getDeclaredMethod("stopTethering", Int::class.java) ConnectivityManager::class.java.getDeclaredMethod("stopTethering", Int::class.java)
} }
private val getLastTetherError by lazy { private val getLastTetherError by lazy @SuppressLint("SoonBlockedPrivateApi") {
ConnectivityManager::class.java.getDeclaredMethod("getLastTetherError", String::class.java) ConnectivityManager::class.java.getDeclaredMethod("getLastTetherError", String::class.java)
} }
@@ -210,50 +193,50 @@ object TetheringManager {
Class.forName("android.net.TetheringManager\$TetheringRequest\$Builder") Class.forName("android.net.TetheringManager\$TetheringRequest\$Builder")
} }
@get:RequiresApi(30) @get:RequiresApi(30)
private val newTetheringRequestBuilder by lazy { classTetheringRequestBuilder.getConstructor(Int::class.java) } private val newTetheringRequestBuilder by lazy @TargetApi(30) {
classTetheringRequestBuilder.getConstructor(Int::class.java)
}
// @get:RequiresApi(30) // @get:RequiresApi(30)
// private val setStaticIpv4Addresses by lazy { // private val setStaticIpv4Addresses by lazy {
// classTetheringRequestBuilder.getDeclaredMethod("setStaticIpv4Addresses", // classTetheringRequestBuilder.getDeclaredMethod("setStaticIpv4Addresses",
// LinkAddress::class.java, LinkAddress::class.java) // LinkAddress::class.java, LinkAddress::class.java)
// } // }
@get:RequiresApi(30) @get:RequiresApi(30)
private val setExemptFromEntitlementCheck by lazy { private val setExemptFromEntitlementCheck by lazy @TargetApi(30) {
classTetheringRequestBuilder.getDeclaredMethod("setExemptFromEntitlementCheck", Boolean::class.java) classTetheringRequestBuilder.getDeclaredMethod("setExemptFromEntitlementCheck", Boolean::class.java)
} }
@get:RequiresApi(30) @get:RequiresApi(30)
private val setShouldShowEntitlementUi by lazy { private val setShouldShowEntitlementUi by lazy @TargetApi(30) {
classTetheringRequestBuilder.getDeclaredMethod("setShouldShowEntitlementUi", Boolean::class.java) classTetheringRequestBuilder.getDeclaredMethod("setShouldShowEntitlementUi", Boolean::class.java)
} }
@get:RequiresApi(30) @get:RequiresApi(30)
private val build by lazy { classTetheringRequestBuilder.getDeclaredMethod("build") } private val build by lazy @TargetApi(30) { classTetheringRequestBuilder.getDeclaredMethod("build") }
@get:RequiresApi(30) @get:RequiresApi(30)
private val interfaceStartTetheringCallback by lazy { private val interfaceStartTetheringCallback by lazy {
Class.forName("android.net.TetheringManager\$StartTetheringCallback") Class.forName("android.net.TetheringManager\$StartTetheringCallback")
} }
@get:RequiresApi(30) @get:RequiresApi(30)
private val startTethering by lazy { private val startTethering by lazy @TargetApi(30) {
clazz.getDeclaredMethod("startTethering", Class.forName("android.net.TetheringManager\$TetheringRequest"), clazz.getDeclaredMethod("startTethering", Class.forName("android.net.TetheringManager\$TetheringRequest"),
Executor::class.java, interfaceStartTetheringCallback) Executor::class.java, interfaceStartTetheringCallback)
} }
@get:RequiresApi(30) @get:RequiresApi(30)
private val stopTethering by lazy { clazz.getDeclaredMethod("stopTethering", Int::class.java) } private val stopTethering by lazy @TargetApi(30) { clazz.getDeclaredMethod("stopTethering", Int::class.java) }
@Deprecated("Legacy API") @Deprecated("Legacy API")
@RequiresApi(24)
fun startTetheringLegacy(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback, fun startTetheringLegacy(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
handler: Handler? = null, cacheDir: File = app.deviceStorage.codeCacheDir) { handler: Handler? = null, cacheDir: File = app.deviceStorage.codeCacheDir) {
val reference = WeakReference(callback) val reference = WeakReference(callback)
val proxy = ProxyBuilder.forClass(classOnStartTetheringCallback).apply { val proxy = ProxyBuilder.forClass(classOnStartTetheringCallback).apply {
dexCache(cacheDir) dexCache(cacheDir)
handler { proxy, method, args -> handler { proxy, method, args ->
if (args.isNotEmpty()) Timber.w("Unexpected args for ${method.name}: $args")
@Suppress("NAME_SHADOWING") val callback = reference.get() @Suppress("NAME_SHADOWING") val callback = reference.get()
when (method.name) { if (args.isEmpty()) when (method.name) {
"onTetheringStarted" -> callback?.onTetheringStarted() "onTetheringStarted" -> return@handler callback?.onTetheringStarted()
"onTetheringFailed" -> callback?.onTetheringFailed() "onTetheringFailed" -> return@handler callback?.onTetheringFailed()
else -> ProxyBuilder.callSuper(proxy, method, args)
} }
ProxyBuilder.callSuper(proxy, method, args)
} }
}.build() }.build()
startTetheringLegacy(Services.connectivity, type, showProvisioningUi, proxy, handler) startTetheringLegacy(Services.connectivity, type, showProvisioningUi, proxy, handler)
@@ -275,13 +258,9 @@ object TetheringManager {
arrayOf(interfaceStartTetheringCallback), object : InvocationHandler { arrayOf(interfaceStartTetheringCallback), object : InvocationHandler {
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? { override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
@Suppress("NAME_SHADOWING") val callback = reference.get() @Suppress("NAME_SHADOWING") val callback = reference.get()
return when (val name = method.name) { return when {
"onTetheringStarted" -> { method.matches("onTetheringStarted") -> callback?.onTetheringStarted()
if (!args.isNullOrEmpty()) Timber.w("Unexpected args for $name: $args") method.matches("onTetheringFailed", Integer.TYPE) -> {
callback?.onTetheringStarted()
}
"onTetheringFailed" -> {
if (args?.size != 1) Timber.w("Unexpected args for $name: $args")
callback?.onTetheringFailed(args?.get(0) as Int) callback?.onTetheringFailed(args?.get(0) as Int)
} }
else -> callSuper(interfaceStartTetheringCallback, proxy, method, args) else -> callSuper(interfaceStartTetheringCallback, proxy, method, args)
@@ -312,7 +291,6 @@ object TetheringManager {
* configures tethering with the preferred local IPv4 link address to use. * configures tethering with the preferred local IPv4 link address to use.
* *@see setStaticIpv4Addresses * *@see setStaticIpv4Addresses
*/ */
@RequiresApi(24)
fun startTethering(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback, fun startTethering(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
handler: Handler? = null, cacheDir: File = app.deviceStorage.codeCacheDir) { handler: Handler? = null, cacheDir: File = app.deviceStorage.codeCacheDir) {
if (Build.VERSION.SDK_INT >= 30) try { if (Build.VERSION.SDK_INT >= 30) try {
@@ -384,12 +362,10 @@ object TetheringManager {
* {@link ConnectivityManager.TETHERING_USB}, or * {@link ConnectivityManager.TETHERING_USB}, or
* {@link ConnectivityManager.TETHERING_BLUETOOTH}. * {@link ConnectivityManager.TETHERING_BLUETOOTH}.
*/ */
@RequiresApi(24)
fun stopTethering(type: Int) { fun stopTethering(type: Int) {
if (Build.VERSION.SDK_INT >= 30) stopTethering(instance, type) if (Build.VERSION.SDK_INT >= 30) stopTethering(instance, type)
else stopTetheringLegacy(Services.connectivity, type) else stopTetheringLegacy(Services.connectivity, type)
} }
@RequiresApi(24)
fun stopTethering(type: Int, callback: (Exception) -> Unit) { fun stopTethering(type: Int, callback: (Exception) -> Unit) {
try { try {
stopTethering(type) stopTethering(type)
@@ -423,6 +399,23 @@ object TetheringManager {
*/ */
fun onTetheringSupported(supported: Boolean) {} fun onTetheringSupported(supported: Boolean) {}
/**
* Called when tethering supported status changed.
*
* This will be called immediately after the callback is registered, and may be called
* multiple times later upon changes.
*
* Tethering may be disabled via system properties, device configuration, or device
* policy restrictions.
*
* @param supportedTypes a set of @TetheringType which is supported.
*/
@TargetApi(31)
fun onSupportedTetheringTypes(supportedTypes: Set<Int?>) {
if ((supportedTypes - expectedTypes).isNotEmpty()) Timber.w(Exception(
"Unexpected supported tethering types: ${supportedTypes.joinToString()}"))
}
/** /**
* Called when tethering upstream changed. * Called when tethering upstream changed.
* *
@@ -445,7 +438,7 @@ object TetheringManager {
* *@param reg The new regular expressions. * *@param reg The new regular expressions.
* @hide * @hide
*/ */
fun onTetherableInterfaceRegexpsChanged(args: Array<out Any?>?) {} fun onTetherableInterfaceRegexpsChanged(reg: Any?) {}
/** /**
* Called when there was a change in the list of tetherable interfaces. Tetherable * Called when there was a change in the list of tetherable interfaces. Tetherable
@@ -507,11 +500,11 @@ object TetheringManager {
Class.forName("android.net.TetheringManager\$TetheringEventCallback") Class.forName("android.net.TetheringManager\$TetheringEventCallback")
} }
@get:RequiresApi(30) @get:RequiresApi(30)
private val registerTetheringEventCallback by lazy { private val registerTetheringEventCallback by lazy @TargetApi(30) {
clazz.getDeclaredMethod("registerTetheringEventCallback", Executor::class.java, interfaceTetheringEventCallback) clazz.getDeclaredMethod("registerTetheringEventCallback", Executor::class.java, interfaceTetheringEventCallback)
} }
@get:RequiresApi(30) @get:RequiresApi(30)
private val unregisterTetheringEventCallback by lazy { private val unregisterTetheringEventCallback by lazy @TargetApi(30) {
clazz.getDeclaredMethod("unregisterTetheringEventCallback", interfaceTetheringEventCallback) clazz.getDeclaredMethod("unregisterTetheringEventCallback", interfaceTetheringEventCallback)
} }
@@ -541,40 +534,38 @@ object TetheringManager {
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? { override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
val callback = reference.get() val callback = reference.get()
val noArgs = args?.size ?: 0 return when {
return when (val name = method.name) { method.matches("onTetheringSupported", Boolean::class.java) -> {
"onTetheringSupported" -> {
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
callback?.onTetheringSupported(args!![0] as Boolean) callback?.onTetheringSupported(args!![0] as Boolean)
} }
"onUpstreamChanged" -> { method.matches1<java.util.Set<*>>("onSupportedTetheringTypes") -> {
if (noArgs != 1) Timber.w("Unexpected args for $name: $args") @Suppress("UNCHECKED_CAST")
callback?.onSupportedTetheringTypes(args!![0] as Set<Int?>)
}
method.matches1<Network>("onUpstreamChanged") -> {
callback?.onUpstreamChanged(args!![0] as Network?) callback?.onUpstreamChanged(args!![0] as Network?)
} }
"onTetherableInterfaceRegexpsChanged" -> { method.name == "onTetherableInterfaceRegexpsChanged" &&
if (regexpsSent) callback?.onTetherableInterfaceRegexpsChanged(args) method.parameters.singleOrNull()?.type?.name ==
"android.net.TetheringManager\$TetheringInterfaceRegexps" -> {
if (regexpsSent) callback?.onTetherableInterfaceRegexpsChanged(args!!.single())
regexpsSent = true regexpsSent = true
} }
"onTetherableInterfacesChanged" -> { method.matches1<java.util.List<*>>("onTetherableInterfacesChanged") -> {
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
callback?.onTetherableInterfacesChanged(args!![0] as List<String?>) callback?.onTetherableInterfacesChanged(args!![0] as List<String?>)
} }
"onTetheredInterfacesChanged" -> { method.matches1<java.util.List<*>>("onTetheredInterfacesChanged") -> {
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
callback?.onTetheredInterfacesChanged(args!![0] as List<String?>) callback?.onTetheredInterfacesChanged(args!![0] as List<String?>)
} }
"onError" -> { method.matches("onError", String::class.java, Integer.TYPE) -> {
if (noArgs != 2) Timber.w("Unexpected args for $name: $args")
callback?.onError(args!![0] as String, args[1] as Int) callback?.onError(args!![0] as String, args[1] as Int)
} }
"onClientsChanged" -> { method.matches1<java.util.Collection<*>>("onClientsChanged") -> {
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
callback?.onClientsChanged(args!![0] as Collection<*>) callback?.onClientsChanged(args!![0] as Collection<*>)
} }
"onOffloadStatusChanged" -> { method.matches("onOffloadStatusChanged", Integer.TYPE) -> {
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
callback?.onOffloadStatusChanged(args!![0] as Int) callback?.onOffloadStatusChanged(args!![0] as Int)
} }
else -> callSuper(interfaceTetheringEventCallback, proxy, method, args) else -> callSuper(interfaceTetheringEventCallback, proxy, method, args)
@@ -596,7 +587,11 @@ object TetheringManager {
@RequiresApi(30) @RequiresApi(30)
fun unregisterTetheringEventCallback(callback: TetheringEventCallback) { fun unregisterTetheringEventCallback(callback: TetheringEventCallback) {
val proxy = synchronized(callbackMap) { callbackMap.remove(callback) } ?: return val proxy = synchronized(callbackMap) { callbackMap.remove(callback) } ?: return
try {
unregisterTetheringEventCallback(instance, proxy) unregisterTetheringEventCallback(instance, proxy)
} catch (e: InvocationTargetException) {
if (!e.targetException.let { it is IllegalStateException && it.cause is DeadObjectException }) throw e
}
} }
/** /**
@@ -636,14 +631,11 @@ object TetheringManager {
"TETHER_ERROR_UNSUPPORTED", "TETHER_ERROR_UNAVAIL_IFACE", "TETHER_ERROR_MASTER_ERROR", "TETHER_ERROR_UNSUPPORTED", "TETHER_ERROR_UNAVAIL_IFACE", "TETHER_ERROR_MASTER_ERROR",
"TETHER_ERROR_TETHER_IFACE_ERROR", "TETHER_ERROR_UNTETHER_IFACE_ERROR", "TETHER_ERROR_ENABLE_NAT_ERROR", "TETHER_ERROR_TETHER_IFACE_ERROR", "TETHER_ERROR_UNTETHER_IFACE_ERROR", "TETHER_ERROR_ENABLE_NAT_ERROR",
"TETHER_ERROR_DISABLE_NAT_ERROR", "TETHER_ERROR_IFACE_CFG_ERROR", "TETHER_ERROR_PROVISION_FAILED", "TETHER_ERROR_DISABLE_NAT_ERROR", "TETHER_ERROR_IFACE_CFG_ERROR", "TETHER_ERROR_PROVISION_FAILED",
"TETHER_ERROR_DHCPSERVER_ERROR", "TETHER_ERROR_ENTITLEMENT_UNKNOWN") { clazz } "TETHER_ERROR_DHCPSERVER_ERROR", "TETHER_ERROR_ENTITLEMENT_UNKNOWN") @TargetApi(30) { clazz }
@RequiresApi(30) @RequiresApi(30)
const val TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION = 14 const val TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION = 14
val Intent.tetheredIfaces get() = getStringArrayListExtra( val Intent.tetheredIfaces get() = getStringArrayListExtra(EXTRA_ACTIVE_TETHER)
if (Build.VERSION.SDK_INT >= 26) EXTRA_ACTIVE_TETHER else EXTRA_ACTIVE_TETHER_LEGACY) val Intent.localOnlyTetheredIfaces get() = getStringArrayListExtra(
val Intent.localOnlyTetheredIfaces get() = if (Build.VERSION.SDK_INT >= 26) {
getStringArrayListExtra(
if (Build.VERSION.SDK_INT >= 30) EXTRA_ACTIVE_LOCAL_ONLY else EXTRA_ACTIVE_LOCAL_ONLY_LEGACY) if (Build.VERSION.SDK_INT >= 30) EXTRA_ACTIVE_LOCAL_ONLY else EXTRA_ACTIVE_LOCAL_ONLY_LEGACY)
} else emptyList<String>()
} }

View File

@@ -1,14 +1,12 @@
package be.mygod.vpnhotspot.net.monitor package be.mygod.vpnhotspot.net.monitor
import android.annotation.TargetApi
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.LinkProperties import android.net.LinkProperties
import android.net.Network import android.net.Network
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.os.Build import android.os.Build
import android.os.Handler
import android.os.Looper
import be.mygod.vpnhotspot.util.Services import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.globalNetworkRequestBuilder
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -20,10 +18,10 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
* Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1: * Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1:
* https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e * https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
*/ */
private val networkRequest = networkRequestBuilder() private val networkRequest = globalNetworkRequestBuilder().apply {
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
.build() }.build()
private val networkCallback = object : ConnectivityManager.NetworkCallback() { private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
val properties = Services.connectivity.getLinkProperties(network) val properties = Services.connectivity.getLinkProperties(network)
@@ -53,23 +51,10 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
callback.onAvailable(currentLinkProperties) callback.onAvailable(currentLinkProperties)
} }
} else { } else {
when (Build.VERSION.SDK_INT) { if (Build.VERSION.SDK_INT >= 31) {
in 31..Int.MAX_VALUE -> @TargetApi(31) {
Services.connectivity.registerBestMatchingNetworkCallback(networkRequest, networkCallback, Services.connectivity.registerBestMatchingNetworkCallback(networkRequest, networkCallback,
Handler(Looper.getMainLooper())) Services.mainHandler)
} } else Services.connectivity.requestNetwork(networkRequest, networkCallback, Services.mainHandler)
in 24..27 -> @TargetApi(24) {
Services.connectivity.registerDefaultNetworkCallback(networkCallback)
}
else -> try {
Services.connectivity.requestNetwork(networkRequest, networkCallback)
} catch (e: SecurityException) {
// SecurityException would be thrown in requestNetwork on Android 6.0 thanks to Google's stupid bug
if (Build.VERSION.SDK_INT != 23) throw e
GlobalScope.launch { callback.onFallback() }
return
}
}
registered = true registered = true
} }
} }

View File

@@ -6,6 +6,7 @@ import android.net.Network
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import be.mygod.vpnhotspot.util.Services import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.allInterfaceNames import be.mygod.vpnhotspot.util.allInterfaceNames
import be.mygod.vpnhotspot.util.globalNetworkRequestBuilder
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
@@ -18,7 +19,7 @@ class InterfaceMonitor(private val ifaceRegex: String) : UpstreamMonitor() {
Timber.d(e); Timber.d(e);
{ it == ifaceRegex } { it == ifaceRegex }
} }
private val request = networkRequestBuilder().apply { private val request = globalNetworkRequestBuilder().apply {
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED) removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
@@ -76,7 +77,7 @@ class InterfaceMonitor(private val ifaceRegex: String) : UpstreamMonitor() {
callback.onAvailable(currentLinkProperties) callback.onAvailable(currentLinkProperties)
} }
} else { } else {
Services.connectivity.registerNetworkCallback(request, networkCallback) Services.registerNetworkCallback(request, networkCallback)
registered = true registered = true
} }
} }

View File

@@ -5,7 +5,6 @@ import androidx.core.content.edit
import be.mygod.librootkotlinx.RootServer import be.mygod.librootkotlinx.RootServer
import be.mygod.librootkotlinx.isEBADF import be.mygod.librootkotlinx.isEBADF
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.BuildConfig
import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.net.Routing import be.mygod.vpnhotspot.net.Routing
import be.mygod.vpnhotspot.root.ProcessData import be.mygod.vpnhotspot.root.ProcessData
@@ -25,12 +24,12 @@ abstract class IpMonitor {
companion object { companion object {
const val KEY = "service.ipMonitor" const val KEY = "service.ipMonitor"
// https://android.googlesource.com/platform/external/iproute2/+/7f7a711/lib/libnetlink.c#493 // https://android.googlesource.com/platform/external/iproute2/+/7f7a711/lib/libnetlink.c#493
private val errorMatcher = ("(^Cannot bind netlink socket: |" + private val errorMatcher = ("(?:^Cannot (?:bind netlink socket|send dump request): |^request send failed: |" +
"Dump (was interrupted and may be inconsistent.|terminated)$)").toRegex() "Dump (was interrupted and may be inconsistent.|terminated)$)").toRegex()
var currentMode: Mode var currentMode: Mode
get() { get() {
val isLegacy = Build.VERSION.SDK_INT < 30 || BuildConfig.TARGET_SDK < 30 // Completely restricted on Android 13: https://github.com/termux/termux-app/issues/2993#issuecomment-1250312777
val defaultMode = if (isLegacy) @Suppress("DEPRECATION") { val defaultMode = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
Mode.Poll Mode.Poll
} else Mode.MonitorRoot } else Mode.MonitorRoot
return Mode.valueOf(app.pref.getString(KEY, defaultMode.toString()) ?: "") return Mode.valueOf(app.pref.getString(KEY, defaultMode.toString()) ?: "")
@@ -114,8 +113,8 @@ abstract class IpMonitor {
try { try {
RootManager.use { server -> RootManager.use { server ->
// while we only need to use this server once, we need to also keep the server alive // while we only need to use this server once, we need to also keep the server alive
handleChannel(server.create(ProcessListener(errorMatcher, Routing.IP, "monitor", monitoredObject), handleChannel(server.create(ProcessListener(errorMatcher,
this)) Routing.IP, "monitor", monitoredObject), this))
} }
} catch (_: CancellationException) { } catch (_: CancellationException) {
} catch (e: Exception) { } catch (e: Exception) {
@@ -152,7 +151,7 @@ abstract class IpMonitor {
fun flushAsync() = GlobalScope.launch(Dispatchers.IO) { flush() } fun flushAsync() = GlobalScope.launch(Dispatchers.IO) { flush() }
private suspend fun work(server: RootServer?): RootServer? { private suspend fun work(server: RootServer?): RootServer? {
if (currentMode != Mode.PollRoot) try { if (currentMode != Mode.PollRoot && currentMode != Mode.MonitorRoot) try {
poll() poll()
return server return server
} catch (e: IOException) { } catch (e: IOException) {

View File

@@ -33,30 +33,41 @@ class TetherTimeoutMonitor(private val timeout: Long = 0,
private const val MIN_SOFT_AP_TIMEOUT_DELAY_MS = 600_000 // 10 minutes private const val MIN_SOFT_AP_TIMEOUT_DELAY_MS = 600_000 // 10 minutes
@Deprecated("Use SoftApConfigurationCompat instead") @Deprecated("Use SoftApConfigurationCompat instead")
@get:RequiresApi(28)
val enabled get() = Settings.Global.getInt(app.contentResolver, SOFT_AP_TIMEOUT_ENABLED, 1) == 1 val enabled get() = Settings.Global.getInt(app.contentResolver, SOFT_AP_TIMEOUT_ENABLED, 1) == 1
@Deprecated("Use SoftApConfigurationCompat instead") @Deprecated("Use SoftApConfigurationCompat instead")
suspend fun setEnabled(value: Boolean) = SettingsGlobalPut.int(SOFT_AP_TIMEOUT_ENABLED, if (value) 1 else 0) suspend fun setEnabled(value: Boolean) = SettingsGlobalPut.int(SOFT_AP_TIMEOUT_ENABLED, if (value) 1 else 0)
val defaultTimeout: Int get() { val defaultTimeout: Int get() {
val delay = if (Build.VERSION.SDK_INT >= 28) try { val delay = try {
if (Build.VERSION.SDK_INT < 30) Resources.getSystem().run { if (Build.VERSION.SDK_INT < 30) Resources.getSystem().run {
getInteger(getIdentifier("config_wifi_framework_soft_ap_timeout_delay", "integer", "android")) getInteger(getIdentifier("config_wifi_framework_soft_ap_timeout_delay", "integer", "android"))
} else { } else {
val info = WifiApManager.resolvedActivity.activityInfo val info = WifiApManager.resolvedActivity.activityInfo
val resources = app.packageManager.getResourcesForApplication(info.applicationInfo) val resources = app.packageManager.getResourcesForApplication(info.applicationInfo)
resources.getInteger(resources.findIdentifier("config_wifiFrameworkSoftApShutDownTimeoutMilliseconds", resources.getInteger(resources.findIdentifier(
"integer", WifiApManager.RESOURCES_PACKAGE, info.packageName)) "config_wifiFrameworkSoftApShutDownTimeoutMilliseconds", "integer",
WifiApManager.RESOURCES_PACKAGE, info.packageName))
} }
} catch (e: RuntimeException) { } catch (e: RuntimeException) {
Timber.w(e) Timber.w(e)
MIN_SOFT_AP_TIMEOUT_DELAY_MS MIN_SOFT_AP_TIMEOUT_DELAY_MS
} else MIN_SOFT_AP_TIMEOUT_DELAY_MS }
return if (Build.VERSION.SDK_INT < 30 && delay < MIN_SOFT_AP_TIMEOUT_DELAY_MS) { return if (Build.VERSION.SDK_INT < 30 && delay < MIN_SOFT_AP_TIMEOUT_DELAY_MS) {
Timber.w("Overriding timeout delay with minimum limit value: $delay < $MIN_SOFT_AP_TIMEOUT_DELAY_MS") Timber.w("Overriding timeout delay with minimum limit value: $delay < $MIN_SOFT_AP_TIMEOUT_DELAY_MS")
MIN_SOFT_AP_TIMEOUT_DELAY_MS MIN_SOFT_AP_TIMEOUT_DELAY_MS
} else delay } else delay
} }
@get:RequiresApi(31)
val defaultTimeoutBridged: Int get() = try {
val info = WifiApManager.resolvedActivity.activityInfo
val resources = app.packageManager.getResourcesForApplication(info.applicationInfo)
resources.getInteger(resources.findIdentifier(
"config_wifiFrameworkSoftApShutDownIdleInstanceInBridgedModeTimeoutMillisecond", "integer",
WifiApManager.RESOURCES_PACKAGE, info.packageName))
} catch (e: RuntimeException) {
Timber.w(e)
MIN_SOFT_AP_TIMEOUT_DELAY_MS
}
} }
private var noClient = true private var noClient = true
@@ -74,7 +85,7 @@ class TetherTimeoutMonitor(private val timeout: Long = 0,
fun onClientsChanged(noClient: Boolean) { fun onClientsChanged(noClient: Boolean) {
this.noClient = noClient this.noClient = noClient
if (!noClient) close() else if (timeoutJob == null) timeoutJob = GlobalScope.launch(context) { if (!noClient) close() else if (timeoutJob == null) timeoutJob = GlobalScope.launch(context) {
delay(if (timeout == 0L) defaultTimeout.toLong() else timeout) delay(if (timeout <= 0L) defaultTimeout.toLong() else timeout)
onTimeout() onTimeout()
} }
} }

View File

@@ -1,9 +1,9 @@
package be.mygod.vpnhotspot.net.monitor package be.mygod.vpnhotspot.net.monitor
import android.net.MacAddress
import androidx.collection.LongSparseArray import androidx.collection.LongSparseArray
import androidx.collection.set import androidx.collection.set
import be.mygod.vpnhotspot.net.IpDev import be.mygod.vpnhotspot.net.IpDev
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.Routing.Companion.IPTABLES import be.mygod.vpnhotspot.net.Routing.Companion.IPTABLES
import be.mygod.vpnhotspot.room.AppDatabase import be.mygod.vpnhotspot.room.AppDatabase
import be.mygod.vpnhotspot.room.TrafficRecord import be.mygod.vpnhotspot.room.TrafficRecord
@@ -11,7 +11,12 @@ import be.mygod.vpnhotspot.util.Event2
import be.mygod.vpnhotspot.util.RootSession import be.mygod.vpnhotspot.util.RootSession
import be.mygod.vpnhotspot.util.parseNumericAddress import be.mygod.vpnhotspot.util.parseNumericAddress
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.* import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.net.InetAddress import java.net.InetAddress
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -23,8 +28,8 @@ object TrafficRecorder {
private val records = mutableMapOf<IpDev, TrafficRecord>() private val records = mutableMapOf<IpDev, TrafficRecord>()
val foregroundListeners = Event2<Collection<TrafficRecord>, LongSparseArray<TrafficRecord>>() val foregroundListeners = Event2<Collection<TrafficRecord>, LongSparseArray<TrafficRecord>>()
fun register(ip: InetAddress, downstream: String, mac: MacAddressCompat) { fun register(ip: InetAddress, downstream: String, mac: MacAddress) {
val record = TrafficRecord(mac = mac.addr, ip = ip, downstream = downstream) val record = TrafficRecord(mac = mac, ip = ip, downstream = downstream)
AppDatabase.instance.trafficRecordDao.insert(record) AppDatabase.instance.trafficRecordDao.insert(record)
synchronized(this) { synchronized(this) {
val key = IpDev(ip, downstream) val key = IpDev(ip, downstream)
@@ -107,9 +112,9 @@ object TrafficRecorder {
record.sentBytes = columns[1].toLong() record.sentBytes = columns[1].toLong()
} }
} }
if (oldRecord.id != null) { oldRecord.id?.let { oldId ->
check(records.put(key, record) == oldRecord) check(records.put(key, record) == oldRecord)
oldRecords[oldRecord.id!!] = oldRecord oldRecords[oldId] = oldRecord
} }
} }
else -> check(false) else -> check(false)
@@ -130,6 +135,7 @@ object TrafficRecorder {
} }
fun update(timeout: Boolean = false) { fun update(timeout: Boolean = false) {
synchronized(this) { synchronized(this) {
unscheduleUpdateLocked()
if (records.isEmpty()) return if (records.isEmpty()) return
val timestamp = System.currentTimeMillis() val timestamp = System.currentTimeMillis()
if (!timeout && timestamp - lastUpdate <= 100) return if (!timeout && timestamp - lastUpdate <= 100) return
@@ -141,7 +147,6 @@ object TrafficRecorder {
SmartSnackbar.make(e).show() SmartSnackbar.make(e).show()
} }
lastUpdate = timestamp lastUpdate = timestamp
updateJob = null
scheduleUpdateLocked() scheduleUpdateLocked()
} }
} }
@@ -156,5 +161,5 @@ object TrafficRecorder {
/** /**
* Possibly inefficient. Don't call this too often. * Possibly inefficient. Don't call this too often.
*/ */
fun isWorking(mac: MacAddressCompat) = records.values.any { it.mac == mac.addr } fun isWorking(mac: MacAddress) = records.values.any { it.mac == mac }
} }

View File

@@ -2,9 +2,6 @@ package be.mygod.vpnhotspot.net.monitor
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.LinkProperties import android.net.LinkProperties
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -23,13 +20,6 @@ abstract class UpstreamMonitor {
} }
private var monitor = generateMonitor() private var monitor = generateMonitor()
fun networkRequestBuilder() = NetworkRequest.Builder().apply {
if (Build.VERSION.SDK_INT == 23) { // workarounds for OEM bugs
removeCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
removeCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL)
}
}
fun registerCallback(callback: Callback) = synchronized(this) { monitor.registerCallback(callback) } fun registerCallback(callback: Callback) = synchronized(this) { monitor.registerCallback(callback) }
fun unregisterCallback(callback: Callback) = synchronized(this) { monitor.unregisterCallback(callback) } fun unregisterCallback(callback: Callback) = synchronized(this) { monitor.unregisterCallback(callback) }
@@ -56,15 +46,6 @@ abstract class UpstreamMonitor {
* Called if some possibly stacked interface is available * Called if some possibly stacked interface is available
*/ */
fun onAvailable(properties: LinkProperties? = null) fun onAvailable(properties: LinkProperties? = null)
/**
* Called on API 23- from DefaultNetworkMonitor. This indicates that there isn't a good way of telling the
* default network (see DefaultNetworkMonitor) and we are using rules at priority 22000
* (RULE_PRIORITY_DEFAULT_NETWORK) as our fallback rules, which would work fine until Android 9.0 broke it in
* commit: https://android.googlesource.com/platform/system/netd/+/758627c4d93392190b08e9aaea3bbbfb92a5f364
*/
fun onFallback() {
throw UnsupportedOperationException()
}
} }
val callbacks = mutableSetOf<Callback>() val callbacks = mutableSetOf<Callback>()

View File

@@ -5,15 +5,16 @@ import android.net.LinkProperties
import android.net.Network import android.net.Network
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import be.mygod.vpnhotspot.util.Services import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.globalNetworkRequestBuilder
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
object VpnMonitor : UpstreamMonitor() { object VpnMonitor : UpstreamMonitor() {
private val request = networkRequestBuilder() private val request = globalNetworkRequestBuilder().apply {
.addTransportType(NetworkCapabilities.TRANSPORT_VPN) addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build() }.build()
private var registered = false private var registered = false
private val available = HashMap<Network, LinkProperties?>() private val available = HashMap<Network, LinkProperties?>()
@@ -60,7 +61,7 @@ object VpnMonitor : UpstreamMonitor() {
callback.onAvailable(currentLinkProperties) callback.onAvailable(currentLinkProperties)
} }
} else { } else {
Services.connectivity.registerNetworkCallback(request, networkCallback) Services.registerNetworkCallback(request, networkCallback)
registered = true registered = true
} }
} }

View File

@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot.net.wifi package be.mygod.vpnhotspot.net.wifi
import android.net.MacAddress
import android.net.wifi.p2p.WifiP2pGroup import android.net.wifi.p2p.WifiP2pGroup
import be.mygod.vpnhotspot.RepeaterService import be.mygod.vpnhotspot.RepeaterService
import be.mygod.vpnhotspot.net.MacAddressCompat import be.mygod.vpnhotspot.net.MacAddressCompat
@@ -53,8 +54,8 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) {
var bssids = listOfNotNull(group?.owner?.deviceAddress, ownerAddress) var bssids = listOfNotNull(group?.owner?.deviceAddress, ownerAddress)
.distinct() .distinct()
.filter { .filter {
val mac = MacAddress.fromString(it)
try { try {
val mac = MacAddressCompat.fromString(it)
mac != MacAddressCompat.ALL_ZEROS_ADDRESS && mac != MacAddressCompat.ANY_ADDRESS mac != MacAddressCompat.ALL_ZEROS_ADDRESS && mac != MacAddressCompat.ANY_ADDRESS
} catch (_: IllegalArgumentException) { } catch (_: IllegalArgumentException) {
false false
@@ -75,7 +76,13 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) {
if (matchedBssid.isEmpty()) { if (matchedBssid.isEmpty()) {
check(block.pskLine == null && block.psk == null) check(block.pskLine == null && block.psk == null)
if (match.groups[5] != null) { if (match.groups[5] != null) {
block.psk = match.groupValues[5].apply { check(length in 8..63) } block.psk = match.groupValues[5].apply {
when (length) {
in 8..63 -> { }
64 -> error("WPA-PSK hex not supported")
else -> error("Unknown length $length")
}
}
} }
block.pskLine = block.size block.pskLine = block.size
} else if (bssids.any { matchedBssid.equals(it, true) }) { } else if (bssids.any { matchedBssid.equals(it, true) }) {
@@ -120,7 +127,7 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) {
add("\tmode=3") add("\tmode=3")
add("\tdisabled=2") add("\tdisabled=2")
add("}") add("}")
if (target == null) target = this target = this
}) })
} }
content = Content(result, target!!, persistentMacLine, legacy) content = Content(result, target!!, persistentMacLine, legacy)
@@ -135,13 +142,12 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) {
} }
val psk by lazy { group?.passphrase ?: content.target.psk!! } val psk by lazy { group?.passphrase ?: content.target.psk!! }
val bssid by lazy { val bssid by lazy {
content.target.bssid?.let { MacAddressCompat.fromString(it) } content.target.bssid?.let { MacAddress.fromString(it) }
} }
suspend fun update(ssid: String, psk: String, bssid: MacAddressCompat?) { suspend fun update(ssid: WifiSsidCompat, psk: String, bssid: MacAddress?) {
val (lines, block, persistentMacLine, legacy) = content val (lines, block, persistentMacLine, legacy) = content
block[block.ssidLine!!] = "\tssid=" + ssid.toByteArray() block[block.ssidLine!!] = "\tssid=${ssid.hex}"
.joinToString("") { (it.toInt() and 255).toString(16).padStart(2, '0') }
block[block.pskLine!!] = "\tpsk=\"$psk\"" // no control chars or weird stuff block[block.pskLine!!] = "\tpsk=\"$psk\"" // no control chars or weird stuff
if (bssid != null) { if (bssid != null) {
persistentMacLine?.let { lines[it] = PERSISTENT_MAC + bssid } persistentMacLine?.let { lines[it] = PERSISTENT_MAC + bssid }

View File

@@ -1,20 +1,27 @@
package be.mygod.vpnhotspot.net.wifi package be.mygod.vpnhotspot.net.wifi
import android.annotation.TargetApi
import android.os.Build
import android.os.Parcelable import android.os.Parcelable
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.util.LongConstantLookup import be.mygod.vpnhotspot.util.LongConstantLookup
import be.mygod.vpnhotspot.util.UnblockCentral
import timber.log.Timber
@JvmInline @JvmInline
@RequiresApi(30) @RequiresApi(30)
value class SoftApCapability(val inner: Parcelable) { value class SoftApCapability(val inner: Parcelable) {
companion object { companion object {
private val clazz by lazy { Class.forName("android.net.wifi.SoftApCapability") } val clazz by lazy { Class.forName("android.net.wifi.SoftApCapability") }
private val getMaxSupportedClients by lazy { clazz.getDeclaredMethod("getMaxSupportedClients") } private val getMaxSupportedClients by lazy { clazz.getDeclaredMethod("getMaxSupportedClients") }
private val areFeaturesSupported by lazy { clazz.getDeclaredMethod("areFeaturesSupported", Long::class.java) } private val areFeaturesSupported by lazy { clazz.getDeclaredMethod("areFeaturesSupported", Long::class.java) }
@get:RequiresApi(31) @get:RequiresApi(31)
private val getSupportedChannelList by lazy { private val getSupportedChannelList by lazy {
clazz.getDeclaredMethod("getSupportedChannelList", Int::class.java) clazz.getDeclaredMethod("getSupportedChannelList", Int::class.java)
} }
@get:RequiresApi(31)
@get:TargetApi(33)
private val getCountryCode by lazy { UnblockCentral.getCountryCode(clazz) }
@RequiresApi(31) @RequiresApi(31)
const val SOFTAP_FEATURE_BAND_24G_SUPPORTED = 32L const val SOFTAP_FEATURE_BAND_24G_SUPPORTED = 32L
@@ -38,4 +45,11 @@ value class SoftApCapability(val inner: Parcelable) {
return supportedFeatures return supportedFeatures
} }
fun getSupportedChannelList(band: Int) = getSupportedChannelList(inner, band) as IntArray fun getSupportedChannelList(band: Int) = getSupportedChannelList(inner, band) as IntArray
@get:RequiresApi(31)
val countryCode: String? get() = try {
getCountryCode(inner) as String?
} catch (e: ReflectiveOperationException) {
if (Build.VERSION.SDK_INT >= 33) Timber.w(e)
null
}
} }

View File

@@ -3,43 +3,39 @@ package be.mygod.vpnhotspot.net.wifi
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.annotation.TargetApi import android.annotation.TargetApi
import android.net.MacAddress import android.net.MacAddress
import android.net.wifi.ScanResult
import android.net.wifi.SoftApConfiguration import android.net.wifi.SoftApConfiguration
import android.net.wifi.WifiSsid
import android.os.Build import android.os.Build
import android.os.Parcelable import android.os.Parcelable
import android.util.SparseIntArray import android.util.SparseIntArray
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.os.BuildCompat
import androidx.core.util.keyIterator
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toCompat
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.requireSingleBand
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.setChannel
import be.mygod.vpnhotspot.net.wifi.WifiSsidCompat.Companion.toCompat
import be.mygod.vpnhotspot.util.ConstantLookup import be.mygod.vpnhotspot.util.ConstantLookup
import be.mygod.vpnhotspot.util.UnblockCentral import be.mygod.vpnhotspot.util.UnblockCentral
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
import java.lang.reflect.InvocationTargetException
@Parcelize @Parcelize
data class SoftApConfigurationCompat( data class SoftApConfigurationCompat(
var ssid: String? = null, var ssid: WifiSsidCompat? = null,
@Deprecated("Workaround for using inline class with Parcelize, use bssid") var bssid: MacAddress? = null,
var bssidAddr: Long? = null,
var passphrase: String? = null, var passphrase: String? = null,
var isHiddenSsid: Boolean = false, var isHiddenSsid: Boolean = false,
/** /**
* 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, * You should probably set or modify this field directly only when you want to use bridged AP,
* see also [android.net.wifi.WifiManager.isBridgedApConcurrencySupported]. * see also [android.net.wifi.WifiManager.isBridgedApConcurrencySupported].
* Otherwise, use [optimizeChannels] or [setChannel]. * Otherwise, use [requireSingleBand] and [setChannel].
*/ */
@TargetApi(23) var channels: SparseIntArray = SparseIntArray(1).apply { append(BAND_2GHZ, 0) },
var channels: SparseIntArray = SparseIntArray(1).apply { put(BAND_2GHZ, 0) },
var securityType: Int = SoftApConfiguration.SECURITY_TYPE_OPEN, var securityType: Int = SoftApConfiguration.SECURITY_TYPE_OPEN,
@TargetApi(30) @TargetApi(30)
var maxNumberOfClients: Int = 0, var maxNumberOfClients: Int = 0,
@TargetApi(28)
var isAutoShutdownEnabled: Boolean = true, var isAutoShutdownEnabled: Boolean = true,
@TargetApi(28)
var shutdownTimeoutMillis: Long = 0, var shutdownTimeoutMillis: Long = 0,
@TargetApi(30) @TargetApi(30)
var isClientControlByUserEnabled: Boolean = false, var isClientControlByUserEnabled: Boolean = false,
@@ -48,14 +44,29 @@ data class SoftApConfigurationCompat(
@RequiresApi(30) @RequiresApi(30)
var allowedClientList: List<MacAddress> = emptyList(), var allowedClientList: List<MacAddress> = emptyList(),
@TargetApi(31) @TargetApi(31)
var macRandomizationSetting: Int = RANDOMIZATION_PERSISTENT, var macRandomizationSetting: Int = if (Build.VERSION.SDK_INT >= 33) {
RANDOMIZATION_NON_PERSISTENT
} else RANDOMIZATION_PERSISTENT,
@TargetApi(31) @TargetApi(31)
var isBridgedModeOpportunisticShutdownEnabled: Boolean = true, var isBridgedModeOpportunisticShutdownEnabled: Boolean = true,
@TargetApi(31) @TargetApi(31)
var isIeee80211axEnabled: Boolean = true, var isIeee80211axEnabled: Boolean = true,
@TargetApi(33)
var isIeee80211beEnabled: Boolean = true,
@TargetApi(31) @TargetApi(31)
var isUserConfiguration: Boolean = true, var isUserConfiguration: Boolean = true,
var underlying: Parcelable? = null) : Parcelable { @TargetApi(33)
var bridgedModeOpportunisticShutdownTimeoutMillis: Long = -1L,
@TargetApi(33)
var vendorElements: List<ScanResult.InformationElement> = emptyList(),
@TargetApi(33)
var persistentRandomizedMacAddress: MacAddress? = null,
@TargetApi(33)
var allowedAcsChannels: Map<Int, Set<Int>> = emptyMap(),
@TargetApi(33)
var maxChannelBandwidth: Int = CHANNEL_WIDTH_AUTO,
var underlying: Parcelable? = null,
) : Parcelable {
companion object { companion object {
const val BAND_2GHZ = 1 const val BAND_2GHZ = 1
const val BAND_5GHZ = 2 const val BAND_5GHZ = 2
@@ -64,20 +75,32 @@ data class SoftApConfigurationCompat(
@TargetApi(31) @TargetApi(31)
const val BAND_60GHZ = 8 const val BAND_60GHZ = 8
const val BAND_LEGACY = BAND_2GHZ or BAND_5GHZ const val BAND_LEGACY = BAND_2GHZ or BAND_5GHZ
@TargetApi(30)
const val BAND_ANY_30 = BAND_LEGACY or BAND_6GHZ
@TargetApi(31)
const val BAND_ANY_31 = BAND_ANY_30 or BAND_60GHZ
val BAND_TYPES by lazy { val BAND_TYPES by lazy {
if (BuildCompat.isAtLeastS()) try { if (Build.VERSION.SDK_INT >= 31) try {
return@lazy UnblockCentral.SoftApConfiguration_BAND_TYPES return@lazy UnblockCentral.SoftApConfiguration_BAND_TYPES
} catch (e: ReflectiveOperationException) { } catch (e: ReflectiveOperationException) {
Timber.w(e) Timber.w(e)
} }
intArrayOf(BAND_2GHZ, BAND_5GHZ, BAND_6GHZ, BAND_60GHZ) intArrayOf(BAND_2GHZ, BAND_5GHZ, BAND_6GHZ, BAND_60GHZ)
} }
val bandLookup = ConstantLookup<SoftApConfiguration>("BAND_", null, "2GHZ", "5GHZ") @RequiresApi(31)
val bandLookup = ConstantLookup<SoftApConfiguration>("BAND_")
@TargetApi(31) @TargetApi(31)
const val RANDOMIZATION_NONE = 0 const val RANDOMIZATION_NONE = 0
@TargetApi(31) @TargetApi(31)
const val RANDOMIZATION_PERSISTENT = 1 const val RANDOMIZATION_PERSISTENT = 1
@TargetApi(33)
const val RANDOMIZATION_NON_PERSISTENT = 2
@TargetApi(33)
const val CHANNEL_WIDTH_AUTO = -1
@TargetApi(30)
const val CHANNEL_WIDTH_INVALID = 0
fun isLegacyEitherBand(band: Int) = band and BAND_LEGACY == BAND_LEGACY fun isLegacyEitherBand(band: Int) = band and BAND_LEGACY == BAND_LEGACY
@@ -86,15 +109,19 @@ data class SoftApConfigurationCompat(
*/ */
private const val LEGACY_WPA2_PSK = 4 private const val LEGACY_WPA2_PSK = 4
val securityTypes = arrayOf("OPEN", "WPA2-PSK", "WPA3-SAE", "WPA3-SAE Transition mode") val securityTypes = arrayOf(
"OPEN",
private val qrSanitizer = Regex("([\\\\\":;,])") "WPA2-PSK",
"WPA3-SAE Transition mode",
"WPA3-SAE",
"WPA3-OWE Transition",
"WPA3-OWE",
)
/** /**
* Based on: * Based on:
* https://elixir.bootlin.com/linux/v5.12.8/source/net/wireless/util.c#L75 * https://elixir.bootlin.com/linux/v5.12.8/source/net/wireless/util.c#L75
* TODO: update for Android 12 * https://cs.android.com/android/platform/superproject/+/master:packages/modules/Wifi/framework/java/android/net/wifi/ScanResult.java;l=789;drc=71d758698c45984d3f8de981bf98e56902480f16
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/wifi/java/android/net/wifi/ScanResult.java;l=624;drc=f7ccda05642b55700d67a288462bada488fc7f5e
*/ */
fun channelToFrequency(band: Int, chan: Int) = when (band) { fun channelToFrequency(band: Int, chan: Int) = when (band) {
BAND_2GHZ -> when (chan) { BAND_2GHZ -> when (chan) {
@@ -109,7 +136,7 @@ data class SoftApConfigurationCompat(
} }
BAND_6GHZ -> when (chan) { BAND_6GHZ -> when (chan) {
2 -> 5935 2 -> 5935
in 1..233 -> 5950 + chan * 5 in 1..253 -> 5950 + chan * 5
else -> throw IllegalArgumentException("Invalid 6GHz channel $chan") else -> throw IllegalArgumentException("Invalid 6GHz channel $chan")
} }
BAND_60GHZ -> { BAND_60GHZ -> {
@@ -134,7 +161,6 @@ data class SoftApConfigurationCompat(
* *
* https://android.googlesource.com/platform/frameworks/base/+/android-6.0.0_r1/wifi/java/android/net/wifi/WifiConfiguration.java#242 * https://android.googlesource.com/platform/frameworks/base/+/android-6.0.0_r1/wifi/java/android/net/wifi/WifiConfiguration.java#242
*/ */
@get:RequiresApi(23)
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
/** /**
* The band which AP resides on * The band which AP resides on
@@ -142,7 +168,6 @@ data class SoftApConfigurationCompat(
* By default, 2G is chosen * By default, 2G is chosen
*/ */
private val apBand by lazy { android.net.wifi.WifiConfiguration::class.java.getDeclaredField("apBand") } private val apBand by lazy { android.net.wifi.WifiConfiguration::class.java.getDeclaredField("apBand") }
@get:RequiresApi(23)
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
/** /**
* The channel which AP resides on * The channel which AP resides on
@@ -154,6 +179,10 @@ data class SoftApConfigurationCompat(
android.net.wifi.WifiConfiguration::class.java.getDeclaredField("apChannel") android.net.wifi.WifiConfiguration::class.java.getDeclaredField("apChannel")
} }
@get:RequiresApi(33)
private val getAllowedAcsChannels by lazy @TargetApi(33) {
SoftApConfiguration::class.java.getDeclaredMethod("getAllowedAcsChannels", Int::class.java)
}
@get:RequiresApi(30) @get:RequiresApi(30)
private val getAllowedClientList by lazy @TargetApi(30) { private val getAllowedClientList by lazy @TargetApi(30) {
SoftApConfiguration::class.java.getDeclaredMethod("getAllowedClientList") SoftApConfiguration::class.java.getDeclaredMethod("getAllowedClientList")
@@ -164,6 +193,10 @@ data class SoftApConfigurationCompat(
private val getBlockedClientList by lazy @TargetApi(30) { private val getBlockedClientList by lazy @TargetApi(30) {
SoftApConfiguration::class.java.getDeclaredMethod("getBlockedClientList") SoftApConfiguration::class.java.getDeclaredMethod("getBlockedClientList")
} }
@get:RequiresApi(33)
private val getBridgedModeOpportunisticShutdownTimeoutMillis by lazy @TargetApi(33) {
SoftApConfiguration::class.java.getDeclaredMethod("getBridgedModeOpportunisticShutdownTimeoutMillis")
}
@get:RequiresApi(30) @get:RequiresApi(30)
private val getChannel by lazy @TargetApi(30) { private val getChannel by lazy @TargetApi(30) {
SoftApConfiguration::class.java.getDeclaredMethod("getChannel") SoftApConfiguration::class.java.getDeclaredMethod("getChannel")
@@ -176,14 +209,26 @@ data class SoftApConfigurationCompat(
private val getMacRandomizationSetting by lazy @TargetApi(31) { private val getMacRandomizationSetting by lazy @TargetApi(31) {
SoftApConfiguration::class.java.getDeclaredMethod("getMacRandomizationSetting") SoftApConfiguration::class.java.getDeclaredMethod("getMacRandomizationSetting")
} }
@get:RequiresApi(33)
private val getMaxChannelBandwidth by lazy @TargetApi(33) {
SoftApConfiguration::class.java.getDeclaredMethod("getMaxChannelBandwidth")
}
@get:RequiresApi(30) @get:RequiresApi(30)
private val getMaxNumberOfClients by lazy @TargetApi(30) { private val getMaxNumberOfClients by lazy @TargetApi(30) {
SoftApConfiguration::class.java.getDeclaredMethod("getMaxNumberOfClients") SoftApConfiguration::class.java.getDeclaredMethod("getMaxNumberOfClients")
} }
@get:RequiresApi(33)
private val getPersistentRandomizedMacAddress by lazy @TargetApi(33) {
SoftApConfiguration::class.java.getDeclaredMethod("getPersistentRandomizedMacAddress")
}
@get:RequiresApi(30) @get:RequiresApi(30)
private val getShutdownTimeoutMillis by lazy @TargetApi(30) { private val getShutdownTimeoutMillis by lazy @TargetApi(30) {
SoftApConfiguration::class.java.getDeclaredMethod("getShutdownTimeoutMillis") SoftApConfiguration::class.java.getDeclaredMethod("getShutdownTimeoutMillis")
} }
@get:RequiresApi(33)
private val getVendorElements by lazy @TargetApi(33) {
SoftApConfiguration::class.java.getDeclaredMethod("getVendorElements")
}
@get:RequiresApi(30) @get:RequiresApi(30)
private val isAutoShutdownEnabled by lazy @TargetApi(30) { private val isAutoShutdownEnabled by lazy @TargetApi(30) {
SoftApConfiguration::class.java.getDeclaredMethod("isAutoShutdownEnabled") SoftApConfiguration::class.java.getDeclaredMethod("isAutoShutdownEnabled")
@@ -200,6 +245,10 @@ data class SoftApConfigurationCompat(
private val isIeee80211axEnabled by lazy @TargetApi(31) { private val isIeee80211axEnabled by lazy @TargetApi(31) {
SoftApConfiguration::class.java.getDeclaredMethod("isIeee80211axEnabled") SoftApConfiguration::class.java.getDeclaredMethod("isIeee80211axEnabled")
} }
@get:RequiresApi(33)
private val isIeee80211beEnabled by lazy @TargetApi(33) {
SoftApConfiguration::class.java.getDeclaredMethod("isIeee80211beEnabled")
}
@get:RequiresApi(31) @get:RequiresApi(31)
private val isUserConfiguration by lazy @TargetApi(31) { private val isUserConfiguration by lazy @TargetApi(31) {
SoftApConfiguration::class.java.getDeclaredMethod("isUserConfiguration") SoftApConfiguration::class.java.getDeclaredMethod("isUserConfiguration")
@@ -210,78 +259,106 @@ data class SoftApConfigurationCompat(
@get:RequiresApi(30) @get:RequiresApi(30)
private val newBuilder by lazy @TargetApi(30) { classBuilder.getConstructor(SoftApConfiguration::class.java) } private val newBuilder by lazy @TargetApi(30) { classBuilder.getConstructor(SoftApConfiguration::class.java) }
@get:RequiresApi(30) @get:RequiresApi(30)
private val build by lazy { classBuilder.getDeclaredMethod("build") } private val build by lazy @TargetApi(30) { classBuilder.getDeclaredMethod("build") }
@get:RequiresApi(33)
private val setAllowedAcsChannels by lazy @TargetApi(33) {
classBuilder.getDeclaredMethod("setAllowedAcsChannels", Int::class.java, IntArray::class.java)
}
@get:RequiresApi(30) @get:RequiresApi(30)
private val setAllowedClientList by lazy { private val setAllowedClientList by lazy @TargetApi(30) {
classBuilder.getDeclaredMethod("setAllowedClientList", java.util.List::class.java) classBuilder.getDeclaredMethod("setAllowedClientList", java.util.List::class.java)
} }
@get:RequiresApi(30) @get:RequiresApi(30)
private val setAutoShutdownEnabled by lazy { private val setAutoShutdownEnabled by lazy @TargetApi(30) {
classBuilder.getDeclaredMethod("setAutoShutdownEnabled", Boolean::class.java) classBuilder.getDeclaredMethod("setAutoShutdownEnabled", Boolean::class.java)
} }
@get:RequiresApi(30) @get:RequiresApi(30)
private val setBand by lazy { classBuilder.getDeclaredMethod("setBand", Int::class.java) } private val setBand by lazy @TargetApi(30) { classBuilder.getDeclaredMethod("setBand", Int::class.java) }
@get:RequiresApi(30) @get:RequiresApi(30)
private val setBlockedClientList by lazy { private val setBlockedClientList by lazy @TargetApi(30) {
classBuilder.getDeclaredMethod("setBlockedClientList", java.util.List::class.java) classBuilder.getDeclaredMethod("setBlockedClientList", java.util.List::class.java)
} }
@get:RequiresApi(31) @get:RequiresApi(31)
private val setBridgedModeOpportunisticShutdownEnabled by lazy { private val setBridgedModeOpportunisticShutdownEnabled by lazy @TargetApi(31) {
classBuilder.getDeclaredMethod("setBridgedModeOpportunisticShutdownEnabled", Boolean::class.java) classBuilder.getDeclaredMethod("setBridgedModeOpportunisticShutdownEnabled", Boolean::class.java)
} }
@get:RequiresApi(33)
private val setBridgedModeOpportunisticShutdownTimeoutMillis by lazy @TargetApi(33) {
classBuilder.getDeclaredMethod("setBridgedModeOpportunisticShutdownTimeoutMillis", Long::class.java)
}
@get:RequiresApi(30) @get:RequiresApi(30)
private val setBssid by lazy @TargetApi(30) { private val setBssid by lazy @TargetApi(30) {
classBuilder.getDeclaredMethod("setBssid", MacAddress::class.java) classBuilder.getDeclaredMethod("setBssid", MacAddress::class.java)
} }
@get:RequiresApi(30) @get:RequiresApi(30)
private val setChannel by lazy { private val setChannel by lazy @TargetApi(30) {
classBuilder.getDeclaredMethod("setChannel", Int::class.java, Int::class.java) classBuilder.getDeclaredMethod("setChannel", Int::class.java, Int::class.java)
} }
@get:RequiresApi(31) @get:RequiresApi(31)
private val setChannels by lazy { private val setChannels by lazy @TargetApi(31) {
classBuilder.getDeclaredMethod("setChannels", SparseIntArray::class.java) classBuilder.getDeclaredMethod("setChannels", SparseIntArray::class.java)
} }
@get:RequiresApi(30) @get:RequiresApi(30)
private val setClientControlByUserEnabled by lazy { private val setClientControlByUserEnabled by lazy @TargetApi(30) {
classBuilder.getDeclaredMethod("setClientControlByUserEnabled", Boolean::class.java) classBuilder.getDeclaredMethod("setClientControlByUserEnabled", Boolean::class.java)
} }
@get:RequiresApi(30) @get:RequiresApi(30)
private val setHiddenSsid by lazy { classBuilder.getDeclaredMethod("setHiddenSsid", Boolean::class.java) } private val setHiddenSsid by lazy @TargetApi(30) {
classBuilder.getDeclaredMethod("setHiddenSsid", Boolean::class.java)
}
@get:RequiresApi(31) @get:RequiresApi(31)
private val setIeee80211axEnabled by lazy { private val setIeee80211axEnabled by lazy @TargetApi(31) {
classBuilder.getDeclaredMethod("setIeee80211axEnabled", Boolean::class.java) classBuilder.getDeclaredMethod("setIeee80211axEnabled", Boolean::class.java)
} }
@get:RequiresApi(33)
private val setIeee80211beEnabled by lazy @TargetApi(33) {
classBuilder.getDeclaredMethod("setIeee80211beEnabled", Boolean::class.java)
}
@get:RequiresApi(31) @get:RequiresApi(31)
private val setMacRandomizationSetting by lazy { private val setMacRandomizationSetting by lazy @TargetApi(31) {
classBuilder.getDeclaredMethod("setMacRandomizationSetting", Int::class.java) classBuilder.getDeclaredMethod("setMacRandomizationSetting", Int::class.java)
} }
@get:RequiresApi(33)
private val setMaxChannelBandwidth by lazy @TargetApi(33) {
classBuilder.getDeclaredMethod("setMaxChannelBandwidth", Int::class.java)
}
@get:RequiresApi(30) @get:RequiresApi(30)
private val setMaxNumberOfClients by lazy { private val setMaxNumberOfClients by lazy @TargetApi(31) {
classBuilder.getDeclaredMethod("setMaxNumberOfClients", Int::class.java) classBuilder.getDeclaredMethod("setMaxNumberOfClients", Int::class.java)
} }
@get:RequiresApi(30) @get:RequiresApi(30)
private val setPassphrase by lazy { private val setPassphrase by lazy @TargetApi(30) {
classBuilder.getDeclaredMethod("setPassphrase", String::class.java, Int::class.java) classBuilder.getDeclaredMethod("setPassphrase", String::class.java, Int::class.java)
} }
@get:RequiresApi(33)
private val setRandomizedMacAddress by lazy @TargetApi(33) {
UnblockCentral.setRandomizedMacAddress(classBuilder)
}
@get:RequiresApi(30) @get:RequiresApi(30)
private val setShutdownTimeoutMillis by lazy { private val setShutdownTimeoutMillis by lazy @TargetApi(30) {
classBuilder.getDeclaredMethod("setShutdownTimeoutMillis", Long::class.java) classBuilder.getDeclaredMethod("setShutdownTimeoutMillis", Long::class.java)
} }
@get:RequiresApi(30) @get:RequiresApi(30)
private val setSsid by lazy { classBuilder.getDeclaredMethod("setSsid", String::class.java) } private val setSsid by lazy @TargetApi(30) { classBuilder.getDeclaredMethod("setSsid", String::class.java) }
@get:RequiresApi(31) @get:RequiresApi(33)
private val setUserConfiguration by lazy @TargetApi(31) { UnblockCentral.setUserConfiguration(classBuilder) } private val setVendorElements by lazy @TargetApi(33) {
classBuilder.getDeclaredMethod("setVendorElements", java.util.List::class.java)
}
@get:RequiresApi(33)
private val setWifiSsid by lazy @TargetApi(33) {
classBuilder.getDeclaredMethod("setWifiSsid", WifiSsid::class.java)
}
@Deprecated("Class deprecated in framework") @Deprecated("Class deprecated in framework")
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
fun android.net.wifi.WifiConfiguration.toCompat() = SoftApConfigurationCompat( fun android.net.wifi.WifiConfiguration.toCompat() = SoftApConfigurationCompat(
SSID, WifiSsidCompat.fromUtf8Text(SSID),
BSSID?.let { MacAddressCompat.fromString(it) }?.addr, BSSID?.let { MacAddress.fromString(it) },
preSharedKey, preSharedKey,
hiddenSSID, hiddenSSID,
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/wifi/java/android/net/wifi/SoftApConfToXmlMigrationUtil.java;l=87;drc=aa6527cf41671d1ed417b8ebdb6b3aa614f62344 // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/wifi/java/android/net/wifi/SoftApConfToXmlMigrationUtil.java;l=87;drc=aa6527cf41671d1ed417b8ebdb6b3aa614f62344
SparseIntArray(1).apply { SparseIntArray(1).also {
if (Build.VERSION.SDK_INT < 23) put(BAND_LEGACY, 0) else put(when (val band = apBand.getInt(this)) { it.append(when (val band = apBand.getInt(this)) {
0 -> BAND_2GHZ 0 -> BAND_2GHZ
1 -> BAND_5GHZ 1 -> BAND_5GHZ
-1 -> BAND_LEGACY -1 -> BAND_LEGACY
@@ -302,24 +379,27 @@ data class SoftApConfigurationCompat(
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
} }
android.net.wifi.WifiConfiguration.KeyMgmt.SAE -> SoftApConfiguration.SECURITY_TYPE_WPA3_SAE android.net.wifi.WifiConfiguration.KeyMgmt.SAE -> SoftApConfiguration.SECURITY_TYPE_WPA3_SAE
android.net.wifi.WifiConfiguration.KeyMgmt.OWE -> SoftApConfiguration.SECURITY_TYPE_WPA3_OWE
else -> android.net.wifi.WifiConfiguration.KeyMgmt.strings else -> android.net.wifi.WifiConfiguration.KeyMgmt.strings
.getOrElse<String>(selected) { "?" }.let { .getOrElse<String>(selected) { "?" }.let {
throw IllegalArgumentException("Unrecognized key management $it ($selected)") throw IllegalArgumentException("Unrecognized key management $it ($selected)")
} }
} }
}, },
isAutoShutdownEnabled = if (Build.VERSION.SDK_INT >= 28) TetherTimeoutMonitor.enabled else false, isAutoShutdownEnabled = TetherTimeoutMonitor.enabled,
underlying = this) underlying = this)
@RequiresApi(30) @RequiresApi(30)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun SoftApConfiguration.toCompat() = SoftApConfigurationCompat( fun SoftApConfiguration.toCompat() = SoftApConfigurationCompat(
ssid, if (Build.VERSION.SDK_INT >= 33) wifiSsid?.toCompat() else @Suppress("DEPRECATION") {
bssid?.toCompat()?.addr, WifiSsidCompat.fromUtf8Text(ssid)
},
bssid,
passphrase, passphrase,
isHiddenSsid, isHiddenSsid,
if (BuildCompat.isAtLeastS()) getChannels(this) as SparseIntArray else SparseIntArray(1).apply { if (Build.VERSION.SDK_INT >= 31) getChannels(this) as SparseIntArray else SparseIntArray(1).also {
put(getBand(this) as Int, getChannel(this) as Int) it.append(getBand(this) as Int, getChannel(this) as Int)
}, },
securityType, securityType,
getMaxNumberOfClients(this) as Int, getMaxNumberOfClients(this) as Int,
@@ -328,49 +408,71 @@ data class SoftApConfigurationCompat(
isClientControlByUserEnabled(this) as Boolean, isClientControlByUserEnabled(this) as Boolean,
getBlockedClientList(this) as List<MacAddress>, getBlockedClientList(this) as List<MacAddress>,
getAllowedClientList(this) as List<MacAddress>, getAllowedClientList(this) as List<MacAddress>,
getMacRandomizationSetting(this) as Int, underlying = this,
isBridgedModeOpportunisticShutdownEnabled(this) as Boolean, ).also {
isIeee80211axEnabled(this) as Boolean, if (Build.VERSION.SDK_INT < 31) return@also
isUserConfiguration(this) as Boolean, it.macRandomizationSetting = getMacRandomizationSetting(this) as Int
this) 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<ScanResult.InformationElement>
it.persistentRandomizedMacAddress = getPersistentRandomizedMacAddress(this) as MacAddress?
it.allowedAcsChannels = BAND_TYPES.map { bandType ->
try {
bandType to (getAllowedAcsChannels(this, bandType) as IntArray).toSet()
} catch (e: InvocationTargetException) {
if (e.targetException !is IllegalArgumentException) throw e
null
} }
}.filterNotNull().toMap()
@Suppress("DEPRECATION") it.maxChannelBandwidth = getMaxChannelBandwidth(this) as Int
inline var bssid: MacAddressCompat?
get() = bssidAddr?.let { MacAddressCompat(it) }
set(value) {
bssidAddr = value?.addr
} }
/** /**
* Only single band/channel can be supplied on API 23-30 * Only single band/channel can be supplied on API 23-30
*/ */
fun requireSingleBand(): Pair<Int, Int> { fun requireSingleBand(channels: SparseIntArray): Pair<Int, Int> {
require(channels.size() == 1) { "Unsupported number of bands configured" } require(channels.size() == 1) { "Unsupported number of bands configured" }
return channels.keyAt(0) to channels.valueAt(0) return channels.keyAt(0) to channels.valueAt(0)
} }
fun getChannel(band: Int): Int {
var result = -1 @RequiresApi(30)
for (b in channels.keyIterator()) if (band and b == band) { private fun setChannelsCompat(builder: Any, channels: SparseIntArray) = if (Build.VERSION.SDK_INT < 31) {
require(result == -1) { "Duplicate band found" } val (band, channel) = requireSingleBand(channels)
result = channels[b] if (channel == 0) setBand(builder, band) else setChannel(builder, channel, band)
} } else setChannels(builder, channels)
return result @get:RequiresApi(30)
} private val staticBuilder by lazy @TargetApi(30) { classBuilder.newInstance() }
fun setChannel(channel: Int, band: Int = BAND_LEGACY) { @RequiresApi(30)
channels = SparseIntArray(1).apply { put(band, channel) } fun testPlatformValidity(channels: SparseIntArray) = setChannelsCompat(staticBuilder, channels)
} @RequiresApi(30)
fun optimizeChannels(channels: SparseIntArray = this.channels) { fun testPlatformValidity(bssid: MacAddress) = setBssid(staticBuilder, bssid)
this.channels = SparseIntArray(channels.size()).apply { @RequiresApi(33)
var setBand = 0 fun testPlatformValidity(vendorElements: List<ScanResult.InformationElement>) =
for (band in channels.keyIterator()) if (channels[band] == 0) setBand = setBand or band setVendorElements(staticBuilder, vendorElements)
if (setBand != 0) put(setBand, 0) // merge all bands into one @RequiresApi(33)
for (band in channels.keyIterator()) if (band and setBand == 0) put(band, channels[band]) 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 setMacRandomizationEnabled(enabled: Boolean) { fun setChannel(channel: Int, band: Int = BAND_LEGACY) {
macRandomizationSetting = if (enabled) RANDOMIZATION_PERSISTENT else RANDOMIZATION_NONE channels = SparseIntArray(1).apply {
append(when {
channel <= 0 || band != BAND_LEGACY -> band
channel > 14 -> BAND_5GHZ
else -> BAND_2GHZ
}, channel)
}
} }
/** /**
@@ -383,25 +485,22 @@ data class SoftApConfigurationCompat(
@Deprecated("Class deprecated in framework, use toPlatform().toWifiConfiguration()") @Deprecated("Class deprecated in framework, use toPlatform().toWifiConfiguration()")
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
fun toWifiConfiguration(): android.net.wifi.WifiConfiguration { fun toWifiConfiguration(): android.net.wifi.WifiConfiguration {
val (band, channel) = requireSingleBand() val (band, channel) = requireSingleBand(channels)
val wc = underlying as? android.net.wifi.WifiConfiguration val wc = underlying as? android.net.wifi.WifiConfiguration
val result = if (wc == null) android.net.wifi.WifiConfiguration() else android.net.wifi.WifiConfiguration(wc) val result = if (wc == null) android.net.wifi.WifiConfiguration() else android.net.wifi.WifiConfiguration(wc)
val original = wc?.toCompat() val original = wc?.toCompat()
result.SSID = ssid result.SSID = ssid?.toString()
result.preSharedKey = passphrase result.preSharedKey = passphrase
result.hiddenSSID = isHiddenSsid result.hiddenSSID = isHiddenSsid
if (Build.VERSION.SDK_INT >= 23) {
apBand.setInt(result, when (band) { apBand.setInt(result, when (band) {
BAND_2GHZ -> 0 BAND_2GHZ -> 0
BAND_5GHZ -> 1 BAND_5GHZ -> 1
else -> { else -> {
require(Build.VERSION.SDK_INT >= 28) { "A band must be specified on this platform" }
require(isLegacyEitherBand(band)) { "Convert fail, unsupported band setting :$band" } require(isLegacyEitherBand(band)) { "Convert fail, unsupported band setting :$band" }
-1 -1
} }
}) })
apChannel.setInt(result, channel) apChannel.setInt(result, channel)
} else require(isLegacyEitherBand(band)) { "Specifying band is unsupported on this platform" }
if (original?.securityType != securityType) { if (original?.securityType != securityType) {
result.allowedKeyManagement.clear() result.allowedKeyManagement.clear()
result.allowedKeyManagement.set(when (securityType) { result.allowedKeyManagement.set(when (securityType) {
@@ -411,6 +510,8 @@ data class SoftApConfigurationCompat(
// CHANGED: not actually converted in framework-wifi // CHANGED: not actually converted in framework-wifi
SoftApConfiguration.SECURITY_TYPE_WPA3_SAE, SoftApConfiguration.SECURITY_TYPE_WPA3_SAE,
SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION -> android.net.wifi.WifiConfiguration.KeyMgmt.SAE SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION -> android.net.wifi.WifiConfiguration.KeyMgmt.SAE
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE,
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE_TRANSITION -> android.net.wifi.WifiConfiguration.KeyMgmt.OWE
else -> throw IllegalArgumentException("Convert fail, unsupported security type :$securityType") else -> throw IllegalArgumentException("Convert fail, unsupported security type :$securityType")
}) })
result.allowedAuthAlgorithms.clear() result.allowedAuthAlgorithms.clear()
@@ -425,29 +526,59 @@ data class SoftApConfigurationCompat(
fun toPlatform(): SoftApConfiguration { fun toPlatform(): SoftApConfiguration {
val sac = underlying as? SoftApConfiguration val sac = underlying as? SoftApConfiguration
val builder = if (sac == null) classBuilder.newInstance() else newBuilder.newInstance(sac) val builder = if (sac == null) classBuilder.newInstance() else newBuilder.newInstance(sac)
setSsid(builder, ssid) if (Build.VERSION.SDK_INT >= 33) {
setPassphrase(builder, if (securityType == SoftApConfiguration.SECURITY_TYPE_OPEN) null else passphrase, setWifiSsid(builder, ssid?.toPlatform())
securityType) } else setSsid(builder, ssid?.toString())
if (BuildCompat.isAtLeastS()) setChannels(builder, channels) else { setPassphrase(builder, when (securityType) {
val (band, channel) = requireSingleBand() SoftApConfiguration.SECURITY_TYPE_OPEN,
if (channel == 0) setBand(builder, band) else setChannel(builder, channel, band) SoftApConfiguration.SECURITY_TYPE_WPA3_OWE_TRANSITION,
} SoftApConfiguration.SECURITY_TYPE_WPA3_OWE -> null
setBssid(builder, bssid?.toPlatform()) else -> passphrase
}, securityType)
setChannelsCompat(builder, channels)
setBssid(builder,
if (Build.VERSION.SDK_INT < 31 || macRandomizationSetting == RANDOMIZATION_NONE) bssid else null)
setMaxNumberOfClients(builder, maxNumberOfClients) setMaxNumberOfClients(builder, maxNumberOfClients)
try {
setShutdownTimeoutMillis(builder, shutdownTimeoutMillis) setShutdownTimeoutMillis(builder, shutdownTimeoutMillis)
} catch (e: InvocationTargetException) {
if (e.targetException is IllegalArgumentException) try {
setShutdownTimeoutMillis(builder, -1 - shutdownTimeoutMillis)
} catch (e2: InvocationTargetException) {
e2.addSuppressed(e)
throw e2
} else throw e
}
setAutoShutdownEnabled(builder, isAutoShutdownEnabled) setAutoShutdownEnabled(builder, isAutoShutdownEnabled)
setClientControlByUserEnabled(builder, isClientControlByUserEnabled) setClientControlByUserEnabled(builder, isClientControlByUserEnabled)
setHiddenSsid(builder, isHiddenSsid) setHiddenSsid(builder, isHiddenSsid)
setAllowedClientList(builder, allowedClientList) setAllowedClientList(builder, allowedClientList)
setBlockedClientList(builder, blockedClientList) setBlockedClientList(builder, blockedClientList)
if (BuildCompat.isAtLeastS()) { if (Build.VERSION.SDK_INT >= 31) {
setMacRandomizationSetting(builder, macRandomizationSetting) setMacRandomizationSetting(builder, macRandomizationSetting)
setBridgedModeOpportunisticShutdownEnabled(builder, isBridgedModeOpportunisticShutdownEnabled) setBridgedModeOpportunisticShutdownEnabled(builder, isBridgedModeOpportunisticShutdownEnabled)
setIeee80211axEnabled(builder, isIeee80211axEnabled) setIeee80211axEnabled(builder, isIeee80211axEnabled)
if (sac?.let { isUserConfiguration(it) as Boolean } != false != isUserConfiguration) try { if (Build.VERSION.SDK_INT >= 33) {
setUserConfiguration(builder, isUserConfiguration) setIeee80211beEnabled(builder, isIeee80211beEnabled)
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) { } catch (e: ReflectiveOperationException) {
Timber.w(e) // as far as we are concerned, this field is not used anywhere so ignore for now Timber.w(e)
}
for (bandType in BAND_TYPES) {
val value = allowedAcsChannels[bandType] ?: emptySet()
try {
setAllowedAcsChannels(builder, bandType, value.toIntArray())
} catch (e: InvocationTargetException) {
if (value.isNotEmpty()) throw e
}
}
setMaxChannelBandwidth(builder, maxChannelBandwidth)
} }
} }
return build(builder) as SoftApConfiguration return build(builder) as SoftApConfiguration
@@ -458,21 +589,21 @@ data class SoftApConfigurationCompat(
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/4a5ff58/src/com/android/settings/wifi/dpp/WifiNetworkConfig.java#161 * Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/4a5ff58/src/com/android/settings/wifi/dpp/WifiNetworkConfig.java#161
*/ */
fun toQrCode() = StringBuilder("WIFI:").apply { fun toQrCode() = StringBuilder("WIFI:").apply {
fun String.sanitize() = qrSanitizer.replace(this) { "\\${it.groupValues[1]}" }
when (securityType) { when (securityType) {
SoftApConfiguration.SECURITY_TYPE_OPEN -> { } SoftApConfiguration.SECURITY_TYPE_OPEN, SoftApConfiguration.SECURITY_TYPE_WPA3_OWE_TRANSITION,
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK -> append("T:WPA;") SoftApConfiguration.SECURITY_TYPE_WPA3_OWE -> { }
SoftApConfiguration.SECURITY_TYPE_WPA3_SAE, SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION -> { SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION -> {
append("T:SAE;") append("T:WPA;")
} }
SoftApConfiguration.SECURITY_TYPE_WPA3_SAE -> append("T:SAE;")
else -> throw IllegalArgumentException("Unsupported authentication type") else -> throw IllegalArgumentException("Unsupported authentication type")
} }
append("S:") append("S:")
append(ssid!!.sanitize()) append(ssid!!.toMeCard())
append(';') append(';')
passphrase?.let { passphrase -> passphrase?.let { passphrase ->
append("P:") append("P:")
append(passphrase.sanitize()) append(WifiSsidCompat.toMeCard(passphrase))
append(';') append(';')
} }
if (isHiddenSsid) append("H:true;") if (isHiddenSsid) append("H:true;")

View File

@@ -12,7 +12,7 @@ import timber.log.Timber
@RequiresApi(30) @RequiresApi(30)
value class SoftApInfo(val inner: Parcelable) { value class SoftApInfo(val inner: Parcelable) {
companion object { companion object {
private val clazz by lazy { Class.forName("android.net.wifi.SoftApInfo") } val clazz by lazy { Class.forName("android.net.wifi.SoftApInfo") }
private val getFrequency by lazy { clazz.getDeclaredMethod("getFrequency") } private val getFrequency by lazy { clazz.getDeclaredMethod("getFrequency") }
private val getBandwidth by lazy { clazz.getDeclaredMethod("getBandwidth") } private val getBandwidth by lazy { clazz.getDeclaredMethod("getBandwidth") }
@get:RequiresApi(31) @get:RequiresApi(31)
@@ -30,7 +30,7 @@ value class SoftApInfo(val inner: Parcelable) {
val frequency get() = getFrequency(inner) as Int val frequency get() = getFrequency(inner) as Int
val bandwidth get() = getBandwidth(inner) as Int val bandwidth get() = getBandwidth(inner) as Int
@get:RequiresApi(31) @get:RequiresApi(31)
val bssid get() = getBssid(inner) as MacAddress val bssid get() = getBssid(inner) as MacAddress?
@get:RequiresApi(31) @get:RequiresApi(31)
val wifiStandard get() = getWifiStandard(inner) as Int val wifiStandard get() = getWifiStandard(inner) as Int
@get:RequiresApi(31) @get:RequiresApi(31)

View File

@@ -0,0 +1,27 @@
package be.mygod.vpnhotspot.net.wifi
import android.net.wifi.ScanResult
import androidx.annotation.RequiresApi
import timber.log.Timber
@RequiresApi(33)
object VendorElements {
fun serialize(input: List<ScanResult.InformationElement>) = input.joinToString("\n") { element ->
element.bytes.let { buffer ->
StringBuilder().apply {
while (buffer.hasRemaining()) append("%02x".format(buffer.get()))
}.toString()
}.also {
if (element.id != 221 || element.idExt != 0 || it.isEmpty()) Timber.w(Exception(
"Unexpected InformationElement ${element.id}, ${element.idExt}, $it"))
}
}
fun deserialize(input: CharSequence?) = (input ?: "").split("\n").map { line ->
if (line.isBlank()) return@map null
require(line.length % 2 == 0) { "Input should be hex: $line" }
(0 until line.length / 2).map {
Integer.parseInt(line.substring(it * 2, it * 2 + 2), 16).toByte()
}.toByteArray()
}.filterNotNull().map { ScanResult.InformationElement(221, 0, it) }
}

View File

@@ -1,13 +1,15 @@
package be.mygod.vpnhotspot.net.wifi package be.mygod.vpnhotspot.net.wifi
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.content.ClipData import android.content.ClipData
import android.content.ClipDescription
import android.content.DialogInterface import android.content.DialogInterface
import android.net.MacAddress
import android.net.wifi.SoftApConfiguration import android.net.wifi.SoftApConfiguration
import android.os.Build import android.os.Build
import android.os.Parcelable import android.os.Parcelable
import android.text.Editable import android.text.Editable
import android.text.InputFilter
import android.text.TextWatcher import android.text.TextWatcher
import android.util.Base64 import android.util.Base64
import android.util.SparseIntArray import android.util.SparseIntArray
@@ -16,10 +18,11 @@ import android.view.View
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.Spinner import android.widget.Spinner
import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.os.BuildCompat import androidx.core.os.persistableBundleOf
import androidx.core.view.isGone import androidx.core.view.isGone
import be.mygod.librootkotlinx.toByteArray import be.mygod.librootkotlinx.toByteArray
import be.mygod.librootkotlinx.toParcelable import be.mygod.librootkotlinx.toParcelable
@@ -28,14 +31,16 @@ import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.RepeaterService import be.mygod.vpnhotspot.RepeaterService
import be.mygod.vpnhotspot.databinding.DialogWifiApBinding import be.mygod.vpnhotspot.databinding.DialogWifiApBinding
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
import be.mygod.vpnhotspot.util.QRCodeDialog import be.mygod.vpnhotspot.util.QRCodeDialog
import be.mygod.vpnhotspot.util.RangeInput
import be.mygod.vpnhotspot.util.readableMessage import be.mygod.vpnhotspot.util.readableMessage
import be.mygod.vpnhotspot.util.showAllowingStateLoss import be.mygod.vpnhotspot.util.showAllowingStateLoss
import be.mygod.vpnhotspot.widget.SmartSnackbar import com.google.android.material.textfield.TextInputLayout
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
/** /**
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/39b4674/src/com/android/settings/wifi/WifiApDialog.java * Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/39b4674/src/com/android/settings/wifi/WifiApDialog.java
@@ -48,26 +53,34 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
companion object { companion object {
private const val BASE64_FLAGS = Base64.NO_PADDING or Base64.NO_WRAP private const val BASE64_FLAGS = Base64.NO_PADDING or Base64.NO_WRAP
private val nonMacChars = "[^0-9a-fA-F:]+".toRegex() private val nonMacChars = "[^0-9a-fA-F:]+".toRegex()
private val baseOptions by lazy { listOf(ChannelOption.Disabled, ChannelOption.Auto) } private val channels2G = (1..14).map { ChannelOption(SoftApConfigurationCompat.BAND_2GHZ, it) }
private val channels2G by lazy {
baseOptions + (1..14).map { ChannelOption(it, SoftApConfigurationCompat.BAND_2GHZ) }
}
private val channels5G by lazy { private val channels5G by lazy {
baseOptions + (1..196).map { ChannelOption(it, SoftApConfigurationCompat.BAND_5GHZ) } channels2G + (1..196).map { ChannelOption(SoftApConfigurationCompat.BAND_5GHZ, it) }
}
@get:RequiresApi(30)
private val channels6G by lazy {
baseOptions + (1..233).map { ChannelOption(it, SoftApConfigurationCompat.BAND_6GHZ) }
}
@get:RequiresApi(31)
private val channels60G by lazy {
baseOptions + (1..6).map { ChannelOption(it, SoftApConfigurationCompat.BAND_60GHZ) }
} }
private fun genAutoOptions(band: Int) = (1..band).filter { it and band == it }.map { ChannelOption(it) }
/** /**
* Source: https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/c2fc6a1/service/java/com/android/server/wifi/p2p/SupplicantP2pIfaceHal.java#1396 * Source: https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/c2fc6a1/service/java/com/android/server/wifi/p2p/SupplicantP2pIfaceHal.java#1396
*/ */
private val p2pChannels by lazy { private val p2pUnsafeOptions by lazy {
baseOptions + (15..165).map { ChannelOption(it, SoftApConfigurationCompat.BAND_5GHZ) } listOf(ChannelOption(SoftApConfigurationCompat.BAND_LEGACY)) +
channels2G + (15..165).map { ChannelOption(SoftApConfigurationCompat.BAND_5GHZ, it) }
}
private val p2pSafeOptions by lazy { genAutoOptions(SoftApConfigurationCompat.BAND_LEGACY) + channels5G }
private val softApOptions by lazy {
if (Build.VERSION.SDK_INT >= 30) {
genAutoOptions(SoftApConfigurationCompat.BAND_ANY_31) +
channels5G +
(1..253).map { ChannelOption(SoftApConfigurationCompat.BAND_6GHZ, it) } +
(1..6).map { ChannelOption(SoftApConfigurationCompat.BAND_60GHZ, it) }
} else p2pSafeOptions
}
@get:RequiresApi(30)
private val bandWidthOptions by lazy {
SoftApInfo.channelWidthLookup.lookup.let { lookup ->
Array(lookup.size()) { BandWidth(lookup.keyAt(it), lookup.valueAt(it).substring(14)) }.apply { sort() }
}
} }
} }
@@ -80,62 +93,99 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
*/ */
val p2pMode: Boolean = false) : Parcelable val p2pMode: Boolean = false) : Parcelable
private open class ChannelOption(val channel: Int = 0, private val band: Int = 0) { private open class ChannelOption(val band: Int = 0, val channel: Int = 0) {
object Disabled : ChannelOption(-1) { object Disabled : ChannelOption(-1) {
override fun toString() = app.getString(R.string.wifi_ap_choose_disabled) override fun toString() = app.getString(R.string.wifi_ap_choose_disabled)
} }
object Auto : ChannelOption() { override fun toString() = if (channel == 0) {
override fun toString() = app.getString(R.string.wifi_ap_choose_auto) val format = DecimalFormat("#.#", DecimalFormatSymbols.getInstance(app.resources.configuration.locales[0]))
app.getString(R.string.wifi_ap_choose_G, arrayOf(
SoftApConfigurationCompat.BAND_2GHZ to 2.4,
SoftApConfigurationCompat.BAND_5GHZ to 5,
SoftApConfigurationCompat.BAND_6GHZ to 6,
SoftApConfigurationCompat.BAND_60GHZ to 60,
).filter { (mask, _) -> band and mask == mask }.joinToString("/") { (_, name) -> format.format(name) })
} else "${SoftApConfigurationCompat.channelToFrequency(band, channel)} MHz ($channel)"
} }
override fun toString() = "${SoftApConfigurationCompat.channelToFrequency(band, channel)} MHz ($channel)"
private class BandWidth(val width: Int, val name: String = "") : Comparable<BandWidth> {
override fun compareTo(other: BandWidth) = width - other.width
override fun toString() = name
} }
private lateinit var dialogView: DialogWifiApBinding private lateinit var dialogView: DialogWifiApBinding
private lateinit var base: SoftApConfigurationCompat private lateinit var base: SoftApConfigurationCompat
private var pasted = false
private var started = false private var started = false
private val currentChannels5G get() = if (arg.p2pMode && !RepeaterService.safeMode) p2pChannels else channels5G private val currentChannels get() = when {
!arg.p2pMode -> softApOptions
RepeaterService.safeMode -> p2pSafeOptions
else -> p2pUnsafeOptions
}
private val acsList by lazy {
listOf(
Triple(SoftApConfigurationCompat.BAND_2GHZ, dialogView.acs2g, dialogView.acs2gWrapper),
Triple(SoftApConfigurationCompat.BAND_5GHZ, dialogView.acs5g, dialogView.acs5gWrapper),
Triple(SoftApConfigurationCompat.BAND_6GHZ, dialogView.acs6g, dialogView.acs6gWrapper),
)
}
override val ret get() = Arg(generateConfig()) override val ret get() = Arg(generateConfig())
private val hexToggleable get() = if (arg.p2pMode) !RepeaterService.safeMode else Build.VERSION.SDK_INT >= 33
private var hexSsid = false
set(value) {
field = value
dialogView.ssidWrapper.setEndIconActivated(value)
}
private val ssid get() =
if (hexSsid) WifiSsidCompat.fromHex(dialogView.ssid.text) else WifiSsidCompat.fromUtf8Text(dialogView.ssid.text)
private fun generateChannels() = SparseIntArray(2).apply {
if (!arg.p2pMode && Build.VERSION.SDK_INT >= 31) {
(dialogView.bandSecondary.selectedItem as ChannelOption?)?.apply { if (band >= 0) put(band, channel) }
}
(dialogView.bandPrimary.selectedItem as ChannelOption).apply { put(band, channel) }
}
private fun generateConfig(full: Boolean = true) = base.copy( private fun generateConfig(full: Boolean = true) = base.copy(
ssid = dialogView.ssid.text.toString(), ssid = ssid,
passphrase = if (dialogView.password.length() != 0) dialogView.password.text.toString() else null).apply { passphrase = if (dialogView.password.length() != 0) dialogView.password.text.toString() else null).apply {
if (!arg.p2pMode) { if (!arg.p2pMode) {
securityType = dialogView.security.selectedItemPosition securityType = dialogView.security.selectedItemPosition
isHiddenSsid = dialogView.hiddenSsid.isChecked isHiddenSsid = dialogView.hiddenSsid.isChecked
} }
if (full) @TargetApi(28) { if (full) {
isAutoShutdownEnabled = dialogView.autoShutdown.isChecked isAutoShutdownEnabled = dialogView.autoShutdown.isChecked
shutdownTimeoutMillis = dialogView.timeout.text.let { text -> shutdownTimeoutMillis = dialogView.timeout.text.let { text ->
if (text.isNullOrEmpty()) 0 else text.toString().toLong() if (text.isNullOrEmpty()) 0 else text.toString().toLong()
} }
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) { channels = generateChannels()
val channels = SparseIntArray(4)
for ((band, spinner) in arrayOf(SoftApConfigurationCompat.BAND_2GHZ to dialogView.band2G,
SoftApConfigurationCompat.BAND_5GHZ to dialogView.band5G,
SoftApConfigurationCompat.BAND_6GHZ to dialogView.band6G,
SoftApConfigurationCompat.BAND_60GHZ to dialogView.band60G)) {
val channel = (spinner.selectedItem as ChannelOption?)?.channel
if (channel != null && channel >= 0) channels.put(band, channel)
}
if (!arg.p2pMode && BuildCompat.isAtLeastS() && dialogView.bridgedMode.isChecked) {
this.channels = channels
} else optimizeChannels(channels)
}
bssid = if (dialogView.bssid.length() != 0) {
MacAddressCompat.fromString(dialogView.bssid.text.toString())
} else null
maxNumberOfClients = dialogView.maxClient.text.let { text -> maxNumberOfClients = dialogView.maxClient.text.let { text ->
if (text.isNullOrEmpty()) 0 else text.toString().toInt() if (text.isNullOrEmpty()) 0 else text.toString().toInt()
} }
isClientControlByUserEnabled = dialogView.clientUserControl.isChecked isClientControlByUserEnabled = dialogView.clientUserControl.isChecked
allowedClientList = (dialogView.allowedList.text ?: "").split(nonMacChars) allowedClientList = (dialogView.allowedList.text ?: "").split(nonMacChars)
.filter { it.isNotEmpty() }.map { MacAddressCompat.fromString(it).toPlatform() } .filter { it.isNotEmpty() }.map(MacAddress::fromString)
blockedClientList = (dialogView.blockedList.text ?: "").split(nonMacChars) blockedClientList = (dialogView.blockedList.text ?: "").split(nonMacChars)
.filter { it.isNotEmpty() }.map { MacAddressCompat.fromString(it).toPlatform() } .filter { it.isNotEmpty() }.map(MacAddress::fromString)
setMacRandomizationEnabled(dialogView.macRandomization.isChecked) macRandomizationSetting = dialogView.macRandomization.selectedItemPosition
bssid = if ((arg.p2pMode || Build.VERSION.SDK_INT < 31 && macRandomizationSetting ==
SoftApConfigurationCompat.RANDOMIZATION_NONE) && dialogView.bssid.length() != 0) {
MacAddress.fromString(dialogView.bssid.text.toString())
} else null
isBridgedModeOpportunisticShutdownEnabled = dialogView.bridgedModeOpportunisticShutdown.isChecked isBridgedModeOpportunisticShutdownEnabled = dialogView.bridgedModeOpportunisticShutdown.isChecked
isIeee80211axEnabled = dialogView.ieee80211ax.isChecked isIeee80211axEnabled = dialogView.ieee80211ax.isChecked
isIeee80211beEnabled = dialogView.ieee80211be.isChecked
isUserConfiguration = dialogView.userConfig.isChecked isUserConfiguration = dialogView.userConfig.isChecked
bridgedModeOpportunisticShutdownTimeoutMillis = dialogView.bridgedTimeout.text.let { text ->
if (text.isNullOrEmpty()) -1L else text.toString().toLong()
}
vendorElements = VendorElements.deserialize(dialogView.vendorElements.text)
persistentRandomizedMacAddress = if (dialogView.persistentRandomizedMac.length() != 0) {
MacAddress.fromString(dialogView.persistentRandomizedMac.text.toString())
} else null
allowedAcsChannels = acsList.associate { (band, text, _) -> band to RangeInput.fromString(text.text) }
if (!arg.p2pMode && Build.VERSION.SDK_INT >= 33) {
maxChannelBandwidth = (dialogView.maxChannelBandwidth.selectedItem as BandWidth).width
}
} }
} }
@@ -148,6 +198,31 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
setNegativeButton(R.string.donations__button_close, null) setNegativeButton(R.string.donations__button_close, null)
dialogView.toolbar.inflateMenu(R.menu.toolbar_configuration) dialogView.toolbar.inflateMenu(R.menu.toolbar_configuration)
dialogView.toolbar.setOnMenuItemClickListener(this@WifiApDialogFragment) dialogView.toolbar.setOnMenuItemClickListener(this@WifiApDialogFragment)
dialogView.ssidWrapper.setLengthCounter {
try {
ssid?.bytes?.size ?: 0
} catch (_: IllegalArgumentException) {
0
}
}
if (hexToggleable) dialogView.ssidWrapper.apply {
endIconMode = TextInputLayout.END_ICON_CUSTOM
setEndIconOnClickListener {
val ssid = try {
ssid
} catch (_: IllegalArgumentException) {
return@setEndIconOnClickListener
}
val newText = if (hexSsid) ssid?.run {
decode().also { if (it == null) return@setEndIconOnClickListener }
} else ssid?.hex
hexSsid = !hexSsid
dialogView.ssid.setText(newText)
}
findViewById<View>(com.google.android.material.R.id.text_input_end_icon).apply {
tooltipText = contentDescription
}
}
if (!arg.readOnly) dialogView.ssid.addTextChangedListener(this@WifiApDialogFragment) if (!arg.readOnly) dialogView.ssid.addTextChangedListener(this@WifiApDialogFragment)
if (arg.p2pMode) dialogView.securityWrapper.isGone = true else dialogView.security.apply { if (arg.p2pMode) dialogView.securityWrapper.isGone = true else dialogView.security.apply {
adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0, adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0,
@@ -157,99 +232,115 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
onItemSelectedListener = object : AdapterView.OnItemSelectedListener { onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) = error("Must select something") override fun onNothingSelected(parent: AdapterView<*>?) = error("Must select something")
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
dialogView.passwordWrapper.isGone = position == SoftApConfiguration.SECURITY_TYPE_OPEN when (position) {
SoftApConfiguration.SECURITY_TYPE_OPEN,
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE_TRANSITION,
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE -> dialogView.passwordWrapper.isGone = true
else -> {
dialogView.passwordWrapper.isGone = false
if (position == SoftApConfiguration.SECURITY_TYPE_WPA3_SAE) {
dialogView.passwordWrapper.isCounterEnabled = false
dialogView.passwordWrapper.counterMaxLength = 0
dialogView.password.filters = emptyArray()
} else {
dialogView.passwordWrapper.isCounterEnabled = true
dialogView.passwordWrapper.counterMaxLength = 63
dialogView.password.filters = arrayOf(InputFilter.LengthFilter(63))
}
}
}
validate()
} }
} }
} }
if (!arg.readOnly) dialogView.password.addTextChangedListener(this@WifiApDialogFragment) if (!arg.readOnly) dialogView.password.addTextChangedListener(this@WifiApDialogFragment)
if (!arg.p2pMode && Build.VERSION.SDK_INT < 28) dialogView.autoShutdown.isGone = true
if (arg.p2pMode || Build.VERSION.SDK_INT >= 30) { if (arg.p2pMode || Build.VERSION.SDK_INT >= 30) {
dialogView.timeoutWrapper.helperText = getString(R.string.wifi_hotspot_timeout_default, dialogView.timeoutWrapper.helperText = getString(R.string.wifi_hotspot_timeout_default,
TetherTimeoutMonitor.defaultTimeout) TetherTimeoutMonitor.defaultTimeout)
dialogView.timeout.addTextChangedListener(this@WifiApDialogFragment) if (!arg.readOnly) dialogView.timeout.addTextChangedListener(this@WifiApDialogFragment)
} else dialogView.timeoutWrapper.isGone = true } else dialogView.timeoutWrapper.isGone = true
fun Spinner.configure(options: List<ChannelOption>) { fun Spinner.configure(options: List<ChannelOption>) {
adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0, options).apply { adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0, options).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
} }
onItemSelectedListener = this@WifiApDialogFragment if (!arg.readOnly) onItemSelectedListener = this@WifiApDialogFragment
} }
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) { dialogView.bandPrimary.configure(currentChannels)
dialogView.band2G.configure(channels2G) if (Build.VERSION.SDK_INT >= 31 && !arg.p2pMode) {
dialogView.band5G.configure(currentChannels5G) dialogView.bandSecondary.configure(listOf(ChannelOption.Disabled) + currentChannels)
} else { } else dialogView.bandSecondary.isGone = true
dialogView.bandWrapper2G.isGone = true if (arg.p2pMode || Build.VERSION.SDK_INT < 30) dialogView.accessControlGroup.isGone = true
dialogView.bandWrapper5G.isGone = true else if (!arg.readOnly) {
}
if (Build.VERSION.SDK_INT >= 30 && !arg.p2pMode) dialogView.band6G.configure(channels6G)
else dialogView.bandWrapper6G.isGone = true
if (BuildCompat.isAtLeastS() && !arg.p2pMode) dialogView.band60G.configure(channels60G)
else dialogView.bandWrapper60G.isGone = true
dialogView.bssid.addTextChangedListener(this@WifiApDialogFragment)
if (arg.p2pMode) dialogView.hiddenSsid.isGone = true
if (arg.p2pMode || Build.VERSION.SDK_INT < 30) {
dialogView.maxClientWrapper.isGone = true
dialogView.clientUserControl.isGone = true
dialogView.blockedListWrapper.isGone = true
dialogView.allowedListWrapper.isGone = true
} else {
dialogView.maxClient.addTextChangedListener(this@WifiApDialogFragment) dialogView.maxClient.addTextChangedListener(this@WifiApDialogFragment)
dialogView.blockedList.addTextChangedListener(this@WifiApDialogFragment) dialogView.blockedList.addTextChangedListener(this@WifiApDialogFragment)
dialogView.allowedList.addTextChangedListener(this@WifiApDialogFragment) dialogView.allowedList.addTextChangedListener(this@WifiApDialogFragment)
} }
if (!arg.readOnly) dialogView.bssid.addTextChangedListener(this@WifiApDialogFragment)
if (arg.p2pMode) dialogView.hiddenSsid.isGone = true
if (arg.p2pMode && Build.VERSION.SDK_INT >= 29) dialogView.macRandomization.isEnabled = false if (arg.p2pMode && Build.VERSION.SDK_INT >= 29) dialogView.macRandomization.isEnabled = false
else if (arg.p2pMode || !BuildCompat.isAtLeastS()) dialogView.macRandomization.isGone = true else if (arg.p2pMode || Build.VERSION.SDK_INT < 31) dialogView.macRandomizationWrapper.isGone = true
if (arg.p2pMode || !BuildCompat.isAtLeastS()) { else dialogView.macRandomization.onItemSelectedListener = this@WifiApDialogFragment
dialogView.bridgedMode.isGone = true if (arg.p2pMode || Build.VERSION.SDK_INT < 31) {
dialogView.bridgedModeOpportunisticShutdown.isGone = true
dialogView.ieee80211ax.isGone = true dialogView.ieee80211ax.isGone = true
dialogView.bridgedModeOpportunisticShutdown.isGone = true
dialogView.userConfig.isGone = true dialogView.userConfig.isGone = true
dialogView.bridgedTimeoutWrapper.isGone = true
} else {
dialogView.bridgedTimeoutWrapper.helperText = getString(R.string.wifi_hotspot_timeout_default,
TetherTimeoutMonitor.defaultTimeoutBridged)
}
if (Build.VERSION.SDK_INT < 33) dialogView.vendorElementsWrapper.isGone = true
else if (!arg.readOnly) dialogView.vendorElements.addTextChangedListener(this@WifiApDialogFragment)
if (arg.p2pMode || Build.VERSION.SDK_INT < 33) {
dialogView.ieee80211be.isGone = true
dialogView.bridgedTimeout.isEnabled = false
dialogView.persistentRandomizedMacWrapper.isGone = true
for ((_, _, wrapper) in acsList) wrapper.isGone = true
dialogView.maxChannelBandwidthWrapper.isGone = true
} else {
dialogView.maxChannelBandwidth.adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0,
bandWidthOptions).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
if (!arg.readOnly) {
dialogView.bridgedTimeout.addTextChangedListener(this@WifiApDialogFragment)
dialogView.persistentRandomizedMac.addTextChangedListener(this@WifiApDialogFragment)
for ((_, text, _) in acsList) text.addTextChangedListener(this@WifiApDialogFragment)
dialogView.acs5g.addTextChangedListener(this@WifiApDialogFragment)
dialogView.acs6g.addTextChangedListener(this@WifiApDialogFragment)
dialogView.maxChannelBandwidth.onItemSelectedListener = this@WifiApDialogFragment
}
} }
base = arg.configuration base = arg.configuration
populateFromConfiguration() populateFromConfiguration()
} }
private fun locate(band: Int, channels: List<ChannelOption>): Int { private fun locate(i: Int): Int {
val channel = base.getChannel(band) val band = base.channels.keyAt(i)
val selection = channels.indexOfFirst { it.channel == channel } val channel = base.channels.valueAt(i)
val selection = currentChannels.indexOfFirst { it.band == band && it.channel == channel }
return if (selection == -1) { return if (selection == -1) {
Timber.w(Exception("Unable to locate $band, $channel, ${arg.p2pMode && !RepeaterService.safeMode}")) val msg = "Unable to locate $band, $channel, ${arg.p2pMode && !RepeaterService.safeMode}"
if (pasted || arg.p2pMode) Timber.w(msg) else Timber.w(Exception(msg))
0 0
} else selection } else selection
} }
private var userBridgedMode = false
private fun setBridgedMode() {
var auto = 0
var set = 0
for (s in arrayOf(dialogView.band2G, dialogView.band5G, dialogView.band6G)) when (s.selectedItem) {
is ChannelOption.Auto -> auto = 1
!is ChannelOption.Disabled -> ++set
}
if (auto + set > 1) {
if (dialogView.bridgedMode.isEnabled) {
userBridgedMode = dialogView.bridgedMode.isChecked
dialogView.bridgedMode.isEnabled = false
dialogView.bridgedMode.isChecked = true
}
} else if (!dialogView.bridgedMode.isEnabled) {
dialogView.bridgedMode.isEnabled = true
dialogView.bridgedMode.isChecked = userBridgedMode
}
}
private fun populateFromConfiguration() { private fun populateFromConfiguration() {
dialogView.ssid.setText(base.ssid) dialogView.ssid.setText(base.ssid.let { ssid ->
when {
ssid == null -> null
hexSsid -> ssid.hex
hexToggleable -> ssid.decode() ?: ssid.hex.also { hexSsid = true }
else -> ssid.toString()
}
})
if (!arg.p2pMode) dialogView.security.setSelection(base.securityType) if (!arg.p2pMode) dialogView.security.setSelection(base.securityType)
dialogView.password.setText(base.passphrase) dialogView.password.setText(base.passphrase)
dialogView.autoShutdown.isChecked = base.isAutoShutdownEnabled dialogView.autoShutdown.isChecked = base.isAutoShutdownEnabled
dialogView.timeout.setText(base.shutdownTimeoutMillis.let { if (it == 0L) "" else it.toString() }) dialogView.timeout.setText(base.shutdownTimeoutMillis.let { if (it <= 0) "" else it.toString() })
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) { dialogView.bandPrimary.setSelection(locate(0))
dialogView.band2G.setSelection(locate(SoftApConfigurationCompat.BAND_2GHZ, channels2G)) if (Build.VERSION.SDK_INT >= 31 && !arg.p2pMode) {
dialogView.band5G.setSelection(locate(SoftApConfigurationCompat.BAND_5GHZ, currentChannels5G)) dialogView.bandSecondary.setSelection(if (base.channels.size() > 1) locate(1) + 1 else 0)
dialogView.band6G.setSelection(locate(SoftApConfigurationCompat.BAND_6GHZ, channels6G))
dialogView.band60G.setSelection(locate(SoftApConfigurationCompat.BAND_60GHZ, channels60G))
userBridgedMode = base.channels.size() > 1
dialogView.bridgedMode.isChecked = userBridgedMode
setBridgedMode()
} }
dialogView.bssid.setText(base.bssid?.toString()) dialogView.bssid.setText(base.bssid?.toString())
dialogView.hiddenSsid.isChecked = base.isHiddenSsid dialogView.hiddenSsid.isChecked = base.isHiddenSsid
@@ -257,11 +348,22 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
dialogView.clientUserControl.isChecked = base.isClientControlByUserEnabled dialogView.clientUserControl.isChecked = base.isClientControlByUserEnabled
dialogView.blockedList.setText(base.blockedClientList.joinToString("\n")) dialogView.blockedList.setText(base.blockedClientList.joinToString("\n"))
dialogView.allowedList.setText(base.allowedClientList.joinToString("\n")) dialogView.allowedList.setText(base.allowedClientList.joinToString("\n"))
dialogView.macRandomization.isChecked = dialogView.macRandomization.setSelection(base.macRandomizationSetting)
base.macRandomizationSetting == SoftApConfigurationCompat.RANDOMIZATION_PERSISTENT
dialogView.bridgedModeOpportunisticShutdown.isChecked = base.isBridgedModeOpportunisticShutdownEnabled dialogView.bridgedModeOpportunisticShutdown.isChecked = base.isBridgedModeOpportunisticShutdownEnabled
dialogView.ieee80211ax.isChecked = base.isIeee80211axEnabled dialogView.ieee80211ax.isChecked = base.isIeee80211axEnabled
dialogView.ieee80211be.isChecked = base.isIeee80211beEnabled
dialogView.userConfig.isChecked = base.isUserConfiguration dialogView.userConfig.isChecked = base.isUserConfiguration
dialogView.bridgedTimeout.setText(base.bridgedModeOpportunisticShutdownTimeoutMillis.let {
if (it == -1L) "" else it.toString()
})
dialogView.vendorElements.setText(VendorElements.serialize(base.vendorElements))
dialogView.persistentRandomizedMac.setText(base.persistentRandomizedMacAddress?.toString())
for ((band, text, _) in acsList) text.setText(RangeInput.toString(base.allowedAcsChannels[band]))
if (Build.VERSION.SDK_INT >= 33) bandWidthOptions.binarySearch(BandWidth(base.maxChannelBandwidth)).let {
if (it < 0) {
Timber.w(Exception("Cannot locate bandwidth ${base.maxChannelBandwidth}"))
} else dialogView.maxChannelBandwidth.setSelection(it)
}
} }
override fun onStart() { override fun onStart() {
@@ -270,64 +372,62 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
validate() validate()
} }
@TargetApi(28)
private fun validate() { private fun validate() {
if (!started) return if (!started) return
val ssidLength = dialogView.ssid.text.toString().toByteArray().size val (ssidOk, ssidError) = 0.let {
dialogView.ssidWrapper.error = if (arg.p2pMode && RepeaterService.safeMode && ssidLength < 9) { val ssid = try {
ssid
} catch (e: IllegalArgumentException) {
return@let false to e.readableMessage
}
val ssidLength = ssid?.bytes?.size ?: 0
if (ssidLength in 1..32) true to if (arg.p2pMode && RepeaterService.safeMode && ssidLength < 9) {
requireContext().getString(R.string.settings_service_repeater_safe_mode_warning) requireContext().getString(R.string.settings_service_repeater_safe_mode_warning)
} else null } else null else false to " "
}
dialogView.ssidWrapper.error = ssidError
val selectedSecurity = if (arg.p2pMode) { val selectedSecurity = if (arg.p2pMode) {
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
} else dialogView.security.selectedItemPosition } else dialogView.security.selectedItemPosition
// see also: https://android.googlesource.com/platform/frameworks/base/+/92c8f59/wifi/java/android/net/wifi/SoftApConfiguration.java#688 // see also: https://android.googlesource.com/platform/frameworks/base/+/92c8f59/wifi/java/android/net/wifi/SoftApConfiguration.java#688
val passwordValid = when (selectedSecurity) { val passwordValid = when (selectedSecurity) {
SoftApConfiguration.SECURITY_TYPE_OPEN,
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE_TRANSITION,
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE -> true
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION -> { SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION -> {
dialogView.password.length() >= 8 dialogView.password.length() in 8..63
} }
else -> true // do not try to validate else -> dialogView.password.length() > 0
} }
dialogView.passwordWrapper.error = if (passwordValid) null else " " dialogView.passwordWrapper.error = if (passwordValid) null else " "
val timeoutError = dialogView.timeout.text.let { text -> val timeoutError = dialogView.timeout.text.let { text ->
if (text.isNullOrEmpty()) null else try { if (text.isNullOrEmpty()) null else try {
text.toString().toLong() SoftApConfigurationCompat.testPlatformTimeoutValidity(text.toString().toLong())
null null
} catch (e: NumberFormatException) { } catch (e: Exception) {
e.readableMessage e.readableMessage
} }
} }
dialogView.timeoutWrapper.error = timeoutError dialogView.timeoutWrapper.error = timeoutError
val isBandValid = when { val bandError = if (!arg.p2pMode && Build.VERSION.SDK_INT >= 30) {
arg.p2pMode || Build.VERSION.SDK_INT in 23 until 30 -> { try {
val option5G = dialogView.band5G.selectedItem SoftApConfigurationCompat.testPlatformValidity(generateChannels())
when (dialogView.band2G.selectedItem) { null
is ChannelOption.Disabled -> option5G !is ChannelOption.Disabled && } catch (e: Exception) {
(!arg.p2pMode || RepeaterService.safeMode || option5G !is ChannelOption.Auto) e.readableMessage
is ChannelOption.Auto ->
(arg.p2pMode || Build.VERSION.SDK_INT >= 28) && option5G is ChannelOption.Auto ||
(!arg.p2pMode || RepeaterService.safeMode) && option5G is ChannelOption.Disabled
else -> option5G is ChannelOption.Disabled
}
}
Build.VERSION.SDK_INT == 30 && !BuildCompat.isAtLeastS() -> {
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 dialogView.bssidWrapper.error = null
val bssidValid = dialogView.bssid.length() == 0 || try { val bssidValid = hideBssid || dialogView.bssid.length() == 0 || try {
MacAddressCompat.fromString(dialogView.bssid.text.toString()) val mac = MacAddress.fromString(dialogView.bssid.text.toString())
if (Build.VERSION.SDK_INT >= 30 && !arg.p2pMode) SoftApConfigurationCompat.testPlatformValidity(mac)
true true
} catch (e: IllegalArgumentException) { } catch (e: Exception) {
dialogView.bssidWrapper.error = e.readableMessage dialogView.bssidWrapper.error = e.readableMessage
false false
} }
@@ -340,26 +440,80 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
} }
} }
dialogView.maxClientWrapper.error = maxClientError dialogView.maxClientWrapper.error = maxClientError
val blockedListError = try { val listsNoError = if (Build.VERSION.SDK_INT >= 30) {
(dialogView.blockedList.text ?: "").split(nonMacChars) val (blockedList, blockedListError) = try {
.filter { it.isNotEmpty() }.forEach { MacAddressCompat.fromString(it).toPlatform() } (dialogView.blockedList.text ?: "").split(nonMacChars).filter { it.isNotEmpty() }
null .map(MacAddress::fromString).toSet() to null
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
e.readableMessage null to e.readableMessage
} }
dialogView.blockedListWrapper.error = blockedListError dialogView.blockedListWrapper.error = blockedListError
val allowedListError = try { val allowedListError = try {
(dialogView.allowedList.text ?: "").split(nonMacChars) (dialogView.allowedList.text ?: "").split(nonMacChars).filter { it.isNotEmpty() }.forEach {
.filter { it.isNotEmpty() }.forEach { MacAddressCompat.fromString(it).toPlatform() } val mac = MacAddress.fromString(it)
require(blockedList?.contains(mac) != true) { "A MAC address exists in both client lists" }
}
null null
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
e.readableMessage e.readableMessage
} }
dialogView.allowedListWrapper.error = allowedListError dialogView.allowedListWrapper.error = allowedListError
val canCopy = timeoutError == null && bssidValid && maxClientError == null && blockedListError == null && blockedListError == null && allowedListError == 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.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) {
dialogView.persistentRandomizedMacWrapper.error = e.readableMessage
false
}
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 = (dialog as? AlertDialog)?.getButton(DialogInterface.BUTTON_POSITIVE)?.isEnabled =
ssidLength in 1..32 && passwordValid && isBandValid && canCopy ssidOk && passwordValid && bandError == null && canCopy
dialogView.toolbar.menu.findItem(android.R.id.copy).isEnabled = canCopy dialogView.toolbar.menu.findItem(android.R.id.copy).isEnabled = canCopy
} }
@@ -372,10 +526,15 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
override fun onMenuItemClick(item: MenuItem?): Boolean { override fun onMenuItemClick(item: MenuItem?): Boolean {
return when (item?.itemId) { return when (item?.itemId) {
android.R.id.copy -> { android.R.id.copy -> try {
app.clipboard.setPrimaryClip(ClipData.newPlainText(null, app.clipboard.setPrimaryClip(ClipData.newPlainText(null,
Base64.encodeToString(generateConfig().toByteArray(), BASE64_FLAGS))) Base64.encodeToString(generateConfig().toByteArray(), BASE64_FLAGS)).apply {
description.extras = persistableBundleOf(ClipDescription.EXTRA_IS_SENSITIVE to true)
})
true true
} catch (e: RuntimeException) {
Toast.makeText(context, e.readableMessage, Toast.LENGTH_LONG).show()
false
} }
android.R.id.paste -> try { android.R.id.paste -> try {
app.clipboard.primaryClip?.getItemAt(0)?.text?.apply { app.clipboard.primaryClip?.getItemAt(0)?.text?.apply {
@@ -385,12 +544,13 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
arg.configuration.underlying?.let { check(it.javaClass == newUnderlying.javaClass) } arg.configuration.underlying?.let { check(it.javaClass == newUnderlying.javaClass) }
} else config.underlying = arg.configuration.underlying } else config.underlying = arg.configuration.underlying
base = config base = config
pasted = true
populateFromConfiguration() populateFromConfiguration()
} }
} }
true true
} catch (e: IllegalArgumentException) { } catch (e: RuntimeException) {
SmartSnackbar.make(e).show() Toast.makeText(context, e.readableMessage, Toast.LENGTH_LONG).show()
false false
} }
R.id.share_qr -> { R.id.share_qr -> {

View File

@@ -10,13 +10,8 @@ import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Parcelable import android.os.Parcelable
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.os.BuildCompat
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat import be.mygod.vpnhotspot.util.*
import be.mygod.vpnhotspot.util.ConstantLookup
import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.callSuper
import be.mygod.vpnhotspot.util.findIdentifier
import timber.log.Timber import timber.log.Timber
import java.lang.reflect.InvocationHandler import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method import java.lang.reflect.Method
@@ -39,7 +34,8 @@ object WifiApManager {
PackageManager.MATCH_SYSTEM_ONLY).single() PackageManager.MATCH_SYSTEM_ONLY).single()
private const val CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED = "config_wifi_p2p_mac_randomization_supported" private const val CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED = "config_wifi_p2p_mac_randomization_supported"
val p2pMacRandomizationSupported get() = when (Build.VERSION.SDK_INT) { val p2pMacRandomizationSupported get() = try {
when (Build.VERSION.SDK_INT) {
29 -> Resources.getSystem().run { 29 -> Resources.getSystem().run {
getBoolean(getIdentifier(CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED, "bool", "android")) getBoolean(getIdentifier(CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED, "bool", "android"))
} }
@@ -51,6 +47,10 @@ object WifiApManager {
} }
else -> false else -> false
} }
} catch (e: RuntimeException) {
Timber.w(e)
false
}
@get:RequiresApi(30) @get:RequiresApi(30)
private val apMacRandomizationSupported by lazy { private val apMacRandomizationSupported by lazy {
@@ -59,6 +59,92 @@ object WifiApManager {
@get:RequiresApi(30) @get:RequiresApi(30)
val isApMacRandomizationSupported get() = apMacRandomizationSupported(Services.wifi) as Boolean val isApMacRandomizationSupported get() = apMacRandomizationSupported(Services.wifi) as Boolean
/**
* Broadcast intent action indicating that Wi-Fi AP has been enabled, disabled,
* enabling, disabling, or failed.
*/
const val WIFI_AP_STATE_CHANGED_ACTION = "android.net.wifi.WIFI_AP_STATE_CHANGED"
/**
* The lookup key for an int that indicates whether Wi-Fi AP is enabled,
* disabled, enabling, disabling, or failed. Retrieve it with [Intent.getIntExtra].
*
* @see WIFI_AP_STATE_DISABLED
* @see WIFI_AP_STATE_DISABLING
* @see WIFI_AP_STATE_ENABLED
* @see WIFI_AP_STATE_ENABLING
* @see WIFI_AP_STATE_FAILED
*/
private const val EXTRA_WIFI_AP_STATE = "wifi_state"
/**
* An extra containing the int error code for Soft AP start failure.
* Can be obtained from the [WIFI_AP_STATE_CHANGED_ACTION] using [Intent.getIntExtra].
* This extra will only be attached if [EXTRA_WIFI_AP_STATE] is
* attached and is equal to [WIFI_AP_STATE_FAILED].
*
* The error code will be one of:
* {@link #SAP_START_FAILURE_GENERAL},
* {@link #SAP_START_FAILURE_NO_CHANNEL},
* {@link #SAP_START_FAILURE_UNSUPPORTED_CONFIGURATION}
*
* Source: https://android.googlesource.com/platform/frameworks/base/+/android-6.0.0_r1/wifi/java/android/net/wifi/WifiManager.java#210
*/
val EXTRA_WIFI_AP_FAILURE_REASON get() =
if (Build.VERSION.SDK_INT >= 30) "android.net.wifi.extra.WIFI_AP_FAILURE_REASON" else "wifi_ap_error_code"
/**
* The lookup key for a String extra that stores the interface name used for the Soft AP.
* This extra is included in the broadcast [WIFI_AP_STATE_CHANGED_ACTION].
* Retrieve its value with [Intent.getStringExtra].
*
* Source: https://android.googlesource.com/platform/frameworks/base/+/android-8.0.0_r1/wifi/java/android/net/wifi/WifiManager.java#413
*/
val EXTRA_WIFI_AP_INTERFACE_NAME get() =
if (Build.VERSION.SDK_INT >= 30) "android.net.wifi.extra.WIFI_AP_INTERFACE_NAME" else "wifi_ap_interface_name"
fun checkWifiApState(state: Int) = if (state < WIFI_AP_STATE_DISABLING || state > WIFI_AP_STATE_FAILED) {
Timber.w(Exception("Unknown state $state"))
false
} else true
val Intent.wifiApState get() =
getIntExtra(EXTRA_WIFI_AP_STATE, WIFI_AP_STATE_DISABLED).also { checkWifiApState(it) }
/**
* Wi-Fi AP is currently being disabled. The state will change to
* [WIFI_AP_STATE_DISABLED] if it finishes successfully.
*
* @see WIFI_AP_STATE_CHANGED_ACTION
* @see #getWifiApState()
*/
const val WIFI_AP_STATE_DISABLING = 10
/**
* Wi-Fi AP is disabled.
*
* @see WIFI_AP_STATE_CHANGED_ACTION
* @see #getWifiState()
*/
const val WIFI_AP_STATE_DISABLED = 11
/**
* Wi-Fi AP is currently being enabled. The state will change to
* {@link #WIFI_AP_STATE_ENABLED} if it finishes successfully.
*
* @see WIFI_AP_STATE_CHANGED_ACTION
* @see #getWifiApState()
*/
const val WIFI_AP_STATE_ENABLING = 12
/**
* Wi-Fi AP is enabled.
*
* @see WIFI_AP_STATE_CHANGED_ACTION
* @see #getWifiApState()
*/
const val WIFI_AP_STATE_ENABLED = 13
/**
* Wi-Fi AP is in a failed state. This state will occur when an error occurs during
* enabling or disabling
*
* @see WIFI_AP_STATE_CHANGED_ACTION
* @see #getWifiApState()
*/
const val WIFI_AP_STATE_FAILED = 14
private val getWifiApConfiguration by lazy { WifiManager::class.java.getDeclaredMethod("getWifiApConfiguration") } private val getWifiApConfiguration by lazy { WifiManager::class.java.getDeclaredMethod("getWifiApConfiguration") }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
private val setWifiApConfiguration by lazy { private val setWifiApConfiguration by lazy {
@@ -72,29 +158,29 @@ object WifiApManager {
WifiManager::class.java.getDeclaredMethod("setSoftApConfiguration", SoftApConfiguration::class.java) WifiManager::class.java.getDeclaredMethod("setSoftApConfiguration", SoftApConfiguration::class.java)
} }
@get:RequiresApi(30)
val configuration get() = getSoftApConfiguration(Services.wifi) as SoftApConfiguration
/** /**
* Requires NETWORK_SETTINGS permission (or root) on API 30+, and OVERRIDE_WIFI_CONFIG on API 29-. * Requires NETWORK_SETTINGS permission (or root) on API 30+, and OVERRIDE_WIFI_CONFIG on API 29-.
*/ */
val configurationCompat get() = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") { @Deprecated("Use configuration instead", ReplaceWith("configuration"))
(getWifiApConfiguration(Services.wifi) as android.net.wifi.WifiConfiguration?)?.toCompat() @Suppress("DEPRECATION")
?: SoftApConfigurationCompat() val configurationLegacy get() = getWifiApConfiguration(Services.wifi) as android.net.wifi.WifiConfiguration?
} else configuration.toCompat() /**
fun setConfigurationCompat(value: SoftApConfigurationCompat) = (if (Build.VERSION.SDK_INT >= 30) { * Requires NETWORK_SETTINGS permission (or root).
setSoftApConfiguration(Services.wifi, value.toPlatform()) */
} else @Suppress("DEPRECATION") { @get:RequiresApi(30)
setWifiApConfiguration(Services.wifi, value.toWifiConfiguration()) val configuration get() = getSoftApConfiguration(Services.wifi) as SoftApConfiguration
}) as Boolean @Deprecated("Use SoftApConfiguration instead")
@Suppress("DEPRECATION")
fun setConfiguration(value: android.net.wifi.WifiConfiguration?) =
setWifiApConfiguration(Services.wifi, value) as Boolean
fun setConfiguration(value: SoftApConfiguration) = setSoftApConfiguration(Services.wifi, value) as Boolean
@RequiresApi(28)
interface SoftApCallbackCompat { interface SoftApCallbackCompat {
/** /**
* Called when soft AP state changes. * Called when soft AP state changes.
* *
* @param state the new AP state. One of {@link #WIFI_AP_STATE_DISABLED}, * @param state the new AP state. One of [WIFI_AP_STATE_DISABLED], [WIFI_AP_STATE_DISABLING],
* {@link #WIFI_AP_STATE_DISABLING}, {@link #WIFI_AP_STATE_ENABLED}, * [WIFI_AP_STATE_ENABLED], [WIFI_AP_STATE_ENABLING], [WIFI_AP_STATE_FAILED]
* {@link #WIFI_AP_STATE_ENABLING}, {@link #WIFI_AP_STATE_FAILED}
* @param failureReason reason when in failed state. One of * @param failureReason reason when in failed state. One of
* {@link #SAP_START_FAILURE_GENERAL}, * {@link #SAP_START_FAILURE_GENERAL},
* {@link #SAP_START_FAILURE_NO_CHANNEL}, * {@link #SAP_START_FAILURE_NO_CHANNEL},
@@ -150,7 +236,6 @@ object WifiApManager {
@RequiresApi(30) @RequiresApi(30)
fun onBlockedClientConnecting(client: Parcelable, blockedReason: Int) { } fun onBlockedClientConnecting(client: Parcelable, blockedReason: Int) { }
} }
@RequiresApi(28)
val failureReasonLookup = ConstantLookup<WifiManager>("SAP_START_FAILURE_", "GENERAL", "NO_CHANNEL") val failureReasonLookup = ConstantLookup<WifiManager>("SAP_START_FAILURE_", "GENERAL", "NO_CHANNEL")
@get:RequiresApi(30) @get:RequiresApi(30)
val clientBlockLookup by lazy { ConstantLookup<WifiManager>("SAP_CLIENT_") } val clientBlockLookup by lazy { ConstantLookup<WifiManager>("SAP_CLIENT_") }
@@ -166,7 +251,6 @@ object WifiApManager {
WifiManager::class.java.getDeclaredMethod("unregisterSoftApCallback", interfaceSoftApCallback) WifiManager::class.java.getDeclaredMethod("unregisterSoftApCallback", interfaceSoftApCallback)
} }
@RequiresApi(28)
fun registerSoftApCallback(callback: SoftApCallbackCompat, executor: Executor): Any { fun registerSoftApCallback(callback: SoftApCallbackCompat, executor: Executor): Any {
val proxy = Proxy.newProxyInstance(interfaceSoftApCallback.classLoader, val proxy = Proxy.newProxyInstance(interfaceSoftApCallback.classLoader,
arrayOf(interfaceSoftApCallback), object : InvocationHandler { arrayOf(interfaceSoftApCallback), object : InvocationHandler {
@@ -177,55 +261,36 @@ object WifiApManager {
} else invokeActual(proxy, method, args) } else invokeActual(proxy, method, args)
private fun invokeActual(proxy: Any, method: Method, args: Array<out Any?>?): Any? { private fun invokeActual(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
val noArgs = args?.size ?: 0 return when {
return when (val name = method.name) { method.matches("onStateChanged", Integer.TYPE, Integer.TYPE) -> {
"onStateChanged" -> {
if (noArgs != 2) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
callback.onStateChanged(args!![0] as Int, args[1] as Int) callback.onStateChanged(args!![0] as Int, args[1] as Int)
} }
"onNumClientsChanged" -> @Suppress("DEPRECATION") { method.matches("onNumClientsChanged", Integer.TYPE) -> {
if (Build.VERSION.SDK_INT >= 30) Timber.w(Exception("Unexpected onNumClientsChanged")) if (Build.VERSION.SDK_INT >= 30) Timber.w(Exception("Unexpected onNumClientsChanged"))
if (noArgs != 1) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
callback.onNumClientsChanged(args!![0] as Int) callback.onNumClientsChanged(args!![0] as Int)
} }
"onConnectedClientsChanged" -> @TargetApi(30) { method.matches1<java.util.List<*>>("onConnectedClientsChanged") -> @TargetApi(30) {
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onConnectedClientsChanged")) if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onConnectedClientsChanged"))
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
when (noArgs) { callback.onConnectedClientsChanged(args!![0] as List<Parcelable>)
1 -> callback.onConnectedClientsChanged(args!![0] as List<Parcelable>)
2 -> null // we use the old method which returns all clients in one call
else -> {
Timber.w("Unexpected args for $name: ${args?.contentToString()}")
null
} }
} method.matches1<java.util.List<*>>("onInfoChanged") -> @TargetApi(31) {
} if (Build.VERSION.SDK_INT < 31) Timber.w(Exception("Unexpected onInfoChanged API 31+"))
"onInfoChanged" -> @TargetApi(30) {
if (noArgs != 1) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
val arg = args!![0]
if (arg is List<*>) {
if (!BuildCompat.isAtLeastS()) Timber.w(Exception("Unexpected onInfoChanged API 31+"))
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
callback.onInfoChanged(arg as List<Parcelable>) callback.onInfoChanged(args!![0] as List<Parcelable>)
} else {
when (Build.VERSION.SDK_INT) {
30 -> { }
in 31..Int.MAX_VALUE -> return null // ignore old version calls
else -> Timber.w(Exception("Unexpected onInfoChanged API 30"))
} }
Build.VERSION.SDK_INT >= 30 && method.matches("onInfoChanged", SoftApInfo.clazz) -> {
if (Build.VERSION.SDK_INT >= 31) return null // ignore old version calls
val arg = args!![0]
val info = SoftApInfo(arg as Parcelable) val info = SoftApInfo(arg as Parcelable)
callback.onInfoChanged( // check for legacy empty info with CHANNEL_WIDTH_INVALID callback.onInfoChanged(if (info.frequency == 0 && info.bandwidth ==
if (info.frequency == 0 && info.bandwidth == 0) emptyList() else listOf(arg)) SoftApConfigurationCompat.CHANNEL_WIDTH_INVALID) emptyList() else listOf(arg))
} }
} Build.VERSION.SDK_INT >= 30 && method.matches("onCapabilityChanged", SoftApCapability.clazz) -> {
"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()}")
callback.onCapabilityChanged(args!![0] as Parcelable) callback.onCapabilityChanged(args!![0] as Parcelable)
} }
"onBlockedClientConnecting" -> @TargetApi(30) { Build.VERSION.SDK_INT >= 30 && method.matches("onBlockedClientConnecting", WifiClient.clazz,
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onBlockedClientConnecting")) Int::class.java) -> {
if (noArgs != 2) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
callback.onBlockedClientConnecting(args!![0] as Parcelable, args[1] as Int) callback.onBlockedClientConnecting(args!![0] as Parcelable, args[1] as Int)
} }
else -> callSuper(interfaceSoftApCallback, proxy, method, args) else -> callSuper(interfaceSoftApCallback, proxy, method, args)
@@ -237,7 +302,6 @@ object WifiApManager {
} else registerSoftApCallback(Services.wifi, proxy, null) } else registerSoftApCallback(Services.wifi, proxy, null)
return proxy return proxy
} }
@RequiresApi(28)
fun unregisterSoftApCallback(key: Any) = unregisterSoftApCallback(Services.wifi, key) fun unregisterSoftApCallback(key: Any) = unregisterSoftApCallback(Services.wifi, key)
@get:RequiresApi(30) @get:RequiresApi(30)
@@ -253,43 +317,9 @@ object WifiApManager {
private val cancelLocalOnlyHotspotRequest by lazy { private val cancelLocalOnlyHotspotRequest by lazy {
WifiManager::class.java.getDeclaredMethod("cancelLocalOnlyHotspotRequest") WifiManager::class.java.getDeclaredMethod("cancelLocalOnlyHotspotRequest")
} }
@RequiresApi(26) /**
* This is the only way to unregister requests besides app exiting.
* Therefore, we are happy with crashing the app if reflection fails.
*/
fun cancelLocalOnlyHotspotRequest() = cancelLocalOnlyHotspotRequest(Services.wifi) fun cancelLocalOnlyHotspotRequest() = cancelLocalOnlyHotspotRequest(Services.wifi)
@Suppress("DEPRECATION")
private val setWifiApEnabled by lazy {
WifiManager::class.java.getDeclaredMethod("setWifiApEnabled",
android.net.wifi.WifiConfiguration::class.java, Boolean::class.java)
}
/**
* Start AccessPoint mode with the specified
* configuration. If the radio is already running in
* AP mode, update the new configuration
* Note that starting in access point mode disables station
* mode operation
* @param wifiConfig SSID, security and channel details as
* part of WifiConfiguration
* @return {@code true} if the operation succeeds, {@code false} otherwise
*/
@Suppress("DEPRECATION")
private fun WifiManager.setWifiApEnabled(wifiConfig: android.net.wifi.WifiConfiguration?, enabled: Boolean) =
setWifiApEnabled(this, wifiConfig, enabled) as Boolean
/**
* Although the functionalities were removed in API 26, it is already not functioning correctly on API 25.
*
* See also: https://android.googlesource.com/platform/frameworks/base/+/5c0b10a4a9eecc5307bb89a271221f2b20448797%5E%21/
*/
@Suppress("DEPRECATION")
@Deprecated("Not usable since API 26, malfunctioning on API 25")
fun start(wifiConfig: android.net.wifi.WifiConfiguration? = null) {
Services.wifi.isWifiEnabled = false
Services.wifi.setWifiApEnabled(wifiConfig, true)
}
@Suppress("DEPRECATION")
@Deprecated("Not usable since API 26")
fun stop() {
Services.wifi.setWifiApEnabled(null, false)
Services.wifi.isWifiEnabled = true
}
} }

View File

@@ -11,7 +11,7 @@ import timber.log.Timber
@RequiresApi(30) @RequiresApi(30)
value class WifiClient(val inner: Parcelable) { value class WifiClient(val inner: Parcelable) {
companion object { companion object {
private val clazz by lazy { Class.forName("android.net.wifi.WifiClient") } val clazz by lazy { Class.forName("android.net.wifi.WifiClient") }
private val getMacAddress by lazy { clazz.getDeclaredMethod("getMacAddress") } private val getMacAddress by lazy { clazz.getDeclaredMethod("getMacAddress") }
@get:RequiresApi(31) @get:RequiresApi(31)
private val getApInstanceIdentifier by lazy @TargetApi(31) { UnblockCentral.getApInstanceIdentifier(clazz) } private val getApInstanceIdentifier by lazy @TargetApi(31) { UnblockCentral.getApInstanceIdentifier(clazz) }

View File

@@ -1,15 +1,18 @@
package be.mygod.vpnhotspot.net.wifi package be.mygod.vpnhotspot.net.wifi
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.net.MacAddress
import android.net.wifi.ScanResult
import android.net.wifi.WpsInfo import android.net.wifi.WpsInfo
import android.net.wifi.p2p.WifiP2pGroup import android.net.wifi.p2p.WifiP2pGroup
import android.net.wifi.p2p.WifiP2pInfo
import android.net.wifi.p2p.WifiP2pManager import android.net.wifi.p2p.WifiP2pManager
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.MacAddressCompat import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.util.callSuper import be.mygod.vpnhotspot.util.callSuper
import be.mygod.vpnhotspot.util.matches
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import timber.log.Timber
import java.lang.reflect.InvocationHandler import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method import java.lang.reflect.Method
import java.lang.reflect.Proxy import java.lang.reflect.Proxy
@@ -53,6 +56,15 @@ object WifiP2pManagerHelper {
return result.future.await() return result.future.await()
} }
@SuppressLint("MissingPermission") // this method will fail correctly if permission is missing
@RequiresApi(33)
suspend fun WifiP2pManager.setVendorElements(c: WifiP2pManager.Channel,
ve: List<ScanResult.InformationElement>): Int? {
val result = ResultListener()
setVendorElements(c, ve, result)
return result.future.await()
}
/** /**
* Available since Android 4.3. * Available since Android 4.3.
* *
@@ -98,9 +110,8 @@ object WifiP2pManagerHelper {
private val interfacePersistentGroupInfoListener by lazy { private val interfacePersistentGroupInfoListener by lazy {
Class.forName("android.net.wifi.p2p.WifiP2pManager\$PersistentGroupInfoListener") Class.forName("android.net.wifi.p2p.WifiP2pManager\$PersistentGroupInfoListener")
} }
private val getGroupList by lazy { private val classWifiP2pGroupList by lazy { Class.forName("android.net.wifi.p2p.WifiP2pGroupList") }
Class.forName("android.net.wifi.p2p.WifiP2pGroupList").getDeclaredMethod("getGroupList") private val getGroupList by lazy { classWifiP2pGroupList.getDeclaredMethod("getGroupList") }
}
private val requestPersistentGroupInfo by lazy { private val requestPersistentGroupInfo by lazy {
WifiP2pManager::class.java.getDeclaredMethod("requestPersistentGroupInfo", WifiP2pManager::class.java.getDeclaredMethod("requestPersistentGroupInfo",
WifiP2pManager.Channel::class.java, interfacePersistentGroupInfoListener) WifiP2pManager.Channel::class.java, interfacePersistentGroupInfoListener)
@@ -116,9 +127,8 @@ object WifiP2pManagerHelper {
val result = CompletableDeferred<Collection<WifiP2pGroup>>() val result = CompletableDeferred<Collection<WifiP2pGroup>>()
requestPersistentGroupInfo(this, c, Proxy.newProxyInstance(interfacePersistentGroupInfoListener.classLoader, requestPersistentGroupInfo(this, c, Proxy.newProxyInstance(interfacePersistentGroupInfoListener.classLoader,
arrayOf(interfacePersistentGroupInfoListener), object : InvocationHandler { arrayOf(interfacePersistentGroupInfoListener), object : InvocationHandler {
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? = when (method.name) { override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? = when {
"onPersistentGroupInfoAvailable" -> { method.matches("onPersistentGroupInfoAvailable", classWifiP2pGroupList) -> {
if (args?.size != 1) Timber.w(IllegalArgumentException("Unexpected args: $args"))
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
result.complete(getGroupList(args!![0]) as Collection<WifiP2pGroup>) result.complete(getGroupList(args!![0]) as Collection<WifiP2pGroup>)
} }
@@ -128,14 +138,22 @@ object WifiP2pManagerHelper {
return result.await() return result.await()
} }
@SuppressLint("MissingPermission") suspend fun WifiP2pManager.requestConnectionInfo(c: WifiP2pManager.Channel) =
CompletableDeferred<WifiP2pInfo?>().apply { requestConnectionInfo(c) { complete(it) } }.await()
@SuppressLint("MissingPermission") // missing permission simply leads to null result
@RequiresApi(29) @RequiresApi(29)
suspend fun WifiP2pManager.requestDeviceAddress(c: WifiP2pManager.Channel): MacAddressCompat? { suspend fun WifiP2pManager.requestDeviceAddress(c: WifiP2pManager.Channel): MacAddress? {
val future = CompletableDeferred<String?>() val future = CompletableDeferred<String?>()
requestDeviceInfo(c) { future.complete(it?.deviceAddress) } requestDeviceInfo(c) { future.complete(it?.deviceAddress) }
return future.await()?.let { return future.await()?.let {
val address = if (it.isEmpty()) null else MacAddressCompat.fromString(it) val address = if (it.isEmpty()) null else MacAddress.fromString(it)
if (address == MacAddressCompat.ANY_ADDRESS) null else address if (address == MacAddressCompat.ANY_ADDRESS) null else address
} }
} }
@SuppressLint("MissingPermission") // missing permission simply leads to null result
suspend fun WifiP2pManager.requestGroupInfo(c: WifiP2pManager.Channel) =
CompletableDeferred<WifiP2pGroup?>().apply { requestGroupInfo(c) { complete(it) } }.await()
@RequiresApi(29)
suspend fun WifiP2pManager.requestP2pState(c: WifiP2pManager.Channel) =
CompletableDeferred<Int>().apply { requestP2pState(c) { complete(it) } }.await()
} }

View File

@@ -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()
}

View File

@@ -1,42 +0,0 @@
package be.mygod.vpnhotspot.preference
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.core.os.bundleOf
import androidx.preference.EditTextPreferenceDialogFragmentCompat
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.widget.AlwaysAutoCompleteEditText
class AlwaysAutoCompleteEditTextPreferenceDialogFragment : EditTextPreferenceDialogFragmentCompat() {
companion object {
private const val ARG_SUGGESTIONS = "suggestions"
}
fun setArguments(key: String, suggestions: Array<String>) {
arguments = bundleOf(ARG_KEY to key, ARG_SUGGESTIONS to suggestions)
}
private lateinit var editText: AlwaysAutoCompleteEditText
override fun onCreateDialogView(context: Context) = super.onCreateDialogView(context).apply {
editText = AlwaysAutoCompleteEditText(context).apply {
id = android.R.id.edit
minHeight = resources.getDimensionPixelSize(R.dimen.touch_target_min)
}
val oldEditText = findViewById<View>(android.R.id.edit)!!
val container = oldEditText.parent as ViewGroup
container.removeView(oldEditText)
container.addView(editText, oldEditText.layoutParams)
}
override fun onBindDialogView(view: View) {
super.onBindDialogView(view)
editText.hint = (preference.summaryProvider as SummaryFallbackProvider).fallback
arguments?.getStringArray(ARG_SUGGESTIONS)?.let { suggestions ->
editText.setAdapter(ArrayAdapter(view.context, android.R.layout.select_dialog_item, suggestions))
}
editText.clearFocus() // having focus is buggy currently
}
}

View File

@@ -0,0 +1,82 @@
package be.mygod.vpnhotspot.preference
import android.content.Context
import android.net.ConnectivityManager
import android.net.LinkProperties
import android.net.Network
import android.net.NetworkCapabilities
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.withStarted
import androidx.preference.EditTextPreferenceDialogFragmentCompat
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.allInterfaceNames
import be.mygod.vpnhotspot.util.globalNetworkRequestBuilder
import be.mygod.vpnhotspot.widget.AlwaysAutoCompleteEditText
import kotlinx.coroutines.launch
class AutoCompleteNetworkPreferenceDialogFragment : EditTextPreferenceDialogFragmentCompat() {
fun setArguments(key: String) {
arguments = bundleOf(ARG_KEY to key)
}
private lateinit var editText: AlwaysAutoCompleteEditText
private lateinit var adapter: ArrayAdapter<String>
private fun updateAdapter() {
adapter.clear()
adapter.addAll(interfaceNames.flatMap { it.value })
}
private val interfaceNames = mutableMapOf<Network, List<String>>()
private val callback = object : ConnectivityManager.NetworkCallback() {
override fun onLinkPropertiesChanged(network: Network, properties: LinkProperties) {
interfaceNames[network] = properties.allInterfaceNames
lifecycleScope.launch {
withStarted { updateAdapter() }
}
}
override fun onLost(network: Network) {
interfaceNames.remove(network)
lifecycleScope.launch {
withStarted { updateAdapter() }
}
}
}
override fun onCreateDialogView(context: Context) = super.onCreateDialogView(context)!!.apply {
val oldEditText = findViewById<View>(android.R.id.edit)!!
val container = oldEditText.parent as ViewGroup
container.removeView(oldEditText)
container.addView(layoutInflater.inflate(R.layout.preference_widget_edittext_autocomplete, container, false),
oldEditText.layoutParams)
}
override fun onBindDialogView(view: View) {
super.onBindDialogView(view)
editText = view.findViewById(android.R.id.edit)
editText.hint = (preference.summaryProvider as SummaryFallbackProvider).fallback
adapter = ArrayAdapter(view.context, android.R.layout.select_dialog_item)
editText.setAdapter(adapter)
}
override fun onStart() {
super.onStart()
Services.registerNetworkCallback(globalNetworkRequestBuilder().apply {
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
}.build(), callback)
}
override fun onStop() {
Services.connectivity.unregisterNetworkCallback(callback)
interfaceNames.clear()
updateAdapter()
super.onStop()
}
}

View File

@@ -8,7 +8,6 @@ import android.text.style.StyleSpan
import android.util.AttributeSet import android.util.AttributeSet
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference import androidx.preference.Preference
import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor
@@ -31,7 +30,7 @@ class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(co
if (internet) SpannableStringBuilder(ifname).apply { if (internet) SpannableStringBuilder(ifname).apply {
setSpan(StyleSpan(Typeface.BOLD), 0, length, 0) setSpan(StyleSpan(Typeface.BOLD), 0, length, 0)
} else ifname } else ifname
}.joinTo(SpannableStringBuilder()).let { if (it.isEmpty()) "" else it } }.joinTo(SpannableStringBuilder()).ifEmpty { "" }
override fun onAvailable(properties: LinkProperties?) { override fun onAvailable(properties: LinkProperties?) {
val result = mutableMapOf<String, Boolean>() val result = mutableMapOf<String, Boolean>()
@@ -51,15 +50,11 @@ class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(co
} }
private val primary = Monitor() private val primary = Monitor()
private val fallback: Monitor = object : Monitor() { private val fallback = Monitor()
override fun onFallback() {
currentInterfaces = mapOf("<default>" to true)
onUpdate()
}
}
init { init {
(context as LifecycleOwner).lifecycle.addObserver(this) (context as LifecycleOwner).lifecycle.addObserver(this)
onUpdate()
} }
override fun onStart(owner: LifecycleOwner) { override fun onStart(owner: LifecycleOwner) {
@@ -71,8 +66,8 @@ class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(co
FallbackUpstreamMonitor.unregisterCallback(fallback) FallbackUpstreamMonitor.unregisterCallback(fallback)
} }
private fun onUpdate() = (context as LifecycleOwner).lifecycleScope.launchWhenStarted { private fun onUpdate() {
summary = context.getText(R.string.settings_service_upstream_monitor_summary).format( summary = context.getText(R.string.settings_service_upstream_monitor_summary).format(
context.resources.configuration.locale, primary.charSequence, fallback.charSequence) context.resources.configuration.locales[0], primary.charSequence, fallback.charSequence)
} }
} }

View File

@@ -1,37 +1,38 @@
package be.mygod.vpnhotspot.room package be.mygod.vpnhotspot.room
import android.net.MacAddress
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.map import androidx.lifecycle.map
import androidx.room.* import androidx.room.*
import be.mygod.vpnhotspot.net.MacAddressCompat import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toLong
@Entity @Entity
data class ClientRecord(@PrimaryKey data class ClientRecord(@PrimaryKey
val mac: Long, val mac: MacAddress,
var nickname: CharSequence = "", var nickname: CharSequence = "",
var blocked: Boolean = false, var blocked: Boolean = false,
var macLookupPending: Boolean = true) { var macLookupPending: Boolean = true) {
@androidx.room.Dao @androidx.room.Dao
abstract class Dao { abstract class Dao {
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac") @Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
protected abstract fun lookupBlocking(mac: Long): ClientRecord? protected abstract fun lookupBlocking(mac: MacAddress): ClientRecord?
fun lookupOrDefaultBlocking(mac: MacAddressCompat) = lookupBlocking(mac.addr) ?: ClientRecord(mac.addr) fun lookupOrDefaultBlocking(mac: MacAddress) = lookupBlocking(mac) ?: ClientRecord(mac)
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac") @Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
protected abstract suspend fun lookup(mac: Long): ClientRecord? protected abstract suspend fun lookup(mac: MacAddress): ClientRecord?
suspend fun lookupOrDefault(mac: Long) = lookup(mac) ?: ClientRecord(mac) suspend fun lookupOrDefault(mac: MacAddress) = lookup(mac) ?: ClientRecord(mac)
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac") @Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
protected abstract fun lookupSync(mac: Long): LiveData<ClientRecord?> protected abstract fun lookupSync(mac: MacAddress): LiveData<ClientRecord?>
fun lookupOrDefaultSync(mac: MacAddressCompat) = lookupSync(mac.addr).map { it ?: ClientRecord(mac.addr) } fun lookupOrDefaultSync(mac: MacAddress) = lookupSync(mac).map { it ?: ClientRecord(mac) }
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
protected abstract suspend fun updateInternal(value: ClientRecord): Long protected abstract suspend fun updateInternal(value: ClientRecord): Long
suspend fun update(value: ClientRecord) = check(updateInternal(value) == value.mac) suspend fun update(value: ClientRecord) = check(updateInternal(value) == value.mac.toLong())
@Transaction @Transaction
open suspend fun upsert(mac: MacAddressCompat, operation: suspend ClientRecord.() -> Unit) = lookupOrDefault( open suspend fun upsert(mac: MacAddress, operation: suspend ClientRecord.() -> Unit) = lookupOrDefault(
mac.addr).apply { mac).apply {
operation() operation()
update(this) update(this)
} }

View File

@@ -1,8 +1,11 @@
package be.mygod.vpnhotspot.room package be.mygod.vpnhotspot.room
import android.net.MacAddress
import android.text.TextUtils import android.text.TextUtils
import androidx.room.TypeConverter import androidx.room.TypeConverter
import be.mygod.librootkotlinx.useParcel import be.mygod.librootkotlinx.useParcel
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toLong
import timber.log.Timber import timber.log.Timber
import java.net.InetAddress import java.net.InetAddress
@@ -27,6 +30,14 @@ object Converters {
} }
} }
@JvmStatic
@TypeConverter
fun persistMacAddress(address: MacAddress) = address.toLong()
@JvmStatic
@TypeConverter
fun unpersistMacAddress(address: Long) = MacAddressCompat(address).toPlatform()
@JvmStatic @JvmStatic
@TypeConverter @TypeConverter
fun persistInetAddress(address: InetAddress): ByteArray = address.address fun persistInetAddress(address: InetAddress): ByteArray = address.address

View File

@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot.room package be.mygod.vpnhotspot.room
import android.net.MacAddress
import android.os.Parcelable import android.os.Parcelable
import androidx.room.* import androidx.room.*
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@@ -22,7 +23,7 @@ data class TrafficRecord(
/** /**
* Foreign key/ID for (possibly non-existent, i.e. default) entry in ClientRecord. * Foreign key/ID for (possibly non-existent, i.e. default) entry in ClientRecord.
*/ */
val mac: Long, val mac: MacAddress,
/** /**
* For now only stats for IPv4 will be recorded. But I'm going to put the more general class here just in case. * For now only stats for IPv4 will be recorded. But I'm going to put the more general class here just in case.
*/ */
@@ -58,7 +59,7 @@ data class TrafficRecord(
/* We only want to find the last record for each chain so that we don't double count */ /* We only want to find the last record for each chain so that we don't double count */
WHERE TrafficRecord.mac = :mac AND Next.id IS NULL WHERE TrafficRecord.mac = :mac AND Next.id IS NULL
""") """)
abstract suspend fun queryStats(mac: Long): ClientStats abstract suspend fun queryStats(mac: MacAddress): ClientStats
} }
} }

View File

@@ -1,7 +1,6 @@
package be.mygod.vpnhotspot.root package be.mygod.vpnhotspot.root
import android.content.Context import android.content.Context
import android.os.Build
import android.os.Parcelable import android.os.Parcelable
import android.os.RemoteException import android.os.RemoteException
import android.provider.Settings import android.provider.Settings
@@ -30,18 +29,10 @@ fun ProcessBuilder.fixPath(redirect: Boolean = false) = apply {
@Parcelize @Parcelize
data class Dump(val path: String, val cacheDir: File = app.deviceStorage.codeCacheDir) : RootCommandNoResult { data class Dump(val path: String, val cacheDir: File = app.deviceStorage.codeCacheDir) : RootCommandNoResult {
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun execute() = withContext(Dispatchers.IO) { override suspend fun execute() = withContext(Dispatchers.IO) {
FileOutputStream(path, true).use { out -> FileOutputStream(path, true).use { out ->
val process = ProcessBuilder("sh").fixPath(true).start() val process = ProcessBuilder("sh").fixPath(true).start()
process.outputStream.bufferedWriter().use { commands -> process.outputStream.bufferedWriter().use { commands ->
// https://android.googlesource.com/platform/external/iptables/+/android-7.0.0_r1/iptables/Android.mk#34
val iptablesSave = if (Build.VERSION.SDK_INT < 24) File(cacheDir, "iptables-save").absolutePath.also {
commands.appendLine("ln -sf /system/bin/iptables $it")
} else "iptables-save"
val ip6tablesSave = if (Build.VERSION.SDK_INT < 24) File(cacheDir, "ip6tables-save").absolutePath.also {
commands.appendLine("ln -sf /system/bin/ip6tables $it")
} else "ip6tables-save"
commands.appendLine(""" commands.appendLine("""
|echo dumpsys ${Context.WIFI_P2P_SERVICE} |echo dumpsys ${Context.WIFI_P2P_SERVICE}
|dumpsys ${Context.WIFI_P2P_SERVICE} |dumpsys ${Context.WIFI_P2P_SERVICE}
@@ -50,13 +41,13 @@ data class Dump(val path: String, val cacheDir: File = app.deviceStorage.codeCac
|dumpsys ${Context.CONNECTIVITY_SERVICE} tethering |dumpsys ${Context.CONNECTIVITY_SERVICE} tethering
|echo |echo
|echo iptables -t filter |echo iptables -t filter
|$iptablesSave -t filter |iptables-save -t filter
|echo |echo
|echo iptables -t nat |echo iptables -t nat
|$iptablesSave -t nat |iptables-save -t nat
|echo |echo
|echo ip6tables-save |echo ip6tables-save
|$ip6tablesSave |ip6tables-save
|echo |echo
|echo ip rule |echo ip rule
|$IP rule |$IP rule
@@ -125,7 +116,7 @@ class ProcessListener(private val terminateRegex: Regex,
parent.join() parent.join()
} finally { } finally {
parent.cancel() parent.cancel()
if (Build.VERSION.SDK_INT < 26) process.destroy() else if (process.isAlive) process.destroyForcibly() if (process.isAlive) process.destroyForcibly()
parent.join() parent.join()
} }
} }
@@ -162,7 +153,6 @@ data class StartTethering(private val type: Int,
@Deprecated("Old API since API 30") @Deprecated("Old API since API 30")
@Parcelize @Parcelize
@RequiresApi(24)
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
data class StartTetheringLegacy(private val cacheDir: File, private val type: Int, data class StartTetheringLegacy(private val cacheDir: File, private val type: Int,
private val showProvisioningUi: Boolean) : RootCommand<ParcelableBoolean> { private val showProvisioningUi: Boolean) : RootCommand<ParcelableBoolean> {
@@ -184,7 +174,6 @@ data class StartTetheringLegacy(private val cacheDir: File, private val type: In
} }
@Parcelize @Parcelize
@RequiresApi(24)
data class StopTethering(private val type: Int) : RootCommandNoResult { data class StopTethering(private val type: Int) : RootCommandNoResult {
override suspend fun execute(): Parcelable? { override suspend fun execute(): Parcelable? {
TetheringManager.stopTethering(type) TetheringManager.stopTethering(type)
@@ -209,12 +198,11 @@ data class SettingsGlobalPut(val name: String, val value: String) : RootCommandN
} }
} }
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun execute() = withContext(Dispatchers.IO) { override suspend fun execute() = withContext(Dispatchers.IO) {
val process = ProcessBuilder("settings", "put", "global", name, value).fixPath(true).start() val process = ProcessBuilder("settings", "put", "global", name, value).fixPath(true).start()
val error = process.inputStream.bufferedReader().readText() val error = process.inputStream.bufferedReader().readText()
check(process.waitFor() == 0) val exit = process.waitFor()
if (error.isNotEmpty()) throw RemoteException(error) if (exit != 0 || error.isNotEmpty()) throw RemoteException("Process exited with $exit: $error")
null null
} }
} }

View File

@@ -1,5 +1,7 @@
package be.mygod.vpnhotspot.root package be.mygod.vpnhotspot.root
import android.net.MacAddress
import android.net.wifi.ScanResult
import android.net.wifi.p2p.WifiP2pManager import android.net.wifi.p2p.WifiP2pManager
import android.os.Looper import android.os.Looper
import android.os.Parcelable import android.os.Parcelable
@@ -11,6 +13,7 @@ import be.mygod.librootkotlinx.*
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.deletePersistentGroup import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.deletePersistentGroup
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestDeviceAddress import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestDeviceAddress
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupInfo import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupInfo
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setVendorElements
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
import be.mygod.vpnhotspot.util.Services import be.mygod.vpnhotspot.util.Services
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@@ -35,10 +38,8 @@ object RepeaterCommands {
@Parcelize @Parcelize
@RequiresApi(29) @RequiresApi(29)
class RequestDeviceAddress : RootCommand<ParcelableLong?> { class RequestDeviceAddress : RootCommand<MacAddress?> {
override suspend fun execute() = Services.p2p!!.run { override suspend fun execute() = Services.p2p!!.run { requestDeviceAddress(obtainChannel()) }
requestDeviceAddress(obtainChannel())?.let { ParcelableLong(it.addr) }
}
} }
@Parcelize @Parcelize
@@ -55,6 +56,14 @@ object RepeaterCommands {
} }
} }
@Parcelize
@RequiresApi(33)
data class SetVendorElements(private val ve: List<ScanResult.InformationElement>) : RootCommand<ParcelableInt?> {
override suspend fun execute() = Services.p2p!!.run {
setVendorElements(obtainChannel(), ve)?.let { ParcelableInt(it) }
}
}
@Parcelize @Parcelize
data class WriteP2pConfig(val data: String, val legacy: Boolean) : RootCommandNoResult { data class WriteP2pConfig(val data: String, val legacy: Boolean) : RootCommandNoResult {
override suspend fun execute(): Parcelable? { override suspend fun execute(): Parcelable? {

View File

@@ -6,6 +6,8 @@ import android.util.Log
import be.mygod.librootkotlinx.* import be.mygod.librootkotlinx.*
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.util.Services import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.UnblockCentral
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
@@ -31,6 +33,7 @@ object RootManager : RootSession(), Logger {
}) })
Logger.me = RootManager Logger.me = RootManager
Services.init { systemContext } Services.init { systemContext }
UnblockCentral.needInit = false
return null return null
} }
} }
@@ -42,7 +45,10 @@ object RootManager : RootSession(), Logger {
override suspend fun initServer(server: RootServer) { override suspend fun initServer(server: RootServer) {
Logger.me = this Logger.me = this
server.init(app.deviceStorage) AppProcess.shouldRelocateHeuristics.let {
FirebaseCrashlytics.getInstance().setCustomKey("RootManager.relocateEnabled", it)
server.init(app.deviceStorage, it)
}
server.execute(RootInit()) server.execute(RootInit())
} }
} }

View File

@@ -2,7 +2,7 @@ package be.mygod.vpnhotspot.root
import android.os.Parcelable import android.os.Parcelable
import be.mygod.librootkotlinx.RootCommand import be.mygod.librootkotlinx.RootCommand
import be.mygod.librootkotlinx.RootCommandOneWay import be.mygod.librootkotlinx.RootCommandNoResult
import be.mygod.vpnhotspot.net.Routing import be.mygod.vpnhotspot.net.Routing
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
@@ -13,8 +13,7 @@ import timber.log.Timber
object RoutingCommands { object RoutingCommands {
@Parcelize @Parcelize
class Clean : RootCommandOneWay { class Clean : RootCommandNoResult {
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun execute() = withContext(Dispatchers.IO) { override suspend fun execute() = withContext(Dispatchers.IO) {
val process = ProcessBuilder("sh").fixPath(true).start() val process = ProcessBuilder("sh").fixPath(true).start()
process.outputStream.bufferedWriter().use(Routing.Companion::appendCleanCommands) process.outputStream.bufferedWriter().use(Routing.Companion::appendCleanCommands)
@@ -23,6 +22,7 @@ object RoutingCommands {
else -> Timber.w("Unexpected exit code $code") else -> Timber.w("Unexpected exit code $code")
} }
check(process.waitFor() == 0) check(process.waitFor() == 0)
null
} }
} }

View File

@@ -1,13 +1,19 @@
package be.mygod.vpnhotspot.root package be.mygod.vpnhotspot.root
import android.annotation.TargetApi
import android.content.ClipData
import android.net.wifi.SoftApConfiguration
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
import android.os.Build
import android.os.Parcelable import android.os.Parcelable
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import be.mygod.librootkotlinx.ParcelableBoolean import be.mygod.librootkotlinx.ParcelableBoolean
import be.mygod.librootkotlinx.RootCommand import be.mygod.librootkotlinx.RootCommand
import be.mygod.librootkotlinx.RootCommandChannel import be.mygod.librootkotlinx.RootCommandChannel
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.net.wifi.WifiApManager import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.net.wifi.WifiClient
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.* import kotlinx.coroutines.channels.*
@@ -15,7 +21,6 @@ import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
object WifiApCommands { object WifiApCommands {
@RequiresApi(28)
sealed class SoftApCallbackParcel : Parcelable { sealed class SoftApCallbackParcel : Parcelable {
abstract fun dispatch(callback: WifiApManager.SoftApCallbackCompat) abstract fun dispatch(callback: WifiApManager.SoftApCallbackCompat)
@@ -55,7 +60,6 @@ object WifiApCommands {
} }
@Parcelize @Parcelize
@RequiresApi(28)
class RegisterSoftApCallback : RootCommandChannel<SoftApCallbackParcel> { class RegisterSoftApCallback : RootCommandChannel<SoftApCallbackParcel> {
override fun create(scope: CoroutineScope) = scope.produce(capacity = capacity) { override fun create(scope: CoroutineScope) = scope.produce(capacity = capacity) {
val finish = CompletableDeferred<Unit>() val finish = CompletableDeferred<Unit>()
@@ -111,7 +115,6 @@ object WifiApCommands {
private val callbacks = mutableSetOf<WifiApManager.SoftApCallbackCompat>() private val callbacks = mutableSetOf<WifiApManager.SoftApCallbackCompat>()
private val lastCallback = AutoFiringCallbacks() private val lastCallback = AutoFiringCallbacks()
private var rootCallbackJob: Job? = null private var rootCallbackJob: Job? = null
@RequiresApi(28)
private suspend fun handleChannel(channel: ReceiveChannel<SoftApCallbackParcel>) = channel.consumeEach { parcel -> private suspend fun handleChannel(channel: ReceiveChannel<SoftApCallbackParcel>) = channel.consumeEach { parcel ->
when (parcel) { when (parcel) {
is SoftApCallbackParcel.OnStateChanged -> synchronized(callbacks) { lastCallback.state = parcel } is SoftApCallbackParcel.OnStateChanged -> synchronized(callbacks) { lastCallback.state = parcel }
@@ -121,10 +124,22 @@ object WifiApCommands {
} }
is SoftApCallbackParcel.OnInfoChanged -> synchronized(callbacks) { lastCallback.info = parcel } is SoftApCallbackParcel.OnInfoChanged -> synchronized(callbacks) { lastCallback.info = parcel }
is SoftApCallbackParcel.OnCapabilityChanged -> synchronized(callbacks) { lastCallback.capability = parcel } is SoftApCallbackParcel.OnCapabilityChanged -> synchronized(callbacks) { lastCallback.capability = parcel }
is SoftApCallbackParcel.OnBlockedClientConnecting -> @TargetApi(30) { // passively consume events
val client = WifiClient(parcel.client)
val macAddress = client.macAddress
var name = macAddress.toString()
if (Build.VERSION.SDK_INT >= 31) client.apInstanceIdentifier?.let { name += "%$it" }
val reason = WifiApManager.clientBlockLookup(parcel.blockedReason, true)
Timber.i("$name blocked from connecting: $reason (${parcel.blockedReason})")
SmartSnackbar.make(app.getString(R.string.tethering_manage_wifi_client_blocked, name, reason)).apply {
action(R.string.tethering_manage_wifi_copy_mac) {
app.clipboard.setPrimaryClip(ClipData.newPlainText(null, macAddress.toString()))
}
}.show()
}
} }
for (callback in synchronized(callbacks) { callbacks.toList() }) parcel.dispatch(callback) for (callback in synchronized(callbacks) { callbacks.toList() }) parcel.dispatch(callback)
} }
@RequiresApi(28)
fun registerSoftApCallback(callback: WifiApManager.SoftApCallbackCompat) = synchronized(callbacks) { fun registerSoftApCallback(callback: WifiApManager.SoftApCallbackCompat) = synchronized(callbacks) {
val wasEmpty = callbacks.isEmpty() val wasEmpty = callbacks.isEmpty()
callbacks.add(callback) callbacks.add(callback)
@@ -141,7 +156,6 @@ object WifiApCommands {
null null
} else lastCallback } else lastCallback
}?.toSequence()?.forEach { it?.dispatch(callback) } }?.toSequence()?.forEach { it?.dispatch(callback) }
@RequiresApi(28)
fun unregisterSoftApCallback(callback: WifiApManager.SoftApCallbackCompat) = synchronized(callbacks) { fun unregisterSoftApCallback(callback: WifiApManager.SoftApCallbackCompat) = synchronized(callbacks) {
if (callbacks.remove(callback) && callbacks.isEmpty()) { if (callbacks.remove(callback) && callbacks.isEmpty()) {
rootCallbackJob!!.cancel() rootCallbackJob!!.cancel()
@@ -150,13 +164,29 @@ object WifiApCommands {
} }
@Parcelize @Parcelize
class GetConfiguration : RootCommand<SoftApConfigurationCompat> { @Deprecated("Use GetConfiguration instead", ReplaceWith("GetConfiguration"))
override suspend fun execute() = WifiApManager.configurationCompat @Suppress("DEPRECATION")
class GetConfigurationLegacy : RootCommand<android.net.wifi.WifiConfiguration?> {
override suspend fun execute() = WifiApManager.configurationLegacy
}
@Parcelize
@RequiresApi(30)
class GetConfiguration : RootCommand<SoftApConfiguration> {
override suspend fun execute() = WifiApManager.configuration
} }
@Parcelize @Parcelize
data class SetConfiguration(val configuration: SoftApConfigurationCompat) : RootCommand<ParcelableBoolean> { @Deprecated("Use SetConfiguration instead", ReplaceWith("SetConfiguration"))
override suspend fun execute() = ParcelableBoolean(WifiApManager.setConfigurationCompat(configuration)) @Suppress("DEPRECATION")
data class SetConfigurationLegacy(
val configuration: android.net.wifi.WifiConfiguration?,
) : RootCommand<ParcelableBoolean> {
override suspend fun execute() = ParcelableBoolean(WifiApManager.setConfiguration(configuration))
}
@Parcelize
@RequiresApi(30)
data class SetConfiguration(val configuration: SoftApConfiguration) : RootCommand<ParcelableBoolean> {
override suspend fun execute() = ParcelableBoolean(WifiApManager.setConfiguration(configuration))
} }
@Parcelize @Parcelize

View File

@@ -0,0 +1,12 @@
package be.mygod.vpnhotspot.util
import android.app.Activity
interface AppUpdate {
class IgnoredException(cause: Throwable?) : RuntimeException(cause)
val downloaded: Boolean? get() = null
val message: String? get() = null
val stalenessDays: Int? get() = null
fun updateForResult(activity: Activity, requestCode: Int): Unit = error("Update not supported")
}

View File

@@ -9,10 +9,10 @@ import timber.log.Timber
class ConstantLookup(private val prefix: String, private val lookup29: Array<out String?>, class ConstantLookup(private val prefix: String, private val lookup29: Array<out String?>,
private val clazz: () -> Class<*>) { private val clazz: () -> Class<*>) {
private val lookup by lazy { val lookup by lazy {
SparseArrayCompat<String>().apply { SparseArrayCompat<String>().apply {
for (field in clazz().declaredFields) try { for (field in clazz().declaredFields) try {
if (field.name.startsWith(prefix)) put(field.getInt(null), field.name) if (field?.type == Int::class.java && field.name.startsWith(prefix)) put(field.getInt(null), field.name)
} catch (e: Exception) { } catch (e: Exception) {
Timber.w(e) Timber.w(e)
} }
@@ -30,17 +30,15 @@ class ConstantLookup(private val prefix: String, private val lookup29: Array<out
} }
} }
@Suppress("FunctionName")
fun ConstantLookup(prefix: String, vararg lookup29: String?, clazz: () -> Class<*>) = fun ConstantLookup(prefix: String, vararg lookup29: String?, clazz: () -> Class<*>) =
ConstantLookup(prefix, lookup29, clazz) ConstantLookup(prefix, lookup29, clazz)
@Suppress("FunctionName")
inline fun <reified T> ConstantLookup(prefix: String, vararg lookup29: String?) = inline fun <reified T> ConstantLookup(prefix: String, vararg lookup29: String?) =
ConstantLookup(prefix, lookup29) { T::class.java } ConstantLookup(prefix, lookup29) { T::class.java }
class LongConstantLookup(private val clazz: Class<*>, private val prefix: String) { class LongConstantLookup(private val clazz: Class<*>, private val prefix: String) {
private val lookup = LongSparseArray<String>().apply { private val lookup = LongSparseArray<String>().apply {
for (field in clazz.declaredFields) try { for (field in clazz.declaredFields) try {
if (field.name.startsWith(prefix)) put(field.getLong(null), field.name) if (field.type == Long::class.java && field.name.startsWith(prefix)) put(field.getLong(null), field.name)
} catch (e: Exception) { } catch (e: Exception) {
Timber.w(e) Timber.w(e)
} }

View File

@@ -1,12 +1,10 @@
package be.mygod.vpnhotspot.util package be.mygod.vpnhotspot.util
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
@SuppressLint("Registered") @SuppressLint("Registered")
@TargetApi(24)
class DeviceStorageApp(context: Context) : Application() { class DeviceStorageApp(context: Context) : Application() {
init { init {
attachBaseContext(context.createDeviceProtectedStorageContext()) attachBaseContext(context.createDeviceProtectedStorageContext())

View File

@@ -1,14 +1,15 @@
package be.mygod.vpnhotspot.util package be.mygod.vpnhotspot.util
import android.content.ComponentName import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.os.Build import android.os.Build
import android.os.DeadObjectException
import android.os.IBinder import android.os.IBinder
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import androidx.annotation.RequiresApi import be.mygod.vpnhotspot.BootReceiver
@RequiresApi(24)
abstract class KillableTileService : TileService(), ServiceConnection { abstract class KillableTileService : TileService(), ServiceConnection {
protected var tapPending = false protected var tapPending = false
@@ -25,4 +26,10 @@ abstract class KillableTileService : TileService(), ServiceConnection {
onClick() onClick()
} }
} }
override fun onBind(intent: Intent?) = try {
super.onBind(intent)
} catch (_: DeadObjectException) {
null
}.also { BootReceiver.startIfEnabled() }
} }

View File

@@ -0,0 +1,43 @@
package be.mygod.vpnhotspot.util
object RangeInput {
fun toString(input: IntArray) = StringBuilder().apply {
if (input.isEmpty()) return@apply
input.sort()
var pending: Int? = null
var last = input[0]
append(last)
for (channel in input.asSequence().drop(1)) {
if (channel == last + 1) pending = channel else {
pending?.let {
append('-')
append(it)
pending = null
}
append(",\u200b") // zero-width space to save space
append(channel)
}
last = channel
}
pending?.let {
append('-')
append(it)
}
}.toString()
fun toString(input: Set<Int>?) = input?.run { toString(toIntArray()) }
fun fromString(input: CharSequence?, min: Int = 1, max: Int = 999) = mutableSetOf<Int>().apply {
if (input == null) return@apply
for (unit in input.split(',')) {
if (unit.isBlank()) continue
val blocks = unit.split('-', limit = 2).map { i ->
i.trim { it == '\u200b' || it.isWhitespace() }.toInt()
}
require(blocks[0] in min..max) { "Out of range: ${blocks[0]}" }
if (blocks.size == 2) {
require(blocks[1] in min..max) { "Out of range: ${blocks[1]}" }
addAll(blocks[0]..blocks[1])
} else add(blocks[0])
}
}
}

View File

@@ -28,8 +28,8 @@ class RootSession : AutoCloseable {
private var server: RootServer? = runBlocking { RootManager.acquire() } private var server: RootServer? = runBlocking { RootManager.acquire() }
override fun close() { override fun close() {
server = null
server?.let { runBlocking { RootManager.release(it) } } server?.let { runBlocking { RootManager.release(it) } }
server = null
} }
/** /**

View File

@@ -2,8 +2,11 @@ package be.mygod.vpnhotspot.util
import android.content.Context import android.content.Context
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkRequest
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
import android.net.wifi.p2p.WifiP2pManager import android.net.wifi.p2p.WifiP2pManager
import android.os.Handler
import android.os.Looper
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import timber.log.Timber import timber.log.Timber
@@ -14,6 +17,7 @@ object Services {
contextInit = context contextInit = context
} }
val mainHandler by lazy { Handler(Looper.getMainLooper()) }
val connectivity by lazy { context.getSystemService<ConnectivityManager>()!! } val connectivity by lazy { context.getSystemService<ConnectivityManager>()!! }
val p2p by lazy { val p2p by lazy {
try { try {
@@ -24,4 +28,7 @@ object Services {
} }
} }
val wifi by lazy { context.getSystemService<WifiManager>()!! } val wifi by lazy { context.getSystemService<WifiManager>()!! }
fun registerNetworkCallback(request: NetworkRequest, networkCallback: ConnectivityManager.NetworkCallback) =
connectivity.registerNetworkCallback(request, networkCallback, mainHandler)
} }

View File

@@ -1,10 +1,12 @@
package be.mygod.vpnhotspot.util package be.mygod.vpnhotspot.util
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.net.MacAddress
import android.net.wifi.SoftApConfiguration import android.net.wifi.SoftApConfiguration
import android.net.wifi.p2p.WifiP2pConfig import android.net.wifi.p2p.WifiP2pConfig
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import timber.log.Timber import be.mygod.vpnhotspot.App.Companion.app
import me.weishu.reflection.Reflection
/** /**
* The central object for accessing all the useful blocked APIs. Thanks Google! * The central object for accessing all the useful blocked APIs. Thanks Google!
@@ -12,26 +14,19 @@ import timber.log.Timber
* Lazy cannot be used directly as it will create inner classes. * Lazy cannot be used directly as it will create inner classes.
*/ */
@SuppressLint("BlockedPrivateApi", "DiscouragedPrivateApi") @SuppressLint("BlockedPrivateApi", "DiscouragedPrivateApi")
@Suppress("FunctionName")
object UnblockCentral { object UnblockCentral {
var needInit = true
/** /**
* Retrieve this property before doing dangerous shit. * Retrieve this property before doing dangerous shit.
*/ */
@get:RequiresApi(28) private val init by lazy { if (needInit) check(Reflection.unseal(app.deviceStorage) == 0) }
private val init by lazy {
try {
Class.forName("dalvik.system.VMDebug").getDeclaredMethod("allowHiddenApiReflectionFrom", Class::class.java)
.invoke(null, UnblockCentral::class.java)
true
} catch (e: ReflectiveOperationException) {
Timber.w(e)
false
}
}
@RequiresApi(31) @RequiresApi(33)
fun setUserConfiguration(clazz: Class<*>) = init.let { fun getCountryCode(clazz: Class<*>) = init.let { clazz.getDeclaredMethod("getCountryCode") }
clazz.getDeclaredMethod("setUserConfiguration", Boolean::class.java)
@RequiresApi(33)
fun setRandomizedMacAddress(clazz: Class<*>) = init.let {
clazz.getDeclaredMethod("setRandomizedMacAddress", MacAddress::class.java)
} }
@get:RequiresApi(31) @get:RequiresApi(31)

View File

@@ -1,23 +1,16 @@
package be.mygod.vpnhotspot.util package be.mygod.vpnhotspot.util
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.content.* import android.content.*
import android.content.res.Resources import android.content.res.Resources
import android.net.InetAddresses import android.net.*
import android.net.LinkProperties
import android.net.RouteInfo
import android.os.Build import android.os.Build
import android.os.RemoteException import android.os.RemoteException
import android.system.ErrnoException
import android.system.Os
import android.system.OsConstants
import android.text.* import android.text.*
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.RequiresApi
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.databinding.BindingAdapter import androidx.databinding.BindingAdapter
@@ -26,18 +19,23 @@ import androidx.fragment.app.FragmentManager
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.MacAddressCompat import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import timber.log.Timber import timber.log.Timber
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.lang.invoke.MethodHandles import java.lang.invoke.MethodHandles
import java.lang.reflect.InvocationHandler import java.lang.reflect.InvocationHandler
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method import java.lang.reflect.Method
import java.net.HttpURLConnection
import java.net.InetAddress import java.net.InetAddress
import java.net.NetworkInterface import java.net.NetworkInterface
import java.net.SocketException import java.net.SocketException
import java.util.* import java.net.URL
import java.util.Locale
import kotlin.coroutines.resumeWithException
tailrec fun Throwable.getRootCause(): Throwable { tailrec fun Throwable.getRootCause(): Throwable {
if (this is InvocationTargetException || this is RemoteException) return (cause ?: return this).getRootCause() if (this is InvocationTargetException || this is RemoteException) return (cause ?: return this).getRootCause()
@@ -55,6 +53,10 @@ fun Long.toPluralInt(): Int {
return (this % 1000000000).toInt() + 1000000000 return (this % 1000000000).toInt() + 1000000000
} }
fun Method.matches(name: String, vararg classes: Class<*>) = this.name == name && parameterCount == classes.size &&
classes.indices.all { i -> parameters[i].type == classes[i] }
inline fun <reified T> Method.matches1(name: String) = matches(name, T::class.java)
fun Context.ensureReceiverUnregistered(receiver: BroadcastReceiver) { fun Context.ensureReceiverUnregistered(receiver: BroadcastReceiver) {
try { try {
unregisterReceiver(receiver) unregisterReceiver(receiver)
@@ -143,12 +145,13 @@ fun makeIpSpan(ip: InetAddress) = ip.hostAddress.let {
} }
} }
fun makeMacSpan(mac: String) = if (app.hasTouch) SpannableString(mac).apply { fun makeMacSpan(mac: String) = if (app.hasTouch) SpannableString(mac).apply {
setSpan(CustomTabsUrlSpan("https://macvendors.co/results/$mac"), 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) setSpan(CustomTabsUrlSpan("https://maclookup.app/search/result?mac=$mac"), 0, length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
} else mac } else mac
fun NetworkInterface.formatAddresses(macOnly: Boolean = false) = SpannableStringBuilder().apply { fun NetworkInterface.formatAddresses(macOnly: Boolean = false) = SpannableStringBuilder().apply {
try { try {
val address = hardwareAddress?.let(MacAddressCompat::fromBytes) val address = hardwareAddress?.let(MacAddress::fromBytes)
if (address != null && address != MacAddressCompat.ANY_ADDRESS) appendLine(makeMacSpan(address.toString())) if (address != null && address != MacAddressCompat.ANY_ADDRESS) appendLine(makeMacSpan(address.toString()))
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
Timber.w(e) Timber.w(e)
@@ -200,8 +203,7 @@ fun Resources.findIdentifier(name: String, defType: String, defPackage: String,
if (alternativePackage != null && it == 0) getIdentifier(name, defType, alternativePackage) else it if (alternativePackage != null && it == 0) getIdentifier(name, defType, alternativePackage) else it
} }
@get:RequiresApi(26) private val newLookup by lazy {
private val newLookup by lazy @TargetApi(26) {
MethodHandles.Lookup::class.java.getDeclaredConstructor(Class::class.java, Int::class.java).apply { MethodHandles.Lookup::class.java.getDeclaredConstructor(Class::class.java, Int::class.java).apply {
isAccessible = true isAccessible = true
} }
@@ -213,8 +215,12 @@ private val newLookup by lazy @TargetApi(26) {
* See also: https://stackoverflow.com/a/49532463/2245107 * See also: https://stackoverflow.com/a/49532463/2245107
*/ */
fun InvocationHandler.callSuper(interfaceClass: Class<*>, proxy: Any, method: Method, args: Array<out Any?>?) = when { fun InvocationHandler.callSuper(interfaceClass: Class<*>, proxy: Any, method: Method, args: Array<out Any?>?) = when {
Build.VERSION.SDK_INT >= 26 && method.isDefault -> newLookup.newInstance(interfaceClass, 0xf) // ALL_MODES method.isDefault -> try {
.`in`(interfaceClass).unreflectSpecial(method, interfaceClass).bindTo(proxy).run { newLookup.newInstance(interfaceClass, 0xf) // ALL_MODES
} catch (e: ReflectiveOperationException) {
Timber.w(e)
MethodHandles.lookup().`in`(interfaceClass)
}.unreflectSpecial(method, interfaceClass).bindTo(proxy).run {
if (args == null) invokeWithArguments() else invokeWithArguments(*args) if (args == null) invokeWithArguments() else invokeWithArguments(*args)
} }
// otherwise, we just redispatch it to InvocationHandler // otherwise, we just redispatch it to InvocationHandler
@@ -234,13 +240,26 @@ fun InvocationHandler.callSuper(interfaceClass: Class<*>, proxy: Any, method: Me
} }
} }
@Suppress("FunctionName") fun globalNetworkRequestBuilder() = NetworkRequest.Builder().apply {
fun if_nametoindex(ifname: String) = if (Build.VERSION.SDK_INT >= 26) { if (Build.VERSION.SDK_INT >= 31) setIncludeOtherUidNetworks(true)
Os.if_nametoindex(ifname) }
} else try {
File("/sys/class/net/$ifname/ifindex").inputStream().bufferedReader().use { it.readLine().trim().toInt() } suspend fun <T> connectCancellable(url: String, block: suspend (HttpURLConnection) -> T): T {
} catch (_: FileNotFoundException) { @Suppress("BlockingMethodInNonBlockingContext")
NetworkInterface.getByName(ifname)?.index ?: 0 val conn = URL(url).openConnection() as HttpURLConnection
} catch (e: IOException) { return suspendCancellableCoroutine { cont ->
if ((e.cause as? ErrnoException)?.errno == OsConstants.ENODEV) 0 else throw e val job = GlobalScope.launch(Dispatchers.IO) {
try {
cont.resume(block(conn)) { cont.resumeWithException(it) }
} catch (e: Throwable) {
cont.resumeWithException(e)
} finally {
conn.disconnect()
}
}
cont.invokeOnCancellation {
job.cancel(it as? CancellationException)
conn.disconnect()
}
}
} }

View File

@@ -5,14 +5,15 @@ import android.graphics.Rect
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import androidx.appcompat.widget.AppCompatAutoCompleteTextView import androidx.appcompat.widget.AppCompatAutoCompleteTextView
import be.mygod.vpnhotspot.R
/** /**
* Based on: https://gist.github.com/furycomptuers/4961368 * Based on: https://gist.github.com/furycomptuers/4961368
*/ */
class AlwaysAutoCompleteEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, class AlwaysAutoCompleteEditText @JvmOverloads constructor(
defStyleAttr: Int = R.attr.autoCompleteTextViewStyle) : context: Context,
AppCompatAutoCompleteTextView(context, attrs, defStyleAttr) { attrs: AttributeSet? = null,
defStyleAttr: Int = androidx.appcompat.R.attr.autoCompleteTextViewStyle,
) : AppCompatAutoCompleteTextView(context, attrs, defStyleAttr) {
override fun enoughToFilter() = true override fun enoughToFilter() = true
override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) { override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) {

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M21,10.12h-6.78l2.74,-2.82c-2.73,-2.7 -7.15,-2.8 -9.88,-0.1c-2.73,2.71 -2.73,7.08 0,9.79s7.15,2.71 9.88,0C18.32,15.65 19,14.08 19,12.1h2c0,1.98 -0.88,4.55 -2.64,6.29c-3.51,3.48 -9.21,3.48 -12.72,0c-3.5,-3.47 -3.53,-9.11 -0.02,-12.58s9.14,-3.47 12.65,0L21,3V10.12zM12.5,8v4.25l3.5,2.08l-0.72,1.21L11,13V8H12.5z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,4L5,4c-1.11,0 -2,0.9 -2,2v12c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.9,-2 -2,-2zM11,11L9.5,11v-0.5h-2v3h2L9.5,13L11,13v1c0,0.55 -0.45,1 -1,1L7,15c-0.55,0 -1,-0.45 -1,-1v-4c0,-0.55 0.45,-1 1,-1h3c0.55,0 1,0.45 1,1v1zM18,11h-1.5v-0.5h-2v3h2L16.5,13L18,13v1c0,0.55 -0.45,1 -1,1h-3c-0.55,0 -1,-0.45 -1,-1v-4c0,-0.55 0.45,-1 1,-1h3c0.55,0 1,0.45 1,1v1z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19.5,5.5v13h-15v-13h15zM19,4L5,4c-1.11,0 -2,0.9 -2,2v12c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.9,-2 -2,-2zM11,11L9.5,11v-0.5h-2v3h2L9.5,13L11,13v1c0,0.55 -0.45,1 -1,1L7,15c-0.55,0 -1,-0.45 -1,-1v-4c0,-0.55 0.45,-1 1,-1h3c0.55,0 1,0.45 1,1v1zM18,11h-1.5v-0.5h-2v3h2L16.5,13L18,13v1c0,0.55 -0.45,1 -1,1h-3c-0.55,0 -1,-0.45 -1,-1v-4c0,-0.55 0.45,-1 1,-1h3c0.55,0 1,0.45 1,1v1z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M18.32,4.26C16.84,3.05 15.01,2.25 13,2.05v2.02c1.46,0.18 2.79,0.76 3.9,1.62L18.32,4.26zM19.93,11h2.02c-0.2,-2.01 -1,-3.84 -2.21,-5.32L18.31,7.1C19.17,8.21 19.75,9.54 19.93,11zM18.31,16.9l1.43,1.43c1.21,-1.48 2.01,-3.32 2.21,-5.32h-2.02C19.75,14.46 19.17,15.79 18.31,16.9zM13,19.93v2.02c2.01,-0.2 3.84,-1 5.32,-2.21l-1.43,-1.43C15.79,19.17 14.46,19.75 13,19.93zM13,12V7h-2v5H7l5,5l5,-5H13zM11,19.93v2.02c-5.05,-0.5 -9,-4.76 -9,-9.95s3.95,-9.45 9,-9.95v2.02C7.05,4.56 4,7.92 4,12S7.05,19.44 11,19.93z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="108dp" android:viewportHeight="108.0"
android:viewportWidth="108.0" android:width="108dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#7000" android:pathData="M48,50a4,4 0,1 0,4 4A4,4 0,0 0,48 50ZM60,54A12,12 0,1 0,42 64.38l2,-3.48a8,8 0,1 1,8 0l2,3.48A12,12 0,0 0,60 54ZM48,34A20,20 0,0 0,38 71.3l2,-3.46a16,16 0,1 1,16 0l2,3.46A20,20 0,0 0,48 34Z"/>
<path android:fillColor="#000" android:pathData="M59.3,50a12,12 0,1 0,0 8H68v8h8V58h4V50ZM48,58a4,4 0,1 1,4 -4A4,4 0,0 1,48 58Z"/>
</vector>

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