Bump to minapi 28

This commit is contained in:
Mygod
2023-02-08 18:45:28 -05:00
parent 8ba727c1b8
commit 15c3c5a6d7
89 changed files with 361 additions and 809 deletions

View File

@@ -1,7 +1,7 @@
# 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://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) [![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)
@@ -10,10 +10,10 @@
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**)
| 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)) | | 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 | ✓ | | Auto update | Email updates via watching releases | ✓ |
| In-app update channel | GitHub | Google Play | | In-app update channel | GitHub | Google Play |
| [Sponsor/Donation](https://github.com/sponsors/Mygod) | | Google Play In-App Purchases only | | [Sponsor/Donation](https://github.com/sponsors/Mygod) | | Google Play In-App Purchases only |
This app is useful for: This app is useful for:
@@ -73,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.
@@ -82,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.
@@ -164,43 +164,41 @@ Greylisted/blacklisted APIs or internal constants: (some constants are hardcoded
* (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,unsupported` * `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 26) `Ljava/lang/invoke/MethodHandles$Lookup;-><init>(Ljava/lang/Class;I)V,unsupported` * `Ljava/lang/invoke/MethodHandles$Lookup;-><init>(Ljava/lang/Class;I)V,unsupported`
* (since API 26) `Ljava/lang/invoke/MethodHandles$Lookup;->ALL_MODES:I,lo-prio,max-target-o` * `Ljava/lang/invoke/MethodHandles$Lookup;->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`
@@ -217,13 +215,13 @@ 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 24) `Landroid/net/TetheringManager;->TETHERING_USB:I,sdk,system-api,test-api` * `Landroid/net/TetheringManager;->TETHERING_USB:I,sdk,system-api,test-api`
* (since API 24) `Landroid/net/TetheringManager;->TETHERING_WIFI:I,sdk,system-api,test-api` * `Landroid/net/TetheringManager;->TETHERING_WIFI: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`
@@ -308,25 +306,25 @@ 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`
* (since API 23) `Landroid/net/wifi/WifiManager;->EXTRA_WIFI_AP_FAILURE_REASON:Ljava/lang/String;,sdk,system-api,test-api` * `Landroid/net/wifi/WifiManager;->EXTRA_WIFI_AP_FAILURE_REASON:Ljava/lang/String;,sdk,system-api,test-api`
* (since API 26) `Landroid/net/wifi/WifiManager;->EXTRA_WIFI_AP_INTERFACE_NAME: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`
* (since API 23) `Landroid/net/wifi/WifiManager;->EXTRA_WIFI_AP_STATE: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 23) `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 23) `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_CHANGED_ACTION:Ljava/lang/String;,sdk,system-api,test-api` * `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_CHANGED_ACTION:Ljava/lang/String;,sdk,system-api,test-api`
* (since API 23) `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_DISABLED:I,sdk,system-api,test-api` * `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_DISABLED:I,sdk,system-api,test-api`
* (since API 23) `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_DISABLING:I,sdk,system-api,test-api` * `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_DISABLING:I,sdk,system-api,test-api`
* (since API 23) `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_ENABLED:I,sdk,system-api,test-api` * `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_ENABLED:I,sdk,system-api,test-api`
* (since API 23) `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_ENABLING:I,sdk,system-api,test-api` * `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_ENABLING:I,sdk,system-api,test-api`
* (since API 23) `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_FAILED: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 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`
@@ -351,15 +349,14 @@ Nonexported system resources:
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

@@ -22,7 +22,7 @@ android {
kotlinOptions.jvmTarget = javaVersion.toString() kotlinOptions.jvmTarget = javaVersion.toString()
defaultConfig { defaultConfig {
applicationId = "be.mygod.vpnhotspot" applicationId = "be.mygod.vpnhotspot"
minSdk = 21 minSdk = 28
targetSdk = 33 targetSdk = 33
resourceConfigurations.addAll(arrayOf("it", "pt-rBR", "ru", "zh-rCN", "zh-rTW")) resourceConfigurations.addAll(arrayOf("it", "pt-rBR", "ru", "zh-rCN", "zh-rTW"))
versionCode = 1000 versionCode = 1000

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

@@ -103,7 +103,7 @@ object UpdateChecker {
} catch (e: Exception) { } catch (e: Exception) {
Timber.w(e) Timber.w(e)
} finally { } finally {
conn.disconnectCompat() conn.disconnect()
putLong(KEY_LAST_FETCHED, System.currentTimeMillis()) putLong(KEY_LAST_FETCHED, System.currentTimeMillis())
} }
} }

View File

@@ -90,8 +90,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"
@@ -107,8 +106,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>
@@ -119,12 +117,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>
@@ -138,8 +134,7 @@
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>
@@ -153,8 +148,7 @@
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>
@@ -168,8 +162,7 @@
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>
@@ -193,23 +186,6 @@
android:name="android.service.quicksettings.TOGGLEABLE_TILE" android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" /> android:value="true" />
</service> </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>
<meta-data
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
</service>
<receiver <receiver
android:name=".BootReceiver" android:name=".BootReceiver"

View File

@@ -15,7 +15,6 @@ 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.preference.PreferenceManager import androidx.preference.PreferenceManager
import be.mygod.librootkotlinx.NoShellException import be.mygod.librootkotlinx.NoShellException
@@ -46,13 +45,11 @@ class App : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
app = this app = this
if (Build.VERSION.SDK_INT >= 24) @SuppressLint("RestrictedApi") { deviceStorage = DeviceStorageApp(this)
deviceStorage = DeviceStorageApp(this) // alternative to PreferenceManager.getDefaultSharedPreferencesName(this)
// alternative to PreferenceManager.getDefaultSharedPreferencesName(this) deviceStorage.moveSharedPreferencesFrom(this, PreferenceManager(this).sharedPreferencesName)
deviceStorage.moveSharedPreferencesFrom(this, PreferenceManager(this).sharedPreferencesName) deviceStorage.moveDatabaseFrom(this, AppDatabase.DB_NAME)
deviceStorage.moveDatabaseFrom(this, AppDatabase.DB_NAME) BootReceiver.migrateIfNecessary()
BootReceiver.migrateIfNecessary()
} else deviceStorage = this
Services.init { this } Services.init { this }
// overhead of debug mode is minimal: https://github.com/Kotlin/kotlinx.coroutines/blob/f528898/docs/debugging.md#debug-mode // overhead of debug mode is minimal: https://github.com/Kotlin/kotlinx.coroutines/blob/f528898/docs/debugging.md#debug-mode
@@ -62,7 +59,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() {
@@ -115,19 +112,13 @@ class App : Application() {
* https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/53e0284/service/java/com/android/server/wifi/WifiSettingsStore.java#228 * 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) { inline fun <reified T> startServiceWithLocation(context: Context) {
val canStart = Build.VERSION.SDK_INT >= 33 || if (Build.VERSION.SDK_INT >= 28) { if (Build.VERSION.SDK_INT < 33 && location?.isLocationEnabled != true) try {
location?.isLocationEnabled == true
} else @Suppress("DEPRECATION") {
Settings.Secure.getInt(context.contentResolver, Settings.Secure.LOCATION_MODE,
Settings.Secure.LOCATION_MODE_OFF) != Settings.Secure.LOCATION_MODE_OFF
}
if (canStart) ContextCompat.startForegroundService(context, Intent(context, T::class.java)) else try {
context.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)) context.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
Toast.makeText(context, R.string.tethering_location_off, Toast.LENGTH_LONG).show() Toast.makeText(context, R.string.tethering_location_off, Toast.LENGTH_LONG).show()
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
app.logEvent("location_settings") { param("message", e.toString()) } app.logEvent("location_settings") { param("message", e.toString()) }
SmartSnackbar.make(R.string.tethering_location_off).show() SmartSnackbar.make(R.string.tethering_location_off).show()
} } else context.startForegroundService(Intent(context, T::class.java))
} }
lateinit var deviceStorage: Application lateinit var deviceStorage: Application
@@ -145,10 +136,10 @@ class App : Application() {
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

@@ -6,7 +6,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Parcelable import android.os.Parcelable
import androidx.annotation.RequiresApi
import be.mygod.librootkotlinx.toByteArray import be.mygod.librootkotlinx.toByteArray
import be.mygod.librootkotlinx.toParcelable import be.mygod.librootkotlinx.toParcelable
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
@@ -74,7 +73,6 @@ class BootReceiver : BroadcastReceiver() {
inline fun <reified T> add(value: Startable) = add(T::class.java.name, value) inline fun <reified T> add(value: Startable) = add(T::class.java.name, value)
inline fun <reified T> delete() = delete(T::class.java.name) inline fun <reified T> delete() = delete(T::class.java.name)
@RequiresApi(24)
fun migrateIfNecessary() { fun migrateIfNecessary() {
val oldFile = File(app.noBackupFilesDir, FILENAME) val oldFile = File(app.noBackupFilesDir, FILENAME)
if (oldFile.canRead()) try { if (oldFile.canRead()) try {

View File

@@ -94,7 +94,6 @@ class EBegFragment : AppCompatDialogFragment() {
}.build())) }.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/")
} }

View File

@@ -5,7 +5,6 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi
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.net.monitor.TetherTimeoutMonitor import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
@@ -21,7 +20,6 @@ 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 {
inner class Binder : android.os.Binder() { inner class Binder : android.os.Binder() {
/** /**
@@ -162,7 +160,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
}) })
} }
@@ -183,10 +181,8 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
private fun unregisterReceiver(exit: Boolean = false) { private fun unregisterReceiver(exit: Boolean = false) {
IpNeighbourMonitor.unregisterCallback(this) IpNeighbourMonitor.unregisterCallback(this)
if (Build.VERSION.SDK_INT >= 28) { timeoutMonitor?.close()
timeoutMonitor?.close() timeoutMonitor = null
timeoutMonitor = null
}
launch { launch {
routingManager?.stop() routingManager?.stop()
routingManager = null routingManager = null

View File

@@ -4,7 +4,6 @@ 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.graphics.Insets
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
@@ -50,16 +49,14 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
setContentView(binding.root) setContentView(binding.root)
binding.navigation.setOnItemSelectedListener(this) binding.navigation.setOnItemSelectedListener(this)
val badge = binding.navigation.getOrCreateBadge(R.id.navigation_clients).apply { val badge = binding.navigation.getOrCreateBadge(R.id.navigation_clients).apply {
backgroundColor = ContextCompat.getColor(this@MainActivity, R.color.colorSecondary) backgroundColor = resources.getColor(R.color.colorSecondary, theme)
badgeTextColor = ContextCompat.getColor(this@MainActivity, badgeTextColor = resources.getColor(androidx.appcompat.R.color.primary_text_default_material_light, theme)
androidx.appcompat.R.color.primary_text_default_material_light)
} }
updateItem = binding.navigation.menu.findItem(R.id.navigation_update) updateItem = binding.navigation.menu.findItem(R.id.navigation_update)
updateItem.isCheckable = false updateItem.isCheckable = false
updateBadge = binding.navigation.getOrCreateBadge(R.id.navigation_update).apply { updateBadge = binding.navigation.getOrCreateBadge(R.id.navigation_update).apply {
backgroundColor = ContextCompat.getColor(this@MainActivity, R.color.colorSecondary) backgroundColor = resources.getColor(R.color.colorSecondary, theme)
badgeTextColor = ContextCompat.getColor(this@MainActivity, badgeTextColor = resources.getColor(androidx.appcompat.R.color.primary_text_default_material_light, theme)
androidx.appcompat.R.color.primary_text_default_material_light)
} }
if (savedInstanceState == null) displayFragment(TetheringFragment()) if (savedInstanceState == null) displayFragment(TetheringFragment())
val model by viewModels<ClientViewModel>() val model by viewModels<ClientViewModel>()

View File

@@ -8,6 +8,7 @@ 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.location.LocationManager
import android.net.MacAddress
import android.net.wifi.ScanResult 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.*
@@ -16,10 +17,10 @@ import android.os.Looper
import android.provider.Settings import android.provider.Settings
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
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.VendorElements
@@ -98,17 +99,19 @@ 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) @get:RequiresApi(33)
@set:RequiresApi(33) @set:RequiresApi(33)
var vendorElements: List<ScanResult.InformationElement> var vendorElements: List<ScanResult.InformationElement>
@@ -128,19 +131,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
@@ -152,7 +153,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
@@ -220,7 +221,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
@Parcelize @Parcelize
class Starter : BootReceiver.Startable { class Starter : BootReceiver.Startable {
override fun start(context: Context) { override fun start(context: Context) {
ContextCompat.startForegroundService(context, Intent(context, RepeaterService::class.java)) context.startForegroundService(Intent(context, RepeaterService::class.java))
} }
} }
@@ -400,7 +401,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
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 {
val filter = 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)
@@ -479,7 +480,7 @@ 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)
} }
} }
@@ -554,10 +555,8 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
p2pPoller?.cancel() 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
@@ -574,7 +573,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
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()

View File

@@ -1,13 +1,8 @@
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.app.ServiceCompat
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.*
@@ -25,16 +20,15 @@ object ServiceNotification {
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 isInactive = inactive.isNotEmpty() && deviceCounts.isEmpty()
val builder = NotificationCompat.Builder(context, if (isInactive) CHANNEL_INACTIVE else CHANNEL_ACTIVE).apply { val builder = Notification.Builder(context, if (isInactive) CHANNEL_INACTIVE else CHANNEL_ACTIVE).apply {
setWhen(0) setWhen(0)
setCategory(NotificationCompat.CATEGORY_SERVICE) setCategory(Notification.CATEGORY_SERVICE)
color = ContextCompat.getColor(context, R.color.colorPrimary) setColor(context.resources.getColor(R.color.colorPrimary, context.theme))
setContentTitle(context.getText(R.string.notification_tethering_title)) setContentTitle(context.getText(R.string.notification_tethering_title))
setSmallIcon(R.drawable.ic_quick_settings_tile_on) setSmallIcon(R.drawable.ic_quick_settings_tile_on)
setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
setVisibility(NotificationCompat.VISIBILITY_PUBLIC) setVisibility(Notification.VISIBILITY_PUBLIC)
priority = if (isInactive) NotificationCompat.PRIORITY_MIN else NotificationCompat.PRIORITY_LOW
} }
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)
@@ -45,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()!!
} }
} }
@@ -65,26 +59,23 @@ object ServiceNotification {
fun stopForeground(service: Service) = synchronized(this) { fun stopForeground(service: Service) = synchronized(this) {
deviceCountsMap.remove(service) ?: return@synchronized deviceCountsMap.remove(service) ?: return@synchronized
val shutdown = deviceCountsMap.isEmpty() val shutdown = deviceCountsMap.isEmpty()
ServiceCompat.stopForeground(service, service.stopForeground(if (shutdown) Service.STOP_FOREGROUND_REMOVE else Service.STOP_FOREGROUND_DETACH)
if (shutdown) ServiceCompat.STOP_FOREGROUND_REMOVE else ServiceCompat.STOP_FOREGROUND_DETACH)
if (!shutdown) manager.notify(NOTIFICATION_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,
NotificationChannel(CHANNEL_ACTIVE, app.getText(R.string.notification_channel_tethering), NotificationManager.IMPORTANCE_LOW).apply {
app.getText(R.string.notification_channel_tethering), NotificationManager.IMPORTANCE_LOW).apply { lockscreenVisibility = Notification.VISIBILITY_PUBLIC
lockscreenVisibility = Notification.VISIBILITY_PUBLIC manager.createNotificationChannel(this)
manager.createNotificationChannel(this)
}
NotificationChannel(CHANNEL_INACTIVE,
app.getText(R.string.notification_channel_monitor), NotificationManager.IMPORTANCE_LOW).apply {
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
manager.createNotificationChannel(this)
}
// remove old service channels
manager.deleteNotificationChannel("hotspot")
manager.deleteNotificationChannel("repeater")
} }
NotificationChannel(CHANNEL_INACTIVE,
app.getText(R.string.notification_channel_monitor), NotificationManager.IMPORTANCE_LOW).apply {
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
manager.createNotificationChannel(this)
}
// remove old service channels
manager.deleteNotificationChannel("hotspot")
manager.deleteNotificationChannel("repeater")
} }
} }

View File

@@ -47,24 +47,22 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
SummaryFallbackProvider(findPreference(UpstreamMonitor.KEY)!!) SummaryFallbackProvider(findPreference(UpstreamMonitor.KEY)!!)
SummaryFallbackProvider(findPreference(FallbackUpstreamMonitor.KEY)!!) SummaryFallbackProvider(findPreference(FallbackUpstreamMonitor.KEY)!!)
findPreference<TwoStatePreference>("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.launchWhenCreated { isEnabled = false
isEnabled = false try {
try { TetherOffloadManager.setEnabled(newValue as Boolean)
TetherOffloadManager.setEnabled(newValue as Boolean) } catch (_: CancellationException) {
} catch (_: CancellationException) { } catch (e: Exception) {
} catch (e: Exception) { Timber.w(e)
Timber.w(e) SmartSnackbar.make(e).show()
SmartSnackbar.make(e).show()
}
isChecked = TetherOffloadManager.enabled
isEnabled = true
} }
false isChecked = TetherOffloadManager.enabled
isEnabled = true
} }
} else parent!!.removePreference(this) false
}
} }
findPreference<TwoStatePreference>(BootReceiver.KEY)!!.setOnPreferenceChangeListener { _, value -> findPreference<TwoStatePreference>(BootReceiver.KEY)!!.setOnPreferenceChangeListener { _, value ->
BootReceiver.onUserSettingUpdated(value as Boolean) BootReceiver.onUserSettingUpdated(value as Boolean)

View File

@@ -2,9 +2,7 @@ package be.mygod.vpnhotspot
import android.content.Context 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 androidx.core.content.ContextCompat
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
import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.TetheringManager
@@ -46,7 +44,7 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
@Parcelize @Parcelize
data class Starter(val monitored: ArrayList<String>) : BootReceiver.Startable { data class Starter(val monitored: ArrayList<String>) : BootReceiver.Startable {
override fun start(context: Context) { override fun start(context: Context) {
ContextCompat.startForegroundService(context, Intent(context, TetheringService::class.java).apply { context.startForegroundService(Intent(context, TetheringService::class.java).apply {
putStringArrayListExtra(EXTRA_ADD_INTERFACES_MONITOR, monitored) putStringArrayListExtra(EXTRA_ADD_INTERFACES_MONITOR, monitored)
}) })
} }
@@ -113,7 +111,7 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
BootReceiver.startIfEnabled() 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()) {

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
@@ -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,6 +3,7 @@ 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.Build
import android.os.IBinder import android.os.IBinder
@@ -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
} }
@@ -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
@@ -30,7 +31,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,18 +41,21 @@ 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(getText(R.string.clients_nickname_set_to_vendor), listener) setNeutralButton(getText(R.string.clients_nickname_set_to_vendor), listener)
@@ -64,7 +67,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
@@ -135,7 +138,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
} }
@@ -160,7 +163,7 @@ class ClientsFragment : Fragment() {
withContext(Dispatchers.Unconfined) { withContext(Dispatchers.Unconfined) {
StatsDialogFragment().apply { StatsDialogFragment().apply {
arg(StatsArg(client.title.value ?: return@withContext, arg(StatsArg(client.title.value ?: return@withContext,
AppDatabase.instance.trafficRecordDao.queryStats(client.mac.addr))) AppDatabase.instance.trafficRecordDao.queryStats(client.mac)))
}.showAllowingStateLoss(parentFragmentManager) }.showAllowingStateLoss(parentFragmentManager)
} }
} }
@@ -201,9 +204,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 +219,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)

View File

@@ -1,12 +1,11 @@
package be.mygod.vpnhotspot.client package be.mygod.vpnhotspot.client
import android.content.Context import android.content.Context
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.disconnectCompat
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@@ -22,25 +21,26 @@ import java.net.URL
* 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 val macLookupBusy = mutableMapOf<MacAddress, Pair<HttpURLConnection, 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)?.let { (conn, job) ->
job.cancel() job.cancel()
conn.disconnectCompat() 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 val conn = URL("https://macvendors.co/api/$mac").openConnection() as HttpURLConnection
macLookupBusy[mac] = conn to GlobalScope.launch(Dispatchers.IO) { macLookupBusy[mac] = conn to GlobalScope.launch(Dispatchers.IO) {
@@ -70,7 +70,7 @@ object MacLookup {
} }
} }
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("country"))?.also { return it }
val address = obj.optString("address") val address = obj.optString("address")
if (address.isBlank()) return null if (address.isBlank()) return null

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.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothProfile import android.bluetooth.BluetoothProfile
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
@@ -9,7 +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 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
@@ -38,7 +36,6 @@ class BluetoothTethering(context: Context, private val adapter: BluetoothAdapter
/** /**
* 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 -> {
@@ -109,7 +106,6 @@ class BluetoothTethering(context: Context, private val adapter: BluetoothAdapter
* https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/TetherSettings.java#384 * https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/TetherSettings.java#384
*/ */
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
@RequiresApi(24)
fun start(callback: TetheringManager.StartTetheringCallback, context: Context) { fun start(callback: TetheringManager.StartTetheringCallback, context: Context) {
if (pendingCallback == null) try { if (pendingCallback == null) try {
if (adapter.state == BluetoothAdapter.STATE_OFF) { if (adapter.state == BluetoothAdapter.STATE_OFF) {
@@ -123,7 +119,6 @@ class BluetoothTethering(context: Context, private val adapter: BluetoothAdapter
pendingCallback = null pendingCallback = null
} }
} }
@RequiresApi(24)
fun stop(callback: (Exception) -> Unit) { fun stop(callback: (Exception) -> Unit) {
TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH, callback) TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH, callback)
stoppedByUser = true stoppedByUser = true

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,14 +1,14 @@
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.content.Context
import android.content.ServiceConnection
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.view.View import android.view.View
import androidx.annotation.RequiresApi
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.BuildConfig
import be.mygod.vpnhotspot.LocalOnlyHotspotService import be.mygod.vpnhotspot.LocalOnlyHotspotService
import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
@@ -16,7 +16,6 @@ import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import be.mygod.vpnhotspot.util.formatAddresses import be.mygod.vpnhotspot.util.formatAddresses
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 = when { val permission = when {

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

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,7 +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_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
@@ -33,11 +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_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,6 +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.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
@@ -16,16 +17,18 @@ 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.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
@@ -34,7 +37,11 @@ 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
@@ -91,7 +98,7 @@ 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 parent.startRepeater.launch(if (Build.VERSION.SDK_INT >= 33) { } else parent.startRepeater.launch(if (Build.VERSION.SDK_INT >= 33) {
Manifest.permission.NEARBY_WIFI_DEVICES Manifest.permission.NEARBY_WIFI_DEVICES
} else Manifest.permission.ACCESS_FINE_LOCATION) } else Manifest.permission.ACCESS_FINE_LOCATION)
@@ -229,7 +236,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
} }

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

@@ -5,7 +5,6 @@ import android.annotation.TargetApi
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Parcelable import android.os.Parcelable
@@ -27,7 +26,6 @@ import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
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.wifi.* import be.mygod.vpnhotspot.net.wifi.*
import be.mygod.vpnhotspot.net.wifi.WifiApManager.wifiApState
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
@@ -57,7 +55,7 @@ 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
@@ -134,41 +132,29 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
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 val receiver = broadcastReceiver { _, intent ->
failureReason = if (intent.wifiApState == WifiApManager.WIFI_AP_STATE_FAILED) {
intent.getIntExtra(WifiApManager.EXTRA_WIFI_AP_FAILURE_REASON, 0)
} else null
data.notifyChange()
}
private var failureReason: Int? = null private var failureReason: Int? = null
private var numClients: Int? = null private var numClients: Int? = null
private var info = emptyList<Parcelable>() private var info = emptyList<Parcelable>()
private var capability: Parcelable? = null private var capability: Parcelable? = null
init { init {
if (Build.VERSION.SDK_INT >= 23) parent.viewLifecycleOwner.lifecycle.addObserver(this) parent.viewLifecycleOwner.lifecycle.addObserver(this)
} }
override fun onStart(owner: LifecycleOwner) { override fun onStart(owner: LifecycleOwner) {
if (Build.VERSION.SDK_INT < 28) { WifiApCommands.registerSoftApCallback(this)
parent.requireContext().registerReceiver(receiver,
IntentFilter(WifiApManager.WIFI_AP_STATE_CHANGED_ACTION))
} else WifiApCommands.registerSoftApCallback(this)
} }
override fun onStop(owner: LifecycleOwner) { override fun onStop(owner: LifecycleOwner) {
if (Build.VERSION.SDK_INT < 28) { WifiApCommands.unregisterSoftApCallback(this)
parent.requireContext().unregisterReceiver(receiver)
} else WifiApCommands.unregisterSoftApCallback(this)
} }
override fun onStateChanged(state: Int, failureReason: Int) { override fun onStateChanged(state: Int, failureReason: Int) {
@@ -277,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
@@ -286,7 +271,6 @@ 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, adapter: BluetoothAdapter) :
TetherManager(parent), DefaultLifecycleObserver { TetherManager(parent), DefaultLifecycleObserver {
private val tethering = BluetoothTethering(parent.requireContext(), adapter) { data.notifyChange() } private val tethering = BluetoothTethering(parent.requireContext(), adapter) { data.notifyChange() }
@@ -332,23 +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)
} }
@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

@@ -15,7 +15,6 @@ 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.core.content.getSystemService
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@@ -51,16 +50,13 @@ 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) }
@get:RequiresApi(24)
internal val bluetoothManager by lazy { internal val bluetoothManager by lazy {
if (Build.VERSION.SDK_INT >= 24) requireContext().getSystemService<BluetoothManager>()?.adapter?.let { requireContext().getSystemService<BluetoothManager>()?.adapter?.let {
TetherManager.Bluetooth(this@TetheringFragment, it) TetherManager.Bluetooth(this@TetheringFragment, it)
} else null }
} }
@get:RequiresApi(24) private val tetherManagers by lazy {
private val tetherManagers by lazy @TargetApi(24) {
listOfNotNull( listOfNotNull(
TetherManager.Wifi(this@TetheringFragment), TetherManager.Wifi(this@TetheringFragment),
TetherManager.Usb(this@TetheringFragment), TetherManager.Usb(this@TetheringFragment),
@@ -69,7 +65,6 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
} }
@get:RequiresApi(30) @get:RequiresApi(30)
private val ethernetManager by lazy @TargetApi(30) { TetherManager.Ethernet(this@TetheringFragment) } private val ethernetManager by lazy @TargetApi(30) { TetherManager.Ethernet(this@TetheringFragment) }
private val wifiManagerLegacy by lazy { TetherManager.WifiLegacy(this@TetheringFragment) }
var activeIfaces = emptyList<String>() var activeIfaces = emptyList<String>()
var localOnlyIfaces = emptyList<String>() var localOnlyIfaces = emptyList<String>()
@@ -106,24 +101,18 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
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.add(ethernetManager) list.add(ethernetManager)
ethernetManager.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) }
} }
@@ -140,7 +129,6 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
R.string.repeater_missing_location_permissions, Snackbar.LENGTH_LONG).show() 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())
} }
@@ -170,7 +158,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
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
} }
@@ -183,14 +171,14 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
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 =
Build.VERSION.SDK_INT >= 26 && adapter.localOnlyHotspotManager.binder?.configuration != null adapter.localOnlyHotspotManager.binder?.configuration != null
true true
} }
R.id.configuration_repeater -> { R.id.configuration_repeater -> {
adapter.repeaterManager.configure() adapter.repeaterManager.configure()
true true
} }
R.id.configuration_temp_hotspot -> @TargetApi(26) { R.id.configuration_temp_hotspot -> {
WifiApDialogFragment().apply { WifiApDialogFragment().apply {
arg(WifiApDialogFragment.Arg(adapter.localOnlyHotspotManager.binder?.configuration ?: return false, arg(WifiApDialogFragment.Arg(adapter.localOnlyHotspotManager.binder?.configuration ?: return false,
readOnly = true)) readOnly = true))
@@ -216,7 +204,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
null null
} 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()
@@ -245,7 +233,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
if (which == DialogInterface.BUTTON_POSITIVE) viewLifecycleOwner.lifecycleScope.launchWhenCreated { if (which == DialogInterface.BUTTON_POSITIVE) viewLifecycleOwner.lifecycleScope.launchWhenCreated {
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) {
@@ -299,7 +287,7 @@ 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?) {

View File

@@ -11,14 +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 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
@@ -27,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) }
@@ -110,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()))
} }
} }
@@ -211,7 +208,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()))
} }
} }
@@ -228,23 +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)
} }
@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
} }
@@ -65,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[4]) 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))
@@ -79,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()
@@ -138,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,95 +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): MacAddressCompat {
val buffer = when (addr.size) {
ETHER_ADDR_LEN -> ByteBuffer.allocate(Long.SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN).put(addr)
8 -> {
require(addr.take(2).all { it == 0.toByte() }) {
"Unrecognized padding " + addr.joinToString(":") { "%02x".format(it) }
}
ByteBuffer.allocate(Long.SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN).put(addr, 2, ETHER_ADDR_LEN)
}
else -> return fromString(String(addr))
}
buffer.rewind()
return MacAddressCompat(buffer.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) order(ByteOrder.LITTLE_ENDIAN)
var start = 0 put(toByteArray())
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() rewind()
MacAddressCompat(long) }.long
}
@RequiresApi(28)
fun MacAddress.toCompat() = fromBytes(toByteArray())
} }
fun validate() = require(addr and ((1L shl 48) - 1).inv() == 0L) fun toPlatform() = MacAddress.fromBytes(ByteBuffer.allocate(8).run {
fun toList() = ByteBuffer.allocate(8).run {
order(ByteOrder.LITTLE_ENDIAN) order(ByteOrder.LITTLE_ENDIAN)
putLong(addr) putLong(addr)
array().take(6) array().take(6)
} }.toByteArray())
@RequiresApi(28)
fun toPlatform() = MacAddress.fromBytes(toList().toByteArray())
override fun toString() = toList().joinToString(":") { "%02x".format(it) }
fun toOui() = toList().joinToString("") { "%02x".format(it) }.substring(0, 9)
} }

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,
} }
@@ -155,14 +154,14 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
* The only case when upstream is null is on API 23- and we are using system default rules. * 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 = if (upstream.isEmpty()) 0 else 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()) { if (upstream.isEmpty()) {
ipRule("goto $RULE_PRIORITY_TETHERING", priority) // skip unreachable rule ipRule("goto $RULE_PRIORITY_TETHERING", priority) // skip unreachable rule
} else ipRuleLookup(ifindex, priority) } else ipRuleLookup(ifindex, priority)
@TargetApi(28) when (masqueradeMode) { when (masqueradeMode) {
MasqueradeMode.None -> { } // nothing to be done here MasqueradeMode.None -> { } // nothing to be done here
MasqueradeMode.Simple -> { MasqueradeMode.Simple -> {
// note: specifying -i wouldn't work for POSTROUTING // note: specifying -i wouldn't work for POSTROUTING
@@ -225,16 +224,10 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
updateDnsRoute() updateDnsRoute()
} }
} }
private val fallbackUpstream = object : Upstream(RULE_PRIORITY_UPSTREAM_FALLBACK) { private val fallbackUpstream = Upstream(RULE_PRIORITY_UPSTREAM_FALLBACK)
@SuppressLint("NewApi")
override fun onFallback() = onAvailable(LinkProperties().apply {
interfaceName = ""
setDnsServers(listOf(parseNumericAddress("8.8.8.8")))
})
}
private val upstream = Upstream(RULE_PRIORITY_UPSTREAM) private val upstream = Upstream(RULE_PRIORITY_UPSTREAM)
private inner class Client(private val ip: Inet4Address, mac: MacAddressCompat) : AutoCloseable { private inner class Client(private val ip: Inet4Address, mac: MacAddress) : AutoCloseable {
private val transaction = RootSession.beginTransaction().safeguard { private val transaction = RootSession.beginTransaction().safeguard {
val address = ip.hostAddress val address = ip.hostAddress
iptablesInsert("vpnhotspot_acl -i $downstream -s $address -j ACCEPT") iptablesInsert("vpnhotspot_acl -i $downstream -s $address -j ACCEPT")
@@ -287,9 +280,9 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
* but may be broken when system tethering shutdown before local-only interfaces. * but may be broken when system tethering shutdown before local-only interfaces.
*/ */
fun ipForward() { fun ipForward() {
if (Build.VERSION.SDK_INT >= 23) try { try {
transaction.ndc("ipfwd", "ndc ipfwd enable vpnhotspot_$downstream", transaction.ndc("ipfwd", "ndc ipfwd enable vpnhotspot_$downstream",
"ndc ipfwd disable vpnhotspot_$downstream") "ndc ipfwd disable vpnhotspot_$downstream")
return return
} catch (e: RoutingCommands.UnexpectedOutputException) { } catch (e: RoutingCommands.UnexpectedOutputException) {
Timber.w(IOException("ndc ipfwd enable failure", e)) Timber.w(IOException("ndc ipfwd enable failure", e))

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

@@ -94,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)
@@ -107,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
@@ -131,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.
@@ -141,7 +137,6 @@ object TetheringManager {
* 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.
@@ -149,7 +144,6 @@ object TetheringManager {
* Requires BLUETOOTH permission. * Requires BLUETOOTH permission.
* @see startTethering * @see startTethering
*/ */
@RequiresApi(24)
const val TETHERING_BLUETOOTH = 2 const val TETHERING_BLUETOOTH = 2
/** /**
* Ethernet tethering type. * Ethernet tethering type.
@@ -178,16 +172,13 @@ 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 @TargetApi(24) {
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)
} }
@@ -232,7 +223,6 @@ object TetheringManager {
private val stopTethering by lazy @TargetApi(30) { 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)
@@ -299,7 +289,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 {
@@ -371,12 +360,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)
@@ -625,10 +612,7 @@ object TetheringManager {
@RequiresApi(30) @RequiresApi(30)
const val TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION = 14 const val TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION = 14
val Intent.tetheredIfaces get() = getStringArrayListExtra( val Intent.tetheredIfaces get() = getStringArrayListExtra(EXTRA_ACTIVE_TETHER)
if (Build.VERSION.SDK_INT >= 26) EXTRA_ACTIVE_TETHER else EXTRA_ACTIVE_TETHER_LEGACY) val Intent.localOnlyTetheredIfaces get() = getStringArrayListExtra(
val Intent.localOnlyTetheredIfaces get() = if (Build.VERSION.SDK_INT >= 26) { if (Build.VERSION.SDK_INT >= 30) EXTRA_ACTIVE_LOCAL_ONLY else EXTRA_ACTIVE_LOCAL_ONLY_LEGACY)
getStringArrayListExtra(
if (Build.VERSION.SDK_INT >= 30) EXTRA_ACTIVE_LOCAL_ONLY else EXTRA_ACTIVE_LOCAL_ONLY_LEGACY)
} else emptyList<String>()
} }

View File

@@ -1,6 +1,5 @@
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
@@ -52,29 +51,10 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
callback.onAvailable(currentLinkProperties) callback.onAvailable(currentLinkProperties)
} }
} else { } else {
when (Build.VERSION.SDK_INT) { if (Build.VERSION.SDK_INT >= 31) {
in 31..Int.MAX_VALUE -> @TargetApi(31) { Services.connectivity.registerBestMatchingNetworkCallback(networkRequest, networkCallback,
Services.connectivity.registerBestMatchingNetworkCallback(networkRequest, networkCallback, Services.mainHandler)
Services.mainHandler) } else Services.connectivity.requestNetwork(networkRequest, networkCallback, Services.mainHandler)
}
in 28..30 -> @TargetApi(28) {
Services.connectivity.requestNetwork(networkRequest, networkCallback, Services.mainHandler)
}
in 26..27 -> @TargetApi(26) {
Services.connectivity.registerDefaultNetworkCallback(networkCallback, Services.mainHandler)
}
in 24..25 -> @TargetApi(24) {
Services.connectivity.registerDefaultNetworkCallback(networkCallback)
}
else -> try {
Services.connectivity.requestNetwork(networkRequest, networkCallback)
} catch (e: RuntimeException) {
// 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

@@ -77,7 +77,7 @@ class InterfaceMonitor(private val ifaceRegex: String) : UpstreamMonitor() {
callback.onAvailable(currentLinkProperties) callback.onAvailable(currentLinkProperties)
} }
} else { } else {
Services.registerNetworkCallbackCompat(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

View File

@@ -33,13 +33,12 @@ 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 {
@@ -52,7 +51,7 @@ class TetherTimeoutMonitor(private val timeout: Long = 0,
} 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

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

@@ -46,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

@@ -61,7 +61,7 @@ object VpnMonitor : UpstreamMonitor() {
callback.onAvailable(currentLinkProperties) callback.onAvailable(currentLinkProperties)
} }
} else { } else {
Services.registerNetworkCallbackCompat(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
@@ -126,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)
@@ -141,10 +142,10 @@ 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: String, 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.toByteArray()
.joinToString("") { (it.toInt() and 255).toString(16).padStart(2, '0') } .joinToString("") { (it.toInt() and 255).toString(16).padStart(2, '0') }

View File

@@ -9,10 +9,9 @@ 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 be.mygod.vpnhotspot.BuildConfig
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.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
@@ -22,8 +21,7 @@ import java.lang.reflect.InvocationTargetException
@Parcelize @Parcelize
data class SoftApConfigurationCompat( data class SoftApConfigurationCompat(
var ssid: String? = null, var ssid: String? = 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,
/** /**
@@ -31,14 +29,11 @@ data class SoftApConfigurationCompat(
* see also [android.net.wifi.WifiManager.isBridgedApConcurrencySupported]. * see also [android.net.wifi.WifiManager.isBridgedApConcurrencySupported].
* Otherwise, use [requireSingleBand] and [setChannel]. * Otherwise, use [requireSingleBand] and [setChannel].
*/ */
@TargetApi(23)
var channels: SparseIntArray = SparseIntArray(1).apply { append(BAND_2GHZ, 0) }, var channels: SparseIntArray = SparseIntArray(1).apply { append(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,
@@ -166,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
@@ -174,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
@@ -356,17 +349,17 @@ data class SoftApConfigurationCompat(
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
fun android.net.wifi.WifiConfiguration.toCompat() = SoftApConfigurationCompat( fun android.net.wifi.WifiConfiguration.toCompat() = SoftApConfigurationCompat(
SSID, 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).also { SparseIntArray(1).also {
if (Build.VERSION.SDK_INT >= 23) it.append(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
else -> throw IllegalArgumentException("Unexpected band $band") else -> throw IllegalArgumentException("Unexpected band $band")
}, apChannel.getInt(this)) else it.append(BAND_LEGACY, 0) }, apChannel.getInt(this))
}, },
allowedKeyManagement.nextSetBit(0).let { selected -> allowedKeyManagement.nextSetBit(0).let { selected ->
require(allowedKeyManagement.nextSetBit(selected + 1) < 0) { require(allowedKeyManagement.nextSetBit(selected + 1) < 0) {
@@ -389,14 +382,14 @@ data class SoftApConfigurationCompat(
} }
} }
}, },
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, ssid,
bssid?.toCompat()?.addr, bssid,
passphrase, passphrase,
isHiddenSsid, isHiddenSsid,
if (Build.VERSION.SDK_INT >= 31) getChannels(this) as SparseIntArray else SparseIntArray(1).also { if (Build.VERSION.SDK_INT >= 31) getChannels(this) as SparseIntArray else SparseIntArray(1).also {
@@ -466,13 +459,6 @@ data class SoftApConfigurationCompat(
setBridgedModeOpportunisticShutdownTimeoutMillis(staticBuilder, timeout) setBridgedModeOpportunisticShutdownTimeoutMillis(staticBuilder, timeout)
} }
@Suppress("DEPRECATION")
inline var bssid: MacAddressCompat?
get() = bssidAddr?.let { MacAddressCompat(it) }
set(value) {
bssidAddr = value?.addr
}
fun setChannel(channel: Int, band: Int = BAND_LEGACY) { fun setChannel(channel: Int, band: Int = BAND_LEGACY) {
channels = SparseIntArray(1).apply { channels = SparseIntArray(1).apply {
append(when { append(when {
@@ -500,18 +486,15 @@ data class SoftApConfigurationCompat(
result.SSID = ssid result.SSID = ssid
result.preSharedKey = passphrase result.preSharedKey = passphrase
result.hiddenSSID = isHiddenSsid result.hiddenSSID = isHiddenSsid
if (Build.VERSION.SDK_INT >= 23) { apBand.setInt(result, when (band) {
apBand.setInt(result, when (band) { BAND_2GHZ -> 0
BAND_2GHZ -> 0 BAND_5GHZ -> 1
BAND_5GHZ -> 1 else -> {
else -> { require(isLegacyEitherBand(band)) { "Convert fail, unsupported band setting :$band" }
require(Build.VERSION.SDK_INT >= 28) { "A band must be specified on this platform" } -1
require(isLegacyEitherBand(band)) { "Convert fail, unsupported band setting :$band" } }
-1 })
} apChannel.setInt(result, channel)
})
apChannel.setInt(result, channel)
} else require(isLegacyEitherBand(band)) { "Specifying band is unsupported on this platform" }
if (original?.securityType != securityType) { if (original?.securityType != securityType) {
result.allowedKeyManagement.clear() result.allowedKeyManagement.clear()
result.allowedKeyManagement.set(when (securityType) { result.allowedKeyManagement.set(when (securityType) {
@@ -545,9 +528,8 @@ data class SoftApConfigurationCompat(
else -> passphrase else -> passphrase
}, securityType) }, securityType)
setChannelsCompat(builder, channels) setChannelsCompat(builder, channels)
setBssid(builder, bssid?.run { setBssid(builder,
if (Build.VERSION.SDK_INT >= 31 && macRandomizationSetting != RANDOMIZATION_NONE) null else toPlatform() if (Build.VERSION.SDK_INT < 31 || macRandomizationSetting == RANDOMIZATION_NONE) bssid else null)
})
setMaxNumberOfClients(builder, maxNumberOfClients) setMaxNumberOfClients(builder, maxNumberOfClients)
try { try {
setShutdownTimeoutMillis(builder, shutdownTimeoutMillis) setShutdownTimeoutMillis(builder, shutdownTimeoutMillis)

View File

@@ -1,10 +1,10 @@
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.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
@@ -31,7 +31,6 @@ 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.RangeInput
@@ -68,17 +67,12 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
} }
private val p2pSafeOptions by lazy { genAutoOptions(SoftApConfigurationCompat.BAND_LEGACY) + channels5G } private val p2pSafeOptions by lazy { genAutoOptions(SoftApConfigurationCompat.BAND_LEGACY) + channels5G }
private val softApOptions by lazy { private val softApOptions by lazy {
when (Build.VERSION.SDK_INT) { if (Build.VERSION.SDK_INT >= 30) {
in 30..Int.MAX_VALUE -> { genAutoOptions(SoftApConfigurationCompat.BAND_ANY_31) +
genAutoOptions(SoftApConfigurationCompat.BAND_ANY_31) + channels5G +
channels5G + (1..253).map { ChannelOption(SoftApConfigurationCompat.BAND_6GHZ, it) } +
(1..253).map { ChannelOption(SoftApConfigurationCompat.BAND_6GHZ, it) } + (1..6).map { ChannelOption(SoftApConfigurationCompat.BAND_60GHZ, it) }
(1..6).map { ChannelOption(SoftApConfigurationCompat.BAND_60GHZ, it) } } else p2pSafeOptions
}
in 28 until 30 -> p2pSafeOptions
else -> listOf(ChannelOption(SoftApConfigurationCompat.BAND_2GHZ),
ChannelOption(SoftApConfigurationCompat.BAND_5GHZ)) + channels5G
}
} }
@get:RequiresApi(30) @get:RequiresApi(30)
@@ -149,24 +143,24 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
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() channels = generateChannels()
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)
macRandomizationSetting = dialogView.macRandomization.selectedItemPosition macRandomizationSetting = dialogView.macRandomization.selectedItemPosition
bssid = if ((arg.p2pMode || Build.VERSION.SDK_INT < 31 && macRandomizationSetting == bssid = if ((arg.p2pMode || Build.VERSION.SDK_INT < 31 && macRandomizationSetting ==
SoftApConfigurationCompat.RANDOMIZATION_NONE) && dialogView.bssid.length() != 0) { SoftApConfigurationCompat.RANDOMIZATION_NONE) && dialogView.bssid.length() != 0) {
MacAddressCompat.fromString(dialogView.bssid.text.toString()) MacAddress.fromString(dialogView.bssid.text.toString())
} else null } else null
isBridgedModeOpportunisticShutdownEnabled = dialogView.bridgedModeOpportunisticShutdown.isChecked isBridgedModeOpportunisticShutdownEnabled = dialogView.bridgedModeOpportunisticShutdown.isChecked
isIeee80211axEnabled = dialogView.ieee80211ax.isChecked isIeee80211axEnabled = dialogView.ieee80211ax.isChecked
@@ -177,7 +171,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
} }
vendorElements = VendorElements.deserialize(dialogView.vendorElements.text) vendorElements = VendorElements.deserialize(dialogView.vendorElements.text)
persistentRandomizedMacAddress = if (dialogView.persistentRandomizedMac.length() != 0) { persistentRandomizedMacAddress = if (dialogView.persistentRandomizedMac.length() != 0) {
MacAddressCompat.fromString(dialogView.persistentRandomizedMac.text.toString()).toPlatform() MacAddress.fromString(dialogView.persistentRandomizedMac.text.toString())
} else null } else null
allowedAcsChannels = acsList.associate { (band, text, _) -> band to RangeInput.fromString(text.text) } allowedAcsChannels = acsList.associate { (band, text, _) -> band to RangeInput.fromString(text.text) }
if (!arg.p2pMode && Build.VERSION.SDK_INT >= 33) { if (!arg.p2pMode && Build.VERSION.SDK_INT >= 33) {
@@ -227,7 +221,6 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
} }
} }
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)
@@ -239,12 +232,10 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
} }
if (!arg.readOnly) onItemSelectedListener = this@WifiApDialogFragment if (!arg.readOnly) onItemSelectedListener = this@WifiApDialogFragment
} }
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) { dialogView.bandPrimary.configure(currentChannels)
dialogView.bandPrimary.configure(currentChannels) if (Build.VERSION.SDK_INT >= 31 && !arg.p2pMode) {
if (Build.VERSION.SDK_INT >= 31 && !arg.p2pMode) { dialogView.bandSecondary.configure(listOf(ChannelOption.Disabled) + currentChannels)
dialogView.bandSecondary.configure(listOf(ChannelOption.Disabled) + currentChannels) } else dialogView.bandSecondary.isGone = true
} else dialogView.bandSecondary.isGone = true
} else dialogView.bandGroup.isGone = true
if (arg.p2pMode || Build.VERSION.SDK_INT < 30) dialogView.accessControlGroup.isGone = true if (arg.p2pMode || Build.VERSION.SDK_INT < 30) dialogView.accessControlGroup.isGone = true
else if (!arg.readOnly) { else if (!arg.readOnly) {
dialogView.maxClient.addTextChangedListener(this@WifiApDialogFragment) dialogView.maxClient.addTextChangedListener(this@WifiApDialogFragment)
@@ -307,11 +298,9 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
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 <= 0) "" 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.bandPrimary.setSelection(locate(0)) if (Build.VERSION.SDK_INT >= 31 && !arg.p2pMode) {
if (Build.VERSION.SDK_INT >= 31 && !arg.p2pMode) { dialogView.bandSecondary.setSelection(if (base.channels.size() > 1) locate(1) + 1 else 0)
dialogView.bandSecondary.setSelection(if (base.channels.size() > 1) locate(1) + 1 else 0)
}
} }
dialogView.bssid.setText(base.bssid?.toString()) dialogView.bssid.setText(base.bssid?.toString())
dialogView.hiddenSsid.isChecked = base.isHiddenSsid dialogView.hiddenSsid.isChecked = base.isHiddenSsid
@@ -343,7 +332,6 @@ 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 ssidLength = dialogView.ssid.text.toString().toByteArray().size
@@ -389,10 +377,8 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
dialogView.bssidWrapper.isGone = hideBssid dialogView.bssidWrapper.isGone = hideBssid
dialogView.bssidWrapper.error = null dialogView.bssidWrapper.error = null
val bssidValid = hideBssid || dialogView.bssid.length() == 0 || try { val bssidValid = hideBssid || dialogView.bssid.length() == 0 || try {
val mac = MacAddressCompat.fromString(dialogView.bssid.text.toString()) val mac = MacAddress.fromString(dialogView.bssid.text.toString())
if (Build.VERSION.SDK_INT >= 30 && !arg.p2pMode) { if (Build.VERSION.SDK_INT >= 30 && !arg.p2pMode) SoftApConfigurationCompat.testPlatformValidity(mac)
SoftApConfigurationCompat.testPlatformValidity(mac.toPlatform())
}
true true
} catch (e: Exception) { } catch (e: Exception) {
dialogView.bssidWrapper.error = e.readableMessage dialogView.bssidWrapper.error = e.readableMessage
@@ -409,15 +395,15 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
dialogView.maxClientWrapper.error = maxClientError dialogView.maxClientWrapper.error = maxClientError
val listsNoError = if (Build.VERSION.SDK_INT >= 30) { val listsNoError = if (Build.VERSION.SDK_INT >= 30) {
val (blockedList, blockedListError) = try { val (blockedList, blockedListError) = try {
(dialogView.blockedList.text ?: "").split(nonMacChars) (dialogView.blockedList.text ?: "").split(nonMacChars).filter { it.isNotEmpty() }
.filter { it.isNotEmpty() }.map { MacAddressCompat.fromString(it).toPlatform() }.toSet() to null .map(MacAddress::fromString).toSet() to null
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
null to 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).filter { it.isNotEmpty() }.forEach { (dialogView.allowedList.text ?: "").split(nonMacChars).filter { it.isNotEmpty() }.forEach {
val mac = MacAddressCompat.fromString(it).toPlatform() val mac = MacAddress.fromString(it)
require(blockedList?.contains(mac) != true) { "A MAC address exists in both client lists" } require(blockedList?.contains(mac) != true) { "A MAC address exists in both client lists" }
} }
null null
@@ -449,9 +435,9 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
dialogView.vendorElementsWrapper.error = vendorElementsError dialogView.vendorElementsWrapper.error = vendorElementsError
dialogView.persistentRandomizedMacWrapper.error = null dialogView.persistentRandomizedMacWrapper.error = null
val persistentRandomizedMacValid = dialogView.persistentRandomizedMac.length() == 0 || try { val persistentRandomizedMacValid = dialogView.persistentRandomizedMac.length() == 0 || try {
MacAddressCompat.fromString(dialogView.persistentRandomizedMac.text.toString()) MacAddress.fromString(dialogView.persistentRandomizedMac.text.toString())
true true
} catch (e: Exception) { } catch (e: IllegalArgumentException) {
dialogView.persistentRandomizedMacWrapper.error = e.readableMessage dialogView.persistentRandomizedMacWrapper.error = e.readableMessage
false false
} }
@@ -496,9 +482,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
android.R.id.copy -> try { android.R.id.copy -> try {
app.clipboard.setPrimaryClip(ClipData.newPlainText(null, app.clipboard.setPrimaryClip(ClipData.newPlainText(null,
Base64.encodeToString(generateConfig().toByteArray(), BASE64_FLAGS)).apply { Base64.encodeToString(generateConfig().toByteArray(), BASE64_FLAGS)).apply {
if (Build.VERSION.SDK_INT >= 24) { description.extras = persistableBundleOf(ClipDescription.EXTRA_IS_SENSITIVE to true)
description.extras = persistableBundleOf(ClipDescription.EXTRA_IS_SENSITIVE to true)
}
}) })
true true
} catch (e: RuntimeException) { } catch (e: RuntimeException) {

View File

@@ -88,7 +88,6 @@ object WifiApManager {
* *
* Source: https://android.googlesource.com/platform/frameworks/base/+/android-6.0.0_r1/wifi/java/android/net/wifi/WifiManager.java#210 * Source: https://android.googlesource.com/platform/frameworks/base/+/android-6.0.0_r1/wifi/java/android/net/wifi/WifiManager.java#210
*/ */
@get:RequiresApi(23)
val EXTRA_WIFI_AP_FAILURE_REASON get() = 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" if (Build.VERSION.SDK_INT >= 30) "android.net.wifi.extra.WIFI_AP_FAILURE_REASON" else "wifi_ap_error_code"
/** /**
@@ -98,7 +97,6 @@ object WifiApManager {
* *
* Source: https://android.googlesource.com/platform/frameworks/base/+/android-8.0.0_r1/wifi/java/android/net/wifi/WifiManager.java#413 * Source: https://android.googlesource.com/platform/frameworks/base/+/android-8.0.0_r1/wifi/java/android/net/wifi/WifiManager.java#413
*/ */
@get:RequiresApi(26)
val EXTRA_WIFI_AP_INTERFACE_NAME get() = 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" if (Build.VERSION.SDK_INT >= 30) "android.net.wifi.extra.WIFI_AP_INTERFACE_NAME" else "wifi_ap_interface_name"
@@ -177,7 +175,6 @@ object WifiApManager {
setWifiApConfiguration(Services.wifi, value) as Boolean setWifiApConfiguration(Services.wifi, value) as Boolean
fun setConfiguration(value: SoftApConfiguration) = setSoftApConfiguration(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.
@@ -239,7 +236,6 @@ object WifiApManager {
@RequiresApi(30) @RequiresApi(30)
fun onBlockedClientConnecting(client: Parcelable, blockedReason: Int) { } fun onBlockedClientConnecting(client: Parcelable, blockedReason: Int) { }
} }
@RequiresApi(23)
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_") }
@@ -255,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 {
@@ -270,7 +265,7 @@ object WifiApManager {
method.matches("onStateChanged", Integer.TYPE, Integer.TYPE) -> { method.matches("onStateChanged", Integer.TYPE, Integer.TYPE) -> {
callback.onStateChanged(args!![0] as Int, args[1] as Int) callback.onStateChanged(args!![0] as Int, args[1] as Int)
} }
method.matches("onNumClientsChanged", Integer.TYPE) -> @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"))
callback.onNumClientsChanged(args!![0] as Int) callback.onNumClientsChanged(args!![0] as Int)
} }
@@ -307,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)
private val cancelLocalOnlyHotspotRequest by lazy { private val cancelLocalOnlyHotspotRequest by lazy {
@@ -317,43 +311,5 @@ object WifiApManager {
* This is the only way to unregister requests besides app exiting. * This is the only way to unregister requests besides app exiting.
* Therefore, we are happy with crashing the app if reflection fails. * Therefore, we are happy with crashing the app if reflection fails.
*/ */
@RequiresApi(26)
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

@@ -1,6 +1,7 @@
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.ScanResult
import android.net.wifi.WpsInfo import android.net.wifi.WpsInfo
import android.net.wifi.p2p.WifiP2pGroup import android.net.wifi.p2p.WifiP2pGroup
@@ -9,9 +10,8 @@ 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.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
import be.mygod.vpnhotspot.util.callSuper import be.mygod.vpnhotspot.util.callSuper
import be.mygod.vpnhotspot.util.matchesCompat import be.mygod.vpnhotspot.util.matches
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import java.lang.reflect.InvocationHandler import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method import java.lang.reflect.Method
@@ -128,7 +128,7 @@ object WifiP2pManagerHelper {
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 { override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? = when {
method.matchesCompat("onPersistentGroupInfoAvailable", args, classWifiP2pGroupList) -> { method.matches("onPersistentGroupInfoAvailable", classWifiP2pGroupList) -> {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
result.complete(getGroupList(args!![0]) as Collection<WifiP2pGroup>) result.complete(getGroupList(args!![0]) as Collection<WifiP2pGroup>)
} }
@@ -142,11 +142,11 @@ object WifiP2pManagerHelper {
CompletableDeferred<WifiP2pInfo?>().apply { requestConnectionInfo(c) { complete(it) } }.await() CompletableDeferred<WifiP2pInfo?>().apply { requestConnectionInfo(c) { complete(it) } }.await()
@SuppressLint("MissingPermission") // missing permission simply leads to null result @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
} }
} }

View File

@@ -63,7 +63,7 @@ class AutoCompleteNetworkPreferenceDialogFragment : EditTextPreferenceDialogFrag
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
Services.registerNetworkCallbackCompat(globalNetworkRequestBuilder().apply { Services.registerNetworkCallback(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)

View File

@@ -3,7 +3,6 @@ package be.mygod.vpnhotspot.preference
import android.content.Context import android.content.Context
import android.graphics.Typeface import android.graphics.Typeface
import android.net.LinkProperties import android.net.LinkProperties
import android.os.Build
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.style.StyleSpan import android.text.style.StyleSpan
import android.util.AttributeSet import android.util.AttributeSet
@@ -41,7 +40,7 @@ class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(co
internet == true || try { internet == true || try {
route.matches(internetV4Address) || route.matches(internetV6Address) route.matches(internetV4Address) || route.matches(internetV6Address)
} catch (e: RuntimeException) { } catch (e: RuntimeException) {
if (Build.VERSION.SDK_INT >= 23) Timber.w(e) else Timber.d(e) Timber.w(e)
false false
} }
} }
@@ -52,12 +51,7 @@ 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)

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,7 +198,6 @@ 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()

View File

@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot.root package be.mygod.vpnhotspot.root
import android.net.MacAddress
import android.net.wifi.ScanResult 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
@@ -37,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

View File

@@ -1,7 +1,6 @@
package be.mygod.vpnhotspot.root package be.mygod.vpnhotspot.root
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Build
import android.os.Parcelable import android.os.Parcelable
import android.util.Log import android.util.Log
import be.mygod.librootkotlinx.* import be.mygod.librootkotlinx.*
@@ -34,7 +33,7 @@ object RootManager : RootSession(), Logger {
}) })
Logger.me = RootManager Logger.me = RootManager
Services.init { systemContext } Services.init { systemContext }
if (Build.VERSION.SDK_INT >= 28) UnblockCentral.needInit = false UnblockCentral.needInit = false
return null return null
} }
} }

View File

@@ -14,7 +14,6 @@ import timber.log.Timber
object RoutingCommands { object RoutingCommands {
@Parcelize @Parcelize
class Clean : RootCommandNoResult { 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)

View File

@@ -20,7 +20,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)
@@ -60,7 +59,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>()
@@ -115,7 +113,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 }
@@ -141,7 +138,6 @@ object WifiApCommands {
} }
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)
@@ -158,7 +154,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()

View File

@@ -30,10 +30,8 @@ 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 }

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

@@ -8,10 +8,8 @@ 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 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

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

@@ -5,7 +5,6 @@ import android.net.ConnectivityManager
import android.net.NetworkRequest 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.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
@@ -30,7 +29,6 @@ object Services {
} }
val wifi by lazy { context.getSystemService<WifiManager>()!! } val wifi by lazy { context.getSystemService<WifiManager>()!! }
fun registerNetworkCallbackCompat(request: NetworkRequest, networkCallback: ConnectivityManager.NetworkCallback) = fun registerNetworkCallback(request: NetworkRequest, networkCallback: ConnectivityManager.NetworkCallback) =
if (Build.VERSION.SDK_INT >= 26) connectivity.registerNetworkCallback(request, networkCallback, mainHandler) connectivity.registerNetworkCallback(request, networkCallback, mainHandler)
else connectivity.registerNetworkCallback(request, networkCallback)
} }

View File

@@ -13,9 +13,7 @@ import me.weishu.reflection.Reflection
* *
* Lazy cannot be used directly as it will create inner classes. * Lazy cannot be used directly as it will create inner classes.
*/ */
@RequiresApi(28)
@SuppressLint("BlockedPrivateApi", "DiscouragedPrivateApi") @SuppressLint("BlockedPrivateApi", "DiscouragedPrivateApi")
@Suppress("FunctionName")
object UnblockCentral { object UnblockCentral {
var needInit = true var needInit = true
/** /**

View File

@@ -1,21 +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.* import android.net.*
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
@@ -24,18 +19,11 @@ 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.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
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
@@ -57,23 +45,10 @@ fun Long.toPluralInt(): Int {
return (this % 1000000000).toInt() + 1000000000 return (this % 1000000000).toInt() + 1000000000
} }
@RequiresApi(26)
fun Method.matches(name: String, vararg classes: Class<*>) = this.name == name && parameterCount == classes.size && fun Method.matches(name: String, vararg classes: Class<*>) = this.name == name && parameterCount == classes.size &&
classes.indices.all { i -> parameters[i].type == classes[i] } classes.indices.all { i -> parameters[i].type == classes[i] }
@RequiresApi(26)
inline fun <reified T> Method.matches1(name: String) = matches(name, T::class.java) inline fun <reified T> Method.matches1(name: String) = matches(name, T::class.java)
fun Method.matchesCompat(name: String, args: Array<out Any?>?, vararg classes: Class<*>) =
if (Build.VERSION.SDK_INT < 26) {
this.name == name && args?.size ?: 0 == classes.size && classes.indices.all { i ->
args!![i]?.let { classes[i].isInstance(it) } != false
}
} else matches(name, *classes)
fun HttpURLConnection.disconnectCompat() {
if (Build.VERSION.SDK_INT < 26) GlobalScope.launch(Dispatchers.IO) { disconnect() } else disconnect()
}
fun Context.ensureReceiverUnregistered(receiver: BroadcastReceiver) { fun Context.ensureReceiverUnregistered(receiver: BroadcastReceiver) {
try { try {
unregisterReceiver(receiver) unregisterReceiver(receiver)
@@ -167,7 +142,7 @@ fun makeMacSpan(mac: String) = if (app.hasTouch) SpannableString(mac).apply {
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)
@@ -219,8 +194,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
} }
@@ -232,7 +206,7 @@ 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 -> try { method.isDefault -> try {
newLookup.newInstance(interfaceClass, 0xf) // ALL_MODES newLookup.newInstance(interfaceClass, 0xf) // ALL_MODES
} catch (e: ReflectiveOperationException) { } catch (e: ReflectiveOperationException) {
Timber.w(e) Timber.w(e)
@@ -258,20 +232,5 @@ fun InvocationHandler.callSuper(interfaceClass: Class<*>, proxy: Any, method: Me
} }
fun globalNetworkRequestBuilder() = NetworkRequest.Builder().apply { fun globalNetworkRequestBuilder() = NetworkRequest.Builder().apply {
if (Build.VERSION.SDK_INT >= 31) setIncludeOtherUidNetworks(true) else if (Build.VERSION.SDK_INT == 23) { if (Build.VERSION.SDK_INT >= 31) setIncludeOtherUidNetworks(true)
// workarounds for OEM bugs
removeCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
removeCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL)
}
}
@Suppress("FunctionName")
fun if_nametoindex(ifname: String) = if (Build.VERSION.SDK_INT >= 26) {
Os.if_nametoindex(ifname)
} else try {
File("/sys/class/net/$ifname/ifindex").inputStream().bufferedReader().use { it.readLine().trim().toInt() }
} catch (_: FileNotFoundException) {
NetworkInterface.getByName(ifname)?.index ?: 0
} catch (e: IOException) {
if ((e.cause as? ErrnoException)?.errno == OsConstants.ENODEV) 0 else throw e
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -53,7 +53,6 @@
se il tethering VPN non funziona.</string> se il tethering VPN non funziona.</string>
<string name="tethering_manage_usb">Tethering USB</string> <string name="tethering_manage_usb">Tethering USB</string>
<string name="tethering_manage_wifi">Hotspot Wi\u2011Fi</string> <string name="tethering_manage_wifi">Hotspot Wi\u2011Fi</string>
<string name="tethering_manage_wifi_legacy">Hotspot Wi\u2011Fi (legacy)</string>
<string name="tethering_manage_bluetooth">Tethering Bluetooth</string> <string name="tethering_manage_bluetooth">Tethering Bluetooth</string>
<string name="tethering_manage_ethernet" msgid="959743110824197356">"Tethering Ethernet"</string> <string name="tethering_manage_ethernet" msgid="959743110824197356">"Tethering Ethernet"</string>

View File

@@ -50,7 +50,6 @@
<string name="tethering_manage_offload_enabled">Por favor, desative a Aceleração de hardware de tethering nas configurações de desenvolvedor caso o VPN Hotspot não funcionar corretamente.</string> <string name="tethering_manage_offload_enabled">Por favor, desative a Aceleração de hardware de tethering nas configurações de desenvolvedor caso o VPN Hotspot não funcionar corretamente.</string>
<string name="tethering_manage_usb">Tethering USB</string> <string name="tethering_manage_usb">Tethering USB</string>
<string name="tethering_manage_wifi">Ponto de acesso Wi\u2011Fi</string> <string name="tethering_manage_wifi">Ponto de acesso Wi\u2011Fi</string>
<string name="tethering_manage_wifi_legacy">Ponto de acesso Wi\u2011Fi (legacy)</string>
<string name="tethering_manage_bluetooth">Tethering Bluetooth</string> <string name="tethering_manage_bluetooth">Tethering Bluetooth</string>
<string name="tethering_manage_ethernet">Tethering Ethernet</string> <string name="tethering_manage_ethernet">Tethering Ethernet</string>
<string name="tethering_manage_ncm">Tethering USB (NCM)</string> <string name="tethering_manage_ncm">Tethering USB (NCM)</string>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="api_lt_25">false</bool>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="api_ge_26">true</bool>
</resources>

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string-array name="settings_service_masquerade" tools:ignore="InconsistentArrays">
<item>@string/settings_service_masquerade_none</item>
<item>@string/settings_service_masquerade_simple</item>
<item>@string/settings_service_masquerade_netd</item>
</string-array>
<string-array name="settings_service_masquerade_values" tools:ignore="InconsistentArrays">
<item>None</item>
<item>Simple</item>
<item>Netd</item>
</string-array>
</resources>

View File

@@ -57,7 +57,6 @@
--> -->
<string name="tethering_manage_usb">USB 网络共享</string> <string name="tethering_manage_usb">USB 网络共享</string>
<string name="tethering_manage_wifi">WLAN 热点</string> <string name="tethering_manage_wifi">WLAN 热点</string>
<string name="tethering_manage_wifi_legacy">WLAN 热点 (旧 API)</string>
<string name="tethering_manage_bluetooth">蓝牙网络共享</string> <string name="tethering_manage_bluetooth">蓝牙网络共享</string>
<string name="tethering_manage_ethernet" msgid="959743110824197356">"以太网络共享"</string> <string name="tethering_manage_ethernet" msgid="959743110824197356">"以太网络共享"</string>
<string name="tethering_manage_ncm">USB 网络共享 (NCM)</string> <string name="tethering_manage_ncm">USB 网络共享 (NCM)</string>

View File

@@ -66,7 +66,6 @@
--> -->
<string name="tethering_manage_usb">USB 網路共用</string> <string name="tethering_manage_usb">USB 網路共用</string>
<string name="tethering_manage_wifi">Wi\u2011Fi 無線基地台</string> <string name="tethering_manage_wifi">Wi\u2011Fi 無線基地台</string>
<string name="tethering_manage_wifi_legacy">Wi\u2011Fi 無線基地台 (舊版)</string>
<string name="tethering_manage_bluetooth">藍牙網路共用</string> <string name="tethering_manage_bluetooth">藍牙網路共用</string>
<string name="tethering_manage_ethernet" msgid="959743110824197356">"乙太網路網路共用"</string> <string name="tethering_manage_ethernet" msgid="959743110824197356">"乙太網路網路共用"</string>
<string name="tethering_manage_ncm">USB 網路共用 (NCM)</string> <string name="tethering_manage_ncm">USB 網路共用 (NCM)</string>

View File

@@ -9,10 +9,12 @@
<string-array name="settings_service_masquerade" tools:ignore="InconsistentArrays"> <string-array name="settings_service_masquerade" tools:ignore="InconsistentArrays">
<item>@string/settings_service_masquerade_none</item> <item>@string/settings_service_masquerade_none</item>
<item>@string/settings_service_masquerade_simple</item> <item>@string/settings_service_masquerade_simple</item>
<item>@string/settings_service_masquerade_netd</item>
</string-array> </string-array>
<string-array name="settings_service_masquerade_values" tools:ignore="InconsistentArrays"> <string-array name="settings_service_masquerade_values" tools:ignore="InconsistentArrays">
<item>None</item> <item>None</item>
<item>Simple</item> <item>Simple</item>
<item>Netd</item>
</string-array> </string-array>
<string-array name="settings_service_wifi_lock"> <string-array name="settings_service_wifi_lock">

View File

@@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<bool name="api_ge_30">false</bool> <bool name="api_ge_30">false</bool>
<bool name="api_ge_26">false</bool>
<bool name="api_lt_25">true</bool>
<bool name="is_day">true</bool> <bool name="is_day">true</bool>
</resources> </resources>

View File

@@ -69,7 +69,6 @@
if VPN tethering does not work.</string> if VPN tethering does not work.</string>
<string name="tethering_manage_usb">USB tethering</string> <string name="tethering_manage_usb">USB tethering</string>
<string name="tethering_manage_wifi">Wi\u2011Fi hotspot</string> <string name="tethering_manage_wifi">Wi\u2011Fi hotspot</string>
<string name="tethering_manage_wifi_legacy">Wi\u2011Fi hotspot (legacy)</string>
<string name="tethering_manage_bluetooth">Bluetooth tethering</string> <string name="tethering_manage_bluetooth">Bluetooth tethering</string>
<string name="tethering_manage_ethernet">Ethernet tethering</string> <string name="tethering_manage_ethernet">Ethernet tethering</string>
<string name="tethering_manage_ncm">USB tethering (NCM)</string> <string name="tethering_manage_ncm">USB tethering (NCM)</string>

View File

@@ -2,7 +2,7 @@
<style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar"> <style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar">
<item name="android:navigationBarColor">@color/navigationBarColor</item> <item name="android:navigationBarColor">@color/navigationBarColor</item>
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">@bool/is_day</item> <item name="android:windowLightNavigationBar">@bool/is_day</item>
<item name="actionBarStyle">@style/Widget.MaterialComponents.Light.ActionBar.Solid</item> <item name="actionBarStyle">@style/Widget.MaterialComponents.Light.ActionBar.Solid</item>
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>

View File

@@ -1,13 +0,0 @@
package be.mygod.vpnhotspot.net
import org.junit.Assert.*
import org.junit.Test
class MacAddressCompatTest {
@Test
fun macSerialization() {
for (test in listOf("01:23:45:67:89:ab", "DE:AD:88:88:BE:EF")) {
assertTrue(test.equals(MacAddressCompat.fromString(test).toString(), true))
}
}
}